/src/libjxl/lib/extras/enc/apng.cc
Line | Count | Source |
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 | | // Parts of this code are taken from apngdis, which has the following license: |
7 | | /* APNG Disassembler 2.8 |
8 | | * |
9 | | * Deconstructs APNG files into individual frames. |
10 | | * |
11 | | * http://apngdis.sourceforge.net |
12 | | * |
13 | | * Copyright (c) 2010-2015 Max Stepin |
14 | | * maxst at users.sourceforge.net |
15 | | * |
16 | | * zlib license |
17 | | * ------------ |
18 | | * |
19 | | * This software is provided 'as-is', without any express or implied |
20 | | * warranty. In no event will the authors be held liable for any damages |
21 | | * arising from the use of this software. |
22 | | * |
23 | | * Permission is granted to anyone to use this software for any purpose, |
24 | | * including commercial applications, and to alter it and redistribute it |
25 | | * freely, subject to the following restrictions: |
26 | | * |
27 | | * 1. The origin of this software must not be misrepresented; you must not |
28 | | * claim that you wrote the original software. If you use this software |
29 | | * in a product, an acknowledgment in the product documentation would be |
30 | | * appreciated but is not required. |
31 | | * 2. Altered source versions must be plainly marked as such, and must not be |
32 | | * misrepresented as being the original software. |
33 | | * 3. This notice may not be removed or altered from any source distribution. |
34 | | * |
35 | | */ |
36 | | |
37 | | #include "lib/extras/enc/apng.h" |
38 | | |
39 | | // IWYU pragma: no_include <pngconf.h> |
40 | | |
41 | | #include <memory> |
42 | | |
43 | | #include "lib/extras/enc/encode.h" |
44 | | |
45 | | #if !JPEGXL_ENABLE_APNG |
46 | | |
47 | | namespace jxl { |
48 | | namespace extras { |
49 | 0 | std::unique_ptr<Encoder> GetAPNGEncoder() { return nullptr; } |
50 | | } // namespace extras |
51 | | } // namespace jxl |
52 | | |
53 | | #else // JPEGXL_ENABLE_APNG |
54 | | |
55 | | #include <jxl/codestream_header.h> |
56 | | #include <jxl/color_encoding.h> |
57 | | #include <jxl/types.h> |
58 | | |
59 | | #include <cmath> |
60 | | #include <cstdint> |
61 | | #include <cstdio> |
62 | | #include <cstring> |
63 | | #include <string> |
64 | | #include <vector> |
65 | | |
66 | | #include "lib/extras/exif.h" |
67 | | #include "lib/extras/packed_image.h" |
68 | | #include "lib/jxl/base/byte_order.h" |
69 | | #include "lib/jxl/base/common.h" |
70 | | #include "lib/jxl/base/compiler_specific.h" |
71 | | #include "lib/jxl/base/data_parallel.h" |
72 | | #include "lib/jxl/base/printf_macros.h" |
73 | | #include "lib/jxl/base/status.h" |
74 | | #include "png.h" /* original (unpatched) libpng is ok */ |
75 | | |
76 | | namespace jxl { |
77 | | namespace extras { |
78 | | |
79 | | namespace { |
80 | | |
81 | | constexpr unsigned char kExifSignature[6] = {0x45, 0x78, 0x69, |
82 | | 0x66, 0x00, 0x00}; |
83 | | |
84 | | class APNGEncoder : public Encoder { |
85 | | public: |
86 | | std::vector<JxlPixelFormat> AcceptedFormats() const override { |
87 | | std::vector<JxlPixelFormat> formats; |
88 | | for (const uint32_t num_channels : {1, 2, 3, 4}) { |
89 | | for (const JxlDataType data_type : |
90 | | {JXL_TYPE_UINT8, JXL_TYPE_UINT16, JXL_TYPE_FLOAT}) { |
91 | | for (JxlEndianness endianness : {JXL_BIG_ENDIAN, JXL_LITTLE_ENDIAN}) { |
92 | | formats.push_back( |
93 | | JxlPixelFormat{num_channels, data_type, endianness, /*align=*/0}); |
94 | | } |
95 | | } |
96 | | } |
97 | | return formats; |
98 | | } |
99 | | Status Encode(const PackedPixelFile& ppf, EncodedImage* encoded_image, |
100 | | ThreadPool* pool) const override { |
101 | | // Encode main image frames |
102 | | JXL_RETURN_IF_ERROR(VerifyBasicInfo(ppf.info)); |
103 | | encoded_image->icc.clear(); |
104 | | JXL_RETURN_IF_ERROR( |
105 | | EncodePackedPixelFileToAPNG(ppf, pool, &encoded_image->bitstreams)); |
106 | | |
107 | | // Encode extra channels |
108 | | for (size_t i = 0; i < ppf.extra_channels_info.size(); ++i) { |
109 | | encoded_image->extra_channel_bitstreams.emplace_back(); |
110 | | auto& ec_bitstreams = encoded_image->extra_channel_bitstreams.back(); |
111 | | JXL_RETURN_IF_ERROR( |
112 | | EncodePackedPixelFileToAPNG(ppf, pool, &ec_bitstreams, true, i)); |
113 | | } |
114 | | return true; |
115 | | } |
116 | | |
117 | | private: |
118 | | Status EncodePackedPixelFileToAPNG( |
119 | | const PackedPixelFile& ppf, ThreadPool* pool, |
120 | | std::vector<std::vector<uint8_t> >* bitstreams, |
121 | | bool encode_extra_channels = false, size_t extra_channel_index = 0) const; |
122 | | }; |
123 | | |
124 | | void PngWrite(png_structp png_ptr, png_bytep data, png_size_t length) { |
125 | | std::vector<uint8_t>* bytes = |
126 | | static_cast<std::vector<uint8_t>*>(png_get_io_ptr(png_ptr)); |
127 | | bytes->insert(bytes->end(), data, data + length); |
128 | | } |
129 | | |
130 | | // Stores XMP and EXIF/IPTC into key/value strings for PNG |
131 | | class BlobsWriterPNG { |
132 | | public: |
133 | | static Status Encode(const PackedMetadata& blobs, |
134 | | std::vector<std::string>* strings) { |
135 | | if (!blobs.exif.empty()) { |
136 | | // PNG viewers typically ignore Exif orientation but not all of them do |
137 | | // (and e.g. cjxl doesn't), so we overwrite the Exif orientation to the |
138 | | // identity to avoid repeated orientation. |
139 | | std::vector<uint8_t> exif = blobs.exif; |
140 | | ResetExifOrientation(exif); |
141 | | // By convention, the data is prefixed with "Exif\0\0" when stored in |
142 | | // the legacy (and non-standard) "Raw profile type exif" text chunk |
143 | | // currently used here. |
144 | | // TODO(user): Store Exif data in an eXIf chunk instead, which always |
145 | | // begins with the TIFF header. |
146 | | if (exif.size() >= sizeof kExifSignature && |
147 | | memcmp(exif.data(), kExifSignature, sizeof kExifSignature) != 0) { |
148 | | exif.insert(exif.begin(), kExifSignature, |
149 | | kExifSignature + sizeof kExifSignature); |
150 | | } |
151 | | JXL_RETURN_IF_ERROR(EncodeBase16("exif", exif, strings)); |
152 | | } |
153 | | if (!blobs.iptc.empty()) { |
154 | | JXL_RETURN_IF_ERROR(EncodeBase16("iptc", blobs.iptc, strings)); |
155 | | } |
156 | | if (!blobs.xmp.empty()) { |
157 | | // TODO(user): Store XMP data in an "XML:com.adobe.xmp" text chunk |
158 | | // instead. |
159 | | JXL_RETURN_IF_ERROR(EncodeBase16("xmp", blobs.xmp, strings)); |
160 | | } |
161 | | return true; |
162 | | } |
163 | | |
164 | | private: |
165 | | // TODO(eustas): use array |
166 | | static JXL_INLINE char EncodeNibble(const uint8_t nibble) { |
167 | | if (nibble < 16) { |
168 | | return (nibble < 10) ? '0' + nibble : 'a' + nibble - 10; |
169 | | } else { |
170 | | JXL_DEBUG_ABORT("Internal logic error"); |
171 | | return 0; |
172 | | } |
173 | | } |
174 | | |
175 | | static Status EncodeBase16(const std::string& type, |
176 | | const std::vector<uint8_t>& bytes, |
177 | | std::vector<std::string>* strings) { |
178 | | // Encoding: base16 with newline after 72 chars. |
179 | | const size_t base16_size = |
180 | | 2 * bytes.size() + DivCeil(bytes.size(), static_cast<size_t>(36)) + 1; |
181 | | std::string base16; |
182 | | base16.reserve(base16_size); |
183 | | for (size_t i = 0; i < bytes.size(); ++i) { |
184 | | if (i % 36 == 0) base16.push_back('\n'); |
185 | | base16.push_back(EncodeNibble(bytes[i] >> 4)); |
186 | | base16.push_back(EncodeNibble(bytes[i] & 0x0F)); |
187 | | } |
188 | | base16.push_back('\n'); |
189 | | JXL_ENSURE(base16.length() == base16_size); |
190 | | |
191 | | char key[30]; |
192 | | snprintf(key, sizeof(key), "Raw profile type %s", type.c_str()); |
193 | | |
194 | | char header[30]; |
195 | | snprintf(header, sizeof(header), "\n%s\n%8" PRIuS, type.c_str(), |
196 | | bytes.size()); |
197 | | |
198 | | strings->emplace_back(key); |
199 | | strings->push_back(std::string(header) + base16); |
200 | | return true; |
201 | | } |
202 | | }; |
203 | | |
204 | | void MaybeAddCICP(const JxlColorEncoding& c_enc, png_structp png_ptr, |
205 | | png_infop info_ptr) { |
206 | | png_byte cicp_data[4] = {}; |
207 | | png_unknown_chunk cicp_chunk; |
208 | | if (c_enc.color_space != JXL_COLOR_SPACE_RGB) { |
209 | | return; |
210 | | } |
211 | | if (c_enc.primaries == JXL_PRIMARIES_P3) { |
212 | | if (c_enc.white_point == JXL_WHITE_POINT_D65) { |
213 | | cicp_data[0] = 12; |
214 | | } else if (c_enc.white_point == JXL_WHITE_POINT_DCI) { |
215 | | cicp_data[0] = 11; |
216 | | } else { |
217 | | return; |
218 | | } |
219 | | } else if (c_enc.primaries != JXL_PRIMARIES_CUSTOM && |
220 | | c_enc.white_point == JXL_WHITE_POINT_D65) { |
221 | | cicp_data[0] = static_cast<png_byte>(c_enc.primaries); |
222 | | } else { |
223 | | return; |
224 | | } |
225 | | if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_UNKNOWN || |
226 | | c_enc.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) { |
227 | | return; |
228 | | } |
229 | | cicp_data[1] = static_cast<png_byte>(c_enc.transfer_function); |
230 | | cicp_data[2] = 0; |
231 | | cicp_data[3] = 1; |
232 | | cicp_chunk.data = cicp_data; |
233 | | cicp_chunk.size = sizeof(cicp_data); |
234 | | cicp_chunk.location = PNG_HAVE_IHDR; |
235 | | memcpy(cicp_chunk.name, "cICP", 5); |
236 | | png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS, |
237 | | reinterpret_cast<const png_byte*>("cICP"), 1); |
238 | | png_set_unknown_chunks(png_ptr, info_ptr, &cicp_chunk, 1); |
239 | | } |
240 | | |
241 | | bool MaybeAddSRGB(const JxlColorEncoding& c_enc, png_structp png_ptr, |
242 | | png_infop info_ptr) { |
243 | | if (c_enc.transfer_function == JXL_TRANSFER_FUNCTION_SRGB && |
244 | | (c_enc.color_space == JXL_COLOR_SPACE_GRAY || |
245 | | (c_enc.color_space == JXL_COLOR_SPACE_RGB && |
246 | | c_enc.primaries == JXL_PRIMARIES_SRGB && |
247 | | c_enc.white_point == JXL_WHITE_POINT_D65))) { |
248 | | png_set_sRGB(png_ptr, info_ptr, c_enc.rendering_intent); |
249 | | png_set_cHRM_fixed(png_ptr, info_ptr, 31270, 32900, 64000, 33000, 30000, |
250 | | 60000, 15000, 6000); |
251 | | png_set_gAMA_fixed(png_ptr, info_ptr, 45455); |
252 | | return true; |
253 | | } |
254 | | return false; |
255 | | } |
256 | | |
257 | | void MaybeAddCHRM(const JxlColorEncoding& c_enc, png_structp png_ptr, |
258 | | png_infop info_ptr) { |
259 | | if (c_enc.color_space != JXL_COLOR_SPACE_RGB) return; |
260 | | if (c_enc.primaries == 0) return; |
261 | | png_set_cHRM(png_ptr, info_ptr, c_enc.white_point_xy[0], |
262 | | c_enc.white_point_xy[1], c_enc.primaries_red_xy[0], |
263 | | c_enc.primaries_red_xy[1], c_enc.primaries_green_xy[0], |
264 | | c_enc.primaries_green_xy[1], c_enc.primaries_blue_xy[0], |
265 | | c_enc.primaries_blue_xy[1]); |
266 | | } |
267 | | |
268 | | void MaybeAddGAMA(const JxlColorEncoding& c_enc, png_structp png_ptr, |
269 | | png_infop info_ptr) { |
270 | | switch (c_enc.transfer_function) { |
271 | | case JXL_TRANSFER_FUNCTION_LINEAR: |
272 | | png_set_gAMA_fixed(png_ptr, info_ptr, PNG_FP_1); |
273 | | break; |
274 | | case JXL_TRANSFER_FUNCTION_SRGB: |
275 | | png_set_gAMA_fixed(png_ptr, info_ptr, 45455); |
276 | | break; |
277 | | case JXL_TRANSFER_FUNCTION_GAMMA: |
278 | | png_set_gAMA(png_ptr, info_ptr, c_enc.gamma); |
279 | | break; |
280 | | |
281 | | default:; |
282 | | // No gAMA chunk. |
283 | | } |
284 | | } |
285 | | |
286 | | void MaybeAddCLLi(const JxlColorEncoding& c_enc, const float intensity_target, |
287 | | png_structp png_ptr, png_infop info_ptr) { |
288 | | if (c_enc.transfer_function != JXL_TRANSFER_FUNCTION_PQ) return; |
289 | | if (intensity_target == 10000) return; |
290 | | |
291 | | const uint32_t max_content_light_level = |
292 | | static_cast<uint32_t>(10000.f * Clamp1(intensity_target, 0.f, 10000.f)); |
293 | | png_byte chunk_data[8] = {}; |
294 | | png_save_uint_32(chunk_data, max_content_light_level); |
295 | | // Leave MaxFALL set to 0. |
296 | | png_unknown_chunk chunk; |
297 | | memcpy(chunk.name, "cLLI", 5); |
298 | | chunk.data = chunk_data; |
299 | | chunk.size = sizeof chunk_data; |
300 | | chunk.location = PNG_HAVE_IHDR; |
301 | | png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS, |
302 | | reinterpret_cast<const png_byte*>("cLLI"), 1); |
303 | | png_set_unknown_chunks(png_ptr, info_ptr, &chunk, 1); |
304 | | } |
305 | | |
306 | | Status APNGEncoder::EncodePackedPixelFileToAPNG( |
307 | | const PackedPixelFile& ppf, ThreadPool* pool, |
308 | | std::vector<std::vector<uint8_t> >* bitstreams, bool encode_extra_channels, |
309 | | size_t extra_channel_index) const { |
310 | | JxlExtraChannelInfo ec_info{}; |
311 | | if (encode_extra_channels) { |
312 | | if (ppf.extra_channels_info.size() <= extra_channel_index) { |
313 | | return JXL_FAILURE("Invalid index for extra channel"); |
314 | | } |
315 | | ec_info = ppf.extra_channels_info[extra_channel_index].ec_info; |
316 | | } |
317 | | |
318 | | bool has_alpha = !encode_extra_channels && (ppf.info.alpha_bits != 0); |
319 | | bool is_gray = encode_extra_channels || (ppf.info.num_color_channels == 1); |
320 | | size_t color_channels = |
321 | | encode_extra_channels ? 1 : ppf.info.num_color_channels; |
322 | | size_t num_channels = color_channels + (has_alpha ? 1 : 0); |
323 | | |
324 | | bitstreams->emplace_back(); |
325 | | auto bytes = &bitstreams->back(); |
326 | | |
327 | | size_t count = 0; |
328 | | size_t anim_chunks = 0; |
329 | | |
330 | | for (const auto& frame : ppf.frames) { |
331 | | if (!ppf.info.have_animation && count > 0) { |
332 | | // in the case of non-coalesced layers (composite still), write multiple |
333 | | // files |
334 | | bitstreams->emplace_back(); |
335 | | bytes = &bitstreams->back(); |
336 | | } |
337 | | const PackedImage& color = encode_extra_channels |
338 | | ? frame.extra_channels[extra_channel_index] |
339 | | : frame.color; |
340 | | |
341 | | size_t xsize = color.xsize; |
342 | | size_t ysize = color.ysize; |
343 | | size_t num_samples = num_channels * xsize * ysize; |
344 | | |
345 | | uint32_t bits_per_sample = encode_extra_channels ? ec_info.bits_per_sample |
346 | | : ppf.info.bits_per_sample; |
347 | | if (!encode_extra_channels) { |
348 | | JXL_RETURN_IF_ERROR(VerifyPackedImage(color, ppf.info)); |
349 | | } else { |
350 | | JXL_RETURN_IF_ERROR(VerifyFormat(color.format)); |
351 | | JXL_RETURN_IF_ERROR(VerifyBitDepth(color.format.data_type, |
352 | | bits_per_sample, |
353 | | ec_info.exponent_bits_per_sample)); |
354 | | } |
355 | | const JxlPixelFormat format = color.format; |
356 | | const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels()); |
357 | | JXL_RETURN_IF_ERROR(PackedImage::ValidateDataType(format.data_type)); |
358 | | size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type); |
359 | | size_t bytes_per_sample = data_bits_per_sample / 8; |
360 | | size_t out_bytes_per_sample = bytes_per_sample > 1 ? 2 : 1; |
361 | | size_t out_stride = xsize * num_channels * out_bytes_per_sample; |
362 | | size_t out_size = ysize * out_stride; |
363 | | std::vector<uint8_t> out(out_size); |
364 | | |
365 | | if (format.data_type == JXL_TYPE_UINT8) { |
366 | | if (bits_per_sample < 8) { |
367 | | float mul = 255.0 / ((1u << bits_per_sample) - 1); |
368 | | for (size_t i = 0; i < num_samples; ++i) { |
369 | | out[i] = static_cast<uint8_t>(std::lround(in[i] * mul)); |
370 | | } |
371 | | } else { |
372 | | memcpy(out.data(), in, out_size); |
373 | | } |
374 | | } else if (format.data_type == JXL_TYPE_UINT16) { |
375 | | if (bits_per_sample < 16 || format.endianness != JXL_BIG_ENDIAN) { |
376 | | float mul = 65535.0 / ((1u << bits_per_sample) - 1); |
377 | | const uint8_t* p_in = in; |
378 | | uint8_t* p_out = out.data(); |
379 | | for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) { |
380 | | uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE16(p_in) |
381 | | : LoadLE16(p_in)); |
382 | | StoreBE16(static_cast<uint32_t>(std::lround(val * mul)), p_out); |
383 | | } |
384 | | } else { |
385 | | memcpy(out.data(), in, out_size); |
386 | | } |
387 | | } else if (format.data_type == JXL_TYPE_FLOAT) { |
388 | | constexpr float kMul = 65535.0; |
389 | | const uint8_t* p_in = in; |
390 | | uint8_t* p_out = out.data(); |
391 | | for (size_t i = 0; i < num_samples; |
392 | | ++i, p_in += sizeof(float), p_out += 2) { |
393 | | float val = |
394 | | Clamp1(format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat(p_in) |
395 | | : format.endianness == JXL_LITTLE_ENDIAN |
396 | | ? LoadLEFloat(p_in) |
397 | | : *reinterpret_cast<const float*>(p_in), |
398 | | 0.f, 1.f); |
399 | | StoreBE16(static_cast<uint32_t>(std::lround(val * kMul)), p_out); |
400 | | } |
401 | | } |
402 | | png_structp png_ptr; |
403 | | png_infop info_ptr; |
404 | | |
405 | | png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, |
406 | | nullptr); |
407 | | |
408 | | if (!png_ptr) return JXL_FAILURE("Could not init png encoder"); |
409 | | |
410 | | info_ptr = png_create_info_struct(png_ptr); |
411 | | if (!info_ptr) return JXL_FAILURE("Could not init png info struct"); |
412 | | png_set_compression_level(png_ptr, 1); |
413 | | |
414 | | png_set_write_fn(png_ptr, bytes, PngWrite, nullptr); |
415 | | png_set_flush(png_ptr, 0); |
416 | | |
417 | | int width = xsize; |
418 | | int height = ysize; |
419 | | |
420 | | png_byte color_type = (is_gray ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_RGB); |
421 | | if (has_alpha) color_type |= PNG_COLOR_MASK_ALPHA; |
422 | | png_byte bit_depth = out_bytes_per_sample * 8; |
423 | | |
424 | | png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type, |
425 | | PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, |
426 | | PNG_FILTER_TYPE_BASE); |
427 | | if (count == 0 || !ppf.info.have_animation) { |
428 | | if (!encode_extra_channels) { |
429 | | if (!MaybeAddSRGB(ppf.color_encoding, png_ptr, info_ptr)) { |
430 | | if (ppf.primary_color_representation != |
431 | | PackedPixelFile::kIccIsPrimary) { |
432 | | MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr); |
433 | | } |
434 | | if (!ppf.icc.empty()) { |
435 | | png_set_benign_errors(png_ptr, 1); |
436 | | png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(), |
437 | | ppf.icc.size()); |
438 | | } |
439 | | MaybeAddCHRM(ppf.color_encoding, png_ptr, info_ptr); |
440 | | MaybeAddGAMA(ppf.color_encoding, png_ptr, info_ptr); |
441 | | } |
442 | | MaybeAddCLLi(ppf.color_encoding, ppf.info.intensity_target, png_ptr, |
443 | | info_ptr); |
444 | | |
445 | | std::vector<std::string> textstrings; |
446 | | JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(ppf.metadata, &textstrings)); |
447 | | for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) { |
448 | | png_text text; |
449 | | text.key = const_cast<png_charp>(textstrings[kk].c_str()); |
450 | | text.text = const_cast<png_charp>(textstrings[kk + 1].c_str()); |
451 | | text.compression = PNG_TEXT_COMPRESSION_zTXt; |
452 | | png_set_text(png_ptr, info_ptr, &text, 1); |
453 | | } |
454 | | } |
455 | | |
456 | | png_write_info(png_ptr, info_ptr); |
457 | | } else { |
458 | | // fake writing a header, otherwise libpng gets confused |
459 | | size_t pos = bytes->size(); |
460 | | png_write_info(png_ptr, info_ptr); |
461 | | bytes->resize(pos); |
462 | | } |
463 | | |
464 | | if (ppf.info.have_animation) { |
465 | | if (count == 0) { |
466 | | png_byte adata[8]; |
467 | | png_save_uint_32(adata, ppf.frames.size()); |
468 | | png_save_uint_32(adata + 4, ppf.info.animation.num_loops); |
469 | | png_byte actl[5] = "acTL"; |
470 | | png_write_chunk(png_ptr, actl, adata, 8); |
471 | | } |
472 | | png_byte fdata[26]; |
473 | | // TODO(jon): also make this work for the non-coalesced case |
474 | | png_save_uint_32(fdata, anim_chunks++); |
475 | | png_save_uint_32(fdata + 4, width); |
476 | | png_save_uint_32(fdata + 8, height); |
477 | | png_save_uint_32(fdata + 12, 0); |
478 | | png_save_uint_32(fdata + 16, 0); |
479 | | png_save_uint_16(fdata + 20, frame.frame_info.duration * |
480 | | ppf.info.animation.tps_denominator); |
481 | | png_save_uint_16(fdata + 22, ppf.info.animation.tps_numerator); |
482 | | fdata[24] = 1; |
483 | | fdata[25] = 0; |
484 | | png_byte fctl[5] = "fcTL"; |
485 | | png_write_chunk(png_ptr, fctl, fdata, 26); |
486 | | } |
487 | | |
488 | | std::vector<uint8_t*> rows(height); |
489 | | for (int y = 0; y < height; ++y) { |
490 | | rows[y] = out.data() + y * out_stride; |
491 | | } |
492 | | |
493 | | png_write_flush(png_ptr); |
494 | | const size_t pos = bytes->size(); |
495 | | png_write_image(png_ptr, rows.data()); |
496 | | png_write_flush(png_ptr); |
497 | | if (count > 0 && ppf.info.have_animation) { |
498 | | std::vector<uint8_t> fdata(4); |
499 | | png_save_uint_32(fdata.data(), anim_chunks++); |
500 | | size_t p = pos; |
501 | | while (p + 8 < bytes->size()) { |
502 | | size_t len = png_get_uint_32(bytes->data() + p); |
503 | | JXL_ENSURE(bytes->operator[](p + 4) == 'I'); |
504 | | JXL_ENSURE(bytes->operator[](p + 5) == 'D'); |
505 | | JXL_ENSURE(bytes->operator[](p + 6) == 'A'); |
506 | | JXL_ENSURE(bytes->operator[](p + 7) == 'T'); |
507 | | fdata.insert(fdata.end(), bytes->data() + p + 8, |
508 | | bytes->data() + p + 8 + len); |
509 | | p += len + 12; |
510 | | } |
511 | | bytes->resize(pos); |
512 | | |
513 | | png_byte fdat[5] = "fdAT"; |
514 | | png_write_chunk(png_ptr, fdat, fdata.data(), fdata.size()); |
515 | | } |
516 | | |
517 | | count++; |
518 | | if (count == ppf.frames.size() || !ppf.info.have_animation) { |
519 | | png_write_end(png_ptr, nullptr); |
520 | | } |
521 | | |
522 | | png_destroy_write_struct(&png_ptr, &info_ptr); |
523 | | } |
524 | | |
525 | | return true; |
526 | | } |
527 | | |
528 | | } // namespace |
529 | | |
530 | | std::unique_ptr<Encoder> GetAPNGEncoder() { |
531 | | return jxl::make_unique<APNGEncoder>(); |
532 | | } |
533 | | |
534 | | } // namespace extras |
535 | | } // namespace jxl |
536 | | |
537 | | #endif // JPEGXL_ENABLE_APNG |