mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 10:12:06 +00:00
Implement grayscale DMG palette specs (#1709)
This commit is contained in:
@@ -29,8 +29,10 @@ struct Options {
|
||||
NO_SPEC,
|
||||
EXPLICIT,
|
||||
EMBEDDED,
|
||||
DMG,
|
||||
} palSpecType = NO_SPEC; // -c
|
||||
std::vector<std::array<std::optional<Rgba>, 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
|
||||
|
||||
@@ -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
|
||||
|
||||
20
man/rgbgfx.1
20
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
|
||||
|
||||
@@ -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 "???";
|
||||
}());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,7 +1262,8 @@ continue_visiting_tiles:;
|
||||
if (options.palSpecType == Options::EMBEDDED) {
|
||||
generatePalSpec(png);
|
||||
}
|
||||
auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC
|
||||
auto [mappings, palettes] =
|
||||
options.palSpecType == Options::NO_SPEC || options.palSpecType == Options::DMG
|
||||
? generatePalettes(protoPalettes, png)
|
||||
: makePalsAsSpecified(protoPalettes);
|
||||
outputPalettes(palettes);
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -58,7 +58,15 @@ uint16_t Rgba::cgbColor() const {
|
||||
|
||||
uint8_t Rgba::grayIndex() const {
|
||||
assume(isGray());
|
||||
// Convert from 0..<256 to hasTransparentPixels..<nbColorsPerPal
|
||||
// Note that `maxOpaqueColors()` already takes `hasTransparentPixels` into account
|
||||
return (255 - red) * options.maxOpaqueColors() / 256 + options.hasTransparentPixels;
|
||||
// 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 gray * options.maxOpaqueColors() / 256 + options.hasTransparentPixels;
|
||||
}
|
||||
|
||||
BIN
test/gfx/dmg_1bit_round_trip.1bpp
Normal file
BIN
test/gfx/dmg_1bit_round_trip.1bpp
Normal file
Binary file not shown.
2
test/gfx/dmg_1bit_round_trip.flags
Normal file
2
test/gfx/dmg_1bit_round_trip.flags
Normal file
@@ -0,0 +1,2 @@
|
||||
-d 1
|
||||
-c dmg=1b
|
||||
2
test/gfx/dmg_2bit.flags
Normal file
2
test/gfx/dmg_2bit.flags
Normal file
@@ -0,0 +1,2 @@
|
||||
-c nonexistent.gpl
|
||||
-c DMG=E4
|
||||
BIN
test/gfx/dmg_2bit.out.2bpp
Normal file
BIN
test/gfx/dmg_2bit.out.2bpp
Normal file
Binary file not shown.
BIN
test/gfx/dmg_2bit.out.pal
Normal file
BIN
test/gfx/dmg_2bit.out.pal
Normal file
Binary file not shown.
BIN
test/gfx/dmg_2bit.png
Normal file
BIN
test/gfx/dmg_2bit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 B |
2
test/gfx/dmg_plte.flags
Normal file
2
test/gfx/dmg_plte.flags
Normal file
@@ -0,0 +1,2 @@
|
||||
-c embedded
|
||||
-c DMG=E4
|
||||
BIN
test/gfx/dmg_plte.out.2bpp
Normal file
BIN
test/gfx/dmg_plte.out.2bpp
Normal file
Binary file not shown.
BIN
test/gfx/dmg_plte.out.pal
Normal file
BIN
test/gfx/dmg_plte.out.pal
Normal file
Binary file not shown.
BIN
test/gfx/dmg_plte.png
Normal file
BIN
test/gfx/dmg_plte.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 118 B |
BIN
test/gfx/dmg_round_trip.2bpp
Normal file
BIN
test/gfx/dmg_round_trip.2bpp
Normal file
Binary file not shown.
1
test/gfx/dmg_round_trip.flags
Normal file
1
test/gfx/dmg_round_trip.flags
Normal file
@@ -0,0 +1 @@
|
||||
-c dmg=72
|
||||
Reference in New Issue
Block a user