From 9c46b751381385eeb37e907bf748cc25ed9fb20e Mon Sep 17 00:00:00 2001 From: slime Date: Sat, 10 Sep 2022 23:17:42 -0300 Subject: [PATCH] ImageData:encode now supports saving exr image files. Also add support for loading uint exr image files, and fix a memory leak in the exr loading code. Resolves #1813. --- src/modules/image/FormatHandler.cpp | 5 + src/modules/image/FormatHandler.h | 6 + src/modules/image/ImageData.cpp | 5 +- src/modules/image/magpie/EXRHandler.cpp | 192 +++++++++++++++++++++++- src/modules/image/magpie/EXRHandler.h | 13 +- src/modules/image/magpie/PNGHandler.cpp | 6 + src/modules/image/magpie/PNGHandler.h | 13 +- src/modules/image/magpie/STBHandler.cpp | 20 ++- src/modules/image/magpie/STBHandler.h | 3 + 9 files changed, 236 insertions(+), 27 deletions(-) diff --git a/src/modules/image/FormatHandler.cpp b/src/modules/image/FormatHandler.cpp index 9b4fc0050..f032a6c51 100644 --- a/src/modules/image/FormatHandler.cpp +++ b/src/modules/image/FormatHandler.cpp @@ -70,5 +70,10 @@ void FormatHandler::freeRawPixels(unsigned char *mem) delete[] mem; } +void FormatHandler::freeEncodedImage(unsigned char *mem) +{ + delete[] mem; +} + } // image } // love diff --git a/src/modules/image/FormatHandler.h b/src/modules/image/FormatHandler.h index 2f3f63180..a15def9b7 100644 --- a/src/modules/image/FormatHandler.h +++ b/src/modules/image/FormatHandler.h @@ -43,6 +43,7 @@ public: { ENCODED_TGA, ENCODED_PNG, + ENCODED_EXR, ENCODED_MAX_ENUM }; @@ -116,6 +117,11 @@ public: **/ virtual void freeRawPixels(unsigned char *mem); + /** + * Frees encoded image memory allocated by the format handler. + **/ + virtual void freeEncodedImage(unsigned char *mem); + }; // FormatHandler } // image diff --git a/src/modules/image/ImageData.cpp b/src/modules/image/ImageData.cpp index 2a5be2819..ca07ea299 100644 --- a/src/modules/image/ImageData.cpp +++ b/src/modules/image/ImageData.cpp @@ -206,12 +206,12 @@ love::filesystem::FileData *ImageData::encode(FormatHandler::EncodedFormat encod } catch (love::Exception &) { - encoder->freeRawPixels(encodedimage.data); + encoder->freeEncodedImage(encodedimage.data); throw; } memcpy(filedata->getData(), encodedimage.data, encodedimage.size); - encoder->freeRawPixels(encodedimage.data); + encoder->freeEncodedImage(encodedimage.data); if (writefile) { @@ -859,6 +859,7 @@ StringMap::Entry { {"tga", FormatHandler::ENCODED_TGA}, {"png", FormatHandler::ENCODED_PNG}, + {"exr", FormatHandler::ENCODED_EXR}, }; StringMap ImageData::encodedFormats(ImageData::encodedFormatEntries, sizeof(ImageData::encodedFormatEntries)); diff --git a/src/modules/image/magpie/EXRHandler.cpp b/src/modules/image/magpie/EXRHandler.cpp index 41a76b223..a5a40e205 100644 --- a/src/modules/image/magpie/EXRHandler.cpp +++ b/src/modules/image/magpie/EXRHandler.cpp @@ -47,8 +47,27 @@ bool EXRHandler::canDecode(Data *data) return ParseEXRVersionFromMemory(&version, (const unsigned char *) data->getData(), data->getSize()) == TINYEXR_SUCCESS; } -bool EXRHandler::canEncode(PixelFormat /*rawFormat*/, EncodedFormat /*encodedFormat*/) +bool EXRHandler::canEncode(PixelFormat rawFormat, EncodedFormat encodedFormat) { + if (encodedFormat != ENCODED_EXR) + return false; + + switch (rawFormat) + { + case PIXELFORMAT_R16_FLOAT: + case PIXELFORMAT_R32_FLOAT: + case PIXELFORMAT_R32_UINT: + case PIXELFORMAT_RG16_FLOAT: + case PIXELFORMAT_RG32_FLOAT: + case PIXELFORMAT_RG32_UINT: + case PIXELFORMAT_RGBA16_FLOAT: + case PIXELFORMAT_RGBA32_FLOAT: + case PIXELFORMAT_RGBA32_UINT: + return true; + default: + return false; + } + return false; } @@ -76,7 +95,7 @@ static void getEXRChannels(const EXRHeader &header, const EXRImage &image, T *rg } template -static T *loadEXRChannels(int width, int height, T *rgba[4], T one) +static T *readEXRChannels(int width, int height, T *rgba[4], T one) { T *data = nullptr; @@ -105,6 +124,20 @@ static T *loadEXRChannels(int width, int height, T *rgba[4], T one) return data; } +template +static void writeEXRChannels(int width, int height, int components, const T *pixels, T *rgba[4]) +{ + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + size_t offset = y * width + x; + for (int c = 0; c < components; c++) + rgba[c][offset] = pixels[offset * components + c]; + } + } +} + FormatHandler::DecodedImage EXRHandler::decode(Data *data) { const char *err = "unknown error"; @@ -131,7 +164,10 @@ FormatHandler::DecodedImage EXRHandler::decode(Data *data) throw love::Exception("Could not parse EXR image header: %s", err); if (LoadEXRImageFromMemory(&exrImage, &exrHeader, mem, memsize, &err) != TINYEXR_SUCCESS) + { + FreeEXRHeader(&exrHeader); throw love::Exception("Could not decode EXR image: %s", err); + } } catch (love::Exception &) { @@ -145,6 +181,7 @@ FormatHandler::DecodedImage EXRHandler::decode(Data *data) { if (pixelType != exrHeader.pixel_types[i]) { + FreeEXRHeader(&exrHeader); FreeEXRImage(&exrImage); throw love::Exception("Could not decode EXR image: all channels must have the same data type."); } @@ -153,7 +190,25 @@ FormatHandler::DecodedImage EXRHandler::decode(Data *data) img.width = exrImage.width; img.height = exrImage.height; - if (pixelType == TINYEXR_PIXELTYPE_HALF) + if (pixelType == TINYEXR_PIXELTYPE_UINT) + { + img.format = PIXELFORMAT_RGBA32_UINT; + + uint32 *rgba[4] = {nullptr}; + getEXRChannels(exrHeader, exrImage, rgba); + + try + { + img.data = (unsigned char *) readEXRChannels(img.width, img.height, rgba, 1u); + } + catch (love::Exception &) + { + FreeEXRHeader(&exrHeader); + FreeEXRImage(&exrImage); + throw; + } + } + else if (pixelType == TINYEXR_PIXELTYPE_HALF) { img.format = PIXELFORMAT_RGBA16_FLOAT; @@ -162,10 +217,11 @@ FormatHandler::DecodedImage EXRHandler::decode(Data *data) try { - img.data = (unsigned char *) loadEXRChannels(img.width, img.height, rgba, float32to16(1.0f)); + img.data = (unsigned char *) readEXRChannels(img.width, img.height, rgba, float32to16(1.0f)); } catch (love::Exception &) { + FreeEXRHeader(&exrHeader); FreeEXRImage(&exrImage); throw; } @@ -179,30 +235,145 @@ FormatHandler::DecodedImage EXRHandler::decode(Data *data) try { - img.data = (unsigned char *) loadEXRChannels(img.width, img.height, rgba, 1.0f); + img.data = (unsigned char *) readEXRChannels(img.width, img.height, rgba, 1.0f); } catch (love::Exception &) { + FreeEXRHeader(&exrHeader); FreeEXRImage(&exrImage); throw; } } else { + FreeEXRHeader(&exrHeader); FreeEXRImage(&exrImage); throw love::Exception("Could not decode EXR image: unknown pixel format."); } img.size = getPixelFormatSliceSize(img.format, img.width, img.height); + FreeEXRHeader(&exrHeader); FreeEXRImage(&exrImage); return img; } -FormatHandler::EncodedImage EXRHandler::encode(const DecodedImage & /*img*/, EncodedFormat /*encodedFormat*/) +FormatHandler::EncodedImage EXRHandler::encode(const DecodedImage &img, EncodedFormat encodedFormat) { - throw love::Exception("Invalid format."); + if (!canEncode(img.format, encodedFormat)) + { + if (encodedFormat != ENCODED_EXR) + throw love::Exception("EXR encoder cannot encode to non-EXR format."); + else + throw love::Exception("EXR encoder cannot encode the given pixel format."); + } + + const auto &formatinfo = getPixelFormatInfo(img.format); + + EXRHeader exrHeader; + InitEXRHeader(&exrHeader); + + exrHeader.num_channels = formatinfo.components; + + // TODO: this could be configurable. + exrHeader.compression_type = TINYEXR_COMPRESSIONTYPE_ZIP; + + // TinyEXR expects malloc here because FreeEXRHeader uses free(). + exrHeader.channels = (EXRChannelInfo *) malloc(sizeof(EXRChannelInfo) * exrHeader.num_channels); + exrHeader.pixel_types = (int *) malloc(sizeof(int) * exrHeader.num_channels); + exrHeader.requested_pixel_types = (int *) malloc(sizeof(int) * exrHeader.num_channels); + + int pixeltype = -1; + if (formatinfo.dataType == PIXELFORMATTYPE_UINT) + { + pixeltype = TINYEXR_PIXELTYPE_UINT; + } + else if (formatinfo.dataType == PIXELFORMATTYPE_SFLOAT) + { + if (formatinfo.blockSize / formatinfo.components == 2) + pixeltype = TINYEXR_PIXELTYPE_HALF; + else if (formatinfo.blockSize / formatinfo.components == 4) + pixeltype = TINYEXR_PIXELTYPE_FLOAT; + } + + if (pixeltype == -1) + { + FreeEXRHeader(&exrHeader); + throw love::Exception("Cannot convert the given pixel format to an EXR pixel type."); + } + + for (int i = 0; i < exrHeader.num_channels; i++) + { + exrHeader.channels[i] = EXRChannelInfo(); + + const char names[] = {'R', 'G', 'B', 'A'}; + exrHeader.channels[i].name[0] = names[i]; + + exrHeader.pixel_types[i] = pixeltype; + exrHeader.requested_pixel_types[i] = pixeltype; + } + + EXRImage exrImage; + InitEXRImage(&exrImage); + + exrImage.width = img.width; + exrImage.height = img.height; + exrImage.num_channels = exrHeader.num_channels; + + exrImage.images = (unsigned char **) malloc(sizeof(unsigned char *) * exrImage.num_channels); + + for (int i = 0; i < exrImage.num_channels; i++) + { + size_t componentsize = pixeltype == TINYEXR_PIXELTYPE_HALF ? 2 : 4; + + exrImage.images[i] = (unsigned char *) malloc(img.width * img.height * componentsize); + + if (exrImage.images[i] == nullptr) + { + FreeEXRHeader(&exrHeader); + FreeEXRImage(&exrImage); + throw love::Exception("Out of memory."); + } + } + + if (pixeltype == TINYEXR_PIXELTYPE_UINT) + { + writeEXRChannels(img.width, img.height, formatinfo.components, (const uint32 *) img.data, (uint32 **) exrImage.images); + } + else if (pixeltype == TINYEXR_PIXELTYPE_HALF) + { + writeEXRChannels(img.width, img.height, formatinfo.components, (const float16 *) img.data, (float16 **) exrImage.images); + } + else if (pixeltype == TINYEXR_PIXELTYPE_FLOAT) + { + writeEXRChannels(img.width, img.height, formatinfo.components, (const float *) img.data, (float **) exrImage.images); + } + + EncodedImage encimg; + + const char *err = nullptr; + encimg.size = SaveEXRImageToMemory(&exrImage, &exrHeader, &encimg.data, &err); + + FreeEXRHeader(&exrHeader); + FreeEXRImage(&exrImage); + + std::string errstring; + if (err != nullptr) + { + errstring = err; + FreeEXRErrorMessage(err); + } + + if (encimg.size == 0) + { + if (!errstring.empty()) + throw love::Exception("Could not encode EXR image: %s", errstring.c_str()); + else + throw love::Exception("Could not encode EXR image"); + } + + return encimg; } void EXRHandler::freeRawPixels(unsigned char *mem) @@ -210,6 +381,13 @@ void EXRHandler::freeRawPixels(unsigned char *mem) delete[] mem; } +void EXRHandler::freeEncodedImage(unsigned char *mem) +{ + // SaveEXRImageToMemory uses malloc. + if (mem != nullptr) + ::free(mem); +} + } // magpie } // image } // love diff --git a/src/modules/image/magpie/EXRHandler.h b/src/modules/image/magpie/EXRHandler.h index 2882ed14f..587619057 100644 --- a/src/modules/image/magpie/EXRHandler.h +++ b/src/modules/image/magpie/EXRHandler.h @@ -36,15 +36,18 @@ class EXRHandler : public FormatHandler { public: + virtual ~EXRHandler() {} + // Implements FormatHandler. - virtual bool canDecode(Data *data); - virtual bool canEncode(PixelFormat rawFormat, EncodedFormat encodedFormat); + bool canDecode(Data *data) override; + bool canEncode(PixelFormat rawFormat, EncodedFormat encodedFormat) override; - virtual DecodedImage decode(Data *data); - virtual EncodedImage encode(const DecodedImage &img, EncodedFormat format); + DecodedImage decode(Data *data) override; + EncodedImage encode(const DecodedImage &img, EncodedFormat format) override; - virtual void freeRawPixels(unsigned char *mem); + void freeRawPixels(unsigned char *mem) override; + void freeEncodedImage(unsigned char *mem) override; }; // EXRHandler diff --git a/src/modules/image/magpie/PNGHandler.cpp b/src/modules/image/magpie/PNGHandler.cpp index 74dbc2e94..d993777be 100644 --- a/src/modules/image/magpie/PNGHandler.cpp +++ b/src/modules/image/magpie/PNGHandler.cpp @@ -264,6 +264,12 @@ void PNGHandler::freeRawPixels(unsigned char *mem) ::free(mem); } +void PNGHandler::freeEncodedImage(unsigned char *mem) +{ + if (mem) + ::free(mem); +} + } // magpie } // image } // love diff --git a/src/modules/image/magpie/PNGHandler.h b/src/modules/image/magpie/PNGHandler.h index 88e94d1c8..76fd5c46d 100644 --- a/src/modules/image/magpie/PNGHandler.h +++ b/src/modules/image/magpie/PNGHandler.h @@ -37,15 +37,18 @@ class PNGHandler : public FormatHandler { public: + virtual ~PNGHandler() {} + // Implements FormatHandler. - virtual bool canDecode(Data *data); - virtual bool canEncode(PixelFormat rawFormat, EncodedFormat encodedFormat); + bool canDecode(Data *data) override; + bool canEncode(PixelFormat rawFormat, EncodedFormat encodedFormat) override; - virtual DecodedImage decode(Data *data); - virtual EncodedImage encode(const DecodedImage &img, EncodedFormat format); + DecodedImage decode(Data *data) override; + EncodedImage encode(const DecodedImage &img, EncodedFormat format) override; - virtual void freeRawPixels(unsigned char *mem); + void freeRawPixels(unsigned char *mem) override; + void freeEncodedImage(unsigned char *mem) override; }; // PNGHandler diff --git a/src/modules/image/magpie/STBHandler.cpp b/src/modules/image/magpie/STBHandler.cpp index 0b4234f4c..edc619965 100644 --- a/src/modules/image/magpie/STBHandler.cpp +++ b/src/modules/image/magpie/STBHandler.cpp @@ -123,15 +123,14 @@ FormatHandler::EncodedImage STBHandler::encode(const DecodedImage &img, EncodedF encimg.size = (img.width * img.height * bpp) + headerlen; - // We need to use malloc because we use stb_image_free (which uses free()) - // as our custom free() function, which is called by the ImageData after - // encode() is complete. - // stb_image's source code is compiled with this source, so calling malloc() - // directly is fine. - encimg.data = (unsigned char *) malloc(encimg.size); - - if (encimg.data == nullptr) + try + { + encimg.data = new unsigned char[encimg.size]; + } + catch (std::exception &) + { throw love::Exception("Out of memory."); + } // here's the header for the Targa file format. encimg.data[0] = 0; // ID field size @@ -176,6 +175,11 @@ void STBHandler::freeRawPixels(unsigned char *mem) stbi_image_free(mem); } +void STBHandler::freeEncodedImage(unsigned char *mem) +{ + delete[] mem; +} + } // magpie } // image } // love diff --git a/src/modules/image/magpie/STBHandler.h b/src/modules/image/magpie/STBHandler.h index 301f0a19d..65dd6df0a 100644 --- a/src/modules/image/magpie/STBHandler.h +++ b/src/modules/image/magpie/STBHandler.h @@ -40,6 +40,8 @@ class STBHandler final : public FormatHandler { public: + virtual ~STBHandler() {} + // Implements FormatHandler. bool canDecode(Data *data) override; @@ -49,6 +51,7 @@ public: EncodedImage encode(const DecodedImage &img, EncodedFormat format) override; void freeRawPixels(unsigned char *mem) override; + void freeEncodedImage(unsigned char *mem) override; }; // STBHandler