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:
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
// 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user