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