ref: cc27169ecdd4e29944e207acc4a39f25e78b0a8f
parent: 843022772be7e72c5348a72709d61dce1c62adb6
author: ISSOtm <[email protected]>
date: Sun Mar 27 16:21:00 EDT 2022
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.
--- a/Makefile
+++ b/Makefile
@@ -110,6 +110,7 @@
src/gfx/pal_sorting.o \
src/gfx/process.o \
src/gfx/proto_palette.o \
+ src/gfx/reverse.o \
src/gfx/rgba.o \
src/extern/getopt.o \
src/error.o
--- a/include/gfx/main.hpp
+++ b/include/gfx/main.hpp
@@ -20,6 +20,9 @@
#include "gfx/rgba.hpp"
struct Options {
+ uint8_t reversedWidth = 0; // -r, in pixels
+ bool reverse() const { return reversedWidth != 0; }
+
bool useColorCurve = false; // -C
bool fixInput = false; // -f
bool allowMirroring = false; // -m
@@ -36,7 +39,7 @@
} palSpecType = NO_SPEC; // -c
std::vector<std::array<Rgba, 4>> palSpec{};
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
uint8_t nbPalettes = 8; // -n
std::string output{}; // -o
@@ -83,5 +86,13 @@
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 */
--- /dev/null
+++ b/include/gfx/reverse.hpp
@@ -1,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 */
--- a/include/gfx/rgba.hpp
+++ b/include/gfx/rgba.hpp
@@ -17,12 +17,22 @@
uint8_t blue;
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)
*/
- explicit Rgba(uint32_t rgba = 0)
+ explicit constexpr Rgba(uint32_t rgba = 0)
: 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
--- a/man/rgbgfx.1
+++ b/man/rgbgfx.1
@@ -14,6 +14,7 @@
.Nd Game Boy graphics converter
.Sh SYNOPSIS
.Nm
+.Op Fl r Ar stride
.Op Fl CfmuVZ
.Op Fl v Op Fl v No ...
.Op Fl a Ar attrmap | Fl A
@@ -33,7 +34,7 @@
.Sh DESCRIPTION
The
.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
The main function of
.Nm
@@ -214,6 +215,22 @@
cannot be more than
.Ql 1 << Ar depth
.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
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.
@@ -430,6 +447,46 @@
TODO.
.Ss Attrmap data
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
Some flags have had their functionality removed.
.Fl D
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -75,6 +75,7 @@
"gfx/pal_sorting.cpp"
"gfx/process.cpp"
"gfx/proto_palette.cpp"
+ "gfx/reverse.cpp"
"gfx/rgba.cpp"
"extern/getopt.c"
"error.c"
--- a/src/gfx/main.cpp
+++ b/src/gfx/main.cpp
@@ -27,6 +27,7 @@
#include "version.h"
#include "gfx/process.hpp"
+#include "gfx/reverse.hpp"
using namespace std::literals::string_view_literals;
@@ -83,7 +84,7 @@
}
// 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
@@ -113,6 +114,7 @@
{"output", required_argument, NULL, 'o'},
{"output-palette", no_argument, NULL, 'P'},
{"palette", required_argument, NULL, 'p'},
+ {"reverse", required_argument, NULL, 'r'},
{"output-tilemap", no_argument, NULL, 'T'},
{"tilemap", required_argument, NULL, 't'},
{"unit-size", required_argument, NULL, 'U'},
@@ -125,7 +127,7 @@
};
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"
" [-o <out_file>] [-p <pal_file> | -P] [-s nb_colors] [-t <tile_map> | -T]\n"
" [-U unit_size] [-x <tiles>] <file>\n"
@@ -430,6 +432,14 @@
break;
case 'n':
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;
case 'o':
options.output = musl_optarg;
@@ -441,15 +451,24 @@
autoPalettes = false;
options.palettes = musl_optarg;
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':
options.nbColorsPerPal = parseNumber(arg, "Number of colors per palette", 4);
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) {
- error("Palette size (-s) argument must not exceed 4!");
+ error("Palette size (-s) must not exceed 4!");
} else if (options.nbColorsPerPal == 0) {
- error("Palette size (-s) argument may not be 0!");
+ error("Palette size (-s) may not be 0!");
}
break;
case 'T':
@@ -678,7 +697,11 @@
return 0;
}
- process();
+ if (options.reverse()) {
+ reverse();
+ } else {
+ process();
+ }
return 0;
}
--- a/src/gfx/process.cpp
+++ b/src/gfx/process.cpp
@@ -552,14 +552,6 @@
}
}
-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 {
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
--- /dev/null
+++ b/src/gfx/reverse.cpp
@@ -1,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();
+}