From 7e151f16c3faf4df58e4469d4a22426115e6b79a Mon Sep 17 00:00:00 2001 From: Rangi <35663410+Rangi42@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:53:33 -0400 Subject: [PATCH] Factor out a single PNG-reading function to encapsulate the libpng API (#1765) --- Makefile | 3 + include/gfx/pal_sorting.hpp | 9 +- include/gfx/png.hpp | 21 ++ include/gfx/rgba.hpp | 16 +- src/CMakeLists.txt | 1 + src/asm/output.cpp | 2 +- src/gfx/pal_sorting.cpp | 19 +- src/gfx/pal_spec.cpp | 182 ++----------- src/gfx/png.cpp | 216 +++++++++++++++ src/gfx/process.cpp | 462 ++++++++------------------------ test/gfx/damaged1.err | 2 +- test/gfx/damaged2.err | 2 +- test/gfx/damaged9.err | 2 +- test/gfx/interlaced.flags | 1 + test/gfx/interlaced.out.2bpp | Bin 0 -> 64 bytes test/gfx/interlaced.out.attrmap | Bin 0 -> 16 bytes test/gfx/interlaced.out.pal | Bin 0 -> 64 bytes test/gfx/interlaced.out.tilemap | Bin 0 -> 16 bytes test/gfx/interlaced.png | Bin 0 -> 400 bytes 19 files changed, 395 insertions(+), 543 deletions(-) create mode 100644 include/gfx/png.hpp create mode 100644 src/gfx/png.cpp create mode 100644 test/gfx/interlaced.flags create mode 100644 test/gfx/interlaced.out.2bpp create mode 100644 test/gfx/interlaced.out.attrmap create mode 100644 test/gfx/interlaced.out.pal create mode 100644 test/gfx/interlaced.out.tilemap create mode 100644 test/gfx/interlaced.png diff --git a/Makefile b/Makefile index 8201e1f1..6aa93e31 100644 --- a/Makefile +++ b/Makefile @@ -105,6 +105,7 @@ rgbgfx_obj := \ src/gfx/pal_packing.o \ src/gfx/pal_sorting.o \ src/gfx/pal_spec.o \ + src/gfx/png.o \ src/gfx/process.o \ src/gfx/proto_palette.o \ src/gfx/reverse.o \ @@ -152,6 +153,8 @@ src/gfx/pal_sorting.o: src/gfx/pal_sorting.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/pal_spec.o: src/gfx/pal_spec.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< +src/gfx/png.o: src/gfx/png.cpp + $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/process.o: src/gfx/process.cpp $Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $< src/gfx/proto_palette.o: src/gfx/proto_palette.cpp diff --git a/include/gfx/pal_sorting.hpp b/include/gfx/pal_sorting.hpp index a068efcc..19f2b268 100644 --- a/include/gfx/pal_sorting.hpp +++ b/include/gfx/pal_sorting.hpp @@ -5,7 +5,6 @@ #include #include -#include #include #include "gfx/rgba.hpp" @@ -16,13 +15,7 @@ static constexpr size_t NB_COLOR_SLOTS = (1 << (5 * 3)) + 1; struct Palette; -void sortIndexed( - std::vector &palettes, - int palSize, - png_color const *palRGB, - int palAlphaSize, - png_byte *palAlpha -); +void sortIndexed(std::vector &palettes, std::vector const &embPal); void sortGrayscale( std::vector &palettes, std::array, NB_COLOR_SLOTS> const &colors ); diff --git a/include/gfx/png.hpp b/include/gfx/png.hpp new file mode 100644 index 00000000..18b43484 --- /dev/null +++ b/include/gfx/png.hpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MIT + +#ifndef RGBDS_GFX_PNG_HPP +#define RGBDS_GFX_PNG_HPP + +#include +#include +#include + +#include "gfx/rgba.hpp" + +struct Png { + uint32_t width, height; + std::vector pixels{}; + std::vector palette{}; + + Png() {} + Png(char const *filename, std::streambuf &file); +}; + +#endif // RGBDS_GFX_PNG_HPP diff --git a/include/gfx/rgba.hpp b/include/gfx/rgba.hpp index 859564dc..b86968b9 100644 --- a/include/gfx/rgba.hpp +++ b/include/gfx/rgba.hpp @@ -17,16 +17,16 @@ struct Rgba { explicit constexpr Rgba(uint32_t rgba = 0) : red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {} - static constexpr Rgba fromCGBColor(uint16_t cgbColor) { - constexpr auto _5to8 = [](uint8_t fiveBpp) -> uint8_t { - fiveBpp &= 0b11111; // For caller's convenience - return fiveBpp << 3 | fiveBpp >> 2; + static constexpr Rgba fromCGBColor(uint16_t color) { + constexpr auto _5to8 = [](uint8_t channel) -> uint8_t { + channel &= 0b11111; // For caller's convenience + return channel << 3 | channel >> 2; }; return { - _5to8(cgbColor), - _5to8(cgbColor >> 5), - _5to8(cgbColor >> 10), - static_cast(cgbColor & 0x8000 ? 0x00 : 0xFF), + _5to8(color), + _5to8(color >> 5), + _5to8(color >> 10), + static_cast(color & 0x8000 ? 0x00 : 0xFF), }; } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ec1602d2..22cd030f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -79,6 +79,7 @@ set(rgbgfx_src "gfx/pal_packing.cpp" "gfx/pal_sorting.cpp" "gfx/pal_spec.cpp" + "gfx/png.cpp" "gfx/process.cpp" "gfx/proto_palette.cpp" "gfx/reverse.cpp" diff --git a/src/asm/output.cpp b/src/asm/output.cpp index 754c0f2a..38c3f489 100644 --- a/src/asm/output.cpp +++ b/src/asm/output.cpp @@ -149,7 +149,7 @@ static void writeRpn(std::vector &rpnexpr, std::vector const & for (size_t offset = 0; offset < rpn.size();) { uint8_t rpndata = rpn[offset++]; - auto getSymName = [&](){ + auto getSymName = [&]() { std::string symName; for (uint8_t c; (c = rpn[offset++]) != 0;) { symName += c; diff --git a/src/gfx/pal_sorting.cpp b/src/gfx/pal_sorting.cpp index 8c9112e3..08878777 100644 --- a/src/gfx/pal_sorting.cpp +++ b/src/gfx/pal_sorting.cpp @@ -8,27 +8,14 @@ #include "gfx/main.hpp" -void sortIndexed( - std::vector &palettes, - int palSize, - png_color const *palRGB, - int palAlphaSize, - png_byte *palAlpha -) { +void sortIndexed(std::vector &palettes, std::vector const &embPal) { options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palettes using embedded palette...\n"); - auto pngToRgb = [&palRGB, &palAlphaSize, &palAlpha](int index) { - png_color const &c = palRGB[index]; - return Rgba( - c.red, c.green, c.blue, palAlpha && index < palAlphaSize ? palAlpha[index] : 0xFF - ); - }; - for (Palette &pal : palettes) { std::sort(RANGE(pal), [&](uint16_t lhs, uint16_t rhs) { // Iterate through the PNG's palette, looking for either of the two - for (int i = 0; i < palSize; ++i) { - uint16_t color = pngToRgb(i).cgbColor(); + for (Rgba const &rgba : embPal) { + uint16_t color = rgba.cgbColor(); if (color == Rgba::transparent) { continue; } diff --git a/src/gfx/pal_spec.cpp b/src/gfx/pal_spec.cpp index d3b65183..fa653cdc 100644 --- a/src/gfx/pal_spec.cpp +++ b/src/gfx/pal_spec.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -23,6 +22,7 @@ #include "platform.hpp" #include "gfx/main.hpp" +#include "gfx/png.hpp" #include "gfx/warning.hpp" using namespace std::string_view_literals; @@ -265,7 +265,7 @@ static std::optional parseColor(std::string const &str, size_t &n, uint16_ return std::optional{Rgba(*r, *g, *b, 0xFF)}; } -static void parsePSPFile(std::filebuf &file) { +static void parsePSPFile(char const *, std::filebuf &file) { // https://www.selapa.net/swatches/colors/fileformats.php#psp_pal std::string line; @@ -328,7 +328,7 @@ static void parsePSPFile(std::filebuf &file) { } } -static void parseGPLFile(std::filebuf &file) { +static void parseGPLFile(char const *, std::filebuf &file) { // https://gitlab.gnome.org/GNOME/gimp/-/blob/gimp-2-10/app/core/gimppalette-load.c#L39 std::string line; @@ -383,7 +383,7 @@ static void parseGPLFile(std::filebuf &file) { } } -static void parseHEXFile(std::filebuf &file) { +static void parseHEXFile(char const *, std::filebuf &file) { // https://lospec.com/palette-list/tag/gbc uint16_t nbColors = 0; @@ -430,7 +430,7 @@ static void parseHEXFile(std::filebuf &file) { } } -static void parseACTFile(std::filebuf &file) { +static void parseACTFile(char const *, std::filebuf &file) { // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626 std::array buf{}; @@ -482,7 +482,7 @@ static void parseACTFile(std::filebuf &file) { } } -static void parseACOFile(std::filebuf &file) { +static void parseACOFile(char const *, std::filebuf &file) { // https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819 char buf[10]; @@ -551,7 +551,7 @@ static void parseACOFile(std::filebuf &file) { } } -static void parseGBCFile(std::filebuf &file) { +static void parseGBCFile(char const *, std::filebuf &file) { // This only needs to be able to read back files generated by `rgbgfx -p` options.palSpec.clear(); @@ -579,41 +579,15 @@ static void parseGBCFile(std::filebuf &file) { } } -[[noreturn]] -static void handlePngError(png_structp, char const *msg) { - fatal("Error reading palette file: %s", msg); -} - -static void handlePngWarning(png_structp, char const *msg) { - warnx("In palette file: %s", msg); -} - -static void readPngData(png_structp png, png_bytep data, size_t length) { - std::filebuf *file = reinterpret_cast(png_get_io_ptr(png)); - std::streamsize expectedLen = length; - std::streamsize nbBytesRead = file->sgetn(reinterpret_cast(data), expectedLen); - - if (nbBytesRead != expectedLen) { - fatal( - "Error reading palette file: file too short (expected at least %zd more bytes after " - "reading %zu)", - length - nbBytesRead, - static_cast(file->pubseekoff(0, std::ios_base::cur)) - ); - } -} - -static bool checkPngSwatch(std::vector const &image, uint32_t base, uint32_t swatchSize) { - Rgba topLeft(image[base], image[base + 1], image[base + 2], image[base + 3]); - uint32_t rowFactor = swatchSize * options.nbColorsPerPal; +static bool checkPngSwatch(std::vector const &pixels, uint32_t base, uint32_t swatchSize) { for (uint32_t y = 0; y < swatchSize; y++) { + uint32_t yOffset = y * swatchSize * options.nbColorsPerPal + base; + for (uint32_t x = 0; x < swatchSize; x++) { if (x == 0 && y == 0) { continue; } - uint32_t offset = base + (y * rowFactor + x) * 4; - Rgba pixel(image[offset], image[offset + 1], image[offset + 2], image[offset + 3]); - if (pixel != topLeft) { + if (pixels[yOffset + x] != pixels[base]) { return false; } } @@ -621,135 +595,38 @@ static bool checkPngSwatch(std::vector const &image, uint32_t base, ui return true; } -static void parsePNGFile(std::filebuf &file) { - std::array pngHeader; - if (file.sgetn(reinterpret_cast(pngHeader.data()), pngHeader.size()) - != static_cast(pngHeader.size()) // Not enough bytes? - || png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) { - // LCOV_EXCL_START - error("Palette file does not appear to be a PNG palette file"); - return; - // LCOV_EXCL_STOP - } - - png_structp png = - png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, handlePngError, handlePngWarning); - if (!png) { - // LCOV_EXCL_START - error("Failed to create PNG read structure: %s", strerror(errno)); - return; - // LCOV_EXCL_STOP - } - - png_infop info = png_create_info_struct(png); - Defer destroyPng{[&] { png_destroy_read_struct(&png, &info, nullptr); }}; - if (!info) { - // LCOV_EXCL_START - error("Failed to create PNG info structure: %s", strerror(errno)); - return; - // LCOV_EXCL_STOP - } - - png_set_read_fn(png, &file, readPngData); - png_set_sig_bytes(png, pngHeader.size()); - - // Process all chunks up to but not including the image data - png_read_info(png, info); - - uint32_t width, height; - int bitDepth, colorType, interlaceType; - png_get_IHDR( - png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr, nullptr - ); - - png_colorp embeddedPal = nullptr; - int nbColors; - png_bytep transparencyPal = nullptr; - int nbTransparentEntries; - if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) { - if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) { - assume(nbTransparentEntries <= nbColors); - } - } - - // Set up transformations to turn everything into RGBA888 for simplicity of handling - - // Convert grayscale to RGB - switch (colorType & ~PNG_COLOR_MASK_ALPHA) { - case PNG_COLOR_TYPE_GRAY: - png_set_gray_to_rgb(png); // This also converts tRNS to alpha - break; - case PNG_COLOR_TYPE_PALETTE: - png_set_palette_to_rgb(png); - break; - } - - if (png_get_valid(png, info, PNG_INFO_tRNS)) { - // If we read a tRNS chunk, convert it to alpha - png_set_tRNS_to_alpha(png); - } else if (!(colorType & PNG_COLOR_MASK_ALPHA)) { - // Otherwise, if we lack an alpha channel, default to full opacity - png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER); - } - - // Scale 16bpp back to 8 (we don't need all of that precision anyway) - if (bitDepth == 16) { - png_set_scale_16(png); - } else if (bitDepth < 8) { - png_set_packing(png); - } - - if (interlaceType != PNG_INTERLACE_NONE) { - png_set_interlace_handling(png); - } - - // Update `info` with the transformations - png_read_update_info(png, info); - // These shouldn't have changed - assume(png_get_image_width(png, info) == width); - assume(png_get_image_height(png, info) == height); - // These should have changed, however - assume(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA); - assume(png_get_bit_depth(png, info) == 8); - - // Now that metadata has been read, we can process the image data - - std::vector image(width * height * 4); - std::vector rowPtrs(height); - for (uint32_t y = 0; y < height; ++y) { - rowPtrs[y] = image.data() + y * width * 4; - } - png_read_image(png, rowPtrs.data()); +static void parsePNGFile(char const *filename, std::filebuf &file) { + Png png{filename, file}; // The image width must evenly divide into a color swatch for each color per palette - if (width % options.nbColorsPerPal != 0) { + if (png.width % options.nbColorsPerPal != 0) { error( "PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu8 " color swatches wide", - width, - height, + png.width, + png.height, options.nbColorsPerPal ); return; } // Infer the color swatch size (width and height) from the image width - uint32_t swatchSize = width / options.nbColorsPerPal; + uint32_t swatchSize = png.width / options.nbColorsPerPal; // The image height must evenly divide into a color swatch for each palette - if (height % swatchSize != 0) { + if (png.height % swatchSize != 0) { error( "PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu32 " pixels high", - width, - height, + png.width, + png.height, swatchSize ); return; } // More palettes than the maximum are a warning, not an error - uint32_t nbPals = height / swatchSize; + uint32_t nbPals = png.height / swatchSize; if (nbPals > options.nbPalettes) { warnx( "PNG palette file contains %" PRIu32 " palette rows, but there can only be %" PRIu16 @@ -763,17 +640,16 @@ static void parsePNGFile(std::filebuf &file) { options.palSpec.clear(); // Get each color from the top-left pixel of each swatch - uint32_t colorFactor = swatchSize * 4; - uint32_t palFactor = swatchSize * options.nbColorsPerPal; - for (uint32_t palIdx = 0; palIdx < nbPals; ++palIdx) { + for (uint32_t y = 0; y < nbPals; ++y) { + uint32_t yOffset = y * swatchSize * swatchSize * options.nbColorsPerPal; options.palSpec.emplace_back(); - for (uint32_t colorIdx = 0; colorIdx < options.nbColorsPerPal; ++colorIdx) { - std::optional &color = options.palSpec.back()[colorIdx]; - uint32_t offset = (palIdx * palFactor + colorIdx) * colorFactor; - color = Rgba(image[offset], image[offset + 1], image[offset + 2], image[offset + 3]); + + for (uint32_t x = 0; x < options.nbColorsPerPal; ++x) { + uint32_t offset = yOffset + x * swatchSize; + options.palSpec.back()[x] = png.pixels[offset]; // Check that each swatch is completely one color - if (!checkPngSwatch(image, offset, swatchSize)) { + if (!checkPngSwatch(png.pixels, offset, swatchSize)) { error("PNG palette file uses multiple colors in one color swatch"); return; } @@ -821,7 +697,7 @@ void parseExternalPalSpec(char const *arg) { return; } - std::get<1> (*iter)(file); + std::get<1> (*iter)(path, file); } void parseDmgPalSpec(char const * const rawArg) { diff --git a/src/gfx/png.cpp b/src/gfx/png.cpp new file mode 100644 index 00000000..74e6545f --- /dev/null +++ b/src/gfx/png.cpp @@ -0,0 +1,216 @@ +// SPDX-License-Identifier: MIT + +#include "gfx/png.hpp" + +#include + +#include "gfx/main.hpp" +#include "gfx/rgba.hpp" +#include "gfx/warning.hpp" + +struct Input { + char const *filename; + std::streambuf &file; + + Input(char const *filename_, std::streambuf &file_) : filename(filename_), file(file_) {} +}; + +[[noreturn]] +static void handleError(png_structp png, char const *msg) { + Input const &input = *reinterpret_cast(png_get_error_ptr(png)); + + fatal("Error reading PNG image (\"%s\"): %s", input.filename, msg); +} + +static void handleWarning(png_structp png, char const *msg) { + Input const &input = *reinterpret_cast(png_get_error_ptr(png)); + + warnx("In PNG image (\"%s\"): %s", input.filename, msg); +} + +static void readData(png_structp png, png_bytep data, size_t length) { + Input &input = *reinterpret_cast(png_get_io_ptr(png)); + std::streamsize expectedLen = length; + std::streamsize nbBytesRead = input.file.sgetn(reinterpret_cast(data), expectedLen); + + if (nbBytesRead != expectedLen) { + fatal( + "Error reading PNG image (\"%s\"): file too short (expected at least %zd more " + "bytes after reading %zu)", + input.filename, + length - nbBytesRead, + static_cast(input.file.pubseekoff(0, std::ios_base::cur)) + ); + } +} + +Png::Png(char const *filename, std::streambuf &file) { + Input input(filename, file); + + options.verbosePrint(Options::VERB_LOG_ACT, "Reading PNG file \"%s\"\n", input.filename); + + std::array pngHeader; + if (input.file.sgetn(reinterpret_cast(pngHeader.data()), pngHeader.size()) + != static_cast(pngHeader.size()) // Not enough bytes? + || png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) { + fatal("PNG file (\"%s\") is not a valid PNG image!", input.filename); // LCOV_EXCL_LINE + } + + options.verbosePrint(Options::VERB_INTERM, "PNG header signature is OK\n"); + + png_structp png = png_create_read_struct( + PNG_LIBPNG_VER_STRING, static_cast(&input), handleError, handleWarning + ); + if (!png) { + fatal("Failed to create PNG read structure: %s", strerror(errno)); // LCOV_EXCL_LINE + } + + png_infop info = png_create_info_struct(png); + Defer destroyPng{[&] { png_destroy_read_struct(&png, info ? &info : nullptr, nullptr); }}; + if (!info) { + fatal("Failed to create PNG info structure: %s", strerror(errno)); // LCOV_EXCL_LINE + } + + png_set_read_fn(png, &input, readData); + png_set_sig_bytes(png, pngHeader.size()); + + // Process all chunks up to but not including the image data + png_read_info(png, info); + + int bitDepth, colorType, interlaceType; + png_get_IHDR( + png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr, nullptr + ); + + pixels.resize(static_cast(width) * static_cast(height)); + + auto colorTypeName = [](int type) { + switch (type) { + case PNG_COLOR_TYPE_GRAY: + return "grayscale"; + case PNG_COLOR_TYPE_GRAY_ALPHA: + return "grayscale + alpha"; + case PNG_COLOR_TYPE_PALETTE: + return "palette"; + case PNG_COLOR_TYPE_RGB: + return "RGB"; + case PNG_COLOR_TYPE_RGB_ALPHA: + return "RGB + alpha"; + default: + return "unknown color type"; + } + }; + auto interlaceTypeName = [](int type) { + switch (type) { + case PNG_INTERLACE_NONE: + return "not interlaced"; + case PNG_INTERLACE_ADAM7: + return "interlaced (Adam7)"; + default: + return "unknown interlace type"; + } + }; + options.verbosePrint( + Options::VERB_INTERM, + "PNG image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n", + width, + height, + bitDepth, + colorTypeName(colorType), + interlaceTypeName(interlaceType) + ); + + int nbColors = 0; + png_colorp embeddedPal = nullptr; + if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) { + int nbTransparentEntries = 0; + png_bytep transparencyPal = nullptr; + if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) { + assume(nbTransparentEntries <= nbColors); + } + + for (int i = 0; i < nbColors; ++i) { + png_color const &color = embeddedPal[i]; + palette.emplace_back( + color.red, + color.green, + color.blue, + transparencyPal && i < nbTransparentEntries ? transparencyPal[i] : 0xFF + ); + } + + options.verbosePrint( + Options::VERB_INTERM, "Embedded PNG palette has %d colors: [", nbColors + ); + for (int i = 0; i < nbColors; ++i) { + if (i > 0) { + options.verbosePrint(Options::VERB_INTERM, ", "); + } + options.verbosePrint(Options::VERB_INTERM, "#%08x", palette[i].toCSS()); + } + options.verbosePrint(Options::VERB_INTERM, "]\n"); + } else { + options.verbosePrint(Options::VERB_INTERM, "No embedded PNG palette\n"); + } + + // Set up transformations to turn everything into RGBA8888 for simplicity of handling + + // Convert grayscale to RGB + switch (colorType & ~PNG_COLOR_MASK_ALPHA) { + case PNG_COLOR_TYPE_GRAY: + png_set_gray_to_rgb(png); // This also converts tRNS to alpha + break; + case PNG_COLOR_TYPE_PALETTE: + png_set_palette_to_rgb(png); + break; + } + + if (png_get_valid(png, info, PNG_INFO_tRNS)) { + // If we read a tRNS chunk, convert it to alpha + png_set_tRNS_to_alpha(png); + } else if (!(colorType & PNG_COLOR_MASK_ALPHA)) { + // Otherwise, if we lack an alpha channel, default to full opacity + png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER); + } + + // Scale 16bpp back to 8 (we don't need all of that precision anyway) + if (bitDepth == 16) { + png_set_scale_16(png); + } else if (bitDepth < 8) { + png_set_packing(png); + } + + // Deinterlace rows so they can trivially be read in order + if (interlaceType != PNG_INTERLACE_NONE) { + png_set_interlace_handling(png); + } + + // Update `info` with the transformations + png_read_update_info(png, info); + // These shouldn't have changed + assume(png_get_image_width(png, info) == width); + assume(png_get_image_height(png, info) == height); + // These should have changed, however + assume(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA); + assume(png_get_bit_depth(png, info) == 8); + + // Now that metadata has been read, we can read the image data + std::vector image(width * height * 4); + std::vector rowPtrs(height); + for (uint32_t y = 0; y < height; ++y) { + rowPtrs[y] = image.data() + y * width * 4; + } + png_read_image(png, rowPtrs.data()); + + // We don't care about chunks after the image data (comments, etc.) + png_read_end(png, nullptr); + + // Finally, process the image data from RGBA8888 bytes into `Rgba` colors + for (uint32_t y = 0; y < height; ++y) { + for (uint32_t x = 0; x < width; ++x) { + uint32_t idx = y * width + x; + uint32_t off = idx * 4; + pixels[idx] = Rgba(image[off], image[off + 1], image[off + 2], image[off + 3]); + } + } +} diff --git a/src/gfx/process.cpp b/src/gfx/process.cpp index 6afd1a8a..c358a155 100644 --- a/src/gfx/process.cpp +++ b/src/gfx/process.cpp @@ -23,6 +23,7 @@ #include "gfx/main.hpp" #include "gfx/pal_packing.hpp" #include "gfx/pal_sorting.hpp" +#include "gfx/png.hpp" #include "gfx/proto_palette.hpp" #include "gfx/warning.hpp" @@ -41,9 +42,10 @@ public: // color), then the other color is returned. Otherwise, `nullptr` is returned. [[nodiscard]] Rgba const *registerColor(Rgba const &rgba) { - std::optional &slot = _colors[rgba.cgbColor()]; + uint16_t color = rgba.cgbColor(); + std::optional &slot = _colors[color]; - if (rgba.cgbColor() == Rgba::transparent && !isBgColorTransparent()) { + if (color == Rgba::transparent && !isBgColorTransparent()) { options.hasTransparentPixels = true; } @@ -67,70 +69,12 @@ public: auto end() const { return _colors.end(); } }; -class Png { - std::string const &path; - File file{}; - png_structp png = nullptr; - png_infop info = nullptr; +struct Image { + Png png{}; + ImagePalette colors{}; - // These are cached for speed - uint32_t width, height; - std::vector pixels; - ImagePalette colors; - int colorType; - int nbColors; - png_colorp embeddedPal = nullptr; - int nbTransparentEntries; - png_bytep transparencyPal = nullptr; - - [[noreturn]] - static void handleError(png_structp png, char const *msg) { - Png *self = reinterpret_cast(png_get_error_ptr(png)); - - fatal("Error reading input image (\"%s\"): %s", self->c_str(), msg); - } - - static void handleWarning(png_structp png, char const *msg) { - Png *self = reinterpret_cast(png_get_error_ptr(png)); - - warnx("In input image (\"%s\"): %s", self->c_str(), msg); - } - - static void readData(png_structp png, png_bytep data, size_t length) { - Png *self = reinterpret_cast(png_get_io_ptr(png)); - std::streamsize expectedLen = length; - std::streamsize nbBytesRead = - self->file->sgetn(reinterpret_cast(data), expectedLen); - - if (nbBytesRead != expectedLen) { - fatal( - "Error reading input image (\"%s\"): file too short (expected at least %zd more " - "bytes after reading %zu)", - self->c_str(), - length - nbBytesRead, - static_cast(self->file->pubseekoff(0, std::ios_base::cur)) - ); - } - } - -public: - ImagePalette const &getColors() const { return colors; } - - int getColorType() const { return colorType; } - - std::tuple getEmbeddedPal() const { - return {nbColors, embeddedPal, nbTransparentEntries, transparencyPal}; - } - - uint32_t getWidth() const { return width; } - - uint32_t getHeight() const { return height; } - - Rgba &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; } - - Rgba const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; } - - char const *c_str() const { return file.c_str(path); } + Rgba &pixel(uint32_t x, uint32_t y) { return png.pixels[y * png.width + x]; } + Rgba const &pixel(uint32_t x, uint32_t y) const { return png.pixels[y * png.width + x]; } bool isSuitableForGrayscale() const { // Check that all of the grays don't fall into the same "bin" @@ -170,62 +114,22 @@ public: return true; } - // Reads a PNG and notes all of its colors - // - // This code is more complicated than strictly necessary, but that's because of the API - // being used: the "high-level" interface doesn't provide all the transformations we need, - // so we use the "lower-level" one instead. - // We also use that occasion to only read the PNG one line at a time, since we store all of - // the pixel data in `pixels`, which saves on memory allocations. - explicit Png(std::string const &filePath) : path(filePath), colors() { - if (file.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) { - fatal("Failed to open input image (\"%s\"): %s", file.c_str(path), strerror(errno)); + explicit Image(std::string const &path) { + File input; + if (input.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) { + fatal("Failed to open input image (\"%s\"): %s", input.c_str(path), strerror(errno)); } - options.verbosePrint(Options::VERB_LOG_ACT, "Opened input file\n"); + png = Png(input.c_str(path), *input); - std::array pngHeader; - if (file->sgetn(reinterpret_cast(pngHeader.data()), pngHeader.size()) - != static_cast(pngHeader.size()) // Not enough bytes? - || png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) { - fatal("Input file (\"%s\") is not a PNG image!", file.c_str(path)); + // Validate input slice + if (options.inputSlice.width == 0 && png.width % 8 != 0) { + fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", png.width); } - - options.verbosePrint(Options::VERB_INTERM, "PNG header signature is OK\n"); - - png = png_create_read_struct( - PNG_LIBPNG_VER_STRING, static_cast(this), handleError, handleWarning - ); - if (!png) { - fatal("Failed to create PNG read structure: %s", strerror(errno)); // LCOV_EXCL_LINE + if (options.inputSlice.height == 0 && png.height % 8 != 0) { + fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", png.height); } - - info = png_create_info_struct(png); - if (!info) { - // LCOV_EXCL_START - png_destroy_read_struct(&png, nullptr, nullptr); - fatal("Failed to create PNG info structure: %s", strerror(errno)); - // LCOV_EXCL_STOP - } - - png_set_read_fn(png, this, readData); - png_set_sig_bytes(png, pngHeader.size()); - - // Process all chunks up to but not including the image data - png_read_info(png, info); - - int bitDepth, interlaceType; - png_get_IHDR( - png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr, nullptr - ); - - if (options.inputSlice.width == 0 && width % 8 != 0) { - fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", width); - } - if (options.inputSlice.height == 0 && height % 8 != 0) { - fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", height); - } - if (options.inputSlice.right() > width || options.inputSlice.bottom() > height) { + if (options.inputSlice.right() > png.width || options.inputSlice.bottom() > png.height) { error( "Image slice ((%" PRIu16 ", %" PRIu16 ") to (%" PRIu32 ", %" PRIu32 ")) is outside the image bounds (%" PRIu32 "x%" PRIu32 ")!", @@ -233,8 +137,8 @@ public: options.inputSlice.top, options.inputSlice.right(), options.inputSlice.bottom(), - width, - height + png.width, + png.height ); if (options.inputSlice.width % 8 == 0 && options.inputSlice.height % 8 == 0) { fprintf( @@ -250,111 +154,6 @@ public: giveUp(); } - pixels.resize(static_cast(width) * static_cast(height)); - - auto colorTypeName = [this]() { - switch (colorType) { - case PNG_COLOR_TYPE_GRAY: - return "grayscale"; - case PNG_COLOR_TYPE_GRAY_ALPHA: - return "grayscale + alpha"; - case PNG_COLOR_TYPE_PALETTE: - return "palette"; - case PNG_COLOR_TYPE_RGB: - return "RGB"; - case PNG_COLOR_TYPE_RGB_ALPHA: - return "RGB + alpha"; - default: - fatal("Unknown color type %d", colorType); - } - }; - auto interlaceTypeName = [&interlaceType]() { - switch (interlaceType) { - case PNG_INTERLACE_NONE: - return "not interlaced"; - case PNG_INTERLACE_ADAM7: - return "interlaced (Adam7)"; - default: - fatal("Unknown interlace type %d", interlaceType); - } - }; - options.verbosePrint( - Options::VERB_INTERM, - "Input image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n", - width, - height, - bitDepth, - colorTypeName(), - interlaceTypeName() - ); - - if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) { - if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) { - assume(nbTransparentEntries <= nbColors); - } - - options.verbosePrint( - Options::VERB_INTERM, "Embedded palette has %d colors: [", nbColors - ); - for (int i = 0; i < nbColors; ++i) { - png_color const &color = embeddedPal[i]; - options.verbosePrint( - Options::VERB_INTERM, - "#%02x%02x%02x%02x%s", - color.red, - color.green, - color.blue, - transparencyPal && i < nbTransparentEntries ? transparencyPal[i] : 0xFF, - i != nbColors - 1 ? ", " : "]\n" - ); - } - } else { - options.verbosePrint(Options::VERB_INTERM, "No embedded palette\n"); - } - - // Set up transformations to turn everything into RGBA888 for simplicity of handling - - // Convert grayscale to RGB - switch (colorType & ~PNG_COLOR_MASK_ALPHA) { - case PNG_COLOR_TYPE_GRAY: - png_set_gray_to_rgb(png); // This also converts tRNS to alpha - break; - case PNG_COLOR_TYPE_PALETTE: - png_set_palette_to_rgb(png); - break; - } - - if (png_get_valid(png, info, PNG_INFO_tRNS)) { - // If we read a tRNS chunk, convert it to alpha - png_set_tRNS_to_alpha(png); - } else if (!(colorType & PNG_COLOR_MASK_ALPHA)) { - // Otherwise, if we lack an alpha channel, default to full opacity - png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER); - } - - // Scale 16bpp back to 8 (we don't need all of that precision anyway) - if (bitDepth == 16) { - png_set_scale_16(png); - } else if (bitDepth < 8) { - png_set_packing(png); - } - - // Do NOT call `png_set_interlace_handling`. We want to expand the rows ourselves. - - // Update `info` with the transformations - png_read_update_info(png, info); - // These shouldn't have changed - assume(png_get_image_width(png, info) == width); - assume(png_get_image_height(png, info) == height); - // These should have changed, however - assume(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA); - assume(png_get_bit_depth(png, info) == 8); - - // Now that metadata has been read, we can process the image data - - size_t nbRowBytes = png_get_rowbytes(png, info); - assume(nbRowBytes != 0); - std::vector row(nbRowBytes); // Holds known-conflicting color pairs to avoid warning about them twice. // We don't need to worry about transitivity, as ImagePalette slots are immutable once // assigned, and conflicts always occur between that and another color. @@ -363,103 +162,67 @@ public: // Holds colors whose alpha value is ambiguous std::vector indeterminates; - // Assign a color to the given position, and register it in the image palette as well - auto assignColor = [&](png_uint_32 x, png_uint_32 y, Rgba &&color) { - if (!color.isTransparent() && !color.isOpaque()) { - uint32_t css = color.toCSS(); - if (std::find(RANGE(indeterminates), css) == indeterminates.end()) { - error( - "Color #%08x is neither transparent (alpha < %u) nor opaque (alpha >= " - "%u) [first seen at x: %" PRIu32 ", y: %" PRIu32 "]", - css, - Rgba::transparency_threshold, - Rgba::opacity_threshold, - x, - y - ); - indeterminates.push_back(css); - } - } else if (Rgba const *other = colors.registerColor(color); other) { - std::tuple conflicting{color.toCSS(), other->toCSS()}; - // Do not report combinations twice - if (std::find(RANGE(conflicts), conflicting) == conflicts.end()) { - warnx( - "Fusing colors #%08x and #%08x into Game Boy color $%04x [first seen " - "at x: %" PRIu32 ", y: %" PRIu32 "]", - std::get<0>(conflicting), - std::get<1>(conflicting), - color.cgbColor(), - x, - y - ); - // Do not report this combination again - conflicts.emplace_back(conflicting); - } - } + // Register colors from image + for (uint32_t y = 0; y < png.height; ++y) { + for (uint32_t x = 0; x < png.width; ++x) { + Rgba const &color = pixel(x, y); - pixel(x, y) = color; - }; - - if (interlaceType == PNG_INTERLACE_NONE) { - for (png_uint_32 y = 0; y < height; ++y) { - png_bytep ptr = row.data(); - png_read_row(png, ptr, nullptr); - - for (png_uint_32 x = 0; x < width; ++x) { - assignColor(x, y, Rgba(ptr[0], ptr[1], ptr[2], ptr[3])); - ptr += 4; - } - } - } else { - assume(interlaceType == PNG_INTERLACE_ADAM7); - - // For interlace to work properly, we must read the image `nbPasses` times - for (int pass = 0; pass < PNG_INTERLACE_ADAM7_PASSES; ++pass) { - // The interlacing pass must be skipped if its width or height is reported as zero - if (PNG_PASS_COLS(width, pass) == 0 || PNG_PASS_ROWS(height, pass) == 0) { - continue; - } - - png_uint_32 xStep = 1u << PNG_PASS_COL_SHIFT(pass); - png_uint_32 yStep = 1u << PNG_PASS_ROW_SHIFT(pass); - - for (png_uint_32 y = PNG_PASS_START_ROW(pass); y < height; y += yStep) { - png_bytep ptr = row.data(); - png_read_row(png, ptr, nullptr); - - for (png_uint_32 x = PNG_PASS_START_COL(pass); x < width; x += xStep) { - assignColor(x, y, Rgba(ptr[0], ptr[1], ptr[2], ptr[3])); - ptr += 4; + // Assign a color to the given position, and register it in the image palette + if (color.isTransparent() == color.isOpaque()) { + uint32_t css = color.toCSS(); + if (std::find(RANGE(indeterminates), css) == indeterminates.end()) { + error( + "Color #%08x is neither transparent (alpha < %u) nor opaque (alpha >= " + "%u) [first seen at x: %" PRIu32 ", y: %" PRIu32 "]", + css, + Rgba::transparency_threshold, + Rgba::opacity_threshold, + x, + y + ); + indeterminates.push_back(css); + } + } else if (Rgba const *other = colors.registerColor(color); other) { + std::tuple conflicting{color.toCSS(), other->toCSS()}; + // Do not report combinations twice + if (std::find(RANGE(conflicts), conflicting) == conflicts.end()) { + warnx( + "Fusing colors #%08x and #%08x into Game Boy color $%04x [first seen " + "at x: %" PRIu32 ", y: %" PRIu32 "]", + std::get<0>(conflicting), + std::get<1>(conflicting), + color.cgbColor(), + x, + y + ); + // Do not report this combination again + conflicts.emplace_back(conflicting); } } } } - - // We don't care about chunks after the image data (comments, etc.) - png_read_end(png, nullptr); } - ~Png() { png_destroy_read_struct(&png, &info, nullptr); } - class TilesVisitor { - Png const &_png; + Image const &_image; bool const _columnMajor; uint32_t const _width, _height; uint32_t const _limit = _columnMajor ? _height : _width; public: - TilesVisitor(Png const &png, bool columnMajor, uint32_t width, uint32_t height) - : _png(png), _columnMajor(columnMajor), _width(width), _height(height) {} + TilesVisitor(Image const &image, bool columnMajor, uint32_t width, uint32_t height) + : _image(image), _columnMajor(columnMajor), _width(width), _height(height) {} class Tile { - Png const &_png; + Image const &_image; + public: uint32_t const x, y; - Tile(Png const &png, uint32_t x_, uint32_t y_) : _png(png), x(x_), y(y_) {} + Tile(Image const &image, uint32_t x_, uint32_t y_) : _image(image), x(x_), y(y_) {} Rgba pixel(uint32_t xOfs, uint32_t yOfs) const { - return _png.pixel(x + xOfs, y + yOfs); + return _image.pixel(x + xOfs, y + yOfs); } }; @@ -473,7 +236,7 @@ public: return {x + options.inputSlice.left, y + options.inputSlice.top}; } Tile operator*() const { - return {parent._png, x + options.inputSlice.left, y + options.inputSlice.top}; + return {parent._image, x + options.inputSlice.left, y + options.inputSlice.top}; } Iterator &operator++() { @@ -496,13 +259,14 @@ public: return ++it; // ...now one-past-last! } }; + public: TilesVisitor visitAsTiles() const { return { *this, options.columnMajor, - options.inputSlice.width ? options.inputSlice.width * 8 : width, - options.inputSlice.height ? options.inputSlice.height * 8 : height, + options.inputSlice.width ? options.inputSlice.width * 8 : png.width, + options.inputSlice.height ? options.inputSlice.height * 8 : png.height, }; } }; @@ -522,10 +286,7 @@ private: public: // Creates a new raw tile, and returns a reference to it so it can be filled in - RawTile &newTile() { - _tiles.emplace_back(); - return _tiles.back(); - } + RawTile &newTile() { return _tiles.emplace_back(); } }; struct AttrmapEntry { @@ -547,32 +308,30 @@ struct AttrmapEntry { } }; -static void generatePalSpec(Png const &png) { +static void generatePalSpec(Image const &image) { // Generate a palette spec from the first few colors in the embedded palette - auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal(); - if (embPalRGB == nullptr) { + std::vector const &embPal = image.png.palette; + if (embPal.empty()) { fatal("`-c embedded` was given, but the PNG does not have an embedded palette!"); } + // Ignore extraneous colors if they are unused + size_t nbColors = embPal.size(); + if (nbColors > options.maxOpaqueColors()) { + nbColors = options.maxOpaqueColors(); + } + // Fill in the palette spec options.palSpec.clear(); - options.palSpec.emplace_back(); // A single palette, with `#00000000`s (transparent) - assume(options.palSpec.size() == 1); - if (embPalSize > options.maxOpaqueColors()) { // Ignore extraneous colors if they are unused - embPalSize = options.maxOpaqueColors(); - } - for (int i = 0; i < embPalSize; ++i) { - options.palSpec[0][i] = Rgba( - embPalRGB[i].red, - embPalRGB[i].green, - embPalRGB[i].blue, - embPalAlpha && i < embPalAlphaSize ? embPalAlpha[i] : 0xFF - ); + auto &palette = options.palSpec.emplace_back(); + assume(nbColors <= palette.size()); + for (size_t i = 0; i < nbColors; ++i) { + palette[i] = embPal[i]; } } static std::tuple, std::vector> - generatePalettes(std::vector const &protoPalettes, Png const &png) { + generatePalettes(std::vector const &protoPalettes, Image const &image) { // Run a "pagination" problem solver auto [mappings, nbPalettes] = overloadAndRemove(protoPalettes); assume(mappings.size() == protoPalettes.size()); @@ -609,16 +368,15 @@ static std::tuple, std::vector> // "Sort" colors in the generated palettes, see the man page for the flowchart if (options.palSpecType == Options::DMG) { - sortGrayscale(palettes, png.getColors().raw()); - } else if (auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal(); - embPalRGB != nullptr) { + sortGrayscale(palettes, image.colors.raw()); + } else if (!image.png.palette.empty()) { warning( WARNING_EMBEDDED, "Sorting palette colors by PNG's embedded PLTE chunk without '-c/--colors embedded'" ); - sortIndexed(palettes, embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha); - } else if (png.isSuitableForGrayscale()) { - sortGrayscale(palettes, png.getColors().raw()); + sortIndexed(palettes, image.png.palette); + } else if (image.isSuitableForGrayscale()) { + sortGrayscale(palettes, image.colors.raw()); } else { sortRgb(palettes); } @@ -641,8 +399,8 @@ static std::tuple, std::vector> auto listColors = [](auto const &list) { static char buf[sizeof(", $XXXX, $XXXX, $XXXX, $XXXX")]; char *ptr = buf; - for (uint16_t cgbColor : list) { - ptr += snprintf(ptr, sizeof(", $XXXX"), ", $%04x", cgbColor); + for (uint16_t color : list) { + ptr += snprintf(ptr, sizeof(", $XXXX"), ", $%04x", color); } return &buf[literal_strlen(", ")]; }; @@ -753,7 +511,7 @@ public: mutable uint16_t tileID; static uint16_t - rowBitplanes(Png::TilesVisitor::Tile const &tile, Palette const &palette, uint32_t y) { + rowBitplanes(Image::TilesVisitor::Tile const &tile, Palette const &palette, uint32_t y) { uint16_t row = 0; for (uint32_t x = 0; x < 8; ++x) { row <<= 1; @@ -776,7 +534,7 @@ public: } } - TileData(Png::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) { + TileData(Image::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) { size_t writeIndex = 0; for (uint32_t y = 0; y < 8; ++y) { uint16_t bitplanes = rowBitplanes(tile, palette, y); @@ -856,7 +614,7 @@ struct std::hash { }; static void outputUnoptimizedTileData( - Png const &png, + Image const &image, std::vector const &attrmap, std::vector const &palettes, std::vector const &mappings @@ -868,14 +626,14 @@ static void outputUnoptimizedTileData( // LCOV_EXCL_STOP } - uint16_t widthTiles = options.inputSlice.width ? options.inputSlice.width : png.getWidth() / 8; + uint16_t widthTiles = options.inputSlice.width ? options.inputSlice.width : image.png.width / 8; uint16_t heightTiles = - options.inputSlice.height ? options.inputSlice.height : png.getHeight() / 8; + options.inputSlice.height ? options.inputSlice.height : image.png.height / 8; uint64_t nbTiles = widthTiles * heightTiles; uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0; uint64_t tileIdx = 0; - for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) { + for (auto [tile, attr] : zip(image.visitAsTiles(), attrmap)) { // Do not emit fully-background tiles. if (attr.isBackgroundTile()) { ++tileIdx; @@ -993,7 +751,7 @@ struct UniqueTiles { // 8-bit tile IDs + the bank bit; this will save the work when we output the data later (potentially // twice) static UniqueTiles dedupTiles( - Png const &png, + Image const &image, std::vector &attrmap, std::vector const &palettes, std::vector const &mappings @@ -1044,7 +802,7 @@ static UniqueTiles dedupTiles( } bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty(); - for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) { + for (auto [tile, attr] : zip(image.visitAsTiles(), attrmap)) { if (attr.isBackgroundTile()) { attr.xFlip = false; attr.yFlip = false; @@ -1157,16 +915,12 @@ void process() { options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr)); options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n"); - Png png(options.input); // This also sets `hasTransparentPixels` as a side effect - ImagePalette const &colors = png.getColors(); - - // Now, we have all the image's colors in `colors` - // The next step is to order the palette + Image image(options.input); // This also sets `hasTransparentPixels` as a side effect // LCOV_EXCL_START if (options.verbosity >= Options::VERB_INTERM) { fputs("Image colors: [ ", stderr); - for (std::optional const &slot : colors) { + for (std::optional const &slot : image.colors) { if (!slot.has_value()) { continue; } @@ -1182,7 +936,7 @@ void process() { "Image contains transparent pixels, not compatible with a DMG palette specification" ); } - if (!png.isSuitableForGrayscale()) { + if (!image.isSuitableForGrayscale()) { fatal("Image contains too many or non-gray colors, not compatible with a DMG palette " "specification"); } @@ -1195,7 +949,7 @@ void process() { std::vector protoPalettes; std::vector attrmap{}; - for (auto tile : png.visitAsTiles()) { + for (auto tile : image.visitAsTiles()) { AttrmapEntry &attrs = attrmap.emplace_back(); // Count the unique non-transparent colors for packing @@ -1227,8 +981,8 @@ void process() { } ProtoPalette protoPalette; - for (uint16_t cgbColor : tileColors) { - protoPalette.add(cgbColor); + for (uint16_t color : tileColors) { + protoPalette.add(color); } if (options.bgColor.has_value() @@ -1295,17 +1049,17 @@ continue_visiting_tiles:; // LCOV_EXCL_STOP if (options.palSpecType == Options::EMBEDDED) { - generatePalSpec(png); + generatePalSpec(image); } auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC || options.palSpecType == Options::DMG - ? generatePalettes(protoPalettes, png) + ? generatePalettes(protoPalettes, image) : makePalsAsSpecified(protoPalettes); outputPalettes(palettes); // If deduplication is not happening, we just need to output the tile data and/or maps as-is if (!options.allowDedup) { - uint32_t const nbTilesH = png.getHeight() / 8, nbTilesW = png.getWidth() / 8; + uint32_t const nbTilesH = image.png.height / 8, nbTilesW = image.png.width / 8; // Check the tile count if (uint32_t nbTiles = nbTilesW * nbTilesH; @@ -1326,7 +1080,7 @@ continue_visiting_tiles:; if (!options.output.empty()) { options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n"); - outputUnoptimizedTileData(png, attrmap, palettes, mappings); + outputUnoptimizedTileData(image, attrmap, palettes, mappings); } if (!options.tilemap.empty() || !options.attrmap.empty() || !options.palmap.empty()) { @@ -1339,7 +1093,7 @@ continue_visiting_tiles:; } else { // All of these require the deduplication process to be performed to be output options.verbosePrint(Options::VERB_LOG_ACT, "Deduplicating tiles...\n"); - UniqueTiles tiles = dedupTiles(png, attrmap, palettes, mappings); + UniqueTiles tiles = dedupTiles(image, attrmap, palettes, mappings); if (size_t nbTiles = tiles.size(); nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) { diff --git a/test/gfx/damaged1.err b/test/gfx/damaged1.err index 868e2561..4cf84370 100644 --- a/test/gfx/damaged1.err +++ b/test/gfx/damaged1.err @@ -1,2 +1,2 @@ -FATAL: Error reading input image ("damaged1.png"): IDAT: invalid code -- missing end-of-block +FATAL: Error reading PNG image ("damaged1.png"): IDAT: invalid code -- missing end-of-block Conversion aborted after 1 error diff --git a/test/gfx/damaged2.err b/test/gfx/damaged2.err index d94e9a40..5cfd98ad 100644 --- a/test/gfx/damaged2.err +++ b/test/gfx/damaged2.err @@ -1,2 +1,2 @@ -FATAL: Error reading input image ("damaged2.png"): IDAT: invalid code -- missing end-of-block +FATAL: Error reading PNG image ("damaged2.png"): IDAT: invalid code -- missing end-of-block Conversion aborted after 1 error diff --git a/test/gfx/damaged9.err b/test/gfx/damaged9.err index e899a319..51ffde74 100644 --- a/test/gfx/damaged9.err +++ b/test/gfx/damaged9.err @@ -1,2 +1,2 @@ -FATAL: Error reading input image ("damaged9.png"): IDAT: invalid code -- missing end-of-block +FATAL: Error reading PNG image ("damaged9.png"): IDAT: invalid code -- missing end-of-block Conversion aborted after 1 error diff --git a/test/gfx/interlaced.flags b/test/gfx/interlaced.flags new file mode 100644 index 00000000..4bca84d6 --- /dev/null +++ b/test/gfx/interlaced.flags @@ -0,0 +1 @@ +-m diff --git a/test/gfx/interlaced.out.2bpp b/test/gfx/interlaced.out.2bpp new file mode 100644 index 0000000000000000000000000000000000000000..1f592f8eddfad63494a2acba54db53f9ae600229 GIT binary patch literal 64 zcmdN857k}}AG*G&K6F3#|M34W#IK_8#Tm31;~7@5)-tUU{mQ*M99f?0U&tQ-ixeEL literal 0 HcmV?d00001 diff --git a/test/gfx/interlaced.out.attrmap b/test/gfx/interlaced.out.attrmap new file mode 100644 index 0000000000000000000000000000000000000000..257446ed48453d37ef76dc234a655b9935ab31f1 GIT binary patch literal 16 VcmZQzVqgIxMm8okR%Uiqb^rlb05<>t literal 0 HcmV?d00001 diff --git a/test/gfx/interlaced.out.pal b/test/gfx/interlaced.out.pal new file mode 100644 index 0000000000000000000000000000000000000000..8cdc1c73dc852c158cace3d0bf4a6e73a1b8e139 GIT binary patch literal 64 ycmexg|DfKXuAv4(GBEtFuV>!RSRw);LHzx7@;L$y5E8`yZT~>H#f*UgLIVJJ$r-%> literal 0 HcmV?d00001 diff --git a/test/gfx/interlaced.out.tilemap b/test/gfx/interlaced.out.tilemap new file mode 100644 index 0000000000000000000000000000000000000000..c6e3a3f2443d2288a85744044d24c916ea0c4c2a GIT binary patch literal 16 TcmZQz00KrvCJ<&~W?}{a0A>Ia literal 0 HcmV?d00001 diff --git a/test/gfx/interlaced.png b/test/gfx/interlaced.png new file mode 100644 index 0000000000000000000000000000000000000000..ba7d7124c6d10eacfcf88e9b7e98f3bd6db9acd6 GIT binary patch literal 400 zcmV;B0dM|^P)x>!`Bbq-tl?D@1pBN-GE<>=`c}E&6H;@xHc~+T;8UT+8m)ssATUe zIY&t6d+Fj0*;j(>rNgLuKPodi`(EcIV}4X*uQS%N^=ochz@#%7DK|1+_D0g~fJ}DBxV(c*cE`B9 ugPiS-k$yngH=^ttQTC1Yj)5GkZ~O;&Ey}M_)$r5+0000