mirror of
https://github.com/gbdev/rgbds.git
synced 2025-11-20 10:12:06 +00:00
Factor out a single PNG-reading function to encapsulate the libpng API (#1765)
This commit is contained in:
3
Makefile
3
Makefile
@@ -105,6 +105,7 @@ rgbgfx_obj := \
|
|||||||
src/gfx/pal_packing.o \
|
src/gfx/pal_packing.o \
|
||||||
src/gfx/pal_sorting.o \
|
src/gfx/pal_sorting.o \
|
||||||
src/gfx/pal_spec.o \
|
src/gfx/pal_spec.o \
|
||||||
|
src/gfx/png.o \
|
||||||
src/gfx/process.o \
|
src/gfx/process.o \
|
||||||
src/gfx/proto_palette.o \
|
src/gfx/proto_palette.o \
|
||||||
src/gfx/reverse.o \
|
src/gfx/reverse.o \
|
||||||
@@ -152,6 +153,8 @@ src/gfx/pal_sorting.o: src/gfx/pal_sorting.cpp
|
|||||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||||
src/gfx/pal_spec.o: src/gfx/pal_spec.cpp
|
src/gfx/pal_spec.o: src/gfx/pal_spec.cpp
|
||||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||||
|
src/gfx/png.o: src/gfx/png.cpp
|
||||||
|
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||||
src/gfx/process.o: src/gfx/process.cpp
|
src/gfx/process.o: src/gfx/process.cpp
|
||||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||||
src/gfx/proto_palette.o: src/gfx/proto_palette.cpp
|
src/gfx/proto_palette.o: src/gfx/proto_palette.cpp
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <png.h>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "gfx/rgba.hpp"
|
#include "gfx/rgba.hpp"
|
||||||
@@ -16,13 +15,7 @@ static constexpr size_t NB_COLOR_SLOTS = (1 << (5 * 3)) + 1;
|
|||||||
|
|
||||||
struct Palette;
|
struct Palette;
|
||||||
|
|
||||||
void sortIndexed(
|
void sortIndexed(std::vector<Palette> &palettes, std::vector<Rgba> const &embPal);
|
||||||
std::vector<Palette> &palettes,
|
|
||||||
int palSize,
|
|
||||||
png_color const *palRGB,
|
|
||||||
int palAlphaSize,
|
|
||||||
png_byte *palAlpha
|
|
||||||
);
|
|
||||||
void sortGrayscale(
|
void sortGrayscale(
|
||||||
std::vector<Palette> &palettes, std::array<std::optional<Rgba>, NB_COLOR_SLOTS> const &colors
|
std::vector<Palette> &palettes, std::array<std::optional<Rgba>, NB_COLOR_SLOTS> const &colors
|
||||||
);
|
);
|
||||||
|
|||||||
21
include/gfx/png.hpp
Normal file
21
include/gfx/png.hpp
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#ifndef RGBDS_GFX_PNG_HPP
|
||||||
|
#define RGBDS_GFX_PNG_HPP
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "gfx/rgba.hpp"
|
||||||
|
|
||||||
|
struct Png {
|
||||||
|
uint32_t width, height;
|
||||||
|
std::vector<Rgba> pixels{};
|
||||||
|
std::vector<Rgba> palette{};
|
||||||
|
|
||||||
|
Png() {}
|
||||||
|
Png(char const *filename, std::streambuf &file);
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // RGBDS_GFX_PNG_HPP
|
||||||
@@ -17,16 +17,16 @@ struct Rgba {
|
|||||||
explicit constexpr Rgba(uint32_t rgba = 0)
|
explicit constexpr Rgba(uint32_t rgba = 0)
|
||||||
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
|
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
|
||||||
|
|
||||||
static constexpr Rgba fromCGBColor(uint16_t cgbColor) {
|
static constexpr Rgba fromCGBColor(uint16_t color) {
|
||||||
constexpr auto _5to8 = [](uint8_t fiveBpp) -> uint8_t {
|
constexpr auto _5to8 = [](uint8_t channel) -> uint8_t {
|
||||||
fiveBpp &= 0b11111; // For caller's convenience
|
channel &= 0b11111; // For caller's convenience
|
||||||
return fiveBpp << 3 | fiveBpp >> 2;
|
return channel << 3 | channel >> 2;
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
_5to8(cgbColor),
|
_5to8(color),
|
||||||
_5to8(cgbColor >> 5),
|
_5to8(color >> 5),
|
||||||
_5to8(cgbColor >> 10),
|
_5to8(color >> 10),
|
||||||
static_cast<uint8_t>(cgbColor & 0x8000 ? 0x00 : 0xFF),
|
static_cast<uint8_t>(color & 0x8000 ? 0x00 : 0xFF),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ set(rgbgfx_src
|
|||||||
"gfx/pal_packing.cpp"
|
"gfx/pal_packing.cpp"
|
||||||
"gfx/pal_sorting.cpp"
|
"gfx/pal_sorting.cpp"
|
||||||
"gfx/pal_spec.cpp"
|
"gfx/pal_spec.cpp"
|
||||||
|
"gfx/png.cpp"
|
||||||
"gfx/process.cpp"
|
"gfx/process.cpp"
|
||||||
"gfx/proto_palette.cpp"
|
"gfx/proto_palette.cpp"
|
||||||
"gfx/reverse.cpp"
|
"gfx/reverse.cpp"
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ static void writeRpn(std::vector<uint8_t> &rpnexpr, std::vector<uint8_t> const &
|
|||||||
for (size_t offset = 0; offset < rpn.size();) {
|
for (size_t offset = 0; offset < rpn.size();) {
|
||||||
uint8_t rpndata = rpn[offset++];
|
uint8_t rpndata = rpn[offset++];
|
||||||
|
|
||||||
auto getSymName = [&](){
|
auto getSymName = [&]() {
|
||||||
std::string symName;
|
std::string symName;
|
||||||
for (uint8_t c; (c = rpn[offset++]) != 0;) {
|
for (uint8_t c; (c = rpn[offset++]) != 0;) {
|
||||||
symName += c;
|
symName += c;
|
||||||
|
|||||||
@@ -8,27 +8,14 @@
|
|||||||
|
|
||||||
#include "gfx/main.hpp"
|
#include "gfx/main.hpp"
|
||||||
|
|
||||||
void sortIndexed(
|
void sortIndexed(std::vector<Palette> &palettes, std::vector<Rgba> const &embPal) {
|
||||||
std::vector<Palette> &palettes,
|
|
||||||
int palSize,
|
|
||||||
png_color const *palRGB,
|
|
||||||
int palAlphaSize,
|
|
||||||
png_byte *palAlpha
|
|
||||||
) {
|
|
||||||
options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palettes using embedded palette...\n");
|
options.verbosePrint(Options::VERB_LOG_ACT, "Sorting palettes using embedded palette...\n");
|
||||||
|
|
||||||
auto pngToRgb = [&palRGB, &palAlphaSize, &palAlpha](int index) {
|
|
||||||
png_color const &c = palRGB[index];
|
|
||||||
return Rgba(
|
|
||||||
c.red, c.green, c.blue, palAlpha && index < palAlphaSize ? palAlpha[index] : 0xFF
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
for (Palette &pal : palettes) {
|
for (Palette &pal : palettes) {
|
||||||
std::sort(RANGE(pal), [&](uint16_t lhs, uint16_t rhs) {
|
std::sort(RANGE(pal), [&](uint16_t lhs, uint16_t rhs) {
|
||||||
// Iterate through the PNG's palette, looking for either of the two
|
// Iterate through the PNG's palette, looking for either of the two
|
||||||
for (int i = 0; i < palSize; ++i) {
|
for (Rgba const &rgba : embPal) {
|
||||||
uint16_t color = pngToRgb(i).cgbColor();
|
uint16_t color = rgba.cgbColor();
|
||||||
if (color == Rgba::transparent) {
|
if (color == Rgba::transparent) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <png.h>
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <streambuf>
|
#include <streambuf>
|
||||||
@@ -23,6 +22,7 @@
|
|||||||
#include "platform.hpp"
|
#include "platform.hpp"
|
||||||
|
|
||||||
#include "gfx/main.hpp"
|
#include "gfx/main.hpp"
|
||||||
|
#include "gfx/png.hpp"
|
||||||
#include "gfx/warning.hpp"
|
#include "gfx/warning.hpp"
|
||||||
|
|
||||||
using namespace std::string_view_literals;
|
using namespace std::string_view_literals;
|
||||||
@@ -265,7 +265,7 @@ static std::optional<Rgba> parseColor(std::string const &str, size_t &n, uint16_
|
|||||||
return std::optional<Rgba>{Rgba(*r, *g, *b, 0xFF)};
|
return std::optional<Rgba>{Rgba(*r, *g, *b, 0xFF)};
|
||||||
}
|
}
|
||||||
|
|
||||||
static void parsePSPFile(std::filebuf &file) {
|
static void parsePSPFile(char const *, std::filebuf &file) {
|
||||||
// https://www.selapa.net/swatches/colors/fileformats.php#psp_pal
|
// https://www.selapa.net/swatches/colors/fileformats.php#psp_pal
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
@@ -328,7 +328,7 @@ static void parsePSPFile(std::filebuf &file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void parseGPLFile(std::filebuf &file) {
|
static void parseGPLFile(char const *, std::filebuf &file) {
|
||||||
// https://gitlab.gnome.org/GNOME/gimp/-/blob/gimp-2-10/app/core/gimppalette-load.c#L39
|
// https://gitlab.gnome.org/GNOME/gimp/-/blob/gimp-2-10/app/core/gimppalette-load.c#L39
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
@@ -383,7 +383,7 @@ static void parseGPLFile(std::filebuf &file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void parseHEXFile(std::filebuf &file) {
|
static void parseHEXFile(char const *, std::filebuf &file) {
|
||||||
// https://lospec.com/palette-list/tag/gbc
|
// https://lospec.com/palette-list/tag/gbc
|
||||||
|
|
||||||
uint16_t nbColors = 0;
|
uint16_t nbColors = 0;
|
||||||
@@ -430,7 +430,7 @@ static void parseHEXFile(std::filebuf &file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void parseACTFile(std::filebuf &file) {
|
static void parseACTFile(char const *, std::filebuf &file) {
|
||||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626
|
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1070626
|
||||||
|
|
||||||
std::array<char, 772> buf{};
|
std::array<char, 772> buf{};
|
||||||
@@ -482,7 +482,7 @@ static void parseACTFile(std::filebuf &file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void parseACOFile(std::filebuf &file) {
|
static void parseACOFile(char const *, std::filebuf &file) {
|
||||||
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819
|
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577411_pgfId-1055819
|
||||||
|
|
||||||
char buf[10];
|
char buf[10];
|
||||||
@@ -551,7 +551,7 @@ static void parseACOFile(std::filebuf &file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void parseGBCFile(std::filebuf &file) {
|
static void parseGBCFile(char const *, std::filebuf &file) {
|
||||||
// This only needs to be able to read back files generated by `rgbgfx -p`
|
// This only needs to be able to read back files generated by `rgbgfx -p`
|
||||||
options.palSpec.clear();
|
options.palSpec.clear();
|
||||||
|
|
||||||
@@ -579,41 +579,15 @@ static void parseGBCFile(std::filebuf &file) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[[noreturn]]
|
static bool checkPngSwatch(std::vector<Rgba> const &pixels, uint32_t base, uint32_t swatchSize) {
|
||||||
static void handlePngError(png_structp, char const *msg) {
|
|
||||||
fatal("Error reading palette file: %s", msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handlePngWarning(png_structp, char const *msg) {
|
|
||||||
warnx("In palette file: %s", msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void readPngData(png_structp png, png_bytep data, size_t length) {
|
|
||||||
std::filebuf *file = reinterpret_cast<std::filebuf *>(png_get_io_ptr(png));
|
|
||||||
std::streamsize expectedLen = length;
|
|
||||||
std::streamsize nbBytesRead = file->sgetn(reinterpret_cast<char *>(data), expectedLen);
|
|
||||||
|
|
||||||
if (nbBytesRead != expectedLen) {
|
|
||||||
fatal(
|
|
||||||
"Error reading palette file: file too short (expected at least %zd more bytes after "
|
|
||||||
"reading %zu)",
|
|
||||||
length - nbBytesRead,
|
|
||||||
static_cast<size_t>(file->pubseekoff(0, std::ios_base::cur))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool checkPngSwatch(std::vector<png_byte> const &image, uint32_t base, uint32_t swatchSize) {
|
|
||||||
Rgba topLeft(image[base], image[base + 1], image[base + 2], image[base + 3]);
|
|
||||||
uint32_t rowFactor = swatchSize * options.nbColorsPerPal;
|
|
||||||
for (uint32_t y = 0; y < swatchSize; y++) {
|
for (uint32_t y = 0; y < swatchSize; y++) {
|
||||||
|
uint32_t yOffset = y * swatchSize * options.nbColorsPerPal + base;
|
||||||
|
|
||||||
for (uint32_t x = 0; x < swatchSize; x++) {
|
for (uint32_t x = 0; x < swatchSize; x++) {
|
||||||
if (x == 0 && y == 0) {
|
if (x == 0 && y == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
uint32_t offset = base + (y * rowFactor + x) * 4;
|
if (pixels[yOffset + x] != pixels[base]) {
|
||||||
Rgba pixel(image[offset], image[offset + 1], image[offset + 2], image[offset + 3]);
|
|
||||||
if (pixel != topLeft) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -621,135 +595,38 @@ static bool checkPngSwatch(std::vector<png_byte> const &image, uint32_t base, ui
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void parsePNGFile(std::filebuf &file) {
|
static void parsePNGFile(char const *filename, std::filebuf &file) {
|
||||||
std::array<unsigned char, 8> pngHeader;
|
Png png{filename, file};
|
||||||
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) {
|
|
||||||
// LCOV_EXCL_START
|
|
||||||
error("Palette file does not appear to be a PNG palette file");
|
|
||||||
return;
|
|
||||||
// LCOV_EXCL_STOP
|
|
||||||
}
|
|
||||||
|
|
||||||
png_structp png =
|
|
||||||
png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, handlePngError, handlePngWarning);
|
|
||||||
if (!png) {
|
|
||||||
// LCOV_EXCL_START
|
|
||||||
error("Failed to create PNG read structure: %s", strerror(errno));
|
|
||||||
return;
|
|
||||||
// LCOV_EXCL_STOP
|
|
||||||
}
|
|
||||||
|
|
||||||
png_infop info = png_create_info_struct(png);
|
|
||||||
Defer destroyPng{[&] { png_destroy_read_struct(&png, &info, nullptr); }};
|
|
||||||
if (!info) {
|
|
||||||
// LCOV_EXCL_START
|
|
||||||
error("Failed to create PNG info structure: %s", strerror(errno));
|
|
||||||
return;
|
|
||||||
// LCOV_EXCL_STOP
|
|
||||||
}
|
|
||||||
|
|
||||||
png_set_read_fn(png, &file, readPngData);
|
|
||||||
png_set_sig_bytes(png, pngHeader.size());
|
|
||||||
|
|
||||||
// Process all chunks up to but not including the image data
|
|
||||||
png_read_info(png, info);
|
|
||||||
|
|
||||||
uint32_t width, height;
|
|
||||||
int bitDepth, colorType, interlaceType;
|
|
||||||
png_get_IHDR(
|
|
||||||
png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr, nullptr
|
|
||||||
);
|
|
||||||
|
|
||||||
png_colorp embeddedPal = nullptr;
|
|
||||||
int nbColors;
|
|
||||||
png_bytep transparencyPal = nullptr;
|
|
||||||
int nbTransparentEntries;
|
|
||||||
if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) {
|
|
||||||
if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) {
|
|
||||||
assume(nbTransparentEntries <= nbColors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up transformations to turn everything into RGBA888 for simplicity of handling
|
|
||||||
|
|
||||||
// Convert grayscale to RGB
|
|
||||||
switch (colorType & ~PNG_COLOR_MASK_ALPHA) {
|
|
||||||
case PNG_COLOR_TYPE_GRAY:
|
|
||||||
png_set_gray_to_rgb(png); // This also converts tRNS to alpha
|
|
||||||
break;
|
|
||||||
case PNG_COLOR_TYPE_PALETTE:
|
|
||||||
png_set_palette_to_rgb(png);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (png_get_valid(png, info, PNG_INFO_tRNS)) {
|
|
||||||
// If we read a tRNS chunk, convert it to alpha
|
|
||||||
png_set_tRNS_to_alpha(png);
|
|
||||||
} else if (!(colorType & PNG_COLOR_MASK_ALPHA)) {
|
|
||||||
// Otherwise, if we lack an alpha channel, default to full opacity
|
|
||||||
png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale 16bpp back to 8 (we don't need all of that precision anyway)
|
|
||||||
if (bitDepth == 16) {
|
|
||||||
png_set_scale_16(png);
|
|
||||||
} else if (bitDepth < 8) {
|
|
||||||
png_set_packing(png);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interlaceType != PNG_INTERLACE_NONE) {
|
|
||||||
png_set_interlace_handling(png);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update `info` with the transformations
|
|
||||||
png_read_update_info(png, info);
|
|
||||||
// These shouldn't have changed
|
|
||||||
assume(png_get_image_width(png, info) == width);
|
|
||||||
assume(png_get_image_height(png, info) == height);
|
|
||||||
// These should have changed, however
|
|
||||||
assume(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA);
|
|
||||||
assume(png_get_bit_depth(png, info) == 8);
|
|
||||||
|
|
||||||
// Now that metadata has been read, we can process the image data
|
|
||||||
|
|
||||||
std::vector<png_byte> image(width * height * 4);
|
|
||||||
std::vector<png_bytep> rowPtrs(height);
|
|
||||||
for (uint32_t y = 0; y < height; ++y) {
|
|
||||||
rowPtrs[y] = image.data() + y * width * 4;
|
|
||||||
}
|
|
||||||
png_read_image(png, rowPtrs.data());
|
|
||||||
|
|
||||||
// The image width must evenly divide into a color swatch for each color per palette
|
// The image width must evenly divide into a color swatch for each color per palette
|
||||||
if (width % options.nbColorsPerPal != 0) {
|
if (png.width % options.nbColorsPerPal != 0) {
|
||||||
error(
|
error(
|
||||||
"PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu8
|
"PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu8
|
||||||
" color swatches wide",
|
" color swatches wide",
|
||||||
width,
|
png.width,
|
||||||
height,
|
png.height,
|
||||||
options.nbColorsPerPal
|
options.nbColorsPerPal
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infer the color swatch size (width and height) from the image width
|
// Infer the color swatch size (width and height) from the image width
|
||||||
uint32_t swatchSize = width / options.nbColorsPerPal;
|
uint32_t swatchSize = png.width / options.nbColorsPerPal;
|
||||||
|
|
||||||
// The image height must evenly divide into a color swatch for each palette
|
// The image height must evenly divide into a color swatch for each palette
|
||||||
if (height % swatchSize != 0) {
|
if (png.height % swatchSize != 0) {
|
||||||
error(
|
error(
|
||||||
"PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu32
|
"PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu32
|
||||||
" pixels high",
|
" pixels high",
|
||||||
width,
|
png.width,
|
||||||
height,
|
png.height,
|
||||||
swatchSize
|
swatchSize
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// More palettes than the maximum are a warning, not an error
|
// More palettes than the maximum are a warning, not an error
|
||||||
uint32_t nbPals = height / swatchSize;
|
uint32_t nbPals = png.height / swatchSize;
|
||||||
if (nbPals > options.nbPalettes) {
|
if (nbPals > options.nbPalettes) {
|
||||||
warnx(
|
warnx(
|
||||||
"PNG palette file contains %" PRIu32 " palette rows, but there can only be %" PRIu16
|
"PNG palette file contains %" PRIu32 " palette rows, but there can only be %" PRIu16
|
||||||
@@ -763,17 +640,16 @@ static void parsePNGFile(std::filebuf &file) {
|
|||||||
options.palSpec.clear();
|
options.palSpec.clear();
|
||||||
|
|
||||||
// Get each color from the top-left pixel of each swatch
|
// Get each color from the top-left pixel of each swatch
|
||||||
uint32_t colorFactor = swatchSize * 4;
|
for (uint32_t y = 0; y < nbPals; ++y) {
|
||||||
uint32_t palFactor = swatchSize * options.nbColorsPerPal;
|
uint32_t yOffset = y * swatchSize * swatchSize * options.nbColorsPerPal;
|
||||||
for (uint32_t palIdx = 0; palIdx < nbPals; ++palIdx) {
|
|
||||||
options.palSpec.emplace_back();
|
options.palSpec.emplace_back();
|
||||||
for (uint32_t colorIdx = 0; colorIdx < options.nbColorsPerPal; ++colorIdx) {
|
|
||||||
std::optional<Rgba> &color = options.palSpec.back()[colorIdx];
|
for (uint32_t x = 0; x < options.nbColorsPerPal; ++x) {
|
||||||
uint32_t offset = (palIdx * palFactor + colorIdx) * colorFactor;
|
uint32_t offset = yOffset + x * swatchSize;
|
||||||
color = Rgba(image[offset], image[offset + 1], image[offset + 2], image[offset + 3]);
|
options.palSpec.back()[x] = png.pixels[offset];
|
||||||
|
|
||||||
// Check that each swatch is completely one color
|
// Check that each swatch is completely one color
|
||||||
if (!checkPngSwatch(image, offset, swatchSize)) {
|
if (!checkPngSwatch(png.pixels, offset, swatchSize)) {
|
||||||
error("PNG palette file uses multiple colors in one color swatch");
|
error("PNG palette file uses multiple colors in one color swatch");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -821,7 +697,7 @@ void parseExternalPalSpec(char const *arg) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::get<1> (*iter)(file);
|
std::get<1> (*iter)(path, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseDmgPalSpec(char const * const rawArg) {
|
void parseDmgPalSpec(char const * const rawArg) {
|
||||||
|
|||||||
216
src/gfx/png.cpp
Normal file
216
src/gfx/png.cpp
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
#include "gfx/png.hpp"
|
||||||
|
|
||||||
|
#include <png.h>
|
||||||
|
|
||||||
|
#include "gfx/main.hpp"
|
||||||
|
#include "gfx/rgba.hpp"
|
||||||
|
#include "gfx/warning.hpp"
|
||||||
|
|
||||||
|
struct Input {
|
||||||
|
char const *filename;
|
||||||
|
std::streambuf &file;
|
||||||
|
|
||||||
|
Input(char const *filename_, std::streambuf &file_) : filename(filename_), file(file_) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
[[noreturn]]
|
||||||
|
static void handleError(png_structp png, char const *msg) {
|
||||||
|
Input const &input = *reinterpret_cast<Input *>(png_get_error_ptr(png));
|
||||||
|
|
||||||
|
fatal("Error reading PNG image (\"%s\"): %s", input.filename, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handleWarning(png_structp png, char const *msg) {
|
||||||
|
Input const &input = *reinterpret_cast<Input *>(png_get_error_ptr(png));
|
||||||
|
|
||||||
|
warnx("In PNG image (\"%s\"): %s", input.filename, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void readData(png_structp png, png_bytep data, size_t length) {
|
||||||
|
Input &input = *reinterpret_cast<Input *>(png_get_io_ptr(png));
|
||||||
|
std::streamsize expectedLen = length;
|
||||||
|
std::streamsize nbBytesRead = input.file.sgetn(reinterpret_cast<char *>(data), expectedLen);
|
||||||
|
|
||||||
|
if (nbBytesRead != expectedLen) {
|
||||||
|
fatal(
|
||||||
|
"Error reading PNG image (\"%s\"): file too short (expected at least %zd more "
|
||||||
|
"bytes after reading %zu)",
|
||||||
|
input.filename,
|
||||||
|
length - nbBytesRead,
|
||||||
|
static_cast<size_t>(input.file.pubseekoff(0, std::ios_base::cur))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Png::Png(char const *filename, std::streambuf &file) {
|
||||||
|
Input input(filename, file);
|
||||||
|
|
||||||
|
options.verbosePrint(Options::VERB_LOG_ACT, "Reading PNG file \"%s\"\n", input.filename);
|
||||||
|
|
||||||
|
std::array<unsigned char, 8> pngHeader;
|
||||||
|
if (input.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("PNG file (\"%s\") is not a valid PNG image!", input.filename); // LCOV_EXCL_LINE
|
||||||
|
}
|
||||||
|
|
||||||
|
options.verbosePrint(Options::VERB_INTERM, "PNG header signature is OK\n");
|
||||||
|
|
||||||
|
png_structp png = png_create_read_struct(
|
||||||
|
PNG_LIBPNG_VER_STRING, static_cast<png_voidp>(&input), handleError, handleWarning
|
||||||
|
);
|
||||||
|
if (!png) {
|
||||||
|
fatal("Failed to create PNG read structure: %s", strerror(errno)); // LCOV_EXCL_LINE
|
||||||
|
}
|
||||||
|
|
||||||
|
png_infop info = png_create_info_struct(png);
|
||||||
|
Defer destroyPng{[&] { png_destroy_read_struct(&png, info ? &info : nullptr, nullptr); }};
|
||||||
|
if (!info) {
|
||||||
|
fatal("Failed to create PNG info structure: %s", strerror(errno)); // LCOV_EXCL_LINE
|
||||||
|
}
|
||||||
|
|
||||||
|
png_set_read_fn(png, &input, readData);
|
||||||
|
png_set_sig_bytes(png, pngHeader.size());
|
||||||
|
|
||||||
|
// Process all chunks up to but not including the image data
|
||||||
|
png_read_info(png, info);
|
||||||
|
|
||||||
|
int bitDepth, colorType, interlaceType;
|
||||||
|
png_get_IHDR(
|
||||||
|
png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr, nullptr
|
||||||
|
);
|
||||||
|
|
||||||
|
pixels.resize(static_cast<size_t>(width) * static_cast<size_t>(height));
|
||||||
|
|
||||||
|
auto colorTypeName = [](int type) {
|
||||||
|
switch (type) {
|
||||||
|
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:
|
||||||
|
return "unknown color type";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto interlaceTypeName = [](int type) {
|
||||||
|
switch (type) {
|
||||||
|
case PNG_INTERLACE_NONE:
|
||||||
|
return "not interlaced";
|
||||||
|
case PNG_INTERLACE_ADAM7:
|
||||||
|
return "interlaced (Adam7)";
|
||||||
|
default:
|
||||||
|
return "unknown interlace type";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
options.verbosePrint(
|
||||||
|
Options::VERB_INTERM,
|
||||||
|
"PNG image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
bitDepth,
|
||||||
|
colorTypeName(colorType),
|
||||||
|
interlaceTypeName(interlaceType)
|
||||||
|
);
|
||||||
|
|
||||||
|
int nbColors = 0;
|
||||||
|
png_colorp embeddedPal = nullptr;
|
||||||
|
if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) {
|
||||||
|
int nbTransparentEntries = 0;
|
||||||
|
png_bytep transparencyPal = nullptr;
|
||||||
|
if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) {
|
||||||
|
assume(nbTransparentEntries <= nbColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < nbColors; ++i) {
|
||||||
|
png_color const &color = embeddedPal[i];
|
||||||
|
palette.emplace_back(
|
||||||
|
color.red,
|
||||||
|
color.green,
|
||||||
|
color.blue,
|
||||||
|
transparencyPal && i < nbTransparentEntries ? transparencyPal[i] : 0xFF
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.verbosePrint(
|
||||||
|
Options::VERB_INTERM, "Embedded PNG palette has %d colors: [", nbColors
|
||||||
|
);
|
||||||
|
for (int i = 0; i < nbColors; ++i) {
|
||||||
|
if (i > 0) {
|
||||||
|
options.verbosePrint(Options::VERB_INTERM, ", ");
|
||||||
|
}
|
||||||
|
options.verbosePrint(Options::VERB_INTERM, "#%08x", palette[i].toCSS());
|
||||||
|
}
|
||||||
|
options.verbosePrint(Options::VERB_INTERM, "]\n");
|
||||||
|
} else {
|
||||||
|
options.verbosePrint(Options::VERB_INTERM, "No embedded PNG palette\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up transformations to turn everything into RGBA8888 for simplicity of handling
|
||||||
|
|
||||||
|
// Convert grayscale to RGB
|
||||||
|
switch (colorType & ~PNG_COLOR_MASK_ALPHA) {
|
||||||
|
case PNG_COLOR_TYPE_GRAY:
|
||||||
|
png_set_gray_to_rgb(png); // This also converts tRNS to alpha
|
||||||
|
break;
|
||||||
|
case PNG_COLOR_TYPE_PALETTE:
|
||||||
|
png_set_palette_to_rgb(png);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (png_get_valid(png, info, PNG_INFO_tRNS)) {
|
||||||
|
// If we read a tRNS chunk, convert it to alpha
|
||||||
|
png_set_tRNS_to_alpha(png);
|
||||||
|
} else if (!(colorType & PNG_COLOR_MASK_ALPHA)) {
|
||||||
|
// Otherwise, if we lack an alpha channel, default to full opacity
|
||||||
|
png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale 16bpp back to 8 (we don't need all of that precision anyway)
|
||||||
|
if (bitDepth == 16) {
|
||||||
|
png_set_scale_16(png);
|
||||||
|
} else if (bitDepth < 8) {
|
||||||
|
png_set_packing(png);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deinterlace rows so they can trivially be read in order
|
||||||
|
if (interlaceType != PNG_INTERLACE_NONE) {
|
||||||
|
png_set_interlace_handling(png);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update `info` with the transformations
|
||||||
|
png_read_update_info(png, info);
|
||||||
|
// These shouldn't have changed
|
||||||
|
assume(png_get_image_width(png, info) == width);
|
||||||
|
assume(png_get_image_height(png, info) == height);
|
||||||
|
// These should have changed, however
|
||||||
|
assume(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA);
|
||||||
|
assume(png_get_bit_depth(png, info) == 8);
|
||||||
|
|
||||||
|
// Now that metadata has been read, we can read the image data
|
||||||
|
std::vector<png_byte> image(width * height * 4);
|
||||||
|
std::vector<png_bytep> rowPtrs(height);
|
||||||
|
for (uint32_t y = 0; y < height; ++y) {
|
||||||
|
rowPtrs[y] = image.data() + y * width * 4;
|
||||||
|
}
|
||||||
|
png_read_image(png, rowPtrs.data());
|
||||||
|
|
||||||
|
// We don't care about chunks after the image data (comments, etc.)
|
||||||
|
png_read_end(png, nullptr);
|
||||||
|
|
||||||
|
// Finally, process the image data from RGBA8888 bytes into `Rgba` colors
|
||||||
|
for (uint32_t y = 0; y < height; ++y) {
|
||||||
|
for (uint32_t x = 0; x < width; ++x) {
|
||||||
|
uint32_t idx = y * width + x;
|
||||||
|
uint32_t off = idx * 4;
|
||||||
|
pixels[idx] = Rgba(image[off], image[off + 1], image[off + 2], image[off + 3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "gfx/main.hpp"
|
#include "gfx/main.hpp"
|
||||||
#include "gfx/pal_packing.hpp"
|
#include "gfx/pal_packing.hpp"
|
||||||
#include "gfx/pal_sorting.hpp"
|
#include "gfx/pal_sorting.hpp"
|
||||||
|
#include "gfx/png.hpp"
|
||||||
#include "gfx/proto_palette.hpp"
|
#include "gfx/proto_palette.hpp"
|
||||||
#include "gfx/warning.hpp"
|
#include "gfx/warning.hpp"
|
||||||
|
|
||||||
@@ -41,9 +42,10 @@ public:
|
|||||||
// color), then the other color is returned. Otherwise, `nullptr` is returned.
|
// color), then the other color is returned. Otherwise, `nullptr` is returned.
|
||||||
[[nodiscard]]
|
[[nodiscard]]
|
||||||
Rgba const *registerColor(Rgba const &rgba) {
|
Rgba const *registerColor(Rgba const &rgba) {
|
||||||
std::optional<Rgba> &slot = _colors[rgba.cgbColor()];
|
uint16_t color = rgba.cgbColor();
|
||||||
|
std::optional<Rgba> &slot = _colors[color];
|
||||||
|
|
||||||
if (rgba.cgbColor() == Rgba::transparent && !isBgColorTransparent()) {
|
if (color == Rgba::transparent && !isBgColorTransparent()) {
|
||||||
options.hasTransparentPixels = true;
|
options.hasTransparentPixels = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,70 +69,12 @@ public:
|
|||||||
auto end() const { return _colors.end(); }
|
auto end() const { return _colors.end(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
class Png {
|
struct Image {
|
||||||
std::string const &path;
|
Png png{};
|
||||||
File file{};
|
ImagePalette colors{};
|
||||||
png_structp png = nullptr;
|
|
||||||
png_infop info = nullptr;
|
|
||||||
|
|
||||||
// These are cached for speed
|
Rgba &pixel(uint32_t x, uint32_t y) { return png.pixels[y * png.width + x]; }
|
||||||
uint32_t width, height;
|
Rgba const &pixel(uint32_t x, uint32_t y) const { return png.pixels[y * png.width + x]; }
|
||||||
std::vector<Rgba> pixels;
|
|
||||||
ImagePalette colors;
|
|
||||||
int colorType;
|
|
||||||
int nbColors;
|
|
||||||
png_colorp embeddedPal = nullptr;
|
|
||||||
int nbTransparentEntries;
|
|
||||||
png_bytep transparencyPal = nullptr;
|
|
||||||
|
|
||||||
[[noreturn]]
|
|
||||||
static void handleError(png_structp png, char const *msg) {
|
|
||||||
Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
|
|
||||||
|
|
||||||
fatal("Error reading input image (\"%s\"): %s", self->c_str(), msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handleWarning(png_structp png, char const *msg) {
|
|
||||||
Png *self = reinterpret_cast<Png *>(png_get_error_ptr(png));
|
|
||||||
|
|
||||||
warnx("In input image (\"%s\"): %s", self->c_str(), msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void readData(png_structp png, png_bytep data, size_t length) {
|
|
||||||
Png *self = reinterpret_cast<Png *>(png_get_io_ptr(png));
|
|
||||||
std::streamsize expectedLen = length;
|
|
||||||
std::streamsize nbBytesRead =
|
|
||||||
self->file->sgetn(reinterpret_cast<char *>(data), expectedLen);
|
|
||||||
|
|
||||||
if (nbBytesRead != expectedLen) {
|
|
||||||
fatal(
|
|
||||||
"Error reading input image (\"%s\"): file too short (expected at least %zd more "
|
|
||||||
"bytes after reading %zu)",
|
|
||||||
self->c_str(),
|
|
||||||
length - nbBytesRead,
|
|
||||||
static_cast<size_t>(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, int, png_bytep> getEmbeddedPal() const {
|
|
||||||
return {nbColors, embeddedPal, nbTransparentEntries, transparencyPal};
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t getWidth() const { return width; }
|
|
||||||
|
|
||||||
uint32_t getHeight() const { return height; }
|
|
||||||
|
|
||||||
Rgba &pixel(uint32_t x, uint32_t y) { return pixels[y * width + x]; }
|
|
||||||
|
|
||||||
Rgba const &pixel(uint32_t x, uint32_t y) const { return pixels[y * width + x]; }
|
|
||||||
|
|
||||||
char const *c_str() const { return file.c_str(path); }
|
|
||||||
|
|
||||||
bool isSuitableForGrayscale() const {
|
bool isSuitableForGrayscale() const {
|
||||||
// Check that all of the grays don't fall into the same "bin"
|
// Check that all of the grays don't fall into the same "bin"
|
||||||
@@ -170,62 +114,22 @@ public:
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads a PNG and notes all of its colors
|
explicit Image(std::string const &path) {
|
||||||
//
|
File input;
|
||||||
// This code is more complicated than strictly necessary, but that's because of the API
|
if (input.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) {
|
||||||
// being used: the "high-level" interface doesn't provide all the transformations we need,
|
fatal("Failed to open input image (\"%s\"): %s", input.c_str(path), strerror(errno));
|
||||||
// so we use the "lower-level" one instead.
|
|
||||||
// We also use that occasion to only read the PNG one line at a time, since we store all of
|
|
||||||
// the pixel data in `pixels`, which saves on memory allocations.
|
|
||||||
explicit Png(std::string const &filePath) : path(filePath), colors() {
|
|
||||||
if (file.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) {
|
|
||||||
fatal("Failed to open input image (\"%s\"): %s", file.c_str(path), strerror(errno));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
options.verbosePrint(Options::VERB_LOG_ACT, "Opened input file\n");
|
png = Png(input.c_str(path), *input);
|
||||||
|
|
||||||
std::array<unsigned char, 8> pngHeader;
|
// Validate input slice
|
||||||
if (file->sgetn(reinterpret_cast<char *>(pngHeader.data()), pngHeader.size())
|
if (options.inputSlice.width == 0 && png.width % 8 != 0) {
|
||||||
!= static_cast<std::streamsize>(pngHeader.size()) // Not enough bytes?
|
fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", png.width);
|
||||||
|| png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) {
|
|
||||||
fatal("Input file (\"%s\") is not a PNG image!", file.c_str(path));
|
|
||||||
}
|
}
|
||||||
|
if (options.inputSlice.height == 0 && png.height % 8 != 0) {
|
||||||
options.verbosePrint(Options::VERB_INTERM, "PNG header signature is OK\n");
|
fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", png.height);
|
||||||
|
|
||||||
png = png_create_read_struct(
|
|
||||||
PNG_LIBPNG_VER_STRING, static_cast<png_voidp>(this), handleError, handleWarning
|
|
||||||
);
|
|
||||||
if (!png) {
|
|
||||||
fatal("Failed to create PNG read structure: %s", strerror(errno)); // LCOV_EXCL_LINE
|
|
||||||
}
|
}
|
||||||
|
if (options.inputSlice.right() > png.width || options.inputSlice.bottom() > png.height) {
|
||||||
info = png_create_info_struct(png);
|
|
||||||
if (!info) {
|
|
||||||
// LCOV_EXCL_START
|
|
||||||
png_destroy_read_struct(&png, nullptr, nullptr);
|
|
||||||
fatal("Failed to create PNG info structure: %s", strerror(errno));
|
|
||||||
// LCOV_EXCL_STOP
|
|
||||||
}
|
|
||||||
|
|
||||||
png_set_read_fn(png, this, readData);
|
|
||||||
png_set_sig_bytes(png, pngHeader.size());
|
|
||||||
|
|
||||||
// Process all chunks up to but not including the image data
|
|
||||||
png_read_info(png, info);
|
|
||||||
|
|
||||||
int bitDepth, interlaceType;
|
|
||||||
png_get_IHDR(
|
|
||||||
png, info, &width, &height, &bitDepth, &colorType, &interlaceType, nullptr, nullptr
|
|
||||||
);
|
|
||||||
|
|
||||||
if (options.inputSlice.width == 0 && width % 8 != 0) {
|
|
||||||
fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", width);
|
|
||||||
}
|
|
||||||
if (options.inputSlice.height == 0 && height % 8 != 0) {
|
|
||||||
fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", height);
|
|
||||||
}
|
|
||||||
if (options.inputSlice.right() > width || options.inputSlice.bottom() > height) {
|
|
||||||
error(
|
error(
|
||||||
"Image slice ((%" PRIu16 ", %" PRIu16 ") to (%" PRIu32 ", %" PRIu32
|
"Image slice ((%" PRIu16 ", %" PRIu16 ") to (%" PRIu32 ", %" PRIu32
|
||||||
")) is outside the image bounds (%" PRIu32 "x%" PRIu32 ")!",
|
")) is outside the image bounds (%" PRIu32 "x%" PRIu32 ")!",
|
||||||
@@ -233,8 +137,8 @@ public:
|
|||||||
options.inputSlice.top,
|
options.inputSlice.top,
|
||||||
options.inputSlice.right(),
|
options.inputSlice.right(),
|
||||||
options.inputSlice.bottom(),
|
options.inputSlice.bottom(),
|
||||||
width,
|
png.width,
|
||||||
height
|
png.height
|
||||||
);
|
);
|
||||||
if (options.inputSlice.width % 8 == 0 && options.inputSlice.height % 8 == 0) {
|
if (options.inputSlice.width % 8 == 0 && options.inputSlice.height % 8 == 0) {
|
||||||
fprintf(
|
fprintf(
|
||||||
@@ -250,111 +154,6 @@ public:
|
|||||||
giveUp();
|
giveUp();
|
||||||
}
|
}
|
||||||
|
|
||||||
pixels.resize(static_cast<size_t>(width) * static_cast<size_t>(height));
|
|
||||||
|
|
||||||
auto colorTypeName = [this]() {
|
|
||||||
switch (colorType) {
|
|
||||||
case PNG_COLOR_TYPE_GRAY:
|
|
||||||
return "grayscale";
|
|
||||||
case PNG_COLOR_TYPE_GRAY_ALPHA:
|
|
||||||
return "grayscale + alpha";
|
|
||||||
case PNG_COLOR_TYPE_PALETTE:
|
|
||||||
return "palette";
|
|
||||||
case PNG_COLOR_TYPE_RGB:
|
|
||||||
return "RGB";
|
|
||||||
case PNG_COLOR_TYPE_RGB_ALPHA:
|
|
||||||
return "RGB + alpha";
|
|
||||||
default:
|
|
||||||
fatal("Unknown color type %d", colorType);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
auto interlaceTypeName = [&interlaceType]() {
|
|
||||||
switch (interlaceType) {
|
|
||||||
case PNG_INTERLACE_NONE:
|
|
||||||
return "not interlaced";
|
|
||||||
case PNG_INTERLACE_ADAM7:
|
|
||||||
return "interlaced (Adam7)";
|
|
||||||
default:
|
|
||||||
fatal("Unknown interlace type %d", interlaceType);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
options.verbosePrint(
|
|
||||||
Options::VERB_INTERM,
|
|
||||||
"Input image: %" PRIu32 "x%" PRIu32 " pixels, %dbpp %s, %s\n",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
bitDepth,
|
|
||||||
colorTypeName(),
|
|
||||||
interlaceTypeName()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (png_get_PLTE(png, info, &embeddedPal, &nbColors) != 0) {
|
|
||||||
if (png_get_tRNS(png, info, &transparencyPal, &nbTransparentEntries, nullptr)) {
|
|
||||||
assume(nbTransparentEntries <= nbColors);
|
|
||||||
}
|
|
||||||
|
|
||||||
options.verbosePrint(
|
|
||||||
Options::VERB_INTERM, "Embedded palette has %d colors: [", nbColors
|
|
||||||
);
|
|
||||||
for (int i = 0; i < nbColors; ++i) {
|
|
||||||
png_color const &color = embeddedPal[i];
|
|
||||||
options.verbosePrint(
|
|
||||||
Options::VERB_INTERM,
|
|
||||||
"#%02x%02x%02x%02x%s",
|
|
||||||
color.red,
|
|
||||||
color.green,
|
|
||||||
color.blue,
|
|
||||||
transparencyPal && i < nbTransparentEntries ? transparencyPal[i] : 0xFF,
|
|
||||||
i != nbColors - 1 ? ", " : "]\n"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
options.verbosePrint(Options::VERB_INTERM, "No embedded palette\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up transformations to turn everything into RGBA888 for simplicity of handling
|
|
||||||
|
|
||||||
// Convert grayscale to RGB
|
|
||||||
switch (colorType & ~PNG_COLOR_MASK_ALPHA) {
|
|
||||||
case PNG_COLOR_TYPE_GRAY:
|
|
||||||
png_set_gray_to_rgb(png); // This also converts tRNS to alpha
|
|
||||||
break;
|
|
||||||
case PNG_COLOR_TYPE_PALETTE:
|
|
||||||
png_set_palette_to_rgb(png);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (png_get_valid(png, info, PNG_INFO_tRNS)) {
|
|
||||||
// If we read a tRNS chunk, convert it to alpha
|
|
||||||
png_set_tRNS_to_alpha(png);
|
|
||||||
} else if (!(colorType & PNG_COLOR_MASK_ALPHA)) {
|
|
||||||
// Otherwise, if we lack an alpha channel, default to full opacity
|
|
||||||
png_set_add_alpha(png, 0xFFFF, PNG_FILLER_AFTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale 16bpp back to 8 (we don't need all of that precision anyway)
|
|
||||||
if (bitDepth == 16) {
|
|
||||||
png_set_scale_16(png);
|
|
||||||
} else if (bitDepth < 8) {
|
|
||||||
png_set_packing(png);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do NOT call `png_set_interlace_handling`. We want to expand the rows ourselves.
|
|
||||||
|
|
||||||
// Update `info` with the transformations
|
|
||||||
png_read_update_info(png, info);
|
|
||||||
// These shouldn't have changed
|
|
||||||
assume(png_get_image_width(png, info) == width);
|
|
||||||
assume(png_get_image_height(png, info) == height);
|
|
||||||
// These should have changed, however
|
|
||||||
assume(png_get_color_type(png, info) == PNG_COLOR_TYPE_RGBA);
|
|
||||||
assume(png_get_bit_depth(png, info) == 8);
|
|
||||||
|
|
||||||
// Now that metadata has been read, we can process the image data
|
|
||||||
|
|
||||||
size_t nbRowBytes = png_get_rowbytes(png, info);
|
|
||||||
assume(nbRowBytes != 0);
|
|
||||||
std::vector<png_byte> row(nbRowBytes);
|
|
||||||
// Holds known-conflicting color pairs to avoid warning about them twice.
|
// Holds known-conflicting color pairs to avoid warning about them twice.
|
||||||
// We don't need to worry about transitivity, as ImagePalette slots are immutable once
|
// We don't need to worry about transitivity, as ImagePalette slots are immutable once
|
||||||
// assigned, and conflicts always occur between that and another color.
|
// assigned, and conflicts always occur between that and another color.
|
||||||
@@ -363,103 +162,67 @@ public:
|
|||||||
// Holds colors whose alpha value is ambiguous
|
// Holds colors whose alpha value is ambiguous
|
||||||
std::vector<uint32_t> indeterminates;
|
std::vector<uint32_t> indeterminates;
|
||||||
|
|
||||||
// Assign a color to the given position, and register it in the image palette as well
|
// Register colors from image
|
||||||
auto assignColor = [&](png_uint_32 x, png_uint_32 y, Rgba &&color) {
|
for (uint32_t y = 0; y < png.height; ++y) {
|
||||||
if (!color.isTransparent() && !color.isOpaque()) {
|
for (uint32_t x = 0; x < png.width; ++x) {
|
||||||
uint32_t css = color.toCSS();
|
Rgba const &color = pixel(x, y);
|
||||||
if (std::find(RANGE(indeterminates), css) == indeterminates.end()) {
|
|
||||||
error(
|
|
||||||
"Color #%08x is neither transparent (alpha < %u) nor opaque (alpha >= "
|
|
||||||
"%u) [first seen at x: %" PRIu32 ", y: %" PRIu32 "]",
|
|
||||||
css,
|
|
||||||
Rgba::transparency_threshold,
|
|
||||||
Rgba::opacity_threshold,
|
|
||||||
x,
|
|
||||||
y
|
|
||||||
);
|
|
||||||
indeterminates.push_back(css);
|
|
||||||
}
|
|
||||||
} else if (Rgba const *other = colors.registerColor(color); other) {
|
|
||||||
std::tuple conflicting{color.toCSS(), other->toCSS()};
|
|
||||||
// Do not report combinations twice
|
|
||||||
if (std::find(RANGE(conflicts), conflicting) == conflicts.end()) {
|
|
||||||
warnx(
|
|
||||||
"Fusing colors #%08x and #%08x into Game Boy color $%04x [first seen "
|
|
||||||
"at x: %" PRIu32 ", y: %" PRIu32 "]",
|
|
||||||
std::get<0>(conflicting),
|
|
||||||
std::get<1>(conflicting),
|
|
||||||
color.cgbColor(),
|
|
||||||
x,
|
|
||||||
y
|
|
||||||
);
|
|
||||||
// Do not report this combination again
|
|
||||||
conflicts.emplace_back(conflicting);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pixel(x, y) = color;
|
// Assign a color to the given position, and register it in the image palette
|
||||||
};
|
if (color.isTransparent() == color.isOpaque()) {
|
||||||
|
uint32_t css = color.toCSS();
|
||||||
if (interlaceType == PNG_INTERLACE_NONE) {
|
if (std::find(RANGE(indeterminates), css) == indeterminates.end()) {
|
||||||
for (png_uint_32 y = 0; y < height; ++y) {
|
error(
|
||||||
png_bytep ptr = row.data();
|
"Color #%08x is neither transparent (alpha < %u) nor opaque (alpha >= "
|
||||||
png_read_row(png, ptr, nullptr);
|
"%u) [first seen at x: %" PRIu32 ", y: %" PRIu32 "]",
|
||||||
|
css,
|
||||||
for (png_uint_32 x = 0; x < width; ++x) {
|
Rgba::transparency_threshold,
|
||||||
assignColor(x, y, Rgba(ptr[0], ptr[1], ptr[2], ptr[3]));
|
Rgba::opacity_threshold,
|
||||||
ptr += 4;
|
x,
|
||||||
}
|
y
|
||||||
}
|
);
|
||||||
} else {
|
indeterminates.push_back(css);
|
||||||
assume(interlaceType == PNG_INTERLACE_ADAM7);
|
}
|
||||||
|
} else if (Rgba const *other = colors.registerColor(color); other) {
|
||||||
// For interlace to work properly, we must read the image `nbPasses` times
|
std::tuple conflicting{color.toCSS(), other->toCSS()};
|
||||||
for (int pass = 0; pass < PNG_INTERLACE_ADAM7_PASSES; ++pass) {
|
// Do not report combinations twice
|
||||||
// The interlacing pass must be skipped if its width or height is reported as zero
|
if (std::find(RANGE(conflicts), conflicting) == conflicts.end()) {
|
||||||
if (PNG_PASS_COLS(width, pass) == 0 || PNG_PASS_ROWS(height, pass) == 0) {
|
warnx(
|
||||||
continue;
|
"Fusing colors #%08x and #%08x into Game Boy color $%04x [first seen "
|
||||||
}
|
"at x: %" PRIu32 ", y: %" PRIu32 "]",
|
||||||
|
std::get<0>(conflicting),
|
||||||
png_uint_32 xStep = 1u << PNG_PASS_COL_SHIFT(pass);
|
std::get<1>(conflicting),
|
||||||
png_uint_32 yStep = 1u << PNG_PASS_ROW_SHIFT(pass);
|
color.cgbColor(),
|
||||||
|
x,
|
||||||
for (png_uint_32 y = PNG_PASS_START_ROW(pass); y < height; y += yStep) {
|
y
|
||||||
png_bytep ptr = row.data();
|
);
|
||||||
png_read_row(png, ptr, nullptr);
|
// Do not report this combination again
|
||||||
|
conflicts.emplace_back(conflicting);
|
||||||
for (png_uint_32 x = PNG_PASS_START_COL(pass); x < width; x += xStep) {
|
|
||||||
assignColor(x, y, Rgba(ptr[0], ptr[1], ptr[2], ptr[3]));
|
|
||||||
ptr += 4;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We don't care about chunks after the image data (comments, etc.)
|
|
||||||
png_read_end(png, nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
~Png() { png_destroy_read_struct(&png, &info, nullptr); }
|
|
||||||
|
|
||||||
class TilesVisitor {
|
class TilesVisitor {
|
||||||
Png const &_png;
|
Image const &_image;
|
||||||
bool const _columnMajor;
|
bool const _columnMajor;
|
||||||
uint32_t const _width, _height;
|
uint32_t const _width, _height;
|
||||||
uint32_t const _limit = _columnMajor ? _height : _width;
|
uint32_t const _limit = _columnMajor ? _height : _width;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TilesVisitor(Png const &png, bool columnMajor, uint32_t width, uint32_t height)
|
TilesVisitor(Image const &image, bool columnMajor, uint32_t width, uint32_t height)
|
||||||
: _png(png), _columnMajor(columnMajor), _width(width), _height(height) {}
|
: _image(image), _columnMajor(columnMajor), _width(width), _height(height) {}
|
||||||
|
|
||||||
class Tile {
|
class Tile {
|
||||||
Png const &_png;
|
Image const &_image;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
uint32_t const x, y;
|
uint32_t const x, y;
|
||||||
|
|
||||||
Tile(Png const &png, uint32_t x_, uint32_t y_) : _png(png), x(x_), y(y_) {}
|
Tile(Image const &image, uint32_t x_, uint32_t y_) : _image(image), x(x_), y(y_) {}
|
||||||
|
|
||||||
Rgba pixel(uint32_t xOfs, uint32_t yOfs) const {
|
Rgba pixel(uint32_t xOfs, uint32_t yOfs) const {
|
||||||
return _png.pixel(x + xOfs, y + yOfs);
|
return _image.pixel(x + xOfs, y + yOfs);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -473,7 +236,7 @@ public:
|
|||||||
return {x + options.inputSlice.left, y + options.inputSlice.top};
|
return {x + options.inputSlice.left, y + options.inputSlice.top};
|
||||||
}
|
}
|
||||||
Tile operator*() const {
|
Tile operator*() const {
|
||||||
return {parent._png, x + options.inputSlice.left, y + options.inputSlice.top};
|
return {parent._image, x + options.inputSlice.left, y + options.inputSlice.top};
|
||||||
}
|
}
|
||||||
|
|
||||||
Iterator &operator++() {
|
Iterator &operator++() {
|
||||||
@@ -496,13 +259,14 @@ public:
|
|||||||
return ++it; // ...now one-past-last!
|
return ++it; // ...now one-past-last!
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TilesVisitor visitAsTiles() const {
|
TilesVisitor visitAsTiles() const {
|
||||||
return {
|
return {
|
||||||
*this,
|
*this,
|
||||||
options.columnMajor,
|
options.columnMajor,
|
||||||
options.inputSlice.width ? options.inputSlice.width * 8 : width,
|
options.inputSlice.width ? options.inputSlice.width * 8 : png.width,
|
||||||
options.inputSlice.height ? options.inputSlice.height * 8 : height,
|
options.inputSlice.height ? options.inputSlice.height * 8 : png.height,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -522,10 +286,7 @@ private:
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
// Creates a new raw tile, and returns a reference to it so it can be filled in
|
// Creates a new raw tile, and returns a reference to it so it can be filled in
|
||||||
RawTile &newTile() {
|
RawTile &newTile() { return _tiles.emplace_back(); }
|
||||||
_tiles.emplace_back();
|
|
||||||
return _tiles.back();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AttrmapEntry {
|
struct AttrmapEntry {
|
||||||
@@ -547,32 +308,30 @@ struct AttrmapEntry {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static void generatePalSpec(Png const &png) {
|
static void generatePalSpec(Image const &image) {
|
||||||
// Generate a palette spec from the first few colors in the embedded palette
|
// Generate a palette spec from the first few colors in the embedded palette
|
||||||
auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal();
|
std::vector<Rgba> const &embPal = image.png.palette;
|
||||||
if (embPalRGB == nullptr) {
|
if (embPal.empty()) {
|
||||||
fatal("`-c embedded` was given, but the PNG does not have an embedded palette!");
|
fatal("`-c embedded` was given, but the PNG does not have an embedded palette!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ignore extraneous colors if they are unused
|
||||||
|
size_t nbColors = embPal.size();
|
||||||
|
if (nbColors > options.maxOpaqueColors()) {
|
||||||
|
nbColors = options.maxOpaqueColors();
|
||||||
|
}
|
||||||
|
|
||||||
// Fill in the palette spec
|
// Fill in the palette spec
|
||||||
options.palSpec.clear();
|
options.palSpec.clear();
|
||||||
options.palSpec.emplace_back(); // A single palette, with `#00000000`s (transparent)
|
auto &palette = options.palSpec.emplace_back();
|
||||||
assume(options.palSpec.size() == 1);
|
assume(nbColors <= palette.size());
|
||||||
if (embPalSize > options.maxOpaqueColors()) { // Ignore extraneous colors if they are unused
|
for (size_t i = 0; i < nbColors; ++i) {
|
||||||
embPalSize = options.maxOpaqueColors();
|
palette[i] = embPal[i];
|
||||||
}
|
|
||||||
for (int i = 0; i < embPalSize; ++i) {
|
|
||||||
options.palSpec[0][i] = Rgba(
|
|
||||||
embPalRGB[i].red,
|
|
||||||
embPalRGB[i].green,
|
|
||||||
embPalRGB[i].blue,
|
|
||||||
embPalAlpha && i < embPalAlphaSize ? embPalAlpha[i] : 0xFF
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::tuple<std::vector<size_t>, std::vector<Palette>>
|
static std::tuple<std::vector<size_t>, std::vector<Palette>>
|
||||||
generatePalettes(std::vector<ProtoPalette> const &protoPalettes, Png const &png) {
|
generatePalettes(std::vector<ProtoPalette> const &protoPalettes, Image const &image) {
|
||||||
// Run a "pagination" problem solver
|
// Run a "pagination" problem solver
|
||||||
auto [mappings, nbPalettes] = overloadAndRemove(protoPalettes);
|
auto [mappings, nbPalettes] = overloadAndRemove(protoPalettes);
|
||||||
assume(mappings.size() == protoPalettes.size());
|
assume(mappings.size() == protoPalettes.size());
|
||||||
@@ -609,16 +368,15 @@ static std::tuple<std::vector<size_t>, std::vector<Palette>>
|
|||||||
|
|
||||||
// "Sort" colors in the generated palettes, see the man page for the flowchart
|
// "Sort" colors in the generated palettes, see the man page for the flowchart
|
||||||
if (options.palSpecType == Options::DMG) {
|
if (options.palSpecType == Options::DMG) {
|
||||||
sortGrayscale(palettes, png.getColors().raw());
|
sortGrayscale(palettes, image.colors.raw());
|
||||||
} else if (auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal();
|
} else if (!image.png.palette.empty()) {
|
||||||
embPalRGB != nullptr) {
|
|
||||||
warning(
|
warning(
|
||||||
WARNING_EMBEDDED,
|
WARNING_EMBEDDED,
|
||||||
"Sorting palette colors by PNG's embedded PLTE chunk without '-c/--colors embedded'"
|
"Sorting palette colors by PNG's embedded PLTE chunk without '-c/--colors embedded'"
|
||||||
);
|
);
|
||||||
sortIndexed(palettes, embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha);
|
sortIndexed(palettes, image.png.palette);
|
||||||
} else if (png.isSuitableForGrayscale()) {
|
} else if (image.isSuitableForGrayscale()) {
|
||||||
sortGrayscale(palettes, png.getColors().raw());
|
sortGrayscale(palettes, image.colors.raw());
|
||||||
} else {
|
} else {
|
||||||
sortRgb(palettes);
|
sortRgb(palettes);
|
||||||
}
|
}
|
||||||
@@ -641,8 +399,8 @@ static std::tuple<std::vector<size_t>, std::vector<Palette>>
|
|||||||
auto listColors = [](auto const &list) {
|
auto listColors = [](auto const &list) {
|
||||||
static char buf[sizeof(", $XXXX, $XXXX, $XXXX, $XXXX")];
|
static char buf[sizeof(", $XXXX, $XXXX, $XXXX, $XXXX")];
|
||||||
char *ptr = buf;
|
char *ptr = buf;
|
||||||
for (uint16_t cgbColor : list) {
|
for (uint16_t color : list) {
|
||||||
ptr += snprintf(ptr, sizeof(", $XXXX"), ", $%04x", cgbColor);
|
ptr += snprintf(ptr, sizeof(", $XXXX"), ", $%04x", color);
|
||||||
}
|
}
|
||||||
return &buf[literal_strlen(", ")];
|
return &buf[literal_strlen(", ")];
|
||||||
};
|
};
|
||||||
@@ -753,7 +511,7 @@ public:
|
|||||||
mutable uint16_t tileID;
|
mutable uint16_t tileID;
|
||||||
|
|
||||||
static uint16_t
|
static uint16_t
|
||||||
rowBitplanes(Png::TilesVisitor::Tile const &tile, Palette const &palette, uint32_t y) {
|
rowBitplanes(Image::TilesVisitor::Tile const &tile, Palette const &palette, uint32_t y) {
|
||||||
uint16_t row = 0;
|
uint16_t row = 0;
|
||||||
for (uint32_t x = 0; x < 8; ++x) {
|
for (uint32_t x = 0; x < 8; ++x) {
|
||||||
row <<= 1;
|
row <<= 1;
|
||||||
@@ -776,7 +534,7 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TileData(Png::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) {
|
TileData(Image::TilesVisitor::Tile const &tile, Palette const &palette) : _hash(0) {
|
||||||
size_t writeIndex = 0;
|
size_t writeIndex = 0;
|
||||||
for (uint32_t y = 0; y < 8; ++y) {
|
for (uint32_t y = 0; y < 8; ++y) {
|
||||||
uint16_t bitplanes = rowBitplanes(tile, palette, y);
|
uint16_t bitplanes = rowBitplanes(tile, palette, y);
|
||||||
@@ -856,7 +614,7 @@ struct std::hash<TileData> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static void outputUnoptimizedTileData(
|
static void outputUnoptimizedTileData(
|
||||||
Png const &png,
|
Image const &image,
|
||||||
std::vector<AttrmapEntry> const &attrmap,
|
std::vector<AttrmapEntry> const &attrmap,
|
||||||
std::vector<Palette> const &palettes,
|
std::vector<Palette> const &palettes,
|
||||||
std::vector<size_t> const &mappings
|
std::vector<size_t> const &mappings
|
||||||
@@ -868,14 +626,14 @@ static void outputUnoptimizedTileData(
|
|||||||
// LCOV_EXCL_STOP
|
// LCOV_EXCL_STOP
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t widthTiles = options.inputSlice.width ? options.inputSlice.width : png.getWidth() / 8;
|
uint16_t widthTiles = options.inputSlice.width ? options.inputSlice.width : image.png.width / 8;
|
||||||
uint16_t heightTiles =
|
uint16_t heightTiles =
|
||||||
options.inputSlice.height ? options.inputSlice.height : png.getHeight() / 8;
|
options.inputSlice.height ? options.inputSlice.height : image.png.height / 8;
|
||||||
uint64_t nbTiles = widthTiles * heightTiles;
|
uint64_t nbTiles = widthTiles * heightTiles;
|
||||||
uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
|
uint64_t nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 0;
|
||||||
uint64_t tileIdx = 0;
|
uint64_t tileIdx = 0;
|
||||||
|
|
||||||
for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) {
|
for (auto [tile, attr] : zip(image.visitAsTiles(), attrmap)) {
|
||||||
// Do not emit fully-background tiles.
|
// Do not emit fully-background tiles.
|
||||||
if (attr.isBackgroundTile()) {
|
if (attr.isBackgroundTile()) {
|
||||||
++tileIdx;
|
++tileIdx;
|
||||||
@@ -993,7 +751,7 @@ struct UniqueTiles {
|
|||||||
// 8-bit tile IDs + the bank bit; this will save the work when we output the data later (potentially
|
// 8-bit tile IDs + the bank bit; this will save the work when we output the data later (potentially
|
||||||
// twice)
|
// twice)
|
||||||
static UniqueTiles dedupTiles(
|
static UniqueTiles dedupTiles(
|
||||||
Png const &png,
|
Image const &image,
|
||||||
std::vector<AttrmapEntry> &attrmap,
|
std::vector<AttrmapEntry> &attrmap,
|
||||||
std::vector<Palette> const &palettes,
|
std::vector<Palette> const &palettes,
|
||||||
std::vector<size_t> const &mappings
|
std::vector<size_t> const &mappings
|
||||||
@@ -1044,7 +802,7 @@ static UniqueTiles dedupTiles(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty();
|
bool inputWithoutOutput = !options.inputTileset.empty() && options.output.empty();
|
||||||
for (auto [tile, attr] : zip(png.visitAsTiles(), attrmap)) {
|
for (auto [tile, attr] : zip(image.visitAsTiles(), attrmap)) {
|
||||||
if (attr.isBackgroundTile()) {
|
if (attr.isBackgroundTile()) {
|
||||||
attr.xFlip = false;
|
attr.xFlip = false;
|
||||||
attr.yFlip = false;
|
attr.yFlip = false;
|
||||||
@@ -1157,16 +915,12 @@ void process() {
|
|||||||
options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
|
options.verbosePrint(Options::VERB_CFG, "Using libpng %s\n", png_get_libpng_ver(nullptr));
|
||||||
|
|
||||||
options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
|
options.verbosePrint(Options::VERB_LOG_ACT, "Reading tiles...\n");
|
||||||
Png png(options.input); // This also sets `hasTransparentPixels` as a side effect
|
Image image(options.input); // This also sets `hasTransparentPixels` as a side effect
|
||||||
ImagePalette const &colors = png.getColors();
|
|
||||||
|
|
||||||
// Now, we have all the image's colors in `colors`
|
|
||||||
// The next step is to order the palette
|
|
||||||
|
|
||||||
// LCOV_EXCL_START
|
// LCOV_EXCL_START
|
||||||
if (options.verbosity >= Options::VERB_INTERM) {
|
if (options.verbosity >= Options::VERB_INTERM) {
|
||||||
fputs("Image colors: [ ", stderr);
|
fputs("Image colors: [ ", stderr);
|
||||||
for (std::optional<Rgba> const &slot : colors) {
|
for (std::optional<Rgba> const &slot : image.colors) {
|
||||||
if (!slot.has_value()) {
|
if (!slot.has_value()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1182,7 +936,7 @@ void process() {
|
|||||||
"Image contains transparent pixels, not compatible with a DMG palette specification"
|
"Image contains transparent pixels, not compatible with a DMG palette specification"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!png.isSuitableForGrayscale()) {
|
if (!image.isSuitableForGrayscale()) {
|
||||||
fatal("Image contains too many or non-gray colors, not compatible with a DMG palette "
|
fatal("Image contains too many or non-gray colors, not compatible with a DMG palette "
|
||||||
"specification");
|
"specification");
|
||||||
}
|
}
|
||||||
@@ -1195,7 +949,7 @@ void process() {
|
|||||||
std::vector<ProtoPalette> protoPalettes;
|
std::vector<ProtoPalette> protoPalettes;
|
||||||
std::vector<AttrmapEntry> attrmap{};
|
std::vector<AttrmapEntry> attrmap{};
|
||||||
|
|
||||||
for (auto tile : png.visitAsTiles()) {
|
for (auto tile : image.visitAsTiles()) {
|
||||||
AttrmapEntry &attrs = attrmap.emplace_back();
|
AttrmapEntry &attrs = attrmap.emplace_back();
|
||||||
|
|
||||||
// Count the unique non-transparent colors for packing
|
// Count the unique non-transparent colors for packing
|
||||||
@@ -1227,8 +981,8 @@ void process() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ProtoPalette protoPalette;
|
ProtoPalette protoPalette;
|
||||||
for (uint16_t cgbColor : tileColors) {
|
for (uint16_t color : tileColors) {
|
||||||
protoPalette.add(cgbColor);
|
protoPalette.add(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.bgColor.has_value()
|
if (options.bgColor.has_value()
|
||||||
@@ -1295,17 +1049,17 @@ continue_visiting_tiles:;
|
|||||||
// LCOV_EXCL_STOP
|
// LCOV_EXCL_STOP
|
||||||
|
|
||||||
if (options.palSpecType == Options::EMBEDDED) {
|
if (options.palSpecType == Options::EMBEDDED) {
|
||||||
generatePalSpec(png);
|
generatePalSpec(image);
|
||||||
}
|
}
|
||||||
auto [mappings, palettes] =
|
auto [mappings, palettes] =
|
||||||
options.palSpecType == Options::NO_SPEC || options.palSpecType == Options::DMG
|
options.palSpecType == Options::NO_SPEC || options.palSpecType == Options::DMG
|
||||||
? generatePalettes(protoPalettes, png)
|
? generatePalettes(protoPalettes, image)
|
||||||
: makePalsAsSpecified(protoPalettes);
|
: makePalsAsSpecified(protoPalettes);
|
||||||
outputPalettes(palettes);
|
outputPalettes(palettes);
|
||||||
|
|
||||||
// If deduplication is not happening, we just need to output the tile data and/or maps as-is
|
// If deduplication is not happening, we just need to output the tile data and/or maps as-is
|
||||||
if (!options.allowDedup) {
|
if (!options.allowDedup) {
|
||||||
uint32_t const nbTilesH = png.getHeight() / 8, nbTilesW = png.getWidth() / 8;
|
uint32_t const nbTilesH = image.png.height / 8, nbTilesW = image.png.width / 8;
|
||||||
|
|
||||||
// Check the tile count
|
// Check the tile count
|
||||||
if (uint32_t nbTiles = nbTilesW * nbTilesH;
|
if (uint32_t nbTiles = nbTilesW * nbTilesH;
|
||||||
@@ -1326,7 +1080,7 @@ continue_visiting_tiles:;
|
|||||||
|
|
||||||
if (!options.output.empty()) {
|
if (!options.output.empty()) {
|
||||||
options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n");
|
options.verbosePrint(Options::VERB_LOG_ACT, "Generating unoptimized tile data...\n");
|
||||||
outputUnoptimizedTileData(png, attrmap, palettes, mappings);
|
outputUnoptimizedTileData(image, attrmap, palettes, mappings);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.tilemap.empty() || !options.attrmap.empty() || !options.palmap.empty()) {
|
if (!options.tilemap.empty() || !options.attrmap.empty() || !options.palmap.empty()) {
|
||||||
@@ -1339,7 +1093,7 @@ continue_visiting_tiles:;
|
|||||||
} else {
|
} else {
|
||||||
// All of these require the deduplication process to be performed to be output
|
// All of these require the deduplication process to be performed to be output
|
||||||
options.verbosePrint(Options::VERB_LOG_ACT, "Deduplicating tiles...\n");
|
options.verbosePrint(Options::VERB_LOG_ACT, "Deduplicating tiles...\n");
|
||||||
UniqueTiles tiles = dedupTiles(png, attrmap, palettes, mappings);
|
UniqueTiles tiles = dedupTiles(image, attrmap, palettes, mappings);
|
||||||
|
|
||||||
if (size_t nbTiles = tiles.size();
|
if (size_t nbTiles = tiles.size();
|
||||||
nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) {
|
nbTiles > options.maxNbTiles[0] + options.maxNbTiles[1]) {
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
FATAL: Error reading input image ("damaged1.png"): IDAT: invalid code -- missing end-of-block
|
FATAL: Error reading PNG image ("damaged1.png"): IDAT: invalid code -- missing end-of-block
|
||||||
Conversion aborted after 1 error
|
Conversion aborted after 1 error
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
FATAL: Error reading input image ("damaged2.png"): IDAT: invalid code -- missing end-of-block
|
FATAL: Error reading PNG image ("damaged2.png"): IDAT: invalid code -- missing end-of-block
|
||||||
Conversion aborted after 1 error
|
Conversion aborted after 1 error
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
FATAL: Error reading input image ("damaged9.png"): IDAT: invalid code -- missing end-of-block
|
FATAL: Error reading PNG image ("damaged9.png"): IDAT: invalid code -- missing end-of-block
|
||||||
Conversion aborted after 1 error
|
Conversion aborted after 1 error
|
||||||
|
|||||||
1
test/gfx/interlaced.flags
Normal file
1
test/gfx/interlaced.flags
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-m
|
||||||
BIN
test/gfx/interlaced.out.2bpp
Normal file
BIN
test/gfx/interlaced.out.2bpp
Normal file
Binary file not shown.
BIN
test/gfx/interlaced.out.attrmap
Normal file
BIN
test/gfx/interlaced.out.attrmap
Normal file
Binary file not shown.
BIN
test/gfx/interlaced.out.pal
Normal file
BIN
test/gfx/interlaced.out.pal
Normal file
Binary file not shown.
BIN
test/gfx/interlaced.out.tilemap
Normal file
BIN
test/gfx/interlaced.out.tilemap
Normal file
Binary file not shown.
BIN
test/gfx/interlaced.png
Normal file
BIN
test/gfx/interlaced.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 400 B |
Reference in New Issue
Block a user