diff --git a/include/gfx/main.hpp b/include/gfx/main.hpp index f951cca6..e4bedb13 100644 --- a/include/gfx/main.hpp +++ b/include/gfx/main.hpp @@ -29,8 +29,10 @@ struct Options { NO_SPEC, EXPLICIT, EMBEDDED, + DMG, } palSpecType = NO_SPEC; // -c std::vector, 4>> palSpec{}; + uint8_t palSpecDmg = 0; uint8_t bitDepth = 2; // -d std::string inputTileset{}; // -i struct { @@ -65,6 +67,12 @@ struct Options { mutable bool hasTransparentPixels = false; uint8_t maxOpaqueColors() const { return nbColorsPerPal - hasTransparentPixels; } + + uint8_t dmgColors[4] = {}; + uint8_t dmgValue(uint8_t i) const { + assume(i < 4); + return (palSpecDmg >> (2 * i)) & 0b11; + } }; extern Options options; @@ -119,27 +127,4 @@ static constexpr auto flipTable = ([]() constexpr { return table; })(); -// Parsing helpers. - -static constexpr uint8_t nibble(char c) { - if (c >= 'a') { - assume(c <= 'f'); - return c - 'a' + 10; - } else if (c >= 'A') { - assume(c <= 'F'); - return c - 'A' + 10; - } else { - assume(c >= '0' && c <= '9'); - return c - '0'; - } -} - -static constexpr uint8_t toHex(char c1, char c2) { - return nibble(c1) * 16 + nibble(c2); -} - -static constexpr uint8_t singleToHex(char c) { - return toHex(c, c); -} - #endif // RGBDS_GFX_MAIN_HPP diff --git a/include/gfx/pal_spec.hpp b/include/gfx/pal_spec.hpp index 0b3abd4f..9423b97f 100644 --- a/include/gfx/pal_spec.hpp +++ b/include/gfx/pal_spec.hpp @@ -3,7 +3,10 @@ #ifndef RGBDS_GFX_PAL_SPEC_HPP #define RGBDS_GFX_PAL_SPEC_HPP -void parseInlinePalSpec(char const * const arg); +void parseInlinePalSpec(char const * const rawArg); void parseExternalPalSpec(char const *arg); +void parseDmgPalSpec(char const * const rawArg); + +void parseBackgroundPalSpec(char const *arg); #endif // RGBDS_GFX_PAL_SPEC_HPP diff --git a/man/rgbgfx.1 b/man/rgbgfx.1 index 62eca623..8db8e1a3 100644 --- a/man/rgbgfx.1 +++ b/man/rgbgfx.1 @@ -159,6 +159,24 @@ is the case-insensitive word then the first four colors of the input PNG's embedded palette are used. It is an error if the PNG is not indexed, or if colors other than these 4 are used. .Pq This is different from the default behavior of indexed PNGs, as then unused entries in the embedded palette are ignored, whereas they are not with Fl c Cm embedded . +.It Sy DMG palette spec +If +.Ar pal_spec +starts with case-insensitive +.Cm dmg= , +then the following two-digit hexadecimal number specifies four grayscale DMG color indexes. +The number functions like the DMG's $FF47 +.Sy BGP +register +(see +.Lk https://gbdev.io/pandocs/Palettes.html Pan Docs +for more information): +the low two bits 0-1 specify which gray shade goes in color index 0, +the next two bits 2-3 specify which gray shade goes in color index 1, +and so on. +Gray shade 0 is the lightest (white), 3 is the darkest (black). +The same gray shade cannot go in two color indexes. +To specify a DMG palette, the input PNG must have all its colors in shades of gray, without any transparent colors. .It Sy external palette spec Otherwise, .Ar pal_spec @@ -528,6 +546,8 @@ Otherwise, if the PNG only contains shades of gray, they will be categorized int .Dq bins as there are colors per palette, and the palette is set to these bins. The darkest gray will end up in bin #0, and so on; note that this is the opposite of the RGB method below. +This is equivalent to having specified a DMG palette of +.Fl c Cm dmg=E4 . If two distinct grays end up in the same bin, the RGB method is used instead. .Pp Be careful that diff --git a/src/gfx/main.cpp b/src/gfx/main.cpp index fa4a0235..9de3857e 100644 --- a/src/gfx/main.cpp +++ b/src/gfx/main.cpp @@ -16,7 +16,6 @@ #include "extern/getopt.hpp" #include "file.hpp" -#include "helpers.hpp" // assume #include "platform.hpp" #include "version.hpp" @@ -354,7 +353,6 @@ static char *parseArgv(int argc, char *argv[]) { for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) { char *arg = musl_optarg; // Make a copy for scanning uint16_t number; - size_t size; switch (ch) { case 'A': localOptions.autoAttrmap = true; @@ -367,41 +365,7 @@ static char *parseArgv(int argc, char *argv[]) { options.attrmap = musl_optarg; break; case 'B': - if (strcasecmp(musl_optarg, "transparent") == 0) { - options.bgColor = Rgba(0x00, 0x00, 0x00, 0x00); - break; - } - if (musl_optarg[0] != '#') { - error("Background color specification must be `#rgb`, `#rrggbb`, or `transparent`"); - break; - } - size = strspn(&musl_optarg[1], "0123456789ABCDEFabcdef"); - switch (size) { - case 3: - options.bgColor = Rgba( - singleToHex(musl_optarg[1]), - singleToHex(musl_optarg[2]), - singleToHex(musl_optarg[3]), - 0xFF - ); - break; - case 6: - options.bgColor = Rgba( - toHex(musl_optarg[1], musl_optarg[2]), - toHex(musl_optarg[3], musl_optarg[4]), - toHex(musl_optarg[5], musl_optarg[6]), - 0xFF - ); - break; - default: - error("Unknown background color specification \"%s\"", musl_optarg); - } - if (musl_optarg[size + 1] != '\0') { - error( - "Unexpected text \"%s\" after background color specification", - &musl_optarg[size + 1] - ); - } + parseBackgroundPalSpec(musl_optarg); break; case 'b': number = parseNumber(arg, "Bank 0 base tile ID", 0); @@ -442,18 +406,18 @@ static char *parseArgv(int argc, char *argv[]) { options.useColorCurve = true; break; case 'c': + localOptions.externalPalSpec = nullptr; // Allow overriding a previous pal spec if (musl_optarg[0] == '#') { options.palSpecType = Options::EXPLICIT; parseInlinePalSpec(musl_optarg); } else if (strcasecmp(musl_optarg, "embedded") == 0) { // Use PLTE, error out if missing options.palSpecType = Options::EMBEDDED; + } else if (strncasecmp(musl_optarg, "dmg=", literal_strlen("dmg=")) == 0) { + options.palSpecType = Options::DMG; + parseDmgPalSpec(&musl_optarg[literal_strlen("dmg=")]); } else { options.palSpecType = Options::EXPLICIT; - // Can't parse the file yet, as "flat" color collections need to know the palette - // size to be split; thus, we defer that. - // If a following `-c` overrides a previous one, the `fmt` part of an overridden - // external palette spec will not be validated, but I guess that's okay. localOptions.externalPalSpec = musl_optarg; } break; @@ -877,6 +841,8 @@ int main(int argc, char *argv[]) { return "Explicit"; case Options::EMBEDDED: return "Embedded"; + case Options::DMG: + return "DMG"; } return "???"; }()); diff --git a/src/gfx/pal_packing.cpp b/src/gfx/pal_packing.cpp index a4540eb9..a08c829e 100644 --- a/src/gfx/pal_packing.cpp +++ b/src/gfx/pal_packing.cpp @@ -299,7 +299,7 @@ static void decant( break; } auto attrs = from.begin(); - std::advance(attrs, (iter - processed.begin())); + std::advance(attrs, iter - processed.begin()); // Build up the "component"... colors.clear(); diff --git a/src/gfx/pal_spec.cpp b/src/gfx/pal_spec.cpp index 65f8fef9..23113550 100644 --- a/src/gfx/pal_spec.cpp +++ b/src/gfx/pal_spec.cpp @@ -28,6 +28,27 @@ static void skipWhitespace(Str const &str, size_t &pos) { pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length()); } +static constexpr uint8_t nibble(char c) { + if (c >= 'a') { + assume(c <= 'f'); + return c - 'a' + 10; + } else if (c >= 'A') { + assume(c <= 'F'); + return c - 'A' + 10; + } else { + assume(c >= '0' && c <= '9'); + return c - '0'; + } +} + +static constexpr uint8_t toHex(char c1, char c2) { + return nibble(c1) * 16 + nibble(c2); +} + +static constexpr uint8_t singleToHex(char c) { + return toHex(c, c); +} + void parseInlinePalSpec(char const * const rawArg) { // List of #rrggbb/#rgb colors (or #none); comma-separated. // Palettes are separated by colons. @@ -595,3 +616,61 @@ void parseExternalPalSpec(char const *arg) { std::get<1> (*iter)(file); } + +void parseDmgPalSpec(char const * const rawArg) { + // Two hex digit DMG palette spec + + std::string_view arg(rawArg); + + if (arg.length() != 2 + || arg.find_first_not_of("0123456789ABCDEFabcdef"sv) != std::string_view::npos) { + error("Unknown DMG palette specification \"%s\"", rawArg); + return; + } + + options.palSpecDmg = toHex(arg[0], arg[1]); + + // Map gray shades to their DMG color indexes for fast lookup by `Rgba::grayIndex` + for (uint8_t i = 0; i < 4; ++i) { + options.dmgColors[options.dmgValue(i)] = i; + } + + // Validate that DMG palette spec does not have conflicting colors + for (uint8_t i = 0; i < 3; ++i) { + for (uint8_t j = i + 1; j < 4; ++j) { + if (options.dmgValue(i) == options.dmgValue(j)) { + error("DMG palette specification maps two gray shades to the same color index"); + return; + } + } + } +} + +void parseBackgroundPalSpec(char const *arg) { + if (strcasecmp(arg, "transparent") == 0) { + options.bgColor = Rgba(0x00, 0x00, 0x00, 0x00); + return; + } + + if (arg[0] != '#') { + error("Background color specification must be `#rgb`, `#rrggbb`, or `transparent`"); + return; + } + + size_t size = strspn(&arg[1], "0123456789ABCDEFabcdef"); + switch (size) { + case 3: + options.bgColor = Rgba(singleToHex(arg[1]), singleToHex(arg[2]), singleToHex(arg[3]), 0xFF); + break; + case 6: + options.bgColor = + Rgba(toHex(arg[1], arg[2]), toHex(arg[3], arg[4]), toHex(arg[5], arg[6]), 0xFF); + break; + default: + error("Unknown background color specification \"%s\"", arg); + } + + if (arg[size + 1] != '\0') { + error("Unexpected text \"%s\" after background color specification", &arg[size + 1]); + } +} diff --git a/src/gfx/process.cpp b/src/gfx/process.cpp index ac8d4c9a..369a4c86 100644 --- a/src/gfx/process.cpp +++ b/src/gfx/process.cpp @@ -586,8 +586,10 @@ static std::tuple, std::vector> } // "Sort" colors in the generated palettes, see the man page for the flowchart - auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal(); - if (embPalRGB != nullptr) { + if (options.palSpecType == Options::DMG) { + sortGrayscale(palettes, png.getColors().raw()); + } else if (auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal(); + embPalRGB != nullptr) { sortIndexed(palettes, embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha); } else if (png.isSuitableForGrayscale()) { sortGrayscale(palettes, png.getColors().raw()); @@ -1139,6 +1141,18 @@ void process() { } // LCOV_EXCL_STOP + if (options.palSpecType == Options::DMG) { + if (options.hasTransparentPixels) { + fatal( + "Image contains transparent pixels, not compatible with a DMG palette specification" + ); + } + if (!png.isSuitableForGrayscale()) { + fatal("Image contains too many or non-gray colors, not compatible with a DMG palette " + "specification"); + } + } + // Now, iterate through the tiles, generating proto-palettes as we go // We do this unconditionally because this performs the image validation (which we want to // perform even if no output is requested), and because it's necessary to generate any @@ -1248,9 +1262,10 @@ continue_visiting_tiles:; if (options.palSpecType == Options::EMBEDDED) { generatePalSpec(png); } - auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC - ? generatePalettes(protoPalettes, png) - : makePalsAsSpecified(protoPalettes); + auto [mappings, palettes] = + options.palSpecType == Options::NO_SPEC || options.palSpecType == Options::DMG + ? generatePalettes(protoPalettes, png) + : makePalsAsSpecified(protoPalettes); outputPalettes(palettes); // If deduplication is not happening, we just need to output the tile data and/or maps as-is diff --git a/src/gfx/reverse.cpp b/src/gfx/reverse.cpp index 51d7a72a..6e3c3125 100644 --- a/src/gfx/reverse.cpp +++ b/src/gfx/reverse.cpp @@ -195,10 +195,13 @@ void reverse() { Options::VERB_INTERM, "Reversed image dimensions: %zux%zu tiles\n", width, height ); - std::vector, 4>> palettes{ - {Rgba(0xFFFFFFFF), Rgba(0xAAAAAAFF), Rgba(0x555555FF), Rgba(0x000000FF)} + Rgba const grayColors[4] = { + Rgba(0xFFFFFFFF), Rgba(0xAAAAAAFF), Rgba(0x555555FF), Rgba(0x000000FF) }; - // If a palette file is used as input, it overrides the default colors. + std::vector, 4>> palettes{ + {grayColors[0], grayColors[1], grayColors[2], grayColors[3]} + }; + // If a palette file or palette spec is used as input, it overrides the default colors. if (!options.palettes.empty()) { File file; if (!file.open(options.palettes, std::ios::in | std::ios::binary)) { @@ -255,6 +258,10 @@ void reverse() { putc('\n', stderr); } } + } else if (options.palSpecType == Options::DMG) { + for (size_t i = 0; i < palettes[0].size(); ++i) { + palettes[0][i] = grayColors[options.dmgValue(i)]; + } } else if (options.palSpecType == Options::EMBEDDED) { warning("An embedded palette was requested, but no palette file was specified; ignoring " "request."); diff --git a/src/gfx/rgba.cpp b/src/gfx/rgba.cpp index ace209f3..aefe041d 100644 --- a/src/gfx/rgba.cpp +++ b/src/gfx/rgba.cpp @@ -58,7 +58,15 @@ uint16_t Rgba::cgbColor() const { uint8_t Rgba::grayIndex() const { assume(isGray()); - // Convert from 0..<256 to hasTransparentPixels..