Implement preliminary version of "reverse" feature

Not hooked to all RGBGFX flags yet, but good enough for most use cases
(and as a base for future development, should I need to `reset --hard`.)

TODOs marked appropriately.
This commit is contained in:
ISSOtm
2022-03-27 20:21:00 +02:00
committed by Eldred Habert
parent 843022772b
commit cc27169ecd
9 changed files with 428 additions and 18 deletions

View File

@@ -110,6 +110,7 @@ rgbgfx_obj := \
src/gfx/pal_sorting.o \ src/gfx/pal_sorting.o \
src/gfx/process.o \ src/gfx/process.o \
src/gfx/proto_palette.o \ src/gfx/proto_palette.o \
src/gfx/reverse.o \
src/gfx/rgba.o \ src/gfx/rgba.o \
src/extern/getopt.o \ src/extern/getopt.o \
src/error.o src/error.o

View File

@@ -20,6 +20,9 @@
#include "gfx/rgba.hpp" #include "gfx/rgba.hpp"
struct Options { struct Options {
uint8_t reversedWidth = 0; // -r, in pixels
bool reverse() const { return reversedWidth != 0; }
bool useColorCurve = false; // -C bool useColorCurve = false; // -C
bool fixInput = false; // -f bool fixInput = false; // -f
bool allowMirroring = false; // -m bool allowMirroring = false; // -m
@@ -36,7 +39,7 @@ struct Options {
} palSpecType = NO_SPEC; // -c } palSpecType = NO_SPEC; // -c
std::vector<std::array<Rgba, 4>> palSpec{}; std::vector<std::array<Rgba, 4>> palSpec{};
uint8_t bitDepth = 2; // -d uint8_t bitDepth = 2; // -d
std::array<uint32_t, 4> inputSlice{0, 0, 0, 0}; // -L std::array<uint32_t, 4> inputSlice{0, 0, 0, 0}; // -L (margins in clockwise order, like CSS)
std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // -N std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // -N
uint8_t nbPalettes = 8; // -n uint8_t nbPalettes = 8; // -n
std::string output{}; // -o std::string output{}; // -o
@@ -84,4 +87,12 @@ struct Palette {
uint8_t size() const; uint8_t size() const;
}; };
static constexpr uint8_t flip(uint8_t byte) {
// To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
byte = (byte & 0b0000'1111) << 4 | (byte & 0b1111'0000) >> 4;
byte = (byte & 0b0011'0011) << 2 | (byte & 0b1100'1100) >> 2;
byte = (byte & 0b0101'0101) << 1 | (byte & 0b1010'1010) >> 1;
return byte;
}
#endif /* RGBDS_GFX_MAIN_HPP */ #endif /* RGBDS_GFX_MAIN_HPP */

14
include/gfx/reverse.hpp Normal file
View File

@@ -0,0 +1,14 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#ifndef RGBDS_GFX_REVERSE_HPP
#define RGBDS_GFX_REVERSE_HPP
void reverse();
#endif /* RGBDS_GFX_REVERSE_HPP */

View File

@@ -17,13 +17,23 @@ struct Rgba {
uint8_t blue; uint8_t blue;
uint8_t alpha; uint8_t alpha;
Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {} constexpr Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
: red(r), green(g), blue(b), alpha(a) {}
/** /**
* Constructs the color from a "packed" RGBA representation (0xRRGGBBAA) * Constructs the color from a "packed" RGBA representation (0xRRGGBBAA)
*/ */
explicit Rgba(uint32_t rgba = 0) explicit constexpr Rgba(uint32_t rgba = 0)
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {} : red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
static constexpr Rgba fromCGBColor(uint16_t cgbColor) {
constexpr auto _5to8 = [](uint8_t fiveBpp) -> uint8_t {
fiveBpp &= 0b11111; // For caller's convenience
return fiveBpp << 3 | fiveBpp >> 2;
};
return {_5to8(cgbColor), _5to8(cgbColor >> 5), _5to8(cgbColor >> 10),
(uint8_t)(cgbColor & 0x8000 ? 0x00 : 0xFF)};
}
/** /**
* Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS * Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS
* representation * representation

View File

@@ -14,6 +14,7 @@
.Nd Game Boy graphics converter .Nd Game Boy graphics converter
.Sh SYNOPSIS .Sh SYNOPSIS
.Nm .Nm
.Op Fl r Ar stride
.Op Fl CfmuVZ .Op Fl CfmuVZ
.Op Fl v Op Fl v No ... .Op Fl v Op Fl v No ...
.Op Fl a Ar attrmap | Fl A .Op Fl a Ar attrmap | Fl A
@@ -33,7 +34,7 @@
.Sh DESCRIPTION .Sh DESCRIPTION
The The
.Nm .Nm
program converts PNG images into data suitable for display on the Game Boy and Game Boy Color. program converts PNG images into data suitable for display on the Game Boy and Game Boy Color, or vice-versa.
.Pp .Pp
The main function of The main function of
.Nm .Nm
@@ -214,6 +215,22 @@ Specify how many colors each palette contains, including the transparent one if
cannot be more than cannot be more than
.Ql 1 << Ar depth .Ql 1 << Ar depth
.Pq see Fl d . .Pq see Fl d .
.It Fl r Ar width , Fl Fl reverse Ar width
Switches
.Nm
into
.Dq Sy reverse
mode.
In this mode, instead of converting a PNG image into Game Boy data,
.Nm
will attempt to reverse the process, and render Game Boy data into an image.
See
.Sx REVERSE MODE
below for details.
.Pp
.Ar width
is the image's width, in tiles
.Pq including any margins specified by Fl L .
.It Fl t Ar tilemap , Fl Fl tilemap Ar tilemap .It Fl t Ar tilemap , Fl Fl tilemap Ar tilemap
Generate a file of tile indices. Generate a file of tile indices.
For each square of the input image, its corresponding tile map byte contains the index of the associated tile in the tile data file. For each square of the input image, its corresponding tile map byte contains the index of the associated tile in the tile data file.
@@ -430,6 +447,46 @@ There is no padding between colors, nor between palettes; however, empty colors
TODO. TODO.
.Ss Attrmap data .Ss Attrmap data
TODO. TODO.
.Sh REVERSE MODE
.Nm
can produce a PNG image from valid data.
This may be useful for ripping graphics, recovering lost source images, etc.
An important caveat on that last one, though: the conversion process is
.Sy lossy
both ways, so the
.Do reversed Dc image won't be perfectly identical to the original\(embut it should be close to a Game Boy's output .
.Pq Keep in mind that many of consoles output different colors, so there is no true reference rendering.
.Pp
When using reverse mode, make sure to pass the same flags that were given when generating the data, especially
.Fl C , d , N , s , x ,
and
.Fl Z .
.Do At-files Dc may help with this .
.Nm
will warn about any inconsistencies it detects.
.Pp
Files that are normally outputs
.Pq Fl a , p , t
become inputs, and
.Ar file
will be written to instead of read from, and thus needs not exist beforehand.
Any of these inputs not passed is assumed to be some default:
.Bl -column "attribute map"
.It palettes Ta Unspecified palette data makes
.Nm
assume DMG (monochrome Game Boy) mode: a single palette of 4 grays.
It is possible to pass palettes using
.Fl c
instead of
.Fl p .
.It tile data Ta Tile data must be provided, as there is no reasonable assumption to fall back on.
.It tile map Ta A missing tile map makes
.Nm
assume that tiles were not deduplicated, and should be laid out in the order they are stored.
.It attribute map Ta Without an attribute map,
.Nm
assumes that no tiles were mirrored.
.El
.Sh NOTES .Sh NOTES
Some flags have had their functionality removed. Some flags have had their functionality removed.
.Fl D .Fl D

View File

@@ -75,6 +75,7 @@ set(rgbgfx_src
"gfx/pal_sorting.cpp" "gfx/pal_sorting.cpp"
"gfx/process.cpp" "gfx/process.cpp"
"gfx/proto_palette.cpp" "gfx/proto_palette.cpp"
"gfx/reverse.cpp"
"gfx/rgba.cpp" "gfx/rgba.cpp"
"extern/getopt.c" "extern/getopt.c"
"error.c" "error.c"

View File

@@ -27,6 +27,7 @@
#include "version.h" #include "version.h"
#include "gfx/process.hpp" #include "gfx/process.hpp"
#include "gfx/reverse.hpp"
using namespace std::literals::string_view_literals; using namespace std::literals::string_view_literals;
@@ -83,7 +84,7 @@ void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
} }
// Short options // Short options
static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:o:Pp:s:Tt:U:uVvx:Z"; static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:o:Pp:r:s:Tt:U:uVvx:Z";
/* /*
* Equivalent long options * Equivalent long options
@@ -113,6 +114,7 @@ static struct option const longopts[] = {
{"output", required_argument, NULL, 'o'}, {"output", required_argument, NULL, 'o'},
{"output-palette", no_argument, NULL, 'P'}, {"output-palette", no_argument, NULL, 'P'},
{"palette", required_argument, NULL, 'p'}, {"palette", required_argument, NULL, 'p'},
{"reverse", required_argument, NULL, 'r'},
{"output-tilemap", no_argument, NULL, 'T'}, {"output-tilemap", no_argument, NULL, 'T'},
{"tilemap", required_argument, NULL, 't'}, {"tilemap", required_argument, NULL, 't'},
{"unit-size", required_argument, NULL, 'U'}, {"unit-size", required_argument, NULL, 'U'},
@@ -125,7 +127,7 @@ static struct option const longopts[] = {
}; };
static void printUsage(void) { static void printUsage(void) {
fputs("Usage: rgbgfx [-CfmuVZ] [-v [-v ...]] [-a <attr_map> | -A] [-b base_ids]\n" fputs("Usage: rgbgfx [-r] [-CfmuVZ] [-v [-v ...]] [-a <attr_map> | -A] [-b base_ids]\n"
" [-c color_spec] [-d <depth>] [-L slice] [-N nb_tiles] [-n nb_pals]\n" " [-c color_spec] [-d <depth>] [-L slice] [-N nb_tiles] [-n nb_pals]\n"
" [-o <out_file>] [-p <pal_file> | -P] [-s nb_colors] [-t <tile_map> | -T]\n" " [-o <out_file>] [-p <pal_file> | -P] [-s nb_colors] [-t <tile_map> | -T]\n"
" [-U unit_size] [-x <tiles>] <file>\n" " [-U unit_size] [-x <tiles>] <file>\n"
@@ -430,6 +432,14 @@ static char *parseArgv(int argc, char **argv, bool &autoAttrmap, bool &autoTilem
break; break;
case 'n': case 'n':
options.nbPalettes = parseNumber(arg, "Number of palettes", 8); options.nbPalettes = parseNumber(arg, "Number of palettes", 8);
if (*arg != '\0') {
error("Number of palettes (-n) must be a valid number, not \"%s\"", musl_optarg);
}
if (options.nbPalettes > 8) {
error("Number of palettes (-n) must not exceed 8!");
} else if (options.nbPalettes == 0) {
error("Number of palettes (-n) may not be 0!");
}
break; break;
case 'o': case 'o':
options.output = musl_optarg; options.output = musl_optarg;
@@ -441,15 +451,24 @@ static char *parseArgv(int argc, char **argv, bool &autoAttrmap, bool &autoTilem
autoPalettes = false; autoPalettes = false;
options.palettes = musl_optarg; options.palettes = musl_optarg;
break; break;
case 'r':
options.reversedWidth = parseNumber(arg, "Reversed image stride");
if (*arg != '\0') {
error("Reversed image stride (-r) must be a valid number, not \"%s\"", musl_optarg);
}
if (options.reversedWidth == 0) {
error("Reversed image stride (-r) may not be 0!");
}
break;
case 's': case 's':
options.nbColorsPerPal = parseNumber(arg, "Number of colors per palette", 4); options.nbColorsPerPal = parseNumber(arg, "Number of colors per palette", 4);
if (*arg != '\0') { if (*arg != '\0') {
error("Palette size (-s) argument must be a valid number, not \"%s\"", musl_optarg); error("Palette size (-s) must be a valid number, not \"%s\"", musl_optarg);
} }
if (options.nbColorsPerPal > 4) { if (options.nbColorsPerPal > 4) {
error("Palette size (-s) argument must not exceed 4!"); error("Palette size (-s) must not exceed 4!");
} else if (options.nbColorsPerPal == 0) { } else if (options.nbColorsPerPal == 0) {
error("Palette size (-s) argument may not be 0!"); error("Palette size (-s) may not be 0!");
} }
break; break;
case 'T': case 'T':
@@ -678,7 +697,11 @@ int main(int argc, char *argv[]) {
return 0; return 0;
} }
if (options.reverse()) {
reverse();
} else {
process(); process();
}
return 0; return 0;
} }

View File

@@ -552,14 +552,6 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
} }
} }
static uint8_t flip(uint8_t byte) {
// To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
byte = (byte & 0x0F) << 4 | (byte & 0xF0) >> 4;
byte = (byte & 0x33) << 2 | (byte & 0xCC) >> 2;
byte = (byte & 0x55) << 1 | (byte & 0xAA) >> 1;
return byte;
}
class TileData { class TileData {
std::array<uint8_t, 16> _data; std::array<uint8_t, 16> _data;
// The hash is a bit lax: it's the XOR of all lines, and every other nibble is identical // The hash is a bit lax: it's the XOR of all lines, and every other nibble is identical

301
src/gfx/reverse.cpp Normal file
View File

@@ -0,0 +1,301 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include "gfx/reverse.hpp"
#include <algorithm>
#include <array>
#include <assert.h>
#include <cinttypes>
#include <errno.h>
#include <fstream>
#include <optional>
#include <png.h>
#include <string.h>
#include <tuple>
#include <vector>
#include "defaultinitalloc.hpp"
#include "helpers.h"
#include "gfx/main.hpp"
static DefaultInitVec<uint8_t> readInto(std::string path) {
std::filebuf file;
file.open(path, std::ios::in | std::ios::binary);
DefaultInitVec<uint8_t> data(128 * 16); // Begin with some room pre-allocated
size_t curSize = 0;
for (;;) {
size_t oldSize = curSize;
curSize = data.size();
// Fill the new area ([oldSize; curSize[) with bytes
size_t nbRead =
file.sgetn(reinterpret_cast<char *>(&data.data()[oldSize]), curSize - oldSize);
if (nbRead != curSize - oldSize) {
// Shrink the vector to discard bytes that weren't read
data.resize(oldSize + nbRead);
break;
}
// If the vector has some capacity left, use it; otherwise, double the current size
// Arbitrary, but if you got a better idea...
size_t newSize = oldSize != data.capacity() ? data.capacity() : oldSize * 2;
assert(oldSize != newSize);
data.resize(newSize);
}
return data;
}
[[noreturn]] static void pngError(png_structp png, char const *msg) {
fatal("Error writing reversed image (\"%s\"): %s",
static_cast<char const *>(png_get_error_ptr(png)), msg);
}
static void pngWarning(png_structp png, char const *msg) {
warning("While writing reversed image (\"%s\"): %s",
static_cast<char const *>(png_get_error_ptr(png)), msg);
}
void writePng(png_structp png, png_bytep data, size_t length) {
auto &pngFile = *static_cast<std::filebuf *>(png_get_io_ptr(png));
pngFile.sputn(reinterpret_cast<char *>(data), length);
}
void flushPng(png_structp png) {
auto &pngFile = *static_cast<std::filebuf *>(png_get_io_ptr(png));
pngFile.pubsync();
}
void reverse() {
options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
// Check for weird flag combinations
if (options.output.empty()) {
fatal("Tile data must be provided when reversing an image!");
}
if (!options.allowDedup && options.tilemap.empty()) {
warning("Tile deduplication is enabled, but no tilemap is provided?");
}
if (options.useColorCurve) {
warning("The color curve is not yet supported in reverse mode...");
}
options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
auto const tiles = readInto(options.output);
uint8_t tileSize = 8 * options.bitDepth;
if (tiles.size() % tileSize != 0) {
fatal("Tile data size must be a multiple of %" PRIu8 " bytes! (Read %zu)", tileSize,
tiles.size());
}
// By default, assume tiles are not deduplicated, and add the (allegedly) trimmed tiles
size_t nbTileInstances = tiles.size() / tileSize + options.trim; // Image size in tiles
options.verbosePrint(Options::VERB_INTERM, "Read %zu tiles.\n", nbTileInstances);
std::optional<DefaultInitVec<uint8_t>> tilemap;
if (!options.tilemap.empty()) {
tilemap = readInto(options.tilemap);
nbTileInstances = tilemap->size();
// TODO: range check
}
if (nbTileInstances > options.maxNbTiles[0] + options.maxNbTiles[1]) {
warning("Read %zu tiles, more than the limit of %zu + %zu", nbTileInstances,
options.maxNbTiles[0], options.maxNbTiles[1]);
}
if (nbTileInstances % options.reversedWidth) {
fatal("Image size (%zu tiles) is not divisible by the provided stride (%zu tiles), cannot "
"determine image dimensions",
nbTileInstances, options.reversedWidth);
}
size_t width, height;
size_t usefulWidth = options.reversedWidth - options.inputSlice[1] - options.inputSlice[3];
if (usefulWidth % 8 != 0) {
fatal(
"No input slice specified (`-L`), and specified image width (%zu) not a multiple of 8",
usefulWidth);
} else {
width = usefulWidth / 8;
if (nbTileInstances % width != 0) {
fatal("Total number of tiles read (%zu) cannot be divided by image width (%zu tiles)",
nbTileInstances, width);
}
height = nbTileInstances / width;
}
options.verbosePrint(Options::VERB_INTERM, "Reversed image dimensions: %zux%zu tiles\n", width,
height);
// TODO: -U
std::vector<std::array<Rgba, 4>> palettes{
{Rgba(0xffffffff), Rgba(0xaaaaaaff), Rgba(0x555555ff), Rgba(0x000000ff)}
};
if (!options.palettes.empty()) {
std::filebuf file;
file.open(options.palettes, std::ios::in | std::ios::binary);
palettes.clear();
std::array<uint8_t, sizeof(uint16_t) * 4> buf; // 4 colors
size_t nbRead;
do {
nbRead = file.sgetn(reinterpret_cast<char *>(buf.data()), buf.size());
if (nbRead == buf.size()) {
// Expand the colors
auto &palette = palettes.emplace_back();
std::generate(palette.begin(), palette.begin() + options.nbColorsPerPal,
[&buf, i = 0]() mutable {
i += 2;
return Rgba::fromCGBColor(buf[i - 2] + (buf[i - 1] << 8));
});
} else if (nbRead != 0) {
fatal("Palette data size (%zu) is not a multiple of %zu bytes!\n",
palettes.size() * buf.size() + nbRead, buf.size());
}
} while (nbRead != 0);
if (palettes.size() > options.nbPalettes) {
warning("Read %zu palettes, more than the specified limit of %zu", palettes.size(),
options.nbPalettes);
}
}
std::optional<DefaultInitVec<uint8_t>> attrmap;
if (!options.attrmap.empty()) {
attrmap = readInto(options.attrmap);
if (attrmap->size() != nbTileInstances) {
fatal("Attribute map size (%zu tiles) doesn't match image's (%zu)", attrmap->size(),
nbTileInstances);
}
// Scan through the attributes for inconsistencies
// We do this now for two reasons:
// 1. Checking those during the main loop is harmful to optimization, and
// 2. It clutters the code more, and it's not in great shape to begin with
}
// TODO: palette map (overrides attributes)
options.verbosePrint(Options::VERB_LOG_ACT, "Writing image...\n");
std::filebuf pngFile;
pngFile.open(options.input, std::ios::out | std::ios::binary);
png_structp png = png_create_write_struct(
PNG_LIBPNG_VER_STRING,
const_cast<png_voidp>(static_cast<void const *>(options.input.c_str())), pngError,
pngWarning);
if (!png) {
fatal("Couldn't create PNG write struct: %s", strerror(errno));
}
png_infop pngInfo = png_create_info_struct(png);
if (!pngInfo) {
fatal("Couldn't create PNG info struct: %s", strerror(errno));
}
png_set_write_fn(png, &pngFile, writePng, flushPng);
// TODO: if `-f` is passed, write the image indexed instead of RGB
png_set_IHDR(png, pngInfo, options.reversedWidth,
height * 8 + options.inputSlice[0] + options.inputSlice[2], 8,
PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
png_write_info(png, pngInfo);
png_color_8 sbitChunk;
sbitChunk.red = 5;
sbitChunk.green = 5;
sbitChunk.blue = 5;
sbitChunk.alpha = 1;
png_set_sBIT(png, pngInfo, &sbitChunk);
constexpr uint8_t SIZEOF_PIXEL = 4; // Each pixel is 4 bytes (RGBA @ 8 bits/component)
size_t const SIZEOF_ROW = options.reversedWidth * SIZEOF_PIXEL;
std::vector<uint8_t> tileRow(8 * SIZEOF_ROW, 0xFF); // Data for 8 rows of pixels
uint8_t * const rowPtrs[8] = {
&tileRow.data()[0 * SIZEOF_ROW + options.inputSlice[3]],
&tileRow.data()[1 * SIZEOF_ROW + options.inputSlice[3]],
&tileRow.data()[2 * SIZEOF_ROW + options.inputSlice[3]],
&tileRow.data()[3 * SIZEOF_ROW + options.inputSlice[3]],
&tileRow.data()[4 * SIZEOF_ROW + options.inputSlice[3]],
&tileRow.data()[5 * SIZEOF_ROW + options.inputSlice[3]],
&tileRow.data()[6 * SIZEOF_ROW + options.inputSlice[3]],
&tileRow.data()[7 * SIZEOF_ROW + options.inputSlice[3]],
};
auto const fillRows = [&png, &tileRow](size_t nbRows) {
for (size_t _ = 0; _ < nbRows; ++_) {
png_write_row(png, tileRow.data());
}
};
fillRows(options.inputSlice[0]);
for (size_t ty = 0; ty < height; ++ty) {
for (size_t tx = 0; tx < width; ++tx) {
size_t index = options.columnMajor ? ty + tx * width : ty * width + tx;
// Get the tile ID at this location
uint8_t gbcTileID = tilemap.has_value() ? (*tilemap)[index] : index;
// By default, a tile is unflipped, in bank 0, and uses palette #0
uint8_t attribute = attrmap.has_value() ? (*attrmap)[index] : 0x00;
bool bank = attribute & 0x08;
gbcTileID -= options.baseTileIDs[bank];
size_t tileID = gbcTileID + bank * options.maxNbTiles[0];
assert(tileID < nbTileInstances); // Should have been checked earlier
// We do not have data for tiles trimmed with `-x`, so assume they are "blank"
static std::array<uint8_t, 16> const trimmedTile{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
uint8_t const *tileData = tileID > nbTileInstances - options.trim
? trimmedTile.data()
: &tiles[tileID * tileSize];
assert((attribute & 0b111) < palettes.size()); // Should be ensured on data read
auto const &palette = palettes[attribute & 0b111];
for (uint8_t y = 0; y < 8; ++y) {
// If vertically mirrored, fetch the bytes from the other end
uint8_t realY = attribute & 0x40 ? 7 - y : y;
uint8_t bitplane0 = tileData[realY * 2], bitplane1 = tileData[realY * 2 + 1];
if (attribute & 0x20) { // Handle horizontal flip
bitplane0 = flip(bitplane0);
bitplane1 = flip(bitplane1);
}
uint8_t *ptr = &rowPtrs[y][tx * 8 * SIZEOF_PIXEL];
for (uint8_t x = 0; x < 8; ++x) {
uint8_t bit0 = bitplane0 & 0x80, bit1 = bitplane1 & 0x80;
Rgba const &pixel = palette[bit0 >> 7 | bit1 >> 6];
*ptr++ = pixel.red;
*ptr++ = pixel.green;
*ptr++ = pixel.blue;
*ptr++ = pixel.alpha;
// Shift the pixel out
bitplane0 <<= 1;
bitplane1 <<= 1;
}
}
}
// We never modify the pointers, and neither should libpng, despite the overly lax function
// signature.
// (AIUI, casting away const-ness is okay as long as you don't actually modify the
// pointed-to data)
png_write_rows(png, const_cast<png_bytepp>(rowPtrs), 8);
}
// Clear the first row again for the function
std::fill(tileRow.begin(), tileRow.begin() + SIZEOF_ROW, 0xFF);
fillRows(options.inputSlice[2]);
// Finalize the write
png_write_end(png, pngInfo);
png_destroy_write_struct(&png, &pngInfo);
pngFile.close();
}