Implement --background-color (#1508)

Co-authored-by: Rangi42 <sylvie.oukaour+rangi42@gmail.com>
This commit is contained in:
Eldred Habert
2025-05-02 05:39:52 +02:00
committed by GitHub
parent 56f7222230
commit 8cf6c5423a
29 changed files with 167 additions and 54 deletions

View File

@@ -19,6 +19,7 @@ _rgbgfx_completions() {
[Z]="columns:normal" [Z]="columns:normal"
[a]="attr-map:glob-*.attrmap" [a]="attr-map:glob-*.attrmap"
[A]="auto-attr-map:normal" [A]="auto-attr-map:normal"
[B]="background-color:unk"
[b]="base-tiles:unk" [b]="base-tiles:unk"
[c]="colors:unk" [c]="colors:unk"
[d]="depth:unk" [d]="depth:unk"

View File

@@ -28,6 +28,7 @@ local args=(
'(-Z --columns)'{-Z,--columns}'[Read the image in column-major order]' '(-Z --columns)'{-Z,--columns}'[Read the image in column-major order]'
'(-a --attr-map -A --auto-attr-map)'{-a,--attr-map}'+[Generate a map of tile attributes (mirroring)]:attrmap file:_files' '(-a --attr-map -A --auto-attr-map)'{-a,--attr-map}'+[Generate a map of tile attributes (mirroring)]:attrmap file:_files'
'(-B --background-color)'{-B,--background-color}'+[Ignore tiles containing only specified color]:color:'
'(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:' '(-b --base-tiles)'{-b,--base-tiles}'+[Base tile IDs for tile map output]:base tile IDs:'
'(-c --colors)'{-c,--colors}'+[Specify color palettes]:palette spec:' '(-c --colors)'{-c,--colors}'+[Specify color palettes]:palette spec:'
'(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths' '(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths'

View File

@@ -10,6 +10,8 @@
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "helpers.hpp"
#include "gfx/rgba.hpp" #include "gfx/rgba.hpp"
struct Options { struct Options {
@@ -21,6 +23,7 @@ struct Options {
uint8_t verbosity = 0; // -v uint8_t verbosity = 0; // -v
std::string attrmap{}; // -a, -A std::string attrmap{}; // -a, -A
std::optional<Rgba> bgColor{}; // -B
std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
enum { enum {
NO_SPEC, NO_SPEC,
@@ -116,4 +119,27 @@ static constexpr auto flipTable = ([]() constexpr {
return table; 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 #endif // RGBDS_GFX_MAIN_HPP

View File

@@ -103,6 +103,19 @@ and has the same size.
Same as Same as
.Fl a Ar base_path Ns .attrmap .Fl a Ar base_path Ns .attrmap
.Pq see Sx Automatic output paths . .Pq see Sx Automatic output paths .
.It Fl B Ar color , Fl \-background-color Ar color
Set a background color to be omitted from output.
Colors are accepted in
.Ql #rgb
or
.Ql #rrggbb
format, or as
.Ql transparent .
Input tiles which are entirely the specified background color are ignored and will not be output in tile data file.
The tilemap, atrribute map, or palette map files
.Em will
use placeholder values where background tiles were.
If a background color is specified, it cannot be used within tiles which are not ignored.
.It Fl b Ar base_ids , Fl \-base-tiles Ar base_ids .It Fl b Ar base_ids , Fl \-base-tiles Ar base_ids
Set the base IDs for tile map output. Set the base IDs for tile map output.
.Ar base_ids .Ar base_ids
@@ -126,7 +139,7 @@ begins with a hash character
.Ql # , .Ql # ,
it is treated as an inline palette specification. it is treated as an inline palette specification.
It should contain a comma-separated list of hexadecimal colors, each beginning with a hash. It should contain a comma-separated list of hexadecimal colors, each beginning with a hash.
Colors are accepted either as Colors are accepted in
.Ql #rgb .Ql #rgb
or or
.Ql #rrggbb .Ql #rrggbb

View File

@@ -114,7 +114,7 @@ void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
} }
// Short options // Short options
static char const *optstring = "-Aa:b:Cc:d:hi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ"; static char const *optstring = "-Aa:B:b:Cc:d:hi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ";
// Equivalent long options // Equivalent long options
// Please keep in the same order as short opts. // Please keep in the same order as short opts.
@@ -126,6 +126,7 @@ static char const *optstring = "-Aa:b:Cc:d:hi:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ"
static option const longopts[] = { static option const longopts[] = {
{"auto-attr-map", no_argument, nullptr, 'A'}, {"auto-attr-map", no_argument, nullptr, 'A'},
{"attr-map", required_argument, nullptr, 'a'}, {"attr-map", required_argument, nullptr, 'a'},
{"background-color", required_argument, nullptr, 'B'},
{"base-tiles", required_argument, nullptr, 'b'}, {"base-tiles", required_argument, nullptr, 'b'},
{"color-curve", no_argument, nullptr, 'C'}, {"color-curve", no_argument, nullptr, 'C'},
{"colors", required_argument, nullptr, 'c'}, {"colors", required_argument, nullptr, 'c'},
@@ -353,6 +354,7 @@ static char *parseArgv(int argc, char *argv[]) {
for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) { for (int ch; (ch = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1;) {
char *arg = musl_optarg; // Make a copy for scanning char *arg = musl_optarg; // Make a copy for scanning
uint16_t number; uint16_t number;
size_t size;
switch (ch) { switch (ch) {
case 'A': case 'A':
localOptions.autoAttrmap = true; localOptions.autoAttrmap = true;
@@ -364,6 +366,43 @@ static char *parseArgv(int argc, char *argv[]) {
} }
options.attrmap = musl_optarg; options.attrmap = musl_optarg;
break; 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]
);
}
break;
case 'b': case 'b':
number = parseNumber(arg, "Bank 0 base tile ID", 0); number = parseNumber(arg, "Bank 0 base tile ID", 0);
if (number >= 256) { if (number >= 256) {

View File

@@ -23,27 +23,6 @@
using namespace std::string_view_literals; using namespace std::string_view_literals;
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';
}
}
constexpr uint8_t toHex(char c1, char c2) {
return nibble(c1) * 16 + nibble(c2);
}
constexpr uint8_t singleToHex(char c) {
return toHex(c, c);
}
template<typename Str> // Should be std::string or std::string_view template<typename Str> // Should be std::string or std::string_view
static void skipWhitespace(Str const &str, size_t &pos) { static void skipWhitespace(Str const &str, size_t &pos) {
pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length()); pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length());

View File

@@ -25,6 +25,10 @@
#include "gfx/pal_sorting.hpp" #include "gfx/pal_sorting.hpp"
#include "gfx/proto_palette.hpp" #include "gfx/proto_palette.hpp"
static bool isBgColorTransparent() {
return options.bgColor.has_value() && options.bgColor->isTransparent();
}
class ImagePalette { class ImagePalette {
std::array<std::optional<Rgba>, NB_COLOR_SLOTS> _colors; std::array<std::optional<Rgba>, NB_COLOR_SLOTS> _colors;
@@ -38,7 +42,7 @@ public:
Rgba const *registerColor(Rgba const &rgba) { Rgba const *registerColor(Rgba const &rgba) {
std::optional<Rgba> &slot = _colors[rgba.cgbColor()]; std::optional<Rgba> &slot = _colors[rgba.cgbColor()];
if (rgba.cgbColor() == Rgba::transparent) { if (rgba.cgbColor() == Rgba::transparent && !isBgColorTransparent()) {
options.hasTransparentPixels = true; options.hasTransparentPixels = true;
} }
@@ -519,10 +523,12 @@ struct AttrmapEntry {
bool yFlip; bool yFlip;
bool xFlip; bool xFlip;
static constexpr decltype(protoPaletteID) transparent = SIZE_MAX; static constexpr size_t transparent = static_cast<size_t>(-1);
static constexpr size_t background = static_cast<size_t>(-2);
bool isBackgroundTile() const { return protoPaletteID == background; }
size_t getPalID(DefaultInitVec<size_t> const &mappings) const { size_t getPalID(DefaultInitVec<size_t> const &mappings) const {
return protoPaletteID == transparent ? 0 : mappings[protoPaletteID]; return mappings[isBackgroundTile() || protoPaletteID == transparent ? 0 : protoPaletteID];
} }
}; };
@@ -852,7 +858,9 @@ static void outputUnoptimizedTileData(
remainingTiles -= options.trim; remainingTiles -= options.trim;
for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) { for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) {
// If the tile is fully transparent, default to palette 0 // Do not emit fully-background tiles.
if (!attr.isBackgroundTile()) {
// If the tile is fully transparent, this defaults to palette 0.
Palette const &palette = palettes[attr.getPalID(mappings)]; Palette const &palette = palettes[attr.getPalID(mappings)];
for (uint32_t y = 0; y < 8; ++y) { for (uint32_t y = 0; y < 8; ++y) {
uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y); uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y);
@@ -861,6 +869,7 @@ static void outputUnoptimizedTileData(
output->sputc(bitplanes >> 8); output->sputc(bitplanes >> 8);
} }
} }
}
--remainingTiles; --remainingTiles;
if (remainingTiles == 0) { if (remainingTiles == 0) {
@@ -898,18 +907,23 @@ static void outputUnoptimizedMaps(
} }
if (tilemapOutput.has_value()) { if (tilemapOutput.has_value()) {
(*tilemapOutput)->sputc(tileID + options.baseTileIDs[bank]); (*tilemapOutput)
->sputc((attr.isBackgroundTile() ? 0 : tileID) + options.baseTileIDs[bank]);
} }
uint8_t palID = attr.getPalID(mappings);
if (attrmapOutput.has_value()) { if (attrmapOutput.has_value()) {
uint8_t palID = attr.getPalID(mappings) & 7; (*attrmapOutput)->sputc((palID & 7) | bank << 3); // The other flags are all 0
(*attrmapOutput)->sputc(palID | bank << 3); // The other flags are all 0
} }
if (palmapOutput.has_value()) { if (palmapOutput.has_value()) {
(*palmapOutput)->sputc(attr.getPalID(mappings)); (*palmapOutput)->sputc(palID);
} }
// Background tiles are skipped in the tile data, so they should be skipped in the maps too.
if (!attr.isBackgroundTile()) {
++tileID; ++tileID;
} }
} }
}
struct UniqueTiles { struct UniqueTiles {
std::unordered_set<TileData> tileset; std::unordered_set<TileData> tileset;
@@ -1000,7 +1014,14 @@ static UniqueTiles dedupTiles(
bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty(); bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty();
for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) { for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) {
auto [tileID, matchType] = tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]}); if (attr.isBackgroundTile()) {
attr.xFlip = false;
attr.yFlip = false;
attr.bank = 0;
attr.tileID = 0;
} else {
auto [tileID, matchType] =
tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]});
if (inputWithoutOutput && matchType == TileData::NOPE) { if (inputWithoutOutput && matchType == TileData::NOPE) {
error( error(
@@ -1014,8 +1035,9 @@ static UniqueTiles dedupTiles(
attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP; attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP; attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
attr.bank = tileID >= options.maxNbTiles[0]; attr.bank = tileID >= options.maxNbTiles[0];
attr.tileID = attr.tileID = (attr.bank ? tileID - options.maxNbTiles[0] : tileID)
(attr.bank ? tileID - options.maxNbTiles[0] : tileID) + options.baseTileIDs[attr.bank]; + options.baseTileIDs[attr.bank];
}
} }
// Copy elision should prevent the contained `unordered_set` from being re-constructed // Copy elision should prevent the contained `unordered_set` from being re-constructed
@@ -1139,7 +1161,8 @@ void process() {
std::unordered_set<uint16_t> tileColors; std::unordered_set<uint16_t> tileColors;
for (uint32_t y = 0; y < 8; ++y) { for (uint32_t y = 0; y < 8; ++y) {
for (uint32_t x = 0; x < 8; ++x) { for (uint32_t x = 0; x < 8; ++x) {
if (Rgba color = tile.pixel(x, y); !color.isTransparent()) { if (Rgba color = tile.pixel(x, y);
!color.isTransparent() || !options.hasTransparentPixels) {
tileColors.insert(color.cgbColor()); tileColors.insert(color.cgbColor());
} }
} }
@@ -1157,6 +1180,7 @@ void process() {
if (tileColors.empty()) { if (tileColors.empty()) {
// "Empty" proto-palettes screw with the packing process, so discard those // "Empty" proto-palettes screw with the packing process, so discard those
assume(!isBgColorTransparent());
attrs.protoPaletteID = AttrmapEntry::transparent; attrs.protoPaletteID = AttrmapEntry::transparent;
continue; continue;
} }
@@ -1166,6 +1190,21 @@ void process() {
protoPalette.add(cgbColor); protoPalette.add(cgbColor);
} }
if (options.bgColor.has_value()
&& std::find(RANGE(tileColors), options.bgColor->cgbColor()) != tileColors.end()) {
if (tileColors.size() == 1) {
// The tile contains just the background color, skip it.
attrs.protoPaletteID = AttrmapEntry::background;
continue;
}
fatal(
"Tile (%" PRIu32 ", %" PRIu32 ") contains the background color (#%08x)!",
tile.x,
tile.y,
options.bgColor->toCSS()
);
}
// Insert the proto-palette, making sure to avoid overlaps // Insert the proto-palette, making sure to avoid overlaps
for (size_t n = 0; n < protoPalettes.size(); ++n) { for (size_t n = 0; n < protoPalettes.size(); ++n) {
switch (protoPalette.compare(protoPalettes[n])) { switch (protoPalette.compare(protoPalettes[n])) {
@@ -1197,7 +1236,7 @@ void process() {
} }
attrs.protoPaletteID = protoPalettes.size(); attrs.protoPaletteID = protoPalettes.size();
if (protoPalettes.size() == AttrmapEntry::transparent) { // Check for overflow if (protoPalettes.size() == AttrmapEntry::background) { // Check for overflow
fatal( fatal(
"Reached %zu proto-palettes... sorry, this image is too much for me to handle :(", "Reached %zu proto-palettes... sorry, this image is too much for me to handle :(",
AttrmapEntry::transparent AttrmapEntry::transparent

3
test/gfx/bg_fuse.err Normal file
View File

@@ -0,0 +1,3 @@
warning: Fusing colors #a9b9c9ff and #aabbccff into Game Boy color $66f5 [first seen at x: 1, y: 1]
FATAL: Tile (0, 0) contains the background color (#aabbccff)!
Conversion aborted after 1 error

1
test/gfx/bg_fuse.flags Normal file
View File

@@ -0,0 +1 @@
-B #aBc

BIN
test/gfx/bg_fuse.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

2
test/gfx/bg_in_tile.err Normal file
View File

@@ -0,0 +1,2 @@
FATAL: Tile (16, 0) contains the background color (#ffd800ff)!
Conversion aborted after 1 error

View File

@@ -0,0 +1 @@
-B #ffd800

BIN
test/gfx/bg_in_tile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

1
test/gfx/bg_oval.flags Normal file
View File

@@ -0,0 +1 @@
-B #FF00ff

BIN
test/gfx/bg_oval.out.2bpp Normal file

Binary file not shown.

Binary file not shown.

BIN
test/gfx/bg_oval.out.pal Normal file

Binary file not shown.

Binary file not shown.

BIN
test/gfx/bg_oval.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 B

View File

@@ -0,0 +1,4 @@
-B #fff
-i input_tileset_with_bg.in.2bpp
-c gbc:input_tileset_with_bg.in.pal
-u

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

View File

@@ -0,0 +1,2 @@
FATAL: Tile (16, 0) contains the background color (#00000000)!
Conversion aborted after 1 error

View File

@@ -0,0 +1 @@
-B transparent

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B