Coverage Report

Created: 2024-09-08 07:14

/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