apng.cc (18932B)
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 if (intensity_target == 10'000) return; 268 269 const uint32_t max_content_light_level = 270 static_cast<uint32_t>(10'000.f * Clamp1(intensity_target, 0.f, 10'000.f)); 271 png_byte chunk_data[8] = {}; 272 png_save_uint_32(chunk_data, max_content_light_level); 273 // Leave MaxFALL set to 0. 274 png_unknown_chunk chunk; 275 memcpy(chunk.name, "cLLi", 5); 276 chunk.data = chunk_data; 277 chunk.size = sizeof chunk_data; 278 chunk.location = PNG_HAVE_IHDR; 279 png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS, 280 reinterpret_cast<const png_byte*>("cLLi"), 1); 281 png_set_unknown_chunks(png_ptr, info_ptr, &chunk, 1); 282 } 283 284 Status APNGEncoder::EncodePackedPixelFileToAPNG( 285 const PackedPixelFile& ppf, ThreadPool* pool, std::vector<uint8_t>* bytes, 286 bool encode_extra_channels, size_t extra_channel_index) const { 287 JxlExtraChannelInfo ec_info{}; 288 if (encode_extra_channels) { 289 if (ppf.extra_channels_info.size() <= extra_channel_index) { 290 return JXL_FAILURE("Invalid index for extra channel"); 291 } 292 ec_info = ppf.extra_channels_info[extra_channel_index].ec_info; 293 } 294 295 bool has_alpha = !encode_extra_channels && (ppf.info.alpha_bits != 0); 296 bool is_gray = encode_extra_channels || (ppf.info.num_color_channels == 1); 297 size_t color_channels = 298 encode_extra_channels ? 1 : ppf.info.num_color_channels; 299 size_t num_channels = color_channels + (has_alpha ? 1 : 0); 300 301 if (!ppf.info.have_animation && ppf.frames.size() != 1) { 302 return JXL_FAILURE("Invalid number of frames"); 303 } 304 305 size_t count = 0; 306 size_t anim_chunks = 0; 307 308 for (const auto& frame : ppf.frames) { 309 const PackedImage& color = encode_extra_channels 310 ? frame.extra_channels[extra_channel_index] 311 : frame.color; 312 313 size_t xsize = color.xsize; 314 size_t ysize = color.ysize; 315 size_t num_samples = num_channels * xsize * ysize; 316 317 uint32_t bits_per_sample = encode_extra_channels ? ec_info.bits_per_sample 318 : ppf.info.bits_per_sample; 319 if (!encode_extra_channels) { 320 JXL_RETURN_IF_ERROR(VerifyPackedImage(color, ppf.info)); 321 } else { 322 JXL_RETURN_IF_ERROR(VerifyFormat(color.format)); 323 JXL_RETURN_IF_ERROR(VerifyBitDepth(color.format.data_type, 324 bits_per_sample, 325 ec_info.exponent_bits_per_sample)); 326 } 327 const JxlPixelFormat format = color.format; 328 const uint8_t* in = reinterpret_cast<const uint8_t*>(color.pixels()); 329 JXL_RETURN_IF_ERROR(PackedImage::ValidateDataType(format.data_type)); 330 size_t data_bits_per_sample = PackedImage::BitsPerChannel(format.data_type); 331 size_t bytes_per_sample = data_bits_per_sample / 8; 332 size_t out_bytes_per_sample = bytes_per_sample > 1 ? 2 : 1; 333 size_t out_stride = xsize * num_channels * out_bytes_per_sample; 334 size_t out_size = ysize * out_stride; 335 std::vector<uint8_t> out(out_size); 336 337 if (format.data_type == JXL_TYPE_UINT8) { 338 if (bits_per_sample < 8) { 339 float mul = 255.0 / ((1u << bits_per_sample) - 1); 340 for (size_t i = 0; i < num_samples; ++i) { 341 out[i] = static_cast<uint8_t>(std::lroundf(in[i] * mul)); 342 } 343 } else { 344 memcpy(out.data(), in, out_size); 345 } 346 } else if (format.data_type == JXL_TYPE_UINT16) { 347 if (bits_per_sample < 16 || format.endianness != JXL_BIG_ENDIAN) { 348 float mul = 65535.0 / ((1u << bits_per_sample) - 1); 349 const uint8_t* p_in = in; 350 uint8_t* p_out = out.data(); 351 for (size_t i = 0; i < num_samples; ++i, p_in += 2, p_out += 2) { 352 uint32_t val = (format.endianness == JXL_BIG_ENDIAN ? LoadBE16(p_in) 353 : LoadLE16(p_in)); 354 StoreBE16(static_cast<uint32_t>(std::lroundf(val * mul)), p_out); 355 } 356 } else { 357 memcpy(out.data(), in, out_size); 358 } 359 } else if (format.data_type == JXL_TYPE_FLOAT) { 360 constexpr float kMul = 65535.0; 361 const uint8_t* p_in = in; 362 uint8_t* p_out = out.data(); 363 for (size_t i = 0; i < num_samples; 364 ++i, p_in += sizeof(float), p_out += 2) { 365 float val = 366 Clamp1(format.endianness == JXL_BIG_ENDIAN ? LoadBEFloat(p_in) 367 : format.endianness == JXL_LITTLE_ENDIAN 368 ? LoadLEFloat(p_in) 369 : *reinterpret_cast<const float*>(p_in), 370 0.f, 1.f); 371 StoreBE16(static_cast<uint32_t>(std::lroundf(val * kMul)), p_out); 372 } 373 } 374 png_structp png_ptr; 375 png_infop info_ptr; 376 377 png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, 378 nullptr); 379 380 if (!png_ptr) return JXL_FAILURE("Could not init png encoder"); 381 382 info_ptr = png_create_info_struct(png_ptr); 383 if (!info_ptr) return JXL_FAILURE("Could not init png info struct"); 384 png_set_compression_level(png_ptr, 1); 385 386 png_set_write_fn(png_ptr, bytes, PngWrite, nullptr); 387 png_set_flush(png_ptr, 0); 388 389 int width = xsize; 390 int height = ysize; 391 392 png_byte color_type = (is_gray ? PNG_COLOR_TYPE_GRAY : PNG_COLOR_TYPE_RGB); 393 if (has_alpha) color_type |= PNG_COLOR_MASK_ALPHA; 394 png_byte bit_depth = out_bytes_per_sample * 8; 395 396 png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type, 397 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, 398 PNG_FILTER_TYPE_BASE); 399 if (count == 0 && !encode_extra_channels) { 400 if (!MaybeAddSRGB(ppf.color_encoding, png_ptr, info_ptr)) { 401 if (ppf.primary_color_representation != 402 PackedPixelFile::kIccIsPrimary) { 403 MaybeAddCICP(ppf.color_encoding, png_ptr, info_ptr); 404 } 405 if (!ppf.icc.empty()) { 406 png_set_benign_errors(png_ptr, 1); 407 png_set_iCCP(png_ptr, info_ptr, "1", 0, ppf.icc.data(), 408 ppf.icc.size()); 409 } 410 MaybeAddCHRM(ppf.color_encoding, png_ptr, info_ptr); 411 MaybeAddGAMA(ppf.color_encoding, png_ptr, info_ptr); 412 } 413 MaybeAddCLLi(ppf.color_encoding, ppf.info.intensity_target, png_ptr, 414 info_ptr); 415 416 std::vector<std::string> textstrings; 417 JXL_RETURN_IF_ERROR(BlobsWriterPNG::Encode(ppf.metadata, &textstrings)); 418 for (size_t kk = 0; kk + 1 < textstrings.size(); kk += 2) { 419 png_text text; 420 text.key = const_cast<png_charp>(textstrings[kk].c_str()); 421 text.text = const_cast<png_charp>(textstrings[kk + 1].c_str()); 422 text.compression = PNG_TEXT_COMPRESSION_zTXt; 423 png_set_text(png_ptr, info_ptr, &text, 1); 424 } 425 426 png_write_info(png_ptr, info_ptr); 427 } else { 428 // fake writing a header, otherwise libpng gets confused 429 size_t pos = bytes->size(); 430 png_write_info(png_ptr, info_ptr); 431 bytes->resize(pos); 432 } 433 434 if (ppf.info.have_animation) { 435 if (count == 0) { 436 png_byte adata[8]; 437 png_save_uint_32(adata, ppf.frames.size()); 438 png_save_uint_32(adata + 4, ppf.info.animation.num_loops); 439 png_byte actl[5] = "acTL"; 440 png_write_chunk(png_ptr, actl, adata, 8); 441 } 442 png_byte fdata[26]; 443 // TODO(jon): also make this work for the non-coalesced case 444 png_save_uint_32(fdata, anim_chunks++); 445 png_save_uint_32(fdata + 4, width); 446 png_save_uint_32(fdata + 8, height); 447 png_save_uint_32(fdata + 12, 0); 448 png_save_uint_32(fdata + 16, 0); 449 png_save_uint_16(fdata + 20, frame.frame_info.duration * 450 ppf.info.animation.tps_denominator); 451 png_save_uint_16(fdata + 22, ppf.info.animation.tps_numerator); 452 fdata[24] = 1; 453 fdata[25] = 0; 454 png_byte fctl[5] = "fcTL"; 455 png_write_chunk(png_ptr, fctl, fdata, 26); 456 } 457 458 std::vector<uint8_t*> rows(height); 459 for (int y = 0; y < height; ++y) { 460 rows[y] = out.data() + y * out_stride; 461 } 462 463 png_write_flush(png_ptr); 464 const size_t pos = bytes->size(); 465 png_write_image(png_ptr, rows.data()); 466 png_write_flush(png_ptr); 467 if (count > 0) { 468 std::vector<uint8_t> fdata(4); 469 png_save_uint_32(fdata.data(), anim_chunks++); 470 size_t p = pos; 471 while (p + 8 < bytes->size()) { 472 size_t len = png_get_uint_32(bytes->data() + p); 473 JXL_ENSURE(bytes->operator[](p + 4) == 'I'); 474 JXL_ENSURE(bytes->operator[](p + 5) == 'D'); 475 JXL_ENSURE(bytes->operator[](p + 6) == 'A'); 476 JXL_ENSURE(bytes->operator[](p + 7) == 'T'); 477 fdata.insert(fdata.end(), bytes->data() + p + 8, 478 bytes->data() + p + 8 + len); 479 p += len + 12; 480 } 481 bytes->resize(pos); 482 483 png_byte fdat[5] = "fdAT"; 484 png_write_chunk(png_ptr, fdat, fdata.data(), fdata.size()); 485 } 486 487 count++; 488 if (count == ppf.frames.size() || !ppf.info.have_animation) { 489 png_write_end(png_ptr, nullptr); 490 } 491 492 png_destroy_write_struct(&png_ptr, &info_ptr); 493 } 494 495 return true; 496 } 497 498 } // namespace 499 #endif 500 501 std::unique_ptr<Encoder> GetAPNGEncoder() { 502 #if JPEGXL_ENABLE_APNG 503 return jxl::make_unique<APNGEncoder>(); 504 #else 505 return nullptr; 506 #endif 507 } 508 509 } // namespace extras 510 } // namespace jxl