libjxl

FORK: libjxl patches used on blog
git clone https://git.neptards.moe/blog/libjxl.git
Log | Files | Refs | Submodules | README | LICENSE

jpegli.cc (19582B)


      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/jpegli.h"
      7 
      8 #include <jxl/cms.h>
      9 #include <jxl/codestream_header.h>
     10 #include <jxl/types.h>
     11 #include <setjmp.h>
     12 #include <stdint.h>
     13 
     14 #include <algorithm>
     15 #include <cmath>
     16 #include <cstddef>
     17 #include <cstdlib>
     18 #include <cstring>
     19 #include <hwy/aligned_allocator.h>
     20 #include <limits>
     21 #include <string>
     22 #include <utility>
     23 #include <vector>
     24 
     25 #include "lib/extras/enc/encode.h"
     26 #include "lib/extras/packed_image.h"
     27 #include "lib/jpegli/common.h"
     28 #include "lib/jpegli/encode.h"
     29 #include "lib/jpegli/types.h"
     30 #include "lib/jxl/base/byte_order.h"
     31 #include "lib/jxl/base/common.h"
     32 #include "lib/jxl/base/data_parallel.h"
     33 #include "lib/jxl/base/status.h"
     34 #include "lib/jxl/color_encoding_internal.h"
     35 #include "lib/jxl/enc_xyb.h"
     36 #include "lib/jxl/image.h"
     37 #include "lib/jxl/simd_util.h"
     38 
     39 namespace jxl {
     40 namespace extras {
     41 
     42 namespace {
     43 
     44 void MyErrorExit(j_common_ptr cinfo) {
     45   jmp_buf* env = static_cast<jmp_buf*>(cinfo->client_data);
     46   (*cinfo->err->output_message)(cinfo);
     47   jpegli_destroy_compress(reinterpret_cast<j_compress_ptr>(cinfo));
     48   longjmp(*env, 1);
     49 }
     50 
     51 Status VerifyInput(const PackedPixelFile& ppf) {
     52   const JxlBasicInfo& info = ppf.info;
     53   JXL_RETURN_IF_ERROR(Encoder::VerifyBasicInfo(info));
     54   if (ppf.frames.size() != 1) {
     55     return JXL_FAILURE("JPEG input must have exactly one frame.");
     56   }
     57   const PackedImage& image = ppf.frames[0].color;
     58   JXL_RETURN_IF_ERROR(Encoder::VerifyImageSize(image, info));
     59   if (image.format.data_type == JXL_TYPE_FLOAT16) {
     60     return JXL_FAILURE("FLOAT16 input is not supported.");
     61   }
     62   JXL_RETURN_IF_ERROR(Encoder::VerifyBitDepth(image.format.data_type,
     63                                               info.bits_per_sample,
     64                                               info.exponent_bits_per_sample));
     65   if ((image.format.data_type == JXL_TYPE_UINT8 && info.bits_per_sample != 8) ||
     66       (image.format.data_type == JXL_TYPE_UINT16 &&
     67        info.bits_per_sample != 16)) {
     68     return JXL_FAILURE("Only full bit depth unsigned types are supported.");
     69   }
     70   return true;
     71 }
     72 
     73 Status GetColorEncoding(const PackedPixelFile& ppf,
     74                         ColorEncoding* color_encoding) {
     75   if (ppf.primary_color_representation == PackedPixelFile::kIccIsPrimary) {
     76     IccBytes icc = ppf.icc;
     77     JXL_RETURN_IF_ERROR(
     78         color_encoding->SetICC(std::move(icc), JxlGetDefaultCms()));
     79   } else {
     80     JXL_RETURN_IF_ERROR(color_encoding->FromExternal(ppf.color_encoding));
     81   }
     82   if (color_encoding->ICC().empty()) {
     83     return JXL_FAILURE("Invalid color encoding.");
     84   }
     85   return true;
     86 }
     87 
     88 bool HasICCProfile(const std::vector<uint8_t>& app_data) {
     89   size_t pos = 0;
     90   while (pos < app_data.size()) {
     91     if (pos + 16 > app_data.size()) return false;
     92     uint8_t marker = app_data[pos + 1];
     93     size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2;
     94     if (marker == 0xe2 && memcmp(&app_data[pos + 4], "ICC_PROFILE", 12) == 0) {
     95       return true;
     96     }
     97     pos += marker_len;
     98   }
     99   return false;
    100 }
    101 
    102 Status WriteAppData(j_compress_ptr cinfo,
    103                     const std::vector<uint8_t>& app_data) {
    104   size_t pos = 0;
    105   while (pos < app_data.size()) {
    106     if (pos + 4 > app_data.size()) {
    107       return JXL_FAILURE("Incomplete APP header.");
    108     }
    109     uint8_t marker = app_data[pos + 1];
    110     size_t marker_len = (app_data[pos + 2] << 8) + app_data[pos + 3] + 2;
    111     if (app_data[pos] != 0xff || marker < 0xe0 || marker > 0xef) {
    112       return JXL_FAILURE("Invalid APP marker %02x %02x", app_data[pos], marker);
    113     }
    114     if (marker_len <= 4) {
    115       return JXL_FAILURE("Invalid APP marker length.");
    116     }
    117     if (pos + marker_len > app_data.size()) {
    118       return JXL_FAILURE("Incomplete APP data");
    119     }
    120     jpegli_write_marker(cinfo, marker, &app_data[pos + 4], marker_len - 4);
    121     pos += marker_len;
    122   }
    123   return true;
    124 }
    125 
    126 constexpr int kICCMarker = 0xe2;
    127 constexpr unsigned char kICCSignature[12] = {
    128     0x49, 0x43, 0x43, 0x5F, 0x50, 0x52, 0x4F, 0x46, 0x49, 0x4C, 0x45, 0x00};
    129 constexpr uint8_t kUnknownTf = 2;
    130 constexpr unsigned char kCICPTagSignature[4] = {0x63, 0x69, 0x63, 0x70};
    131 constexpr size_t kCICPTagSize = 12;
    132 
    133 bool FindCICPTag(const uint8_t* icc_data, size_t len, bool is_first_chunk,
    134                  size_t* cicp_offset, size_t* cicp_length, uint8_t* cicp_tag,
    135                  size_t* cicp_pos) {
    136   if (is_first_chunk) {
    137     // Look up the offset of the CICP tag from the first chunk of ICC data.
    138     if (len < 132) {
    139       return false;
    140     }
    141     uint32_t tag_count = LoadBE32(&icc_data[128]);
    142     if (len < 132 + 12 * tag_count) {
    143       return false;
    144     }
    145     for (uint32_t i = 0; i < tag_count; ++i) {
    146       if (memcmp(&icc_data[132 + 12 * i], kCICPTagSignature, 4) == 0) {
    147         *cicp_offset = LoadBE32(&icc_data[136 + 12 * i]);
    148         *cicp_length = LoadBE32(&icc_data[140 + 12 * i]);
    149       }
    150     }
    151     if (*cicp_length < kCICPTagSize) {
    152       return false;
    153     }
    154   }
    155   if (*cicp_offset < len) {
    156     size_t n_bytes = std::min(len - *cicp_offset, kCICPTagSize - *cicp_pos);
    157     memcpy(&cicp_tag[*cicp_pos], &icc_data[*cicp_offset], n_bytes);
    158     *cicp_pos += n_bytes;
    159     *cicp_offset = 0;
    160   } else {
    161     *cicp_offset -= len;
    162   }
    163   return true;
    164 }
    165 
    166 uint8_t LookupCICPTransferFunctionFromAppData(const uint8_t* app_data,
    167                                               size_t len) {
    168   size_t last_index = 0;
    169   size_t cicp_offset = 0;
    170   size_t cicp_length = 0;
    171   uint8_t cicp_tag[kCICPTagSize] = {};
    172   size_t cicp_pos = 0;
    173   size_t pos = 0;
    174   while (pos < len) {
    175     const uint8_t* marker = &app_data[pos];
    176     if (pos + 4 > len) {
    177       return kUnknownTf;
    178     }
    179     size_t marker_size = (marker[2] << 8) + marker[3] + 2;
    180     if (pos + marker_size > len) {
    181       return kUnknownTf;
    182     }
    183     if (marker_size < 18 || marker[0] != 0xff || marker[1] != kICCMarker ||
    184         memcmp(&marker[4], kICCSignature, 12) != 0) {
    185       pos += marker_size;
    186       continue;
    187     }
    188     uint8_t index = marker[16];
    189     uint8_t total = marker[17];
    190     const uint8_t* payload = marker + 18;
    191     const size_t payload_size = marker_size - 18;
    192     if (index != last_index + 1 || index > total) {
    193       return kUnknownTf;
    194     }
    195     if (!FindCICPTag(payload, payload_size, last_index == 0, &cicp_offset,
    196                      &cicp_length, &cicp_tag[0], &cicp_pos)) {
    197       return kUnknownTf;
    198     }
    199     if (cicp_pos == kCICPTagSize) {
    200       break;
    201     }
    202     ++last_index;
    203   }
    204   if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) {
    205     return cicp_tag[9];
    206   }
    207   return kUnknownTf;
    208 }
    209 
    210 uint8_t LookupCICPTransferFunctionFromICCProfile(const uint8_t* icc_data,
    211                                                  size_t len) {
    212   size_t cicp_offset = 0;
    213   size_t cicp_length = 0;
    214   uint8_t cicp_tag[kCICPTagSize] = {};
    215   size_t cicp_pos = 0;
    216   if (!FindCICPTag(icc_data, len, true, &cicp_offset, &cicp_length,
    217                    &cicp_tag[0], &cicp_pos)) {
    218     return kUnknownTf;
    219   }
    220   if (cicp_pos >= kCICPTagSize && memcmp(cicp_tag, kCICPTagSignature, 4) == 0) {
    221     return cicp_tag[9];
    222   }
    223   return kUnknownTf;
    224 }
    225 
    226 JpegliDataType ConvertDataType(JxlDataType type) {
    227   switch (type) {
    228     case JXL_TYPE_UINT8:
    229       return JPEGLI_TYPE_UINT8;
    230     case JXL_TYPE_UINT16:
    231       return JPEGLI_TYPE_UINT16;
    232     case JXL_TYPE_FLOAT:
    233       return JPEGLI_TYPE_FLOAT;
    234     default:
    235       return JPEGLI_TYPE_UINT8;
    236   }
    237 }
    238 
    239 JpegliEndianness ConvertEndianness(JxlEndianness endianness) {
    240   switch (endianness) {
    241     case JXL_NATIVE_ENDIAN:
    242       return JPEGLI_NATIVE_ENDIAN;
    243     case JXL_LITTLE_ENDIAN:
    244       return JPEGLI_LITTLE_ENDIAN;
    245     case JXL_BIG_ENDIAN:
    246       return JPEGLI_BIG_ENDIAN;
    247     default:
    248       return JPEGLI_NATIVE_ENDIAN;
    249   }
    250 }
    251 
    252 void ToFloatRow(const uint8_t* row_in, JxlPixelFormat format, size_t len,
    253                 float* row_out) {
    254   bool is_little_endian =
    255       (format.endianness == JXL_LITTLE_ENDIAN ||
    256        (format.endianness == JXL_NATIVE_ENDIAN && IsLittleEndian()));
    257   static constexpr double kMul8 = 1.0 / 255.0;
    258   static constexpr double kMul16 = 1.0 / 65535.0;
    259   if (format.data_type == JXL_TYPE_UINT8) {
    260     for (size_t x = 0; x < len; ++x) {
    261       row_out[x] = row_in[x] * kMul8;
    262     }
    263   } else if (format.data_type == JXL_TYPE_UINT16 && is_little_endian) {
    264     for (size_t x = 0; x < len; ++x) {
    265       row_out[x] = LoadLE16(&row_in[2 * x]) * kMul16;
    266     }
    267   } else if (format.data_type == JXL_TYPE_UINT16 && !is_little_endian) {
    268     for (size_t x = 0; x < len; ++x) {
    269       row_out[x] = LoadBE16(&row_in[2 * x]) * kMul16;
    270     }
    271   } else if (format.data_type == JXL_TYPE_FLOAT && is_little_endian) {
    272     for (size_t x = 0; x < len; ++x) {
    273       row_out[x] = LoadLEFloat(&row_in[4 * x]);
    274     }
    275   } else if (format.data_type == JXL_TYPE_FLOAT && !is_little_endian) {
    276     for (size_t x = 0; x < len; ++x) {
    277       row_out[x] = LoadBEFloat(&row_in[4 * x]);
    278     }
    279   }
    280 }
    281 
    282 Status EncodeJpegToTargetSize(const PackedPixelFile& ppf,
    283                               const JpegSettings& jpeg_settings,
    284                               size_t target_size, ThreadPool* pool,
    285                               std::vector<uint8_t>* output) {
    286   output->clear();
    287   size_t best_error = std::numeric_limits<size_t>::max();
    288   float distance0 = -1.0f;
    289   float distance1 = -1.0f;
    290   float distance = 1.0f;
    291   for (int step = 0; step < 15; ++step) {
    292     JpegSettings settings = jpeg_settings;
    293     settings.libjpeg_quality = 0;
    294     settings.distance = distance;
    295     settings.target_size = 0;
    296     std::vector<uint8_t> compressed;
    297     JXL_RETURN_IF_ERROR(EncodeJpeg(ppf, settings, pool, &compressed));
    298     size_t size = compressed.size();
    299     // prefer being under the target size to being over it
    300     size_t error = size < target_size
    301                        ? target_size - size
    302                        : static_cast<size_t>(1.2f * (size - target_size));
    303     if (error < best_error) {
    304       best_error = error;
    305       std::swap(*output, compressed);
    306     }
    307     float rel_error = size * 1.0f / target_size;
    308     if (std::abs(rel_error - 1.0f) < 0.002f) {
    309       break;
    310     }
    311     if (size < target_size) {
    312       distance1 = distance;
    313     } else {
    314       distance0 = distance;
    315     }
    316     if (distance1 == -1) {
    317       distance *= std::pow(rel_error, 1.5) * 1.05;
    318     } else if (distance0 == -1) {
    319       distance *= std::pow(rel_error, 1.5) * 0.95;
    320     } else {
    321       distance = 0.5 * (distance0 + distance1);
    322     }
    323   }
    324   return true;
    325 }
    326 
    327 }  // namespace
    328 
    329 Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings,
    330                   ThreadPool* pool, std::vector<uint8_t>* compressed) {
    331   if (jpeg_settings.libjpeg_quality > 0) {
    332     auto encoder = Encoder::FromExtension(".jpg");
    333     encoder->SetOption("q", std::to_string(jpeg_settings.libjpeg_quality));
    334     if (!jpeg_settings.libjpeg_chroma_subsampling.empty()) {
    335       encoder->SetOption("chroma_subsampling",
    336                          jpeg_settings.libjpeg_chroma_subsampling);
    337     }
    338     EncodedImage encoded;
    339     JXL_RETURN_IF_ERROR(encoder->Encode(ppf, &encoded, pool));
    340     size_t target_size = encoded.bitstreams[0].size();
    341     return EncodeJpegToTargetSize(ppf, jpeg_settings, target_size, pool,
    342                                   compressed);
    343   }
    344   if (jpeg_settings.target_size > 0) {
    345     return EncodeJpegToTargetSize(ppf, jpeg_settings, jpeg_settings.target_size,
    346                                   pool, compressed);
    347   }
    348   JXL_RETURN_IF_ERROR(VerifyInput(ppf));
    349 
    350   ColorEncoding color_encoding;
    351   JXL_RETURN_IF_ERROR(GetColorEncoding(ppf, &color_encoding));
    352 
    353   ColorSpaceTransform c_transform(*JxlGetDefaultCms());
    354   ColorEncoding xyb_encoding;
    355   if (jpeg_settings.xyb) {
    356     if (ppf.info.num_color_channels != 3) {
    357       return JXL_FAILURE("Only RGB input is supported in XYB mode.");
    358     }
    359     if (HasICCProfile(jpeg_settings.app_data)) {
    360       return JXL_FAILURE("APP data ICC profile is not supported in XYB mode.");
    361     }
    362     const ColorEncoding& c_desired = ColorEncoding::LinearSRGB(false);
    363     JXL_RETURN_IF_ERROR(
    364         c_transform.Init(color_encoding, c_desired, 255.0f, ppf.info.xsize, 1));
    365     xyb_encoding.SetColorSpace(jxl::ColorSpace::kXYB);
    366     xyb_encoding.SetRenderingIntent(jxl::RenderingIntent::kPerceptual);
    367     JXL_RETURN_IF_ERROR(xyb_encoding.CreateICC());
    368   }
    369   const ColorEncoding& output_encoding =
    370       jpeg_settings.xyb ? xyb_encoding : color_encoding;
    371 
    372   // We need to declare all the non-trivial destructor local variables
    373   // before the call to setjmp().
    374   std::vector<uint8_t> pixels;
    375   unsigned char* output_buffer = nullptr;
    376   unsigned long output_size = 0;
    377   std::vector<uint8_t> row_bytes;
    378   size_t rowlen = RoundUpTo(ppf.info.xsize, MaxVectorSize());
    379   hwy::AlignedFreeUniquePtr<float[]> xyb_tmp =
    380       hwy::AllocateAligned<float>(6 * rowlen);
    381   hwy::AlignedFreeUniquePtr<float[]> premul_absorb =
    382       hwy::AllocateAligned<float>(MaxVectorSize() * 12);
    383   ComputePremulAbsorb(255.0f, premul_absorb.get());
    384 
    385   jpeg_compress_struct cinfo;
    386   const auto try_catch_block = [&]() -> bool {
    387     jpeg_error_mgr jerr;
    388     jmp_buf env;
    389     cinfo.err = jpegli_std_error(&jerr);
    390     jerr.error_exit = &MyErrorExit;
    391     if (setjmp(env)) {
    392       return false;
    393     }
    394     cinfo.client_data = static_cast<void*>(&env);
    395     jpegli_create_compress(&cinfo);
    396     jpegli_mem_dest(&cinfo, &output_buffer, &output_size);
    397     const JxlBasicInfo& info = ppf.info;
    398     cinfo.image_width = info.xsize;
    399     cinfo.image_height = info.ysize;
    400     cinfo.input_components = info.num_color_channels;
    401     cinfo.in_color_space =
    402         cinfo.input_components == 1 ? JCS_GRAYSCALE : JCS_RGB;
    403     if (jpeg_settings.xyb) {
    404       jpegli_set_xyb_mode(&cinfo);
    405     } else if (jpeg_settings.use_std_quant_tables) {
    406       jpegli_use_standard_quant_tables(&cinfo);
    407     }
    408     uint8_t cicp_tf = kUnknownTf;
    409     if (!jpeg_settings.app_data.empty()) {
    410       cicp_tf = LookupCICPTransferFunctionFromAppData(
    411           jpeg_settings.app_data.data(), jpeg_settings.app_data.size());
    412     } else if (!output_encoding.IsSRGB()) {
    413       cicp_tf = LookupCICPTransferFunctionFromICCProfile(
    414           output_encoding.ICC().data(), output_encoding.ICC().size());
    415     }
    416     jpegli_set_cicp_transfer_function(&cinfo, cicp_tf);
    417     jpegli_set_defaults(&cinfo);
    418     if (!jpeg_settings.chroma_subsampling.empty()) {
    419       if (jpeg_settings.chroma_subsampling == "444") {
    420         cinfo.comp_info[0].h_samp_factor = 1;
    421         cinfo.comp_info[0].v_samp_factor = 1;
    422       } else if (jpeg_settings.chroma_subsampling == "440") {
    423         cinfo.comp_info[0].h_samp_factor = 1;
    424         cinfo.comp_info[0].v_samp_factor = 2;
    425       } else if (jpeg_settings.chroma_subsampling == "422") {
    426         cinfo.comp_info[0].h_samp_factor = 2;
    427         cinfo.comp_info[0].v_samp_factor = 1;
    428       } else if (jpeg_settings.chroma_subsampling == "420") {
    429         cinfo.comp_info[0].h_samp_factor = 2;
    430         cinfo.comp_info[0].v_samp_factor = 2;
    431       } else {
    432         return false;
    433       }
    434       for (int i = 1; i < cinfo.num_components; ++i) {
    435         cinfo.comp_info[i].h_samp_factor = 1;
    436         cinfo.comp_info[i].v_samp_factor = 1;
    437       }
    438     }
    439     jpegli_enable_adaptive_quantization(
    440         &cinfo, TO_JXL_BOOL(jpeg_settings.use_adaptive_quantization));
    441     if (jpeg_settings.psnr_target > 0.0) {
    442       jpegli_set_psnr(&cinfo, jpeg_settings.psnr_target,
    443                       jpeg_settings.search_tolerance,
    444                       jpeg_settings.min_distance, jpeg_settings.max_distance);
    445     } else if (jpeg_settings.quality > 0.0) {
    446       float distance = jpegli_quality_to_distance(jpeg_settings.quality);
    447       jpegli_set_distance(&cinfo, distance, TRUE);
    448     } else {
    449       jpegli_set_distance(&cinfo, jpeg_settings.distance, TRUE);
    450     }
    451     jpegli_set_progressive_level(&cinfo, jpeg_settings.progressive_level);
    452     cinfo.optimize_coding = TO_JXL_BOOL(jpeg_settings.optimize_coding);
    453     if (!jpeg_settings.app_data.empty()) {
    454       // Make sure jpegli_start_compress() does not write any APP markers.
    455       cinfo.write_JFIF_header = JXL_FALSE;
    456       cinfo.write_Adobe_marker = JXL_FALSE;
    457     }
    458     const PackedImage& image = ppf.frames[0].color;
    459     if (jpeg_settings.xyb) {
    460       jpegli_set_input_format(&cinfo, JPEGLI_TYPE_FLOAT, JPEGLI_NATIVE_ENDIAN);
    461     } else {
    462       jpegli_set_input_format(&cinfo, ConvertDataType(image.format.data_type),
    463                               ConvertEndianness(image.format.endianness));
    464     }
    465     jpegli_start_compress(&cinfo, TRUE);
    466     if (!jpeg_settings.app_data.empty()) {
    467       JXL_RETURN_IF_ERROR(WriteAppData(&cinfo, jpeg_settings.app_data));
    468     }
    469     if ((jpeg_settings.app_data.empty() && !output_encoding.IsSRGB()) ||
    470         jpeg_settings.xyb) {
    471       jpegli_write_icc_profile(&cinfo, output_encoding.ICC().data(),
    472                                output_encoding.ICC().size());
    473     }
    474     const uint8_t* pixels = reinterpret_cast<const uint8_t*>(image.pixels());
    475     if (jpeg_settings.xyb) {
    476       float* src_buf = c_transform.BufSrc(0);
    477       float* dst_buf = c_transform.BufDst(0);
    478       for (size_t y = 0; y < image.ysize; ++y) {
    479         // convert to float
    480         ToFloatRow(&pixels[y * image.stride], image.format, 3 * image.xsize,
    481                    src_buf);
    482         // convert to linear srgb
    483         if (!c_transform.Run(0, src_buf, dst_buf, image.xsize)) {
    484           return false;
    485         }
    486         // deinterleave channels
    487         float* row0 = &xyb_tmp[0];
    488         float* row1 = &xyb_tmp[rowlen];
    489         float* row2 = &xyb_tmp[2 * rowlen];
    490         for (size_t x = 0; x < image.xsize; ++x) {
    491           row0[x] = dst_buf[3 * x + 0];
    492           row1[x] = dst_buf[3 * x + 1];
    493           row2[x] = dst_buf[3 * x + 2];
    494         }
    495         // convert to xyb
    496         LinearRGBRowToXYB(row0, row1, row2, premul_absorb.get(), image.xsize);
    497         // scale xyb
    498         ScaleXYBRow(row0, row1, row2, image.xsize);
    499         // interleave channels
    500         float* row_out = &xyb_tmp[3 * rowlen];
    501         for (size_t x = 0; x < image.xsize; ++x) {
    502           row_out[3 * x + 0] = row0[x];
    503           row_out[3 * x + 1] = row1[x];
    504           row_out[3 * x + 2] = row2[x];
    505         }
    506         // feed to jpegli as native endian floats
    507         JSAMPROW row[] = {reinterpret_cast<uint8_t*>(row_out)};
    508         jpegli_write_scanlines(&cinfo, row, 1);
    509       }
    510     } else {
    511       row_bytes.resize(image.stride);
    512       if (cinfo.num_components == static_cast<int>(image.format.num_channels)) {
    513         for (size_t y = 0; y < info.ysize; ++y) {
    514           memcpy(row_bytes.data(), pixels + y * image.stride, image.stride);
    515           JSAMPROW row[] = {row_bytes.data()};
    516           jpegli_write_scanlines(&cinfo, row, 1);
    517         }
    518       } else {
    519         for (size_t y = 0; y < info.ysize; ++y) {
    520           int bytes_per_channel =
    521               PackedImage::BitsPerChannel(image.format.data_type) / 8;
    522           int bytes_per_pixel = cinfo.num_components * bytes_per_channel;
    523           for (size_t x = 0; x < info.xsize; ++x) {
    524             memcpy(&row_bytes[x * bytes_per_pixel],
    525                    &pixels[y * image.stride + x * image.pixel_stride()],
    526                    bytes_per_pixel);
    527           }
    528           JSAMPROW row[] = {row_bytes.data()};
    529           jpegli_write_scanlines(&cinfo, row, 1);
    530         }
    531       }
    532     }
    533     jpegli_finish_compress(&cinfo);
    534     compressed->resize(output_size);
    535     std::copy_n(output_buffer, output_size, compressed->data());
    536     return true;
    537   };
    538   bool success = try_catch_block();
    539   jpegli_destroy_compress(&cinfo);
    540   if (output_buffer) free(output_buffer);
    541   return success;
    542 }
    543 
    544 }  // namespace extras
    545 }  // namespace jxl