diff --git a/contrib/bash_compl/_rgbgfx.bash b/contrib/bash_compl/_rgbgfx.bash index 9ac6a2724..70a05e8b2 100755 --- a/contrib/bash_compl/_rgbgfx.bash +++ b/contrib/bash_compl/_rgbgfx.bash @@ -18,6 +18,7 @@ _rgbgfx_completions() { [Z]="columns:normal" [a]="attr-map:glob-*.attrmap" [A]="auto-attr-map:normal" + [B]="background-color:unk" [b]="base-tiles:unk" [c]="colors:unk" [d]="depth:unk" diff --git a/contrib/zsh_compl/_rgbgfx b/contrib/zsh_compl/_rgbgfx index b64aa7af9..b2512d2e1 100644 --- a/contrib/zsh_compl/_rgbgfx +++ b/contrib/zsh_compl/_rgbgfx @@ -27,6 +27,7 @@ local args=( '(-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' + '(-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:' '(-c --colors)'{-c,--colors}'+[Specify color palettes]:palette spec:' '(-d --depth)'{-d,--depth}'+[Set bit depth]:bit depth:_depths' diff --git a/include/gfx/main.hpp b/include/gfx/main.hpp index c3c7af6f1..9fc8be25e 100644 --- a/include/gfx/main.hpp +++ b/include/gfx/main.hpp @@ -10,6 +10,8 @@ #include #include +#include "helpers.hpp" + #include "gfx/rgba.hpp" struct Options { @@ -20,7 +22,9 @@ struct Options { bool columnMajor = false; // -Z uint8_t verbosity = 0; // -v - std::string attrmap{}; // -a, -A + std::string attrmap{}; // -a, -A + std::optional bgColor{}; // -B + bool bgColorStrict; // If true, warns when the `bgColor` is ever not alone in a tile. std::array baseTileIDs{0, 0}; // -b enum { NO_SPEC, @@ -121,4 +125,27 @@ static constexpr auto flipTable = ([]() constexpr { return table; })(); +// Parsing helpers. + +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); +} + #endif // RGBDS_GFX_MAIN_HPP diff --git a/man/rgbgfx.1 b/man/rgbgfx.1 index 92db2d55d..97e8496e3 100644 --- a/man/rgbgfx.1 +++ b/man/rgbgfx.1 @@ -103,6 +103,8 @@ and has the same size. Same as .Fl a Ar base_path Ns .attrmap .Pq see Sx Automatic output paths . +.It Fl B Ar color , Fl \-background-color Ar color +TODO .It Fl b Ar base_ids , Fl \-base-tiles Ar base_ids Set the base IDs for tile map output. .Ar base_ids diff --git a/src/gfx/main.cpp b/src/gfx/main.cpp index 438ad8eff..f72720cd8 100644 --- a/src/gfx/main.cpp +++ b/src/gfx/main.cpp @@ -123,6 +123,7 @@ static char const *optstring = "-Aa:b:Cc:d:i:L:mN:n:Oo:Pp:Qq:r:s:Tt:U:uVvXx:YZ"; static option const longopts[] = { {"auto-attr-map", no_argument, nullptr, 'A'}, {"attr-map", required_argument, nullptr, 'a'}, + {"background-color", required_argument, nullptr, 'B'}, {"base-tiles", required_argument, nullptr, 'b'}, {"color-curve", no_argument, nullptr, 'C'}, {"colors", required_argument, nullptr, 'c'}, @@ -367,6 +368,43 @@ static char *parseArgv(int argc, char *argv[]) { warning("Overriding attrmap file %s", options.attrmap.c_str()); options.attrmap = musl_optarg; break; + case 'B': + if (musl_optarg[0] != '#' || musl_optarg[1] == '\0') { + error("Background color specification must be either `#rgb` or `#rrggbb`"); + } else { + size_t colorLen = strspn(&musl_optarg[1], "0123456789ABCDEFabcdef"); + switch (colorLen) { + 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); + } + + options.bgColorStrict = true; + if (musl_optarg[colorLen + 1] == '!') { + options.bgColorStrict = false; + ++colorLen; + } + + if (musl_optarg[colorLen + 1] != '\0') { + error("Unexpected text \"%s\" after background color specification", &musl_optarg[colorLen + 1]); + } + } + break; case 'b': number = parseNumber(arg, "Bank 0 base tile ID", 0); if (number >= 256) { diff --git a/src/gfx/pal_spec.cpp b/src/gfx/pal_spec.cpp index d36b272f4..c1e75fc29 100644 --- a/src/gfx/pal_spec.cpp +++ b/src/gfx/pal_spec.cpp @@ -23,27 +23,6 @@ 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 // Should be std::string or std::string_view static void skipWhitespace(Str const &str, typename Str::size_type &pos) { pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length()); diff --git a/src/gfx/process.cpp b/src/gfx/process.cpp index 9bcb64e59..1db89fded 100644 --- a/src/gfx/process.cpp +++ b/src/gfx/process.cpp @@ -528,9 +528,11 @@ struct AttrmapEntry { bool xFlip; static constexpr decltype(protoPaletteID) transparent = SIZE_MAX; + static constexpr decltype(protoPaletteID) background = transparent - 1; + bool isBackgroundTile() const { return protoPaletteID == background; } size_t getPalID(DefaultInitVec const &mappings) const { - return protoPaletteID == transparent ? 0 : mappings[protoPaletteID]; + return isBackgroundTile() ? 0xFF : mappings[protoPaletteID == transparent ? 0 : protoPaletteID]; } }; @@ -851,13 +853,16 @@ static void outputUnoptimizedTileData( remainingTiles -= options.trim; for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) { - // If the tile is fully transparent, default to palette 0 - Palette const &palette = palettes[attr.getPalID(mappings)]; - for (uint32_t y = 0; y < 8; ++y) { - uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y); - output->sputc(bitplanes & 0xFF); - if (options.bitDepth == 2) { - output->sputc(bitplanes >> 8); + // 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)]; + for (uint32_t y = 0; y < 8; ++y) { + uint16_t bitplanes = TileData::rowBitplanes(tile, palette, y); + output->sputc(bitplanes & 0xFF); + if (options.bitDepth == 2) { + output->sputc(bitplanes >> 8); + } } } @@ -897,14 +902,18 @@ static void outputUnoptimizedMaps( if (tilemapOutput.has_value()) { (*tilemapOutput)->sputc(tileID + options.baseTileIDs[bank]); } + uint8_t palID = attr.getPalID(mappings); if (attrmapOutput.has_value()) { - uint8_t palID = attr.getPalID(mappings) & 7; - (*attrmapOutput)->sputc(palID | bank << 3); // The other flags are all 0 + (*attrmapOutput)->sputc((palID & 7) | bank << 3); // The other flags are all 0 } 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; } } @@ -1000,7 +1009,7 @@ static UniqueTiles dedupTiles( } for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) { - auto [tileID, matchType] = tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]}); + auto [tileID, matchType] = attr.isBackgroundTile() ? std::tuple{uint16_t(0), TileData::EXACT} : tiles.addTile({tile, palettes[mappings[attr.protoPaletteID]]}); if (matchType == TileData::NOPE && options.output.empty()) { error( @@ -1121,6 +1130,10 @@ void process() { // output (with the exception of an un-duplicated tilemap, but that's an acceptable loss.) std::vector protoPalettes; DefaultInitVec attrmap{}; + ProtoPalette bgPal; + if (options.bgColor.has_value()) { + bgPal.add(options.bgColor->cgbColor()); + } for (auto tile : png.visitAsTiles()) { AttrmapEntry &attrs = attrmap.emplace_back(); @@ -1156,6 +1169,28 @@ void process() { protoPalette.add(cgbColor); } + if (options.bgColor.has_value()) { + switch (protoPalette.compare(bgPal)) { + case ProtoPalette::THEY_BIGGER: // Note that ties are resolved as `THEY_BIGGER`. + // The tile contains just the background color, skip it. + attrs.protoPaletteID = AttrmapEntry::background; + continue; + case ProtoPalette::WE_BIGGER: + if (options.bgColorStrict) { + warning( + "Tile (%" PRIu32 ", %" PRIu32 ") contains the background color (#%06" PRIx32 + ")!", + tile.x, + tile.y, + options.bgColor->toCSS() >> 8 + ); + } + break; + case ProtoPalette::NEITHER: + break; + } + } + // Insert the proto-palette, making sure to avoid overlaps for (size_t n = 0; n < protoPalettes.size(); ++n) { switch (protoPalette.compare(protoPalettes[n])) { @@ -1188,7 +1223,7 @@ void process() { } attrs.protoPaletteID = protoPalettes.size(); - if (protoPalettes.size() == AttrmapEntry::transparent) { // Check for overflow + if (protoPalettes.size() == AttrmapEntry::background) { // Check for overflow fatal( "Reached %zu proto-palettes... sorry, this image is too much for me to handle :(", AttrmapEntry::transparent