6#include "absl/strings/str_format.h"
7#include "absl/strings/str_join.h"
21using Rgba = std::array<uint8_t, 4>;
24 const size_t at = (
static_cast<size_t>(y) * png.
width + x) * 4;
29template <
typename T,
typename Get>
32 const T first = get(x0, strip_top);
33 for (
int y = strip_top; y < strip_top +
kSwatchSize; ++y) {
35 if (get(x, y) != first) {
45template <
typename Same>
48 if (swatches < size || swatches % size != 0) {
52 for (
int k = size; repeats && k < swatches; ++k) {
53 repeats = same(k, k % size);
67int MaxIndex(
const std::vector<uint8_t>& indices) {
68 return indices.empty() ? 0
69 : *std::max_element(indices.begin(), indices.end());
85 auto index_at = [&](
int x,
int y) {
86 return png.
indices[
static_cast<size_t>(y) * png.
width + x];
88 std::vector<int> swatch_index(swatches, -1);
90 for (
int s = 0; uniform && s < swatches; ++s) {
91 const auto value = UniformSwatch<uint8_t>(s, strip_top, index_at);
92 uniform = value.has_value();
94 swatch_index[s] = *value;
100 return swatch_index[a] == swatch_index[b];
103 bool in_order = size > 0;
104 for (
int k = 0; in_order && k < size; ++k) {
105 in_order = swatch_index[k] == k;
109 image.
indices.resize(
static_cast<size_t>(strip_top) * png.
width);
119 const auto no_strip = [&](std::string_view why) {
120 return absl::InvalidArgumentError(absl::StrFormat(
121 "This %dx%d image is RGB and has no usable palette strip (%s). Use "
122 "an indexed PNG, or add an 8-pixel-tall strip under the art: 8x8 "
123 "swatches, swatch k holding color k, left to right.",
127 return no_strip(
"width and height must be multiples of 8, height 16+");
131 auto pixel_at = [&](
int x,
int y) {
135 std::vector<Rgba> swatch_color(swatches);
136 for (
int s = 0; s < swatches; ++s) {
137 const auto value = UniformSwatch<Rgba>(s, strip_top, pixel_at);
139 return no_strip(absl::StrFormat(
140 "the 8x8 swatch at x=%d of the bottom 8 rows is not one color",
143 swatch_color[s] = *value;
145 const int size =
StripPeriod(swatches, [&](
int a,
int b) {
146 return swatch_color[a] == swatch_color[b];
149 return no_strip(absl::StrFormat(
150 "%d swatches do not repeat a 4-, 8- or 16-color palette", swatches));
158 image.
palette.assign(swatch_color.begin(), swatch_color.begin() + size);
159 for (
int k = 0; k < size; ++k) {
160 const uint8_t alpha = image.
palette[k][3];
162 if (alpha != 255 && !(k == 0 && alpha == 0)) {
163 return absl::InvalidArgumentError(absl::StrFormat(
164 "Palette strip color %d has alpha %d; strip colors must be opaque "
165 "(color 0 may be fully transparent)",
168 for (
int j = 0; j < k; ++j) {
169 if (image.
palette[j][3] == 255 && alpha == 255 &&
170 std::equal(image.
palette[j].begin(), image.
palette[j].begin() + 3,
172 return absl::InvalidArgumentError(absl::StrFormat(
173 "Palette strip colors %d and %d are both #%02X%02X%02X, so "
174 "pixels of that color cannot be mapped to one index",
182 std::vector<std::string> offenders;
183 size_t offender_count = 0;
184 for (
int y = 0; y < image.
height; ++y) {
185 for (
int x = 0; x < image.
width; ++x) {
190 }
else if (pixel[3] == 255) {
191 for (
int k = 0; k < size; ++k) {
192 if (image.
palette[k][3] == 255 &&
193 std::equal(pixel.begin(), pixel.begin() + 3,
202 offenders.push_back(absl::StrFormat(
"(%d, %d) #%02X%02X%02X a=%d", x,
203 y, pixel[0], pixel[1], pixel[2],
209 image.
indices[
static_cast<size_t>(y) * image.
width + x] =
210 static_cast<uint8_t
>(index);
213 if (offender_count > 0) {
214 return absl::InvalidArgumentError(absl::StrFormat(
215 "%zu pixel(s) are not a palette strip color (exact match required; "
216 "partly transparent pixels are not allowed): %s%s",
217 offender_count, absl::StrJoin(offenders,
", "),
218 offender_count > offenders.size() ?
", ..." :
""));
228 return absl::InvalidArgumentError(
"Image is empty");
230 const size_t pixels =
static_cast<size_t>(png.
width) * png.
height;
232 if (png.
indices.size() != pixels) {
233 return absl::InvalidArgumentError(
"Indexed image data is truncated");
235 return ImportIndexed(png);
237 if (png.
rgba.size() != pixels * 4) {
238 return absl::InvalidArgumentError(
"RGBA image data is truncated");
240 return ImportPaletteStrip(png);
244 const std::vector<uint8_t>& png_bytes) {
254 if (bpp != 2 && bpp != 3 && bpp != 4) {
255 return absl::InvalidArgumentError(
256 absl::StrFormat(
"Unsupported tile depth %dbpp", bpp));
260 return absl::InvalidArgumentError(
261 absl::StrFormat(
"A %dx%d image does not split into 8x8 tiles",
264 const int limit = 1 << bpp;
267 [limit](uint8_t index) { return index >= limit; });
268 if (high != image.
indices.end()) {
269 const size_t at =
static_cast<size_t>(high - image.
indices.begin());
270 return absl::InvalidArgumentError(absl::StrFormat(
271 "Index %d at (%zu, %zu) does not fit %dbpp (indices 0-%d)", *high,
272 at % image.
width, at / image.
width, bpp, limit - 1));
275 const int tiles_wide = image.
width / 8;
276 const int tiles_high = image.
height / 8;
277 std::vector<uint8_t> out;
278 out.reserve(
static_cast<size_t>(tiles_wide) * tiles_high * bpp * 8);
279 for (
int ty = 0; ty < tiles_high; ++ty) {
280 for (
int tx = 0; tx < tiles_wide; ++tx) {
282 for (
int row = 0; row < 8; ++row) {
283 for (
int col = 0; col < 8; ++col) {
284 tile.
data[row * 8 + col] =
285 image.
indices[
static_cast<size_t>(ty * 8 + row) * image.
width +
290 out.insert(out.end(), packed.begin(), packed.end());
300 return absl::FailedPreconditionError(
301 absl::StrFormat(
"Image uses index %d but its palette has %zu colors",
309 return absl::StrFormat(
310 "%dx%d %s, %zu colors%s, highest index %d", image.
width, image.
height,
318 std::string_view group_name,
size_t palette_size,
int row,
int count) {
322 if (row < 0 || row >= rows) {
323 return absl::InvalidArgumentError(absl::StrFormat(
324 "Palette row %d is out of range (0-%d)", row, rows - 1));
326 const size_t skip = layout.has_explicit_transparent ? 1 : 0;
327 const size_t offset =
static_cast<size_t>(row) * layout.colors_per_row + skip;
328 const size_t available =
329 offset >= palette_size
331 : std::min<size_t>(7, std::min<size_t>(layout.colors_per_row - skip,
332 palette_size - offset));
333 if (count < 0 ||
static_cast<size_t>(count) > available) {
334 return absl::InvalidArgumentError(absl::StrFormat(
335 "Palette row %d holds %zu colors after transparency; the image needs "
337 row, available, count));
339 std::vector<int> slots;
340 for (
int i = 0; i < count; ++i) {
341 slots.push_back(
static_cast<int>(offset) + i);
absl::StatusOr< ImportedSheetImage > ImportPaletteStrip(const util::PngImage &png)
Rgba PixelAt(const util::PngImage &png, int x, int y)
constexpr std::array< int, 3 > kStripPaletteSizes
int MaxIndex(const std::vector< uint8_t > &indices)
int StripPeriod(int swatches, Same same)
std::optional< T > UniformSwatch(int swatch, int strip_top, Get get)
absl::StatusOr< ImportedSheetImage > ImportIndexed(const util::PngImage &png)
bool CanHoldStrip(int width, int height)
constexpr int kSwatchSize
std::array< uint8_t, 4 > Rgba
constexpr int kListedOffenders
int GetPaletteRowCount(std::size_t palette_size, int colors_per_row)
PaletteRowLayout GetPaletteRowLayout(std::string_view group_name, std::size_t palette_size)
Editors are the view controllers for the application.
std::string DescribeImportedSheetImage(const ImportedSheetImage &image)
"128x32 indexed PNG, 8 colors (strip removed), highest index 7".
absl::StatusOr< ImportedSheetImage > ImportSheetImagePng(const std::vector< uint8_t > &png_bytes)
Decodes PNG bytes (file or clipboard) and calls ImportSheetImage.
absl::StatusOr< std::vector< uint8_t > > EncodeImportedSheetTiles(const ImportedSheetImage &image, int bpp)
absl::StatusOr< std::vector< int > > PreviewPaletteSlots(std::string_view group_name, size_t palette_size, int row, int count)
absl::StatusOr< ImportedSheetImage > ImportSheetImage(const util::PngImage &png)
Converts a decoded PNG without re-quantizing it.
absl::StatusOr< std::vector< uint8_t > > EncodeImportedSheetPng(const ImportedSheetImage &image)
Re-encodes the art (without the strip) as an indexed PNG in index order.
std::vector< uint8_t > PackBppTile(const snes_tile8 &tile, const uint32_t bpp)
absl::StatusOr< std::vector< uint8_t > > EncodeIndexedPng(int width, int height, const std::vector< uint8_t > &indices, const std::vector< std::array< uint8_t, 4 > > &palette)
absl::StatusOr< PngImage > DecodePng(const std::vector< uint8_t > &bytes)
Image import for the Graphics editor that keeps the image's own palette order and pixel indices.
std::vector< uint8_t > indices
ImportedImageSource source
std::vector< std::array< uint8_t, 4 > > palette
A decoded PNG: palette indices for indexed files, RGBA otherwise.
std::vector< std::array< uint8_t, 4 > > palette
std::vector< uint8_t > indices
std::vector< uint8_t > rgba