Skip to content

Implement --background-color #1508

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
May 2, 2025
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contrib/bash_compl/_rgbgfx.bash
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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"
Expand Down
1 change: 1 addition & 0 deletions contrib/zsh_compl/_rgbgfx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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'
Expand Down
26 changes: 26 additions & 0 deletions include/gfx/main.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
#include <utility>
#include <vector>

#include "helpers.hpp"

#include "gfx/rgba.hpp"

struct Options {
Expand All @@ -21,6 +23,7 @@ struct Options {
uint8_t verbosity = 0; // -v

std::string attrmap{}; // -a, -A
std::optional<Rgba> bgColor{}; // -B
std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
enum {
NO_SPEC,
Expand Down Expand Up @@ -116,4 +119,27 @@ 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
13 changes: 13 additions & 0 deletions man/rgbgfx.1
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,19 @@ 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
Set a background color to be omitted from output.
Colors are accepted as
.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
Set the base IDs for tile map output.
.Ar base_ids
Expand Down
41 changes: 40 additions & 1 deletion src/gfx/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
}

// 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
// Please keep in the same order as short opts.
Expand All @@ -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[] = {
{"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'},
Expand Down Expand Up @@ -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;) {
char *arg = musl_optarg; // Make a copy for scanning
uint16_t number;
size_t size;
switch (ch) {
case 'A':
localOptions.autoAttrmap = true;
Expand All @@ -364,6 +366,43 @@ static char *parseArgv(int argc, char *argv[]) {
}
options.attrmap = musl_optarg;
break;
case 'B':
if (!strcmp(musl_optarg, "transparent") || !strcmp(musl_optarg, "TRANSPARENT")) {
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':
number = parseNumber(arg, "Bank 0 base tile ID", 0);
if (number >= 256) {
Expand Down
21 changes: 0 additions & 21 deletions src/gfx/pal_spec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename Str> // Should be std::string or std::string_view
static void skipWhitespace(Str const &str, size_t &pos) {
pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length());
Expand Down
101 changes: 70 additions & 31 deletions src/gfx/process.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
#include "gfx/pal_sorting.hpp"
#include "gfx/proto_palette.hpp"

static bool isBgColorTransparent() {
return options.bgColor.has_value() && options.bgColor->isTransparent();
}

class ImagePalette {
std::array<std::optional<Rgba>, NB_COLOR_SLOTS> _colors;

Expand All @@ -38,7 +42,7 @@ class ImagePalette {
Rgba const *registerColor(Rgba const &rgba) {
std::optional<Rgba> &slot = _colors[rgba.cgbColor()];

if (rgba.cgbColor() == Rgba::transparent) {
if (rgba.cgbColor() == Rgba::transparent && !isBgColorTransparent()) {
options.hasTransparentPixels = true;
}

Expand Down Expand Up @@ -519,10 +523,12 @@ struct AttrmapEntry {
bool yFlip;
bool xFlip;

static constexpr decltype(protoPaletteID) transparent = SIZE_MAX;
static constexpr size_t transparent = SIZE_MAX;
static constexpr size_t background = transparent - 1;

bool isBackgroundTile() const { return protoPaletteID == background; }
size_t getPalID(DefaultInitVec<size_t> const &mappings) const {
return protoPaletteID == transparent ? 0 : mappings[protoPaletteID];
return mappings[isBackgroundTile() || protoPaletteID == transparent ? 0 : protoPaletteID];
}
};

Expand Down Expand Up @@ -852,13 +858,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);
}
}
}

Expand Down Expand Up @@ -898,16 +907,21 @@ static void outputUnoptimizedMaps(
}

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()) {
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;
}
}

Expand Down Expand Up @@ -1000,22 +1014,30 @@ static UniqueTiles dedupTiles(

bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty();
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) {
error(
"Tile at (%" PRIu32 ", %" PRIu32
") is not within the input tileset, and `-o` was not given!",
tile.x,
tile.y
);
}
if (inputWithoutOutput && matchType == TileData::NOPE) {
error(
"Tile at (%" PRIu32 ", %" PRIu32
") is not within the input tileset, and `-o` was not given!",
tile.x,
tile.y
);
}

attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
attr.bank = tileID >= options.maxNbTiles[0];
attr.tileID =
(attr.bank ? tileID - options.maxNbTiles[0] : tileID) + options.baseTileIDs[attr.bank];
attr.xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
attr.yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
attr.bank = tileID >= options.maxNbTiles[0];
attr.tileID = (attr.bank ? tileID - options.maxNbTiles[0] : tileID)
+ options.baseTileIDs[attr.bank];
}
}

// Copy elision should prevent the contained `unordered_set` from being re-constructed
Expand Down Expand Up @@ -1139,7 +1161,8 @@ void process() {
std::unordered_set<uint16_t> tileColors;
for (uint32_t y = 0; y < 8; ++y) {
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());
}
}
Expand All @@ -1157,6 +1180,7 @@ void process() {

if (tileColors.empty()) {
// "Empty" proto-palettes screw with the packing process, so discard those
assume(!isBgColorTransparent());
attrs.protoPaletteID = AttrmapEntry::transparent;
continue;
}
Expand All @@ -1166,6 +1190,21 @@ void process() {
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
for (size_t n = 0; n < protoPalettes.size(); ++n) {
switch (protoPalette.compare(protoPalettes[n])) {
Expand Down Expand Up @@ -1197,7 +1236,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
Expand Down
3 changes: 3 additions & 0 deletions test/gfx/bg_fuse.err
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions test/gfx/bg_fuse.flags
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-B #aBc
Binary file added test/gfx/bg_fuse.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions test/gfx/bg_in_tile.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FATAL: Tile (16, 0) contains the background color (#ffd800ff)!
Conversion aborted after 1 error
1 change: 1 addition & 0 deletions test/gfx/bg_in_tile.flags
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-B #ffd800
Binary file added test/gfx/bg_in_tile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/gfx/bg_oval.flags
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-B #FF00ff
Binary file added test/gfx/bg_oval.out.2bpp
Binary file not shown.
Binary file added test/gfx/bg_oval.out.attrmap
Binary file not shown.
Binary file added test/gfx/bg_oval.out.pal
Binary file not shown.
Binary file added test/gfx/bg_oval.out.tilemap
Binary file not shown.
Binary file added test/gfx/bg_oval.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions test/gfx/input_tileset_with_bg.flags
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-B #fff
-i input_tileset_with_bg.in.2bpp
-c gbc:input_tileset_with_bg.in.pal
-u
Binary file added test/gfx/input_tileset_with_bg.in.2bpp
Binary file not shown.
Binary file added test/gfx/input_tileset_with_bg.in.pal
Binary file not shown.
Binary file added test/gfx/input_tileset_with_bg.out.2bpp
Binary file not shown.
Binary file added test/gfx/input_tileset_with_bg.out.attrmap
Binary file not shown.
Binary file added test/gfx/input_tileset_with_bg.out.tilemap
Binary file not shown.
Binary file added test/gfx/input_tileset_with_bg.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions test/gfx/trans_bg_in_tile.err
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FATAL: Tile (16, 0) contains the background color (#00000000)!
Conversion aborted after 1 error
1 change: 1 addition & 0 deletions test/gfx/trans_bg_in_tile.flags
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-B transparent
Binary file added test/gfx/trans_bg_in_tile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading