/src/libjxl/lib/extras/dec/apng.cc
Line | Count | Source (jump to first uncovered line) |
1 | | // Copyright (c) the JPEG XL Project Authors. All rights reserved. |
2 | | // |
3 | | // Use of this source code is governed by a BSD-style |
4 | | // license that can be found in the LICENSE file. |
5 | | |
6 | | #include "lib/extras/dec/apng.h" |
7 | | |
8 | | // Parts of this code are taken from apngdis, which has the following license: |
9 | | /* APNG Disassembler 2.8 |
10 | | * |
11 | | * Deconstructs APNG files into individual frames. |
12 | | * |
13 | | * http://apngdis.sourceforge.net |
14 | | * |
15 | | * Copyright (c) 2010-2015 Max Stepin |
16 | | * maxst at users.sourceforge.net |
17 | | * |
18 | | * zlib license |
19 | | * ------------ |
20 | | * |
21 | | * This software is provided 'as-is', without any express or implied |
22 | | * warranty. In no event will the authors be held liable for any damages |
23 | | * arising from the use of this software. |
24 | | * |
25 | | * Permission is granted to anyone to use this software for any purpose, |
26 | | * including commercial applications, and to alter it and redistribute it |
27 | | * freely, subject to the following restrictions: |
28 | | * |
29 | | * 1. The origin of this software must not be misrepresented; you must not |
30 | | * claim that you wrote the original software. If you use this software |
31 | | * in a product, an acknowledgment in the product documentation would be |
32 | | * appreciated but is not required. |
33 | | * 2. Altered source versions must be plainly marked as such, and must not be |
34 | | * misrepresented as being the original software. |
35 | | * 3. This notice may not be removed or altered from any source distribution. |
36 | | * |
37 | | */ |
38 | | |
39 | | #include <jxl/codestream_header.h> |
40 | | #include <jxl/encode.h> |
41 | | |
42 | | #include <array> |
43 | | #include <atomic> |
44 | | #include <cstdint> |
45 | | #include <cstring> |
46 | | #include <limits> |
47 | | #include <memory> |
48 | | #include <string> |
49 | | #include <utility> |
50 | | #include <vector> |
51 | | |
52 | | #include "lib/extras/packed_image.h" |
53 | | #include "lib/extras/size_constraints.h" |
54 | | #include "lib/jxl/base/byte_order.h" |
55 | | #include "lib/jxl/base/common.h" |
56 | | #include "lib/jxl/base/compiler_specific.h" |
57 | | #include "lib/jxl/base/printf_macros.h" |
58 | | #include "lib/jxl/base/rect.h" |
59 | | #include "lib/jxl/base/sanitizers.h" |
60 | | #include "lib/jxl/base/span.h" |
61 | | #include "lib/jxl/base/status.h" |
62 | | #if JPEGXL_ENABLE_APNG |
63 | | #include "png.h" /* original (unpatched) libpng is ok */ |
64 | | #endif |
65 | | |
66 | | namespace jxl { |
67 | | namespace extras { |
68 | | |
69 | | #if !JPEGXL_ENABLE_APNG |
70 | | |
71 | 0 | bool CanDecodeAPNG() { return false; } |
72 | | Status DecodeImageAPNG(const Span<const uint8_t> bytes, |
73 | | const ColorHints& color_hints, PackedPixelFile* ppf, |
74 | 10.4k | const SizeConstraints* constraints) { |
75 | 10.4k | return false; |
76 | 10.4k | } |
77 | | |
78 | | #else // JPEGXL_ENABLE_APNG |
79 | | |
80 | | namespace { |
81 | | |
82 | | constexpr std::array<uint8_t, 8> kPngSignature = {137, 'P', 'N', 'G', |
83 | | '\r', '\n', 26, '\n'}; |
84 | | |
85 | | // Returns floating-point value from the PNG encoding (times 10^5). |
86 | | double F64FromU32(const uint32_t x) { return static_cast<int32_t>(x) * 1E-5; } |
87 | | |
88 | | /** Extract information from 'sRGB' chunk. */ |
89 | | Status DecodeSrgbChunk(const Bytes payload, JxlColorEncoding* color_encoding) { |
90 | | if (payload.size() != 1) return JXL_FAILURE("Wrong sRGB size"); |
91 | | uint8_t ri = payload[0]; |
92 | | // (PNG uses the same values as ICC.) |
93 | | if (ri >= 4) return JXL_FAILURE("Invalid Rendering Intent"); |
94 | | color_encoding->white_point = JXL_WHITE_POINT_D65; |
95 | | color_encoding->primaries = JXL_PRIMARIES_SRGB; |
96 | | color_encoding->transfer_function = JXL_TRANSFER_FUNCTION_SRGB; |
97 | | color_encoding->rendering_intent = static_cast<JxlRenderingIntent>(ri); |
98 | | return true; |
99 | | } |
100 | | |
101 | | /** |
102 | | * Extract information from 'cICP' chunk. |
103 | | * |
104 | | * If the cICP profile is not fully supported, return `false` and leave |
105 | | * `color_encoding` unmodified. |
106 | | */ |
107 | | Status DecodeCicpChunk(const Bytes payload, JxlColorEncoding* color_encoding) { |
108 | | if (payload.size() != 4) return JXL_FAILURE("Wrong cICP size"); |
109 | | JxlColorEncoding color_enc = *color_encoding; |
110 | | |
111 | | // From https://www.itu.int/rec/T-REC-H.273-202107-I/en |
112 | | if (payload[0] == 1) { |
113 | | // IEC 61966-2-1 sRGB |
114 | | color_enc.primaries = JXL_PRIMARIES_SRGB; |
115 | | color_enc.white_point = JXL_WHITE_POINT_D65; |
116 | | } else if (payload[0] == 4) { |
117 | | // Rec. ITU-R BT.470-6 System M |
118 | | color_enc.primaries = JXL_PRIMARIES_CUSTOM; |
119 | | color_enc.primaries_red_xy[0] = 0.67; |
120 | | color_enc.primaries_red_xy[1] = 0.33; |
121 | | color_enc.primaries_green_xy[0] = 0.21; |
122 | | color_enc.primaries_green_xy[1] = 0.71; |
123 | | color_enc.primaries_blue_xy[0] = 0.14; |
124 | | color_enc.primaries_blue_xy[1] = 0.08; |
125 | | color_enc.white_point = JXL_WHITE_POINT_CUSTOM; |
126 | | color_enc.white_point_xy[0] = 0.310; |
127 | | color_enc.white_point_xy[1] = 0.316; |
128 | | } else if (payload[0] == 5) { |
129 | | // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM |
130 | | color_enc.primaries = JXL_PRIMARIES_CUSTOM; |
131 | | color_enc.primaries_red_xy[0] = 0.64; |
132 | | color_enc.primaries_red_xy[1] = 0.33; |
133 | | color_enc.primaries_green_xy[0] = 0.29; |
134 | | color_enc.primaries_green_xy[1] = 0.60; |
135 | | color_enc.primaries_blue_xy[0] = 0.15; |
136 | | color_enc.primaries_blue_xy[1] = 0.06; |
137 | | color_enc.white_point = JXL_WHITE_POINT_D65; |
138 | | } else if (payload[0] == 6 || payload[0] == 7) { |
139 | | // SMPTE ST 170 (2004) / SMPTE ST 240 (1999) |
140 | | color_enc.primaries = JXL_PRIMARIES_CUSTOM; |
141 | | color_enc.primaries_red_xy[0] = 0.630; |
142 | | color_enc.primaries_red_xy[1] = 0.340; |
143 | | color_enc.primaries_green_xy[0] = 0.310; |
144 | | color_enc.primaries_green_xy[1] = 0.595; |
145 | | color_enc.primaries_blue_xy[0] = 0.155; |
146 | | color_enc.primaries_blue_xy[1] = 0.070; |
147 | | color_enc.white_point = JXL_WHITE_POINT_D65; |
148 | | } else if (payload[0] == 8) { |
149 | | // Generic film (colour filters using Illuminant C) |
150 | | color_enc.primaries = JXL_PRIMARIES_CUSTOM; |
151 | | color_enc.primaries_red_xy[0] = 0.681; |
152 | | color_enc.primaries_red_xy[1] = 0.319; |
153 | | color_enc.primaries_green_xy[0] = 0.243; |
154 | | color_enc.primaries_green_xy[1] = 0.692; |
155 | | color_enc.primaries_blue_xy[0] = 0.145; |
156 | | color_enc.primaries_blue_xy[1] = 0.049; |
157 | | color_enc.white_point = JXL_WHITE_POINT_CUSTOM; |
158 | | color_enc.white_point_xy[0] = 0.310; |
159 | | color_enc.white_point_xy[1] = 0.316; |
160 | | } else if (payload[0] == 9) { |
161 | | // Rec. ITU-R BT.2100-2 |
162 | | color_enc.primaries = JXL_PRIMARIES_2100; |
163 | | color_enc.white_point = JXL_WHITE_POINT_D65; |
164 | | } else if (payload[0] == 10) { |
165 | | // CIE 1931 XYZ |
166 | | color_enc.primaries = JXL_PRIMARIES_CUSTOM; |
167 | | color_enc.primaries_red_xy[0] = 1; |
168 | | color_enc.primaries_red_xy[1] = 0; |
169 | | color_enc.primaries_green_xy[0] = 0; |
170 | | color_enc.primaries_green_xy[1] = 1; |
171 | | color_enc.primaries_blue_xy[0] = 0; |
172 | | color_enc.primaries_blue_xy[1] = 0; |
173 | | color_enc.white_point = JXL_WHITE_POINT_E; |
174 | | } else if (payload[0] == 11) { |
175 | | // SMPTE RP 431-2 (2011) |
176 | | color_enc.primaries = JXL_PRIMARIES_P3; |
177 | | color_enc.white_point = JXL_WHITE_POINT_DCI; |
178 | | } else if (payload[0] == 12) { |
179 | | // SMPTE EG 432-1 (2010) |
180 | | color_enc.primaries = JXL_PRIMARIES_P3; |
181 | | color_enc.white_point = JXL_WHITE_POINT_D65; |
182 | | } else if (payload[0] == 22) { |
183 | | color_enc.primaries = JXL_PRIMARIES_CUSTOM; |
184 | | color_enc.primaries_red_xy[0] = 0.630; |
185 | | color_enc.primaries_red_xy[1] = 0.340; |
186 | | color_enc.primaries_green_xy[0] = 0.295; |
187 | | color_enc.primaries_green_xy[1] = 0.605; |
188 | | color_enc.primaries_blue_xy[0] = 0.155; |
189 | | color_enc.primaries_blue_xy[1] = 0.077; |
190 | | color_enc.white_point = JXL_WHITE_POINT_D65; |
191 | | } else { |
192 | | JXL_WARNING("Unsupported primaries specified in cICP chunk: %d", |
193 | | static_cast<int>(payload[0])); |
194 | | return false; |
195 | | } |
196 | | |
197 | | if (payload[1] == 1 || payload[1] == 6 || payload[1] == 14 || |
198 | | payload[1] == 15) { |
199 | | // Rec. ITU-R BT.709-6 |
200 | | color_enc.transfer_function = JXL_TRANSFER_FUNCTION_709; |
201 | | } else if (payload[1] == 4) { |
202 | | // Rec. ITU-R BT.1700-0 625 PAL and 625 SECAM |
203 | | color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; |
204 | | color_enc.gamma = 1 / 2.2; |
205 | | } else if (payload[1] == 5) { |
206 | | // Rec. ITU-R BT.470-6 System B, G |
207 | | color_enc.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; |
208 | | color_enc.gamma = 1 / 2.8; |
209 | | } else if (payload[1] == 8 || payload[1] == 13 || payload[1] == 16 || |
210 | | payload[1] == 17 || payload[1] == 18) { |
211 | | // These codes all match the corresponding JXL enum values |
212 | | color_enc.transfer_function = static_cast<JxlTransferFunction>(payload[1]); |
213 | | } else { |
214 | | JXL_WARNING("Unsupported transfer function specified in cICP chunk: %d", |
215 | | static_cast<int>(payload[1])); |
216 | | return false; |
217 | | } |
218 | | |
219 | | if (payload[2] != 0) { |
220 | | JXL_WARNING("Unsupported color space specified in cICP chunk: %d", |
221 | | static_cast<int>(payload[2])); |
222 | | return false; |
223 | | } |
224 | | if (payload[3] != 1) { |
225 | | JXL_WARNING("Unsupported full-range flag specified in cICP chunk: %d", |
226 | | static_cast<int>(payload[3])); |
227 | | return false; |
228 | | } |
229 | | // cICP has no rendering intent, so use the default |
230 | | color_enc.rendering_intent = JXL_RENDERING_INTENT_RELATIVE; |
231 | | *color_encoding = color_enc; |
232 | | return true; |
233 | | } |
234 | | |
235 | | /** Extract information from 'gAMA' chunk. */ |
236 | | Status DecodeGamaChunk(Bytes payload, JxlColorEncoding* color_encoding) { |
237 | | if (payload.size() != 4) return JXL_FAILURE("Wrong gAMA size"); |
238 | | color_encoding->transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; |
239 | | color_encoding->gamma = F64FromU32(LoadBE32(payload.data())); |
240 | | return true; |
241 | | } |
242 | | |
243 | | /** Extract information from 'cHTM' chunk. */ |
244 | | Status DecodeChrmChunk(Bytes payload, JxlColorEncoding* color_encoding) { |
245 | | if (payload.size() != 32) return JXL_FAILURE("Wrong cHRM size"); |
246 | | const uint8_t* data = payload.data(); |
247 | | color_encoding->white_point = JXL_WHITE_POINT_CUSTOM; |
248 | | color_encoding->white_point_xy[0] = F64FromU32(LoadBE32(data + 0)); |
249 | | color_encoding->white_point_xy[1] = F64FromU32(LoadBE32(data + 4)); |
250 | | |
251 | | color_encoding->primaries = JXL_PRIMARIES_CUSTOM; |
252 | | color_encoding->primaries_red_xy[0] = F64FromU32(LoadBE32(data + 8)); |
253 | | color_encoding->primaries_red_xy[1] = F64FromU32(LoadBE32(data + 12)); |
254 | | color_encoding->primaries_green_xy[0] = F64FromU32(LoadBE32(data + 16)); |
255 | | color_encoding->primaries_green_xy[1] = F64FromU32(LoadBE32(data + 20)); |
256 | | color_encoding->primaries_blue_xy[0] = F64FromU32(LoadBE32(data + 24)); |
257 | | color_encoding->primaries_blue_xy[1] = F64FromU32(LoadBE32(data + 28)); |
258 | | return true; |
259 | | } |
260 | | |
261 | | /** Extracts information from 'cLLi' chunk. */ |
262 | | Status DecodeClliChunk(Bytes payload, float* max_content_light_level) { |
263 | | if (payload.size() != 8) return JXL_FAILURE("Wrong cLLi size"); |
264 | | const uint8_t* data = payload.data(); |
265 | | const uint32_t maxcll_png = |
266 | | Clamp1(png_get_uint_32(data), uint32_t{0}, uint32_t{10000 * 10000}); |
267 | | // Ignore MaxFALL value. |
268 | | *max_content_light_level = static_cast<float>(maxcll_png) / 10000.f; |
269 | | return true; |
270 | | } |
271 | | |
272 | | /** Returns false if invalid. */ |
273 | | JXL_INLINE Status DecodeHexNibble(const char c, uint32_t* JXL_RESTRICT nibble) { |
274 | | if ('a' <= c && c <= 'f') { |
275 | | *nibble = 10 + c - 'a'; |
276 | | } else if ('0' <= c && c <= '9') { |
277 | | *nibble = c - '0'; |
278 | | } else { |
279 | | *nibble = 0; |
280 | | return JXL_FAILURE("Invalid metadata nibble"); |
281 | | } |
282 | | JXL_ENSURE(*nibble < 16); |
283 | | return true; |
284 | | } |
285 | | |
286 | | /** Returns false if invalid. */ |
287 | | JXL_INLINE Status DecodeDecimal(const char** pos, const char* end, |
288 | | uint32_t* JXL_RESTRICT value) { |
289 | | size_t len = 0; |
290 | | *value = 0; |
291 | | while (*pos < end) { |
292 | | char next = **pos; |
293 | | if (next >= '0' && next <= '9') { |
294 | | *value = (*value * 10) + static_cast<uint32_t>(next - '0'); |
295 | | len++; |
296 | | if (len > 8) { |
297 | | break; |
298 | | } |
299 | | } else { |
300 | | // Do not consume terminator (non-decimal digit). |
301 | | break; |
302 | | } |
303 | | (*pos)++; |
304 | | } |
305 | | if (len == 0 || len > 8) { |
306 | | return JXL_FAILURE("Failed to parse decimal"); |
307 | | } |
308 | | return true; |
309 | | } |
310 | | |
311 | | /** |
312 | | * Parses a PNG text chunk with key of the form "Raw profile type ####", with |
313 | | * #### a type. |
314 | | * |
315 | | * Returns whether it could successfully parse the content. |
316 | | * We trust key and encoded are null-terminated because they come from |
317 | | * libpng. |
318 | | */ |
319 | | Status MaybeDecodeBase16(const char* key, const char* encoded, |
320 | | std::string* type, std::vector<uint8_t>* bytes) { |
321 | | const char* encoded_end = encoded + strlen(encoded); |
322 | | |
323 | | const char* kKey = "Raw profile type "; |
324 | | if (strncmp(key, kKey, strlen(kKey)) != 0) return false; |
325 | | *type = key + strlen(kKey); |
326 | | const size_t kMaxTypeLen = 20; |
327 | | if (type->length() > kMaxTypeLen) return false; // Type too long |
328 | | |
329 | | // Header: freeform string and number of bytes |
330 | | // Expected format is: |
331 | | // \n |
332 | | // profile name/description\n |
333 | | // 40\n (the number of bytes after hex-decoding) |
334 | | // 01234566789abcdef....\n (72 bytes per line max). |
335 | | // 012345667\n (last line) |
336 | | const char* pos = encoded; |
337 | | |
338 | | if (*(pos++) != '\n') return false; |
339 | | while (pos < encoded_end && *pos != '\n') { |
340 | | pos++; |
341 | | } |
342 | | if (pos == encoded_end) return false; |
343 | | // We parsed so far a \n, some number of non \n characters and are now |
344 | | // pointing at a \n. |
345 | | if (*(pos++) != '\n') return false; |
346 | | // Skip leading spaces |
347 | | while (pos < encoded_end && *pos == ' ') { |
348 | | pos++; |
349 | | } |
350 | | uint32_t bytes_to_decode = 0; |
351 | | JXL_RETURN_IF_ERROR(DecodeDecimal(&pos, encoded_end, &bytes_to_decode)); |
352 | | |
353 | | // We need 2*bytes for the hex values plus 1 byte every 36 values, |
354 | | // plus terminal \n for length. |
355 | | size_t tail = static_cast<size_t>(encoded_end - pos); |
356 | | bool ok = ((tail / 2) >= bytes_to_decode); |
357 | | if (ok) tail -= 2 * static_cast<size_t>(bytes_to_decode); |
358 | | ok = ok && (tail == 1 + DivCeil(bytes_to_decode, 36)); |
359 | | if (!ok) { |
360 | | return JXL_FAILURE("Not enough bytes to parse %d bytes in hex", |
361 | | bytes_to_decode); |
362 | | } |
363 | | JXL_ENSURE(bytes->empty()); |
364 | | bytes->reserve(bytes_to_decode); |
365 | | |
366 | | // Encoding: base16 with newline after 72 chars. |
367 | | // pos points to the \n before the first line of hex values. |
368 | | for (size_t i = 0; i < bytes_to_decode; ++i) { |
369 | | if (i % 36 == 0) { |
370 | | if (pos + 1 >= encoded_end) return false; // Truncated base16 1 |
371 | | if (*pos != '\n') return false; // Expected newline |
372 | | ++pos; |
373 | | } |
374 | | |
375 | | if (pos + 2 >= encoded_end) return false; // Truncated base16 2; |
376 | | uint32_t nibble0; |
377 | | uint32_t nibble1; |
378 | | JXL_RETURN_IF_ERROR(DecodeHexNibble(pos[0], &nibble0)); |
379 | | JXL_RETURN_IF_ERROR(DecodeHexNibble(pos[1], &nibble1)); |
380 | | bytes->push_back(static_cast<uint8_t>((nibble0 << 4) + nibble1)); |
381 | | pos += 2; |
382 | | } |
383 | | if (pos + 1 != encoded_end) return false; // Too many encoded bytes |
384 | | if (pos[0] != '\n') return false; // Incorrect metadata terminator |
385 | | return true; |
386 | | } |
387 | | |
388 | | /** Retrieves XMP and EXIF/IPTC from itext and text. */ |
389 | | Status DecodeBlob(const png_text_struct& info, PackedMetadata* metadata) { |
390 | | // We trust these are properly null-terminated by libpng. |
391 | | const char* key = info.key; |
392 | | const char* value = info.text; |
393 | | if (strstr(key, "XML:com.adobe.xmp")) { |
394 | | metadata->xmp.resize(strlen(value)); // safe, see above |
395 | | memcpy(metadata->xmp.data(), value, metadata->xmp.size()); |
396 | | } |
397 | | |
398 | | std::string type; |
399 | | std::vector<uint8_t> bytes; |
400 | | |
401 | | // Handle text chunks annotated with key "Raw profile type ####", with |
402 | | // #### a type, which may contain metadata. |
403 | | const char* kKey = "Raw profile type "; |
404 | | if (strncmp(key, kKey, strlen(kKey)) != 0) return false; |
405 | | |
406 | | if (!MaybeDecodeBase16(key, value, &type, &bytes)) { |
407 | | JXL_WARNING("Couldn't parse 'Raw format type' text chunk"); |
408 | | return false; |
409 | | } |
410 | | if (type == "exif") { |
411 | | // Remove prefix if present. |
412 | | constexpr std::array<uint8_t, 6> kExifPrefix = {'E', 'x', 'i', 'f', 0, 0}; |
413 | | if (bytes.size() >= kExifPrefix.size() && |
414 | | memcmp(bytes.data(), kExifPrefix.data(), kExifPrefix.size()) == 0) { |
415 | | bytes.erase(bytes.begin(), bytes.begin() + kExifPrefix.size()); |
416 | | } |
417 | | if (!metadata->exif.empty()) { |
418 | | JXL_DEBUG_V(2, |
419 | | "overwriting EXIF (%" PRIuS " bytes) with base16 (%" PRIuS |
420 | | " bytes)", |
421 | | metadata->exif.size(), bytes.size()); |
422 | | } |
423 | | metadata->exif = std::move(bytes); |
424 | | } else if (type == "iptc") { |
425 | | // TODO(jon): Deal with IPTC in some way |
426 | | } else if (type == "8bim") { |
427 | | // TODO(jon): Deal with 8bim in some way |
428 | | } else if (type == "xmp") { |
429 | | if (!metadata->xmp.empty()) { |
430 | | JXL_DEBUG_V(2, |
431 | | "overwriting XMP (%" PRIuS " bytes) with base16 (%" PRIuS |
432 | | " bytes)", |
433 | | metadata->xmp.size(), bytes.size()); |
434 | | } |
435 | | metadata->xmp = std::move(bytes); |
436 | | } else { |
437 | | JXL_DEBUG_V( |
438 | | 2, "Unknown type in 'Raw format type' text chunk: %s: %" PRIuS " bytes", |
439 | | type.c_str(), bytes.size()); |
440 | | } |
441 | | return true; |
442 | | } |
443 | | |
444 | | constexpr bool isAbc(char c) { |
445 | | return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z'); |
446 | | } |
447 | | |
448 | | /** Wrap 4-char tag name into ID. */ |
449 | | constexpr uint32_t MakeTag(uint8_t a, uint8_t b, uint8_t c, uint8_t d) { |
450 | | return a | (b << 8) | (c << 16) | (d << 24); |
451 | | } |
452 | | |
453 | | /** Reusable image data container. */ |
454 | | struct Pixels { |
455 | | // Use array instead of vector to avoid memory initialization. |
456 | | std::unique_ptr<uint8_t[]> pixels; |
457 | | size_t pixels_size = 0; |
458 | | std::vector<uint8_t*> rows; |
459 | | std::atomic<bool> has_error{false}; |
460 | | |
461 | | Status Resize(size_t row_bytes, size_t num_rows) { |
462 | | size_t new_size = row_bytes * num_rows; // it is assumed size is sane |
463 | | if (new_size > pixels_size) { |
464 | | pixels.reset(new uint8_t[new_size]); |
465 | | if (!pixels) { |
466 | | // TODO(szabadka): use specialized OOM error code |
467 | | return JXL_FAILURE("Failed to allocate memory for image buffer"); |
468 | | } |
469 | | pixels_size = new_size; |
470 | | } |
471 | | rows.resize(num_rows); |
472 | | for (size_t y = 0; y < num_rows; y++) { |
473 | | rows[y] = pixels.get() + y * row_bytes; |
474 | | } |
475 | | return true; |
476 | | } |
477 | | }; |
478 | | |
479 | | /** |
480 | | * Helper that chunks in-memory input. |
481 | | */ |
482 | | struct Reader { |
483 | | explicit Reader(Span<const uint8_t> data) : data_(data) {} |
484 | | |
485 | | const Span<const uint8_t> data_; |
486 | | size_t offset_ = 0; |
487 | | |
488 | | Bytes Peek(size_t len) const { |
489 | | size_t cap = data_.size() - offset_; |
490 | | size_t to_copy = std::min(cap, len); |
491 | | return {data_.data() + offset_, to_copy}; |
492 | | } |
493 | | |
494 | | Bytes Read(size_t len) { |
495 | | Bytes result = Peek(len); |
496 | | offset_ += result.size(); |
497 | | return result; |
498 | | } |
499 | | |
500 | | /* Returns empty Span on error. */ |
501 | | Bytes ReadChunk() { |
502 | | Bytes len = Peek(4); |
503 | | if (len.size() != 4) { |
504 | | return Bytes(); |
505 | | } |
506 | | const auto size = png_get_uint_32(len.data()); |
507 | | // NB: specification allows 2^31 - 1 |
508 | | constexpr size_t kMaxPNGChunkSize = 1u << 30; // 1 GB |
509 | | // Check first, to avoid overflow. |
510 | | if (size > kMaxPNGChunkSize) { |
511 | | JXL_WARNING("APNG chunk size is too big"); |
512 | | return Bytes(); |
513 | | } |
514 | | size_t full_size = size + 12; // size does not include itself, tag and CRC. |
515 | | Bytes result = Read(full_size); |
516 | | return (result.size() == full_size) ? result : Bytes(); |
517 | | } |
518 | | |
519 | | bool Eof() const { return offset_ == data_.size(); } |
520 | | }; |
521 | | |
522 | | void ProgressiveRead_OnInfo(png_structp png_ptr, png_infop info_ptr) { |
523 | | png_set_expand(png_ptr); |
524 | | png_set_palette_to_rgb(png_ptr); |
525 | | png_set_tRNS_to_alpha(png_ptr); |
526 | | (void)png_set_interlace_handling(png_ptr); |
527 | | png_read_update_info(png_ptr, info_ptr); |
528 | | } |
529 | | |
530 | | void ProgressiveRead_OnRow(png_structp png_ptr, png_bytep new_row, |
531 | | png_uint_32 row_num, int pass) { |
532 | | Pixels* frame = reinterpret_cast<Pixels*>(png_get_progressive_ptr(png_ptr)); |
533 | | if (!frame) { |
534 | | JXL_DEBUG_ABORT("Internal logic error"); |
535 | | return; |
536 | | } |
537 | | if (row_num >= frame->rows.size()) { |
538 | | frame->has_error = true; |
539 | | return; |
540 | | } |
541 | | png_progressive_combine_row(png_ptr, frame->rows[row_num], new_row); |
542 | | } |
543 | | |
544 | | // Holds intermediate state during parsing APNG file. |
545 | | struct Context { |
546 | | ~Context() { |
547 | | // Make sure png memory is released in any case. |
548 | | ResetPngDecoder(); |
549 | | } |
550 | | |
551 | | bool CreatePngDecoder() { |
552 | | png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, |
553 | | nullptr); |
554 | | info_ptr = png_create_info_struct(png_ptr); |
555 | | return (png_ptr != nullptr && info_ptr != nullptr); |
556 | | } |
557 | | |
558 | | /** |
559 | | * Initialize PNG decoder. |
560 | | * |
561 | | * TODO(eustas): add details |
562 | | */ |
563 | | bool InitPngDecoder(const std::vector<Bytes>& chunksInfo, |
564 | | const RectT<uint64_t>& viewport) { |
565 | | ResetPngDecoder(); |
566 | | |
567 | | png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, |
568 | | nullptr); |
569 | | info_ptr = png_create_info_struct(png_ptr); |
570 | | if (png_ptr == nullptr || info_ptr == nullptr) { |
571 | | return false; |
572 | | } |
573 | | |
574 | | if (setjmp(png_jmpbuf(png_ptr))) { |
575 | | return false; |
576 | | } |
577 | | |
578 | | /* hIST chunk tail is not processed properly; skip this chunk completely; |
579 | | see https://github.com/glennrp/libpng/pull/413 */ |
580 | | constexpr std::array<uint8_t, 5> kIgnoredChunks = {'h', 'I', 'S', 'T', 0}; |
581 | | png_set_keep_unknown_chunks(png_ptr, 1, kIgnoredChunks.data(), |
582 | | static_cast<int>(kIgnoredChunks.size() / 5)); |
583 | | |
584 | | png_set_crc_action(png_ptr, PNG_CRC_QUIET_USE, PNG_CRC_QUIET_USE); |
585 | | png_set_progressive_read_fn(png_ptr, static_cast<void*>(&frameRaw), |
586 | | ProgressiveRead_OnInfo, ProgressiveRead_OnRow, |
587 | | nullptr); |
588 | | |
589 | | png_process_data(png_ptr, info_ptr, |
590 | | const_cast<uint8_t*>(kPngSignature.data()), |
591 | | kPngSignature.size()); |
592 | | |
593 | | // Patch dimensions. |
594 | | png_save_uint_32(ihdr.data() + 8, static_cast<uint32_t>(viewport.xsize())); |
595 | | png_save_uint_32(ihdr.data() + 12, static_cast<uint32_t>(viewport.ysize())); |
596 | | png_process_data(png_ptr, info_ptr, ihdr.data(), ihdr.size()); |
597 | | |
598 | | for (const auto& chunk : chunksInfo) { |
599 | | png_process_data(png_ptr, info_ptr, const_cast<uint8_t*>(chunk.data()), |
600 | | chunk.size()); |
601 | | } |
602 | | |
603 | | return true; |
604 | | } |
605 | | |
606 | | /** |
607 | | * Pass chunk to PNG decoder. |
608 | | */ |
609 | | bool FeedChunks(const Bytes& chunk1, const Bytes& chunk2 = Bytes()) { |
610 | | // TODO(eustas): turn to DCHECK |
611 | | if (!png_ptr || !info_ptr) return false; |
612 | | |
613 | | if (setjmp(png_jmpbuf(png_ptr))) { |
614 | | return false; |
615 | | } |
616 | | |
617 | | for (const auto& chunk : {chunk1, chunk2}) { |
618 | | if (!chunk.empty()) { |
619 | | png_process_data(png_ptr, info_ptr, const_cast<uint8_t*>(chunk.data()), |
620 | | chunk.size()); |
621 | | } |
622 | | } |
623 | | return true; |
624 | | } |
625 | | |
626 | | bool FinalizeStream(PackedMetadata* metadata) { |
627 | | // TODO(eustas): turn to DCHECK |
628 | | if (!png_ptr || !info_ptr) return false; |
629 | | |
630 | | if (setjmp(png_jmpbuf(png_ptr))) { |
631 | | return false; |
632 | | } |
633 | | |
634 | | const std::array<uint8_t, 12> kFooter = {0, 0, 0, 0, 73, 69, |
635 | | 78, 68, 174, 66, 96, 130}; |
636 | | png_process_data(png_ptr, info_ptr, const_cast<uint8_t*>(kFooter.data()), |
637 | | kFooter.size()); |
638 | | // before destroying: check if we encountered any metadata chunks |
639 | | png_textp text_ptr = nullptr; |
640 | | int num_text = 0; |
641 | | if (png_get_text(png_ptr, info_ptr, &text_ptr, &num_text) != 0) { |
642 | | msan::UnpoisonMemory(text_ptr, sizeof(png_text_struct) * num_text); |
643 | | for (int i = 0; i < num_text; i++) { |
644 | | Status result = DecodeBlob(text_ptr[i], metadata); |
645 | | // Ignore unknown / malformed blob. |
646 | | (void)result; |
647 | | } |
648 | | } |
649 | | |
650 | | return true; |
651 | | } |
652 | | |
653 | | void ResetPngDecoder() { |
654 | | png_destroy_read_struct(&png_ptr, &info_ptr, nullptr); |
655 | | // Just in case. Not all versions on libpng wipe-out the pointers. |
656 | | png_ptr = nullptr; |
657 | | info_ptr = nullptr; |
658 | | } |
659 | | |
660 | | std::array<uint8_t, 25> ihdr; // (modified) copy of file IHDR chunk |
661 | | png_structp png_ptr = nullptr; |
662 | | png_infop info_ptr = nullptr; |
663 | | Pixels frameRaw = {}; |
664 | | }; |
665 | | |
666 | | enum class DisposeOp : uint8_t { NONE = 0, BACKGROUND = 1, PREVIOUS = 2 }; |
667 | | |
668 | | constexpr uint8_t kLastDisposeOp = static_cast<uint32_t>(DisposeOp::PREVIOUS); |
669 | | |
670 | | enum class BlendOp : uint8_t { SOURCE = 0, OVER = 1 }; |
671 | | |
672 | | constexpr uint8_t kLastBlendOp = static_cast<uint32_t>(BlendOp::OVER); |
673 | | |
674 | | // fcTL |
675 | | struct FrameControl { |
676 | | uint32_t delay_num; |
677 | | uint32_t delay_den; |
678 | | RectT<uint64_t> viewport; |
679 | | DisposeOp dispose_op; |
680 | | BlendOp blend_op; |
681 | | }; |
682 | | |
683 | | struct Frame { |
684 | | PackedImage pixels; |
685 | | FrameControl metadata; |
686 | | }; |
687 | | |
688 | | bool ValidateViewport(const RectT<uint64_t>& r) { |
689 | | constexpr uint32_t kMaxPngDim = 1000000UL; |
690 | | return (r.xsize() <= kMaxPngDim) && (r.ysize() <= kMaxPngDim); |
691 | | } |
692 | | |
693 | | /** |
694 | | * Setup #channels, bpp, colorspace, etc. from PNG values. |
695 | | */ |
696 | | void SetColorData(PackedPixelFile* ppf, uint8_t color_type, uint8_t bit_depth, |
697 | | png_color_8p sig_bits, uint32_t has_transparency) { |
698 | | bool palette_used = ((color_type & 1) != 0); |
699 | | bool color_used = ((color_type & 2) != 0); |
700 | | bool alpha_channel_used = ((color_type & 4) != 0); |
701 | | if (palette_used) { |
702 | | if (!color_used || alpha_channel_used) { |
703 | | JXL_DEBUG_V(2, "Unexpected PNG color type"); |
704 | | } |
705 | | } |
706 | | |
707 | | ppf->info.bits_per_sample = bit_depth; |
708 | | |
709 | | if (palette_used) { |
710 | | // palette will actually be 8-bit regardless of the index bitdepth |
711 | | ppf->info.bits_per_sample = 8; |
712 | | } |
713 | | if (color_used) { |
714 | | ppf->info.num_color_channels = 3; |
715 | | ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB; |
716 | | if (sig_bits) { |
717 | | if (sig_bits->red == sig_bits->green && |
718 | | sig_bits->green == sig_bits->blue) { |
719 | | ppf->info.bits_per_sample = sig_bits->red; |
720 | | } else { |
721 | | int maxbps = |
722 | | std::max(sig_bits->red, std::max(sig_bits->green, sig_bits->blue)); |
723 | | JXL_DEBUG_V(2, |
724 | | "sBIT chunk: bit depths for R, G, and B are not the same " |
725 | | "(%i %i %i), while in JPEG XL they have to be the same. " |
726 | | "Setting RGB bit depth to %i.", |
727 | | sig_bits->red, sig_bits->green, sig_bits->blue, maxbps); |
728 | | ppf->info.bits_per_sample = maxbps; |
729 | | } |
730 | | } |
731 | | } else { |
732 | | ppf->info.num_color_channels = 1; |
733 | | ppf->color_encoding.color_space = JXL_COLOR_SPACE_GRAY; |
734 | | if (sig_bits) ppf->info.bits_per_sample = sig_bits->gray; |
735 | | } |
736 | | if (alpha_channel_used || has_transparency) { |
737 | | ppf->info.alpha_bits = ppf->info.bits_per_sample; |
738 | | if (sig_bits && sig_bits->alpha != ppf->info.bits_per_sample) { |
739 | | JXL_DEBUG_V(2, |
740 | | "sBIT chunk: bit depths for RGBA are inconsistent " |
741 | | "(%i %i %i %i). Setting A bitdepth to %i.", |
742 | | sig_bits->red, sig_bits->green, sig_bits->blue, |
743 | | sig_bits->alpha, ppf->info.bits_per_sample); |
744 | | } |
745 | | } else { |
746 | | ppf->info.alpha_bits = 0; |
747 | | } |
748 | | ppf->color_encoding.color_space = (ppf->info.num_color_channels == 1) |
749 | | ? JXL_COLOR_SPACE_GRAY |
750 | | : JXL_COLOR_SPACE_RGB; |
751 | | } |
752 | | |
753 | | // Color profile chunks: cICP has the highest priority, followed by |
754 | | // iCCP and sRGB (which shouldn't co-exist, but if they do, we use |
755 | | // iCCP), followed finally by gAMA and cHRM. |
756 | | enum class ColorInfoType { |
757 | | NONE = 0, |
758 | | GAMA_OR_CHRM = 1, |
759 | | ICCP_OR_SRGB = 2, |
760 | | CICP = 3 |
761 | | }; |
762 | | |
763 | | } // namespace |
764 | | |
765 | | bool CanDecodeAPNG() { return true; } |
766 | | |
767 | | /** |
768 | | * Parse and decode PNG file. |
769 | | * |
770 | | * Useful PNG chunks: |
771 | | * acTL : animation control (#frames, loop count) |
772 | | * fcTL : frame control (seq#, viewport, delay, disposal blending) |
773 | | * bKGD : preferable background |
774 | | * IDAT : "default image" |
775 | | * if single fcTL goes before IDAT, then it is also first frame |
776 | | * fdAT : seq# + IDAT-like content |
777 | | * PLTE : palette |
778 | | * cICP : coding-independent code points for video signal type identification |
779 | | * iCCP : embedded ICC profile |
780 | | * sRGB : standard RGB colour space |
781 | | * eXIf : exchangeable image file profile |
782 | | * gAMA : image gamma |
783 | | * cHRM : primary chromaticities and white point |
784 | | * tRNS : transparency |
785 | | * |
786 | | * PNG chunk ordering: |
787 | | * - IHDR first |
788 | | * - IEND last |
789 | | * - acTL, cHRM, cICP, gAMA, iCCP, sRGB, bKGD, eXIf, PLTE before IDAT |
790 | | * - fdAT after IDAT |
791 | | * |
792 | | * More rules: |
793 | | * - iCCP and sRGB are exclusive |
794 | | * - fcTL and fdAT seq# must be in order fro 0, with no gaps or duplicates |
795 | | * - fcTL before corresponding IDAT / fdAT |
796 | | */ |
797 | | Status DecodeImageAPNG(const Span<const uint8_t> bytes, |
798 | | const ColorHints& color_hints, PackedPixelFile* ppf, |
799 | | const SizeConstraints* constraints) { |
800 | | // Initialize output (default settings in case e.g. only gAMA is given). |
801 | | ppf->frames.clear(); |
802 | | ppf->info.exponent_bits_per_sample = 0; |
803 | | ppf->info.alpha_exponent_bits = 0; |
804 | | ppf->info.orientation = JXL_ORIENT_IDENTITY; |
805 | | ppf->color_encoding.color_space = JXL_COLOR_SPACE_RGB; |
806 | | ppf->color_encoding.white_point = JXL_WHITE_POINT_D65; |
807 | | ppf->color_encoding.primaries = JXL_PRIMARIES_SRGB; |
808 | | ppf->color_encoding.transfer_function = JXL_TRANSFER_FUNCTION_SRGB; |
809 | | ppf->color_encoding.rendering_intent = JXL_RENDERING_INTENT_RELATIVE; |
810 | | |
811 | | Reader input(bytes); |
812 | | |
813 | | // Check signature. |
814 | | Bytes sig = input.Read(kPngSignature.size()); |
815 | | if (sig.size() != 8 || |
816 | | memcmp(sig.data(), kPngSignature.data(), kPngSignature.size()) != 0) { |
817 | | return false; // Return silently if it is not a PNG |
818 | | } |
819 | | |
820 | | // Check IHDR chunk. |
821 | | Context ctx; |
822 | | Bytes ihdr = input.ReadChunk(); |
823 | | if (ihdr.size() != ctx.ihdr.size()) { |
824 | | return JXL_FAILURE("Unexpected first chunk payload size"); |
825 | | } |
826 | | memcpy(ctx.ihdr.data(), ihdr.data(), ihdr.size()); |
827 | | uint32_t id = LoadLE32(ihdr.data() + 4); |
828 | | if (id != MakeTag('I', 'H', 'D', 'R')) { |
829 | | return JXL_FAILURE("First chunk is not IHDR"); |
830 | | } |
831 | | const RectT<uint64_t> image_rect(0, 0, png_get_uint_32(ihdr.data() + 8), |
832 | | png_get_uint_32(ihdr.data() + 12)); |
833 | | if (!ValidateViewport(image_rect)) { |
834 | | return JXL_FAILURE("PNG image dimensions are too large"); |
835 | | } |
836 | | |
837 | | // Chunks we supply to PNG decoder for every animation frame. |
838 | | std::vector<Bytes> passthrough_chunks; |
839 | | if (!ctx.InitPngDecoder(passthrough_chunks, image_rect)) { |
840 | | return JXL_FAILURE("Failed to initialize PNG decoder"); |
841 | | } |
842 | | |
843 | | // Marker that this PNG is animated. |
844 | | bool seen_actl = false; |
845 | | // First IDAT is a very important milestone; at this moment we freeze |
846 | | // gathered metadata. |
847 | | bool seen_idat = false; |
848 | | // fCTL can occur multiple times, but only once before IDAT. |
849 | | bool seen_fctl = false; |
850 | | // Logical EOF. |
851 | | bool seen_iend = false; |
852 | | |
853 | | ColorInfoType color_info_type = ColorInfoType::NONE; |
854 | | |
855 | | // Flag that we processed some IDAT / fDAT after image / frame start. |
856 | | bool seen_pixel_data = false; |
857 | | |
858 | | uint32_t num_channels; |
859 | | JxlPixelFormat format = {}; |
860 | | size_t bytes_per_pixel = 0; |
861 | | std::vector<Frame> frames; |
862 | | FrameControl current_frame = {/*delay_num=*/1, /*delay_den=*/10, image_rect, |
863 | | DisposeOp::NONE, BlendOp::SOURCE}; |
864 | | |
865 | | // Copies frame pixels / metadata from temporary storage. |
866 | | // TODO(eustas): avoid copying. |
867 | | const auto finalize_frame = [&]() -> Status { |
868 | | if (!seen_pixel_data) { |
869 | | return JXL_FAILURE("Frame / image without fdAT / IDAT chunks"); |
870 | | } |
871 | | if (!ctx.FinalizeStream(&ppf->metadata)) { |
872 | | return JXL_FAILURE("Failed to finalize PNG substream"); |
873 | | } |
874 | | if (ctx.frameRaw.has_error) { |
875 | | return JXL_FAILURE("Internal error"); |
876 | | } |
877 | | // Allocates the frame buffer. |
878 | | const RectT<uint64_t>& vp = current_frame.viewport; |
879 | | size_t xsize = static_cast<size_t>(vp.xsize()); |
880 | | size_t ysize = static_cast<size_t>(vp.ysize()); |
881 | | JXL_ASSIGN_OR_RETURN(PackedImage image, |
882 | | PackedImage::Create(xsize, ysize, format)); |
883 | | for (size_t y = 0; y < ysize; ++y) { |
884 | | // TODO(eustas): ensure multiplication is safe |
885 | | memcpy(static_cast<uint8_t*>(image.pixels()) + image.stride * y, |
886 | | ctx.frameRaw.rows[y], bytes_per_pixel * xsize); |
887 | | } |
888 | | frames.push_back(Frame{std::move(image), current_frame}); |
889 | | seen_pixel_data = false; |
890 | | return true; |
891 | | }; |
892 | | |
893 | | while (!input.Eof()) { |
894 | | if (seen_iend) { |
895 | | return JXL_FAILURE("Exuberant input after IEND chunk"); |
896 | | } |
897 | | Bytes chunk = input.ReadChunk(); |
898 | | if (chunk.empty()) { |
899 | | return JXL_FAILURE("Malformed chunk"); |
900 | | } |
901 | | Bytes type(chunk.data() + 4, 4); |
902 | | id = LoadLE32(type.data()); |
903 | | // Cut 'size' and 'type' at front and 'CRC' at the end. |
904 | | Bytes payload(chunk.data() + 8, chunk.size() - 12); |
905 | | |
906 | | if (!isAbc(type[0]) || !isAbc(type[1]) || !isAbc(type[2]) || |
907 | | !isAbc(type[3])) { |
908 | | return JXL_FAILURE("Exotic PNG chunk"); |
909 | | } |
910 | | |
911 | | switch (id) { |
912 | | case MakeTag('a', 'c', 'T', 'L'): |
913 | | if (seen_idat) { |
914 | | JXL_DEBUG_V(2, "aCTL after IDAT ignored"); |
915 | | continue; |
916 | | } |
917 | | if (seen_actl) { |
918 | | JXL_DEBUG_V(2, "Duplicate aCTL chunk ignored"); |
919 | | continue; |
920 | | } |
921 | | seen_actl = true; |
922 | | ppf->info.have_animation = JXL_TRUE; |
923 | | // TODO(eustas): decode from chunk? |
924 | | ppf->info.animation.tps_numerator = 1000; |
925 | | ppf->info.animation.tps_denominator = 1; |
926 | | continue; |
927 | | |
928 | | case MakeTag('I', 'E', 'N', 'D'): |
929 | | seen_iend = true; |
930 | | JXL_RETURN_IF_ERROR(finalize_frame()); |
931 | | continue; |
932 | | |
933 | | case MakeTag('f', 'c', 'T', 'L'): { |
934 | | if (payload.size() != 26) { |
935 | | return JXL_FAILURE("Unexpected fcTL payload size: %u", |
936 | | static_cast<uint32_t>(payload.size())); |
937 | | } |
938 | | if (seen_fctl && !seen_idat) { |
939 | | return JXL_FAILURE("More than one fcTL before IDAT"); |
940 | | } |
941 | | if (seen_idat && !seen_actl) { |
942 | | return JXL_FAILURE("fcTL after IDAT, but without acTL"); |
943 | | } |
944 | | seen_fctl = true; |
945 | | |
946 | | // TODO(eustas): check order? |
947 | | // sequence_number = png_get_uint_32(payload.data()); |
948 | | RectT<uint64_t> raw_viewport(png_get_uint_32(payload.data() + 12), |
949 | | png_get_uint_32(payload.data() + 16), |
950 | | png_get_uint_32(payload.data() + 4), |
951 | | png_get_uint_32(payload.data() + 8)); |
952 | | uint8_t dispose_op = payload[24]; |
953 | | if (dispose_op > kLastDisposeOp) { |
954 | | return JXL_FAILURE("Invalid DisposeOp"); |
955 | | } |
956 | | uint8_t blend_op = payload[25]; |
957 | | if (blend_op > kLastBlendOp) { |
958 | | return JXL_FAILURE("Invalid BlendOp"); |
959 | | } |
960 | | FrameControl next_frame = { |
961 | | /*delay_num=*/png_get_uint_16(payload.data() + 20), |
962 | | /*delay_den=*/png_get_uint_16(payload.data() + 22), raw_viewport, |
963 | | static_cast<DisposeOp>(dispose_op), static_cast<BlendOp>(blend_op)}; |
964 | | |
965 | | if (!raw_viewport.Intersection(image_rect).IsSame(raw_viewport)) { |
966 | | // Cropping happened. |
967 | | return JXL_FAILURE("PNG frame is outside of image rect"); |
968 | | } |
969 | | |
970 | | if (!seen_idat) { |
971 | | // "Default" image is the first animation frame. Its viewport must |
972 | | // cover the whole image area. |
973 | | if (!raw_viewport.IsSame(image_rect)) { |
974 | | return JXL_FAILURE( |
975 | | "If the first animation frame is default image, its viewport " |
976 | | "must cover full image"); |
977 | | } |
978 | | } else { |
979 | | JXL_RETURN_IF_ERROR(finalize_frame()); |
980 | | if (!ctx.InitPngDecoder(passthrough_chunks, next_frame.viewport)) { |
981 | | return JXL_FAILURE("Failed to initialize PNG decoder"); |
982 | | } |
983 | | } |
984 | | current_frame = next_frame; |
985 | | continue; |
986 | | } |
987 | | |
988 | | case MakeTag('I', 'D', 'A', 'T'): { |
989 | | if (!frames.empty()) { |
990 | | return JXL_FAILURE("IDAT after default image is over"); |
991 | | } |
992 | | if (!seen_idat) { |
993 | | // First IDAT means that all metadata is ready. |
994 | | seen_idat = true; |
995 | | JXL_ENSURE(image_rect.xsize() == |
996 | | png_get_image_width(ctx.png_ptr, ctx.info_ptr)); |
997 | | JXL_ENSURE(image_rect.ysize() == |
998 | | png_get_image_height(ctx.png_ptr, ctx.info_ptr)); |
999 | | JXL_RETURN_IF_ERROR(VerifyDimensions(constraints, image_rect.xsize(), |
1000 | | image_rect.ysize())); |
1001 | | ppf->info.xsize = image_rect.xsize(); |
1002 | | ppf->info.ysize = image_rect.ysize(); |
1003 | | |
1004 | | png_color_8p sig_bits = nullptr; |
1005 | | // Error is OK -> sig_bits remains nullptr. |
1006 | | png_get_sBIT(ctx.png_ptr, ctx.info_ptr, &sig_bits); |
1007 | | SetColorData(ppf, png_get_color_type(ctx.png_ptr, ctx.info_ptr), |
1008 | | png_get_bit_depth(ctx.png_ptr, ctx.info_ptr), sig_bits, |
1009 | | png_get_valid(ctx.png_ptr, ctx.info_ptr, PNG_INFO_tRNS)); |
1010 | | num_channels = |
1011 | | ppf->info.num_color_channels + (ppf->info.alpha_bits ? 1 : 0); |
1012 | | format = { |
1013 | | /*num_channels=*/num_channels, |
1014 | | /*data_type=*/ppf->info.bits_per_sample > 8 ? JXL_TYPE_UINT16 |
1015 | | : JXL_TYPE_UINT8, |
1016 | | /*endianness=*/JXL_BIG_ENDIAN, |
1017 | | /*align=*/0, |
1018 | | }; |
1019 | | bytes_per_pixel = |
1020 | | num_channels * (format.data_type == JXL_TYPE_UINT16 ? 2 : 1); |
1021 | | // TODO(eustas): ensure multiplication is safe |
1022 | | uint64_t row_bytes = |
1023 | | static_cast<uint64_t>(image_rect.xsize()) * bytes_per_pixel; |
1024 | | uint64_t max_rows = std::numeric_limits<size_t>::max() / row_bytes; |
1025 | | if (image_rect.ysize() > max_rows) { |
1026 | | return JXL_FAILURE("Image too big."); |
1027 | | } |
1028 | | // TODO(eustas): drop frameRaw |
1029 | | JXL_RETURN_IF_ERROR( |
1030 | | ctx.frameRaw.Resize(row_bytes, image_rect.ysize())); |
1031 | | } |
1032 | | |
1033 | | if (!ctx.FeedChunks(chunk)) { |
1034 | | return JXL_FAILURE("Decoding IDAT failed"); |
1035 | | } |
1036 | | seen_pixel_data = true; |
1037 | | continue; |
1038 | | } |
1039 | | |
1040 | | case MakeTag('f', 'd', 'A', 'T'): { |
1041 | | if (!seen_idat) { |
1042 | | return JXL_FAILURE("fdAT chunk before IDAT"); |
1043 | | } |
1044 | | if (!seen_actl) { |
1045 | | return JXL_FAILURE("fdAT chunk before acTL"); |
1046 | | } |
1047 | | /* The 'fdAT' chunk has... the same structure as an 'IDAT' chunk, |
1048 | | * except preceded by a sequence number. */ |
1049 | | if (payload.size() < 4) { |
1050 | | return JXL_FAILURE("Corrupted fdAT chunk"); |
1051 | | } |
1052 | | // Turn 'fdAT' to 'IDAT' by cutting sequence number and replacing tag. |
1053 | | std::array<uint8_t, 8> preamble; |
1054 | | png_save_uint_32(preamble.data(), payload.size() - 4); |
1055 | | memcpy(preamble.data() + 4, "IDAT", 4); |
1056 | | // Cut-off 'size', 'type' and 'sequence_number' |
1057 | | Bytes chunk_tail(chunk.data() + 12, chunk.size() - 12); |
1058 | | if (!ctx.FeedChunks(Bytes(preamble), chunk_tail)) { |
1059 | | return JXL_FAILURE("Decoding fdAT failed"); |
1060 | | } |
1061 | | seen_pixel_data = true; |
1062 | | continue; |
1063 | | } |
1064 | | |
1065 | | case MakeTag('c', 'I', 'C', 'P'): |
1066 | | if (color_info_type == ColorInfoType::CICP) { |
1067 | | JXL_DEBUG_V(2, "Excessive colorspace definition; cICP chunk ignored"); |
1068 | | continue; |
1069 | | } |
1070 | | JXL_RETURN_IF_ERROR(DecodeCicpChunk(payload, &ppf->color_encoding)); |
1071 | | ppf->icc.clear(); |
1072 | | ppf->primary_color_representation = |
1073 | | PackedPixelFile::kColorEncodingIsPrimary; |
1074 | | color_info_type = ColorInfoType::CICP; |
1075 | | continue; |
1076 | | |
1077 | | case MakeTag('i', 'C', 'C', 'P'): { |
1078 | | if (color_info_type == ColorInfoType::ICCP_OR_SRGB) { |
1079 | | return JXL_FAILURE("Repeated iCCP / sRGB chunk"); |
1080 | | } |
1081 | | if (color_info_type > ColorInfoType::ICCP_OR_SRGB) { |
1082 | | JXL_DEBUG_V(2, "Excessive colorspace definition; iCCP chunk ignored"); |
1083 | | continue; |
1084 | | } |
1085 | | // Let PNG decoder deal with chunk processing. |
1086 | | if (!ctx.FeedChunks(chunk)) { |
1087 | | return JXL_FAILURE("Corrupt iCCP chunk"); |
1088 | | } |
1089 | | |
1090 | | // TODO(jon): catch special case of PQ and synthesize color encoding |
1091 | | // in that case |
1092 | | int compression_type = 0; |
1093 | | png_bytep profile = nullptr; |
1094 | | png_charp name = nullptr; |
1095 | | png_uint_32 profile_len = 0; |
1096 | | png_uint_32 ok = |
1097 | | png_get_iCCP(ctx.png_ptr, ctx.info_ptr, &name, &compression_type, |
1098 | | &profile, &profile_len); |
1099 | | if (!ok || !profile_len) { |
1100 | | return JXL_FAILURE("Malformed / incomplete iCCP chunk"); |
1101 | | } |
1102 | | ppf->icc.assign(profile, profile + profile_len); |
1103 | | ppf->primary_color_representation = PackedPixelFile::kIccIsPrimary; |
1104 | | color_info_type = ColorInfoType::ICCP_OR_SRGB; |
1105 | | continue; |
1106 | | } |
1107 | | |
1108 | | case MakeTag('s', 'R', 'G', 'B'): |
1109 | | if (color_info_type == ColorInfoType::ICCP_OR_SRGB) { |
1110 | | return JXL_FAILURE("Repeated iCCP / sRGB chunk"); |
1111 | | } |
1112 | | if (color_info_type > ColorInfoType::ICCP_OR_SRGB) { |
1113 | | JXL_DEBUG_V(2, "Excessive colorspace definition; sRGB chunk ignored"); |
1114 | | continue; |
1115 | | } |
1116 | | JXL_RETURN_IF_ERROR(DecodeSrgbChunk(payload, &ppf->color_encoding)); |
1117 | | color_info_type = ColorInfoType::ICCP_OR_SRGB; |
1118 | | continue; |
1119 | | |
1120 | | case MakeTag('g', 'A', 'M', 'A'): |
1121 | | if (color_info_type >= ColorInfoType::GAMA_OR_CHRM) { |
1122 | | JXL_DEBUG_V(2, "Excessive colorspace definition; gAMA chunk ignored"); |
1123 | | continue; |
1124 | | } |
1125 | | JXL_RETURN_IF_ERROR(DecodeGamaChunk(payload, &ppf->color_encoding)); |
1126 | | color_info_type = ColorInfoType::GAMA_OR_CHRM; |
1127 | | continue; |
1128 | | |
1129 | | case MakeTag('c', 'H', 'R', 'M'): |
1130 | | if (color_info_type >= ColorInfoType::GAMA_OR_CHRM) { |
1131 | | JXL_DEBUG_V(2, "Excessive colorspace definition; cHRM chunk ignored"); |
1132 | | continue; |
1133 | | } |
1134 | | JXL_RETURN_IF_ERROR(DecodeChrmChunk(payload, &ppf->color_encoding)); |
1135 | | color_info_type = ColorInfoType::GAMA_OR_CHRM; |
1136 | | continue; |
1137 | | |
1138 | | case MakeTag('c', 'L', 'L', 'i'): |
1139 | | JXL_RETURN_IF_ERROR( |
1140 | | DecodeClliChunk(payload, &ppf->info.intensity_target)); |
1141 | | continue; |
1142 | | |
1143 | | case MakeTag('e', 'X', 'I', 'f'): |
1144 | | // TODO(eustas): next eXIF chunk overwrites current; is it ok? |
1145 | | ppf->metadata.exif.resize(payload.size()); |
1146 | | memcpy(ppf->metadata.exif.data(), payload.data(), payload.size()); |
1147 | | continue; |
1148 | | |
1149 | | default: |
1150 | | // We don't know what is that, just pass through. |
1151 | | if (!ctx.FeedChunks(chunk)) { |
1152 | | return JXL_FAILURE("PNG decoder failed to process chunk"); |
1153 | | } |
1154 | | // If it happens before IDAT, we consider it metadata and pass to all |
1155 | | // sub-decoders. |
1156 | | if (!seen_idat) { |
1157 | | passthrough_chunks.push_back(chunk); |
1158 | | } |
1159 | | continue; |
1160 | | } |
1161 | | } |
1162 | | |
1163 | | bool color_is_already_set = (color_info_type != ColorInfoType::NONE); |
1164 | | bool is_gray = (ppf->info.num_color_channels == 1); |
1165 | | JXL_RETURN_IF_ERROR( |
1166 | | ApplyColorHints(color_hints, color_is_already_set, is_gray, ppf)); |
1167 | | |
1168 | | if (ppf->color_encoding.transfer_function != JXL_TRANSFER_FUNCTION_PQ) { |
1169 | | // Reset intensity target, in case we set it from cLLi but TF is not PQ. |
1170 | | ppf->info.intensity_target = 0.f; |
1171 | | } |
1172 | | |
1173 | | bool has_nontrivial_background = false; |
1174 | | bool previous_frame_should_be_cleared = false; |
1175 | | for (size_t i = 0; i < frames.size(); i++) { |
1176 | | Frame& frame = frames[i]; |
1177 | | const FrameControl& fc = frame.metadata; |
1178 | | const RectT<uint64_t> vp = fc.viewport; |
1179 | | const auto& pixels = frame.pixels; |
1180 | | size_t xsize = pixels.xsize; |
1181 | | size_t ysize = pixels.ysize; |
1182 | | JXL_ENSURE(xsize == vp.xsize()); |
1183 | | JXL_ENSURE(ysize == vp.ysize()); |
1184 | | |
1185 | | // Before encountering a DISPOSE_OP_NONE frame, the canvas is filled with |
1186 | | // 0, so DISPOSE_OP_BACKGROUND and DISPOSE_OP_PREVIOUS are equivalent. |
1187 | | if (fc.dispose_op == DisposeOp::NONE) { |
1188 | | has_nontrivial_background = true; |
1189 | | } |
1190 | | bool should_blend = fc.blend_op == BlendOp::OVER; |
1191 | | bool use_for_next_frame = |
1192 | | has_nontrivial_background && fc.dispose_op != DisposeOp::PREVIOUS; |
1193 | | size_t x0 = vp.x0(); |
1194 | | size_t y0 = vp.y0(); |
1195 | | if (previous_frame_should_be_cleared) { |
1196 | | const auto& pvp = frames[i - 1].metadata.viewport; |
1197 | | size_t px0 = pvp.x0(); |
1198 | | size_t py0 = pvp.y0(); |
1199 | | size_t pxs = pvp.xsize(); |
1200 | | size_t pys = pvp.ysize(); |
1201 | | if (px0 >= x0 && py0 >= y0 && px0 + pxs <= x0 + xsize && |
1202 | | py0 + pys <= y0 + ysize && fc.blend_op == BlendOp::SOURCE && |
1203 | | use_for_next_frame) { |
1204 | | // If the previous frame is entirely contained in the current frame |
1205 | | // and we are using BLEND_OP_SOURCE, nothing special needs to be done. |
1206 | | ppf->frames.emplace_back(std::move(frame.pixels)); |
1207 | | } else if (px0 == x0 && py0 == y0 && px0 + pxs == x0 + xsize && |
1208 | | py0 + pys == y0 + ysize && use_for_next_frame) { |
1209 | | // If the new frame has the same size as the old one, but we are |
1210 | | // blending, we can instead just not blend. |
1211 | | should_blend = false; |
1212 | | ppf->frames.emplace_back(std::move(frame.pixels)); |
1213 | | } else if (px0 <= x0 && py0 <= y0 && px0 + pxs >= x0 + xsize && |
1214 | | py0 + pys >= y0 + ysize && use_for_next_frame) { |
1215 | | // If the new frame is contained within the old frame, we can pad the |
1216 | | // new frame with zeros and not blend. |
1217 | | JXL_ASSIGN_OR_RETURN(PackedImage new_data, |
1218 | | PackedImage::Create(pxs, pys, pixels.format)); |
1219 | | memset(new_data.pixels(), 0, new_data.pixels_size); |
1220 | | for (size_t y = 0; y < ysize; y++) { |
1221 | | JXL_RETURN_IF_ERROR( |
1222 | | PackedImage::ValidateDataType(new_data.format.data_type)); |
1223 | | size_t bytes_per_pixel = |
1224 | | PackedImage::BitsPerChannel(new_data.format.data_type) * |
1225 | | new_data.format.num_channels / 8; |
1226 | | memcpy( |
1227 | | static_cast<uint8_t*>(new_data.pixels()) + |
1228 | | new_data.stride * (y + y0 - py0) + |
1229 | | bytes_per_pixel * (x0 - px0), |
1230 | | static_cast<const uint8_t*>(pixels.pixels()) + pixels.stride * y, |
1231 | | xsize * bytes_per_pixel); |
1232 | | } |
1233 | | |
1234 | | x0 = px0; |
1235 | | y0 = py0; |
1236 | | xsize = pxs; |
1237 | | ysize = pys; |
1238 | | should_blend = false; |
1239 | | ppf->frames.emplace_back(std::move(new_data)); |
1240 | | } else { |
1241 | | // If all else fails, insert a placeholder blank frame with kReplace. |
1242 | | JXL_ASSIGN_OR_RETURN(PackedImage blank, |
1243 | | PackedImage::Create(pxs, pys, pixels.format)); |
1244 | | memset(blank.pixels(), 0, blank.pixels_size); |
1245 | | ppf->frames.emplace_back(std::move(blank)); |
1246 | | auto& pframe = ppf->frames.back(); |
1247 | | pframe.frame_info.layer_info.crop_x0 = px0; |
1248 | | pframe.frame_info.layer_info.crop_y0 = py0; |
1249 | | pframe.frame_info.layer_info.xsize = pxs; |
1250 | | pframe.frame_info.layer_info.ysize = pys; |
1251 | | pframe.frame_info.duration = 0; |
1252 | | bool is_full_size = px0 == 0 && py0 == 0 && pxs == ppf->info.xsize && |
1253 | | pys == ppf->info.ysize; |
1254 | | pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1; |
1255 | | pframe.frame_info.layer_info.blend_info.blendmode = JXL_BLEND_REPLACE; |
1256 | | pframe.frame_info.layer_info.blend_info.source = 1; |
1257 | | pframe.frame_info.layer_info.save_as_reference = 1; |
1258 | | ppf->frames.emplace_back(std::move(frame.pixels)); |
1259 | | } |
1260 | | } else { |
1261 | | ppf->frames.emplace_back(std::move(frame.pixels)); |
1262 | | } |
1263 | | |
1264 | | auto& pframe = ppf->frames.back(); |
1265 | | pframe.frame_info.layer_info.crop_x0 = x0; |
1266 | | pframe.frame_info.layer_info.crop_y0 = y0; |
1267 | | pframe.frame_info.layer_info.xsize = xsize; |
1268 | | pframe.frame_info.layer_info.ysize = ysize; |
1269 | | pframe.frame_info.duration = |
1270 | | fc.delay_num * 1000 / (fc.delay_den ? fc.delay_den : 100); |
1271 | | pframe.frame_info.layer_info.blend_info.blendmode = |
1272 | | should_blend ? JXL_BLEND_BLEND : JXL_BLEND_REPLACE; |
1273 | | bool is_full_size = x0 == 0 && y0 == 0 && xsize == ppf->info.xsize && |
1274 | | ysize == ppf->info.ysize; |
1275 | | pframe.frame_info.layer_info.have_crop = is_full_size ? 0 : 1; |
1276 | | pframe.frame_info.layer_info.blend_info.source = 1; |
1277 | | pframe.frame_info.layer_info.blend_info.alpha = 0; |
1278 | | pframe.frame_info.layer_info.save_as_reference = use_for_next_frame ? 1 : 0; |
1279 | | |
1280 | | previous_frame_should_be_cleared = |
1281 | | has_nontrivial_background && (fc.dispose_op == DisposeOp::BACKGROUND); |
1282 | | } |
1283 | | |
1284 | | if (ppf->frames.empty()) return JXL_FAILURE("No frames decoded"); |
1285 | | ppf->frames.back().frame_info.is_last = JXL_TRUE; |
1286 | | |
1287 | | return true; |
1288 | | } |
1289 | | |
1290 | | #endif // JPEGXL_ENABLE_APNG |
1291 | | |
1292 | | } // namespace extras |
1293 | | } // namespace jxl |