mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 10:12:06 +00:00
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:
@@ -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
|
||||
|
||||
27
Makefile
27
Makefile
@@ -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!
|
||||
|
||||
@@ -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.
|
||||
|
||||
38
include/defaultinitalloc.hpp
Normal file
38
include/defaultinitalloc.hpp
Normal 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
|
||||
@@ -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 */
|
||||
|
||||
8
include/extern/getopt.h
vendored
8
include/extern/getopt.h
vendored
@@ -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
14
include/gfx/convert.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* This file is part of RGBDS.
|
||||
*
|
||||
* Copyright (c) 2022, Eldred Habert and RGBDS contributors.
|
||||
*
|
||||
* SPDX-License-Identifier: MIT
|
||||
*/
|
||||
|
||||
#ifndef RGBDS_GFX_CONVERT_HPP
|
||||
#define RGBDS_GFX_CONVERT_HPP
|
||||
|
||||
void process();
|
||||
|
||||
#endif /* RGBDS_GFX_CONVERT_HPP */
|
||||
@@ -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
59
include/gfx/main.hpp
Normal 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 */
|
||||
32
include/gfx/pal_packing.hpp
Normal file
32
include/gfx/pal_packing.hpp
Normal 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 */
|
||||
26
include/gfx/pal_sorting.hpp
Normal file
26
include/gfx/pal_sorting.hpp
Normal 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 */
|
||||
44
include/gfx/proto_palette.hpp
Normal file
44
include/gfx/proto_palette.hpp
Normal 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 */
|
||||
@@ -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 */
|
||||
|
||||
94
man/rgbgfx.1
94
man/rgbgfx.1
@@ -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 .
|
||||
|
||||
@@ -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
869
src/gfx/convert.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
385
src/gfx/gb.c
385
src/gfx/gb.c
@@ -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);
|
||||
}
|
||||
358
src/gfx/main.c
358
src/gfx/main.c
@@ -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
321
src/gfx/main.cpp
Normal 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);
|
||||
}
|
||||
@@ -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
359
src/gfx/pal_packing.cpp
Normal 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
64
src/gfx/pal_sorting.cpp
Normal 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
79
src/gfx/proto_palette.cpp
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user