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"