yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
indexed_png.cc
Go to the documentation of this file.
1#include "util/indexed_png.h"
2
3#include <algorithm>
4#include <cstdlib>
5#include <cstring>
6#include <fstream>
7#include <iterator>
8
9#include "absl/status/status.h"
10#include "absl/strings/str_format.h"
11#include "miniz/miniz.h"
12
13namespace yaze::util {
14namespace {
15
16constexpr uint8_t kSignature[8] = {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A};
17// Refuse images this large rather than allocating for a hostile header.
18constexpr uint64_t kMaxPixels = 64ull * 1024 * 1024;
19
20void PutU32(std::vector<uint8_t>& out, uint32_t value) {
21 out.push_back(static_cast<uint8_t>(value >> 24));
22 out.push_back(static_cast<uint8_t>(value >> 16));
23 out.push_back(static_cast<uint8_t>(value >> 8));
24 out.push_back(static_cast<uint8_t>(value));
25}
26
27uint32_t GetU32(const uint8_t* p) {
28 return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) |
29 (uint32_t(p[2]) << 8) | uint32_t(p[3]);
30}
31
32void PutChunk(std::vector<uint8_t>& out, const char type[4],
33 const std::vector<uint8_t>& data) {
34 PutU32(out, static_cast<uint32_t>(data.size()));
35 const size_t crc_start = out.size();
36 out.insert(out.end(), type, type + 4);
37 out.insert(out.end(), data.begin(), data.end());
38 const mz_ulong crc =
39 mz_crc32(MZ_CRC32_INIT, out.data() + crc_start, data.size() + 4);
40 PutU32(out, static_cast<uint32_t>(crc));
41}
42
43uint8_t Paeth(uint8_t a, uint8_t b, uint8_t c) {
44 const int p = int(a) + int(b) - int(c);
45 const int pa = std::abs(p - int(a));
46 const int pb = std::abs(p - int(b));
47 const int pc = std::abs(p - int(c));
48 if (pa <= pb && pa <= pc) {
49 return a;
50 }
51 return pb <= pc ? b : c;
52}
53
54} // namespace
55
56absl::StatusOr<std::vector<uint8_t>> EncodeIndexedPng(
57 int width, int height, const std::vector<uint8_t>& indices,
58 const std::vector<std::array<uint8_t, 4>>& palette) {
59 if (width <= 0 || height <= 0 ||
60 indices.size() != static_cast<size_t>(width) * height) {
61 return absl::InvalidArgumentError("PNG size does not match pixel count");
62 }
63 if (palette.empty() || palette.size() > 256) {
64 return absl::InvalidArgumentError("PNG palette needs 1-256 entries");
65 }
66 for (uint8_t index : indices) {
67 if (index >= palette.size()) {
68 return absl::InvalidArgumentError(
69 absl::StrFormat("Pixel index %d is past the %zu-entry palette", index,
70 palette.size()));
71 }
72 }
73
74 std::vector<uint8_t> raw;
75 raw.reserve(static_cast<size_t>(height) * (width + 1));
76 for (int y = 0; y < height; ++y) {
77 raw.push_back(0); // filter: none
78 raw.insert(raw.end(), indices.begin() + y * width,
79 indices.begin() + (y + 1) * width);
80 }
81 mz_ulong compressed_size = mz_compressBound(raw.size());
82 std::vector<uint8_t> compressed(compressed_size);
83 if (mz_compress2(compressed.data(), &compressed_size, raw.data(), raw.size(),
84 MZ_BEST_COMPRESSION) != MZ_OK) {
85 return absl::InternalError("PNG deflate failed");
86 }
87 compressed.resize(compressed_size);
88
89 std::vector<uint8_t> out(kSignature, kSignature + 8);
90 std::vector<uint8_t> header;
91 PutU32(header, static_cast<uint32_t>(width));
92 PutU32(header, static_cast<uint32_t>(height));
93 header.insert(header.end(), {8, 3, 0, 0, 0}); // 8-bit indexed
94 PutChunk(out, "IHDR", header);
95 std::vector<uint8_t> plte;
96 std::vector<uint8_t> trns;
97 bool any_alpha = false;
98 for (const auto& entry : palette) {
99 plte.insert(plte.end(), {entry[0], entry[1], entry[2]});
100 trns.push_back(entry[3]);
101 any_alpha |= entry[3] != 255;
102 }
103 PutChunk(out, "PLTE", plte);
104 if (any_alpha) {
105 PutChunk(out, "tRNS", trns);
106 }
107 PutChunk(out, "IDAT", compressed);
108 PutChunk(out, "IEND", {});
109 return out;
110}
111
112absl::StatusOr<PngImage> DecodePng(const std::vector<uint8_t>& bytes) {
113 if (bytes.size() < 8 || std::memcmp(bytes.data(), kSignature, 8) != 0) {
114 return absl::InvalidArgumentError("Not a PNG file");
115 }
116 PngImage image;
117 int bit_depth = 0;
118 int color_type = -1;
119 std::vector<uint8_t> idat;
120 std::vector<uint8_t> trns;
121 size_t pos = 8;
122 bool seen_end = false;
123 while (pos + 12 <= bytes.size()) {
124 const uint32_t length = GetU32(&bytes[pos]);
125 if (length > bytes.size() - pos - 12) {
126 return absl::InvalidArgumentError("Truncated PNG chunk");
127 }
128 const std::string type(reinterpret_cast<const char*>(&bytes[pos + 4]), 4);
129 const uint8_t* data = &bytes[pos + 8];
130 const mz_ulong crc = mz_crc32(MZ_CRC32_INIT, &bytes[pos + 4], length + 4);
131 if (crc != GetU32(data + length)) {
132 return absl::InvalidArgumentError("PNG chunk " + type + " fails CRC");
133 }
134 if (type == "IHDR") {
135 if (length != 13) {
136 return absl::InvalidArgumentError("Bad PNG header");
137 }
138 image.width = static_cast<int>(GetU32(data));
139 image.height = static_cast<int>(GetU32(data + 4));
140 bit_depth = data[8];
141 color_type = data[9];
142 if (data[12] != 0) {
143 return absl::UnimplementedError(
144 "Interlaced PNGs are not supported; save without interlacing");
145 }
146 } else if (type == "PLTE") {
147 for (uint32_t i = 0; i + 2 < length; i += 3) {
148 image.palette.push_back({data[i], data[i + 1], data[i + 2], 255});
149 }
150 } else if (type == "tRNS") {
151 trns.assign(data, data + length);
152 } else if (type == "IDAT") {
153 idat.insert(idat.end(), data, data + length);
154 } else if (type == "IEND") {
155 seen_end = true;
156 break;
157 }
158 pos += 12 + length;
159 }
160 if (!seen_end || image.width <= 0 || image.height <= 0) {
161 return absl::InvalidArgumentError("Incomplete PNG");
162 }
163 if (static_cast<uint64_t>(image.width) * image.height > kMaxPixels) {
164 return absl::InvalidArgumentError("PNG is too large");
165 }
166
167 int channels = 0;
168 switch (color_type) {
169 case 0:
170 channels = 1;
171 break;
172 case 2:
173 channels = 3;
174 break;
175 case 3:
176 channels = 1;
177 break;
178 case 4:
179 channels = 2;
180 break;
181 case 6:
182 channels = 4;
183 break;
184 default:
185 return absl::InvalidArgumentError("Unknown PNG color type");
186 }
187 const bool indexed = color_type == 3;
188 if (indexed ? (bit_depth != 1 && bit_depth != 2 && bit_depth != 4 &&
189 bit_depth != 8)
190 : bit_depth != 8) {
191 return absl::UnimplementedError(
192 absl::StrFormat("PNG bit depth %d is not supported for color type %d",
193 bit_depth, color_type));
194 }
195 if (indexed && image.palette.empty()) {
196 return absl::InvalidArgumentError("Indexed PNG has no palette");
197 }
198 for (size_t i = 0; i < trns.size() && i < image.palette.size(); ++i) {
199 image.palette[i][3] = trns[i];
200 }
201
202 const size_t bits_per_pixel = static_cast<size_t>(bit_depth) * channels;
203 const size_t row_bytes = (image.width * bits_per_pixel + 7) / 8;
204 const size_t pixel_bytes = std::max<size_t>(1, bits_per_pixel / 8);
205 mz_ulong raw_size = static_cast<mz_ulong>((row_bytes + 1) * image.height);
206 std::vector<uint8_t> raw(raw_size);
207 if (mz_uncompress(raw.data(), &raw_size, idat.data(), idat.size()) != MZ_OK ||
208 raw_size != raw.size()) {
209 return absl::InvalidArgumentError("PNG image data does not inflate");
210 }
211
212 std::vector<uint8_t> previous(row_bytes, 0);
213 std::vector<uint8_t> row(row_bytes);
214 image.indexed = indexed;
215 if (indexed) {
216 image.indices.resize(static_cast<size_t>(image.width) * image.height);
217 } else {
218 image.rgba.resize(static_cast<size_t>(image.width) * image.height * 4);
219 }
220 for (int y = 0; y < image.height; ++y) {
221 const uint8_t* line = &raw[y * (row_bytes + 1)];
222 const uint8_t filter = line[0];
223 for (size_t x = 0; x < row_bytes; ++x) {
224 const uint8_t a = x >= pixel_bytes ? row[x - pixel_bytes] : 0;
225 const uint8_t b = previous[x];
226 const uint8_t c = x >= pixel_bytes ? previous[x - pixel_bytes] : 0;
227 uint8_t value = line[1 + x];
228 switch (filter) {
229 case 0:
230 break;
231 case 1:
232 value += a;
233 break;
234 case 2:
235 value += b;
236 break;
237 case 3:
238 value += static_cast<uint8_t>((int(a) + int(b)) / 2);
239 break;
240 case 4:
241 value += Paeth(a, b, c);
242 break;
243 default:
244 return absl::InvalidArgumentError("Unknown PNG row filter");
245 }
246 row[x] = value;
247 }
248 for (int x = 0; x < image.width; ++x) {
249 const size_t out = static_cast<size_t>(y) * image.width + x;
250 if (indexed) {
251 const size_t bit = static_cast<size_t>(x) * bit_depth;
252 const uint8_t packed = row[bit / 8];
253 const int shift = 8 - bit_depth - static_cast<int>(bit % 8);
254 image.indices[out] =
255 static_cast<uint8_t>((packed >> shift) & ((1 << bit_depth) - 1));
256 continue;
257 }
258 const uint8_t* p = &row[static_cast<size_t>(x) * channels];
259 uint8_t* dst = &image.rgba[out * 4];
260 switch (color_type) {
261 case 0:
262 dst[0] = dst[1] = dst[2] = p[0];
263 dst[3] = 255;
264 break;
265 case 4:
266 dst[0] = dst[1] = dst[2] = p[0];
267 dst[3] = p[1];
268 break;
269 case 2:
270 dst[0] = p[0];
271 dst[1] = p[1];
272 dst[2] = p[2];
273 dst[3] = 255;
274 break;
275 default:
276 std::memcpy(dst, p, 4);
277 break;
278 }
279 }
280 std::swap(previous, row);
281 }
282 return image;
283}
284
285absl::StatusOr<std::vector<uint8_t>> ReadBinaryFile(const std::string& path) {
286 std::ifstream file(path, std::ios::binary);
287 if (!file) {
288 return absl::NotFoundError("Cannot read " + path);
289 }
290 return std::vector<uint8_t>((std::istreambuf_iterator<char>(file)),
291 std::istreambuf_iterator<char>());
292}
293
294absl::Status WriteBinaryFile(const std::string& path,
295 const std::vector<uint8_t>& bytes) {
296 std::ofstream file(path, std::ios::binary | std::ios::trunc);
297 if (!file) {
298 return absl::PermissionDeniedError("Cannot write " + path);
299 }
300 file.write(reinterpret_cast<const char*>(bytes.data()),
301 static_cast<std::streamsize>(bytes.size()));
302 if (!file) {
303 return absl::DataLossError("Failed writing " + path);
304 }
305 return absl::OkStatus();
306}
307
308} // namespace yaze::util
void PutU32(std::vector< uint8_t > &out, uint32_t value)
uint8_t Paeth(uint8_t a, uint8_t b, uint8_t c)
void PutChunk(std::vector< uint8_t > &out, const char type[4], const std::vector< uint8_t > &data)
absl::Status WriteBinaryFile(const std::string &path, const std::vector< uint8_t > &bytes)
absl::StatusOr< std::vector< uint8_t > > ReadBinaryFile(const std::string &path)
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)
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