Implement grayscale DMG palette specs (#1709)

This commit is contained in:
Rangi
2025-06-30 14:53:05 -04:00
committed by GitHub
parent 5942117ac3
commit 7054d81650
21 changed files with 166 additions and 76 deletions

View File

@@ -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 "???";
}());

View File

@@ -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();

View File

@@ -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]);
}
}

View File

@@ -586,8 +586,10 @@ static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
}
// "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

View File

@@ -195,10 +195,13 @@ void reverse() {
Options::VERB_INTERM, "Reversed image dimensions: %zux%zu tiles\n", width, height
);
std::vector<std::array<std::optional<Rgba>, 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<std::array<std::optional<Rgba>, 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.");

View File

@@ -58,7 +58,15 @@ uint16_t Rgba::cgbColor() const {
uint8_t Rgba::grayIndex() const {
assume(isGray());
// Convert from 0..<256 to hasTransparentPixels..<nbColorsPerPal
// 2bpp shades are inverted from RGB PNG; %00 = white, %11 = black
uint8_t gray = 255 - red;
if (options.palSpecType == Options::DMG) {
assume(!options.hasTransparentPixels);
// Reduce gray shade from 0..<256 to 0..<4, then map to color index,
// then reduce to 0..<nbColorsPerPal
return options.dmgColors[gray * 4 / 256] * options.nbColorsPerPal / 4;
}
// Reduce gray shade from 0..<256 to hasTransparentPixels..<nbColorsPerPal
// Note that `maxOpaqueColors()` already takes `hasTransparentPixels` into account
return (255 - red) * options.maxOpaqueColors() / 256 + options.hasTransparentPixels;
return gray * options.maxOpaqueColors() / 256 + options.hasTransparentPixels;
}