yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
sheet_image_import.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <optional>
5
6#include "absl/strings/str_format.h"
7#include "absl/strings/str_join.h"
10
11namespace yaze::editor {
12
13namespace {
14
15constexpr int kSwatchSize = 8;
16// Strips repeat the palette across the width, so the shortest period that
17// fits is the palette size: 4 (2bpp), 8 (3bpp) or 16 (4bpp) colors.
18constexpr std::array<int, 3> kStripPaletteSizes = {4, 8, 16};
19constexpr int kListedOffenders = 5;
20
21using Rgba = std::array<uint8_t, 4>;
22
23Rgba PixelAt(const util::PngImage& png, int x, int y) {
24 const size_t at = (static_cast<size_t>(y) * png.width + x) * 4;
25 return {png.rgba[at], png.rgba[at + 1], png.rgba[at + 2], png.rgba[at + 3]};
26}
27
28// Value of an 8x8 swatch when every pixel in it is the same, else nullopt.
29template <typename T, typename Get>
30std::optional<T> UniformSwatch(int swatch, int strip_top, Get get) {
31 const int x0 = swatch * kSwatchSize;
32 const T first = get(x0, strip_top);
33 for (int y = strip_top; y < strip_top + kSwatchSize; ++y) {
34 for (int x = x0; x < x0 + kSwatchSize; ++x) {
35 if (get(x, y) != first) {
36 return std::nullopt;
37 }
38 }
39 }
40 return first;
41}
42
43// The palette size whose repeats explain every swatch, or 0. `same(a, b)`
44// compares swatch a with swatch b.
45template <typename Same>
46int StripPeriod(int swatches, Same same) {
47 for (int size : kStripPaletteSizes) {
48 if (swatches < size || swatches % size != 0) {
49 continue;
50 }
51 bool repeats = true;
52 for (int k = size; repeats && k < swatches; ++k) {
53 repeats = same(k, k % size);
54 }
55 if (repeats) {
56 return size;
57 }
58 }
59 return 0;
60}
61
62bool CanHoldStrip(int width, int height) {
63 return width % kSwatchSize == 0 && height % kSwatchSize == 0 &&
64 height >= 2 * kSwatchSize;
65}
66
67int MaxIndex(const std::vector<uint8_t>& indices) {
68 return indices.empty() ? 0
69 : *std::max_element(indices.begin(), indices.end());
70}
71
72absl::StatusOr<ImportedSheetImage> ImportIndexed(const util::PngImage& png) {
75 image.width = png.width;
76 image.height = png.height;
77 image.palette = png.palette;
78 image.indices = png.indices;
79
80 // A strip drawn with the file's own indices (swatch k = index k) is
81 // dropped the same way as an RGB strip.
82 if (CanHoldStrip(png.width, png.height)) {
83 const int strip_top = png.height - kSwatchSize;
84 const int swatches = png.width / kSwatchSize;
85 auto index_at = [&](int x, int y) {
86 return png.indices[static_cast<size_t>(y) * png.width + x];
87 };
88 std::vector<int> swatch_index(swatches, -1);
89 bool uniform = true;
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();
93 if (uniform) {
94 swatch_index[s] = *value;
95 }
96 }
97 const int size =
98 uniform ? StripPeriod(swatches,
99 [&](int a, int b) {
100 return swatch_index[a] == swatch_index[b];
101 })
102 : 0;
103 bool in_order = size > 0;
104 for (int k = 0; in_order && k < size; ++k) {
105 in_order = swatch_index[k] == k;
106 }
107 if (in_order) {
108 image.height = strip_top;
109 image.indices.resize(static_cast<size_t>(strip_top) * png.width);
110 image.strip_removed = true;
111 }
112 }
113 image.max_index = MaxIndex(image.indices);
114 return image;
115}
116
117absl::StatusOr<ImportedSheetImage> ImportPaletteStrip(
118 const util::PngImage& png) {
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.",
124 png.width, png.height, std::string(why)));
125 };
126 if (!CanHoldStrip(png.width, png.height)) {
127 return no_strip("width and height must be multiples of 8, height 16+");
128 }
129 const int strip_top = png.height - kSwatchSize;
130 const int swatches = png.width / kSwatchSize;
131 auto pixel_at = [&](int x, int y) {
132 return PixelAt(png, x, y);
133 };
134
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);
138 if (!value) {
139 return no_strip(absl::StrFormat(
140 "the 8x8 swatch at x=%d of the bottom 8 rows is not one color",
141 s * kSwatchSize));
142 }
143 swatch_color[s] = *value;
144 }
145 const int size = StripPeriod(swatches, [&](int a, int b) {
146 return swatch_color[a] == swatch_color[b];
147 });
148 if (size == 0) {
149 return no_strip(absl::StrFormat(
150 "%d swatches do not repeat a 4-, 8- or 16-color palette", swatches));
151 }
152
153 ImportedSheetImage image;
155 image.width = png.width;
156 image.height = strip_top;
157 image.strip_removed = true;
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];
161 // Only index 0 may be a transparent swatch; it stands for transparency.
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)",
166 k, alpha));
167 }
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,
171 image.palette[k].begin())) {
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",
175 j, k, image.palette[k][0], image.palette[k][1],
176 image.palette[k][2]));
177 }
178 }
179 }
180
181 image.indices.resize(static_cast<size_t>(image.width) * image.height);
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) {
186 const Rgba pixel = PixelAt(png, x, y);
187 int index = -1;
188 if (pixel[3] == 0) {
189 index = 0;
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,
194 image.palette[k].begin())) {
195 index = k;
196 break;
197 }
198 }
199 }
200 if (index < 0) {
201 if (offenders.size() < kListedOffenders) {
202 offenders.push_back(absl::StrFormat("(%d, %d) #%02X%02X%02X a=%d", x,
203 y, pixel[0], pixel[1], pixel[2],
204 pixel[3]));
205 }
206 ++offender_count;
207 continue;
208 }
209 image.indices[static_cast<size_t>(y) * image.width + x] =
210 static_cast<uint8_t>(index);
211 }
212 }
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() ? ", ..." : ""));
219 }
220 image.max_index = MaxIndex(image.indices);
221 return image;
222}
223
224} // namespace
225
226absl::StatusOr<ImportedSheetImage> ImportSheetImage(const util::PngImage& png) {
227 if (png.width <= 0 || png.height <= 0) {
228 return absl::InvalidArgumentError("Image is empty");
229 }
230 const size_t pixels = static_cast<size_t>(png.width) * png.height;
231 if (png.indexed) {
232 if (png.indices.size() != pixels) {
233 return absl::InvalidArgumentError("Indexed image data is truncated");
234 }
235 return ImportIndexed(png);
236 }
237 if (png.rgba.size() != pixels * 4) {
238 return absl::InvalidArgumentError("RGBA image data is truncated");
239 }
240 return ImportPaletteStrip(png);
241}
242
243absl::StatusOr<ImportedSheetImage> ImportSheetImagePng(
244 const std::vector<uint8_t>& png_bytes) {
245 auto png = util::DecodePng(png_bytes);
246 if (!png.ok()) {
247 return png.status();
248 }
249 return ImportSheetImage(*png);
250}
251
252absl::StatusOr<std::vector<uint8_t>> EncodeImportedSheetTiles(
253 const ImportedSheetImage& image, int bpp) {
254 if (bpp != 2 && bpp != 3 && bpp != 4) {
255 return absl::InvalidArgumentError(
256 absl::StrFormat("Unsupported tile depth %dbpp", bpp));
257 }
258 if (image.width % 8 != 0 || image.height % 8 != 0 || image.width == 0 ||
259 image.height == 0) {
260 return absl::InvalidArgumentError(
261 absl::StrFormat("A %dx%d image does not split into 8x8 tiles",
262 image.width, image.height));
263 }
264 const int limit = 1 << bpp;
265 const auto high =
266 std::find_if(image.indices.begin(), image.indices.end(),
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));
273 }
274
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) {
281 snes_tile8 tile{};
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 +
286 tx * 8 + col];
287 }
288 }
289 const auto packed = gfx::PackBppTile(tile, bpp);
290 out.insert(out.end(), packed.begin(), packed.end());
291 }
292 }
293 return out;
294}
295
296absl::StatusOr<std::vector<uint8_t>> EncodeImportedSheetPng(
297 const ImportedSheetImage& image) {
298 if (image.palette.empty() ||
299 image.max_index >= static_cast<int>(image.palette.size())) {
300 return absl::FailedPreconditionError(
301 absl::StrFormat("Image uses index %d but its palette has %zu colors",
302 image.max_index, image.palette.size()));
303 }
304 return util::EncodeIndexedPng(image.width, image.height, image.indices,
305 image.palette);
306}
307
309 return absl::StrFormat(
310 "%dx%d %s, %zu colors%s, highest index %d", image.width, image.height,
311 image.source == ImportedImageSource::kIndexedPng ? "indexed PNG"
312 : "RGB PNG + strip",
313 image.palette.size(), image.strip_removed ? " (strip removed)" : "",
314 image.max_index);
315}
316
317absl::StatusOr<std::vector<int>> PreviewPaletteSlots(
318 std::string_view group_name, size_t palette_size, int row, int count) {
319 const auto layout = internal::GetPaletteRowLayout(group_name, palette_size);
320 const int rows =
321 internal::GetPaletteRowCount(palette_size, layout.colors_per_row);
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));
325 }
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
330 ? 0
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 "
336 "%d",
337 row, available, count));
338 }
339 std::vector<int> slots;
340 for (int i = 0; i < count; ++i) {
341 slots.push_back(static_cast<int>(offset) + i);
342 }
343 return slots;
344}
345
346} // namespace yaze::editor
absl::StatusOr< ImportedSheetImage > ImportPaletteStrip(const util::PngImage &png)
Rgba PixelAt(const util::PngImage &png, int x, int y)
int MaxIndex(const std::vector< uint8_t > &indices)
std::optional< T > UniformSwatch(int swatch, int strip_top, Get get)
absl::StatusOr< ImportedSheetImage > ImportIndexed(const util::PngImage &png)
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)
Definition snes_tile.cc:74
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.
8x8 SNES tile data
uint8_t data[64]
std::vector< std::array< uint8_t, 4 > > palette
A decoded PNG: palette indices for indexed files, RGBA otherwise.
Definition indexed_png.h:21
std::vector< std::array< uint8_t, 4 > > palette
Definition indexed_png.h:28
std::vector< uint8_t > indices
Definition indexed_png.h:26
std::vector< uint8_t > rgba
Definition indexed_png.h:30