#include "StarImage.hpp" #include "StarLogging.hpp" #include namespace Star { void logPngError(png_structp png_ptr, png_const_charp c) { Logger::debug("PNG error in file: '{}', {}", ((IODevice*)png_get_error_ptr(png_ptr))->deviceName(), c); }; void logPngWarning(png_structp png_ptr, png_const_charp c) { Logger::debug("PNG warning in file: '{}', {}", ((IODevice*)png_get_error_ptr(png_ptr))->deviceName(), c); }; void readPngData(png_structp pngPtr, png_bytep data, png_size_t length) { ((IODevice*)png_get_io_ptr(pngPtr))->readFull((char*)data, length); }; Image Image::readPng(IODevicePtr device) { png_byte header[8]; device->readFull((char*)header, sizeof(header)); if (png_sig_cmp(header, 0, sizeof(header))) throw ImageException(strf("File {} is not a png image!", device->deviceName())); png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!png_ptr) throw ImageException("Internal libPNG error"); // Use custom warning function to suppress cerr warnings png_set_error_fn(png_ptr, (png_voidp)device.get(), logPngError, logPngWarning); png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_read_struct(&png_ptr, nullptr, nullptr); throw ImageException("Internal libPNG error"); } png_infop end_info = png_create_info_struct(png_ptr); if (!end_info) { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); throw ImageException("Internal libPNG error"); } if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); throw ImageException("Internal error reading png."); } png_set_read_fn(png_ptr, device.get(), readPngData); // Tell libPNG that we read some of the header. png_set_sig_bytes(png_ptr, sizeof(header)); png_read_info(png_ptr, info_ptr); png_uint_32 img_width = png_get_image_width(png_ptr, info_ptr); png_uint_32 img_height = png_get_image_height(png_ptr, info_ptr); png_uint_32 bitdepth = png_get_bit_depth(png_ptr, info_ptr); png_uint_32 channels = png_get_channels(png_ptr, info_ptr); // Color type. (RGB, RGBA, Luminance, luminance alpha... palette... etc) png_uint_32 color_type = png_get_color_type(png_ptr, info_ptr); if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_palette_to_rgb(png_ptr); channels = 3; bitdepth = 8; } if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { if (bitdepth < 8) { png_set_expand_gray_1_2_4_to_8(png_ptr); bitdepth = 8; } png_set_gray_to_rgb(png_ptr); if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) channels = 4; else channels = 3; } // If the image has a transperancy set, convert it to a full alpha channel if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { png_set_tRNS_to_alpha(png_ptr); channels += 1; } // We don't support 16 bit precision.. so if the image Has 16 bits per channel // precision... round it down to 8. if (bitdepth == 16) { png_set_strip_16(png_ptr); bitdepth = 8; } if (bitdepth != 8 || (channels != 3 && channels != 4)) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); throw ImageException(strf("Unsupported PNG pixel format in file {}", device->deviceName())); } Image image(img_width, img_height, channels == 3 ? PixelFormat::RGB24 : PixelFormat::RGBA32); std::unique_ptr row_ptrs(new png_bytep[img_height]); size_t stride = img_width * channels; for (size_t i = 0; i < img_height; ++i) row_ptrs[i] = (png_bytep)image.data() + (img_height - i - 1) * stride; png_read_image(png_ptr, row_ptrs.get()); png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); return image; } tuple Image::readPngMetadata(IODevicePtr device) { png_byte header[8]; device->readFull((char*)header, sizeof(header)); if (png_sig_cmp(header, 0, sizeof(header))) throw ImageException(strf("File {} is not a png image!", device->deviceName())); png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!png_ptr) throw ImageException("Internal libPNG error"); // Use custom warning function to suppress cerr warnings png_set_error_fn(png_ptr, (png_voidp)device.get(), logPngError, logPngWarning); png_infop info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_read_struct(&png_ptr, nullptr, nullptr); throw ImageException("Internal libPNG error"); } png_infop end_info = png_create_info_struct(png_ptr); if (!end_info) { png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); throw ImageException("Internal libPNG error"); } if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_read_struct(&png_ptr, &info_ptr, &end_info); throw ImageException("Internal error reading png."); } png_set_read_fn(png_ptr, device.get(), readPngData); // Tell libPNG that we read some of the header. png_set_sig_bytes(png_ptr, sizeof(header)); png_read_info(png_ptr, info_ptr); png_uint_32 img_width = png_get_image_width(png_ptr, info_ptr); png_uint_32 img_height = png_get_image_height(png_ptr, info_ptr); png_uint_32 bitdepth = png_get_bit_depth(png_ptr, info_ptr); png_uint_32 channels = png_get_channels(png_ptr, info_ptr); // Color type. (RGB, RGBA, Luminance, luminance alpha... palette... etc) png_uint_32 color_type = png_get_color_type(png_ptr, info_ptr); if (color_type == PNG_COLOR_TYPE_PALETTE) { png_set_palette_to_rgb(png_ptr); channels = 3; bitdepth = 8; } if (color_type == PNG_COLOR_TYPE_GRAY || color_type == PNG_COLOR_TYPE_GRAY_ALPHA) { if (bitdepth < 8) { png_set_expand_gray_1_2_4_to_8(png_ptr); bitdepth = 8; } png_set_gray_to_rgb(png_ptr); if (color_type == PNG_COLOR_TYPE_GRAY_ALPHA) channels = 4; else channels = 3; } // If the image has a transperancy set, convert it to a full alpha channel if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) { png_set_tRNS_to_alpha(png_ptr); channels += 1; } Vec2U imageSize{img_width, img_height}; PixelFormat pixelFormat = channels == 3 ? PixelFormat::RGB24 : PixelFormat::RGBA32; return make_tuple(imageSize, pixelFormat); } Image Image::filled(Vec2U size, Vec4B color, PixelFormat pf) { Image image(size, pf); image.fill(color); return image; } Image::Image(PixelFormat pf) : m_data(nullptr), m_width(0), m_height(0), m_pixelFormat(pf) {} Image::Image(Vec2U size, PixelFormat pf) : Image(size[0], size[1], pf) {} Image::Image(unsigned width, unsigned height, PixelFormat pf) : Image(pf) { reset(width, height, pf); } Image::~Image() { if (m_data) Star::free(m_data); } Image::Image(Image const& image) : Image() { operator=(image); } Image::Image(Image&& image) : Image() { operator=(std::move(image)); } Image& Image::operator=(Image const& image) { reset(image.m_width, image.m_height, image.m_pixelFormat); memcpy(data(), image.data(), m_width * m_height * bytesPerPixel()); return *this; } Image& Image::operator=(Image&& image) { reset(0, 0, m_pixelFormat); m_data = take(image.m_data); m_width = take(image.m_width); m_height = take(image.m_height); m_pixelFormat = take(image.m_pixelFormat); return *this; } void Image::reset(Vec2U size, Maybe pf) { reset(size[0], size[1], pf); } void Image::reset(unsigned width, unsigned height, Maybe pf) { if (!pf) pf = m_pixelFormat; if (m_data && m_width == width && m_height == height && m_pixelFormat == *pf) return; size_t imageSize = width * height * Star::bytesPerPixel(*pf); if (imageSize == 0) { if (m_data) { Star::free(m_data); m_data = nullptr; } } else { uint8_t* newData = nullptr; if (!m_data) newData = (uint8_t*)Star::malloc(imageSize); else newData = (uint8_t*)Star::realloc(m_data, imageSize); if (!newData) throw MemoryException::format("Could not allocate memory for new Image size {}\n", imageSize); m_data = newData; memset(m_data, 0, imageSize); } m_pixelFormat = *pf; m_width = width; m_height = height; } void Image::fill(Vec3B const& c) { if (bitsPerPixel() == 24) { for (unsigned y = 0; y < m_height; ++y) for (unsigned x = 0; x < m_width; ++x) set24(x, y, c); } else { for (unsigned y = 0; y < m_height; ++y) for (unsigned x = 0; x < m_width; ++x) set32(x, y, Vec4B(c, 255)); } } void Image::fill(Vec4B const& c) { if (bitsPerPixel() == 24) { for (unsigned y = 0; y < m_height; ++y) for (unsigned x = 0; x < m_width; ++x) set24(x, y, c.vec3()); } else { for (unsigned y = 0; y < m_height; ++y) for (unsigned x = 0; x < m_width; ++x) set32(x, y, c); } } void Image::fillRect(Vec2U const& pos, Vec2U const& size, Vec3B const& c) { for (unsigned y = pos[1]; y < pos[1] + size[1] && y < m_height; ++y) for (unsigned x = pos[0]; x < pos[0] + size[0] && x < m_width; ++x) set(Vec2U(x, y), c); } void Image::fillRect(Vec2U const& pos, Vec2U const& size, Vec4B const& c) { for (unsigned y = pos[1]; y < pos[1] + size[1] && y < m_height; ++y) for (unsigned x = pos[0]; x < pos[0] + size[0] && x < m_width; ++x) set(Vec2U(x, y), c); } void Image::set(Vec2U const& pos, Vec4B const& c) { if (pos[0] >= m_width || pos[1] >= m_height) { throw ImageException(strf("{} out of range in Image::set", pos)); } else if (bytesPerPixel() == 4) { size_t offset = pos[1] * m_width * 4 + pos[0] * 4; m_data[offset] = c[0]; m_data[offset + 1] = c[1]; m_data[offset + 2] = c[2]; m_data[offset + 3] = c[3]; } else if (bytesPerPixel() == 3) { size_t offset = pos[1] * m_width * 3 + pos[0] * 3; m_data[offset] = c[0]; m_data[offset + 1] = c[1]; m_data[offset + 2] = c[2]; } } void Image::set(Vec2U const& pos, Vec3B const& c) { if (pos[0] >= m_width || pos[1] >= m_height) { throw ImageException(strf("{} out of range in Image::set", pos)); } else if (bytesPerPixel() == 4) { size_t offset = pos[1] * m_width * 4 + pos[0] * 4; m_data[offset] = c[0]; m_data[offset + 1] = c[1]; m_data[offset + 2] = c[2]; m_data[offset + 3] = 255; } else if (bytesPerPixel() == 3) { size_t offset = pos[1] * m_width * 3 + pos[0] * 3; m_data[offset] = c[0]; m_data[offset + 1] = c[1]; m_data[offset + 2] = c[2]; } } Vec4B Image::get(Vec2U const& pos) const { Vec4B c; if (pos[0] >= m_width || pos[1] >= m_height) { throw ImageException(strf("{} out of range in Image::get", pos)); } else if (bytesPerPixel() == 4) { size_t offset = pos[1] * m_width * 4 + pos[0] * 4; c[0] = m_data[offset]; c[1] = m_data[offset + 1]; c[2] = m_data[offset + 2]; c[3] = m_data[offset + 3]; } else if (bytesPerPixel() == 3) { size_t offset = pos[1] * m_width * 3 + pos[0] * 3; c[0] = m_data[offset]; c[1] = m_data[offset + 1]; c[2] = m_data[offset + 2]; c[3] = 255; } return c; } void Image::setrgb(Vec2U const& pos, Vec4B const& c) { if (m_pixelFormat == PixelFormat::BGR24 || m_pixelFormat == PixelFormat::BGRA32) set(pos, Vec4B{c[2], c[1], c[0], c[3]}); else set(pos, c); } void Image::setrgb(Vec2U const& pos, Vec3B const& c) { if (m_pixelFormat == PixelFormat::BGR24 || m_pixelFormat == PixelFormat::BGRA32) set(pos, Vec3B{c[2], c[1], c[0]}); else set(pos, c); } Vec4B Image::getrgb(Vec2U const& pos) const { auto c = get(pos); if (m_pixelFormat == PixelFormat::BGR24 || m_pixelFormat == PixelFormat::BGRA32) return Vec4B{c[2], c[1], c[0], c[3]}; else return c; } Vec4B Image::clamp(Vec2I const& pos) const { Vec4B c; unsigned x = (unsigned)Star::clamp(pos[0], 0, m_width - 1); unsigned y = (unsigned)Star::clamp(pos[1], 0, m_height - 1); if (m_width == 0 || m_height == 0) { return {0, 0, 0, 0}; } else if (bytesPerPixel() == 4) { size_t offset = y * m_width * 4 + x * 4; c[0] = m_data[offset]; c[1] = m_data[offset + 1]; c[2] = m_data[offset + 2]; c[3] = m_data[offset + 3]; } else if (bytesPerPixel() == 3) { size_t offset = y * m_width * 3 + x * 3; c[0] = m_data[offset]; c[1] = m_data[offset + 1]; c[2] = m_data[offset + 2]; c[3] = 255; } return c; } Vec4B Image::clamprgb(Vec2I const& pos) const { auto c = clamp(pos); if (m_pixelFormat == PixelFormat::BGR24 || m_pixelFormat == PixelFormat::BGRA32) return Vec4B{c[2], c[1], c[0], c[3]}; else return c; } Image Image::subImage(Vec2U const& pos, Vec2U const& size) const { if (pos[0] + size[0] > m_width || pos[1] + size[1] > m_height) throw ImageException(strf("call to subImage with pos {} size {} out of image bounds ({}, {})", pos, size, m_width, m_height)); Image sub(size[0], size[1], m_pixelFormat); for (unsigned y = 0; y < size[1]; ++y) { for (unsigned x = 0; x < size[0]; ++x) { sub.set({x, y}, get(pos + Vec2U(x, y))); } } return sub; } void Image::copyInto(Vec2U const& min, Image const& image) { Vec2U max = (min + image.size()).piecewiseMin(size()); for (unsigned y = min[1]; y < max[1]; ++y) { for (unsigned x = min[0]; x < max[0]; ++x) set(x, y, image.get(Vec2U(x, y) - min)); } } void Image::drawInto(Vec2U const& min, Image const& image) { Vec2U max = (min + image.size()).piecewiseMin(size()); for (unsigned y = min[1]; y < max[1]; ++y) { for (unsigned x = min[0]; x < max[0]; ++x) { Vec4B dest = get(Vec2U(x, y)); Vec4B src = image.get(Vec2U(x, y) - min); Vec3U destMultiplied = Vec3U(dest[0], dest[1], dest[2]) * dest[3] / 255; Vec3U srcMultiplied = Vec3U(src[0], src[1], src[2]) * src[3] / 255; // Src over dest alpha composition Vec3U over = srcMultiplied + destMultiplied * (255 - src[3]) / 255; unsigned alpha = src[3] + dest[3] * (255 - src[3]) / 255; set(x, y, Vec4B(over[0], over[1], over[2], alpha)); } } } Image Image::convert(PixelFormat pixelFormat) const { Image converted(m_width, m_height, pixelFormat); converted.copyInto(Vec2U(), *this); return converted; } void Image::writePng(IODevicePtr device) const { auto writePngData = [](png_structp pngPtr, png_bytep data, png_size_t length) { IODevice* device = (IODevice*)png_get_io_ptr(pngPtr); device->writeFull((char*)data, length); }; auto flushPngData = [](png_structp) {}; png_structp png_ptr = nullptr; png_infop info_ptr = nullptr; png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); if (!png_ptr) throw ImageException("Internal libPNG error"); info_ptr = png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_write_struct(&png_ptr, nullptr); throw ImageException("Internal libPNG error"); } if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_write_struct(&png_ptr, &info_ptr); throw ImageException("Internal error reading png."); } unsigned channels = m_pixelFormat == PixelFormat::RGB24 ? 3 : 4; png_set_IHDR(png_ptr, info_ptr, m_width, m_height, 8, channels == 3 ? PNG_COLOR_TYPE_RGB : PNG_COLOR_TYPE_RGBA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); unique_ptr row_ptrs(new png_bytep[m_height]); size_t stride = m_width * 8 * channels / 8; for (size_t i = 0; i < m_height; ++i) { size_t q = (m_height - i - 1) * stride; row_ptrs[i] = (png_bytep)m_data + q; } png_set_write_fn(png_ptr, device.get(), writePngData, flushPngData); png_set_rows(png_ptr, info_ptr, row_ptrs.get()); png_write_png(png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, nullptr); png_destroy_write_struct(&png_ptr, &info_ptr); } }