Reimplement basic RGBGFX features in C++

Currently missing from the old version:
- `-f` ("fixing" the input image to be indexed)
- `-m` (the code for detecting mirrored tiles is missing, but all of the
        "plumbing" is otherwise there)
- `-C`
- `-d`
- `-x` (though I need to check the exact functionality the old one has)
- Also the man page is still a draft and needs to be fleshed out

More planned features are not implemented yet either:
- Explicit palette spec
- Better error messages, also error "images"
- Better 8x16 support, as well as other "dedup unit" sizes
- Support for arbitrary number of palettes & colors per palette
- Other output formats (for example, a "full" palette map for "streaming"
  use cases like gb-open-world)
- Quantization?

Some things may also be bugged:
- Transparency support
- Tile offsets (not exposed yet)
- Tile counts per bank (not exposed yet)

...and performance remains to be checked.
We need to set up some tests, honestly.
This commit is contained in:
ISSOtm
2022-02-06 23:30:37 +01:00
committed by Eldred Habert
parent 34bc650341
commit 8c62e80c18
23 changed files with 2022 additions and 1696 deletions

View File

@@ -10,7 +10,7 @@
cmake_minimum_required(VERSION 3.9 FATAL_ERROR)
project(rgbds
LANGUAGES C)
LANGUAGES C CXX)
# get real path of source and binary directories
get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH)
@@ -45,14 +45,14 @@ else()
# A non-zero optimization level is desired in debug mode, but allow overriding it nonetheless
# TODO: this overrides anything previously set... that's a bit sloppy!
set(CMAKE_C_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "-g -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" CACHE STRING "" FORCE)
endif()
if(MORE_WARNINGS)
add_compile_options(-Werror -Wextra
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond
-Wfloat-equal -Wlogical-op -Wnested-externs -Wnull-dereference
-Wold-style-definition -Wshift-overflow=2 -Wstrict-overflow=5
-Wstrict-prototypes -Wstringop-overflow=4 -Wundef -Wuninitialized -Wunused
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wshift-overflow=2
-Wstringop-overflow=4 -Wstrict-overflow=5 -Wundef -Wuninitialized -Wunused
-Wshadow # TODO: -Wshadow=compatible-local ?
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1
-Wno-format-nonliteral # We have a couple of "dynamic" prints

View File

@@ -7,7 +7,7 @@
#
.SUFFIXES:
.SUFFIXES: .h .y .c .o
.SUFFIXES: .h .y .c .cpp .o
# User-defined variables
@@ -34,10 +34,13 @@ WARNFLAGS := -Wall -pedantic
# Overridable CFLAGS
CFLAGS ?= -O3 -flto -DNDEBUG
CXXFLAGS ?= -O3 -flto -DNDEBUG
# Non-overridable CFLAGS
# _ISOC11_SOURCE is required on certain platforms to get C11 on top of the C99-based POSIX 2008
REALCFLAGS := ${CFLAGS} ${WARNFLAGS} -std=gnu11 -I include \
-D_POSIX_C_SOURCE=200809L -D_ISOC11_SOURCE
REALCXXFLAGS := ${CXXFLAGS} ${WARNFLAGS} -std=c++17 -I include \
-D_POSIX_C_SOURCE=200809L -fno-exceptions -fno-rtti
# Overridable LDFLAGS
LDFLAGS ?=
# Non-overridable LDFLAGS
@@ -102,9 +105,11 @@ rgbfix_obj := \
src/error.o
rgbgfx_obj := \
src/gfx/gb.o \
src/gfx/convert.o \
src/gfx/main.o \
src/gfx/makepng.o \
src/gfx/pal_packing.o \
src/gfx/pal_sorting.o \
src/gfx/proto_palette.o \
src/extern/getopt.o \
src/error.o
@@ -118,7 +123,7 @@ rgbfix: ${rgbfix_obj}
$Q${CC} ${REALLDFLAGS} -o $@ ${rgbfix_obj} ${REALCFLAGS} src/version.c
rgbgfx: ${rgbgfx_obj}
$Q${CC} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCFLAGS} src/version.c ${PNGLDLIBS}
$Q${CXX} ${REALLDFLAGS} ${PNGLDFLAGS} -o $@ ${rgbgfx_obj} ${REALCXXFLAGS} src/version.c ${PNGLDLIBS}
# Rules to process files
@@ -145,7 +150,10 @@ src/asm/parser.c: src/asm/parser.y
${BISON} $$DEFS -d ${YFLAGS} -o $@ $<
.c.o:
$Q${CC} ${REALCFLAGS} ${PNGCFLAGS} -c -o $@ $<
$Q${CC} ${REALCFLAGS} -c -o $@ $<
.cpp.o:
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
# Target used to remove all files generated by other Makefile targets
@@ -214,11 +222,9 @@ checkdiff:
develop:
$Qenv ${MAKE} WARNFLAGS="-Werror -Wextra \
-Walloc-zero -Wcast-align -Wcast-qual -Wduplicated-branches -Wduplicated-cond \
-Wfloat-equal -Wlogical-op -Wnested-externs -Wold-style-definition \
-Wshift-overflow=2 \
-Wstrict-overflow=5 -Wstrict-prototypes -Wundef -Wuninitialized -Wunused \
-Wfloat-equal -Wlogical-op -Wnull-dereference -Wshift-overflow=2 \
-Wstringop-overflow=4 -Wstrict-overflow=5 -Wundef -Wuninitialized -Wunused \
-Wshadow \
-Wnull-dereference -Wstringop-overflow=4 \
-Wformat=2 -Wformat-overflow=2 -Wformat-truncation=1 \
-Wno-format-nonliteral \
-Wno-type-limits -Wno-tautological-constant-out-of-range-compare \
@@ -229,7 +235,8 @@ develop:
-fsanitize=signed-integer-overflow -fsanitize=bounds \
-fsanitize=object-size -fsanitize=bool -fsanitize=enum \
-fsanitize=alignment -fsanitize=null -fsanitize=address" \
CFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls"
CFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls" \
CXXFLAGS="-ggdb3 -Og -fno-omit-frame-pointer -fno-optimize-sibling-calls"
# Targets for the project maintainer to easily create Windows exes.
# This is not for Windows users!

View File

@@ -129,3 +129,11 @@ The RGBDS source code file structure somewhat resembles the following:
- 2018, codebase relicensed under the MIT license.
- 2020, repository is moved to the `gbdev <https://github.com/gbdev>`__ organisation. The `rgbds.gbdev.io <https://rgbds.gbdev.io>`__ website serving documentation and downloads is created.
4. Acknowledgements
-------------------
RGBGFX generates palettes using algorithms found in the paper
`"Algorithms for the Pagination Problem, a Bin Packing with Overlapping Items" <http://arxiv.org/abs/1605.00558>`__
(`GitHub <https://github.com/pagination-problem/pagination>`__, MIT license),
by Aristide Grange, Imed Kacem, and Sébastien Martin.

View File

@@ -0,0 +1,38 @@
/**
* Allocator adaptor that interposes construct() calls to convert value-initialization
* (which is what you get with e.g. `vector::resize`) into default-initialization (which does not
* zero out non-class types).
* From https://stackoverflow.com/questions/21028299/is-this-behavior-of-vectorresizesize-type-n-under-c11-and-boost-container/21028912#21028912
*/
#ifndef DEFAULT_INIT_ALLOC_H
#define DEFAULT_INIT_ALLOC_H
#include <memory>
#include <vector>
template<typename T, typename A = std::allocator<T>>
class default_init_allocator : public A {
using a_t = std::allocator_traits<A>;
public:
template<typename U>
struct rebind {
using other = default_init_allocator<U, typename a_t::template rebind_alloc<U>>;
};
using A::A; // Inherit the allocator's constructors
template<typename U>
void construct(U * ptr) noexcept(std::is_nothrow_default_constructible_v<U>) {
::new(static_cast<void *>(ptr)) U;
}
template<typename U, typename... Args>
void construct(U * ptr, Args && ... args) {
a_t::construct(static_cast<A &>(*this), ptr, std::forward<Args>(args)...);
}
};
template<typename T>
using DefaultInitVec = std::vector<T, default_init_allocator<T>>;
#endif

View File

@@ -12,10 +12,18 @@
#include "helpers.h"
#include "platform.h"
#ifdef __cplusplus
extern "C" {
#endif
void warn(char const NONNULL(fmt), ...) format_(printf, 1, 2);
void warnx(char const NONNULL(fmt), ...) format_(printf, 1, 2);
_Noreturn void err(char const NONNULL(fmt), ...) format_(printf, 1, 2);
_Noreturn void errx(char const NONNULL(fmt), ...) format_(printf, 1, 2);
#ifdef __cplusplus
}
#endif
#endif /* RGBDS_ERROR_H */

View File

@@ -26,6 +26,10 @@
#ifndef RGBDS_EXTERN_GETOPT_H
#define RGBDS_EXTERN_GETOPT_H
#ifdef __cplusplus
extern "C" {
#endif
extern char *musl_optarg;
extern int musl_optind, musl_opterr, musl_optopt, musl_optreset;
@@ -43,4 +47,8 @@ int musl_getopt_long_only(int argc, char **argv, char const *optstring,
#define required_argument 1
#define optional_argument 2
#ifdef __cplusplus
} // extern "C"
#endif
#endif

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

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

View File

@@ -1,91 +0,0 @@
/*
* 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 */

59
include/gfx/main.hpp Normal file
View File

@@ -0,0 +1,59 @@
/*
* 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 <filesystem>
#include <stdint.h>
#include "helpers.h"
struct Options {
bool beVerbose = false; // -v
bool fixInput = false; // -f
bool columnMajor = false; // -h; whether to output the tilemap in columns instead of rows
bool allowMirroring = false; // -m
bool allowDedup = false; // -u
bool useColorCurve = false; // -C
uint8_t bitDepth = 2; // -d
uint64_t trim = 0; // -x
uint8_t nbPalettes = 8; // TODO
uint8_t nbColorsPerPal = 0; // TODO; 0 means "auto" = 1 << bitDepth;
std::array<uint8_t, 2> baseTileIDs{0, 0}; // TODO
std::array<uint16_t, 2> maxNbTiles{384, 0}; // TODO
std::filesystem::path tilemap{}; // -t, -T
std::filesystem::path attrmap{}; // -a, -A
std::filesystem::path palettes{}; // -p, -P
std::filesystem::path output{}; // -o
std::filesystem::path input{}; // positional arg
format_(printf, 2, 3) void verbosePrint(char const *fmt, ...) const;
};
extern Options options;
void warning(char const *fmt, ...);
void error(char const *fmt, ...);
[[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;
decltype(colors)::iterator begin();
decltype(colors)::iterator end();
decltype(colors)::const_iterator begin() const;
decltype(colors)::const_iterator end() const;
};
#endif /* RGBDS_GFX_MAIN_HPP */

View File

@@ -0,0 +1,32 @@
/*
* 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"
class 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 */

View File

@@ -0,0 +1,26 @@
/*
* 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 <png.h>
#include <vector>
class Palette;
namespace sorting {
void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
png_byte *palAlpha);
void grayscale(std::vector<Palette> &palettes);
void rgb(std::vector<Palette> &palettes);
}
#endif /* RGBDS_GFX_PAL_SORTING_HPP */

View File

@@ -0,0 +1,44 @@
/*
* 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 {
// Up to 4 colors, sorted, and where SIZE_MAX means the slot is empty
// (OK because it's not a valid color index)
std::array<uint16_t, 4> _colorIndices{UINT16_MAX, UINT16_MAX, UINT16_MAX, UINT16_MAX};
public:
/**
* Adds the specified color to the set
* Returns false if the set is full
*/
bool add(uint16_t color);
enum ComparisonResult {
NEITHER,
WE_BIGGER,
THEY_BIGGER = -1,
};
ComparisonResult compare(ProtoPalette const &other) const;
ProtoPalette &operator=(ProtoPalette const &other);
size_t size() const;
decltype(_colorIndices)::const_iterator begin() const;
decltype(_colorIndices)::const_iterator end() const;
};
#endif /* RGBDS_GFX_PROTO_PALETTE_HPP */

View File

@@ -9,10 +9,18 @@
#ifndef EXTERN_VERSION_H
#define EXTERN_VERSION_H
#ifdef __cplusplus
extern "C" {
#endif
#define PACKAGE_VERSION_MAJOR 0
#define PACKAGE_VERSION_MINOR 5
#define PACKAGE_VERSION_PATCH 2
char const *get_package_version_string(void);
#ifdef __cplusplus
}
#endif
#endif /* EXTERN_VERSION_H */

View File

@@ -25,23 +25,7 @@
.Sh DESCRIPTION
The
.Nm
program converts PNG images into the Nintendo Game Boy's planar tile format.
.Pp
The resulting colors and their palette indices are determined differently depending on the input PNG file:
.Bl -dash -width Ds
.It
If the file has an embedded palette, that palette's color and order are used.
.It
If not, and the image only contains shades of gray, rgbgfx maps them to the indices appropriate for each shade.
Any undetermined indices are set to respective default shades of gray.
For example: if the bit depth is 2 and the image contains light gray and black, they become the second and fourth colors, and the first and third colors get set to default white and dark gray.
If the image has multiple shades that map to the same index, the palette is instead determined as if the image had color.
.It
If the image has color (or the grayscale method failed), the colors are sorted from lightest to darkest.
.El
.Pp
The input image may not contain more colors than the selected bit depth allows.
Transparent pixels are set to palette index 0.
program converts PNG images into data suitable
.Sh ARGUMENTS
Note that options can be abbreviated as long as the abbreviation is unambiguous:
.Fl Fl verb
@@ -117,29 +101,59 @@ Print the version of the program and exit.
.It Fl v , Fl Fl verbose
Verbose.
Print errors when the command line parameters and the parameters in the PNG file don't match.
.It Fl x Ar tiles , Fl Fl trim-end Ar tiles
.It Fl x Ar amount , Fl Fl trim-end Ar amount
Trim the end of the output file by this many tiles.
.Pq TODO: tiles, or tilemap?
.El
.Sh PALETTE GENERATION
.Nm
must decide in which order to place colors in the palettes, but the input PNG usually lacks any indications.
A lot of the time, the order does not matter; however, sometimes it does, and
.Nm
attempts to account for those cases.
.Bl -bullet -offset indent
.It
First, if the image contains
.Em any
transparent pixel, color #0 of
.Em all
palettes will be allocated to it.
If palettes were explicitly specified using
.Fl TODO ,
then they will specify color #1 onwards.
.Pq If you do not want this, ask your image editor to remove the alpha channel.
.It
If the PNG file internally contains a palette (often dubbed an
.Dq indexed
PNG), then colors in each output palette will be sorted according to their order in the PNG's palette.
Any unused entries will be ignored, and only the first entry is considered if there any duplicates.
.Pq If you want a given color to appear more than once in a palette, you should specify the palettes explicitly instead using Fl TODO .
.It
Otherwise, if the PNG only contains shades of gray, they will be categorized into 4
.Dq bins
.Pq white, light gray, dark gray, and black in this order ,
and the palette is set to these four bins.
(TODO: how does this interact with 1bpp? With more than 1 palette?)
If more than one grey ends up in the same bin, the RGB method below is used instead.
.It
If none of the above apply, colors are sorted from lightest to darkest.
(This is what the old documentation claimed, but the definition of luminance that was used wasn't quite right.
It is kept for compatibility.)
.El
.Pp
Note that the
.Dq indexed
behavior depends on an internal detail of how the PNG is saved.
Since few image editors (such as GIMP) expose that detail, this behavior is only kept for compatibility and should be considered deprecated; if the order of colors in the palettes is important to you, please use
.Fl TODO
to specify the palette explicitly.
.Sh OUTPUT FILES
.Ss Palette data
Palette data is output like a dump of GBC palette memory: the output is a binary file.
Each color is written as GBC-native little-endian RGB555 (that is, the first byte contains the red and the lower 3 bits of green).
There is no padding between colors, nor between palettes; however, empty colors in the palettes are filled as 0xFF bytes.
.Sh EXAMPLES
The following will take a PNG file with a bit depth of 1, 2, or 8, and output planar 2bpp data:
.Pp
.D1 $ rgbgfx -o out.2bpp in.png
.Pp
The following creates a planar 2bpp file with only unique tiles, and its tilemap
.Pa out.tilemap :
.Pp
.D1 $ rgbgfx -T -u -o out.2bpp in.png
.Pp
The following creates a planar 2bpp file with only unique tiles
.Pa accounting for tile mirroring
and its associated tilemap
.Pa out.tilemap
and attrmap
.Pa out.attrmap :
.Pp
.D1 $ rgbgfx -A -T -m -o out.2bpp in.png
.Pp
The following will do nothing:
The following will only validate the PNG [...] but output nothing:
.Pp
.D1 $ rgbgfx in.png
.Sh BUGS
@@ -153,8 +167,10 @@ Please report bugs on
.Xr gbz80 7
.Sh HISTORY
.Nm
was created by
was originally created by
.An stag019
to be included in RGBDS.
It is now maintained by a number of contributors at
It was later rewritten by
.An ISSOtm ,
and is now maintained by a number of contributors at
.Lk https://github.com/gbdev/rgbds .

View File

@@ -70,9 +70,13 @@ set(rgbfix_src
)
set(rgbgfx_src
"gfx/gb.c"
"gfx/main.c"
"gfx/makepng.c"
"gfx/convert.cpp"
"gfx/main.cpp"
"gfx/pal_packing.cpp"
"gfx/pal_sorting.cpp"
"gfx/proto_palette.cpp"
"extern/getopt.c"
"error.c"
)
set(rgblink_src
@@ -97,6 +101,8 @@ foreach(PROG "asm" "fix" "gfx" "link")
install(TARGETS rgb${PROG} RUNTIME DESTINATION bin)
endforeach()
set_target_properties(rgbgfx PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED True)
if(LIBPNG_FOUND) # pkg-config
target_include_directories(rgbgfx PRIVATE ${LIBPNG_INCLUDE_DIRS})
target_link_directories(rgbgfx PRIVATE ${LIBPNG_LIBRARY_DIRS})

869
src/gfx/convert.cpp Normal file
View File

@@ -0,0 +1,869 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include "gfx/convert.hpp"
#include <algorithm>
#include <assert.h>
#include <errno.h>
#include <fstream>
#include <inttypes.h>
#include <memory>
#include <optional>
#include <png.h>
#include <setjmp.h>
#include <string.h>
#include <tuple>
#include <unordered_set>
#include <utility>
#include <vector>
#include "defaultinitalloc.hpp"
#include "helpers.h"
#include "gfx/main.hpp"
#include "gfx/pal_packing.hpp"
#include "gfx/pal_sorting.hpp"
#include "gfx/proto_palette.hpp"
struct Rgba {
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t alpha;
Rgba(uint8_t r, uint8_t g, uint8_t b, uint8_t a) : red(r), green(g), blue(b), alpha(a) {}
Rgba(uint32_t rgba) : red(rgba), green(rgba >> 8), blue(rgba >> 16), alpha(rgba >> 24) {}
operator uint32_t() const {
auto shl = [](uint8_t val, unsigned shift) { return static_cast<uint32_t>(val) << shift; };
return shl(red, 0) | shl(green, 8) | shl(blue, 16) | shl(alpha, 24);
}
bool operator!=(Rgba const &other) const {
return static_cast<uint32_t>(*this) != static_cast<uint32_t>(other);
}
bool isGray() const { return red == green && green == blue; }
/**
* 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 = 5; // TODO: adjust this
/**
* Computes the equivalent CGB color, respects the color curve depending on options
*/
uint16_t cgbColor() const {
if (alpha < transparency_threshold)
return transparent;
if (options.useColorCurve) {
assert(!"TODO");
} else {
return (red >> 3) | (green >> 3) << 5 | (blue >> 3) << 10;
}
}
};
class ImagePalette {
// Use as many slots as there are CGB colors (plus transparency)
std::array<std::optional<Rgba>, 0x8001> _colors;
public:
ImagePalette() = default;
void registerColor(Rgba const &rgba) {
decltype(_colors)::value_type &slot = _colors[rgba.cgbColor()];
if (!slot.has_value()) {
slot.emplace(rgba);
} else if (*slot != rgba) {
warning("Different colors melded together"); // TODO: indicate position
}
}
size_t size() const { return _colors.size(); }
auto begin() const { return _colors.begin(); }
auto end() const { return _colors.end(); }
};
class Png {
std::filesystem::path const &path;
std::filebuf file{};
png_structp png = nullptr;
png_infop info = nullptr;
// These are cached for speed
uint32_t width, height;
DefaultInitVec<uint16_t> pixels;
ImagePalette colors;
int colorType;
int nbColors;
png_colorp embeddedPal = nullptr;
png_bytep transparencyPal = nullptr;
bool isGrayOnly = true;
[[noreturn]] static void handleError(png_structp png, char const *msg) {
struct Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
fatal("Error reading input image (\"%s\"): %s", self->path.c_str(), msg);
}
static void handleWarning(png_structp png, char const *msg) {
struct Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
warning("In input image (\"%s\"): %s", self->path.c_str(), msg);
}
static void readData(png_structp png, png_bytep data, size_t length) {
struct 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->path.c_str(), 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; }
uint16_t &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; }
uint16_t const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; }
bool hasNonGray() const { return !isGrayOnly; }
/**
* 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::filesystem::path 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", path.c_str(), strerror(errno));
}
options.verbosePrint("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!", path.c_str());
}
options.verbosePrint("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 (width % 8 != 0)
fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", width);
if (height % 8 != 0)
fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", height);
// TODO: use an allocator that doesn't zero on init to save potentially a lot of perf
// https://stackoverflow.com/questions/21028299/is-this-behavior-of-vectorresizesize-type-n-under-c11-and-boost-container/21028912#21028912
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("Input image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n", height,
width, 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("Embedded palette has %d colors: [", nbColors);
for (int i = 0; i < nbColors; ++i) {
auto const &color = embeddedPal[i];
options.verbosePrint("#%02x%02x%02x%s", color.red, color.green, color.blue,
i != nbColors - 1 ? ", " : "]\n");
}
} else {
options.verbosePrint("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 we read a tRNS chunk, convert it to alpha
if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png);
// Otherwise, if we lack an alpha channel, default to full opacity
else if (!(colorType & PNG_COLOR_MASK_ALPHA))
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);
// Set interlace handling (MUST be done before `png_read_update_info`)
int nbPasses = png_set_interlace_handling(png);
// 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
std::vector<png_byte> row(png_get_rowbytes(png, info));
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) {
Rgba rgba(row[x * 4], row[x * 4 + 1], row[x * 4 + 2], row[x * 4 + 3]);
colors.registerColor(rgba);
pixel(x, y) = rgba.cgbColor();
isGrayOnly &= rgba.isGray();
}
}
} else {
// For interlace to work properly, we must read the image `nbPasses` times
for (int pass = 0; pass < nbPasses; ++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) {
Rgba rgba(ptr[0], ptr[1], ptr[2], ptr[3]);
colors.registerColor(rgba);
pixel(x, y) = rgba.cgbColor();
isGrayOnly &= rgba.isGray();
ptr += 4;
}
}
}
}
png_read_end(png,
nullptr); // We don't care about chunks after the image data (comments, etc.)
}
~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;
uint32_t const _x, _y;
public:
Tile(Png const &png, uint32_t x, uint32_t y) : _png(png), _x(x), _y(y) {}
uint16_t 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;
public:
std::pair<uint32_t, uint32_t> coords() const { return {x, y}; }
Tile operator*() const { return {parent._png, x, y}; }
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;
}
bool operator!=(iterator const &rhs) const {
return coords() != rhs.coords(); // Compare the returned coord pairs
}
};
public:
iterator begin() const { return {*this, _width, 0, 0}; }
iterator end() const {
iterator it{*this, _width, _width - 8, _height - 8}; // Last valid one
return ++it; // Now one-past-last
}
};
public:
TilesVisitor visitAsTiles(bool columnMajor) const {
return {*this, columnMajor, width, height};
}
};
class RawTiles {
public:
/**
* 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 {
size_t protoPaletteID;
uint16_t tileID;
bool yFlip;
bool xFlip;
};
static void outputPalettes(std::vector<Palette> const &palettes) {
std::filebuf output;
output.open(options.palettes, std::ios_base::out | std::ios_base::binary);
for (Palette const &palette : palettes) {
for (uint16_t color : palette) {
output.sputc(color & 0xFF);
output.sputc(color >> 8);
}
}
}
namespace unoptimized {
// TODO: this is very redundant with `TileData`; try merging both?
static void outputTileData(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
std::vector<Palette> const &palettes,
DefaultInitVec<size_t> const &mappings) {
std::filebuf output;
output.open(options.tilemap, std::ios_base::out | std::ios_base::binary);
auto iter = attrmap.begin();
for (auto tile : png.visitAsTiles(options.columnMajor)) {
Palette const &palette = palettes[mappings[iter->protoPaletteID]];
for (uint32_t y = 0; y < 8; ++y) {
uint16_t row = 0;
for (uint32_t x = 0; x < 8; ++x) {
row <<= 1;
uint8_t index = palette.indexOf(tile.pixel(x, y));
if (index & 1) {
row |= 0x001;
}
if (index & 2) {
row |= 0x100;
}
}
output.sputc(row & 0xFF);
output.sputc(row >> 8);
}
++iter;
}
assert(iter == attrmap.end());
}
static void outputMaps(Png const &png, DefaultInitVec<AttrmapEntry> const &attrmap,
DefaultInitVec<size_t> const &mappings) {
std::optional<std::filebuf> tilemapOutput, attrmapOutput;
if (!options.tilemap.empty()) {
tilemapOutput.emplace();
tilemapOutput->open(options.tilemap, std::ios_base::out | std::ios_base::binary);
}
if (!options.attrmap.empty()) {
attrmapOutput.emplace();
attrmapOutput->open(options.attrmap, std::ios_base::out | std::ios_base::binary);
}
uint8_t tileID = 0;
uint8_t bank = 0;
auto iter = attrmap.begin();
for ([[maybe_unused]] auto tile : png.visitAsTiles(options.columnMajor)) {
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 = mappings[iter->protoPaletteID] & 7;
attrmapOutput->sputc(palID | bank << 3); // The other flags are all 0
++iter;
}
++tileID;
}
}
} // namespace unoptimized
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 {
// TODO: might want to switch to `std::byte` instead?
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:
mutable size_t tileID;
TileData(Png::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) {
for (uint32_t y = 0; y < 8; ++y) {
uint16_t bitplanes = 0;
for (uint32_t x = 0; x < 8; ++x) {
bitplanes <<= 1;
uint8_t index = palette.indexOf(tile.pixel(x, y));
if (index & 1) {
bitplanes |= 1;
}
if (index & 2) {
bitplanes |= 0x100;
}
}
_data[y * 2] = bitplanes & 0xFF;
_data[y * 2 + 1] = 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 ^= flip(bitplanes >> 8) << 8 | flip(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 {
if (std::equal(_data.begin(), _data.end(), other._data.begin()))
return MatchType::EXACT;
if (options.allowMirroring) {
// TODO
}
return MatchType::NOPE;
}
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 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 = 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(); }
};
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;
auto iter = attrmap.begin();
for (auto tile : png.visitAsTiles(options.columnMajor)) {
auto [tileID, matchType] = tiles.addTile(tile, palettes[mappings[iter->protoPaletteID]]);
iter->xFlip = matchType == TileData::HFLIP || matchType == TileData::VHFLIP;
iter->yFlip = matchType == TileData::VFLIP || matchType == TileData::VHFLIP;
assert(tileID < 1 << 10);
iter->tileID = tileID;
++iter;
}
assert(iter == attrmap.end());
return tiles; // Copy elision should prevent the contained `unordered_set` from being
// re-constructed
}
static void outputTileData(UniqueTiles const &tiles) {
std::filebuf output;
output.open(options.output, std::ios_base::out | std::ios_base::binary);
uint16_t tileID = 0;
for (TileData const *tile : tiles) {
assert(tile->tileID == tileID);
++tileID;
output.sputn(reinterpret_cast<char const *>(tile->data().data()), tile->data().size());
}
}
static void outputTilemap(DefaultInitVec<AttrmapEntry> const &attrmap) {
std::filebuf output;
output.open(options.tilemap, std::ios_base::out | std::ios_base::binary);
assert(options.baseTileIDs[0] == 0 && options.baseTileIDs[1] == 0); // TODO: deal with offset
for (AttrmapEntry const &entry : attrmap) {
output.sputc(entry.tileID & 0xFF);
}
}
static void outputAttrmap(DefaultInitVec<AttrmapEntry> const &attrmap,
DefaultInitVec<size_t> const &mappings) {
std::filebuf output;
output.open(options.attrmap, std::ios_base::out | std::ios_base::binary);
assert(options.baseTileIDs[0] == 0 && options.baseTileIDs[1] == 0); // TODO: deal with offset
for (AttrmapEntry const &entry : attrmap) {
uint8_t attr = entry.xFlip << 5 | entry.yFlip << 6;
attr |= (entry.tileID >= options.maxNbTiles[0]) << 3;
attr |= mappings[entry.protoPaletteID] & 7;
output.sputc(attr);
}
}
} // namespace optimized
void process() {
options.verbosePrint("Using libpng %s\n", png_get_libpng_ver(nullptr));
options.verbosePrint("Reading tiles...\n");
Png png(options.input);
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.beVerbose) {
options.verbosePrint("Image colors: [ ");
size_t i = 0;
for (auto const &slot : colors) {
if (!slot.has_value()) {
continue;
}
options.verbosePrint("#%02x%02x%02x%02x%s", slot->red, slot->green, slot->blue,
slot->alpha, i != colors.size() - 1 ? ", " : "");
++i;
}
options.verbosePrint("]\n");
}
// 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(options.columnMajor)) {
ProtoPalette tileColors;
AttrmapEntry &attrs = attrmap.emplace_back();
for (uint32_t y = 0; y < 8; ++y) {
for (uint32_t x = 0; x < 8; ++x) {
tileColors.add(tile.pixel(x, y));
}
}
// Insert the palette, making sure to avoid overlaps
// TODO: if inserting (0, 1), (0, 2), and then (0, 1, 2), we might have a problem!
for (size_t i = 0; i < protoPalettes.size(); ++i) {
switch (tileColors.compare(protoPalettes[i])) {
case ProtoPalette::WE_BIGGER:
protoPalettes[i] = tileColors; // Override them
[[fallthrough]];
case ProtoPalette::THEY_BIGGER:
// Do nothing, they already contain us
attrs.protoPaletteID = i;
goto contained;
case ProtoPalette::NEITHER:
break; // Keep going
}
}
attrs.protoPaletteID = protoPalettes.size();
protoPalettes.push_back(tileColors);
contained:;
}
options.verbosePrint("Image contains %zu proto-palette%s\n", protoPalettes.size(),
protoPalettes.size() != 1 ? "s" : "");
// Sort the proto-palettes by size, which improves the packing algorithm's efficiency
// TODO: try keeping the palettes stored while inserting them instead, might perform better
std::sort(
protoPalettes.begin(), protoPalettes.end(),
[](ProtoPalette const &lhs, ProtoPalette const &rhs) { return lhs.size() < rhs.size(); });
// 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.beVerbose) {
options.verbosePrint("Proto-palette mappings: (%zu palettes)\n", nbPalettes);
for (size_t i = 0; i < mappings.size(); ++i) {
options.verbosePrint("%zu -> %zu\n", i, mappings[i]);
}
}
// TODO: optionally, "decant" the result
// Generate the actual palettes from the mappings
std::vector<Palette> palettes(nbPalettes);
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 [palSize, palRGB, palAlpha] = png.getEmbeddedPal();
if (palRGB) {
sorting::indexed(palettes, palSize, palRGB, palAlpha);
} else if (png.hasNonGray()) {
sorting::rgb(palettes);
} else {
sorting::grayscale(palettes);
}
if (options.beVerbose) {
for (auto &&palette : palettes) {
options.verbosePrint("{ ");
for (uint16_t colorIndex : palette) {
options.verbosePrint("%04" PRIx16 ", ", colorIndex);
}
options.verbosePrint("}\n");
}
}
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("Generating unoptimized tile data...\n");
unoptimized::outputTileData(png, attrmap, palettes, mappings);
}
if (!options.tilemap.empty() || !options.attrmap.empty()) {
options.verbosePrint("Generating unoptimized tilemap and/or attrmap...\n");
unoptimized::outputMaps(png, attrmap, mappings);
}
} else {
// All of these require the deduplication process to be performed to be output
options.verbosePrint("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("Generating optimized tile data...\n");
optimized::outputTileData(tiles);
}
if (!options.tilemap.empty()) {
options.verbosePrint("Generating optimized tilemap...\n");
optimized::outputTilemap(attrmap);
}
if (!options.attrmap.empty()) {
options.verbosePrint("Generating optimized attrmap...\n");
optimized::outputAttrmap(attrmap, mappings);
}
}
}

View File

@@ -1,385 +0,0 @@
/*
* 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);
}

View File

@@ -1,358 +0,0 @@
/*
* 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;
}

321
src/gfx/main.cpp Normal file
View File

@@ -0,0 +1,321 @@
/*
* 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 <charconv>
#include <filesystem>
#include <inttypes.h>
#include <limits>
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "extern/getopt.h"
#include "version.h"
#include "gfx/convert.hpp"
Options options;
static uintmax_t nbErrors;
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++;
fprintf(stderr, "Conversion aborted after %ju error%s\n", nbErrors, nbErrors == 1 ? "" : "s");
exit(1);
}
void Options::verbosePrint(char const *fmt, ...) const {
if (beVerbose) {
va_list ap;
va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
}
// 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);
}
void fputsv(std::string_view const &view, FILE *f) {
if (view.data()) {
fwrite(view.data(), sizeof(char), view.size(), f);
}
}
int main(int argc, char *argv[]) {
int opt;
bool autoAttrmap = false, autoTilemap = false, autoPalettes = false;
auto parseDecimalArg = [&opt](auto &out) {
char const *end = &musl_optarg[strlen(musl_optarg)];
// `options.bitDepth` is not modified if the parse fails entirely
auto result = std::from_chars(musl_optarg, end, out, 10);
if (result.ec == std::errc::result_out_of_range) {
error("Argument to option '%c' (\"%s\") is out of range", opt, musl_optarg);
return false;
} else if (result.ptr != end) {
error("Invalid argument to option '%c' (\"%s\")", opt, musl_optarg);
return false;
}
return true;
};
while ((opt = musl_getopt_long_only(argc, argv, optstring, longopts, nullptr)) != -1) {
switch (opt) {
case 'A':
autoAttrmap = true;
break;
case 'a':
autoAttrmap = false;
options.attrmap = musl_optarg;
break;
case 'C':
options.useColorCurve = true;
break;
case 'd':
if (parseDecimalArg(options.bitDepth) && options.bitDepth != 1
&& options.bitDepth != 2) {
error("Bit depth must be 1 or 2, not %" PRIu8 "");
options.bitDepth = 2;
}
break;
case 'f':
options.fixInput = true;
break;
case 'h':
warning("`-h` is deprecated, use `-???` instead");
[[fallthrough]];
case '?': // TODO
options.columnMajor = true;
break;
case 'm':
options.allowMirroring = true;
[[fallthrough]]; // Imply `-u`
case 'u':
options.allowDedup = true;
break;
case 'o':
options.output = musl_optarg;
break;
case 'P':
autoPalettes = true;
break;
case 'p':
autoPalettes = false;
options.palettes = musl_optarg;
break;
case 'T':
autoTilemap = true;
break;
case 't':
autoTilemap = false;
options.tilemap = musl_optarg;
break;
case 'V':
printf("rgbgfx %s\n", get_package_version_string());
exit(0);
case 'v':
options.beVerbose = true;
break;
case 'x':
parseDecimalArg(options.trim);
break;
case 'D':
case 'F':
warning("Ignoring option '%c'", musl_optopt);
break;
default:
print_usage();
exit(1);
}
}
if (options.nbColorsPerPal == 0) {
options.nbColorsPerPal = 1 << 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);
}
if (musl_optind == argc) {
fputs("FATAL: No input image specified\n", stderr);
print_usage();
exit(1);
} else if (argc - musl_optind != 1) {
fprintf(stderr, "FATAL: %d input images were specified instead of 1\n", argc - musl_optind);
print_usage();
exit(1);
}
options.input = argv[argc - 1];
auto autoOutPath = [](bool autoOptEnabled, std::filesystem::path &path, char const *extension) {
if (autoOptEnabled) {
path = options.input;
path.replace_extension(extension);
}
};
autoOutPath(autoAttrmap, options.attrmap, ".attrmap");
autoOutPath(autoTilemap, options.tilemap, ".tilemap");
autoOutPath(autoPalettes, options.palettes, ".pal");
if (options.beVerbose) {
fprintf(stderr, "rgbgfx %s\n", get_package_version_string());
fputs("Options:\n", stderr);
if (options.fixInput)
fputs("\tConvert input to indexed\n", stderr);
if (options.columnMajor)
fputs("\tOutput {tile,attr}map 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);
fprintf(stderr, "\tTrim the last %" PRIu64 " tiles\n", options.trim);
fprintf(stderr, "\tBase tile IDs: [%" PRIu8 ", %" PRIu8 "]\n", options.baseTileIDs[0],
options.baseTileIDs[1]);
fprintf(stderr, "\tMax number of tiles: [%" PRIu16 ", %" PRIu16 "]\n",
options.maxNbTiles[0], options.maxNbTiles[1]);
auto printPath = [](char const *name, std::filesystem::path 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);
}
process();
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;
}
}
}
uint8_t Palette::indexOf(uint16_t color) const {
return std::distance(colors.begin(), std::find(colors.begin(), colors.end(), color));
}
auto Palette::begin() -> decltype(colors)::iterator {
return colors.begin();
}
auto Palette::end() -> decltype(colors)::iterator {
return std::find(colors.begin(), colors.end(), UINT16_MAX);
}
auto Palette::begin() const -> decltype(colors)::const_iterator {
return colors.begin();
}
auto Palette::end() const -> decltype(colors)::const_iterator {
return std::find(colors.begin(), colors.end(), UINT16_MAX);
}

View File

@@ -1,806 +0,0 @@
/*
* 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);
}

359
src/gfx/pal_packing.cpp Normal file
View File

@@ -0,0 +1,359 @@
/*
* This file is part of RGBDS.
*
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
*
* SPDX-License-Identifier: MIT
*/
#include "gfx/pal_packing.hpp"
#include <assert.h>
#include <bitset>
#include <inttypes.h>
#include <numeric>
#include <optional>
#include <queue>
#include <tuple>
#include <type_traits>
#include <unordered_set>
#include <vector>
#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 palIndex;
/**
* 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) : palIndex(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(decltype(_protoPals) 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::input_iterator_tag;
private:
Constness<decltype(_assigned)> *_array = nullptr;
Inner _iter{};
Iter(decltype(_array) array, decltype(_iter) &&iter) : _array(array), _iter(iter) {
skipEmpty();
}
void skipEmpty() {
while (_iter != _array->end() && !(*_iter).has_value()) {
++_iter;
}
}
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()}; }
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()}; }
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>
auto 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)...);
}
return freeSlot;
}
void remove(iterator const &iter) {
(*iter._iter).reset(); // This time, we want to access the `optional` itself
}
void clear() { _assigned.clear(); }
/**
* Computes the "relative size" of a proto-palette on this palette
*/
double relSizeOf(ProtoPalette const &protoPal) const {
return std::transform_reduce(
protoPal.begin(), protoPal.end(), .0, std::plus<>(), [this](uint16_t color) {
// 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.
return 1.
/ (1
+ std::count_if(
begin(), end(), [this, &color](ProtoPalAttrs const &attrs) {
ProtoPalette const &pal = _protoPals[attrs.palIndex];
return std::find(pal.begin(), pal.end(), color) != pal.end();
}));
});
}
private:
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();
for (ProtoPalAttrs const &attrs : *this) {
for (uint16_t color : _protoPals[attrs.palIndex]) {
colors.insert(color);
}
}
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();
for (uint16_t color : protoPal) {
colors.insert(color);
}
return colors.size() <= 4;
}
};
std::tuple<DefaultInitVec<size_t>, size_t>
overloadAndRemove(std::vector<ProtoPalette> const &protoPalettes) {
options.verbosePrint("Paginating palettes using \"overload-and-remove\" strategy...\n");
struct Iota {
using value_type = size_t;
using difference_type = size_t;
using pointer = value_type const *;
using reference = value_type const &;
using iterator_category = std::input_iterator_tag;
// Use aggregate init etc.
value_type i;
bool operator!=(Iota const &other) { return i != other.i; }
reference operator*() const { return i; }
pointer operator->() const { return &i; }
Iota operator++() {
++i;
return *this;
}
Iota operator++(int) {
Iota copy = *this;
++i;
return copy;
}
};
// Begin with all proto-palettes queued up for insertion
std::queue queue(std::deque<ProtoPalAttrs>(Iota{0}, Iota{protoPalettes.size()}));
// Begin with no pages
std::vector<AssignedProtos> assignments{};
for (; !queue.empty(); queue.pop()) {
ProtoPalAttrs const &attrs = queue.front(); // Valid until the `queue.pop()`
ProtoPalette const &protoPal = protoPalettes[attrs.palIndex];
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("%zu: Rel size: %f (size = %zu)\n", i,
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() > 4) {
options.verbosePrint("Palette %zu is overloaded! (%zu > 4)\n", bestPalIndex,
bestPal.volume());
// 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.palIndex])
< efficiency(protoPalettes[rhs.palIndex]);
});
// 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->palIndex])
- efficiency(protoPalettes[minEfficiencyIter->palIndex])
< .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() > 4) {
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.palIndex];
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("Adding new palette for overflow\n");
assignments.emplace_back(protoPalettes, std::move(attrs));
} else {
options.verbosePrint("Assigning overflow to palette %zu\n", iter - assignments.begin());
iter->assign(std::move(attrs));
}
queue.pop();
}
// Deal with any empty palettes left over from the "un-overloading" step
// TODO (can there be any?)
if (options.beVerbose) {
for (auto &&assignment : assignments) {
options.verbosePrint("{ ");
for (auto &&attrs : assignment) {
for (auto &&colorIndex : protoPalettes[attrs.palIndex]) {
options.verbosePrint("%04" PRIx16 ", ", colorIndex);
}
}
options.verbosePrint("} (%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.palIndex] = i;
}
}
return {mappings, assignments.size()};
}
} // namespace packing

64
src/gfx/pal_sorting.cpp Normal file
View File

@@ -0,0 +1,64 @@
#include "gfx/pal_sorting.hpp"
#include <algorithm>
#include <png.h>
#include <vector>
#include "helpers.h"
#include "gfx/main.hpp"
namespace sorting {
void indexed(std::vector<Palette> &palettes, int palSize, png_color const *palRGB,
png_byte *palAlpha) {
options.verbosePrint("Sorting palettes using embedded palette...\n");
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) {
if (palAlpha && palAlpha[i])
continue;
auto const &c = palRGB[i];
uint16_t cgbColor = c.red >> 3 | (c.green >> 3) << 5 | (c.blue >> 3) << 10;
// Return whether lhs < rhs
if (cgbColor == rhs) {
return false;
}
if (cgbColor == lhs) {
return true;
}
}
unreachable_(); // This should not be possible
});
}
}
void grayscale(std::vector<Palette> &palettes) {
options.verbosePrint("Sorting grayscale-only palettes...\n");
for (Palette &pal : palettes) {
(void)pal; // TODO
}
}
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("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

79
src/gfx/proto_palette.cpp Normal file
View File

@@ -0,0 +1,79 @@
/*
* 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 <stddef.h>
#include <stdint.h>
bool ProtoPalette::add(uint16_t color) {
size_t i = 0;
// Seek the first slot greater than our 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())
return false; // EOF
}
// If we found ourselves, great!
if (_colorIndices[i] == color)
return true;
// Swap entries until the end
while (_colorIndices[i] != UINT16_MAX) {
std::swap(_colorIndices[i], color);
++i;
if (i == _colorIndices.size())
return false; // Oh well
}
// Write that last one into the new slot
_colorIndices[i] = color;
return true;
}
ProtoPalette::ComparisonResult ProtoPalette::compare(ProtoPalette const &other) const {
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 {
++theirs;
weBigger = false;
}
}
weBigger &= ours == _colorIndices.end();
theyBigger &= theirs == other._colorIndices.end();
return theyBigger ? THEY_BIGGER : (weBigger ? WE_BIGGER : NEITHER);
}
ProtoPalette &ProtoPalette::operator=(ProtoPalette const &other) {
_colorIndices = other._colorIndices;
return *this;
}
size_t ProtoPalette::size() const {
return std::distance(begin(), end());
}
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);
}