osb/source/core/StarImage.cpp
Kai Blaschke 42fc1d6714
Fixed a memory leak in Image::readPngMetadata()
The memory allocated by png_create_read_struct() was not freed before exiting the function, wasting lots of memory over time.
2024-02-19 20:47:58 +01:00

522 lines
16 KiB
C++

#include "StarImage.hpp"
#include "StarLogging.hpp"
#include <png.h>
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<png_bytep[]> 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<Vec2U, PixelFormat> 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;
}
png_destroy_read_struct(&png_ptr, &info_ptr, &end_info);
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=(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<PixelFormat> pf) {
reset(size[0], size[1], pf);
}
void Image::reset(unsigned width, unsigned height, Maybe<PixelFormat> 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<int>(pos[0], 0, m_width - 1);
unsigned y = (unsigned)Star::clamp<int>(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<png_bytep[]> 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);
}
}