shithub: rgbds

Download patch

ref: 53ccf325273c1d6a3d4a6965f328861d87b88656
parent: 3cb33dffa28e39fde7e716ec16936060a437a320
author: Jacob Moody <moody@posixcafe.org>
date: Sat Feb 25 17:18:32 EST 2023

restore c version of rgbgfx

--- /dev/null
+++ b/include/gfx/gb.h
@@ -1,0 +1,36 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2013-2018, stag019 and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef RGBDS_GFX_GB_H
+#define RGBDS_GFX_GB_H
+
+#include <stdint.h>
+#include "gfx/main.h"
+
+#define XFLIP 0x40
+#define YFLIP 0x20
+
+void raw_to_gb(const struct RawIndexedImage *raw_image, struct GBImage *gb);
+void output_file(const struct Options *opts, const struct GBImage *gb);
+int get_tile_index(uint8_t *tile, uint8_t **tiles, int num_tiles,
+		   int tile_size);
+uint8_t reverse_bits(uint8_t b);
+void xflip(uint8_t *tile, uint8_t *tile_xflip, int tile_size);
+void yflip(uint8_t *tile, uint8_t *tile_yflip, int tile_size);
+int get_mirrored_tile_index(uint8_t *tile, uint8_t **tiles, int num_tiles,
+			    int tile_size, int *flags);
+void create_mapfiles(const struct Options *opts, struct GBImage *gb,
+		     struct Mapfile *tilemap, struct Mapfile *attrmap);
+void output_tilemap_file(const struct Options *opts,
+			 const struct Mapfile *tilemap);
+void output_attrmap_file(const struct Options *opts,
+			 const struct Mapfile *attrmap);
+void output_palette_file(const struct Options *opts,
+			 const struct RawIndexedImage *raw_image);
+
+#endif
--- /dev/null
+++ b/include/gfx/main.h
@@ -1,0 +1,91 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2013-2018, stag019 and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef RGBDS_GFX_MAIN_H
+#define RGBDS_GFX_MAIN_H
+
+#include <png.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "error.h"
+
+struct Options {
+	bool debug;
+	bool verbose;
+	bool hardfix;
+	bool fix;
+	bool horizontal;
+	bool mirror;
+	bool unique;
+	bool colorcurve;
+	unsigned int trim;
+	char *tilemapfile;
+	bool tilemapout;
+	char *attrmapfile;
+	bool attrmapout;
+	char *palfile;
+	bool palout;
+	char *outfile;
+	char *infile;
+};
+
+struct RGBColor {
+	uint8_t red;
+	uint8_t green;
+	uint8_t blue;
+};
+
+struct ImageOptions {
+	bool horizontal;
+	unsigned int trim;
+	char *tilemapfile;
+	bool tilemapout;
+	char *attrmapfile;
+	bool attrmapout;
+	char *palfile;
+	bool palout;
+};
+
+struct PNGImage {
+	png_struct *png;
+	png_info *info;
+
+	png_byte **data;
+	int width;
+	int height;
+	png_byte depth;
+	png_byte type;
+};
+
+struct RawIndexedImage {
+	uint8_t **data;
+	struct RGBColor *palette;
+	int num_colors;
+	unsigned int width;
+	unsigned int height;
+};
+
+struct GBImage {
+	uint8_t *data;
+	int size;
+	bool horizontal;
+	int trim;
+};
+
+struct Mapfile {
+	uint8_t *data;
+	int size;
+};
+
+extern int depth, colors;
+
+#include "gfx/makepng.h"
+#include "gfx/gb.h"
+
+#endif /* RGBDS_GFX_MAIN_H */
--- a/include/gfx/main.hpp
+++ /dev/null
@@ -1,123 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#ifndef RGBDS_GFX_MAIN_HPP
-#define RGBDS_GFX_MAIN_HPP
-
-#include <array>
-#include <limits.h>
-#include <stdint.h>
-#include <string>
-#include <utility>
-#include <vector>
-
-#include "helpers.h"
-
-#include "gfx/rgba.hpp"
-
-struct Options {
-	uint16_t reversedWidth = 0; // -r, in tiles
-	bool reverse() const { return reversedWidth != 0; }
-
-	bool useColorCurve = false; // -C
-	bool allowMirroring = false; // -m
-	bool allowDedup = false; // -u
-	bool columnMajor = false; // -Z, previously -h
-	uint8_t verbosity = 0; // -v
-
-	std::string attrmap{}; // -a, -A
-	std::array<uint8_t, 2> baseTileIDs{0, 0}; // -b
-	enum {
-		NO_SPEC,
-		EXPLICIT,
-		EMBEDDED,
-	} palSpecType = NO_SPEC; // -c
-	std::vector<std::array<Rgba, 4>> palSpec{};
-	uint8_t bitDepth = 2; // -d
-	struct {
-		uint16_t left;
-		uint16_t top;
-		uint16_t width;
-		uint16_t height;
-	} 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
-	std::string palettes{}; // -p, -P
-	std::string palmap{}; // -q, -Q
-	uint8_t nbColorsPerPal = 0; // -s; 0 means "auto" = 1 << bitDepth;
-	std::string tilemap{}; // -t, -T
-	uint64_t trim = 0; // -x
-
-	std::string input{}; // positional arg
-
-	static constexpr uint8_t VERB_NONE = 0; // Normal, no extra output
-	static constexpr uint8_t VERB_CFG = 1; // Print configuration after parsing options
-	static constexpr uint8_t VERB_LOG_ACT = 2; // Log actions before doing them
-	static constexpr uint8_t VERB_INTERM = 3; // Print some intermediate results
-	static constexpr uint8_t VERB_DEBUG = 4; // Internals are logged
-	static constexpr uint8_t VERB_UNMAPPED = 5; // Unused so far
-	static constexpr uint8_t VERB_VVVVVV = 6; // What, can't I have a little fun?
-	format_(printf, 3, 4) void verbosePrint(uint8_t level, char const *fmt, ...) const;
-
-	mutable bool hasTransparentPixels = false;
-	uint8_t maxOpaqueColors() const { return nbColorsPerPal - hasTransparentPixels; }
-};
-
-extern Options options;
-
-/*
- * Prints the error count, and exits with failure
- */
-[[noreturn]] void giveUp();
-/*
- * Prints a warning, and does not change the error count
- */
-void warning(char const *fmt, ...);
-/*
- * Prints an error, and increments the error count
- */
-void error(char const *fmt, ...);
-/*
- * Prints a fatal error, increments the error count, and gives up
- */
-[[noreturn]] void fatal(char const *fmt, ...);
-
-struct Palette {
-	// An array of 4 GBC-native (RGB555) colors
-	std::array<uint16_t, 4> colors{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
-
-	void addColor(uint16_t color);
-	uint8_t indexOf(uint16_t color) const;
-	uint16_t &operator[](size_t index) { return colors[index]; }
-	uint16_t const &operator[](size_t index) const { return colors[index]; }
-
-	decltype(colors)::iterator begin();
-	decltype(colors)::iterator end();
-	decltype(colors)::const_iterator begin() const;
-	decltype(colors)::const_iterator end() const;
-
-	uint8_t size() const;
-};
-
-namespace detail {
-template<typename T, T... i>
-static constexpr auto flipTable(std::integer_sequence<T, i...>) {
-	return std::array{[](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;
-	}(i)...};
-}
-}
-// Flipping tends to happen fairly often, so take a bite out of dcache to speed it up
-static constexpr auto flipTable = detail::flipTable(std::make_integer_sequence<uint16_t, 256>());
-
-#endif // RGBDS_GFX_MAIN_HPP
--- /dev/null
+++ b/include/gfx/makepng.h
@@ -1,0 +1,21 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2013-2018, stag019 and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef RGBDS_GFX_PNG_H
+#define RGBDS_GFX_PNG_H
+
+#include "gfx/main.h"
+
+struct RawIndexedImage *input_png_file(const struct Options *opts,
+				       struct ImageOptions *png_options);
+void output_png_file(const struct Options *opts,
+		     const struct ImageOptions *png_options,
+		     const struct RawIndexedImage *raw_image);
+void destroy_raw_image(struct RawIndexedImage **raw_image_ptr_ptr);
+
+#endif /* RGBDS_GFX_PNG_H */
--- a/include/gfx/pal_packing.hpp
+++ /dev/null
@@ -1,32 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#ifndef RGBDS_GFX_PAL_PACKING_HPP
-#define RGBDS_GFX_PAL_PACKING_HPP
-
-#include <tuple>
-#include <vector>
-
-#include "defaultinitalloc.hpp"
-
-#include "gfx/main.hpp"
-
-struct Palette;
-class ProtoPalette;
-
-namespace packing {
-
-/*
- * Returns which palette each proto-palette maps to, and how many palettes are necessary
- */
-std::tuple<DefaultInitVec<size_t>, size_t>
-    overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes);
-
-}
-
-#endif // RGBDS_GFX_PAL_PACKING_HPP
--- a/include/gfx/pal_sorting.hpp
+++ /dev/null
@@ -1,32 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#ifndef RGBDS_GFX_PAL_SORTING_HPP
-#define RGBDS_GFX_PAL_SORTING_HPP
-
-#include <array>
-#include <assert.h>
-#include <optional>
-#include <png.h>
-#include <vector>
-
-#include "gfx/rgba.hpp"
-
-struct Palette;
-
-namespace sorting {
-
-void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
-             png_byte *palAlpha);
-void grayscale(std::vector<Palette> &palettes,
-               std::array<std::optional<Rgba>, 0x8001> const &colors);
-void rgb(std::vector<Palette> &palettes);
-
-}
-
-#endif // RGBDS_GFX_PAL_SORTING_HPP
--- a/include/gfx/pal_spec.hpp
+++ /dev/null
@@ -1,15 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#ifndef RGBDS_GFX_PAL_SPEC_HPP
-#define RGBDS_GFX_PAL_SPEC_HPP
-
-void parseInlinePalSpec(char const * const arg);
-void parseExternalPalSpec(char const *arg);
-
-#endif // RGBDS_GFX_PAL_SPEC_HPP
--- a/include/gfx/process.hpp
+++ /dev/null
@@ -1,14 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#ifndef RGBDS_GFX_CONVERT_HPP
-#define RGBDS_GFX_CONVERT_HPP
-
-void process();
-
-#endif // RGBDS_GFX_CONVERT_HPP
--- a/include/gfx/proto_palette.hpp
+++ /dev/null
@@ -1,49 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#ifndef RGBDS_GFX_PROTO_PALETTE_HPP
-#define RGBDS_GFX_PROTO_PALETTE_HPP
-
-#include <algorithm>
-#include <array>
-#include <stddef.h>
-#include <stdint.h>
-
-class ProtoPalette {
-public:
-	static constexpr size_t capacity = 4;
-
-private:
-	// Up to 4 colors, sorted, and where SIZE_MAX means the slot is empty
-	// (OK because it's not a valid color index)
-	// Sorting is done on the raw numerical values to lessen `compare`'s complexity
-	std::array<uint16_t, capacity> _colorIndices{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
-
-public:
-	/*
-	 * Adds the specified color to the set, or **silently drops it** if the set is full.
-	 *
-	 * Returns whether the color was unique.
-	 */
-	bool add(uint16_t color);
-
-	enum ComparisonResult {
-		NEITHER,
-		WE_BIGGER,
-		THEY_BIGGER = -1,
-	};
-	ComparisonResult compare(ProtoPalette const &other) const;
-
-	size_t size() const;
-	bool empty() const;
-
-	decltype(_colorIndices)::const_iterator begin() const;
-	decltype(_colorIndices)::const_iterator end() const;
-};
-
-#endif // RGBDS_GFX_PROTO_PALETTE_HPP
--- a/include/gfx/reverse.hpp
+++ /dev/null
@@ -1,14 +1,0 @@
-/*
- * 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
+++ /dev/null
@@ -1,67 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#ifndef RGBDS_GFX_RGBA_HPP
-#define RGBDS_GFX_RGBA_HPP
-
-#include <cstdint>
-#include <stdint.h>
-
-struct Rgba {
-	uint8_t red;
-	uint8_t green;
-	uint8_t blue;
-	uint8_t alpha;
-
-	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 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
-	 * representation
-	 */
-	uint32_t toCSS() const {
-		auto shl = [](uint8_t val, unsigned shift) { return static_cast<uint32_t>(val) << shift; };
-		return shl(red, 24) | shl(green, 16) | shl(blue, 8) | shl(alpha, 0);
-	}
-	friend bool operator!=(Rgba const &lhs, Rgba const &rhs) { return lhs.toCSS() != rhs.toCSS(); }
-
-	/*
-	 * CGB colors are RGB555, so we use bit 15 to signify that the color is transparent instead
-	 * Since the rest of the bits don't matter then, we return 0x8000 exactly.
-	 */
-	static constexpr uint16_t transparent = 0b1'00000'00000'00000;
-
-	static constexpr uint8_t transparency_threshold = 0x10;
-	bool isTransparent() const { return alpha < transparency_threshold; }
-	static constexpr uint8_t opacity_threshold = 0xF0;
-	bool isOpaque() const { return alpha >= opacity_threshold; }
-	/*
-	 * Computes the equivalent CGB color, respects the color curve depending on options
-	 */
-	uint16_t cgbColor() const;
-
-	bool isGray() const { return red == green && green == blue; }
-	uint8_t grayIndex() const;
-};
-
-#endif // RGBDS_GFX_RGBA_HPP
--- /dev/null
+++ b/src/gfx/gb.c
@@ -1,0 +1,385 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2013-2018, stag019 and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "gfx/gb.h"
+
+void transpose_tiles(struct GBImage *gb, int width)
+{
+	uint8_t *newdata;
+	int i;
+	int newbyte;
+
+	newdata = calloc(gb->size, 1);
+	if (!newdata)
+		err("%s: Failed to allocate memory for new data", __func__);
+
+	for (i = 0; i < gb->size; i++) {
+		newbyte = i / (8 * depth) * width * 8 * depth;
+		newbyte = newbyte % gb->size
+			+ 8 * depth * (newbyte / gb->size)
+			+ i % (8 * depth);
+		newdata[newbyte] = gb->data[i];
+	}
+
+	free(gb->data);
+
+	gb->data = newdata;
+}
+
+void raw_to_gb(const struct RawIndexedImage *raw_image, struct GBImage *gb)
+{
+	uint8_t index;
+
+	for (unsigned int y = 0; y < raw_image->height; y++) {
+		for (unsigned int x = 0; x < raw_image->width; x++) {
+			index = raw_image->data[y][x];
+			index &= (1 << depth) - 1;
+
+			unsigned int byte = y * depth
+				+ x / 8 * raw_image->height / 8 * 8 * depth;
+			gb->data[byte] |= (index & 1) << (7 - x % 8);
+			if (depth == 2) {
+				gb->data[byte + 1] |=
+					(index >> 1) << (7 - x % 8);
+			}
+		}
+	}
+
+	if (!gb->horizontal)
+		transpose_tiles(gb, raw_image->width / 8);
+}
+
+void output_file(const struct Options *opts, const struct GBImage *gb)
+{
+	FILE *f;
+
+	f = fopen(opts->outfile, "wb");
+	if (!f)
+		err("%s: Opening output file '%s' failed", __func__,
+		    opts->outfile);
+
+	fwrite(gb->data, 1, gb->size - gb->trim * 8 * depth, f);
+
+	fclose(f);
+}
+
+int get_tile_index(uint8_t *tile, uint8_t **tiles, int num_tiles, int tile_size)
+{
+	int i, j;
+
+	for (i = 0; i < num_tiles; i++) {
+		for (j = 0; j < tile_size; j++) {
+			if (tile[j] != tiles[i][j])
+				break;
+		}
+
+		if (j >= tile_size)
+			return i;
+	}
+	return -1;
+}
+
+uint8_t reverse_bits(uint8_t b)
+{
+	uint8_t rev = 0;
+
+	rev |= (b & 0x80) >> 7;
+	rev |= (b & 0x40) >> 5;
+	rev |= (b & 0x20) >> 3;
+	rev |= (b & 0x10) >> 1;
+	rev |= (b & 0x08) << 1;
+	rev |= (b & 0x04) << 3;
+	rev |= (b & 0x02) << 5;
+	rev |= (b & 0x01) << 7;
+	return rev;
+}
+
+void xflip(uint8_t *tile, uint8_t *tile_xflip, int tile_size)
+{
+	int i;
+
+	for (i = 0; i < tile_size; i++)
+		tile_xflip[i] = reverse_bits(tile[i]);
+}
+
+void yflip(uint8_t *tile, uint8_t *tile_yflip, int tile_size)
+{
+	int i;
+
+	for (i = 0; i < tile_size; i++)
+		tile_yflip[i] = tile[(tile_size - i - 1) ^ (depth - 1)];
+}
+
+/*
+ * get_mirrored_tile_index looks for `tile` in tile array `tiles`, also
+ * checking x-, y-, and xy-mirrored versions of `tile`. If one is found,
+ * `*flags` is set according to the type of mirroring and the index of the
+ * matched tile is returned. If no match is found, -1 is returned.
+ */
+int get_mirrored_tile_index(uint8_t *tile, uint8_t **tiles, int num_tiles,
+			    int tile_size, int *flags)
+{
+	int index;
+	uint8_t *tile_xflip;
+	uint8_t *tile_yflip;
+
+	index = get_tile_index(tile, tiles, num_tiles, tile_size);
+	if (index >= 0) {
+		*flags = 0;
+		return index;
+	}
+
+	tile_yflip = malloc(tile_size);
+	if (!tile_yflip)
+		err("%s: Failed to allocate memory for Y flip of tile",
+		    __func__);
+	yflip(tile, tile_yflip, tile_size);
+	index = get_tile_index(tile_yflip, tiles, num_tiles, tile_size);
+	if (index >= 0) {
+		*flags = YFLIP;
+		free(tile_yflip);
+		return index;
+	}
+
+	tile_xflip = malloc(tile_size);
+	if (!tile_xflip)
+		err("%s: Failed to allocate memory for X flip of tile",
+		    __func__);
+	xflip(tile, tile_xflip, tile_size);
+	index = get_tile_index(tile_xflip, tiles, num_tiles, tile_size);
+	if (index >= 0) {
+		*flags = XFLIP;
+		free(tile_yflip);
+		free(tile_xflip);
+		return index;
+	}
+
+	yflip(tile_xflip, tile_yflip, tile_size);
+	index = get_tile_index(tile_yflip, tiles, num_tiles, tile_size);
+	if (index >= 0)
+		*flags = XFLIP | YFLIP;
+
+	free(tile_yflip);
+	free(tile_xflip);
+	return index;
+}
+
+void create_mapfiles(const struct Options *opts, struct GBImage *gb,
+		     struct Mapfile *tilemap, struct Mapfile *attrmap)
+{
+	int i, j;
+	int gb_i;
+	int tile_size;
+	int max_tiles;
+	int num_tiles;
+	int index;
+	int flags;
+	int gb_size;
+	uint8_t *tile;
+	uint8_t **tiles;
+
+	tile_size = sizeof(*tile) * depth * 8;
+	gb_size = gb->size - (gb->trim * tile_size);
+	max_tiles = gb_size / tile_size;
+
+	/* If the input image doesn't fill the last tile, increase the count. */
+	if (gb_size > max_tiles * tile_size)
+		max_tiles++;
+
+	tiles = calloc(max_tiles, sizeof(*tiles));
+	if (!tiles)
+		err("%s: Failed to allocate memory for tiles", __func__);
+	num_tiles = 0;
+
+	if (*opts->tilemapfile) {
+		tilemap->data = calloc(max_tiles, sizeof(*tilemap->data));
+		if (!tilemap->data)
+			err("%s: Failed to allocate memory for tilemap data",
+			    __func__);
+		tilemap->size = 0;
+	}
+
+	if (*opts->attrmapfile) {
+		attrmap->data = calloc(max_tiles, sizeof(*attrmap->data));
+		if (!attrmap->data)
+			err("%s: Failed to allocate memory for attrmap data",
+			    __func__);
+		attrmap->size = 0;
+	}
+
+	gb_i = 0;
+	while (gb_i < gb_size) {
+		flags = 0;
+		tile = malloc(tile_size);
+		if (!tile)
+			err("%s: Failed to allocate memory for tile",
+			    __func__);
+		/*
+		 * If the input image doesn't fill the last tile,
+		 * `gb_i` will reach `gb_size`.
+		 */
+		for (i = 0; i < tile_size && gb_i < gb_size; i++) {
+			tile[i] = gb->data[gb_i];
+			gb_i++;
+		}
+		if (opts->unique) {
+			if (opts->mirror) {
+				index = get_mirrored_tile_index(tile, tiles,
+								num_tiles,
+								tile_size,
+								&flags);
+			} else {
+				index = get_tile_index(tile, tiles, num_tiles,
+						       tile_size);
+			}
+
+			if (index < 0) {
+				index = num_tiles;
+				tiles[num_tiles] = tile;
+				num_tiles++;
+			} else {
+				free(tile);
+			}
+		} else {
+			index = num_tiles;
+			tiles[num_tiles] = tile;
+			num_tiles++;
+		}
+		if (*opts->tilemapfile) {
+			tilemap->data[tilemap->size] = index;
+			tilemap->size++;
+		}
+		if (*opts->attrmapfile) {
+			attrmap->data[attrmap->size] = flags;
+			attrmap->size++;
+		}
+	}
+
+	if (opts->unique) {
+		free(gb->data);
+		gb->data = malloc(tile_size * num_tiles);
+		if (!gb->data)
+			err("%s: Failed to allocate memory for tile data",
+			    __func__);
+		for (i = 0; i < num_tiles; i++) {
+			tile = tiles[i];
+			for (j = 0; j < tile_size; j++)
+				gb->data[i * tile_size + j] = tile[j];
+		}
+		gb->size = i * tile_size;
+	}
+
+	for (i = 0; i < num_tiles; i++)
+		free(tiles[i]);
+
+	free(tiles);
+}
+
+void output_tilemap_file(const struct Options *opts,
+			 const struct Mapfile *tilemap)
+{
+	FILE *f;
+
+	f = fopen(opts->tilemapfile, "wb");
+	if (!f)
+		err("%s: Opening tilemap file '%s' failed", __func__,
+		    opts->tilemapfile);
+
+	fwrite(tilemap->data, 1, tilemap->size, f);
+	fclose(f);
+
+	if (opts->tilemapout)
+		free(opts->tilemapfile);
+}
+
+void output_attrmap_file(const struct Options *opts,
+			 const struct Mapfile *attrmap)
+{
+	FILE *f;
+
+	f = fopen(opts->attrmapfile, "wb");
+	if (!f)
+		err("%s: Opening attrmap file '%s' failed", __func__,
+		    opts->attrmapfile);
+
+	fwrite(attrmap->data, 1, attrmap->size, f);
+	fclose(f);
+
+	if (opts->attrmapout)
+		free(opts->attrmapfile);
+}
+
+/*
+ * based on the Gaussian-like curve used by SameBoy since commit
+ * 65dd02cc52f531dbbd3a7e6014e99d5b24e71a4c (Oct 2017)
+ * with ties resolved by comparing the difference of the squares.
+ */
+static int reverse_curve[] = {
+	0,  0,  1,  1,  2,  2,  3,  3,  3,  3,  4,  4,  4,  4,  4,  4,
+	5,  5,  5,  5,  5,  5,  6,  6,  6,  6,  6,  6,  6,  6,  7,  7,
+	7,  7,  7,  7,  7,  7,  7,  8,  8,  8,  8,  8,  8,  8,  8,  8,
+	9,  9,  9,  9,  9,  9,  9,  9,  9,  9,  10, 10, 10, 10, 10, 10,
+	10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+	12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13,
+	13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14,
+	14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+	16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17,
+	17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18,
+	18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19,
+	19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21,
+	21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22,
+	22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24,
+	24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26,
+	26, 27, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 30, 30, 31,
+};
+
+void output_palette_file(const struct Options *opts,
+			 const struct RawIndexedImage *raw_image)
+{
+	FILE *f;
+	int i, color;
+	uint8_t cur_bytes[2];
+
+	f = fopen(opts->palfile, "wb");
+	if (!f)
+		err("%s: Opening palette file '%s' failed", __func__,
+		    opts->palfile);
+
+	for (i = 0; i < raw_image->num_colors; i++) {
+		int r = raw_image->palette[i].red;
+		int g = raw_image->palette[i].green;
+		int b = raw_image->palette[i].blue;
+
+		if (opts->colorcurve) {
+			g = (g * 4 - b) / 3;
+			if (g < 0)
+				g = 0;
+
+			r = reverse_curve[r];
+			g = reverse_curve[g];
+			b = reverse_curve[b];
+		} else {
+			r >>= 3;
+			g >>= 3;
+			b >>= 3;
+		}
+
+		color = b << 10 | g << 5 | r;
+		cur_bytes[0] = color & 0xFF;
+		cur_bytes[1] = color >> 8;
+		fwrite(cur_bytes, 2, 1, f);
+	}
+	fclose(f);
+
+	if (opts->palout)
+		free(opts->palfile);
+}
--- /dev/null
+++ b/src/gfx/getopt.c
@@ -1,0 +1,1 @@
+#include "../extern/getopt.c"
--- /dev/null
+++ b/src/gfx/main.c
@@ -1,0 +1,358 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2013-2018, stag019 and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <png.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "gfx/main.h"
+
+#include "extern/getopt.h"
+#include "version.h"
+
+int depth, colors;
+
+/* Short options */
+static char const *optstring = "Aa:CDd:Ffhmo:Pp:Tt:uVvx:";
+
+/*
+ * Equivalent long options
+ * Please keep in the same order as short opts
+ *
+ * Also, make sure long opts don't create ambiguity:
+ * A long opt's name should start with the same letter as its short opt,
+ * except if it doesn't create any ambiguity (`verbose` versus `version`).
+ * This is because long opt matching, even to a single char, is prioritized
+ * over short opt matching
+ */
+static struct option const longopts[] = {
+	{ "output-attr-map", no_argument,       NULL, 'A' },
+	{ "attr-map",        required_argument, NULL, 'a' },
+	{ "color-curve",     no_argument,       NULL, 'C' },
+	{ "debug",           no_argument,       NULL, 'D' },
+	{ "depth",           required_argument, NULL, 'd' },
+	{ "fix",             no_argument,       NULL, 'f' },
+	{ "fix-and-save",    no_argument,       NULL, 'F' },
+	{ "horizontal",      no_argument,       NULL, 'h' },
+	{ "mirror-tiles",    no_argument,       NULL, 'm' },
+	{ "output",          required_argument, NULL, 'o' },
+	{ "output-palette",  no_argument,       NULL, 'P' },
+	{ "palette",         required_argument, NULL, 'p' },
+	{ "output-tilemap",  no_argument,       NULL, 'T' },
+	{ "tilemap",         required_argument, NULL, 't' },
+	{ "unique-tiles",    no_argument,       NULL, 'u' },
+	{ "version",         no_argument,       NULL, 'V' },
+	{ "verbose",         no_argument,       NULL, 'v' },
+	{ "trim-end",        required_argument, NULL, 'x' },
+	{ NULL,              no_argument,       NULL, 0   }
+};
+
+static void print_usage(void)
+{
+	fputs(
+"Usage: rgbgfx [-CDhmuVv] [-f | -F] [-a <attr_map> | -A] [-d <depth>]\n"
+"              [-o <out_file>] [-p <pal_file> | -P] [-t <tile_map> | -T]\n"
+"              [-x <tiles>] <file>\n"
+"Useful options:\n"
+"    -f, --fix                 make the input image an indexed PNG\n"
+"    -m, --mirror-tiles        optimize out mirrored tiles\n"
+"    -o, --output <path>       set the output binary file\n"
+"    -t, --tilemap <path>      set the output tilemap file\n"
+"    -u, --unique-tiles        optimize out identical tiles\n"
+"    -V, --version             print RGBGFX version and exit\n"
+"\n"
+"For help, use `man rgbgfx' or go to https://rgbds.gbdev.io/docs/\n",
+	      stderr);
+	exit(1);
+}
+
+int main(int argc, char *argv[])
+{
+	int ch, size;
+	struct Options opts = {0};
+	struct ImageOptions png_options = {0};
+	struct RawIndexedImage *raw_image;
+	struct GBImage gb = {0};
+	struct Mapfile tilemap = {0};
+	struct Mapfile attrmap = {0};
+	char *ext;
+
+	opts.tilemapfile = "";
+	opts.attrmapfile = "";
+	opts.palfile = "";
+	opts.outfile = "";
+
+	depth = 2;
+
+	while ((ch = musl_getopt_long_only(argc, argv, optstring, longopts,
+					   NULL)) != -1) {
+		switch (ch) {
+		case 'A':
+			opts.attrmapout = true;
+			break;
+		case 'a':
+			opts.attrmapfile = musl_optarg;
+			break;
+		case 'C':
+			opts.colorcurve = true;
+			break;
+		case 'D':
+			opts.debug = true;
+			break;
+		case 'd':
+			depth = strtoul(musl_optarg, NULL, 0);
+			break;
+		case 'F':
+			opts.hardfix = true;
+			/* fallthrough */
+		case 'f':
+			opts.fix = true;
+			break;
+		case 'h':
+			opts.horizontal = true;
+			break;
+		case 'm':
+			opts.mirror = true;
+			opts.unique = true;
+			break;
+		case 'o':
+			opts.outfile = musl_optarg;
+			break;
+		case 'P':
+			opts.palout = true;
+			break;
+		case 'p':
+			opts.palfile = musl_optarg;
+			break;
+		case 'T':
+			opts.tilemapout = true;
+			break;
+		case 't':
+			opts.tilemapfile = musl_optarg;
+			break;
+		case 'u':
+			opts.unique = true;
+			break;
+		case 'V':
+			printf("rgbgfx %s\n", get_package_version_string());
+			exit(0);
+		case 'v':
+			opts.verbose = true;
+			break;
+		case 'x':
+			opts.trim = strtoul(musl_optarg, NULL, 0);
+			break;
+		default:
+			print_usage();
+			/* NOTREACHED */
+		}
+	}
+	argc -= musl_optind;
+	argv += musl_optind;
+
+	if (argc == 0) {
+		fputs("FATAL: no input files\n", stderr);
+		print_usage();
+	}
+
+#define WARN_MISMATCH(property) \
+	warnx("The PNG's " property \
+	      " setting doesn't match the one defined on the command line")
+
+	opts.infile = argv[argc - 1];
+
+	if (depth != 1 && depth != 2)
+		errx("Depth option must be either 1 or 2.");
+
+	colors = 1 << depth;
+
+	raw_image = input_png_file(&opts, &png_options);
+
+	png_options.tilemapfile = "";
+	png_options.attrmapfile = "";
+	png_options.palfile = "";
+
+	if (png_options.horizontal != opts.horizontal) {
+		if (opts.verbose)
+			WARN_MISMATCH("horizontal");
+
+		if (opts.hardfix)
+			png_options.horizontal = opts.horizontal;
+	}
+
+	if (png_options.horizontal)
+		opts.horizontal = png_options.horizontal;
+
+	if (png_options.trim != opts.trim) {
+		if (opts.verbose)
+			WARN_MISMATCH("trim");
+
+		if (opts.hardfix)
+			png_options.trim = opts.trim;
+	}
+
+	if (png_options.trim)
+		opts.trim = png_options.trim;
+
+	if (raw_image->width % 8) {
+		errx("Input PNG file %s not sized correctly. The image's width must be a multiple of 8.",
+		     opts.infile);
+	}
+	if (raw_image->width / 8 > 1 && raw_image->height % 8) {
+		errx("Input PNG file %s not sized correctly. If the image is more than 1 tile wide, its height must be a multiple of 8.",
+		     opts.infile);
+	}
+
+	if (opts.trim &&
+	    opts.trim > (raw_image->width / 8) * (raw_image->height / 8) - 1) {
+		errx("Trim (%d) for input raw_image file '%s' too large (max: %u)",
+		     opts.trim, opts.infile,
+		     (raw_image->width / 8) * (raw_image->height / 8) - 1);
+	}
+
+	if (strcmp(png_options.tilemapfile, opts.tilemapfile) != 0) {
+		if (opts.verbose)
+			WARN_MISMATCH("tilemap file");
+
+		if (opts.hardfix)
+			png_options.tilemapfile = opts.tilemapfile;
+	}
+	if (!*opts.tilemapfile)
+		opts.tilemapfile = png_options.tilemapfile;
+
+	if (png_options.tilemapout != opts.tilemapout) {
+		if (opts.verbose)
+			WARN_MISMATCH("tilemap file");
+
+		if (opts.hardfix)
+			png_options.tilemapout = opts.tilemapout;
+	}
+	if (png_options.tilemapout)
+		opts.tilemapout = png_options.tilemapout;
+
+	if (strcmp(png_options.attrmapfile, opts.attrmapfile) != 0) {
+		if (opts.verbose)
+			WARN_MISMATCH("attrmap file");
+
+		if (opts.hardfix)
+			png_options.attrmapfile = opts.attrmapfile;
+	}
+	if (!*opts.attrmapfile)
+		opts.attrmapfile = png_options.attrmapfile;
+
+	if (png_options.attrmapout != opts.attrmapout) {
+		if (opts.verbose)
+			WARN_MISMATCH("attrmap file");
+
+		if (opts.hardfix)
+			png_options.attrmapout = opts.attrmapout;
+	}
+	if (png_options.attrmapout)
+		opts.attrmapout = png_options.attrmapout;
+
+	if (strcmp(png_options.palfile, opts.palfile) != 0) {
+		if (opts.verbose)
+			WARN_MISMATCH("palette file");
+
+		if (opts.hardfix)
+			png_options.palfile = opts.palfile;
+	}
+	if (!*opts.palfile)
+		opts.palfile = png_options.palfile;
+
+	if (png_options.palout != opts.palout) {
+		if (opts.verbose)
+			WARN_MISMATCH("palette file");
+
+		if (opts.hardfix)
+			png_options.palout = opts.palout;
+	}
+
+#undef WARN_MISMATCH
+
+	if (png_options.palout)
+		opts.palout = png_options.palout;
+
+	if (!*opts.tilemapfile && opts.tilemapout) {
+		ext = strrchr(opts.infile, '.');
+
+		if (ext != NULL) {
+			size = ext - opts.infile + 9;
+			opts.tilemapfile = malloc(size);
+			strncpy(opts.tilemapfile, opts.infile, size);
+			*strrchr(opts.tilemapfile, '.') = '\0';
+			strcat(opts.tilemapfile, ".tilemap");
+		} else {
+			opts.tilemapfile = malloc(strlen(opts.infile) + 9);
+			strcpy(opts.tilemapfile, opts.infile);
+			strcat(opts.tilemapfile, ".tilemap");
+		}
+	}
+
+	if (!*opts.attrmapfile && opts.attrmapout) {
+		ext = strrchr(opts.infile, '.');
+
+		if (ext != NULL) {
+			size = ext - opts.infile + 9;
+			opts.attrmapfile = malloc(size);
+			strncpy(opts.attrmapfile, opts.infile, size);
+			*strrchr(opts.attrmapfile, '.') = '\0';
+			strcat(opts.attrmapfile, ".attrmap");
+		} else {
+			opts.attrmapfile = malloc(strlen(opts.infile) + 9);
+			strcpy(opts.attrmapfile, opts.infile);
+			strcat(opts.attrmapfile, ".attrmap");
+		}
+	}
+
+	if (!*opts.palfile && opts.palout) {
+		ext = strrchr(opts.infile, '.');
+
+		if (ext != NULL) {
+			size = ext - opts.infile + 5;
+			opts.palfile = malloc(size);
+			strncpy(opts.palfile, opts.infile, size);
+			*strrchr(opts.palfile, '.') = '\0';
+			strcat(opts.palfile, ".pal");
+		} else {
+			opts.palfile = malloc(strlen(opts.infile) + 5);
+			strcpy(opts.palfile, opts.infile);
+			strcat(opts.palfile, ".pal");
+		}
+	}
+
+	gb.size = raw_image->width * raw_image->height * depth / 8;
+	gb.data = calloc(gb.size, 1);
+	gb.trim = opts.trim;
+	gb.horizontal = opts.horizontal;
+
+	if (*opts.outfile || *opts.tilemapfile || *opts.attrmapfile) {
+		raw_to_gb(raw_image, &gb);
+		create_mapfiles(&opts, &gb, &tilemap, &attrmap);
+	}
+
+	if (*opts.outfile)
+		output_file(&opts, &gb);
+
+	if (*opts.tilemapfile)
+		output_tilemap_file(&opts, &tilemap);
+
+	if (*opts.attrmapfile)
+		output_attrmap_file(&opts, &attrmap);
+
+	if (*opts.palfile)
+		output_palette_file(&opts, raw_image);
+
+	if (opts.fix || opts.debug)
+		output_png_file(&opts, &png_options, raw_image);
+
+	destroy_raw_image(&raw_image);
+	free(gb.data);
+
+	return 0;
+}
--- a/src/gfx/main.cpp
+++ /dev/null
@@ -1,828 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#include "gfx/main.hpp"
-
-#include <algorithm>
-#include <assert.h>
-#include <cinttypes>
-#include <cstdint>
-#include <ctype.h>
-#include <fstream>
-#include <ios>
-#include <limits>
-#include <numeric>
-#include <stdarg.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <string_view>
-#include <type_traits>
-
-#include "extern/getopt.h"
-#include "file.hpp"
-#include "platform.h"
-#include "version.h"
-
-#include "gfx/pal_spec.hpp"
-#include "gfx/process.hpp"
-#include "gfx/reverse.hpp"
-
-using namespace std::literals::string_view_literals;
-
-Options options;
-char const *externalPalSpec = nullptr;
-static uintmax_t nbErrors;
-
-[[noreturn]] void giveUp() {
-	fprintf(stderr, "Conversion aborted after %ju error%s\n", nbErrors, nbErrors == 1 ? "" : "s");
-	exit(1);
-}
-
-void warning(char const *fmt, ...) {
-	va_list ap;
-
-	fputs("warning: ", stderr);
-	va_start(ap, fmt);
-	vfprintf(stderr, fmt, ap);
-	va_end(ap);
-	putc('\n', stderr);
-}
-
-void error(char const *fmt, ...) {
-	va_list ap;
-
-	fputs("error: ", stderr);
-	va_start(ap, fmt);
-	vfprintf(stderr, fmt, ap);
-	va_end(ap);
-	putc('\n', stderr);
-
-	if (nbErrors != std::numeric_limits<decltype(nbErrors)>::max())
-		nbErrors++;
-}
-
-[[noreturn]] void fatal(char const *fmt, ...) {
-	va_list ap;
-
-	fputs("FATAL: ", stderr);
-	va_start(ap, fmt);
-	vfprintf(stderr, fmt, ap);
-	va_end(ap);
-	putc('\n', stderr);
-
-	if (nbErrors != std::numeric_limits<decltype(nbErrors)>::max())
-		nbErrors++;
-
-	giveUp();
-}
-
-void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
-	if (verbosity >= level) {
-		va_list ap;
-
-		va_start(ap, fmt);
-		vfprintf(stderr, fmt, ap);
-		va_end(ap);
-	}
-}
-
-// Short options
-static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:o:Pp:Qq:r:s:Tt:U:uVvx:Z";
-
-/*
- * Equivalent long options
- * Please keep in the same order as short opts
- *
- * Also, make sure long opts don't create ambiguity:
- * A long opt's name should start with the same letter as its short opt,
- * except if it doesn't create any ambiguity (`verbose` versus `version`).
- * This is because long opt matching, even to a single char, is prioritized
- * over short opt matching
- */
-static struct option const longopts[] = {
-    {"output-attr-map",    no_argument,       NULL, 'A'},
-    {"attr-map",           required_argument, NULL, 'a'},
-    {"base-tiles",         required_argument, NULL, 'b'},
-    {"color-curve",        no_argument,       NULL, 'C'},
-    {"colors",             required_argument, NULL, 'c'},
-    {"debug",              no_argument,       NULL, 'D'}, // Ignored
-    {"depth",              required_argument, NULL, 'd'},
-    {"fix",                no_argument,       NULL, 'f'},
-    {"fix-and-save",       no_argument,       NULL, 'F'}, // Deprecated
-    {"horizontal",         no_argument,       NULL, 'h'}, // Deprecated
-    {"slice",              required_argument, NULL, 'L'},
-    {"mirror-tiles",       no_argument,       NULL, 'm'},
-    {"nb-tiles",           required_argument, NULL, 'N'},
-    {"nb-palettes",        required_argument, NULL, 'n'},
-    {"output",             required_argument, NULL, 'o'},
-    {"output-palette",     no_argument,       NULL, 'P'},
-    {"palette",            required_argument, NULL, 'p'},
-    {"output-palette-map", no_argument,       NULL, 'Q'},
-    {"palette-map",        required_argument, NULL, 'q'},
-    {"reverse",            required_argument, NULL, 'r'},
-    {"output-tilemap",     no_argument,       NULL, 'T'},
-    {"tilemap",            required_argument, NULL, 't'},
-    {"unit-size",          required_argument, NULL, 'U'},
-    {"unique-tiles",       no_argument,       NULL, 'u'},
-    {"version",            no_argument,       NULL, 'V'},
-    {"verbose",            no_argument,       NULL, 'v'},
-    {"trim-end",           required_argument, NULL, 'x'},
-    {"columns",            no_argument,       NULL, 'Z'},
-    {NULL,                 no_argument,       NULL, 0  }
-};
-
-static void printUsage(void) {
-	fputs("Usage: rgbgfx [-r stride] [-CmuVZ] [-v [-v ...]] [-a <attr_map> | -A]\n"
-	      "       [-b <base_ids>] [-c <colors>] [-d <depth>] [-L <slice>] [-N <nb_tiles>]\n"
-	      "       [-n <nb_pals>] [-o <out_file>] [-p <pal_file> | -P] [-q <pal_map> | -Q]\n"
-	      "       [-s <nb_colors>] [-t <tile_map> | -T] [-x <nb_tiles>] <file>\n"
-	      "Useful options:\n"
-	      "    -m, --mirror-tiles    optimize out mirrored tiles\n"
-	      "    -o, --output <path>   output the tile data to this path\n"
-	      "    -t, --tilemap <path>  output the tile map to this path\n"
-	      "    -u, --unique-tiles    optimize out identical tiles\n"
-	      "    -V, --version         print RGBGFX version and exit\n"
-	      "\n"
-	      "For help, use `man rgbgfx' or go to https://rgbds.gbdev.io/docs/\n",
-	      stderr);
-	exit(1);
-}
-
-/*
- * Parses a number at the beginning of a string, moving the pointer to skip the parsed characters
- * Returns the provided errVal on error
- */
-static uint16_t parseNumber(char *&string, char const *errPrefix, uint16_t errVal = UINT16_MAX) {
-	uint8_t base = 10;
-	if (*string == '\0') {
-		error("%s: expected number, but found nothing", errPrefix);
-		return errVal;
-	} else if (*string == '$') {
-		base = 16;
-		++string;
-	} else if (*string == '%') {
-		base = 2;
-		++string;
-	} else if (*string == '0' && string[1] != '\0') {
-		// Check if we have a "0x" or "0b" here
-		if (string[1] == 'x' || string[1] == 'X') {
-			base = 16;
-			string += 2;
-		} else if (string[1] == 'b' || string[1] == 'B') {
-			base = 2;
-			string += 2;
-		}
-	}
-
-	/*
-	 * Turns a digit into its numeric value in the current base, if it has one.
-	 * Maximum is inclusive. The string_view is modified to "consume" all digits.
-	 * Returns 255 on parse failure (including wrong char for base), in which case
-	 * the string_view may be pointing on garbage.
-	 */
-	auto charIndex = [&base](unsigned char c) -> uint8_t {
-		unsigned char index = c - '0'; // Use wrapping semantics
-		if (base == 2 && index >= 2) {
-			return 255;
-		} else if (index < 10) {
-			return index;
-		} else if (base != 16) {
-			return 255; // Letters are only valid in hex
-		}
-		index = tolower(c) - 'a'; // OK because we pass an `unsigned char`
-		if (index < 6) {
-			return index + 10;
-		}
-		return 255;
-	};
-
-	if (charIndex(*string) == 255) {
-		error("%s: expected digit%s, but found nothing", errPrefix,
-		      base != 10 ? " after base" : "");
-		return errVal;
-	}
-	uint16_t number = 0;
-	do {
-		// Read a character, and check if it's valid in the given base
-		uint8_t index = charIndex(*string);
-		if (index == 255) {
-			break; // Found an invalid character, end
-		}
-		++string;
-
-		number *= base;
-		number += index;
-		// The lax check covers the addition on top of the multiplication
-		if (number >= UINT16_MAX / base) {
-			error("%s: the number is too large!", errPrefix);
-			return errVal;
-		}
-	} while (*string != '\0'); // No more characters?
-
-	return number;
-}
-
-static void skipWhitespace(char *&arg) {
-	arg += strspn(arg, " \t");
-}
-
-static void registerInput(char const *arg) {
-	if (!options.input.empty()) {
-		fprintf(stderr,
-		        "FATAL: input image specified more than once! (first \"%s\", then "
-		        "\"%s\")\n",
-		        options.input.c_str(), arg);
-		printUsage();
-		exit(1);
-	} else if (arg[0] == '\0') { // Empty input path
-		fprintf(stderr, "FATAL: input image path cannot be empty!\n");
-		printUsage();
-		exit(1);
-	} else {
-		options.input = arg;
-	}
-}
-
-/*
- * Turn an "at-file"'s contents into an argv that `getopt` can handle
- * @param argPool Argument characters will be appended to this vector, for storage purposes.
- */
-static std::vector<size_t> readAtFile(std::string const &path, std::vector<char> &argPool) {
-	File file;
-	if (!file.open(path, std::ios_base::in)) {
-		fatal("Error reading @%s: %s", file.c_str(path), strerror(errno));
-	}
-
-	// We only filter out `EOF`, but calling `isblank()` on anything else is UB!
-	static_assert(std::remove_reference_t<decltype(*file)>::traits_type::eof() == EOF,
-	              "isblank(char_traits<...>::eof()) is UB!");
-	std::vector<size_t> argvOfs;
-
-	for (;;) {
-		int c;
-
-		// First, discard any leading whitespace
-		do {
-			c = file->sbumpc();
-			if (c == EOF) {
-				return argvOfs;
-			}
-		} while (isblank(c));
-
-		switch (c) {
-		case '#': // If it's a comment, discard everything until EOL
-			while ((c = file->sbumpc()) != '\n') {
-				if (c == EOF) {
-					return argvOfs;
-				}
-			}
-			continue; // Start processing the next line
-		// If it's an empty line, ignore it
-		case '\r': // Assuming CRLF here
-			file->sbumpc(); // Discard the upcoming '\n'
-			[[fallthrough]];
-		case '\n':
-			continue; // Start processing the next line
-		}
-
-		// Alright, now we can parse the line
-		do {
-			// Read one argument (until the next whitespace char).
-			// We know there is one because we already have its first character in `c`.
-			argvOfs.push_back(argPool.size());
-			// Reading and appending characters one at a time may be inefficient, but I'm counting
-			// on `vector` and `sbumpc` to do the right thing here.
-			argPool.push_back(c); // Push the character we've already read
-			for (;;) {
-				c = file->sbumpc();
-				if (c == EOF || c == '\n' || isblank(c)) {
-					break;
-				} else if (c == '\r') {
-					file->sbumpc(); // Discard the '\n'
-					break;
-				}
-				argPool.push_back(c);
-			}
-			argPool.push_back('\0');
-
-			// Discard whitespace until the next argument (candidate)
-			while (isblank(c)) {
-				c = file->sbumpc();
-			}
-			if (c == '\r') {
-				c = file->sbumpc(); // Skip the '\n'
-			}
-		} while (c != '\n' && c != EOF); // End if we reached EOL
-	}
-}
-/*
- * Parses an arg vector, modifying `options` as options are read.
- * The three booleans are for the "auto path" flags, since their processing must be deferred to the
- * end of option parsing.
- *
- * Returns NULL if the vector was fully parsed, or a pointer (which is part of the arg vector) to an
- * "at-file" path if one is encountered.
- */
-static char *parseArgv(int argc, char **argv, bool &autoAttrmap, bool &autoTilemap,
-                       bool &autoPalettes, bool &autoPalmap) {
-	int opt;
-
-	while ((opt = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1) {
-		char *arg = musl_optarg; // Make a copy for scanning
-		switch (opt) {
-		case 'A':
-			autoAttrmap = true;
-			break;
-		case 'a':
-			autoAttrmap = false;
-			if (!options.attrmap.empty())
-				warning("Overriding attrmap file %s", options.attrmap.c_str());
-			options.attrmap = musl_optarg;
-			break;
-		case 'b':
-			options.baseTileIDs[0] = parseNumber(arg, "Bank 0 base tile ID", 0);
-			if (options.baseTileIDs[0] >= 256) {
-				error("Bank 0 base tile ID must be below 256");
-			}
-			if (*arg == '\0') {
-				options.baseTileIDs[1] = 0;
-				break;
-			}
-			skipWhitespace(arg);
-			if (*arg != ',') {
-				error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"",
-				      musl_optarg);
-				break;
-			}
-			++arg; // Skip comma
-			skipWhitespace(arg);
-			options.baseTileIDs[1] = parseNumber(arg, "Bank 1 base tile ID", 0);
-			if (options.baseTileIDs[1] >= 256) {
-				error("Bank 1 base tile ID must be below 256");
-			}
-			if (*arg != '\0') {
-				error("Base tile IDs must be one or two comma-separated numbers, not \"%s\"",
-				      musl_optarg);
-				break;
-			}
-			break;
-		case 'C':
-			options.useColorCurve = true;
-			break;
-		case 'c':
-			if (musl_optarg[0] == '#') {
-				options.palSpecType = Options::EXPLICIT;
-				parseInlinePalSpec(musl_optarg);
-			} else if (strcasecmp(musl_optarg, "embedded") == 0) {
-				// Use PLTE, error out if missing
-				options.palSpecType = Options::EMBEDDED;
-			} else {
-				options.palSpecType = Options::EXPLICIT;
-				// Can't parse the file yet, as "flat" color collections need to know the palette
-				// size to be split; thus, we defer that
-				// TODO: this does not validate the `fmt` part of any external spec but the last
-				// one, but I guess that's okay
-				externalPalSpec = musl_optarg;
-			}
-			break;
-		case 'd':
-			options.bitDepth = parseNumber(arg, "Bit depth", 2);
-			if (*arg != '\0') {
-				error("Bit depth (-b) argument must be a valid number, not \"%s\"", musl_optarg);
-			} else if (options.bitDepth != 1 && options.bitDepth != 2) {
-				error("Bit depth must be 1 or 2, not %" PRIu8);
-				options.bitDepth = 2;
-			}
-			break;
-		case 'L':
-			options.inputSlice.left = parseNumber(arg, "Input slice left coordinate");
-			if (options.inputSlice.left > INT16_MAX) {
-				error("Input slice left coordinate is out of range!");
-				break;
-			}
-			skipWhitespace(arg);
-			if (*arg != ',') {
-				error("Missing comma after left coordinate in \"%s\"", musl_optarg);
-				break;
-			}
-			++arg;
-			skipWhitespace(arg);
-			options.inputSlice.top = parseNumber(arg, "Input slice upper coordinate");
-			skipWhitespace(arg);
-			if (*arg != ':') {
-				error("Missing colon after upper coordinate in \"%s\"", musl_optarg);
-				break;
-			}
-			++arg;
-			skipWhitespace(arg);
-			options.inputSlice.width = parseNumber(arg, "Input slice width");
-			skipWhitespace(arg);
-			if (options.inputSlice.width == 0) {
-				error("Input slice width may not be 0!");
-			}
-			if (*arg != ',') {
-				error("Missing comma after width in \"%s\"", musl_optarg);
-				break;
-			}
-			++arg;
-			skipWhitespace(arg);
-			options.inputSlice.height = parseNumber(arg, "Input slice height");
-			if (options.inputSlice.height == 0) {
-				error("Input slice height may not be 0!");
-			}
-			if (*arg != '\0') {
-				error("Unexpected extra characters after slice spec in \"%s\"", musl_optarg);
-			}
-			break;
-		case 'm':
-			options.allowMirroring = true;
-			[[fallthrough]]; // Imply `-u`
-		case 'u':
-			options.allowDedup = true;
-			break;
-		case 'N':
-			options.maxNbTiles[0] = parseNumber(arg, "Number of tiles in bank 0", 256);
-			if (options.maxNbTiles[0] > 256) {
-				error("Bank 0 cannot contain more than 256 tiles");
-			}
-			if (*arg == '\0') {
-				options.maxNbTiles[1] = 0;
-				break;
-			}
-			skipWhitespace(arg);
-			if (*arg != ',') {
-				error("Bank capacity must be one or two comma-separated numbers, not \"%s\"",
-				      musl_optarg);
-				break;
-			}
-			++arg; // Skip comma
-			skipWhitespace(arg);
-			options.maxNbTiles[1] = parseNumber(arg, "Number of tiles in bank 1", 256);
-			if (options.maxNbTiles[1] > 256) {
-				error("Bank 1 cannot contain more than 256 tiles");
-			}
-			if (*arg != '\0') {
-				error("Bank capacity must be one or two comma-separated numbers, not \"%s\"",
-				      musl_optarg);
-				break;
-			}
-			break;
-		case 'n':
-			options.nbPalettes = parseNumber(arg, "Number of palettes", 256);
-			if (*arg != '\0') {
-				error("Number of palettes (-n) must be a valid number, not \"%s\"", musl_optarg);
-			}
-			if (options.nbPalettes > 256) {
-				error("Number of palettes (-n) must not exceed 256!");
-			} else if (options.nbPalettes == 0) {
-				error("Number of palettes (-n) may not be 0!");
-			}
-			break;
-		case 'o':
-			if (!options.output.empty())
-				warning("Overriding tile data file %s", options.output.c_str());
-			options.output = musl_optarg;
-			break;
-		case 'P':
-			autoPalettes = true;
-			break;
-		case 'p':
-			autoPalettes = false;
-			if (!options.palettes.empty())
-				warning("Overriding palettes file %s", options.palettes.c_str());
-			options.palettes = musl_optarg;
-			break;
-		case 'Q':
-			autoPalmap = true;
-			break;
-		case 'q':
-			autoPalmap = false;
-			if (!options.palmap.empty())
-				warning("Overriding palette map file %s", options.palmap.c_str());
-			options.palmap = 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) must be a valid number, not \"%s\"", musl_optarg);
-			}
-			if (options.nbColorsPerPal > 4) {
-				error("Palette size (-s) must not exceed 4!");
-			} else if (options.nbColorsPerPal == 0) {
-				error("Palette size (-s) may not be 0!");
-			}
-			break;
-		case 'T':
-			autoTilemap = true;
-			break;
-		case 't':
-			autoTilemap = false;
-			if (!options.tilemap.empty())
-				warning("Overriding tilemap file %s", options.tilemap.c_str());
-			options.tilemap = musl_optarg;
-			break;
-		case 'V':
-			printf("rgbgfx %s\n", get_package_version_string());
-			exit(0);
-		case 'v':
-			if (options.verbosity < Options::VERB_VVVVVV) {
-				++options.verbosity;
-			}
-			break;
-		case 'x':
-			options.trim = parseNumber(arg, "Number of tiles to trim", 0);
-			if (*arg != '\0') {
-				error("Tile trim (-x) argument must be a valid number, not \"%s\"", musl_optarg);
-			}
-			break;
-		case 'h':
-			warning("`-h` is deprecated, use `-Z` instead");
-			[[fallthrough]];
-		case 'Z':
-			options.columnMajor = true;
-			break;
-		case 1: // Positional argument, requested by leading `-` in opt string
-			if (musl_optarg[0] == '@') {
-				// Instruct the caller to process that at-file
-				return &musl_optarg[1];
-			} else {
-				registerInput(musl_optarg);
-			}
-			break;
-		case 'D':
-		case 'F':
-		case 'f':
-			warning("Ignoring retired option `-%c`", opt);
-			break;
-		default:
-			printUsage();
-			exit(1);
-		}
-	}
-
-	return nullptr; // Done processing this argv
-}
-
-int main(int argc, char *argv[]) {
-	bool autoAttrmap = false, autoTilemap = false, autoPalettes = false, autoPalmap = false;
-
-	struct AtFileStackEntry {
-		int parentInd; // Saved offset into parent argv
-		std::vector<char *> argv; // This context's arg pointer vec
-		std::vector<char> argPool;
-
-		AtFileStackEntry(int parentInd_, std::vector<char *> argv_)
-		    : parentInd(parentInd_), argv(argv_) {}
-	};
-	std::vector<AtFileStackEntry> atFileStack;
-
-	int curArgc = argc;
-	char **curArgv = argv;
-	for (;;) {
-		char *atFileName =
-		    parseArgv(curArgc, curArgv, autoAttrmap, autoTilemap, autoPalettes, autoPalmap);
-		if (atFileName) {
-			// Copy `argv[0]` for error reporting, and because option parsing skips it
-			AtFileStackEntry &stackEntry =
-			    atFileStack.emplace_back(musl_optind, std::vector{atFileName});
-			// It would be nice to compute the char pointers on the fly, but reallocs don't allow
-			// that; so we must compute the offsets after the pool is fixed
-			auto offsets = readAtFile(&musl_optarg[1], stackEntry.argPool);
-			stackEntry.argv.reserve(offsets.size() + 2); // Avoid a bunch of reallocs
-			for (size_t ofs : offsets) {
-				stackEntry.argv.push_back(&stackEntry.argPool.data()[ofs]);
-			}
-			stackEntry.argv.push_back(nullptr); // Don't forget the arg vector terminator!
-
-			curArgc = stackEntry.argv.size() - 1;
-			curArgv = stackEntry.argv.data();
-			musl_optind = 1; // Don't use 0 because we're not scanning a different argv per se
-			continue; // Begin scanning that arg vector
-		}
-
-		if (musl_optind != curArgc) {
-			// This happens if `--` is passed, process the remaining arg(s) as positional
-			assert(musl_optind < curArgc);
-			for (int i = musl_optind; i < curArgc; ++i) {
-				registerInput(argv[i]);
-			}
-		}
-
-		// Pop off the top stack entry, or end parsing if none
-		if (atFileStack.empty()) {
-			break;
-		}
-		// OK to restore `optind` directly, because `optpos` must be 0 right now.
-		// (Providing 0 would be a "proper" reset, but we want to resume parsing)
-		musl_optind = atFileStack.back().parentInd;
-		atFileStack.pop_back();
-		if (atFileStack.empty()) {
-			curArgc = argc;
-			curArgv = argv;
-		} else {
-			auto &vec = atFileStack.back().argv;
-			curArgc = vec.size();
-			curArgv = vec.data();
-		}
-	}
-
-	if (options.nbColorsPerPal == 0) {
-		options.nbColorsPerPal = 1u << options.bitDepth;
-	} else if (options.nbColorsPerPal > 1u << options.bitDepth) {
-		error("%" PRIu8 "bpp palettes can only contain %u colors, not %" PRIu8, options.bitDepth,
-		      1u << options.bitDepth, options.nbColorsPerPal);
-	}
-
-	auto autoOutPath = [](bool autoOptEnabled, std::string &path, char const *extension) {
-		if (autoOptEnabled) {
-			constexpr std::string_view chars =
-// Both must start with a dot!
-#if defined(_MSC_VER) || defined(__MINGW32__)
-			    "./\\"sv;
-#else
-			    "./"sv;
-#endif
-			size_t len = options.input.npos;
-			size_t i = options.input.find_last_of(chars);
-			if (i != options.input.npos && options.input[i] == '.') {
-				// We found the last dot, but check if it's part of a stem
-				// (There must be a non-path separator character before it)
-				if (i != 0 && chars.find(options.input[i - 1], 1) == chars.npos) {
-					// We can replace the extension
-					len = i;
-				}
-			}
-			path.assign(options.input, 0, len);
-			path.append(extension);
-		}
-	};
-	autoOutPath(autoAttrmap, options.attrmap, ".attrmap");
-	autoOutPath(autoTilemap, options.tilemap, ".tilemap");
-	autoOutPath(autoPalettes, options.palettes, ".pal");
-	autoOutPath(autoPalmap, options.palmap, ".palmap");
-
-	// Execute deferred external pal spec parsing, now that all other params are known
-	if (externalPalSpec) {
-		parseExternalPalSpec(externalPalSpec);
-	}
-
-	if (options.verbosity >= Options::VERB_CFG) {
-		fprintf(stderr, "rgbgfx %s\n", get_package_version_string());
-
-		if (options.verbosity >= Options::VERB_VVVVVV) {
-			fputc('\n', stderr);
-			static std::array<uint16_t, 21> gfx{
-			    0x1FE, 0x3FF, 0x399, 0x399, 0x3FF, 0x3FF, 0x381, 0x3C3, 0x1FE, 0x078, 0x1FE,
-			    0x3FF, 0x3FF, 0x3FF, 0x37B, 0x37B, 0x0FC, 0x0CC, 0x1CE, 0x1CE, 0x1CE,
-			};
-			static std::array<char const *, 3> textbox{
-			    "  ,----------------------------------------.",
-			    "  | Augh, dimensional interference again?! |",
-			    "  `----------------------------------------'"};
-			for (size_t i = 0; i < gfx.size(); ++i) {
-				uint16_t row = gfx[i];
-				for (uint8_t _ = 0; _ < 10; ++_) {
-					unsigned char c = row & 1 ? '0' : ' ';
-					fputc(c, stderr);
-					// Double the pixel horizontally, otherwise the aspect ratio looks wrong
-					fputc(c, stderr);
-					row >>= 1;
-				}
-				if (i < textbox.size()) {
-					fputs(textbox[i], stderr);
-				}
-				fputc('\n', stderr);
-			}
-			fputc('\n', stderr);
-		}
-
-		fputs("Options:\n", stderr);
-		if (options.columnMajor)
-			fputs("\tVisit image in column-major order\n", stderr);
-		if (options.allowMirroring)
-			fputs("\tAllow mirroring tiles\n", stderr);
-		if (options.allowDedup)
-			fputs("\tAllow deduplicating tiles\n", stderr);
-		if (options.useColorCurve)
-			fputs("\tUse color curve\n", stderr);
-		fprintf(stderr, "\tBit depth: %" PRIu8 "bpp\n", options.bitDepth);
-		if (options.trim != 0)
-			fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
-		fprintf(stderr, "\tMaximum %" PRIu8 " palettes\n", options.nbPalettes);
-		fprintf(stderr, "\tPalettes contain %" PRIu8 " colors\n", options.nbColorsPerPal);
-		fprintf(stderr, "\t%s palette spec\n", []() {
-			switch (options.palSpecType) {
-			case Options::NO_SPEC:
-				return "No";
-			case Options::EXPLICIT:
-				return "Explicit";
-			case Options::EMBEDDED:
-				return "Embedded";
-			}
-			return "???";
-		}());
-		if (options.palSpecType == Options::EXPLICIT) {
-			fputs("\t[\n", stderr);
-			for (std::array<Rgba, 4> const &pal : options.palSpec) {
-				fprintf(stderr, "\t\t#%06x, #%06x, #%06x, #%06x,\n", pal[0].toCSS() >> 8,
-				        pal[1].toCSS() >> 8, pal[2].toCSS() >> 8, pal[3].toCSS() >> 8);
-			}
-			fputs("\t]\n", stderr);
-		}
-		fprintf(stderr,
-		        "\tInput image slice: %" PRIu32 "x%" PRIu32 " pixels starting at (%" PRIi32
-		        ", %" PRIi32 ")\n",
-		        options.inputSlice.width, options.inputSlice.height, options.inputSlice.left,
-		        options.inputSlice.top);
-		fprintf(stderr, "\tBase tile IDs: [%" PRIu8 ", %" PRIu8 "]\n", options.baseTileIDs[0],
-		        options.baseTileIDs[1]);
-		fprintf(stderr, "\tMaximum %" PRIu16 " tiles in bank 0, %" PRIu16 " in bank 1\n",
-		        options.maxNbTiles[0], options.maxNbTiles[1]);
-		auto printPath = [](char const *name, std::string const &path) {
-			if (!path.empty()) {
-				fprintf(stderr, "\t%s: %s\n", name, path.c_str());
-			}
-		};
-		printPath("Input image", options.input);
-		printPath("Output tile data", options.output);
-		printPath("Output tilemap", options.tilemap);
-		printPath("Output attrmap", options.attrmap);
-		printPath("Output palettes", options.palettes);
-		fputs("Ready.\n", stderr);
-	}
-
-	if (options.input.empty()) {
-		fatal("No input image specified");
-	}
-
-	// Do not do anything if option parsing went wrong
-	if (nbErrors) {
-		giveUp();
-	}
-
-	if (options.reverse()) {
-		reverse();
-	} else {
-		process();
-	}
-
-	if (nbErrors) {
-		giveUp();
-	}
-	return 0;
-}
-
-void Palette::addColor(uint16_t color) {
-	for (size_t i = 0; true; ++i) {
-		assert(i < colors.size()); // The packing should guarantee this
-		if (colors[i] == color) { // The color is already present
-			break;
-		} else if (colors[i] == UINT16_MAX) { // Empty slot
-			colors[i] = color;
-			break;
-		}
-	}
-}
-
-/*
- * Returns the ID of the color in the palette, or `size()` if the color is not in
- */
-uint8_t Palette::indexOf(uint16_t color) const {
-	return std::find(colors.begin(), colors.end(), color) - colors.begin();
-}
-
-auto Palette::begin() -> decltype(colors)::iterator {
-	// Skip the first slot if reserved for transparency
-	return colors.begin() + options.hasTransparentPixels;
-}
-auto Palette::end() -> decltype(colors)::iterator {
-	return std::find(begin(), colors.end(), UINT16_MAX);
-}
-
-auto Palette::begin() const -> decltype(colors)::const_iterator {
-	// Skip the first slot if reserved for transparency
-	return colors.begin() + options.hasTransparentPixels;
-}
-auto Palette::end() const -> decltype(colors)::const_iterator {
-	return std::find(begin(), colors.end(), UINT16_MAX);
-}
-
-uint8_t Palette::size() const {
-	return indexOf(UINT16_MAX);
-}
--- /dev/null
+++ b/src/gfx/makepng.c
@@ -1,0 +1,806 @@
+/*
+ * This file is part of RGBDS.
+ *
+ * Copyright (c) 2013-2018, stag019 and RGBDS contributors.
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <png.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "gfx/makepng.h"
+
+static void initialize_png(struct PNGImage *img, FILE * f);
+static struct RawIndexedImage *indexed_png_to_raw(struct PNGImage *img);
+static struct RawIndexedImage *grayscale_png_to_raw(struct PNGImage *img);
+static struct RawIndexedImage *truecolor_png_to_raw(struct PNGImage *img);
+static void get_text(const struct PNGImage *img,
+		     struct ImageOptions *png_options);
+static void set_text(const struct PNGImage *img,
+		     const struct ImageOptions *png_options);
+static void free_png_data(const struct PNGImage *png);
+
+struct RawIndexedImage *input_png_file(const struct Options *opts,
+				       struct ImageOptions *png_options)
+{
+	struct PNGImage img;
+	struct RawIndexedImage *raw_image;
+	FILE *f;
+
+	f = fopen(opts->infile, "rb");
+	if (!f)
+		err("Opening input png file '%s' failed", opts->infile);
+
+	initialize_png(&img, f);
+
+	if (img.depth != depth) {
+		if (opts->verbose) {
+			warnx("Image bit depth is not %d (is %d).",
+			      depth, img.depth);
+		}
+	}
+
+	switch (img.type) {
+	case PNG_COLOR_TYPE_PALETTE:
+		raw_image = indexed_png_to_raw(&img); break;
+	case PNG_COLOR_TYPE_GRAY:
+	case PNG_COLOR_TYPE_GRAY_ALPHA:
+		raw_image = grayscale_png_to_raw(&img); break;
+	case PNG_COLOR_TYPE_RGB:
+	case PNG_COLOR_TYPE_RGB_ALPHA:
+		raw_image = truecolor_png_to_raw(&img); break;
+	default:
+		/* Shouldn't happen, but might as well handle just in case. */
+		errx("Input PNG file is of invalid color type.");
+	}
+
+	get_text(&img, png_options);
+
+	png_destroy_read_struct(&img.png, &img.info, NULL);
+	fclose(f);
+	free_png_data(&img);
+
+	return raw_image;
+}
+
+void output_png_file(const struct Options *opts,
+		     const struct ImageOptions *png_options,
+		     const struct RawIndexedImage *raw_image)
+{
+	FILE *f;
+	char *outfile;
+	struct PNGImage img;
+	png_color *png_palette;
+	int i;
+
+	/*
+	 * TODO: Variable outfile is for debugging purposes. Eventually,
+	 * opts.infile will be used directly.
+	 */
+	if (opts->debug) {
+		outfile = malloc(strlen(opts->infile) + 5);
+		if (!outfile)
+			err("%s: Failed to allocate memory for outfile",
+			    __func__);
+		strcpy(outfile, opts->infile);
+		strcat(outfile, ".out");
+	} else {
+		outfile = opts->infile;
+	}
+
+	f = fopen(outfile, "wb");
+	if (!f)
+		err("Opening output png file '%s' failed", outfile);
+
+	if (opts->debug)
+		free(outfile);
+
+	img.png = png_create_write_struct(PNG_LIBPNG_VER_STRING,
+					  NULL, NULL, NULL);
+	if (!img.png)
+		errx("Creating png structure failed");
+
+	img.info = png_create_info_struct(img.png);
+	if (!img.info)
+		errx("Creating png info structure failed");
+
+	if (setjmp(png_jmpbuf(img.png)))
+		exit(1);
+
+	png_init_io(img.png, f);
+
+	png_set_IHDR(img.png, img.info, raw_image->width, raw_image->height,
+		     8, PNG_COLOR_TYPE_PALETTE, PNG_INTERLACE_NONE,
+		     PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
+
+	png_palette = malloc(sizeof(*png_palette) * raw_image->num_colors);
+	if (!png_palette)
+		err("%s: Failed to allocate memory for PNG palette",
+		    __func__);
+	for (i = 0; i < raw_image->num_colors; i++) {
+		png_palette[i].red   = raw_image->palette[i].red;
+		png_palette[i].green = raw_image->palette[i].green;
+		png_palette[i].blue  = raw_image->palette[i].blue;
+	}
+	png_set_PLTE(img.png, img.info, png_palette, raw_image->num_colors);
+	free(png_palette);
+
+	if (opts->fix)
+		set_text(&img, png_options);
+
+	png_write_info(img.png, img.info);
+
+	png_write_image(img.png, (png_byte **) raw_image->data);
+	png_write_end(img.png, NULL);
+
+	png_destroy_write_struct(&img.png, &img.info);
+	fclose(f);
+}
+
+void destroy_raw_image(struct RawIndexedImage **raw_image_ptr_ptr)
+{
+	struct RawIndexedImage *raw_image = *raw_image_ptr_ptr;
+
+	for (unsigned int y = 0; y < raw_image->height; y++)
+		free(raw_image->data[y]);
+
+	free(raw_image->data);
+	free(raw_image->palette);
+	free(raw_image);
+	*raw_image_ptr_ptr = NULL;
+}
+
+static void initialize_png(struct PNGImage *img, FILE *f)
+{
+	img->png = png_create_read_struct(PNG_LIBPNG_VER_STRING,
+					  NULL, NULL, NULL);
+	if (!img->png)
+		errx("Creating png structure failed");
+
+	img->info = png_create_info_struct(img->png);
+	if (!img->info)
+		errx("Creating png info structure failed");
+
+	if (setjmp(png_jmpbuf(img->png)))
+		exit(1);
+
+	png_init_io(img->png, f);
+
+	png_read_info(img->png, img->info);
+
+	img->width  = png_get_image_width(img->png, img->info);
+	img->height = png_get_image_height(img->png, img->info);
+	img->depth  = png_get_bit_depth(img->png, img->info);
+	img->type   = png_get_color_type(img->png, img->info);
+}
+
+static void read_png(struct PNGImage *img);
+static struct RawIndexedImage *create_raw_image(int width, int height,
+						int num_colors);
+static void set_raw_image_palette(struct RawIndexedImage *raw_image,
+				  png_color const *palette, int num_colors);
+
+static struct RawIndexedImage *indexed_png_to_raw(struct PNGImage *img)
+{
+	struct RawIndexedImage *raw_image;
+	png_color *palette;
+	int colors_in_PLTE;
+	int colors_in_new_palette;
+	png_byte *trans_alpha;
+	int num_trans;
+	png_color_16 *trans_color;
+	png_color *original_palette;
+	uint8_t *old_to_new_palette;
+	int i, x, y;
+
+	if (img->depth < 8)
+		png_set_packing(img->png);
+
+	png_get_PLTE(img->png, img->info, &palette, &colors_in_PLTE);
+
+	raw_image = create_raw_image(img->width, img->height, colors);
+
+	/*
+	 * Transparent palette entries are removed, and the palette is
+	 * collapsed. Transparent pixels are then replaced with palette index 0.
+	 * This way, an indexed PNG can contain transparent pixels in *addition*
+	 * to 4 normal colors.
+	 */
+	if (png_get_tRNS(img->png, img->info, &trans_alpha, &num_trans,
+			 &trans_color)) {
+		original_palette = palette;
+		palette = malloc(sizeof(*palette) * colors_in_PLTE);
+		if (!palette)
+			err("%s: Failed to allocate memory for palette",
+			    __func__);
+		colors_in_new_palette = 0;
+		old_to_new_palette = malloc(sizeof(*old_to_new_palette)
+					    * colors_in_PLTE);
+		if (!old_to_new_palette)
+			err("%s: Failed to allocate memory for new palette",
+			    __func__);
+
+		for (i = 0; i < num_trans; i++) {
+			if (trans_alpha[i] == 0) {
+				old_to_new_palette[i] = 0;
+			} else {
+				old_to_new_palette[i] = colors_in_new_palette;
+				palette[colors_in_new_palette++] =
+					original_palette[i];
+			}
+		}
+		for (i = num_trans; i < colors_in_PLTE; i++) {
+			old_to_new_palette[i] = colors_in_new_palette;
+			palette[colors_in_new_palette++] = original_palette[i];
+		}
+
+		if (colors_in_new_palette != colors_in_PLTE) {
+			palette = realloc(palette,
+					  sizeof(*palette) *
+					  colors_in_new_palette);
+			if (!palette)
+				err("%s: Failed to allocate memory for palette",
+				    __func__);
+		}
+
+		/*
+		 * Setting and validating palette before reading
+		 * allows us to error out *before* doing the data
+		 * transformation if the palette is too long.
+		 */
+		set_raw_image_palette(raw_image, palette,
+				      colors_in_new_palette);
+		read_png(img);
+
+		for (y = 0; y < img->height; y++) {
+			for (x = 0; x < img->width; x++) {
+				raw_image->data[y][x] =
+					old_to_new_palette[img->data[y][x]];
+			}
+		}
+
+		free(palette);
+		free(old_to_new_palette);
+	} else {
+		set_raw_image_palette(raw_image, palette, colors_in_PLTE);
+		read_png(img);
+
+		for (y = 0; y < img->height; y++) {
+			for (x = 0; x < img->width; x++)
+				raw_image->data[y][x] = img->data[y][x];
+		}
+	}
+
+	return raw_image;
+}
+
+static struct RawIndexedImage *grayscale_png_to_raw(struct PNGImage *img)
+{
+	if (img->depth < 8)
+		png_set_expand_gray_1_2_4_to_8(img->png);
+
+	png_set_gray_to_rgb(img->png);
+	return truecolor_png_to_raw(img);
+}
+
+static void rgba_png_palette(struct PNGImage *img,
+			     png_color **palette_ptr_ptr, int *num_colors);
+static struct RawIndexedImage
+	*processed_rgba_png_to_raw(const struct PNGImage *img,
+				   png_color const *palette,
+				   int colors_in_palette);
+
+static struct RawIndexedImage *truecolor_png_to_raw(struct PNGImage *img)
+{
+	struct RawIndexedImage *raw_image;
+	png_color *palette;
+	int colors_in_palette;
+
+	if (img->depth == 16) {
+#if PNG_LIBPNG_VER >= 10504
+		png_set_scale_16(img->png);
+#else
+		png_set_strip_16(img->png);
+#endif
+	}
+
+	if (!(img->type & PNG_COLOR_MASK_ALPHA)) {
+		if (png_get_valid(img->png, img->info, PNG_INFO_tRNS))
+			png_set_tRNS_to_alpha(img->png);
+		else
+			png_set_add_alpha(img->png, 0xFF, PNG_FILLER_AFTER);
+	}
+
+	read_png(img);
+
+	rgba_png_palette(img, &palette, &colors_in_palette);
+	raw_image = processed_rgba_png_to_raw(img, palette, colors_in_palette);
+
+	free(palette);
+
+	return raw_image;
+}
+
+static void rgba_PLTE_palette(struct PNGImage *img,
+			      png_color **palette_ptr_ptr, int *num_colors);
+static void rgba_build_palette(struct PNGImage *img,
+			       png_color **palette_ptr_ptr, int *num_colors);
+
+static void rgba_png_palette(struct PNGImage *img,
+			     png_color **palette_ptr_ptr, int *num_colors)
+{
+	if (png_get_valid(img->png, img->info, PNG_INFO_PLTE))
+		rgba_PLTE_palette(img, palette_ptr_ptr, num_colors);
+	else
+		rgba_build_palette(img, palette_ptr_ptr, num_colors);
+}
+
+static void rgba_PLTE_palette(struct PNGImage *img,
+			      png_color **palette_ptr_ptr, int *num_colors)
+{
+	png_get_PLTE(img->png, img->info, palette_ptr_ptr, num_colors);
+	/*
+	 * Lets us free the palette manually instead of leaving it to libpng,
+	 * which lets us handle a PLTE and a built palette the same way.
+	 */
+	png_data_freer(img->png, img->info,
+		       PNG_USER_WILL_FREE_DATA, PNG_FREE_PLTE);
+}
+
+static void update_built_palette(png_color *palette,
+				 png_color const *pixel_color, png_byte alpha,
+				 int *num_colors, bool *only_grayscale);
+static int fit_grayscale_palette(png_color *palette, int *num_colors);
+static void order_color_palette(png_color *palette, int num_colors);
+
+static void rgba_build_palette(struct PNGImage *img,
+			       png_color **palette_ptr_ptr, int *num_colors)
+{
+	png_color *palette;
+	int y, value_index;
+	png_color cur_pixel_color;
+	png_byte cur_alpha;
+	bool only_grayscale = true;
+
+	/*
+	 * By filling the palette up with black by default, if the image
+	 * doesn't have enough colors, the palette gets padded with black.
+	 */
+	*palette_ptr_ptr = calloc(colors, sizeof(**palette_ptr_ptr));
+	if (!*palette_ptr_ptr)
+		err("%s: Failed to allocate memory for palette", __func__);
+	palette = *palette_ptr_ptr;
+	*num_colors = 0;
+
+	for (y = 0; y < img->height; y++) {
+		value_index = 0;
+		while (value_index < img->width * 4) {
+			cur_pixel_color.red   = img->data[y][value_index++];
+			cur_pixel_color.green = img->data[y][value_index++];
+			cur_pixel_color.blue  = img->data[y][value_index++];
+			cur_alpha = img->data[y][value_index++];
+
+			update_built_palette(palette, &cur_pixel_color,
+					     cur_alpha,
+					     num_colors, &only_grayscale);
+		}
+	}
+
+	/* In order not to count 100% transparent images as grayscale. */
+	only_grayscale = *num_colors ? only_grayscale : false;
+
+	if (!only_grayscale || !fit_grayscale_palette(palette, num_colors))
+		order_color_palette(palette, *num_colors);
+}
+
+static void update_built_palette(png_color *palette,
+				 png_color const *pixel_color, png_byte alpha,
+				 int *num_colors, bool *only_grayscale)
+{
+	bool color_exists;
+	png_color cur_palette_color;
+	int i;
+
+	/*
+	 * Transparent pixels don't count toward the palette,
+	 * as they'll be replaced with color #0 later.
+	 */
+	if (alpha == 0)
+		return;
+
+	if (*only_grayscale && !(pixel_color->red == pixel_color->green &&
+				 pixel_color->red == pixel_color->blue)) {
+		*only_grayscale = false;
+	}
+
+	color_exists = false;
+	for (i = 0; i < *num_colors; i++) {
+		cur_palette_color = palette[i];
+		if (pixel_color->red   == cur_palette_color.red   &&
+		    pixel_color->green == cur_palette_color.green &&
+		    pixel_color->blue  == cur_palette_color.blue) {
+			color_exists = true;
+			break;
+		}
+	}
+	if (!color_exists) {
+		if (*num_colors == colors) {
+			errx("Too many colors in input PNG file to fit into a %d-bit palette (max %d).",
+			     depth, colors);
+		}
+		palette[*num_colors] = *pixel_color;
+		(*num_colors)++;
+	}
+}
+
+static int fit_grayscale_palette(png_color *palette, int *num_colors)
+{
+	int interval = 256 / colors;
+	png_color *fitted_palette = malloc(sizeof(*fitted_palette) * colors);
+	bool *set_indices = calloc(colors, sizeof(*set_indices));
+	int i, shade_index;
+
+	if (!fitted_palette)
+		err("%s: Failed to allocate memory for palette", __func__);
+	if (!set_indices)
+		err("%s: Failed to allocate memory for indices", __func__);
+
+	fitted_palette[0].red   = 0xFF;
+	fitted_palette[0].green = 0xFF;
+	fitted_palette[0].blue  = 0xFF;
+	fitted_palette[colors - 1].red   = 0;
+	fitted_palette[colors - 1].green = 0;
+	fitted_palette[colors - 1].blue  = 0;
+	if (colors == 4) {
+		fitted_palette[1].red   = 0xA9;
+		fitted_palette[1].green = 0xA9;
+		fitted_palette[1].blue  = 0xA9;
+		fitted_palette[2].red   = 0x55;
+		fitted_palette[2].green = 0x55;
+		fitted_palette[2].blue  = 0x55;
+	}
+
+	for (i = 0; i < *num_colors; i++) {
+		shade_index = colors - 1 - palette[i].red / interval;
+		if (set_indices[shade_index]) {
+			free(fitted_palette);
+			free(set_indices);
+			return false;
+		}
+		fitted_palette[shade_index] = palette[i];
+		set_indices[shade_index] = true;
+	}
+
+	for (i = 0; i < colors; i++)
+		palette[i] = fitted_palette[i];
+
+	*num_colors = colors;
+
+	free(fitted_palette);
+	free(set_indices);
+	return true;
+}
+
+/* A combined struct is needed to sort csolors in order of luminance. */
+struct ColorWithLuminance {
+	png_color color;
+	int luminance;
+};
+
+static int compare_luminance(void const *a, void const *b)
+{
+	const struct ColorWithLuminance *x, *y;
+
+	x = (const struct ColorWithLuminance *)a;
+	y = (const struct ColorWithLuminance *)b;
+
+	return y->luminance - x->luminance;
+}
+
+static void order_color_palette(png_color *palette, int num_colors)
+{
+	int i;
+	struct ColorWithLuminance *palette_with_luminance =
+		malloc(sizeof(*palette_with_luminance) * num_colors);
+
+	if (!palette_with_luminance)
+		err("%s: Failed to allocate memory for palette", __func__);
+
+	for (i = 0; i < num_colors; i++) {
+		/*
+		 * Normally this would be done with floats, but since it's only
+		 * used for comparison, we might as well use integer math.
+		 */
+		palette_with_luminance[i].color = palette[i];
+		palette_with_luminance[i].luminance = 2126 * palette[i].red   +
+						      7152 * palette[i].green +
+						       722 * palette[i].blue;
+	}
+	qsort(palette_with_luminance, num_colors,
+	      sizeof(*palette_with_luminance), compare_luminance);
+	for (i = 0; i < num_colors; i++)
+		palette[i] = palette_with_luminance[i].color;
+
+	free(palette_with_luminance);
+}
+
+static void put_raw_image_pixel(struct RawIndexedImage *raw_image,
+				const struct PNGImage *img,
+				int *value_index, int x, int y,
+				png_color const *palette,
+				int colors_in_palette);
+
+static struct RawIndexedImage
+	*processed_rgba_png_to_raw(const struct PNGImage *img,
+				   png_color const *palette,
+				   int colors_in_palette)
+{
+	struct RawIndexedImage *raw_image;
+	int x, y, value_index;
+
+	raw_image = create_raw_image(img->width, img->height, colors);
+
+	set_raw_image_palette(raw_image, palette, colors_in_palette);
+
+	for (y = 0; y < img->height; y++) {
+		x = raw_image->width - 1;
+		value_index = img->width * 4 - 1;
+
+		while (x >= 0) {
+			put_raw_image_pixel(raw_image, img,
+					    &value_index, x, y,
+					    palette, colors_in_palette);
+			x--;
+		}
+	}
+
+	return raw_image;
+}
+
+static uint8_t palette_index_of(png_color const *palette,
+				int num_colors, png_color const *color);
+
+static void put_raw_image_pixel(struct RawIndexedImage *raw_image,
+				const struct PNGImage *img,
+				int *value_index, int x, int y,
+				png_color const *palette,
+				int colors_in_palette)
+{
+	png_color pixel_color;
+	png_byte alpha;
+
+	alpha = img->data[y][*value_index];
+	if (alpha == 0) {
+		raw_image->data[y][x] = 0;
+		*value_index -= 4;
+	} else {
+		(*value_index)--;
+		pixel_color.blue  = img->data[y][(*value_index)--];
+		pixel_color.green = img->data[y][(*value_index)--];
+		pixel_color.red   = img->data[y][(*value_index)--];
+		raw_image->data[y][x] = palette_index_of(palette,
+							 colors_in_palette,
+							 &pixel_color);
+	}
+}
+
+static uint8_t palette_index_of(png_color const *palette,
+				int num_colors, png_color const *color)
+{
+	uint8_t i;
+
+	for (i = 0; i < num_colors; i++) {
+		if (palette[i].red   == color->red   &&
+		    palette[i].green == color->green &&
+		    palette[i].blue  == color->blue) {
+			return i;
+		}
+	}
+	errx("The input PNG file contains colors that don't appear in its embedded palette.");
+}
+
+static void read_png(struct PNGImage *img)
+{
+	int y;
+
+	png_read_update_info(img->png, img->info);
+
+	img->data = malloc(sizeof(*img->data) * img->height);
+	if (!img->data)
+		err("%s: Failed to allocate memory for image data",
+		    __func__);
+	for (y = 0; y < img->height; y++) {
+		img->data[y] = malloc(png_get_rowbytes(img->png, img->info));
+		if (!img->data[y])
+			err("%s: Failed to allocate memory for image data",
+			    __func__);
+	}
+
+	png_read_image(img->png, img->data);
+	png_read_end(img->png, img->info);
+}
+
+static struct RawIndexedImage *create_raw_image(int width, int height,
+						int num_colors)
+{
+	struct RawIndexedImage *raw_image;
+	int y;
+
+	raw_image = malloc(sizeof(*raw_image));
+	if (!raw_image)
+		err("%s: Failed to allocate memory for raw image",
+		    __func__);
+
+	raw_image->width = width;
+	raw_image->height = height;
+	raw_image->num_colors = num_colors;
+
+	raw_image->palette = malloc(sizeof(*raw_image->palette) * num_colors);
+	if (!raw_image->palette)
+		err("%s: Failed to allocate memory for raw image palette",
+		    __func__);
+
+	raw_image->data = malloc(sizeof(*raw_image->data) * height);
+	if (!raw_image->data)
+		err("%s: Failed to allocate memory for raw image data",
+		    __func__);
+	for (y = 0; y < height; y++) {
+		raw_image->data[y] = malloc(sizeof(*raw_image->data[y])
+					    * width);
+		if (!raw_image->data[y])
+			err("%s: Failed to allocate memory for raw image data",
+			    __func__);
+	}
+
+	return raw_image;
+}
+
+static void set_raw_image_palette(struct RawIndexedImage *raw_image,
+				  png_color const *palette, int num_colors)
+{
+	int i;
+
+	if (num_colors > raw_image->num_colors) {
+		errx("Too many colors in input PNG file's palette to fit into a %d-bit palette (%d in input palette, max %d).",
+		     raw_image->num_colors >> 1,
+		     num_colors, raw_image->num_colors);
+	}
+
+	for (i = 0; i < num_colors; i++) {
+		raw_image->palette[i].red   = palette[i].red;
+		raw_image->palette[i].green = palette[i].green;
+		raw_image->palette[i].blue  = palette[i].blue;
+	}
+	for (i = num_colors; i < raw_image->num_colors; i++) {
+		raw_image->palette[i].red   = 0;
+		raw_image->palette[i].green = 0;
+		raw_image->palette[i].blue  = 0;
+	}
+}
+
+static void get_text(const struct PNGImage *img,
+		     struct ImageOptions *png_options)
+{
+	png_text *text;
+	int i, numtxts, numremoved;
+
+	png_get_text(img->png, img->info, &text, &numtxts);
+	for (i = 0; i < numtxts; i++) {
+		if (strcmp(text[i].key, "h") == 0 && !*text[i].text) {
+			png_options->horizontal = true;
+			png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
+		} else if (strcmp(text[i].key, "x") == 0) {
+			png_options->trim = strtoul(text[i].text, NULL, 0);
+			png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
+		} else if (strcmp(text[i].key, "t") == 0) {
+			png_options->tilemapfile = text[i].text;
+			png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
+		} else if (strcmp(text[i].key, "T") == 0 && !*text[i].text) {
+			png_options->tilemapout = true;
+			png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
+		} else if (strcmp(text[i].key, "a") == 0) {
+			png_options->attrmapfile = text[i].text;
+			png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
+		} else if (strcmp(text[i].key, "A") == 0 && !*text[i].text) {
+			png_options->attrmapout = true;
+			png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
+		} else if (strcmp(text[i].key, "p") == 0) {
+			png_options->palfile = text[i].text;
+			png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
+		} else if (strcmp(text[i].key, "P") == 0 && !*text[i].text) {
+			png_options->palout = true;
+			png_free_data(img->png, img->info, PNG_FREE_TEXT, i);
+		}
+	}
+
+	/*
+	 * TODO: Remove this and simply change the warning function not to warn
+	 * instead.
+	 */
+	for (i = 0, numremoved = 0; i < numtxts; i++) {
+		if (text[i].key == NULL)
+			numremoved++;
+
+		text[i].key = text[i + numremoved].key;
+		text[i].text = text[i + numremoved].text;
+		text[i].compression = text[i + numremoved].compression;
+	}
+	png_set_text(img->png, img->info, text, numtxts - numremoved);
+}
+
+static void set_text(const struct PNGImage *img,
+		     const struct ImageOptions *png_options)
+{
+	png_text *text;
+	char buffer[3];
+
+	text = malloc(sizeof(*text));
+	if (!text)
+		err("%s: Failed to allocate memory for PNG text",
+		    __func__);
+
+	if (png_options->horizontal) {
+		text[0].key = "h";
+		text[0].text = "";
+		text[0].compression = PNG_TEXT_COMPRESSION_NONE;
+		png_set_text(img->png, img->info, text, 1);
+	}
+	if (png_options->trim) {
+		text[0].key = "x";
+		snprintf(buffer, 3, "%d", png_options->trim);
+		text[0].text = buffer;
+		text[0].compression = PNG_TEXT_COMPRESSION_NONE;
+		png_set_text(img->png, img->info, text, 1);
+	}
+	if (*png_options->tilemapfile) {
+		text[0].key = "t";
+		text[0].text = "";
+		text[0].compression = PNG_TEXT_COMPRESSION_NONE;
+		png_set_text(img->png, img->info, text, 1);
+	}
+	if (png_options->tilemapout) {
+		text[0].key = "T";
+		text[0].text = "";
+		text[0].compression = PNG_TEXT_COMPRESSION_NONE;
+		png_set_text(img->png, img->info, text, 1);
+	}
+	if (*png_options->attrmapfile) {
+		text[0].key = "a";
+		text[0].text = "";
+		text[0].compression = PNG_TEXT_COMPRESSION_NONE;
+		png_set_text(img->png, img->info, text, 1);
+	}
+	if (png_options->attrmapout) {
+		text[0].key = "A";
+		text[0].text = "";
+		text[0].compression = PNG_TEXT_COMPRESSION_NONE;
+		png_set_text(img->png, img->info, text, 1);
+	}
+	if (*png_options->palfile) {
+		text[0].key = "p";
+		text[0].text = "";
+		text[0].compression = PNG_TEXT_COMPRESSION_NONE;
+		png_set_text(img->png, img->info, text, 1);
+	}
+	if (png_options->palout) {
+		text[0].key = "P";
+		text[0].text = "";
+		text[0].compression = PNG_TEXT_COMPRESSION_NONE;
+		png_set_text(img->png, img->info, text, 1);
+	}
+
+	free(text);
+}
+
+static void free_png_data(const struct PNGImage *img)
+{
+	int y;
+
+	for (y = 0; y < img->height; y++)
+		free(img->data[y]);
+
+	free(img->data);
+}
--- /dev/null
+++ b/src/gfx/mkfile
@@ -1,0 +1,18 @@
+</$objtype/mkfile
+
+TARG=rgbfix
+BIN=$home/bin/$objtype
+
+# ThIs MaKeS It PoRtAbLe
+POSIX=-D PRIu32="%ud" -DPRId32="%d" -DPRIx32="%x" -DPRIX32="%X" -DPRIo32="%o" -DSTDOUT_FILENO=1 -DSTDIN_FILENO=0 -DPRIu8="%ud" -DPRIu16="%ud" -DPRId16="%d" -DPRIx16="%x" -DPRIX16="%X" -DMB_LEN_MAX=4 -DUINT32_C='(uint32_t)' -DSSIZE_MAX='0xFFFFFFFF'
+
+CFLAGS=-Fpw -I ../../include -I/sys/include/npe -D__plan9__ -D__${objtype}__ $POSIX
+
+OFILES=\
+	main.$O \
+	gb.$O \
+	makepng.$O \
+	getopt.$O \
+	version.$O \
+
+</sys/src/cmd/mkone
--- a/src/gfx/pal_packing.cpp
+++ /dev/null
@@ -1,512 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#include "gfx/pal_packing.hpp"
-
-#include <algorithm>
-#include <assert.h>
-#include <bitset>
-#include <cinttypes>
-#include <deque>
-#include <numeric>
-#include <optional>
-#include <queue>
-#include <tuple>
-#include <type_traits>
-#include <unordered_set>
-#include <vector>
-
-#include "defaultinitalloc.hpp"
-
-#include "gfx/main.hpp"
-#include "gfx/proto_palette.hpp"
-
-using std::swap;
-
-namespace packing {
-
-// The solvers here are picked from the paper at http://arxiv.org/abs/1605.00558:
-// "Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items"
-// Their formulation of the problem consists in packing "tiles" into "pages"; here is a
-// correspondence table for our application of it:
-// Paper | RGBGFX
-// ------+-------
-//  Tile | Proto-palette
-//  Page | Palette
-
-/*
- * A reference to a proto-palette, and attached attributes for sorting purposes
- */
-struct ProtoPalAttrs {
-	size_t const protoPalIndex;
-	/*
-	 * Pages from which we are banned (to prevent infinite loops)
-	 * This is dynamic because we wish not to hard-cap the amount of palettes
-	 */
-	std::vector<bool> bannedPages;
-
-	ProtoPalAttrs(size_t index) : protoPalIndex(index) {}
-	bool isBannedFrom(size_t index) const {
-		return index < bannedPages.size() && bannedPages[index];
-	}
-	void banFrom(size_t index) {
-		if (bannedPages.size() <= index) {
-			bannedPages.resize(index + 1);
-		}
-		bannedPages[index] = true;
-	}
-};
-
-/*
- * A collection of proto-palettes assigned to a palette
- * Does not contain the actual color indices because we need to be able to remove elements
- */
-class AssignedProtos {
-	// We leave room for emptied slots to avoid copying the structs around on removal
-	std::vector<std::optional<ProtoPalAttrs>> _assigned;
-	// For resolving proto-palette indices
-	std::vector<ProtoPalette> const *_protoPals;
-
-public:
-	template<typename... Ts>
-	AssignedProtos(std::vector<ProtoPalette> const &protoPals, Ts &&...elems)
-	    : _assigned{std::forward<Ts>(elems)...}, _protoPals{&protoPals} {}
-
-private:
-	template<typename Inner, template<typename> typename Constness>
-	class Iter {
-	public:
-		friend class AssignedProtos;
-		// For `iterator_traits`
-		using difference_type = typename std::iterator_traits<Inner>::difference_type;
-		using value_type = ProtoPalAttrs;
-		using pointer = Constness<value_type> *;
-		using reference = Constness<value_type> &;
-		using iterator_category = std::forward_iterator_tag;
-
-	private:
-		Constness<decltype(_assigned)> *_array = nullptr;
-		Inner _iter{};
-
-		Iter(decltype(_array) array, decltype(_iter) &&iter) : _array(array), _iter(iter) {}
-		Iter &skipEmpty() {
-			while (_iter != _array->end() && !_iter->has_value()) {
-				++_iter;
-			}
-			return *this;
-		}
-
-	public:
-		Iter() = default;
-
-		bool operator==(Iter const &other) const { return _iter == other._iter; }
-		bool operator!=(Iter const &other) const { return !(*this == other); }
-		Iter &operator++() {
-			++_iter;
-			skipEmpty();
-			return *this;
-		}
-		Iter operator++(int) {
-			Iter it = *this;
-			++(*this);
-			return it;
-		}
-		reference operator*() const {
-			assert((*_iter).has_value());
-			return **_iter;
-		}
-		pointer operator->() const {
-			return &(**this); // Invokes the operator above, not quite a no-op!
-		}
-
-		friend void swap(Iter &lhs, Iter &rhs) {
-			swap(lhs._array, rhs._array);
-			swap(lhs._iter, rhs._iter);
-		}
-	};
-public:
-	using iterator = Iter<decltype(_assigned)::iterator, std::remove_const_t>;
-	iterator begin() { return iterator{&_assigned, _assigned.begin()}.skipEmpty(); }
-	iterator end() { return iterator{&_assigned, _assigned.end()}; }
-	using const_iterator = Iter<decltype(_assigned)::const_iterator, std::add_const_t>;
-	const_iterator begin() const {
-		return const_iterator{&_assigned, _assigned.begin()}.skipEmpty();
-	}
-	const_iterator end() const { return const_iterator{&_assigned, _assigned.end()}; }
-
-	/*
-	 * Assigns a new ProtoPalAttrs in a free slot, assuming there is one
-	 * Args are passed to the `ProtoPalAttrs`'s constructor
-	 */
-	template<typename... Ts>
-	void assign(Ts &&...args) {
-		auto freeSlot = std::find_if_not(
-		    _assigned.begin(), _assigned.end(),
-		    [](std::optional<ProtoPalAttrs> const &slot) { return slot.has_value(); });
-
-		if (freeSlot == _assigned.end()) { // We are full, use a new slot
-			_assigned.emplace_back(std::forward<Ts>(args)...);
-		} else { // Reuse a free slot
-			freeSlot->emplace(std::forward<Ts>(args)...);
-		}
-	}
-	void remove(iterator const &iter) {
-		iter._iter->reset(); // This time, we want to access the `optional` itself
-	}
-	void clear() { _assigned.clear(); }
-
-	bool empty() const {
-		return std::find_if(
-		           _assigned.begin(), _assigned.end(),
-		           [](std::optional<ProtoPalAttrs> const &slot) { return slot.has_value(); })
-		       == _assigned.end();
-	}
-	size_t nbProtoPals() const { return std::distance(begin(), end()); }
-
-private:
-	template<typename Iter>
-	static void addUniqueColors(std::unordered_set<uint16_t> &colors, Iter iter, Iter const &end,
-	                            std::vector<ProtoPalette> const &protoPals) {
-		for (; iter != end; ++iter) {
-			ProtoPalette const &protoPal = protoPals[iter->protoPalIndex];
-			colors.insert(protoPal.begin(), protoPal.end());
-		}
-	}
-	// This function should stay private because it returns a reference to a unique object
-	std::unordered_set<uint16_t> &uniqueColors() const {
-		// We check for *distinct* colors by stuffing them into a `set`; this should be
-		// faster than "back-checking" on every element (O(n²))
-		//
-		// TODO: calc84maniac suggested another approach; try implementing it, see if it
-		// performs better:
-		// > So basically you make a priority queue that takes iterators into each of your sets
-		// > (paired with end iterators so you'll know where to stop), and the comparator tests the
-		// > values pointed to by each iterator
-		// > Then each iteration you pop from the queue,
-		// > optionally add one to your count, increment the iterator and push it back into the
-		// > queue if it didn't reach the end
-		// > And you do this until the priority queue is empty
-		static std::unordered_set<uint16_t> colors;
-
-		colors.clear();
-		addUniqueColors(colors, begin(), end(), *_protoPals);
-		return colors;
-	}
-public:
-	/*
-	 * Returns the number of distinct colors
-	 */
-	size_t volume() const { return uniqueColors().size(); }
-	bool canFit(ProtoPalette const &protoPal) const {
-		auto &colors = uniqueColors();
-		colors.insert(protoPal.begin(), protoPal.end());
-		return colors.size() <= options.maxOpaqueColors();
-	}
-
-	/*
-	 * Computes the "relative size" of a proto-palette on this palette
-	 */
-	double relSizeOf(ProtoPalette const &protoPal) const {
-		// NOTE: this function must not call `uniqueColors`, or one of its callers will break!
-		double relSize = 0.;
-		for (uint16_t color : protoPal) {
-			auto n = std::count_if(begin(), end(), [this, &color](ProtoPalAttrs const &attrs) {
-				ProtoPalette const &pal = (*_protoPals)[attrs.protoPalIndex];
-				return std::find(pal.begin(), pal.end(), color) != pal.end();
-			});
-			// NOTE: The paper and the associated code disagree on this: the code has
-			// this `1 +`, whereas the paper does not; its lack causes a division by 0
-			// if the symbol is not found anywhere, so I'm assuming the paper is wrong.
-			relSize += 1. / (1 + n);
-		}
-		return relSize;
-	}
-
-	/*
-	 * Computes the "relative size" of a set of proto-palettes on this palette
-	 */
-	template<typename Iter>
-	auto combinedVolume(Iter &&begin, Iter const &end,
-	                    std::vector<ProtoPalette> const &protoPals) const {
-		auto &colors = uniqueColors();
-		addUniqueColors(colors, std::forward<Iter>(begin), end, protoPals);
-		return colors.size();
-	}
-	/*
-	 * Computes the "relative size" of a set of colors on this palette
-	 */
-	template<typename Iter>
-	auto combinedVolume(Iter &&begin, Iter &&end) const {
-		auto &colors = uniqueColors();
-		colors.insert(std::forward<Iter>(begin), std::forward<Iter>(end));
-		return colors.size();
-	}
-};
-
-static void decant(std::vector<AssignedProtos> &assignments,
-                   std::vector<ProtoPalette> const &protoPalettes) {
-	// "Decanting" is the process of moving all *things* that can fit in a lower index there
-	auto decantOn = [&assignments](auto const &tryDecanting) {
-		// No need to attempt decanting on palette #0, as there are no palettes to decant to
-		for (size_t from = assignments.size(); --from;) {
-			// Scan all palettes before this one
-			for (size_t to = 0; to < from; ++to) {
-				tryDecanting(assignments[to], assignments[from]);
-			}
-
-			// If the proto-palette is now empty, remove it
-			// Doing this now reduces the number of iterations performed by later steps
-			// NB: order is intentionally preserved so as not to alter the "decantation"'s
-			// properties
-			// NB: this does mean that the first step might get empty palettes as its input!
-			// NB: this is safe to do because we go towards the beginning of the vector, thereby not
-			// invalidating our iteration (thus, iterators should not be used to drivethe outer
-			// loop)
-			if (assignments[from].empty()) {
-				assignments.erase(assignments.begin() + from);
-			}
-		}
-	};
-
-	options.verbosePrint(Options::VERB_DEBUG, "%zu palettes before decanting\n",
-	                     assignments.size());
-
-	// Decant on palettes
-	decantOn([&protoPalettes](AssignedProtos &to, AssignedProtos &from) {
-		// If the entire palettes can be merged, move all of `from`'s proto-palettes
-		if (to.combinedVolume(from.begin(), from.end(), protoPalettes)
-		    <= options.maxOpaqueColors()) {
-			for (ProtoPalAttrs &attrs : from) {
-				to.assign(attrs.protoPalIndex);
-			}
-			from.clear();
-		}
-	});
-	options.verbosePrint(Options::VERB_DEBUG, "%zu palettes after decanting on palettes\n",
-	                     assignments.size());
-
-	// Decant on "components" (= proto-pals sharing colors)
-	decantOn([&protoPalettes](AssignedProtos &to, AssignedProtos &from) {
-		// We need to iterate on all the "components", which are groups of proto-palettes sharing at
-		// least one color with another proto-palettes in the group.
-		// We do this by adding the first available proto-palette, and then looking for palettes
-		// with common colors. (As an optimization, we know we can skip palettes already scanned.)
-		std::vector<bool> processed(from.nbProtoPals(), false);
-		std::unordered_set<uint16_t> colors;
-		std::vector<size_t> members;
-		while (true) {
-			auto iter = std::find(processed.begin(), processed.end(), true);
-			if (iter == processed.end()) { // Processed everything!
-				break;
-			}
-			auto attrs = from.begin();
-			std::advance(attrs, (iter - processed.begin()));
-
-			// Build up the "component"...
-			colors.clear();
-			members.clear();
-			assert(members.empty()); // Compiler optimization hint
-			do {
-				ProtoPalette const &protoPal = protoPalettes[attrs->protoPalIndex];
-				// If this is the first proto-pal, or if at least one color matches, add it
-				if (members.empty()
-				    || std::find_first_of(colors.begin(), colors.end(), protoPal.begin(),
-				                          protoPal.end())
-				           != colors.end()) {
-					colors.insert(protoPal.begin(), protoPal.end());
-					members.push_back(iter - processed.begin());
-					*iter = true; // Mark that proto-pal as processed
-				}
-				++iter;
-				++attrs;
-			} while (iter != processed.end());
-
-			if (to.combinedVolume(colors.begin(), colors.end()) <= options.maxOpaqueColors()) {
-				// Iterate through the component's proto-palettes, and transfer them
-				auto member = from.begin();
-				size_t curIndex = 0;
-				for (size_t index : members) {
-					std::advance(member, index - curIndex);
-					curIndex = index;
-					to.assign(std::move(*member));
-					from.remove(member); // Removing does not shift elements, so it's cheap
-				}
-			}
-		}
-	});
-	options.verbosePrint(Options::VERB_DEBUG, "%zu palettes after decanting on \"components\"\n",
-	                     assignments.size());
-
-	// Decant on individual proto-palettes
-	decantOn([&protoPalettes](AssignedProtos &to, AssignedProtos &from) {
-		for (auto iter = from.begin(); iter != from.end(); ++iter) {
-			if (to.canFit(protoPalettes[iter->protoPalIndex])) {
-				to.assign(std::move(*iter));
-				from.remove(iter);
-			}
-		}
-	});
-	options.verbosePrint(Options::VERB_DEBUG, "%zu palettes after decanting on proto-palettes\n",
-	                     assignments.size());
-}
-
-std::tuple<DefaultInitVec<size_t>, size_t>
-    overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes) {
-	options.verbosePrint(Options::VERB_LOG_ACT,
-	                     "Paginating palettes using \"overload-and-remove\" strategy...\n");
-
-	// Sort the proto-palettes by size, which improves the packing algorithm's efficiency
-	DefaultInitVec<size_t> sortedProtoPalIDs(protoPalettes.size());
-	sortedProtoPalIDs.clear();
-	for (size_t i = 0; i < protoPalettes.size(); ++i) {
-		sortedProtoPalIDs.insert(
-		    std::lower_bound(sortedProtoPalIDs.begin(), sortedProtoPalIDs.end(), i), i);
-	}
-	// Begin with all proto-palettes queued up for insertion
-	std::queue<ProtoPalAttrs> queue(
-	    std::deque<ProtoPalAttrs>(sortedProtoPalIDs.begin(), sortedProtoPalIDs.end()));
-	// Begin with no pages
-	std::vector<AssignedProtos> assignments{};
-
-	for (; !queue.empty(); queue.pop()) {
-		ProtoPalAttrs const &attrs = queue.front(); // Valid until the `queue.pop()`
-		options.verbosePrint(Options::VERB_DEBUG, "Handling proto-pal %zu\n", attrs.protoPalIndex);
-
-		ProtoPalette const &protoPal = protoPalettes[attrs.protoPalIndex];
-		size_t bestPalIndex = assignments.size();
-		// We're looking for a palette where the proto-palette's relative size is less than
-		// its actual size; so only overwrite the "not found" index on meeting that criterion
-		double bestRelSize = protoPal.size();
-
-		for (size_t i = 0; i < assignments.size(); ++i) {
-			// Skip the page if this one is banned from it
-			if (attrs.isBannedFrom(i)) {
-				continue;
-			}
-
-			options.verbosePrint(Options::VERB_DEBUG, "%zu/%zu: Rel size: %f (size = %zu)\n", i + 1,
-			                     assignments.size(), assignments[i].relSizeOf(protoPal),
-			                     protoPal.size());
-			if (assignments[i].relSizeOf(protoPal) < bestRelSize) {
-				bestPalIndex = i;
-			}
-		}
-
-		if (bestPalIndex == assignments.size()) {
-			// Found nowhere to put it, create a new page containing just that one
-			assignments.emplace_back(protoPalettes, std::move(attrs));
-		} else {
-			auto &bestPal = assignments[bestPalIndex];
-			// Add the color to that palette
-			bestPal.assign(std::move(attrs));
-
-			// If this overloads the palette, get it back to normal (if possible)
-			while (bestPal.volume() > options.maxOpaqueColors()) {
-				options.verbosePrint(Options::VERB_DEBUG,
-				                     "Palette %zu is overloaded! (%zu > %" PRIu8 ")\n",
-				                     bestPalIndex, bestPal.volume(), options.maxOpaqueColors());
-
-				// Look for a proto-pal minimizing "efficiency" (size / rel_size)
-				auto efficiency = [&bestPal](ProtoPalette const &pal) {
-					return pal.size() / bestPal.relSizeOf(pal);
-				};
-				auto [minEfficiencyIter, maxEfficiencyIter] =
-				    std::minmax_element(bestPal.begin(), bestPal.end(),
-				                        [&efficiency, &protoPalettes](ProtoPalAttrs const &lhs,
-				                                                      ProtoPalAttrs const &rhs) {
-					                        return efficiency(protoPalettes[lhs.protoPalIndex])
-					                               < efficiency(protoPalettes[rhs.protoPalIndex]);
-				                        });
-
-				// All efficiencies are identical iff min equals max
-				// TODO: maybe not ideal to re-compute these two?
-				// TODO: yikes for float comparison! I *think* this threshold is OK?
-				if (efficiency(protoPalettes[maxEfficiencyIter->protoPalIndex])
-				        - efficiency(protoPalettes[minEfficiencyIter->protoPalIndex])
-				    < .001) {
-					break;
-				}
-
-				// Remove the proto-pal with minimal efficiency
-				queue.emplace(std::move(*minEfficiencyIter));
-				queue.back().banFrom(bestPalIndex); // Ban it from this palette
-				bestPal.remove(minEfficiencyIter);
-			}
-		}
-	}
-
-	// Deal with palettes still overloaded, by emptying them
-	for (AssignedProtos &pal : assignments) {
-		if (pal.volume() > options.maxOpaqueColors()) {
-			for (ProtoPalAttrs &attrs : pal) {
-				queue.emplace(std::move(attrs));
-			}
-			pal.clear();
-		}
-	}
-	// Place back any proto-palettes now in the queue via first-fit
-	while (!queue.empty()) {
-		ProtoPalAttrs const &attrs = queue.front();
-		ProtoPalette const &protoPal = protoPalettes[attrs.protoPalIndex];
-		auto iter =
-		    std::find_if(assignments.begin(), assignments.end(),
-		                 [&protoPal](AssignedProtos const &pal) { return pal.canFit(protoPal); });
-		if (iter == assignments.end()) { // No such page, create a new one
-			options.verbosePrint(Options::VERB_DEBUG,
-			                     "Adding new palette (%zu) for overflowing proto-pal %zu\n",
-			                     assignments.size(), attrs.protoPalIndex);
-			assignments.emplace_back(protoPalettes, std::move(attrs));
-		} else {
-			options.verbosePrint(Options::VERB_DEBUG,
-			                     "Assigning overflowing proto-pal %zu to palette %zu\n",
-			                     attrs.protoPalIndex, iter - assignments.begin());
-			iter->assign(std::move(attrs));
-		}
-		queue.pop();
-	}
-
-	if (options.verbosity >= Options::VERB_INTERM) {
-		for (auto &&assignment : assignments) {
-			fprintf(stderr, "{ ");
-			for (auto &&attrs : assignment) {
-				fprintf(stderr, "[%zu] ", attrs.protoPalIndex);
-				for (auto &&colorIndex : protoPalettes[attrs.protoPalIndex]) {
-					fprintf(stderr, "%04" PRIx16 ", ", colorIndex);
-				}
-			}
-			fprintf(stderr, "} (volume = %zu)\n", assignment.volume());
-		}
-	}
-
-	// "Decant" the result
-	decant(assignments, protoPalettes);
-	// Note that the result does not contain any empty palettes
-
-	if (options.verbosity >= Options::VERB_INTERM) {
-		for (auto &&assignment : assignments) {
-			fprintf(stderr, "{ ");
-			for (auto &&attrs : assignment) {
-				fprintf(stderr, "[%zu] ", attrs.protoPalIndex);
-				for (auto &&colorIndex : protoPalettes[attrs.protoPalIndex]) {
-					fprintf(stderr, "%04" PRIx16 ", ", colorIndex);
-				}
-			}
-			fprintf(stderr, "} (volume = %zu)\n", assignment.volume());
-		}
-	}
-
-	DefaultInitVec<size_t> mappings(protoPalettes.size());
-	for (size_t i = 0; i < assignments.size(); ++i) {
-		for (ProtoPalAttrs const &attrs : assignments[i]) {
-			mappings[attrs.protoPalIndex] = i;
-		}
-	}
-	return {mappings, assignments.size()};
-}
-
-} // namespace packing
--- a/src/gfx/pal_sorting.cpp
+++ /dev/null
@@ -1,87 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#include "gfx/pal_sorting.hpp"
-
-#include <algorithm>
-#include <png.h>
-#include <vector>
-
-#include "helpers.h"
-
-#include "gfx/main.hpp"
-#include "gfx/process.hpp"
-
-namespace sorting {
-
-void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
-             png_byte *palAlpha) {
-	options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palettes using embedded palette...\n");
-
-	auto pngToRgb = [&palRGB, &palAlpha](int index) {
-		auto const &c = palRGB[index];
-		return Rgba(c.red, c.green, c.blue, palAlpha ? palAlpha[index] : 0xFF);
-	};
-
-	for (Palette &pal : palettes) {
-		std::sort(pal.begin(), pal.end(), [&](uint16_t lhs, uint16_t rhs) {
-			// Iterate through the PNG's palette, looking for either of the two
-			for (int i = 0; i < palSize; ++i) {
-				uint16_t color = pngToRgb(i).cgbColor();
-				if (color == Rgba::transparent) {
-					continue;
-				}
-				// Return whether lhs < rhs
-				if (color == rhs) {
-					return false;
-				}
-				if (color == lhs) {
-					return true;
-				}
-			}
-			unreachable_(); // This should not be possible
-		});
-	}
-}
-
-void grayscale(std::vector<Palette> &palettes,
-               std::array<std::optional<Rgba>, 0x8001> const &colors) {
-	options.verbosePrint(Options::VERB_LOG_ACT, "Sorting grayscale-only palette...\n");
-
-	// This method is only applicable if there are at most as many colors as colors per palette, so
-	// we should only have a single palette.
-	assert(palettes.size() == 1);
-
-	Palette &palette = palettes[0];
-	std::fill(palette.colors.begin(), palette.colors.end(), Rgba::transparent);
-	for (auto const &slot : colors) {
-		if (!slot.has_value() || slot->isTransparent()) {
-			continue;
-		}
-		palette[slot->grayIndex()] = slot->cgbColor();
-	}
-}
-
-static unsigned int legacyLuminance(uint16_t color) {
-	uint8_t red = color & 0b11111;
-	uint8_t green = color >> 5 & 0b11111;
-	uint8_t blue = color >> 10;
-	return 2126 * red + 7152 * green + 722 * blue;
-}
-
-void rgb(std::vector<Palette> &palettes) {
-	options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palettes by \"\"\"luminance\"\"\"...\n");
-
-	for (Palette &pal : palettes) {
-		std::sort(pal.begin(), pal.end(), [](uint16_t lhs, uint16_t rhs) {
-			return legacyLuminance(lhs) > legacyLuminance(rhs);
-		});
-	}
-}
-
-} // namespace sorting
--- a/src/gfx/pal_spec.cpp
+++ /dev/null
@@ -1,604 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#include "gfx/pal_spec.hpp"
-
-#include <algorithm>
-#include <cassert>
-#include <cinttypes>
-#include <climits>
-#include <cstdint>
-#include <cstdio>
-#include <cstring>
-#include <fstream>
-#include <limits>
-#include <optional>
-#include <ostream>
-#include <streambuf>
-#include <string>
-#include <string_view>
-#include <tuple>
-#include <type_traits>
-#include <unordered_map>
-
-#include "platform.h"
-
-#include "gfx/main.hpp"
-
-using namespace std::string_view_literals;
-
-constexpr uint8_t nibble(char c) {
-	if (c >= 'a') {
-		assert(c <= 'f');
-		return c - 'a' + 10;
-	} else if (c >= 'A') {
-		assert(c <= 'F');
-		return c - 'A' + 10;
-	} else {
-		assert(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, typename Str::size_type &pos) {
-	pos = std::min(str.find_first_not_of(" \t"sv, pos), str.length());
-}
-
-void parseInlinePalSpec(char const * const rawArg) {
-	// List of #rrggbb/#rgb colors, comma-separated, palettes are separated by colons
-
-	std::string_view arg(rawArg);
-	using size_type = decltype(arg)::size_type;
-
-	auto parseError = [&rawArg, &arg](size_type ofs, size_type len, char const *fmt,
-	                                  auto &&...args) {
-		(void)arg; // With NDEBUG, `arg` is otherwise not used
-		assert(ofs <= arg.length());
-		assert(len <= arg.length());
-
-		error(fmt, args...);
-		fprintf(stderr,
-		        "In inline palette spec: %s\n"
-		        "                        ",
-		        rawArg);
-		for (auto i = ofs; i; --i) {
-			putc(' ', stderr);
-		}
-		for (auto i = len; i; --i) {
-			putc('^', stderr);
-		}
-		putc('\n', stderr);
-	};
-
-	options.palSpec.clear();
-	options.palSpec.emplace_back(); // Value-initialized, not default-init'd, so we get zeros
-
-	size_type n = 0; // Index into the argument
-	// TODO: store max `nbColors` ever reached, and compare against palette size later
-	size_t nbColors = 0; // Number of colors in the current palette
-	for (;;) {
-		++n; // Ignore the '#' (checked either by caller or previous loop iteration)
-
-		Rgba &color = options.palSpec.back()[nbColors];
-		auto pos = std::min(arg.find_first_not_of("0123456789ABCDEFabcdef"sv, n), arg.length());
-		switch (pos - n) {
-		case 3:
-			color = Rgba(singleToHex(arg[n + 0]), singleToHex(arg[n + 1]), singleToHex(arg[n + 2]),
-			             0xFF);
-			break;
-		case 6:
-			color = Rgba(toHex(arg[n + 0], arg[n + 1]), toHex(arg[n + 2], arg[n + 3]),
-			             toHex(arg[n + 4], arg[n + 5]), 0xFF);
-			break;
-		case 0:
-			parseError(n - 1, 1, "Missing color after '#'");
-			return;
-		default:
-			parseError(n, pos - n, "Unknown color specification");
-			return;
-		}
-		n = pos;
-
-		// Skip whitespace, if any
-		skipWhitespace(arg, n);
-
-		// Skip comma/semicolon, or end
-		if (n == arg.length()) {
-			break;
-		}
-		switch (arg[n]) {
-		case ',':
-			++n; // Skip it
-
-			++nbColors;
-
-			// A trailing comma may be followed by a semicolon
-			skipWhitespace(arg, n);
-			if (n == arg.length()) {
-				break;
-			} else if (arg[n] != ';' && arg[n] != ':') {
-				if (nbColors == 4) {
-					parseError(n, 1, "Each palette can only contain up to 4 colors");
-					return;
-				}
-				break;
-			}
-			[[fallthrough]];
-
-		case ':':
-		case ';':
-			++n;
-			skipWhitespace(arg, n);
-
-			nbColors = 0; // Start a new palette
-			// Avoid creating a spurious empty palette
-			if (n != arg.length()) {
-				options.palSpec.emplace_back();
-			}
-			break;
-
-		default:
-			parseError(n, 1, "Unexpected character, expected ',', ';', or end of argument");
-			return;
-		}
-
-		// Check again to allow trailing a comma/semicolon
-		if (n == arg.length()) {
-			break;
-		}
-		if (arg[n] != '#') {
-			parseError(n, 1, "Unexpected character, expected '#'");
-			return;
-		}
-	}
-}
-
-/*
- * Tries to read some magic bytes from the provided `file`.
- * Returns whether the magic was correctly read.
- */
-template<size_t n>
-static bool readMagic(std::filebuf &file, char const *magic) {
-	assert(strlen(magic) == n);
-
-	char magicBuf[n];
-	return file.sgetn(magicBuf, n) == n && memcmp(magicBuf, magic, n);
-}
-
-// Like `readMagic`, but automatically determines the size from the string literal's length.
-// Don't worry if you make a mistake, an `assert`'s got your back!
-#define READ_MAGIC(file, magic) \
-	readMagic<sizeof(magic) - 1>(file, magic) // Don't count the terminator
-
-template<typename T, typename U>
-static T readBE(U const *bytes) {
-	T val = 0;
-	for (size_t i = 0; i < sizeof(val); ++i) {
-		val = val << 8 | static_cast<uint8_t>(bytes[i]);
-	}
-	return val;
-}
-
-template<typename T, typename U>
-static T readLE(U const *bytes) {
-	T val = 0;
-	for (size_t i = 0; i < sizeof(val); ++i) {
-		val |= static_cast<uint8_t>(bytes[i]) << (i * 8);
-	}
-	return val;
-}
-
-/*
- * **Appends** the first line read from `file` to the end of the provided `buffer`.
- */
-static void readLine(std::filebuf &file, std::string &buffer) {
-	// TODO: maybe this can be optimized to bulk reads?
-	for (;;) {
-		auto c = file.sbumpc();
-		if (c == std::filebuf::traits_type::eof()) {
-			return;
-		}
-		if (c == '\n') {
-			// Discard a trailing CRLF
-			if (!buffer.empty() && buffer.back() == '\r') {
-				buffer.pop_back();
-			}
-			return;
-		}
-
-		buffer.push_back(c);
-	}
-}
-
-// FIXME: Normally we'd use `std::from_chars`, but that's not available with GCC 7
-/*
- * Parses the initial part of a string_view, advancing the "read index" as it does
- */
-template<typename U> // Should be uint*_t
-static std::optional<U> parseDec(std::string const &str, std::string::size_type &n) {
-	std::string::size_type start = n;
-
-	uintmax_t value = 0; // Use a larger type to handle overflow more easily
-	for (auto end = std::min(str.length(), str.find_first_not_of("0123456789"sv, n)); n < end;
-	     ++n) {
-		value = std::min(value * 10 + (str[n] - '0'), (uintmax_t)std::numeric_limits<U>::max);
-	}
-
-	return n > start ? std::optional<U>{value} : std::nullopt;
-}
-
-static std::optional<Rgba> parseColor(std::string const &str, std::string::size_type &n,
-                                      uint16_t i) {
-	std::optional<uint8_t> r = parseDec<uint8_t>(str, n);
-	if (!r) {
-		error("Failed to parse color #%" PRIu16 " (\"%s\"): invalid red component", i + 1,
-		      str.c_str());
-		return std::nullopt;
-	}
-	skipWhitespace(str, n);
-	if (n == str.length()) {
-		error("Failed to parse color #%" PRIu16 " (\"%s\"): missing green component", i + 1,
-		      str.c_str());
-		return std::nullopt;
-	}
-	std::optional<uint8_t> g = parseDec<uint8_t>(str, n);
-	if (!g) {
-		error("Failed to parse color #%" PRIu16 " (\"%s\"): invalid green component", i + 1,
-		      str.c_str());
-		return std::nullopt;
-	}
-	skipWhitespace(str, n);
-	if (n == str.length()) {
-		error("Failed to parse color #%" PRIu16 " (\"%s\"): missing blue component", i + 1,
-		      str.c_str());
-		return std::nullopt;
-	}
-	std::optional<uint8_t> b = parseDec<uint8_t>(str, n);
-	if (!b) {
-		error("Failed to parse color #%" PRIu16 " (\"%s\"): invalid blue component", i + 1,
-		      str.c_str());
-		return std::nullopt;
-	}
-
-	return std::optional<Rgba>{Rgba(*r, *g, *b, 0xFF)};
-}
-
-static void parsePSPFile(std::filebuf &file) {
-	// https://www.selapa.net/swatches/colors/fileformats.php#psp_pal
-
-	std::string line;
-	readLine(file, line);
-	if (line != "JASC-PAL") {
-		error("Palette file does not appear to be a PSP palette file");
-		return;
-	}
-
-	line.clear();
-	readLine(file, line);
-	if (line != "0100") {
-		error("Unsupported PSP palette file version \"%s\"", line.c_str());
-		return;
-	}
-
-	line.clear();
-	readLine(file, line);
-	std::string::size_type n = 0;
-	std::optional<uint16_t> nbColors = parseDec<uint16_t>(line, n);
-	if (!nbColors || n != line.length()) {
-		error("Invalid \"number of colors\" line in PSP file (%s)", line.c_str());
-		return;
-	}
-
-	if (*nbColors > options.nbColorsPerPal * options.nbPalettes) {
-		warning("PSP file contains %" PRIu16 " colors, but there can only be %" PRIu16
-		        "; ignoring extra",
-		        *nbColors, options.nbColorsPerPal * options.nbPalettes);
-		nbColors = options.nbColorsPerPal * options.nbPalettes;
-	}
-
-	options.palSpec.clear();
-
-	for (uint16_t i = 0; i < *nbColors; ++i) {
-		line.clear();
-		readLine(file, line);
-
-		n = 0;
-		std::optional<Rgba> color = parseColor(line, n, i + 1);
-		if (!color) {
-			return;
-		}
-		if (n != line.length()) {
-			error("Failed to parse color #%" PRIu16
-			      " (\"%s\"): trailing characters after blue component",
-			      i + 1, line.c_str());
-			return;
-		}
-
-		if (i % options.nbColorsPerPal == 0) {
-			options.palSpec.emplace_back();
-		}
-		options.palSpec.back()[i % options.nbColorsPerPal] = *color;
-	}
-}
-
-static void parseGPLFile(std::filebuf &file) {
-	// https://gitlab.gnome.org/GNOME/gimp/-/blob/gimp-2-10/app/core/gimppalette-load.c#L39
-
-	std::string line;
-	readLine(file, line);
-	// FIXME: C++20 will allow `!line.starts_with` instead of `line.rfind` with 0
-	if (line.rfind("GIMP Palette", 0)) {
-		error("Palette file does not appear to be a GPL palette file");
-		return;
-	}
-
-	uint16_t nbColors = 0;
-	uint16_t maxNbColors = options.nbColorsPerPal * options.nbPalettes;
-
-	for (;;) {
-		line.clear();
-		readLine(file, line);
-		if (!line.length()) {
-			break;
-		}
-
-		// FIXME: C++20 will allow `line.starts_with` instead of `!line.rfind` with 0
-		if (!line.rfind("#", 0) || !line.rfind("Name:", 0) || !line.rfind("Column:", 0)) {
-			continue;
-		}
-
-		std::string::size_type n = 0;
-		std::optional<Rgba> color = parseColor(line, n, nbColors + 1);
-		if (!color) {
-			return;
-		}
-
-		++nbColors;
-		if (nbColors < maxNbColors) {
-			if (nbColors % options.nbColorsPerPal == 1) {
-				options.palSpec.emplace_back();
-			}
-			options.palSpec.back()[nbColors % options.nbColorsPerPal] = *color;
-		}
-	}
-
-	if (nbColors > maxNbColors) {
-		warning("GPL file contains %" PRIu16 " colors, but there can only be %" PRIu16
-		        "; ignoring extra",
-		        nbColors, maxNbColors);
-	}
-}
-
-static void parseHEXFile(std::filebuf &file) {
-	// https://lospec.com/palette-list/tag/gbc
-
-	uint16_t nbColors = 0;
-	uint16_t maxNbColors = options.nbColorsPerPal * options.nbPalettes;
-
-	for (;;) {
-		std::string line;
-		readLine(file, line);
-		if (!line.length()) {
-			break;
-		}
-
-		if (line.length() != 6
-		    || line.find_first_not_of("0123456789ABCDEFabcdef"sv) != std::string::npos) {
-			error("Failed to parse color #%" PRIu16 " (\"%s\"): invalid \"rrggbb\" line",
-			      nbColors + 1, line.c_str());
-			return;
-		}
-
-		Rgba color =
-		    Rgba(toHex(line[0], line[1]), toHex(line[2], line[3]), toHex(line[4], line[5]), 0xFF);
-
-		++nbColors;
-		if (nbColors < maxNbColors) {
-			if (nbColors % options.nbColorsPerPal == 1) {
-				options.palSpec.emplace_back();
-			}
-			options.palSpec.back()[nbColors % options.nbColorsPerPal] = color;
-		}
-	}
-
-	if (nbColors > maxNbColors) {
-		warning("HEX file contains %" PRIu16 " colors, but there can only be %" PRIu16
-		        "; ignoring extra",
-		        nbColors, maxNbColors);
-	}
-}
-
-static void parseACTFile(std::filebuf &file) {
-	// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626
-
-	std::array<char, 772> buf;
-	auto len = file.sgetn(buf.data(), buf.size());
-
-	uint16_t nbColors = 256;
-	if (len == 772) {
-		nbColors = readBE<uint16_t>(&buf[768]);
-		// TODO: apparently there is a "transparent color index"? What?
-		if (nbColors > 256 || nbColors == 0) {
-			error("Invalid number of colors in ACT file (%" PRIu16 ")", nbColors);
-			return;
-		}
-	} else if (len != 768) {
-		error("Invalid file size for ACT file (expected 768 or 772 bytes, got %zu", len);
-		return;
-	}
-
-	if (nbColors > options.nbColorsPerPal * options.nbPalettes) {
-		warning("ACT file contains %" PRIu16 " colors, but there can only be %" PRIu16
-		        "; ignoring extra",
-		        nbColors, options.nbColorsPerPal * options.nbPalettes);
-		nbColors = options.nbColorsPerPal * options.nbPalettes;
-	}
-
-	options.palSpec.clear();
-	options.palSpec.emplace_back();
-
-	char const *ptr = buf.data();
-	size_t colorIdx = 0;
-	for (uint16_t i = 0; i < nbColors; ++i) {
-		Rgba &color = options.palSpec.back()[colorIdx];
-		color = Rgba(ptr[0], ptr[1], ptr[2], 0xFF);
-
-		ptr += 3;
-		++colorIdx;
-		if (colorIdx == options.nbColorsPerPal) {
-			options.palSpec.emplace_back();
-			colorIdx = 0;
-		}
-	}
-
-	// Remove the spurious empty palette if there is one
-	if (colorIdx == 0) {
-		options.palSpec.pop_back();
-	}
-}
-
-static void parseACOFile(std::filebuf &file) {
-	// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819
-	// http://www.nomodes.com/aco.html
-
-	char buf[10];
-
-	if (file.sgetn(buf, 2) != 2) {
-		error("Couldn't read ACO file version");
-		return;
-	}
-	if (readBE<uint16_t>(buf) != 1) {
-		error("Palette file does not appear to be an ACO file");
-		return;
-	}
-
-	if (file.sgetn(buf, 2) != 2) {
-		error("Couldn't read number of colors in palette file");
-		return;
-	}
-	uint16_t nbColors = readBE<uint16_t>(buf);
-
-	if (nbColors > options.nbColorsPerPal * options.nbPalettes) {
-		warning("ACO file contains %" PRIu16 " colors, but there can only be %" PRIu16
-		        "; ignoring extra",
-		        nbColors, options.nbColorsPerPal * options.nbPalettes);
-		nbColors = options.nbColorsPerPal * options.nbPalettes;
-	}
-
-	options.palSpec.clear();
-
-	for (uint16_t i = 0; i < nbColors; ++i) {
-		if (file.sgetn(buf, 10) != 10) {
-			error("Failed to read color #%" PRIu16 " from palette file", i + 1);
-			return;
-		}
-
-		if (i % options.nbColorsPerPal == 0) {
-			options.palSpec.emplace_back();
-		}
-
-		Rgba &color = options.palSpec.back()[i % options.nbColorsPerPal];
-		uint16_t colorType = readBE<uint16_t>(buf);
-		switch (colorType) {
-		case 0: // RGB
-			color = Rgba(buf[0], buf[2], buf[4], 0xFF);
-			break;
-		case 1: // HSB
-			error("Unsupported color type (HSB) for ACO file");
-			return;
-		case 2: // CMYK
-			error("Unsupported color type (CMYK) for ACO file");
-			return;
-		case 7: // Lab
-			error("Unsupported color type (lab) for ACO file");
-			return;
-		case 8: // Grayscale
-			error("Unsupported color type (grayscale) for ACO file");
-			return;
-		default:
-			error("Unknown color type (%" PRIu16 ") for ACO file", colorType);
-			return;
-		}
-	}
-
-	// TODO: maybe scan the v2 data instead (if present)
-	// `codecvt` can be used to convert from UTF-16 to UTF-8
-}
-
-static void parseGBCFile(std::filebuf &file) {
-	// This only needs to be able to read back files generated by `rgbgfx -p`
-	options.palSpec.clear();
-
-	for (;;) {
-		char buf[2 * 4];
-		auto len = file.sgetn(buf, sizeof(buf));
-		if (len == 0) {
-			break;
-		} else if (len != sizeof(buf)) {
-			error("GBC palette dump contains %zu 8-byte palette%s, plus %zu byte%s",
-			      options.palSpec.size(), options.palSpec.size() == 1 ? "" : "s", len,
-			      len == 1 ? "" : "s");
-			break;
-		}
-
-		options.palSpec.push_back({Rgba::fromCGBColor(readLE<uint16_t>(&buf[0])),
-		                           Rgba::fromCGBColor(readLE<uint16_t>(&buf[2])),
-		                           Rgba::fromCGBColor(readLE<uint16_t>(&buf[4])),
-		                           Rgba::fromCGBColor(readLE<uint16_t>(&buf[6]))});
-	}
-}
-
-void parseExternalPalSpec(char const *arg) {
-	// `fmt:path`, parse the file according to the given format
-
-	// Split both parts, error out if malformed
-	char const *ptr = strchr(arg, ':');
-	if (ptr == nullptr) {
-		error("External palette spec must have format `fmt:path` (missing colon)");
-		return;
-	}
-	char const *path = ptr + 1;
-
-	static std::array parsers{
-	    std::tuple{"PSP", &parsePSPFile, std::ios::in    },
-	    std::tuple{"GPL", &parseGPLFile, std::ios::in    },
-	    std::tuple{"HEX", &parseHEXFile, std::ios::in    },
-	    std::tuple{"ACT", &parseACTFile, std::ios::binary},
-	    std::tuple{"ACO", &parseACOFile, std::ios::binary},
-	    std::tuple{"GBC", &parseGBCFile, std::ios::binary},
-	};
-
-	auto iter = std::find_if(parsers.begin(), parsers.end(),
-	                         [&arg, &ptr](decltype(parsers)::value_type const &parser) {
-		                         return strncasecmp(arg, std::get<0>(parser), ptr - arg) == 0;
-	                         });
-	if (iter == parsers.end()) {
-		error("Unknown external palette format \"%.*s\"",
-		      static_cast<int>(std::min(ptr - arg, static_cast<decltype(ptr - arg)>(INT_MAX))),
-		      arg);
-		return;
-	}
-
-	std::filebuf file;
-	// Some parsers read the file in text mode, others in binary mode
-	if (!file.open(path, std::ios::in | std::get<2>(*iter))) {
-		error("Failed to open palette file \"%s\"", path);
-		return;
-	}
-
-	std::get<1> (*iter)(file);
-}
--- a/src/gfx/process.cpp
+++ /dev/null
@@ -1,1142 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#include "gfx/process.hpp"
-
-#include <algorithm>
-#include <assert.h>
-#include <cinttypes>
-#include <climits>
-#include <cstdio>
-#include <errno.h>
-#include <fstream>
-#include <memory>
-#include <optional>
-#include <png.h>
-#include <setjmp.h>
-#include <stdint.h>
-#include <string.h>
-#include <tuple>
-#include <unordered_set>
-#include <utility>
-#include <vector>
-
-#include "defaultinitalloc.hpp"
-#include "file.hpp"
-#include "helpers.h"
-#include "itertools.hpp"
-
-#include "gfx/main.hpp"
-#include "gfx/pal_packing.hpp"
-#include "gfx/pal_sorting.hpp"
-#include "gfx/proto_palette.hpp"
-
-class ImagePalette {
-	// Use as many slots as there are CGB colors (plus transparency)
-	std::array<std::optional<Rgba>, 0x8001> _colors;
-
-public:
-	ImagePalette() = default;
-
-	/*
-	 * Registers a color in the palette.
-	 * If the newly inserted color "conflicts" with another one (different color, but same CGB
-	 * color), then the other color is returned. Otherwise, `nullptr` is returned.
-	 */
-	[[nodiscard]] Rgba const *registerColor(Rgba const &rgba) {
-		decltype(_colors)::value_type &slot = _colors[rgba.cgbColor()];
-
-		if (rgba.cgbColor() == Rgba::transparent) {
-			options.hasTransparentPixels = true;
-		}
-
-		if (!slot.has_value()) {
-			slot.emplace(rgba);
-		} else if (*slot != rgba) {
-			assert(slot->cgbColor() != UINT16_MAX);
-			return &*slot;
-		}
-		return nullptr;
-	}
-
-	size_t size() const {
-		return std::count_if(_colors.begin(), _colors.end(),
-		                     [](decltype(_colors)::value_type const &slot) {
-			                     return slot.has_value() && !slot->isTransparent();
-		                     });
-	}
-	decltype(_colors) const &raw() const { return _colors; }
-
-	auto begin() const { return _colors.begin(); }
-	auto end() const { return _colors.end(); }
-};
-
-class Png {
-	std::string const &path;
-	File file{};
-	png_structp png = nullptr;
-	png_infop info = nullptr;
-
-	// These are cached for speed
-	uint32_t width, height;
-	DefaultInitVec<Rgba> pixels;
-	ImagePalette colors;
-	int colorType;
-	int nbColors;
-	png_colorp embeddedPal = nullptr;
-	png_bytep transparencyPal = nullptr;
-
-	[[noreturn]] static void handleError(png_structp png, char const *msg) {
-		Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
-
-		fatal("Error reading input image (\"%s\"): %s", self->file.c_str(self->path), msg);
-	}
-
-	static void handleWarning(png_structp png, char const *msg) {
-		Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
-
-		warning("In input image (\"%s\"): %s", self->file.c_str(self->path), msg);
-	}
-
-	static void readData(png_structp png, png_bytep data, size_t length) {
-		Png *self = reinterpret_cast<Png *>(png_get_io_ptr(png));
-		std::streamsize expectedLen = length;
-		std::streamsize nbBytesRead =
-		    self->file->sgetn(reinterpret_cast<char *>(data), expectedLen);
-
-		if (nbBytesRead != expectedLen) {
-			fatal("Error reading input image (\"%s\"): file too short (expected at least %zd more "
-			      "bytes after reading %lld)",
-			      self->file.c_str(self->path), length - nbBytesRead,
-			      self->file->pubseekoff(0, std::ios_base::cur));
-		}
-	}
-
-public:
-	ImagePalette const &getColors() const { return colors; }
-
-	int getColorType() const { return colorType; }
-
-	std::tuple<int, png_const_colorp, png_bytep> getEmbeddedPal() const {
-		return {nbColors, embeddedPal, transparencyPal};
-	}
-
-	uint32_t getWidth() const { return width; }
-
-	uint32_t getHeight() const { return height; }
-
-	Rgba &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; }
-
-	Rgba const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; }
-
-	bool isSuitableForGrayscale() const {
-		// Check that all of the grays don't fall into the same "bin"
-		if (colors.size() > options.maxOpaqueColors()) { // Apply the Pigeonhole Principle
-			options.verbosePrint(Options::VERB_DEBUG,
-			                     "Too many colors for grayscale sorting (%zu > %" PRIu8 ")\n",
-			                     colors.size(), options.maxOpaqueColors());
-			return false;
-		}
-		uint8_t bins = 0;
-		for (auto const &color : colors) {
-			if (!color.has_value() || color->isTransparent()) {
-				continue;
-			}
-			if (!color->isGray()) {
-				options.verbosePrint(Options::VERB_DEBUG,
-				                     "Found non-gray color #%08x, not using grayscale sorting\n",
-				                     color->toCSS());
-				return false;
-			}
-			uint8_t mask = 1 << color->grayIndex();
-			if (bins & mask) { // Two in the same bin!
-				options.verbosePrint(
-				    Options::VERB_DEBUG,
-				    "Color #%08x conflicts with another one, not using grayscale sorting\n",
-				    color->toCSS());
-				return false;
-			}
-			bins |= mask;
-		}
-		return true;
-	}
-
-	/*
-	 * Reads a PNG and notes all of its colors
-	 *
-	 * This code is more complicated than strictly necessary, but that's because of the API
-	 * being used: the "high-level" interface doesn't provide all the transformations we need,
-	 * so we use the "lower-level" one instead.
-	 * We also use that occasion to only read the PNG one line at a time, since we store all of
-	 * the pixel data in `pixels`, which saves on memory allocations.
-	 */
-	explicit Png(std::string const &filePath) : path(filePath), colors() {
-		if (file.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) {
-			fatal("Failed to open input image (\"%s\"): %s", file.c_str(path), strerror(errno));
-		}
-
-		options.verbosePrint(Options::VERB_LOG_ACT, "Opened input file\n");
-
-		std::array<unsigned char, 8> pngHeader;
-
-		if (file->sgetn(reinterpret_cast<char *>(pngHeader.data()), pngHeader.size())
-		        != static_cast<std::streamsize>(pngHeader.size()) // Not enough bytes?
-		    || png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) {
-			fatal("Input file (\"%s\") is not a PNG image!", file.c_str(path));
-		}
-
-		options.verbosePrint(Options::VERB_INTERM, "PNG header signature is OK\n");
-
-		png = png_create_read_struct(PNG_LIBPNG_VER_STRING, (png_voidp)this, handleError,
-		                             handleWarning);
-		if (!png) {
-			fatal("Failed to allocate PNG structure: %s", strerror(errno));
-		}
-
-		info = png_create_info_struct(png);
-		if (!info) {
-			png_destroy_read_struct(&png, nullptr, nullptr);
-			fatal("Failed to allocate PNG info structure: %s", strerror(errno));
-		}
-
-		png_set_read_fn(png, this, readData);
-		png_set_sig_bytes(png, pngHeader.size());
-
-		// TODO: png_set_crc_action(png, PNG_CRC_ERROR_QUIT, PNG_CRC_WARN_DISCARD);
-
-		// Skipping chunks we don't use should improve performance
-		// TODO: png_set_keep_unknown_chunks(png, ...);
-
-		// Process all chunks up to but not including the image data
-		png_read_info(png, info);
-
-		int bitDepth, interlaceType; //, compressionType, filterMethod;
-
-		png_get_IHDR(png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr,
-		             nullptr);
-
-		if (options.inputSlice.width == 0 && width % 8 != 0) {
-			fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", width);
-		}
-		if (options.inputSlice.height == 0 && height % 8 != 0) {
-			fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", height);
-		}
-
-		pixels.resize(static_cast<size_t>(width) * static_cast<size_t>(height));
-
-		auto colorTypeName = [this]() {
-			switch (colorType) {
-			case PNG_COLOR_TYPE_GRAY:
-				return "grayscale";
-			case PNG_COLOR_TYPE_GRAY_ALPHA:
-				return "grayscale + alpha";
-			case PNG_COLOR_TYPE_PALETTE:
-				return "palette";
-			case PNG_COLOR_TYPE_RGB:
-				return "RGB";
-			case PNG_COLOR_TYPE_RGB_ALPHA:
-				return "RGB + alpha";
-			default:
-				fatal("Unknown color type %d", colorType);
-			}
-		};
-		auto interlaceTypeName = [&interlaceType]() {
-			switch (interlaceType) {
-			case PNG_INTERLACE_NONE:
-				return "not interlaced";
-			case PNG_INTERLACE_ADAM7:
-				return "interlaced (Adam7)";
-			default:
-				fatal("Unknown interlace type %d", interlaceType);
-			}
-		};
-		options.verbosePrint(Options::VERB_INTERM,
-		                     "Input image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n", width,
-		                     height, bitDepth, colorTypeName(), interlaceTypeName());
-
-		if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) {
-			int nbTransparentEntries;
-			if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) {
-				assert(nbTransparentEntries == nbColors);
-			}
-
-			options.verbosePrint(Options::VERB_INTERM, "Embedded palette has %d colors: [",
-			                     nbColors);
-			for (int i = 0; i < nbColors; ++i) {
-				auto const &color = embeddedPal[i];
-				options.verbosePrint(
-				    Options::VERB_INTERM, "#%02x%02x%02x%02x%s", color.red, color.green, color.blue,
-				    transparencyPal ? transparencyPal[i] : 0xFF, i != nbColors - 1 ? ", " : "]\n");
-			}
-		} else {
-			options.verbosePrint(Options::VERB_INTERM, "No embedded palette\n");
-		}
-
-		// Set up transformations; to turn everything into RGBA888
-		// TODO: it's not necessary to uniformize the pixel data (in theory), and not doing
-		// so *might* improve performance, and should reduce memory usage.
-
-		// Convert grayscale to RGB
-		switch (colorType & ~PNG_COLOR_MASK_ALPHA) {
-		case PNG_COLOR_TYPE_GRAY:
-			png_set_gray_to_rgb(png); // This also converts tRNS to alpha
-			break;
-		case PNG_COLOR_TYPE_PALETTE:
-			png_set_palette_to_rgb(png);
-			break;
-		}
-
-		if (png_get_valid(png, info, PNG_INFO_tRNS)) {
-			// If we read a tRNS chunk, convert it to alpha
-			png_set_tRNS_to_alpha(png);
-		} else if (!(colorType & PNG_COLOR_MASK_ALPHA)) {
-			// Otherwise, if we lack an alpha channel, default to full opacity
-			png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER);
-		}
-
-		// Scale 16bpp back to 8 (we don't need all of that precision anyway)
-		if (bitDepth == 16) {
-			png_set_scale_16(png);
-		} else if (bitDepth < 8) {
-			png_set_packing(png);
-		}
-
-		// Do NOT call `png_set_interlace_handling`. We want to expand the rows ourselves.
-
-		// Update `info` with the transformations
-		png_read_update_info(png, info);
-		// These shouldn't have changed
-		assert(png_get_image_width(png, info) == width);
-		assert(png_get_image_height(png, info) == height);
-		// These should have changed, however
-		assert(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA);
-		assert(png_get_bit_depth(png, info) == 8);
-
-		// Now that metadata has been read, we can process the image data
-
-		size_t nbRowBytes = png_get_rowbytes(png, info);
-		assert(nbRowBytes != 0);
-		DefaultInitVec<png_byte> row(nbRowBytes);
-		// Holds known-conflicting color pairs to avoid warning about them twice.
-		// We don't need to worry about transitivity, as ImagePalette slots are immutable once
-		// assigned, and conflicts always occur between that and another color.
-		// For the same reason, we don't need to worry about order, either.
-		std::vector<std::tuple<uint32_t, uint32_t>> conflicts;
-		// Holds colors whose alpha value is ambiguous
-		std::vector<uint32_t> indeterminates;
-
-		// Assign a color to the given position, and register it in the image palette as well
-		auto assignColor = [this, &conflicts, &indeterminates](png_uint_32 x, png_uint_32 y,
-		                                                       Rgba &&color) {
-			if (!color.isTransparent() && !color.isOpaque()) {
-				uint32_t css = color.toCSS();
-				if (std::find(indeterminates.begin(), indeterminates.end(), css)
-				    == indeterminates.end()) {
-					error("Color #%08x is neither transparent (alpha < %u) nor opaque (alpha >= "
-					      "%u) [first seen at x: %" PRIu32 ", y: %" PRIu32 "]",
-					      css, Rgba::transparency_threshold, Rgba::opacity_threshold, x, y);
-					indeterminates.push_back(css);
-				}
-			} else if (Rgba const *other = colors.registerColor(color); other) {
-				std::tuple conflicting{color.toCSS(), other->toCSS()};
-				// Do not report combinations twice
-				if (std::find(conflicts.begin(), conflicts.end(), conflicting) == conflicts.end()) {
-					warning("Fusing colors #%08x and #%08x into Game Boy color $%04x [first seen "
-					        "at x: %" PRIu32 ", y: %" PRIu32 "]",
-					        std::get<0>(conflicting), std::get<1>(conflicting), color.cgbColor(), x,
-					        y);
-					// Do not report this combination again
-					conflicts.emplace_back(conflicting);
-				}
-			}
-
-			pixel(x, y) = color;
-		};
-
-		if (interlaceType == PNG_INTERLACE_NONE) {
-			for (png_uint_32 y = 0; y < height; ++y) {
-				png_read_row(png, row.data(), nullptr);
-
-				for (png_uint_32 x = 0; x < width; ++x) {
-					assignColor(x, y,
-					            Rgba(row[x * 4], row[x * 4 + 1], row[x * 4 + 2], row[x * 4 + 3]));
-				}
-			}
-		} else {
-			assert(interlaceType == PNG_INTERLACE_ADAM7);
-
-			// For interlace to work properly, we must read the image `nbPasses` times
-			for (int pass = 0; pass < PNG_INTERLACE_ADAM7_PASSES; ++pass) {
-				// The interlacing pass must be skipped if its width or height is reported as zero
-				if (PNG_PASS_COLS(width, pass) == 0 || PNG_PASS_ROWS(height, pass) == 0) {
-					continue;
-				}
-
-				png_uint_32 xStep = 1u << PNG_PASS_COL_SHIFT(pass);
-				png_uint_32 yStep = 1u << PNG_PASS_ROW_SHIFT(pass);
-
-				for (png_uint_32 y = PNG_PASS_START_ROW(pass); y < height; y += yStep) {
-					png_bytep ptr = row.data();
-					png_read_row(png, ptr, nullptr);
-
-					for (png_uint_32 x = PNG_PASS_START_COL(pass); x < width; x += xStep) {
-						assignColor(x, y, Rgba(ptr[0], ptr[1], ptr[2], ptr[3]));
-						ptr += 4;
-					}
-				}
-			}
-		}
-
-		// We don't care about chunks after the image data (comments, etc.)
-		png_read_end(png, nullptr);
-	}
-
-	~Png() { png_destroy_read_struct(&png, &info, nullptr); }
-
-	class TilesVisitor {
-		Png const &_png;
-		bool const _columnMajor;
-		uint32_t const _width, _height;
-		uint32_t const _limit = _columnMajor ? _height : _width;
-
-	public:
-		TilesVisitor(Png const &png, bool columnMajor, uint32_t width, uint32_t height)
-		    : _png(png), _columnMajor(columnMajor), _width(width), _height(height) {}
-
-		class Tile {
-			Png const &_png;
-		public:
-			uint32_t const x, y;
-
-			Tile(Png const &png, uint32_t x_, uint32_t y_) : _png(png), x(x_), y(y_) {}
-
-			Rgba pixel(uint32_t xOfs, uint32_t yOfs) const {
-				return _png.pixel(x + xOfs, y + yOfs);
-			}
-		};
-
-	private:
-		struct iterator {
-			TilesVisitor const &parent;
-			uint32_t const limit;
-			uint32_t x, y;
-
-			std::pair<uint32_t, uint32_t> coords() const {
-				return {x + options.inputSlice.left, y + options.inputSlice.top};
-			}
-			Tile operator*() const {
-				return {parent._png, x + options.inputSlice.left, y + options.inputSlice.top};
-			}
-
-			iterator &operator++() {
-				auto [major, minor] = parent._columnMajor ? std::tie(y, x) : std::tie(x, y);
-				major += 8;
-				if (major == limit) {
-					minor += 8;
-					major = 0;
-				}
-				return *this;
-			}
-
-			friend bool operator==(iterator const &lhs, iterator const &rhs) {
-				return lhs.coords() == rhs.coords(); // Compare the returned coord pairs
-			}
-
-			friend bool operator!=(iterator const &lhs, iterator const &rhs) {
-				return lhs.coords() != rhs.coords(); // Compare the returned coord pairs
-			}
-		};
-
-	public:
-		iterator begin() const { return {*this, _limit, 0, 0}; }
-		iterator end() const {
-			iterator it{*this, _limit, _width - 8, _height - 8}; // Last valid one...
-			return ++it; // ...now one-past-last!
-		}
-	};
-public:
-	TilesVisitor visitAsTiles() const {
-		return {*this, options.columnMajor,
-		        options.inputSlice.width ? options.inputSlice.width * 8 : width,
-		        options.inputSlice.height ? options.inputSlice.height * 8 : height};
-	}
-};
-
-class RawTiles {
-	/*
-	 * A tile which only contains indices into the image's global palette
-	 */
-	class RawTile {
-		std::array<std::array<size_t, 8>, 8> _pixelIndices{};
-
-	public:
-		// Not super clean, but it's closer to matrix notation
-		size_t &operator()(size_t x, size_t y) { return _pixelIndices[y][x]; }
-	};
-
-private:
-	std::vector<RawTile> _tiles;
-
-public:
-	/*
-	 * Creates a new raw tile, and returns a reference to it so it can be filled in
-	 */
-	RawTile &newTile() {
-		_tiles.emplace_back();
-		return _tiles.back();
-	}
-};
-
-struct AttrmapEntry {
-	/*
-	 * This field can either be a proto-palette ID, or `transparent` to indicate that the
-	 * corresponding tile is fully transparent. If you are looking to get the palette ID for this
-	 * attrmap entry while correctly handling the above, use `getPalID`.
-	 */
-	size_t protoPaletteID; // Only this field is used when outputting "unoptimized" data
-	uint8_t tileID; // This is the ID as it will be output to the tilemap
-	bool bank;
-	bool yFlip;
-	bool xFlip;
-
-	static constexpr decltype(protoPaletteID) transparent = SIZE_MAX;
-
-	size_t getPalID(DefaultInitVec<size_t> const &mappings) const {
-		return protoPaletteID == transparent ? 0 : mappings[protoPaletteID];
-	}
-};
-
-static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
-    generatePalettes(std::vector<ProtoPalette> const &protoPalettes, Png const &png) {
-	// Run a "pagination" problem solver
-	// TODO: allow picking one of several solvers?
-	auto [mappings, nbPalettes] = packing::overloadAndRemove(protoPalettes);
-	assert(mappings.size() == protoPalettes.size());
-
-	if (options.verbosity >= Options::VERB_INTERM) {
-		fprintf(stderr, "Proto-palette mappings: (%zu palette%s)\n", nbPalettes,
-		        nbPalettes != 1 ? "s" : "");
-		for (size_t i = 0; i < mappings.size(); ++i) {
-			fprintf(stderr, "%zu -> %zu\n", i, mappings[i]);
-		}
-	}
-
-	std::vector<Palette> palettes(nbPalettes);
-	// If the image contains at least one transparent pixel, force transparency in the first slot of
-	// all palettes
-	if (options.hasTransparentPixels) {
-		for (Palette &pal : palettes) {
-			pal.colors[0] = Rgba::transparent;
-		}
-	}
-	// Generate the actual palettes from the mappings
-	for (size_t protoPalID = 0; protoPalID < mappings.size(); ++protoPalID) {
-		auto &pal = palettes[mappings[protoPalID]];
-		for (uint16_t color : protoPalettes[protoPalID]) {
-			pal.addColor(color);
-		}
-	}
-
-	// "Sort" colors in the generated palettes, see the man page for the flowchart
-	auto [embPalSize, embPalRGB, embPalAlpha] = png.getEmbeddedPal();
-	if (embPalRGB != nullptr) {
-		sorting::indexed(palettes, embPalSize, embPalRGB, embPalAlpha);
-	} else if (png.isSuitableForGrayscale()) {
-		sorting::grayscale(palettes, png.getColors().raw());
-	} else {
-		sorting::rgb(palettes);
-	}
-	return {mappings, palettes};
-}
-
-static std::tuple<DefaultInitVec<size_t>, std::vector<Palette>>
-    makePalsAsSpecified(std::vector<ProtoPalette> const &protoPalettes, Png const &png) {
-	if (options.palSpecType == Options::EMBEDDED) {
-		// Generate a palette spec from the first few colors in the embedded palette
-		auto [embPalSize, embPalRGB, embPalAlpha] = png.getEmbeddedPal();
-		if (embPalRGB == nullptr) {
-			fatal("`-c embedded` was given, but the PNG does not have an embedded palette!");
-		}
-
-		// Fill in the palette spec
-		options.palSpec.emplace_back(); // A single palette, with `#00000000`s (transparent)
-		assert(options.palSpec.size() == 1);
-		if (embPalSize > options.maxOpaqueColors()) { // Ignore extraneous colors if they are unused
-			embPalSize = options.maxOpaqueColors();
-		}
-		for (int i = 0; i < embPalSize; ++i) {
-			options.palSpec[0][i] = Rgba(embPalRGB[i].red, embPalRGB[i].green, embPalRGB[i].blue,
-			                             embPalAlpha ? embPalAlpha[i] : 0xFF);
-		}
-	}
-
-	// Convert the palette spec to actual palettes
-	std::vector<Palette> palettes(options.palSpec.size());
-	for (auto [spec, pal] : zip(options.palSpec, palettes)) {
-		for (size_t i = 0; i < options.nbColorsPerPal && spec[i].isOpaque(); ++i) {
-			pal[i] = spec[i].cgbColor();
-		}
-	}
-
-	auto listColors = [](auto const &list) {
-		static char buf[sizeof(", $XXXX, $XXXX, $XXXX, $XXXX")];
-		char *ptr = buf;
-		for (uint16_t cgbColor : list) {
-			sprintf(ptr, ", $%04x", cgbColor);
-			ptr += 7;
-		}
-		return &buf[2];
-	};
-
-	// Iterate through proto-palettes, and try mapping them to the specified palettes
-	DefaultInitVec<size_t> mappings(protoPalettes.size());
-	bool bad = false;
-	for (size_t i = 0; i < protoPalettes.size(); ++i) {
-		ProtoPalette const &protoPal = protoPalettes[i];
-		// Find the palette...
-		auto iter = std::find_if(palettes.begin(), palettes.end(), [&protoPal](Palette const &pal) {
-			// ...which contains all colors in this proto-pal
-			return std::all_of(protoPal.begin(), protoPal.end(), [&pal](uint16_t color) {
-				return std::find(pal.begin(), pal.end(), color) != pal.end();
-			});
-		});
-
-		if (iter == palettes.end()) {
-			assert(!protoPal.empty());
-			error("Could not fit tile colors [%s] in specified palettes", listColors(protoPal));
-			bad = true;
-		}
-		mappings[i] = iter - palettes.begin(); // Bogus value, but whatever
-	}
-	if (bad) {
-		fprintf(stderr, "note: The following palette%s specified:\n",
-		        palettes.size() == 1 ? " was" : "s were");
-		for (Palette const &pal : palettes) {
-			fprintf(stderr, "        [%s]\n", listColors(pal));
-		}
-		giveUp();
-	}
-
-	return {mappings, palettes};
-}
-
-static void outputPalettes(std::vector<Palette> const &palettes) {
-	File output;
-	if (!output.open(options.palettes, std::ios_base::out | std::ios_base::binary)) {
-		fatal("Failed to open \"%s\": %s", output.c_str(options.palettes), strerror(errno));
-	}
-
-	for (Palette const &palette : palettes) {
-		for (uint8_t i = 0; i < options.nbColorsPerPal; ++i) {
-			uint16_t color = palette.colors[i]; // Will return `UINT16_MAX` for unused slots
-			output->sputc(color & 0xFF);
-			output->sputc(color >> 8);
-		}
-	}
-}
-
-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
-	// if horizontal mirroring is in effect. It should still be a reasonable tie-breaker in
-	// non-pathological cases.
-	uint16_t _hash;
-public:
-	// This is an index within the "global" pool; no bank info is encoded here
-	// It's marked as `mutable` so that it can be modified even on a `const` object;
-	// this is necessary because the `set` in which it's inserted refuses any modification for fear
-	// of altering the element's hash, but the tile ID is not part of it.
-	mutable uint16_t tileID;
-
-	static uint16_t rowBitplanes(Png::TilesVisitor::Tile const &tile, Palette const &palette,
-	                             uint32_t y) {
-		uint16_t row = 0;
-		for (uint32_t x = 0; x < 8; ++x) {
-			row <<= 1;
-			uint8_t index = palette.indexOf(tile.pixel(x, y).cgbColor());
-			assert(index < palette.size()); // The color should be in the palette
-			if (index & 1) {
-				row |= 1;
-			}
-			if (index & 2) {
-				row |= 0x100;
-			}
-		}
-		return row;
-	}
-
-	TileData(Png::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) {
-		size_t writeIndex = 0;
-		for (uint32_t y = 0; y < 8; ++y) {
-			uint16_t bitplanes = rowBitplanes(tile, palette, y);
-			_data[writeIndex++] = bitplanes & 0xFF;
-			if (options.bitDepth == 2) {
-				_data[writeIndex++] = bitplanes >> 8;
-			}
-
-			// Update the hash
-			_hash ^= bitplanes;
-			if (options.allowMirroring) {
-				// Count the line itself as mirrorred; vertical mirroring is
-				// already taken care of because the symmetric line will be XOR'd
-				// the same way. (...which is a problem, but probably benign.)
-				_hash ^= flipTable[bitplanes >> 8] << 8 | flipTable[bitplanes & 0xFF];
-			}
-		}
-	}
-
-	auto const &data() const { return _data; }
-	uint16_t hash() const { return _hash; }
-
-	enum MatchType {
-		NOPE,
-		EXACT,
-		HFLIP,
-		VFLIP,
-		VHFLIP,
-	};
-	MatchType tryMatching(TileData const &other) const {
-		// Check for strict equality first, as that can typically be optimized, and it allows
-		// hoisting the mirroring check out of the loop
-		if (_data == other._data) {
-			return MatchType::EXACT;
-		}
-
-		if (!options.allowMirroring) {
-			return MatchType::NOPE;
-		}
-
-		// Check if we have horizontal mirroring, which scans the array forward again
-		if (std::equal(_data.begin(), _data.end(), other._data.begin(),
-		               [](uint8_t lhs, uint8_t rhs) { return lhs == flipTable[rhs]; })) {
-			return MatchType::HFLIP;
-		}
-
-		// Check if we have vertical or vertical+horizontal mirroring, for which we have to read
-		// bitplane *pairs*  backwards
-		bool hasVFlip = true, hasVHFlip = true;
-		for (uint8_t i = 0; i < _data.size(); ++i) {
-			// Flip the bottom bit to get the corresponding row's bitplane 0/1
-			// (This works because the array size is even)
-			uint8_t lhs = _data[i], rhs = other._data[(15 - i) ^ 1];
-			if (lhs != rhs) {
-				hasVFlip = false;
-			}
-			if (lhs != flipTable[rhs]) {
-				hasVHFlip = false;
-			}
-			if (!hasVFlip && !hasVHFlip) {
-				return MatchType::NOPE; // If both have been eliminated, all hope is lost!
-			}
-		}
-
-		// If we have both (i.e. we have symmetry), default to vflip only
-		assert(hasVFlip || hasVHFlip);
-		return hasVFlip ? MatchType::VFLIP : MatchType::VHFLIP;
-	}
-	friend bool operator==(TileData const &lhs, TileData const &rhs) {
-		return lhs.tryMatching(rhs) != MatchType::NOPE;
-	}
-};
-
-template<>
-struct std::hash<TileData> {
-	std::size_t operator()(TileData const &tile) const { return tile.hash(); }
-};
-
-namespace unoptimized {
-
-static void outputTileData(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
-                           std::vector<Palette> const &palettes,
-                           DefaultInitVec<size_t> const &mappings) {
-	File output;
-	if (!output.open(options.output, std::ios_base::out | std::ios_base::binary)) {
-		fatal("Failed to open \"%s\": %s", output.c_str(options.output), strerror(errno));
-	}
-
-	uint64_t remainingTiles = (png.getWidth() / 8) * (png.getHeight() / 8);
-	if (remainingTiles <= options.trim) {
-		return;
-	}
-	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);
-			}
-		}
-
-		--remainingTiles;
-		if (remainingTiles == 0) {
-			break;
-		}
-	}
-	assert(remainingTiles == 0);
-}
-
-static void outputMaps(DefaultInitVec<AttrmapEntry> const &attrmap,
-                       DefaultInitVec<size_t> const &mappings) {
-	std::optional<File> tilemapOutput, attrmapOutput, palmapOutput;
-	if (!options.tilemap.empty()) {
-		tilemapOutput.emplace();
-		if (!tilemapOutput->open(options.tilemap, std::ios_base::out | std::ios_base::binary)) {
-			fatal("Failed to open \"%s\": %s", tilemapOutput->c_str(options.tilemap),
-			      strerror(errno));
-		}
-	}
-	if (!options.attrmap.empty()) {
-		attrmapOutput.emplace();
-		if (!attrmapOutput->open(options.attrmap, std::ios_base::out | std::ios_base::binary)) {
-			fatal("Failed to open \"%s\": %s", attrmapOutput->c_str(options.attrmap),
-			      strerror(errno));
-		}
-	}
-	if (!options.palmap.empty()) {
-		palmapOutput.emplace();
-		if (!palmapOutput->open(options.palmap, std::ios_base::out | std::ios_base::binary)) {
-			fatal("Failed to open \"%s\": %s", palmapOutput->c_str(options.palmap),
-			      strerror(errno));
-		}
-	}
-
-	uint8_t tileID = 0;
-	uint8_t bank = 0;
-	for (auto attr : attrmap) {
-		if (tileID == options.maxNbTiles[bank]) {
-			assert(bank == 0);
-			bank = 1;
-			tileID = 0;
-		}
-
-		if (tilemapOutput.has_value()) {
-			(*tilemapOutput)->sputc(tileID + options.baseTileIDs[bank]);
-		}
-		if (attrmapOutput.has_value()) {
-			uint8_t palID = attr.getPalID(mappings) & 7;
-			(*attrmapOutput)->sputc(palID | bank << 3); // The other flags are all 0
-		}
-		if (palmapOutput.has_value()) {
-			(*palmapOutput)->sputc(attr.getPalID(mappings));
-		}
-		++tileID;
-	}
-}
-
-} // namespace unoptimized
-
-namespace optimized {
-
-struct UniqueTiles {
-	std::unordered_set<TileData> tileset;
-	std::vector<TileData const *> tiles;
-
-	UniqueTiles() = default;
-	// Copies are likely to break pointers, so we really don't want those.
-	// Copy elision should be relied on to be more sure that refs won't be invalidated, too!
-	UniqueTiles(UniqueTiles const &) = delete;
-	UniqueTiles(UniqueTiles &&) = default;
-
-	/*
-	 * Adds a tile to the collection, and returns its ID
-	 */
-	std::tuple<uint16_t, TileData::MatchType> addTile(Png::TilesVisitor::Tile const &tile,
-	                                                  Palette const &palette) {
-		TileData newTile(tile, palette);
-		auto [tileData, inserted] = tileset.insert(newTile);
-
-		TileData::MatchType matchType = TileData::EXACT;
-		if (inserted) {
-			// Give the new tile the next available unique ID
-			tileData->tileID = static_cast<uint16_t>(tiles.size());
-			// Pointers are never invalidated!
-			tiles.emplace_back(&*tileData);
-		} else {
-			matchType = tileData->tryMatching(newTile);
-		}
-		return {tileData->tileID, matchType};
-	}
-
-	auto size() const { return tiles.size(); }
-
-	auto begin() const { return tiles.begin(); }
-	auto end() const { return tiles.end(); }
-};
-
-/*
- * Generate tile data while deduplicating unique tiles (via mirroring if enabled)
- * Additionally, while we have the info handy, convert from the 16-bit "global" tile IDs to
- * 8-bit tile IDs + the bank bit; this will save the work when we output the data later (potentially
- * twice)
- */
-static UniqueTiles dedupTiles(Png const &png, DefaultInitVec<AttrmapEntry> &attrmap,
-                              std::vector<Palette> const &palettes,
-                              DefaultInitVec<size_t> const &mappings) {
-	// Iterate throughout the image, generating tile data as we go
-	// (We don't need the full tile data to be able to dedup tiles, but we don't lose anything
-	// by caching the full tile data anyway, so we might as well.)
-	UniqueTiles tiles;
-
-	for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) {
-		auto [tileID, matchType] = tiles.addTile(tile, palettes[mappings[attr.protoPaletteID]]);
-
-		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
-	return tiles;
-}
-
-static void outputTileData(UniqueTiles const &tiles) {
-	File output;
-	if (!output.open(options.output, std::ios_base::out | std::ios_base::binary)) {
-		fatal("Failed to create \"%s\": %s", output.c_str(options.output), strerror(errno));
-	}
-
-	uint16_t tileID = 0;
-	for (auto iter = tiles.begin(), end = tiles.end() - options.trim; iter != end; ++iter) {
-		TileData const *tile = *iter;
-		assert(tile->tileID == tileID);
-		++tileID;
-		output->sputn(reinterpret_cast<char const *>(tile->data().data()), options.bitDepth * 8);
-	}
-}
-
-static void outputTilemap(DefaultInitVec<AttrmapEntry> const &attrmap) {
-	File output;
-	if (!output.open(options.tilemap, std::ios_base::out | std::ios_base::binary)) {
-		fatal("Failed to create \"%s\": %s", output.c_str(options.tilemap), strerror(errno));
-	}
-
-	for (AttrmapEntry const &entry : attrmap) {
-		output->sputc(entry.tileID); // The tile ID has already been converted
-	}
-}
-
-static void outputAttrmap(DefaultInitVec<AttrmapEntry> const &attrmap,
-                          DefaultInitVec<size_t> const &mappings) {
-	File output;
-	if (!output.open(options.attrmap, std::ios_base::out | std::ios_base::binary)) {
-		fatal("Failed to create \"%s\": %s", output.c_str(options.attrmap), strerror(errno));
-	}
-
-	for (AttrmapEntry const &entry : attrmap) {
-		uint8_t attr = entry.xFlip << 5 | entry.yFlip << 6;
-		attr |= entry.bank << 3;
-		attr |= entry.getPalID(mappings) & 7;
-		output->sputc(attr);
-	}
-}
-
-static void outputPalmap(DefaultInitVec<AttrmapEntry> const &attrmap,
-                         DefaultInitVec<size_t> const &mappings) {
-	File output;
-	if (!output.open(options.attrmap, std::ios_base::out | std::ios_base::binary)) {
-		fatal("Failed to create \"%s\": %s", output.c_str(options.attrmap), strerror(errno));
-	}
-
-	for (AttrmapEntry const &entry : attrmap) {
-		output->sputc(entry.getPalID(mappings));
-	}
-}
-
-} // namespace optimized
-
-void process() {
-	options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
-
-	options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
-	Png png(options.input); // This also sets `hasTransparentPixels` as a side effect
-	ImagePalette const &colors = png.getColors();
-
-	// Now, we have all the image's colors in `colors`
-	// The next step is to order the palette
-
-	if (options.verbosity >= Options::VERB_INTERM) {
-		fputs("Image colors: [ ", stderr);
-		for (auto const &slot : colors) {
-			if (!slot.has_value()) {
-				continue;
-			}
-			fprintf(stderr, "#%08x, ", slot->toCSS());
-		}
-		fputs("]\n", stderr);
-	}
-
-	// Now, iterate through the tiles, generating proto-palettes as we go
-	// We do this unconditionally because this performs the image validation (which we want to
-	// perform even if no output is requested), and because it's necessary to generate any
-	// output (with the exception of an un-duplicated tilemap, but that's an acceptable loss.)
-	std::vector<ProtoPalette> protoPalettes;
-	DefaultInitVec<AttrmapEntry> attrmap{};
-
-	for (auto tile : png.visitAsTiles()) {
-		ProtoPalette tileColors;
-		AttrmapEntry &attrs = attrmap.emplace_back();
-		uint8_t nbColorsInTile = 0;
-
-		for (uint32_t y = 0; y < 8; ++y) {
-			for (uint32_t x = 0; x < 8; ++x) {
-				Rgba color = tile.pixel(x, y);
-				if (!color.isTransparent()) { // Do not count transparency in for packing
-					// Add the color to the proto-pal (if not full), and count it if it was unique.
-					if (tileColors.add(color.cgbColor())) {
-						++nbColorsInTile;
-					}
-				}
-			}
-		}
-
-		if (tileColors.empty()) {
-			// "Empty" proto-palettes screw with the packing process, so discard those
-			attrs.protoPaletteID = AttrmapEntry::transparent;
-			continue;
-		}
-
-		// Insert the proto-palette, making sure to avoid overlaps
-		for (size_t n = 0; n < protoPalettes.size(); ++n) {
-			switch (tileColors.compare(protoPalettes[n])) {
-			case ProtoPalette::WE_BIGGER:
-				protoPalettes[n] = tileColors; // Override them
-				// Remove any other proto-palettes that we encompass
-				// (Example [(0, 1), (0, 2)], inserting (0, 1, 2))
-				/*
-				 * The following code does its job, except that references to the removed
-				 * proto-palettes are not updated, causing issues.
-				 * TODO: overlap might not be detrimental to the packing algorithm.
-				 * Investigation is necessary, especially if pathological cases are found.
-				 *
-				 * for (size_t i = protoPalettes.size(); --i != n;) {
-				 *     if (tileColors.compare(protoPalettes[i]) == ProtoPalette::WE_BIGGER) {
-				 *         protoPalettes.erase(protoPalettes.begin() + i);
-				 *     }
-				 * }
-				 */
-				[[fallthrough]];
-
-			case ProtoPalette::THEY_BIGGER:
-				// Do nothing, they already contain us
-				attrs.protoPaletteID = n;
-				goto contained;
-
-			case ProtoPalette::NEITHER:
-				break; // Keep going
-			}
-		}
-
-		if (nbColorsInTile > options.maxOpaqueColors()) {
-			fatal("Tile at (%" PRIu32 ", %" PRIu32 ") has %zu opaque colors, more than %" PRIu8 "!",
-			      tile.x, tile.y, nbColorsInTile, options.maxOpaqueColors());
-		}
-
-		attrs.protoPaletteID = protoPalettes.size();
-		if (protoPalettes.size() == AttrmapEntry::transparent) { // Check for overflow
-			fatal("Reached %zu proto-palettes... sorry, this image is too much for me to handle :(",
-			      AttrmapEntry::transparent);
-		}
-		protoPalettes.push_back(tileColors);
-contained:;
-	}
-
-	options.verbosePrint(Options::VERB_INTERM, "Image contains %zu proto-palette%s\n",
-	                     protoPalettes.size(), protoPalettes.size() != 1 ? "s" : "");
-	if (options.verbosity >= Options::VERB_INTERM) {
-		for (auto const &protoPal : protoPalettes) {
-			fputs("[ ", stderr);
-			for (uint16_t color : protoPal) {
-				fprintf(stderr, "$%04x, ", color);
-			}
-			fputs("]\n", stderr);
-		}
-	}
-
-	auto [mappings, palettes] = options.palSpecType == Options::NO_SPEC
-	                                ? generatePalettes(protoPalettes, png)
-	                                : makePalsAsSpecified(protoPalettes, png);
-
-	if (options.verbosity >= Options::VERB_INTERM) {
-		for (auto &&palette : palettes) {
-			fputs("{ ", stderr);
-			for (uint16_t colorIndex : palette) {
-				fprintf(stderr, "%04" PRIx16 ", ", colorIndex);
-			}
-			fputs("}\n", stderr);
-		}
-	}
-
-	if (palettes.size() > options.nbPalettes) {
-		// If the palette generation is wrong, other (dependee) operations are likely to be
-		// nonsensical, so fatal-error outright
-		fatal("Generated %zu palettes, over the maximum of %" PRIu8, palettes.size(),
-		      options.nbPalettes);
-	}
-
-	if (!options.palettes.empty()) {
-		outputPalettes(palettes);
-	}
-
-	// If deduplication is not happening, we just need to output the tile data and/or maps as-is
-	if (!options.allowDedup) {
-		uint32_t const nbTilesH = png.getHeight() / 8, nbTilesW = png.getWidth() / 8;
-
-		// Check the tile count
-		if (nbTilesW * nbTilesH > options.maxNbTiles[0] + options.maxNbTiles[1]) {
-			fatal("Image contains %" PRIu32 " tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,
-			      nbTilesW * nbTilesH, options.maxNbTiles[0], options.maxNbTiles[1]);
-		}
-
-		if (!options.output.empty()) {
-			options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n");
-			unoptimized::outputTileData(png, attrmap, palettes, mappings);
-		}
-
-		if (!options.tilemap.empty() || !options.attrmap.empty() || !options.palmap.empty()) {
-			options.verbosePrint(
-			    Options::VERB_LOG_ACT,
-			    "Generating unoptimized tilemap and/or attrmap and/or palmap...\n");
-			unoptimized::outputMaps(attrmap, mappings);
-		}
-	} else {
-		// All of these require the deduplication process to be performed to be output
-		options.verbosePrint(Options::VERB_LOG_ACT, "Deduplicating tiles...\n");
-		optimized::UniqueTiles tiles = optimized::dedupTiles(png, attrmap, palettes, mappings);
-
-		if (tiles.size() > options.maxNbTiles[0] + options.maxNbTiles[1]) {
-			fatal("Image contains %zu tiles, exceeding the limit of %" PRIu16 " + %" PRIu16,
-			      tiles.size(), options.maxNbTiles[0], options.maxNbTiles[1]);
-		}
-
-		if (!options.output.empty()) {
-			options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized tile data...\n");
-			optimized::outputTileData(tiles);
-		}
-
-		if (!options.tilemap.empty()) {
-			options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized tilemap...\n");
-			optimized::outputTilemap(attrmap);
-		}
-
-		if (!options.attrmap.empty()) {
-			options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized attrmap...\n");
-			optimized::outputAttrmap(attrmap, mappings);
-		}
-
-		if (!options.palmap.empty()) {
-			options.verbosePrint(Options::VERB_LOG_ACT, "Generating optimized palmap...\n");
-			optimized::outputPalmap(attrmap, mappings);
-		}
-	}
-}
--- a/src/gfx/proto_palette.cpp
+++ /dev/null
@@ -1,88 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#include "gfx/proto_palette.hpp"
-
-#include <algorithm>
-#include <array>
-#include <cassert>
-#include <stddef.h>
-#include <stdint.h>
-
-bool ProtoPalette::add(uint16_t color) {
-	size_t i = 0;
-
-	// Seek the first slot greater than the new color
-	// (A linear search is better because we don't store the array size,
-	// and there are very few slots anyway)
-	while (_colorIndices[i] < color) {
-		++i;
-		if (i == _colorIndices.size()) {
-			// We reached the end of the array without finding the color, so it's a new one.
-			return true;
-		}
-	}
-	// If we found it, great! Nothing else to do.
-	if (_colorIndices[i] == color) {
-		return false;
-	}
-
-	// Swap entries until the end
-	while (_colorIndices[i] != UINT16_MAX) {
-		std::swap(_colorIndices[i], color);
-		++i;
-		if (i == _colorIndices.size()) {
-			// The set is full, but doesn't include the new color.
-			return true;
-		}
-	}
-	// Write that last one into the new slot
-	_colorIndices[i] = color;
-	return true;
-}
-
-ProtoPalette::ComparisonResult ProtoPalette::compare(ProtoPalette const &other) const {
-	// This works because the sets are sorted numerically
-	assert(std::is_sorted(_colorIndices.begin(), _colorIndices.end()));
-	assert(std::is_sorted(other._colorIndices.begin(), other._colorIndices.end()));
-
-	auto ours = _colorIndices.begin(), theirs = other._colorIndices.begin();
-	bool weBigger = true, theyBigger = true;
-
-	while (ours != _colorIndices.end() && theirs != other._colorIndices.end()) {
-		if (*ours == *theirs) {
-			++ours;
-			++theirs;
-		} else if (*ours < *theirs) {
-			++ours;
-			theyBigger = false;
-		} else { // *ours > *theirs
-			++theirs;
-			weBigger = false;
-		}
-	}
-	weBigger &= theirs == other._colorIndices.end();
-	theyBigger &= ours == _colorIndices.end();
-
-	return theyBigger ? THEY_BIGGER : (weBigger ? WE_BIGGER : NEITHER);
-}
-
-size_t ProtoPalette::size() const {
-	return std::distance(begin(), end());
-}
-
-bool ProtoPalette::empty() const {
-	return _colorIndices[0] == UINT16_MAX;
-}
-
-auto ProtoPalette::begin() const -> decltype(_colorIndices)::const_iterator {
-	return _colorIndices.begin();
-}
-auto ProtoPalette::end() const -> decltype(_colorIndices)::const_iterator {
-	return std::find(_colorIndices.begin(), _colorIndices.end(), UINT16_MAX);
-}
--- a/src/gfx/reverse.cpp
+++ /dev/null
@@ -1,335 +1,0 @@
-/*
- * 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 "file.hpp"
-#include "helpers.h"
-#include "itertools.hpp"
-
-#include "gfx/main.hpp"
-
-static DefaultInitVec<uint8_t> readInto(std::string path) {
-	File file;
-	if (!file.open(path, std::ios::in | std::ios::binary)) {
-		fatal("Failed to open \"%s\": %s", file.c_str(path), strerror(errno));
-	}
-	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<File *>(png_get_io_ptr(png));
-	pngFile->sputn(reinterpret_cast<char *>(data), length);
-}
-
-void flushPng(png_structp png) {
-	auto &pngFile = *static_cast<File *>(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...");
-	}
-
-	if (options.inputSlice.left != 0 || options.inputSlice.top != 0
-	    || options.inputSlice.height != 0) {
-		warning("\"Sliced-off\" pixels are ignored in reverse mode");
-	}
-	if (options.inputSlice.width != 0 && options.inputSlice.width != options.reversedWidth * 8) {
-		warning("Specified input slice width (%" PRIu16
-		        ") doesn't match provided reversing width (%" PRIu8 " * 8)",
-		        options.inputSlice.width, options.reversedWidth);
-	}
-
-	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();
-		options.verbosePrint(Options::VERB_INTERM, "Read %zu tilemap entries.\n", nbTileInstances);
-	}
-
-	if (nbTileInstances == 0) {
-		fatal("Cannot generate empty image");
-	}
-	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]);
-	}
-
-	size_t width = options.reversedWidth, height; // In tiles
-	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()) {
-		File file;
-		if (!file.open(options.palettes, std::ios::in | std::ios::binary)) {
-			fatal("Failed to open \"%s\": %s", file.c_str(options.palettes), strerror(errno));
-		}
-
-		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
-		bool bad = false;
-		for (auto attr : *attrmap) {
-			if ((attr & 0b111) > palettes.size()) {
-				error("Referencing palette %u, but there are only %zu!");
-				bad = true;
-			}
-			if (attr & 0x08 && !tilemap) {
-				warning("Tile in bank 1 but no tilemap specified; ignoring the bank bit");
-			}
-		}
-		if (bad) {
-			giveUp();
-		}
-	}
-
-	if (tilemap) {
-		if (attrmap) {
-			for (auto [id, attr] : zip(*tilemap, *attrmap)) {
-				bool bank = attr & 1 << 3;
-				if (id >= options.maxNbTiles[bank]) {
-					warning("Tile #%" PRIu8
-					        " was referenced, but the limit for bank %u is %" PRIu16,
-					        id, bank, options.maxNbTiles[bank]);
-				}
-			}
-		} else {
-			for (auto id : *tilemap) {
-				if (id >= options.maxNbTiles[0]) {
-					warning("Tile #%" PRIu8 " was referenced, but the limit is %" PRIu16, id,
-					        options.maxNbTiles[0]);
-				}
-			}
-		}
-	}
-
-	std::optional<DefaultInitVec<uint8_t>> palmap;
-	if (!options.palmap.empty()) {
-		palmap = readInto(options.palmap);
-		if (palmap->size() != nbTileInstances) {
-			fatal("Palette map size (%zu tiles) doesn't match image's (%zu)", palmap->size(),
-			      nbTileInstances);
-		}
-	}
-
-	options.verbosePrint(Options::VERB_LOG_ACT, "Writing image...\n");
-	File pngFile;
-	if (!pngFile.open(options.input, std::ios::out | std::ios::binary)) {
-		fatal("Failed to create \"%s\": %s", pngFile.c_str(options.input), strerror(errno));
-	}
-	png_structp png = png_create_write_struct(
-	    PNG_LIBPNG_VER_STRING,
-	    const_cast<png_voidp>(static_cast<void const *>(pngFile.c_str(options.input))), 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);
-
-	png_set_IHDR(png, pngInfo, options.reversedWidth * 8, height * 8, 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 * 8 * 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], &tileRow.data()[1 * SIZEOF_ROW],
-	    &tileRow.data()[2 * SIZEOF_ROW], &tileRow.data()[3 * SIZEOF_ROW],
-	    &tileRow.data()[4 * SIZEOF_ROW], &tileRow.data()[5 * SIZEOF_ROW],
-	    &tileRow.data()[6 * SIZEOF_ROW], &tileRow.data()[7 * SIZEOF_ROW],
-	};
-
-	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;
-			// 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;
-			// Get the tile ID at this location
-			size_t tileID = index;
-			if (tilemap.has_value()) {
-				tileID =
-				    (*tilemap)[index] - options.baseTileIDs[bank] + bank * options.maxNbTiles[0];
-			}
-			assert(tileID < nbTileInstances); // Should have been checked earlier
-			size_t palID = palmap ? (*palmap)[index] : attribute & 0b111;
-			assert(palID < palettes.size()); // Should be ensured on data read
-
-			// 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];
-			auto const &palette = palettes[palID];
-			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 = flipTable[bitplane0];
-					bitplane1 = flipTable[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);
-	}
-
-	// Finalize the write
-	png_write_end(png, pngInfo);
-
-	png_destroy_write_struct(&png, &pngInfo);
-	pngFile.close();
-}
--- a/src/gfx/rgba.cpp
+++ /dev/null
@@ -1,60 +1,0 @@
-/*
- * This file is part of RGBDS.
- *
- * Copyright (c) 2022, Eldred Habert and RGBDS contributors.
- *
- * SPDX-License-Identifier: MIT
- */
-
-#include "gfx/rgba.hpp"
-
-#include <assert.h>
-#include <stdint.h>
-
-#include "gfx/main.hpp" // options
-
-/*
- * based on the Gaussian-like curve used by SameBoy since commit
- * 65dd02cc52f531dbbd3a7e6014e99d5b24e71a4c (Oct 2017)
- * with ties resolved by comparing the difference of the squares.
- */
-static std::array<uint8_t, 256> reverse_curve{
-    0,  0,  1,  1,  2,  2,  3,  3,  3,  3,  4,  4,  4,  4,  4,  4, // These
-    5,  5,  5,  5,  5,  5,  6,  6,  6,  6,  6,  6,  6,  6,  7,  7, // comments
-    7,  7,  7,  7,  7,  7,  7,  8,  8,  8,  8,  8,  8,  8,  8,  8, // prevent
-    9,  9,  9,  9,  9,  9,  9,  9,  9,  9,  10, 10, 10, 10, 10, 10, // clang-format
-    10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, // from
-    12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, // reflowing
-    13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 14, 14, // these
-    14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, // sixteen
-    16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, // 16-item
-    17, 17, 17, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 18, // lines,
-    18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, 19, // which,
-    19, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, // in
-    21, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 22, 22, // my
-    22, 23, 23, 23, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, // opinion,
-    24, 24, 25, 25, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, // help
-    26, 27, 27, 27, 27, 27, 28, 28, 28, 28, 29, 29, 29, 30, 30, 31, // visualization!
-};
-
-uint16_t Rgba::cgbColor() const {
-	if (isTransparent()) {
-		return transparent;
-	}
-	assert(isOpaque());
-
-	uint8_t r = red, g = green, b = blue;
-	if (options.useColorCurve) {
-		g = g * 4 < b ? 0 : (g * 4 - b) / 3;
-		r = reverse_curve[r];
-		g = reverse_curve[g];
-		b = reverse_curve[b];
-	}
-	return (r >> 3) | (g >> 3) << 5 | (b >> 3) << 10;
-}
-
-uint8_t Rgba::grayIndex() const {
-	assert(isGray());
-	// Convert from [0; 256[ to [0; maxOpaqueColors[
-	return static_cast<uint16_t>(255 - red) * options.maxOpaqueColors() / 256;
-}
--- /dev/null
+++ b/src/gfx/version.c
@@ -1,0 +1,1 @@
+#include "../version.c"