Skip to content

Commit 7054d81

Browse files
authored
Implement grayscale DMG palette specs (#1709)
1 parent 5942117 commit 7054d81

21 files changed

+165
-75
lines changed

include/gfx/main.hpp

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ struct Options {
2929
NO_SPEC,
3030
EXPLICIT,
3131
EMBEDDED,
32+
DMG,
3233
} palSpecType = NO_SPEC; // -c
3334
std::vector<std::array<std::optional<Rgba>, 4>> palSpec{};
35+
uint8_t palSpecDmg = 0;
3436
uint8_t bitDepth = 2; // -d
3537
std::string inputTileset{}; // -i
3638
struct {
@@ -65,6 +67,12 @@ struct Options {
6567

6668
mutable bool hasTransparentPixels = false;
6769
uint8_t maxOpaqueColors() const { return nbColorsPerPal - hasTransparentPixels; }
70+
71+
uint8_t dmgColors[4] = {};
72+
uint8_t dmgValue(uint8_t i) const {
73+
assume(i < 4);
74+
return (palSpecDmg >> (2 * i)) & 0b11;
75+
}
6876
};
6977

7078
extern Options options;
@@ -119,27 +127,4 @@ static constexpr auto flipTable = ([]() constexpr {
119127
return table;
120128
})();
121129

122-
// Parsing helpers.
123-
124-
static constexpr uint8_t nibble(char c) {
125-
if (c >= 'a') {
126-
assume(c <= 'f');
127-
return c - 'a' + 10;
128-
} else if (c >= 'A') {
129-
assume(c <= 'F');
130-
return c - 'A' + 10;
131-
} else {
132-
assume(c >= '0' && c <= '9');
133-
return c - '0';
134-
}
135-
}
136-
137-
static constexpr uint8_t toHex(char c1, char c2) {
138-
return nibble(c1) * 16 + nibble(c2);
139-
}
140-
141-
static constexpr uint8_t singleToHex(char c) {
142-
return toHex(c, c);
143-
}
144-
145130
#endif // RGBDS_GFX_MAIN_HPP

include/gfx/pal_spec.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
#ifndef RGBDS_GFX_PAL_SPEC_HPP
44
#define RGBDS_GFX_PAL_SPEC_HPP
55

6-
void parseInlinePalSpec(char const * const arg);
6+
void parseInlinePalSpec(char const * const rawArg);
77
void parseExternalPalSpec(char const *arg);
8+
void parseDmgPalSpec(char const * const rawArg);
9+
10+
void parseBackgroundPalSpec(char const *arg);
811

912
#endif // RGBDS_GFX_PAL_SPEC_HPP

man/rgbgfx.1

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,24 @@ is the case-insensitive word
159159
then the first four colors of the input PNG's embedded palette are used.
160160
It is an error if the PNG is not indexed, or if colors other than these 4 are used.
161161
.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 .
162+
.It Sy DMG palette spec
163+
If
164+
.Ar pal_spec
165+
starts with case-insensitive
166+
.Cm dmg= ,
167+
then the following two-digit hexadecimal number specifies four grayscale DMG color indexes.
168+
The number functions like the DMG's $FF47
169+
.Sy BGP
170+
register
171+
(see
172+
.Lk https://gbdev.io/pandocs/Palettes.html Pan Docs
173+
for more information):
174+
the low two bits 0-1 specify which gray shade goes in color index 0,
175+
the next two bits 2-3 specify which gray shade goes in color index 1,
176+
and so on.
177+
Gray shade 0 is the lightest (white), 3 is the darkest (black).
178+
The same gray shade cannot go in two color indexes.
179+
To specify a DMG palette, the input PNG must have all its colors in shades of gray, without any transparent colors.
162180
.It Sy external palette spec
163181
Otherwise,
164182
.Ar pal_spec
@@ -528,6 +546,8 @@ Otherwise, if the PNG only contains shades of gray, they will be categorized int
528546
.Dq bins
529547
as there are colors per palette, and the palette is set to these bins.
530548
The darkest gray will end up in bin #0, and so on; note that this is the opposite of the RGB method below.
549+
This is equivalent to having specified a DMG palette of
550+
.Fl c Cm dmg=E4 .
531551
If two distinct grays end up in the same bin, the RGB method is used instead.
532552
.Pp
533553
Be careful that

src/gfx/main.cpp

Lines changed: 7 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
#include "extern/getopt.hpp"
1818
#include "file.hpp"
19-
#include "helpers.hpp" // assume
2019
#include "platform.hpp"
2120
#include "version.hpp"
2221

@@ -354,7 +353,6 @@ static char *parseArgv(int argc, char *argv[]) {
354353
for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) {
355354
char *arg = musl_optarg; // Make a copy for scanning
356355
uint16_t number;
357-
size_t size;
358356
switch (ch) {
359357
case 'A':
360358
localOptions.autoAttrmap = true;
@@ -367,41 +365,7 @@ static char *parseArgv(int argc, char *argv[]) {
367365
options.attrmap = musl_optarg;
368366
break;
369367
case 'B':
370-
if (strcasecmp(musl_optarg, "transparent") == 0) {
371-
options.bgColor = Rgba(0x00, 0x00, 0x00, 0x00);
372-
break;
373-
}
374-
if (musl_optarg[0] != '#') {
375-
error("Background color specification must be `#rgb`, `#rrggbb`, or `transparent`");
376-
break;
377-
}
378-
size = strspn(&musl_optarg[1], "0123456789ABCDEFabcdef");
379-
switch (size) {
380-
case 3:
381-
options.bgColor = Rgba(
382-
singleToHex(musl_optarg[1]),
383-
singleToHex(musl_optarg[2]),
384-
singleToHex(musl_optarg[3]),
385-
0xFF
386-
);
387-
break;
388-
case 6:
389-
options.bgColor = Rgba(
390-
toHex(musl_optarg[1], musl_optarg[2]),
391-
toHex(musl_optarg[3], musl_optarg[4]),
392-
toHex(musl_optarg[5], musl_optarg[6]),
393-
0xFF
394-
);
395-
break;
396-
default:
397-
error("Unknown background color specification \"%s\"", musl_optarg);
398-
}
399-
if (musl_optarg[size + 1] != '\0') {
400-
error(
401-
"Unexpected text \"%s\" after background color specification",
402-
&musl_optarg[size + 1]
403-
);
404-
}
368+
parseBackgroundPalSpec(musl_optarg);
405369
break;
406370
case 'b':
407371
number = parseNumber(arg, "Bank 0 base tile ID", 0);
@@ -442,18 +406,18 @@ static char *parseArgv(int argc, char *argv[]) {
442406
options.useColorCurve = true;
443407
break;
444408
case 'c':
409+
localOptions.externalPalSpec = nullptr; // Allow overriding a previous pal spec
445410
if (musl_optarg[0] == '#') {
446411
options.palSpecType = Options::EXPLICIT;
447412
parseInlinePalSpec(musl_optarg);
448413
} else if (strcasecmp(musl_optarg, "embedded") == 0) {
449414
// Use PLTE, error out if missing
450415
options.palSpecType = Options::EMBEDDED;
416+
} else if (strncasecmp(musl_optarg, "dmg=", literal_strlen("dmg=")) == 0) {
417+
options.palSpecType = Options::DMG;
418+
parseDmgPalSpec(&musl_optarg[literal_strlen("dmg=")]);
451419
} else {
452420
options.palSpecType = Options::EXPLICIT;
453-
// Can't parse the file yet, as "flat" color collections need to know the palette
454-
// size to be split; thus, we defer that.
455-
// If a following `-c` overrides a previous one, the `fmt` part of an overridden
456-
// external palette spec will not be validated, but I guess that's okay.
457421
localOptions.externalPalSpec = musl_optarg;
458422
}
459423
break;
@@ -877,6 +841,8 @@ int main(int argc, char *argv[]) {
877841
return "Explicit";
878842
case Options::EMBEDDED:
879843
return "Embedded";
844+
case Options::DMG:
845+
return "DMG";
880846
}
881847
return "???";
882848
}());

src/gfx/pal_packing.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ static void decant(
299299
break;
300300
}
301301
auto attrs = from.begin();
302-
std::advance(attrs, (iter - processed.begin()));
302+
std::advance(attrs, iter - processed.begin());
303303

304304
// Build up the "component"...
305305
colors.clear();

src/gfx/pal_spec.cpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,27 @@ static void skipWhitespace(Str const &str, size_t &pos) {
2828
pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length());
2929
}
3030

31+
static constexpr uint8_t nibble(char c) {
32+
if (c >= 'a') {
33+
assume(c <= 'f');
34+
return c - 'a' + 10;
35+
} else if (c >= 'A') {
36+
assume(c <= 'F');
37+
return c - 'A' + 10;
38+
} else {
39+
assume(c >= '0' && c <= '9');
40+
return c - '0';
41+
}
42+
}
43+
44+
static constexpr uint8_t toHex(char c1, char c2) {
45+
return nibble(c1) * 16 + nibble(c2);
46+
}
47+
48+
static constexpr uint8_t singleToHex(char c) {
49+
return toHex(c, c);
50+
}
51+
3152
void parseInlinePalSpec(char const * const rawArg) {
3253
// List of #rrggbb/#rgb colors (or #none); comma-separated.
3354
// Palettes are separated by colons.
@@ -595,3 +616,61 @@ void parseExternalPalSpec(char const *arg) {
595616

596617
std::get<1> (*iter)(file);
597618
}
619+
620+
void parseDmgPalSpec(char const * const rawArg) {
621+
// Two hex digit DMG palette spec
622+
623+
std::string_view arg(rawArg);
624+
625+
if (arg.length() != 2
626+
|| arg.find_first_not_of("0123456789ABCDEFabcdef"sv) != std::string_view::npos) {
627+
error("Unknown DMG palette specification \"%s\"", rawArg);
628+
return;
629+
}
630+
631+
options.palSpecDmg = toHex(arg[0], arg[1]);
632+
633+
// Map gray shades to their DMG color indexes for fast lookup by `Rgba::grayIndex`
634+
for (uint8_t i = 0; i < 4; ++i) {
635+
options.dmgColors[options.dmgValue(i)] = i;
636+
}
637+
638+
// Validate that DMG palette spec does not have conflicting colors
639+
for (uint8_t i = 0; i < 3; ++i) {
640+
for (uint8_t j = i + 1; j < 4; ++j) {
641+
if (options.dmgValue(i) == options.dmgValue(j)) {
642+
error("DMG palette specification maps two gray shades to the same color index");
643+
return;
644+
}
645+
}
646+
}
647+
}
648+
649+
void parseBackgroundPalSpec(char const *arg) {
650+
if (strcasecmp(arg, "transparent") == 0) {
651+
options.bgColor = Rgba(0x00, 0x00, 0x00, 0x00);
652+
return;
653+
}
654+
655+
if (arg[0] != '#') {
656+
error("Background color specification must be `#rgb`, `#rrggbb`, or `transparent`");
657+
return;
658+
}
659+
660+
size_t size = strspn(&arg[1], "0123456789ABCDEFabcdef");
661+
switch (size) {
662+
case 3:
663+
options.bgColor = Rgba(singleToHex(arg[1]), singleToHex(arg[2]), singleToHex(arg[3]), 0xFF);
664+
break;
665+
case 6:
666+
options.bgColor =
667+
Rgba(toHex(arg[1], arg[2]), toHex(arg[3], arg[4]), toHex(arg[5], arg[6]), 0xFF);
668+
break;
669+
default:
670+
error("Unknown background color specification \"%s\"", arg);
671+
}
672+
673+
if (arg[size + 1] != '\0') {
674+
error("Unexpected text \"%s\" after background color specification", &arg[size + 1]);
675+
}
676+
}

src/gfx/process.cpp

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -586,8 +586,10 @@ static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
586586
}
587587

588588
// "Sort" colors in the generated palettes, see the man page for the flowchart
589-
auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal();
590-
if (embPalRGB != nullptr) {
589+
if (options.palSpecType == Options::DMG) {
590+
sortGrayscale(palettes, png.getColors().raw());
591+
} else if (auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal();
592+
embPalRGB != nullptr) {
591593
sortIndexed(palettes, embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha);
592594
} else if (png.isSuitableForGrayscale()) {
593595
sortGrayscale(palettes, png.getColors().raw());
@@ -1139,6 +1141,18 @@ void process() {
11391141
}
11401142
// LCOV_EXCL_STOP
11411143

1144+
if (options.palSpecType == Options::DMG) {
1145+
if (options.hasTransparentPixels) {
1146+
fatal(
1147+
"Image contains transparent pixels, not compatible with a DMG palette specification"
1148+
);
1149+
}
1150+
if (!png.isSuitableForGrayscale()) {
1151+
fatal("Image contains too many or non-gray colors, not compatible with a DMG palette "
1152+
"specification");
1153+
}
1154+
}
1155+
11421156
// Now, iterate through the tiles, generating proto-palettes as we go
11431157
// We do this unconditionally because this performs the image validation (which we want to
11441158
// perform even if no output is requested), and because it's necessary to generate any
@@ -1248,9 +1262,10 @@ continue_visiting_tiles:;
12481262
if (options.palSpecType == Options::EMBEDDED) {
12491263
generatePalSpec(png);
12501264
}
1251-
auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC
1252-
? generatePalettes(protoPalettes, png)
1253-
: makePalsAsSpecified(protoPalettes);
1265+
auto [mappings, palettes] =
1266+
options.palSpecType == Options::NO_SPEC || options.palSpecType == Options::DMG
1267+
? generatePalettes(protoPalettes, png)
1268+
: makePalsAsSpecified(protoPalettes);
12541269
outputPalettes(palettes);
12551270

12561271
// If deduplication is not happening, we just need to output the tile data and/or maps as-is

src/gfx/reverse.cpp

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,13 @@ void reverse() {
195195
Options::VERB_INTERM, "Reversed image dimensions: %zux%zu tiles\n", width, height
196196
);
197197

198+
Rgba const grayColors[4] = {
199+
Rgba(0xFFFFFFFF), Rgba(0xAAAAAAFF), Rgba(0x555555FF), Rgba(0x000000FF)
200+
};
198201
std::vector<std::array<std::optional<Rgba>, 4>> palettes{
199-
{Rgba(0xFFFFFFFF), Rgba(0xAAAAAAFF), Rgba(0x555555FF), Rgba(0x000000FF)}
202+
{grayColors[0], grayColors[1], grayColors[2], grayColors[3]}
200203
};
201-
// If a palette file is used as input, it overrides the default colors.
204+
// If a palette file or palette spec is used as input, it overrides the default colors.
202205
if (!options.palettes.empty()) {
203206
File file;
204207
if (!file.open(options.palettes, std::ios::in | std::ios::binary)) {
@@ -255,6 +258,10 @@ void reverse() {
255258
putc('\n', stderr);
256259
}
257260
}
261+
} else if (options.palSpecType == Options::DMG) {
262+
for (size_t i = 0; i < palettes[0].size(); ++i) {
263+
palettes[0][i] = grayColors[options.dmgValue(i)];
264+
}
258265
} else if (options.palSpecType == Options::EMBEDDED) {
259266
warning("An embedded palette was requested, but no palette file was specified; ignoring "
260267
"request.");

src/gfx/rgba.cpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,15 @@ uint16_t Rgba::cgbColor() const {
5858

5959
uint8_t Rgba::grayIndex() const {
6060
assume(isGray());
61-
// Convert from 0..<256 to hasTransparentPixels..<nbColorsPerPal
61+
// 2bpp shades are inverted from RGB PNG; %00 = white, %11 = black
62+
uint8_t gray = 255 - red;
63+
if (options.palSpecType == Options::DMG) {
64+
assume(!options.hasTransparentPixels);
65+
// Reduce gray shade from 0..<256 to 0..<4, then map to color index,
66+
// then reduce to 0..<nbColorsPerPal
67+
return options.dmgColors[gray * 4 / 256] * options.nbColorsPerPal / 4;
68+
}
69+
// Reduce gray shade from 0..<256 to hasTransparentPixels..<nbColorsPerPal
6270
// Note that `maxOpaqueColors()` already takes `hasTransparentPixels` into account
63-
return (255 - red) * options.maxOpaqueColors() / 256 + options.hasTransparentPixels;
71+
return gray * options.maxOpaqueColors() / 256 + options.hasTransparentPixels;
6472
}

test/gfx/dmg_1bit_round_trip.1bpp

8 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)