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