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_sorting.o \
|
||||
src/gfx/pal_spec.o \
|
||||
src/gfx/png.o \
|
||||
src/gfx/process.o \
|
||||
src/gfx/proto_palette.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 $@ $<
|
||||
src/gfx/pal_spec.o: src/gfx/pal_spec.cpp
|
||||
$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
|
||||
$Q${CXX} ${REALCXXFLAGS} ${PNGCFLAGS} -c -o $@ $<
|
||||
src/gfx/proto_palette.o: src/gfx/proto_palette.cpp
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <png.h>
|
||||
#include <vector>
|
||||
|
||||
#include "gfx/rgba.hpp"
|
||||
@@ -16,13 +15,7 @@ static constexpr size_t NB_COLOR_SLOTS = (1 << (5 * 3)) + 1;
|
||||
|
||||
struct Palette;
|
||||
|
||||
void sortIndexed(
|
||||
std::vector<Palette> &palettes,
|
||||
int palSize,
|
||||
png_color const *palRGB,
|
||||
int palAlphaSize,
|
||||
png_byte *palAlpha
|
||||
);
|
||||
void sortIndexed(std::vector<Palette> &palettes, std::vector<Rgba> const &embPal);
|
||||
void sortGrayscale(
|
||||
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)
|
||||
: red(rgba >> 24), green(rgba >> 16), blue(rgba >> 8), alpha(rgba) {}
|
||||
|
||||
static constexpr Rgba fromCGBColor(uint16_t cgbColor) {
|
||||
constexpr auto _5to8 = [](uint8_t fiveBpp) -> uint8_t {
|
||||
fiveBpp &= 0b11111; // For caller's convenience
|
||||
return fiveBpp << 3 | fiveBpp >> 2;
|
||||
static constexpr Rgba fromCGBColor(uint16_t color) {
|
||||
constexpr auto _5to8 = [](uint8_t channel) -> uint8_t {
|
||||
channel &= 0b11111; // For caller's convenience
|
||||
return channel << 3 | channel >> 2;
|
||||
};
|
||||
return {
|
||||
_5to8(cgbColor),
|
||||
_5to8(cgbColor >> 5),
|
||||
_5to8(cgbColor >> 10),
|
||||
static_cast<uint8_t>(cgbColor & 0x8000 ? 0x00 : 0xFF),
|
||||
_5to8(color),
|
||||
_5to8(color >> 5),
|
||||
_5to8(color >> 10),
|
||||
static_cast<uint8_t>(color & 0x8000 ? 0x00 : 0xFF),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ set(rgbgfx_src
|
||||
"gfx/pal_packing.cpp"
|
||||
"gfx/pal_sorting.cpp"
|
||||
"gfx/pal_spec.cpp"
|
||||
"gfx/png.cpp"
|
||||
"gfx/process.cpp"
|
||||
"gfx/proto_palette.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();) {
|
||||
uint8_t rpndata = rpn[offset++];
|
||||
|
||||
auto getSymName = [&](){
|
||||
auto getSymName = [&]() {
|
||||
std::string symName;
|
||||
for (uint8_t c; (c = rpn[offset++]) != 0;) {
|
||||
symName += c;
|
||||
|
||||
@@ -8,27 +8,14 @@
|
||||
|
||||
#include "gfx/main.hpp"
|
||||
|
||||
void sortIndexed(
|
||||
std::vector<Palette> &palettes,
|
||||
int palSize,
|
||||
png_color const *palRGB,
|
||||
int palAlphaSize,
|
||||
png_byte *palAlpha
|
||||
) {
|
||||
void sortIndexed(std::vector<Palette> &palettes, std::vector<Rgba> const &embPal) {
|
||||
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) {
|
||||
std::sort(RANGE(pal), [&](uint16_t lhs, uint16_t rhs) {
|
||||
// Iterate through the PNG's palette, looking for either of the two
|
||||
for (int i = 0; i < palSize; ++i) {
|
||||
uint16_t color = pngToRgb(i).cgbColor();
|
||||
for (Rgba const &rgba : embPal) {
|
||||
uint16_t color = rgba.cgbColor();
|
||||
if (color == Rgba::transparent) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#include <inttypes.h>
|
||||
#include <limits.h>
|
||||
#include <optional>
|
||||
#include <png.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <streambuf>
|
||||
@@ -23,6 +22,7 @@
|
||||
#include "platform.hpp"
|
||||
|
||||
#include "gfx/main.hpp"
|
||||
#include "gfx/png.hpp"
|
||||
#include "gfx/warning.hpp"
|
||||
|
||||
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)};
|
||||
}
|
||||
|
||||
static void parsePSPFile(std::filebuf &file) {
|
||||
static void parsePSPFile(char const *, std::filebuf &file) {
|
||||
// https://www.selapa.net/swatches/colors/fileformats.php#psp_pal
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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`
|
||||
options.palSpec.clear();
|
||||
|
||||
@@ -579,41 +579,15 @@ static void parseGBCFile(std::filebuf &file) {
|
||||
}
|
||||
}
|
||||
|
||||
[[noreturn]]
|
||||
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;
|
||||
static bool checkPngSwatch(std::vector<Rgba> const &pixels, uint32_t base, uint32_t swatchSize) {
|
||||
for (uint32_t y = 0; y < swatchSize; y++) {
|
||||
uint32_t yOffset = y * swatchSize * options.nbColorsPerPal + base;
|
||||
|
||||
for (uint32_t x = 0; x < swatchSize; x++) {
|
||||
if (x == 0 && y == 0) {
|
||||
continue;
|
||||
}
|
||||
uint32_t offset = base + (y * rowFactor + x) * 4;
|
||||
Rgba pixel(image[offset], image[offset + 1], image[offset + 2], image[offset + 3]);
|
||||
if (pixel != topLeft) {
|
||||
if (pixels[yOffset + x] != pixels[base]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -621,135 +595,38 @@ static bool checkPngSwatch(std::vector<png_byte> const &image, uint32_t base, ui
|
||||
return true;
|
||||
}
|
||||
|
||||
static void parsePNGFile(std::filebuf &file) {
|
||||
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) {
|
||||
// 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());
|
||||
static void parsePNGFile(char const *filename, std::filebuf &file) {
|
||||
Png png{filename, file};
|
||||
|
||||
// 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(
|
||||
"PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu8
|
||||
" color swatches wide",
|
||||
width,
|
||||
height,
|
||||
png.width,
|
||||
png.height,
|
||||
options.nbColorsPerPal
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
if (height % swatchSize != 0) {
|
||||
if (png.height % swatchSize != 0) {
|
||||
error(
|
||||
"PNG palette file is %" PRIu32 "x%" PRIu32 ", which is not a multiple of %" PRIu32
|
||||
" pixels high",
|
||||
width,
|
||||
height,
|
||||
png.width,
|
||||
png.height,
|
||||
swatchSize
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
warnx(
|
||||
"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();
|
||||
|
||||
// Get each color from the top-left pixel of each swatch
|
||||
uint32_t colorFactor = swatchSize * 4;
|
||||
uint32_t palFactor = swatchSize * options.nbColorsPerPal;
|
||||
for (uint32_t palIdx = 0; palIdx < nbPals; ++palIdx) {
|
||||
for (uint32_t y = 0; y < nbPals; ++y) {
|
||||
uint32_t yOffset = y * swatchSize * swatchSize * options.nbColorsPerPal;
|
||||
options.palSpec.emplace_back();
|
||||
for (uint32_t colorIdx = 0; colorIdx < options.nbColorsPerPal; ++colorIdx) {
|
||||
std::optional<Rgba> &color = options.palSpec.back()[colorIdx];
|
||||
uint32_t offset = (palIdx * palFactor + colorIdx) * colorFactor;
|
||||
color = Rgba(image[offset], image[offset + 1], image[offset + 2], image[offset + 3]);
|
||||
|
||||
for (uint32_t x = 0; x < options.nbColorsPerPal; ++x) {
|
||||
uint32_t offset = yOffset + x * swatchSize;
|
||||
options.palSpec.back()[x] = png.pixels[offset];
|
||||
|
||||
// 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");
|
||||
return;
|
||||
}
|
||||
@@ -821,7 +697,7 @@ void parseExternalPalSpec(char const *arg) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::get<1> (*iter)(file);
|
||||
std::get<1> (*iter)(path, file);
|
||||
}
|
||||
|
||||
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/pal_packing.hpp"
|
||||
#include "gfx/pal_sorting.hpp"
|
||||
#include "gfx/png.hpp"
|
||||
#include "gfx/proto_palette.hpp"
|
||||
#include "gfx/warning.hpp"
|
||||
|
||||
@@ -41,9 +42,10 @@ public:
|
||||
// color), then the other color is returned. Otherwise, `nullptr` is returned.
|
||||
[[nodiscard]]
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -67,70 +69,12 @@ public:
|
||||
auto end() const { return _colors.end(); }
|
||||
};
|
||||
|
||||
class Png {
|
||||
std::string const &path;
|
||||
File file{};
|
||||
png_structp png = nullptr;
|
||||
png_infop info = nullptr;
|
||||
struct Image {
|
||||
Png png{};
|
||||
ImagePalette colors{};
|
||||
|
||||
// These are cached for speed
|
||||
uint32_t width, height;
|
||||
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); }
|
||||
Rgba &pixel(uint32_t x, uint32_t y) { return png.pixels[y * png.width + x]; }
|
||||
Rgba const &pixel(uint32_t x, uint32_t y) const { return png.pixels[y * png.width + x]; }
|
||||
|
||||
bool isSuitableForGrayscale() const {
|
||||
// Check that all of the grays don't fall into the same "bin"
|
||||
@@ -170,62 +114,22 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reads a PNG and notes all of its colors
|
||||
//
|
||||
// This code is more complicated than strictly necessary, but that's because of the API
|
||||
// being used: the "high-level" interface doesn't provide all the transformations we need,
|
||||
// so we use the "lower-level" one instead.
|
||||
// We also use that occasion to only read the PNG one line at a time, since we store all of
|
||||
// the pixel data in `pixels`, which saves on memory allocations.
|
||||
explicit Png(std::string const &filePath) : path(filePath), colors() {
|
||||
if (file.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) {
|
||||
fatal("Failed to open input image (\"%s\"): %s", file.c_str(path), strerror(errno));
|
||||
explicit Image(std::string const &path) {
|
||||
File input;
|
||||
if (input.open(path, std::ios_base::in | std::ios_base::binary) == nullptr) {
|
||||
fatal("Failed to open input image (\"%s\"): %s", input.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;
|
||||
if (file->sgetn(reinterpret_cast<char *>(pngHeader.data()), pngHeader.size())
|
||||
!= static_cast<std::streamsize>(pngHeader.size()) // Not enough bytes?
|
||||
|| png_sig_cmp(pngHeader.data(), 0, pngHeader.size()) != 0) {
|
||||
fatal("Input file (\"%s\") is not a PNG image!", file.c_str(path));
|
||||
// Validate input slice
|
||||
if (options.inputSlice.width == 0 && png.width % 8 != 0) {
|
||||
fatal("Image width (%" PRIu32 " pixels) is not a multiple of 8!", png.width);
|
||||
}
|
||||
|
||||
options.verbosePrint(Options::VERB_INTERM, "PNG header signature is OK\n");
|
||||
|
||||
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.height == 0 && png.height % 8 != 0) {
|
||||
fatal("Image height (%" PRIu32 " pixels) is not a multiple of 8!", 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) {
|
||||
if (options.inputSlice.right() > png.width || options.inputSlice.bottom() > png.height) {
|
||||
error(
|
||||
"Image slice ((%" PRIu16 ", %" PRIu16 ") to (%" PRIu32 ", %" PRIu32
|
||||
")) is outside the image bounds (%" PRIu32 "x%" PRIu32 ")!",
|
||||
@@ -233,8 +137,8 @@ public:
|
||||
options.inputSlice.top,
|
||||
options.inputSlice.right(),
|
||||
options.inputSlice.bottom(),
|
||||
width,
|
||||
height
|
||||
png.width,
|
||||
png.height
|
||||
);
|
||||
if (options.inputSlice.width % 8 == 0 && options.inputSlice.height % 8 == 0) {
|
||||
fprintf(
|
||||
@@ -250,111 +154,6 @@ public:
|
||||
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.
|
||||
// We don't need to worry about transitivity, as ImagePalette slots are immutable once
|
||||
// assigned, and conflicts always occur between that and another color.
|
||||
@@ -363,9 +162,13 @@ public:
|
||||
// Holds colors whose alpha value is ambiguous
|
||||
std::vector<uint32_t> indeterminates;
|
||||
|
||||
// Assign a color to the given position, and register it in the image palette as well
|
||||
auto assignColor = [&](png_uint_32 x, png_uint_32 y, Rgba &&color) {
|
||||
if (!color.isTransparent() && !color.isOpaque()) {
|
||||
// Register colors from image
|
||||
for (uint32_t y = 0; y < png.height; ++y) {
|
||||
for (uint32_t x = 0; x < png.width; ++x) {
|
||||
Rgba const &color = pixel(x, y);
|
||||
|
||||
// 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 (std::find(RANGE(indeterminates), css) == indeterminates.end()) {
|
||||
error(
|
||||
@@ -396,70 +199,30 @@ public:
|
||||
conflicts.emplace_back(conflicting);
|
||||
}
|
||||
}
|
||||
|
||||
pixel(x, y) = color;
|
||||
};
|
||||
|
||||
if (interlaceType == PNG_INTERLACE_NONE) {
|
||||
for (png_uint_32 y = 0; y < height; ++y) {
|
||||
png_bytep ptr = row.data();
|
||||
png_read_row(png, ptr, nullptr);
|
||||
|
||||
for (png_uint_32 x = 0; x < width; ++x) {
|
||||
assignColor(x, y, Rgba(ptr[0], ptr[1], ptr[2], ptr[3]));
|
||||
ptr += 4;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assume(interlaceType == PNG_INTERLACE_ADAM7);
|
||||
|
||||
// For interlace to work properly, we must read the image `nbPasses` times
|
||||
for (int pass = 0; pass < PNG_INTERLACE_ADAM7_PASSES; ++pass) {
|
||||
// The interlacing pass must be skipped if its width or height is reported as zero
|
||||
if (PNG_PASS_COLS(width, pass) == 0 || PNG_PASS_ROWS(height, pass) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
png_uint_32 xStep = 1u << PNG_PASS_COL_SHIFT(pass);
|
||||
png_uint_32 yStep = 1u << PNG_PASS_ROW_SHIFT(pass);
|
||||
|
||||
for (png_uint_32 y = PNG_PASS_START_ROW(pass); y < height; y += yStep) {
|
||||
png_bytep ptr = row.data();
|
||||
png_read_row(png, ptr, nullptr);
|
||||
|
||||
for (png_uint_32 x = PNG_PASS_START_COL(pass); x < width; x += xStep) {
|
||||
assignColor(x, y, Rgba(ptr[0], ptr[1], ptr[2], ptr[3]));
|
||||
ptr += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We don't care about chunks after the image data (comments, etc.)
|
||||
png_read_end(png, nullptr);
|
||||
}
|
||||
|
||||
~Png() { png_destroy_read_struct(&png, &info, nullptr); }
|
||||
|
||||
class TilesVisitor {
|
||||
Png const &_png;
|
||||
Image const &_image;
|
||||
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) {}
|
||||
TilesVisitor(Image const &image, bool columnMajor, uint32_t width, uint32_t height)
|
||||
: _image(image), _columnMajor(columnMajor), _width(width), _height(height) {}
|
||||
|
||||
class Tile {
|
||||
Png const &_png;
|
||||
Image const &_image;
|
||||
|
||||
public:
|
||||
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 {
|
||||
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};
|
||||
}
|
||||
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++() {
|
||||
@@ -496,13 +259,14 @@ public:
|
||||
return ++it; // ...now one-past-last!
|
||||
}
|
||||
};
|
||||
|
||||
public:
|
||||
TilesVisitor visitAsTiles() const {
|
||||
return {
|
||||
*this,
|
||||
options.columnMajor,
|
||||
options.inputSlice.width ? options.inputSlice.width * 8 : width,
|
||||
options.inputSlice.height ? options.inputSlice.height * 8 : height,
|
||||
options.inputSlice.width ? options.inputSlice.width * 8 : png.width,
|
||||
options.inputSlice.height ? options.inputSlice.height * 8 : png.height,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -522,10 +286,7 @@ private:
|
||||
|
||||
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();
|
||||
}
|
||||
RawTile &newTile() { return _tiles.emplace_back(); }
|
||||
};
|
||||
|
||||
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
|
||||
auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal();
|
||||
if (embPalRGB == nullptr) {
|
||||
std::vector<Rgba> const &embPal = image.png.palette;
|
||||
if (embPal.empty()) {
|
||||
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
|
||||
options.palSpec.clear();
|
||||
options.palSpec.emplace_back(); // A single palette, with `#00000000`s (transparent)
|
||||
assume(options.palSpec.size() == 1);
|
||||
if (embPalSize > options.maxOpaqueColors()) { // Ignore extraneous colors if they are unused
|
||||
embPalSize = options.maxOpaqueColors();
|
||||
}
|
||||
for (int i = 0; i < embPalSize; ++i) {
|
||||
options.palSpec[0][i] = Rgba(
|
||||
embPalRGB[i].red,
|
||||
embPalRGB[i].green,
|
||||
embPalRGB[i].blue,
|
||||
embPalAlpha && i < embPalAlphaSize ? embPalAlpha[i] : 0xFF
|
||||
);
|
||||
auto &palette = options.palSpec.emplace_back();
|
||||
assume(nbColors <= palette.size());
|
||||
for (size_t i = 0; i < nbColors; ++i) {
|
||||
palette[i] = embPal[i];
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
auto [mappings, nbPalettes] = overloadAndRemove(protoPalettes);
|
||||
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
|
||||
if (options.palSpecType == Options::DMG) {
|
||||
sortGrayscale(palettes, png.getColors().raw());
|
||||
} else if (auto [embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha] = png.getEmbeddedPal();
|
||||
embPalRGB != nullptr) {
|
||||
sortGrayscale(palettes, image.colors.raw());
|
||||
} else if (!image.png.palette.empty()) {
|
||||
warning(
|
||||
WARNING_EMBEDDED,
|
||||
"Sorting palette colors by PNG's embedded PLTE chunk without '-c/--colors embedded'"
|
||||
);
|
||||
sortIndexed(palettes, embPalSize, embPalRGB, embPalAlphaSize, embPalAlpha);
|
||||
} else if (png.isSuitableForGrayscale()) {
|
||||
sortGrayscale(palettes, png.getColors().raw());
|
||||
sortIndexed(palettes, image.png.palette);
|
||||
} else if (image.isSuitableForGrayscale()) {
|
||||
sortGrayscale(palettes, image.colors.raw());
|
||||
} else {
|
||||
sortRgb(palettes);
|
||||
}
|
||||
@@ -641,8 +399,8 @@ static std::tuple<std::vector<size_t>, std::vector<Palette>>
|
||||
auto listColors = [](auto const &list) {
|
||||
static char buf[sizeof(", $XXXX, $XXXX, $XXXX, $XXXX")];
|
||||
char *ptr = buf;
|
||||
for (uint16_t cgbColor : list) {
|
||||
ptr += snprintf(ptr, sizeof(", $XXXX"), ", $%04x", cgbColor);
|
||||
for (uint16_t color : list) {
|
||||
ptr += snprintf(ptr, sizeof(", $XXXX"), ", $%04x", color);
|
||||
}
|
||||
return &buf[literal_strlen(", ")];
|
||||
};
|
||||
@@ -753,7 +511,7 @@ public:
|
||||
mutable uint16_t tileID;
|
||||
|
||||
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;
|
||||
for (uint32_t x = 0; x < 8; ++x) {
|
||||
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;
|
||||
for (uint32_t y = 0; y < 8; ++y) {
|
||||
uint16_t bitplanes = rowBitplanes(tile, palette, y);
|
||||
@@ -856,7 +614,7 @@ struct std::hash<TileData> {
|
||||
};
|
||||
|
||||
static void outputUnoptimizedTileData(
|
||||
Png const &png,
|
||||
Image const &image,
|
||||
std::vector<AttrmapEntry> const &attrmap,
|
||||
std::vector<Palette> const &palettes,
|
||||
std::vector<size_t> const &mappings
|
||||
@@ -868,14 +626,14 @@ static void outputUnoptimizedTileData(
|
||||
// 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 =
|
||||
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 nbKeptTiles = nbTiles > options.trim ? nbTiles - options.trim : 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.
|
||||
if (attr.isBackgroundTile()) {
|
||||
++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
|
||||
// twice)
|
||||
static UniqueTiles dedupTiles(
|
||||
Png const &png,
|
||||
Image const &image,
|
||||
std::vector<AttrmapEntry> &attrmap,
|
||||
std::vector<Palette> const &palettes,
|
||||
std::vector<size_t> const &mappings
|
||||
@@ -1044,7 +802,7 @@ static UniqueTiles dedupTiles(
|
||||
}
|
||||
|
||||
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()) {
|
||||
attr.xFlip = 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_LOG_ACT, "Reading tiles...\n");
|
||||
Png png(options.input); // This also sets `hasTransparentPixels` as a side effect
|
||||
ImagePalette const &colors = png.getColors();
|
||||
|
||||
// Now, we have all the image's colors in `colors`
|
||||
// The next step is to order the palette
|
||||
Image image(options.input); // This also sets `hasTransparentPixels` as a side effect
|
||||
|
||||
// LCOV_EXCL_START
|
||||
if (options.verbosity >= Options::VERB_INTERM) {
|
||||
fputs("Image colors: [ ", stderr);
|
||||
for (std::optional<Rgba> const &slot : colors) {
|
||||
for (std::optional<Rgba> const &slot : image.colors) {
|
||||
if (!slot.has_value()) {
|
||||
continue;
|
||||
}
|
||||
@@ -1182,7 +936,7 @@ void process() {
|
||||
"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 "
|
||||
"specification");
|
||||
}
|
||||
@@ -1195,7 +949,7 @@ void process() {
|
||||
std::vector<ProtoPalette> protoPalettes;
|
||||
std::vector<AttrmapEntry> attrmap{};
|
||||
|
||||
for (auto tile : png.visitAsTiles()) {
|
||||
for (auto tile : image.visitAsTiles()) {
|
||||
AttrmapEntry &attrs = attrmap.emplace_back();
|
||||
|
||||
// Count the unique non-transparent colors for packing
|
||||
@@ -1227,8 +981,8 @@ void process() {
|
||||
}
|
||||
|
||||
ProtoPalette protoPalette;
|
||||
for (uint16_t cgbColor : tileColors) {
|
||||
protoPalette.add(cgbColor);
|
||||
for (uint16_t color : tileColors) {
|
||||
protoPalette.add(color);
|
||||
}
|
||||
|
||||
if (options.bgColor.has_value()
|
||||
@@ -1295,17 +1049,17 @@ continue_visiting_tiles:;
|
||||
// LCOV_EXCL_STOP
|
||||
|
||||
if (options.palSpecType == Options::EMBEDDED) {
|
||||
generatePalSpec(png);
|
||||
generatePalSpec(image);
|
||||
}
|
||||
auto [mappings, palettes] =
|
||||
options.palSpecType == Options::NO_SPEC || options.palSpecType == Options::DMG
|
||||
? generatePalettes(protoPalettes, png)
|
||||
? generatePalettes(protoPalettes, image)
|
||||
: makePalsAsSpecified(protoPalettes);
|
||||
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;
|
||||
uint32_t const nbTilesH = image.png.height / 8, nbTilesW = image.png.width / 8;
|
||||
|
||||
// Check the tile count
|
||||
if (uint32_t nbTiles = nbTilesW * nbTilesH;
|
||||
@@ -1326,7 +1080,7 @@ continue_visiting_tiles:;
|
||||
|
||||
if (!options.output.empty()) {
|
||||
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()) {
|
||||
@@ -1339,7 +1093,7 @@ continue_visiting_tiles:;
|
||||
} else {
|
||||
// All of these require the deduplication process to be performed to be output
|
||||
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();
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
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