tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

jpegli.cc (20641B)


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