mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 18:22:07 +00:00
Implement preliminary version of "reverse" feature
Not hooked to all RGBGFX flags yet, but good enough for most use cases (and as a base for future development, should I need to `reset --hard`.) TODOs marked appropriately.
This commit is contained in:
1
Makefile
1
Makefile
@@ -110,6 +110,7 @@ rgbgfx_obj := \
|
|||||||
src/gfx/pal_sorting.o \
|
src/gfx/pal_sorting.o \
|
||||||
src/gfx/process.o \
|
src/gfx/process.o \
|
||||||
src/gfx/proto_palette.o \
|
src/gfx/proto_palette.o \
|
||||||
|
src/gfx/reverse.o \
|
||||||
src/gfx/rgba.o \
|
src/gfx/rgba.o \
|
||||||
src/extern/getopt.o \
|
src/extern/getopt.o \
|
||||||
src/error.o
|
src/error.o
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
#include "gfx/rgba.hpp"
|
#include "gfx/rgba.hpp"
|
||||||
|
|
||||||
struct Options {
|
struct Options {
|
||||||
|
uint8_t reversedWidth = 0; // -r, in pixels
|
||||||
|
bool reverse() const { return reversedWidth != 0; }
|
||||||
|
|
||||||
bool useColorCurve = false; // -C
|
bool useColorCurve = false; // -C
|
||||||
bool fixInput = false; // -f
|
bool fixInput = false; // -f
|
||||||
bool allowMirroring = false; // -m
|
bool allowMirroring = false; // -m
|
||||||
@@ -36,7 +39,7 @@ struct Options {
|
|||||||
} palSpecType = NO_SPEC; // -c
|
} palSpecType = NO_SPEC; // -c
|
||||||
std::vector<std::array<Rgba, 4>> palSpec{};
|
std::vector<std::array<Rgba, 4>> palSpec{};
|
||||||
uint8_t bitDepth = 2; // -d
|
uint8_t bitDepth = 2; // -d
|
||||||
std::array<uint32_t, 4> inputSlice{0, 0, 0, 0}; // -L
|
std::array<uint32_t, 4> inputSlice{0, 0, 0, 0}; // -L (margins in clockwise order, like CSS)
|
||||||
std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // -N
|
std::array<uint16_t, 2> maxNbTiles{UINT16_MAX, 0}; // -N
|
||||||
uint8_t nbPalettes = 8; // -n
|
uint8_t nbPalettes = 8; // -n
|
||||||
std::string output{}; // -o
|
std::string output{}; // -o
|
||||||
@@ -84,4 +87,12 @@ struct Palette {
|
|||||||
uint8_t size() const;
|
uint8_t size() const;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static constexpr uint8_t flip(uint8_t byte) {
|
||||||
|
// To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
|
||||||
|
byte = (byte & 0b0000'1111) << 4 | (byte & 0b1111'0000) >> 4;
|
||||||
|
byte = (byte & 0b0011'0011) << 2 | (byte & 0b1100'1100) >> 2;
|
||||||
|
byte = (byte & 0b0101'0101) << 1 | (byte & 0b1010'1010) >> 1;
|
||||||
|
return byte;
|
||||||
|
}
|
||||||
|
|
||||||
#endif /* RGBDS_GFX_MAIN_HPP */
|
#endif /* RGBDS_GFX_MAIN_HPP */
|
||||||
|
|||||||
14
include/gfx/reverse.hpp
Normal file
14
include/gfx/reverse.hpp
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of RGBDS.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef RGBDS_GFX_REVERSE_HPP
|
||||||
|
#define RGBDS_GFX_REVERSE_HPP
|
||||||
|
|
||||||
|
void reverse();
|
||||||
|
|
||||||
|
#endif /* RGBDS_GFX_REVERSE_HPP */
|
||||||
@@ -17,13 +17,23 @@ struct Rgba {
|
|||||||
uint8_t blue;
|
uint8_t blue;
|
||||||
uint8_t alpha;
|
uint8_t alpha;
|
||||||
|
|
||||||
Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {}
|
constexpr Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a)
|
||||||
|
: red(r), green(g), blue(b), alpha(a) {}
|
||||||
/**
|
/**
|
||||||
* Constructs the color from a "packed" RGBA representation (0xRRGGBBAA)
|
* Constructs the color from a "packed" RGBA representation (0xRRGGBBAA)
|
||||||
*/
|
*/
|
||||||
explicit Rgba(uint32_t rgba = 0)
|
explicit constexpr Rgba(uint32_t rgba = 0)
|
||||||
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
|
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
|
||||||
|
|
||||||
|
static constexpr Rgba fromCGBColor(uint16_t cgbColor) {
|
||||||
|
constexpr auto _5to8 = [](uint8_t fiveBpp) -> uint8_t {
|
||||||
|
fiveBpp &= 0b11111; // For caller's convenience
|
||||||
|
return fiveBpp << 3 | fiveBpp >> 2;
|
||||||
|
};
|
||||||
|
return {_5to8(cgbColor), _5to8(cgbColor >> 5), _5to8(cgbColor >> 10),
|
||||||
|
(uint8_t)(cgbColor & 0x8000 ? 0x00 : 0xFF)};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS
|
* Returns this RGBA as a 32-bit number that can be printed in hex (`%08x`) to yield its CSS
|
||||||
* representation
|
* representation
|
||||||
|
|||||||
59
man/rgbgfx.1
59
man/rgbgfx.1
@@ -14,6 +14,7 @@
|
|||||||
.Nd Game Boy graphics converter
|
.Nd Game Boy graphics converter
|
||||||
.Sh SYNOPSIS
|
.Sh SYNOPSIS
|
||||||
.Nm
|
.Nm
|
||||||
|
.Op Fl r Ar stride
|
||||||
.Op Fl CfmuVZ
|
.Op Fl CfmuVZ
|
||||||
.Op Fl v Op Fl v No ...
|
.Op Fl v Op Fl v No ...
|
||||||
.Op Fl a Ar attrmap | Fl A
|
.Op Fl a Ar attrmap | Fl A
|
||||||
@@ -33,7 +34,7 @@
|
|||||||
.Sh DESCRIPTION
|
.Sh DESCRIPTION
|
||||||
The
|
The
|
||||||
.Nm
|
.Nm
|
||||||
program converts PNG images into data suitable for display on the Game Boy and Game Boy Color.
|
program converts PNG images into data suitable for display on the Game Boy and Game Boy Color, or vice-versa.
|
||||||
.Pp
|
.Pp
|
||||||
The main function of
|
The main function of
|
||||||
.Nm
|
.Nm
|
||||||
@@ -214,6 +215,22 @@ Specify how many colors each palette contains, including the transparent one if
|
|||||||
cannot be more than
|
cannot be more than
|
||||||
.Ql 1 << Ar depth
|
.Ql 1 << Ar depth
|
||||||
.Pq see Fl d .
|
.Pq see Fl d .
|
||||||
|
.It Fl r Ar width , Fl Fl reverse Ar width
|
||||||
|
Switches
|
||||||
|
.Nm
|
||||||
|
into
|
||||||
|
.Dq Sy reverse
|
||||||
|
mode.
|
||||||
|
In this mode, instead of converting a PNG image into Game Boy data,
|
||||||
|
.Nm
|
||||||
|
will attempt to reverse the process, and render Game Boy data into an image.
|
||||||
|
See
|
||||||
|
.Sx REVERSE MODE
|
||||||
|
below for details.
|
||||||
|
.Pp
|
||||||
|
.Ar width
|
||||||
|
is the image's width, in tiles
|
||||||
|
.Pq including any margins specified by Fl L .
|
||||||
.It Fl t Ar tilemap , Fl Fl tilemap Ar tilemap
|
.It Fl t Ar tilemap , Fl Fl tilemap Ar tilemap
|
||||||
Generate a file of tile indices.
|
Generate a file of tile indices.
|
||||||
For each square of the input image, its corresponding tile map byte contains the index of the associated tile in the tile data file.
|
For each square of the input image, its corresponding tile map byte contains the index of the associated tile in the tile data file.
|
||||||
@@ -430,6 +447,46 @@ There is no padding between colors, nor between palettes; however, empty colors
|
|||||||
TODO.
|
TODO.
|
||||||
.Ss Attrmap data
|
.Ss Attrmap data
|
||||||
TODO.
|
TODO.
|
||||||
|
.Sh REVERSE MODE
|
||||||
|
.Nm
|
||||||
|
can produce a PNG image from valid data.
|
||||||
|
This may be useful for ripping graphics, recovering lost source images, etc.
|
||||||
|
An important caveat on that last one, though: the conversion process is
|
||||||
|
.Sy lossy
|
||||||
|
both ways, so the
|
||||||
|
.Do reversed Dc image won't be perfectly identical to the original\(embut it should be close to a Game Boy's output .
|
||||||
|
.Pq Keep in mind that many of consoles output different colors, so there is no true reference rendering.
|
||||||
|
.Pp
|
||||||
|
When using reverse mode, make sure to pass the same flags that were given when generating the data, especially
|
||||||
|
.Fl C , d , N , s , x ,
|
||||||
|
and
|
||||||
|
.Fl Z .
|
||||||
|
.Do At-files Dc may help with this .
|
||||||
|
.Nm
|
||||||
|
will warn about any inconsistencies it detects.
|
||||||
|
.Pp
|
||||||
|
Files that are normally outputs
|
||||||
|
.Pq Fl a , p , t
|
||||||
|
become inputs, and
|
||||||
|
.Ar file
|
||||||
|
will be written to instead of read from, and thus needs not exist beforehand.
|
||||||
|
Any of these inputs not passed is assumed to be some default:
|
||||||
|
.Bl -column "attribute map"
|
||||||
|
.It palettes Ta Unspecified palette data makes
|
||||||
|
.Nm
|
||||||
|
assume DMG (monochrome Game Boy) mode: a single palette of 4 grays.
|
||||||
|
It is possible to pass palettes using
|
||||||
|
.Fl c
|
||||||
|
instead of
|
||||||
|
.Fl p .
|
||||||
|
.It tile data Ta Tile data must be provided, as there is no reasonable assumption to fall back on.
|
||||||
|
.It tile map Ta A missing tile map makes
|
||||||
|
.Nm
|
||||||
|
assume that tiles were not deduplicated, and should be laid out in the order they are stored.
|
||||||
|
.It attribute map Ta Without an attribute map,
|
||||||
|
.Nm
|
||||||
|
assumes that no tiles were mirrored.
|
||||||
|
.El
|
||||||
.Sh NOTES
|
.Sh NOTES
|
||||||
Some flags have had their functionality removed.
|
Some flags have had their functionality removed.
|
||||||
.Fl D
|
.Fl D
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ set(rgbgfx_src
|
|||||||
"gfx/pal_sorting.cpp"
|
"gfx/pal_sorting.cpp"
|
||||||
"gfx/process.cpp"
|
"gfx/process.cpp"
|
||||||
"gfx/proto_palette.cpp"
|
"gfx/proto_palette.cpp"
|
||||||
|
"gfx/reverse.cpp"
|
||||||
"gfx/rgba.cpp"
|
"gfx/rgba.cpp"
|
||||||
"extern/getopt.c"
|
"extern/getopt.c"
|
||||||
"error.c"
|
"error.c"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include "version.h"
|
#include "version.h"
|
||||||
|
|
||||||
#include "gfx/process.hpp"
|
#include "gfx/process.hpp"
|
||||||
|
#include "gfx/reverse.hpp"
|
||||||
|
|
||||||
using namespace std::literals::string_view_literals;
|
using namespace std::literals::string_view_literals;
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ void Options::verbosePrint(uint8_t level, char const *fmt, ...) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Short options
|
// Short options
|
||||||
static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:o:Pp:s:Tt:U:uVvx:Z";
|
static char const *optstring = "-Aa:b:Cc:Dd:FfhL:mN:n:o:Pp:r:s:Tt:U:uVvx:Z";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Equivalent long options
|
* Equivalent long options
|
||||||
@@ -113,6 +114,7 @@ static struct option const longopts[] = {
|
|||||||
{"output", required_argument, NULL, 'o'},
|
{"output", required_argument, NULL, 'o'},
|
||||||
{"output-palette", no_argument, NULL, 'P'},
|
{"output-palette", no_argument, NULL, 'P'},
|
||||||
{"palette", required_argument, NULL, 'p'},
|
{"palette", required_argument, NULL, 'p'},
|
||||||
|
{"reverse", required_argument, NULL, 'r'},
|
||||||
{"output-tilemap", no_argument, NULL, 'T'},
|
{"output-tilemap", no_argument, NULL, 'T'},
|
||||||
{"tilemap", required_argument, NULL, 't'},
|
{"tilemap", required_argument, NULL, 't'},
|
||||||
{"unit-size", required_argument, NULL, 'U'},
|
{"unit-size", required_argument, NULL, 'U'},
|
||||||
@@ -125,7 +127,7 @@ static struct option const longopts[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static void printUsage(void) {
|
static void printUsage(void) {
|
||||||
fputs("Usage: rgbgfx [-CfmuVZ] [-v [-v ...]] [-a <attr_map> | -A] [-b base_ids]\n"
|
fputs("Usage: rgbgfx [-r] [-CfmuVZ] [-v [-v ...]] [-a <attr_map> | -A] [-b base_ids]\n"
|
||||||
" [-c color_spec] [-d <depth>] [-L slice] [-N nb_tiles] [-n nb_pals]\n"
|
" [-c color_spec] [-d <depth>] [-L slice] [-N nb_tiles] [-n nb_pals]\n"
|
||||||
" [-o <out_file>] [-p <pal_file> | -P] [-s nb_colors] [-t <tile_map> | -T]\n"
|
" [-o <out_file>] [-p <pal_file> | -P] [-s nb_colors] [-t <tile_map> | -T]\n"
|
||||||
" [-U unit_size] [-x <tiles>] <file>\n"
|
" [-U unit_size] [-x <tiles>] <file>\n"
|
||||||
@@ -430,6 +432,14 @@ static char *parseArgv(int argc, char **argv, bool &autoAttrmap, bool &autoTilem
|
|||||||
break;
|
break;
|
||||||
case 'n':
|
case 'n':
|
||||||
options.nbPalettes = parseNumber(arg, "Number of palettes", 8);
|
options.nbPalettes = parseNumber(arg, "Number of palettes", 8);
|
||||||
|
if (*arg != '\0') {
|
||||||
|
error("Number of palettes (-n) must be a valid number, not \"%s\"", musl_optarg);
|
||||||
|
}
|
||||||
|
if (options.nbPalettes > 8) {
|
||||||
|
error("Number of palettes (-n) must not exceed 8!");
|
||||||
|
} else if (options.nbPalettes == 0) {
|
||||||
|
error("Number of palettes (-n) may not be 0!");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'o':
|
case 'o':
|
||||||
options.output = musl_optarg;
|
options.output = musl_optarg;
|
||||||
@@ -441,15 +451,24 @@ static char *parseArgv(int argc, char **argv, bool &autoAttrmap, bool &autoTilem
|
|||||||
autoPalettes = false;
|
autoPalettes = false;
|
||||||
options.palettes = musl_optarg;
|
options.palettes = musl_optarg;
|
||||||
break;
|
break;
|
||||||
|
case 'r':
|
||||||
|
options.reversedWidth = parseNumber(arg, "Reversed image stride");
|
||||||
|
if (*arg != '\0') {
|
||||||
|
error("Reversed image stride (-r) must be a valid number, not \"%s\"", musl_optarg);
|
||||||
|
}
|
||||||
|
if (options.reversedWidth == 0) {
|
||||||
|
error("Reversed image stride (-r) may not be 0!");
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 's':
|
case 's':
|
||||||
options.nbColorsPerPal = parseNumber(arg, "Number of colors per palette", 4);
|
options.nbColorsPerPal = parseNumber(arg, "Number of colors per palette", 4);
|
||||||
if (*arg != '\0') {
|
if (*arg != '\0') {
|
||||||
error("Palette size (-s) argument must be a valid number, not \"%s\"", musl_optarg);
|
error("Palette size (-s) must be a valid number, not \"%s\"", musl_optarg);
|
||||||
}
|
}
|
||||||
if (options.nbColorsPerPal > 4) {
|
if (options.nbColorsPerPal > 4) {
|
||||||
error("Palette size (-s) argument must not exceed 4!");
|
error("Palette size (-s) must not exceed 4!");
|
||||||
} else if (options.nbColorsPerPal == 0) {
|
} else if (options.nbColorsPerPal == 0) {
|
||||||
error("Palette size (-s) argument may not be 0!");
|
error("Palette size (-s) may not be 0!");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'T':
|
case 'T':
|
||||||
@@ -678,7 +697,11 @@ int main(int argc, char *argv[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.reverse()) {
|
||||||
|
reverse();
|
||||||
|
} else {
|
||||||
process();
|
process();
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -552,14 +552,6 @@ static void outputPalettes(std::vector<Palette> const &palettes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t flip(uint8_t byte) {
|
|
||||||
// To flip all the bits, we'll flip both nibbles, then each nibble half, etc.
|
|
||||||
byte = (byte & 0x0F) << 4 | (byte & 0xF0) >> 4;
|
|
||||||
byte = (byte & 0x33) << 2 | (byte & 0xCC) >> 2;
|
|
||||||
byte = (byte & 0x55) << 1 | (byte & 0xAA) >> 1;
|
|
||||||
return byte;
|
|
||||||
}
|
|
||||||
|
|
||||||
class TileData {
|
class TileData {
|
||||||
std::array<uint8_t, 16> _data;
|
std::array<uint8_t, 16> _data;
|
||||||
// The hash is a bit lax: it's the XOR of all lines, and every other nibble is identical
|
// The hash is a bit lax: it's the XOR of all lines, and every other nibble is identical
|
||||||
|
|||||||
301
src/gfx/reverse.cpp
Normal file
301
src/gfx/reverse.cpp
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of RGBDS.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "gfx/reverse.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <assert.h>
|
||||||
|
#include <cinttypes>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
|
#include <png.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "defaultinitalloc.hpp"
|
||||||
|
#include "helpers.h"
|
||||||
|
|
||||||
|
#include "gfx/main.hpp"
|
||||||
|
|
||||||
|
static DefaultInitVec<uint8_t> readInto(std::string path) {
|
||||||
|
std::filebuf file;
|
||||||
|
file.open(path, std::ios::in | std::ios::binary);
|
||||||
|
DefaultInitVec<uint8_t> data(128 * 16); // Begin with some room pre-allocated
|
||||||
|
|
||||||
|
size_t curSize = 0;
|
||||||
|
for (;;) {
|
||||||
|
size_t oldSize = curSize;
|
||||||
|
curSize = data.size();
|
||||||
|
|
||||||
|
// Fill the new area ([oldSize; curSize[) with bytes
|
||||||
|
size_t nbRead =
|
||||||
|
file.sgetn(reinterpret_cast<char *>(&data.data()[oldSize]), curSize - oldSize);
|
||||||
|
if (nbRead != curSize - oldSize) {
|
||||||
|
// Shrink the vector to discard bytes that weren't read
|
||||||
|
data.resize(oldSize + nbRead);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// If the vector has some capacity left, use it; otherwise, double the current size
|
||||||
|
|
||||||
|
// Arbitrary, but if you got a better idea...
|
||||||
|
size_t newSize = oldSize != data.capacity() ? data.capacity() : oldSize * 2;
|
||||||
|
assert(oldSize != newSize);
|
||||||
|
data.resize(newSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[noreturn]] static void pngError(png_structp png, char const *msg) {
|
||||||
|
fatal("Error writing reversed image (\"%s\"): %s",
|
||||||
|
static_cast<char const *>(png_get_error_ptr(png)), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void pngWarning(png_structp png, char const *msg) {
|
||||||
|
warning("While writing reversed image (\"%s\"): %s",
|
||||||
|
static_cast<char const *>(png_get_error_ptr(png)), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
void writePng(png_structp png, png_bytep data, size_t length) {
|
||||||
|
auto &pngFile = *static_cast<std::filebuf *>(png_get_io_ptr(png));
|
||||||
|
pngFile.sputn(reinterpret_cast<char *>(data), length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void flushPng(png_structp png) {
|
||||||
|
auto &pngFile = *static_cast<std::filebuf *>(png_get_io_ptr(png));
|
||||||
|
pngFile.pubsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reverse() {
|
||||||
|
options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
|
||||||
|
|
||||||
|
// Check for weird flag combinations
|
||||||
|
|
||||||
|
if (options.output.empty()) {
|
||||||
|
fatal("Tile data must be provided when reversing an image!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.allowDedup && options.tilemap.empty()) {
|
||||||
|
warning("Tile deduplication is enabled, but no tilemap is provided?");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.useColorCurve) {
|
||||||
|
warning("The color curve is not yet supported in reverse mode...");
|
||||||
|
}
|
||||||
|
|
||||||
|
options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
|
||||||
|
auto const tiles = readInto(options.output);
|
||||||
|
uint8_t tileSize = 8 * options.bitDepth;
|
||||||
|
if (tiles.size() % tileSize != 0) {
|
||||||
|
fatal("Tile data size must be a multiple of %" PRIu8 " bytes! (Read %zu)", tileSize,
|
||||||
|
tiles.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
// By default, assume tiles are not deduplicated, and add the (allegedly) trimmed tiles
|
||||||
|
size_t nbTileInstances = tiles.size() / tileSize + options.trim; // Image size in tiles
|
||||||
|
options.verbosePrint(Options::VERB_INTERM, "Read %zu tiles.\n", nbTileInstances);
|
||||||
|
std::optional<DefaultInitVec<uint8_t>> tilemap;
|
||||||
|
if (!options.tilemap.empty()) {
|
||||||
|
tilemap = readInto(options.tilemap);
|
||||||
|
nbTileInstances = tilemap->size();
|
||||||
|
|
||||||
|
// TODO: range check
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbTileInstances > options.maxNbTiles[0] + options.maxNbTiles[1]) {
|
||||||
|
warning("Read %zu tiles, more than the limit of %zu + %zu", nbTileInstances,
|
||||||
|
options.maxNbTiles[0], options.maxNbTiles[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nbTileInstances % options.reversedWidth) {
|
||||||
|
fatal("Image size (%zu tiles) is not divisible by the provided stride (%zu tiles), cannot "
|
||||||
|
"determine image dimensions",
|
||||||
|
nbTileInstances, options.reversedWidth);
|
||||||
|
}
|
||||||
|
size_t width, height;
|
||||||
|
size_t usefulWidth = options.reversedWidth - options.inputSlice[1] - options.inputSlice[3];
|
||||||
|
if (usefulWidth % 8 != 0) {
|
||||||
|
fatal(
|
||||||
|
"No input slice specified (`-L`), and specified image width (%zu) not a multiple of 8",
|
||||||
|
usefulWidth);
|
||||||
|
} else {
|
||||||
|
width = usefulWidth / 8;
|
||||||
|
if (nbTileInstances % width != 0) {
|
||||||
|
fatal("Total number of tiles read (%zu) cannot be divided by image width (%zu tiles)",
|
||||||
|
nbTileInstances, width);
|
||||||
|
}
|
||||||
|
height = nbTileInstances / width;
|
||||||
|
}
|
||||||
|
options.verbosePrint(Options::VERB_INTERM, "Reversed image dimensions: %zux%zu tiles\n", width,
|
||||||
|
height);
|
||||||
|
|
||||||
|
// TODO: -U
|
||||||
|
|
||||||
|
std::vector<std::array<Rgba, 4>> palettes{
|
||||||
|
{Rgba(0xffffffff), Rgba(0xaaaaaaff), Rgba(0x555555ff), Rgba(0x000000ff)}
|
||||||
|
};
|
||||||
|
if (!options.palettes.empty()) {
|
||||||
|
std::filebuf file;
|
||||||
|
file.open(options.palettes, std::ios::in | std::ios::binary);
|
||||||
|
|
||||||
|
palettes.clear();
|
||||||
|
std::array<uint8_t, sizeof(uint16_t) * 4> buf; // 4 colors
|
||||||
|
size_t nbRead;
|
||||||
|
do {
|
||||||
|
nbRead = file.sgetn(reinterpret_cast<char *>(buf.data()), buf.size());
|
||||||
|
if (nbRead == buf.size()) {
|
||||||
|
// Expand the colors
|
||||||
|
auto &palette = palettes.emplace_back();
|
||||||
|
std::generate(palette.begin(), palette.begin() + options.nbColorsPerPal,
|
||||||
|
[&buf, i = 0]() mutable {
|
||||||
|
i += 2;
|
||||||
|
return Rgba::fromCGBColor(buf[i - 2] + (buf[i - 1] << 8));
|
||||||
|
});
|
||||||
|
} else if (nbRead != 0) {
|
||||||
|
fatal("Palette data size (%zu) is not a multiple of %zu bytes!\n",
|
||||||
|
palettes.size() * buf.size() + nbRead, buf.size());
|
||||||
|
}
|
||||||
|
} while (nbRead != 0);
|
||||||
|
|
||||||
|
if (palettes.size() > options.nbPalettes) {
|
||||||
|
warning("Read %zu palettes, more than the specified limit of %zu", palettes.size(),
|
||||||
|
options.nbPalettes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<DefaultInitVec<uint8_t>> attrmap;
|
||||||
|
if (!options.attrmap.empty()) {
|
||||||
|
attrmap = readInto(options.attrmap);
|
||||||
|
if (attrmap->size() != nbTileInstances) {
|
||||||
|
fatal("Attribute map size (%zu tiles) doesn't match image's (%zu)", attrmap->size(),
|
||||||
|
nbTileInstances);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan through the attributes for inconsistencies
|
||||||
|
// We do this now for two reasons:
|
||||||
|
// 1. Checking those during the main loop is harmful to optimization, and
|
||||||
|
// 2. It clutters the code more, and it's not in great shape to begin with
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: palette map (overrides attributes)
|
||||||
|
|
||||||
|
options.verbosePrint(Options::VERB_LOG_ACT, "Writing image...\n");
|
||||||
|
std::filebuf pngFile;
|
||||||
|
pngFile.open(options.input, std::ios::out | std::ios::binary);
|
||||||
|
png_structp png = png_create_write_struct(
|
||||||
|
PNG_LIBPNG_VER_STRING,
|
||||||
|
const_cast<png_voidp>(static_cast<void const *>(options.input.c_str())), pngError,
|
||||||
|
pngWarning);
|
||||||
|
if (!png) {
|
||||||
|
fatal("Couldn't create PNG write struct: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
png_infop pngInfo = png_create_info_struct(png);
|
||||||
|
if (!pngInfo) {
|
||||||
|
fatal("Couldn't create PNG info struct: %s", strerror(errno));
|
||||||
|
}
|
||||||
|
png_set_write_fn(png, &pngFile, writePng, flushPng);
|
||||||
|
|
||||||
|
// TODO: if `-f` is passed, write the image indexed instead of RGB
|
||||||
|
png_set_IHDR(png, pngInfo, options.reversedWidth,
|
||||||
|
height * 8 + options.inputSlice[0] + options.inputSlice[2], 8,
|
||||||
|
PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
|
||||||
|
PNG_FILTER_TYPE_DEFAULT);
|
||||||
|
png_write_info(png, pngInfo);
|
||||||
|
|
||||||
|
png_color_8 sbitChunk;
|
||||||
|
sbitChunk.red = 5;
|
||||||
|
sbitChunk.green = 5;
|
||||||
|
sbitChunk.blue = 5;
|
||||||
|
sbitChunk.alpha = 1;
|
||||||
|
png_set_sBIT(png, pngInfo, &sbitChunk);
|
||||||
|
|
||||||
|
constexpr uint8_t SIZEOF_PIXEL = 4; // Each pixel is 4 bytes (RGBA @ 8 bits/component)
|
||||||
|
size_t const SIZEOF_ROW = options.reversedWidth * SIZEOF_PIXEL;
|
||||||
|
std::vector<uint8_t> tileRow(8 * SIZEOF_ROW, 0xFF); // Data for 8 rows of pixels
|
||||||
|
uint8_t * const rowPtrs[8] = {
|
||||||
|
&tileRow.data()[0 * SIZEOF_ROW + options.inputSlice[3]],
|
||||||
|
&tileRow.data()[1 * SIZEOF_ROW + options.inputSlice[3]],
|
||||||
|
&tileRow.data()[2 * SIZEOF_ROW + options.inputSlice[3]],
|
||||||
|
&tileRow.data()[3 * SIZEOF_ROW + options.inputSlice[3]],
|
||||||
|
&tileRow.data()[4 * SIZEOF_ROW + options.inputSlice[3]],
|
||||||
|
&tileRow.data()[5 * SIZEOF_ROW + options.inputSlice[3]],
|
||||||
|
&tileRow.data()[6 * SIZEOF_ROW + options.inputSlice[3]],
|
||||||
|
&tileRow.data()[7 * SIZEOF_ROW + options.inputSlice[3]],
|
||||||
|
};
|
||||||
|
|
||||||
|
auto const fillRows = [&png, &tileRow](size_t nbRows) {
|
||||||
|
for (size_t _ = 0; _ < nbRows; ++_) {
|
||||||
|
png_write_row(png, tileRow.data());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fillRows(options.inputSlice[0]);
|
||||||
|
|
||||||
|
for (size_t ty = 0; ty < height; ++ty) {
|
||||||
|
for (size_t tx = 0; tx < width; ++tx) {
|
||||||
|
size_t index = options.columnMajor ? ty + tx * width : ty * width + tx;
|
||||||
|
// Get the tile ID at this location
|
||||||
|
uint8_t gbcTileID = tilemap.has_value() ? (*tilemap)[index] : index;
|
||||||
|
// By default, a tile is unflipped, in bank 0, and uses palette #0
|
||||||
|
uint8_t attribute = attrmap.has_value() ? (*attrmap)[index] : 0x00;
|
||||||
|
bool bank = attribute & 0x08;
|
||||||
|
gbcTileID -= options.baseTileIDs[bank];
|
||||||
|
size_t tileID = gbcTileID + bank * options.maxNbTiles[0];
|
||||||
|
assert(tileID < nbTileInstances); // Should have been checked earlier
|
||||||
|
|
||||||
|
// We do not have data for tiles trimmed with `-x`, so assume they are "blank"
|
||||||
|
static std::array<uint8_t, 16> const trimmedTile{
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
};
|
||||||
|
uint8_t const *tileData = tileID > nbTileInstances - options.trim
|
||||||
|
? trimmedTile.data()
|
||||||
|
: &tiles[tileID * tileSize];
|
||||||
|
assert((attribute & 0b111) < palettes.size()); // Should be ensured on data read
|
||||||
|
auto const &palette = palettes[attribute & 0b111];
|
||||||
|
for (uint8_t y = 0; y < 8; ++y) {
|
||||||
|
// If vertically mirrored, fetch the bytes from the other end
|
||||||
|
uint8_t realY = attribute & 0x40 ? 7 - y : y;
|
||||||
|
uint8_t bitplane0 = tileData[realY * 2], bitplane1 = tileData[realY * 2 + 1];
|
||||||
|
if (attribute & 0x20) { // Handle horizontal flip
|
||||||
|
bitplane0 = flip(bitplane0);
|
||||||
|
bitplane1 = flip(bitplane1);
|
||||||
|
}
|
||||||
|
uint8_t *ptr = &rowPtrs[y][tx * 8 * SIZEOF_PIXEL];
|
||||||
|
for (uint8_t x = 0; x < 8; ++x) {
|
||||||
|
uint8_t bit0 = bitplane0 & 0x80, bit1 = bitplane1 & 0x80;
|
||||||
|
Rgba const &pixel = palette[bit0 >> 7 | bit1 >> 6];
|
||||||
|
*ptr++ = pixel.red;
|
||||||
|
*ptr++ = pixel.green;
|
||||||
|
*ptr++ = pixel.blue;
|
||||||
|
*ptr++ = pixel.alpha;
|
||||||
|
|
||||||
|
// Shift the pixel out
|
||||||
|
bitplane0 <<= 1;
|
||||||
|
bitplane1 <<= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// We never modify the pointers, and neither should libpng, despite the overly lax function
|
||||||
|
// signature.
|
||||||
|
// (AIUI, casting away const-ness is okay as long as you don't actually modify the
|
||||||
|
// pointed-to data)
|
||||||
|
png_write_rows(png, const_cast<png_bytepp>(rowPtrs), 8);
|
||||||
|
}
|
||||||
|
// Clear the first row again for the function
|
||||||
|
std::fill(tileRow.begin(), tileRow.begin() + SIZEOF_ROW, 0xFF);
|
||||||
|
fillRows(options.inputSlice[2]);
|
||||||
|
|
||||||
|
// Finalize the write
|
||||||
|
png_write_end(png, pngInfo);
|
||||||
|
|
||||||
|
png_destroy_write_struct(&png, &pngInfo);
|
||||||
|
pngFile.close();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user