tor-browser

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

jxl_cms.cc (47929B)


      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 <jxl/cms.h>
      7 
      8 #ifndef JPEGXL_ENABLE_SKCMS
      9 #define JPEGXL_ENABLE_SKCMS 0
     10 #endif
     11 
     12 #include <jxl/cms_interface.h>
     13 
     14 #include <algorithm>
     15 #include <array>
     16 #include <cmath>
     17 #include <cstddef>
     18 #include <cstdint>
     19 #include <cstring>
     20 #include <memory>
     21 
     22 #undef HWY_TARGET_INCLUDE
     23 #define HWY_TARGET_INCLUDE "lib/jxl/cms/jxl_cms.cc"
     24 #include <hwy/foreach_target.h>
     25 #include <hwy/highway.h>
     26 
     27 #include "lib/jxl/base/compiler_specific.h"
     28 #include "lib/jxl/base/matrix_ops.h"
     29 #include "lib/jxl/base/printf_macros.h"
     30 #include "lib/jxl/base/status.h"
     31 #include "lib/jxl/cms/jxl_cms_internal.h"
     32 #include "lib/jxl/cms/transfer_functions-inl.h"
     33 #include "lib/jxl/color_encoding_internal.h"
     34 #if JPEGXL_ENABLE_SKCMS
     35 #include "skcms.h"
     36 #else  // JPEGXL_ENABLE_SKCMS
     37 #include "lcms2.h"
     38 #include "lcms2_plugin.h"
     39 #include "lib/jxl/base/span.h"
     40 #endif  // JPEGXL_ENABLE_SKCMS
     41 
     42 #define JXL_CMS_VERBOSE 0
     43 
     44 // Define these only once. We can't use HWY_ONCE here because it is defined as
     45 // 1 only on the last pass.
     46 #ifndef LIB_JXL_JXL_CMS_CC
     47 #define LIB_JXL_JXL_CMS_CC
     48 
     49 namespace jxl {
     50 namespace {
     51 
     52 using ::jxl::cms::ColorEncoding;
     53 
     54 struct JxlCms {
     55 #if JPEGXL_ENABLE_SKCMS
     56  IccBytes icc_src, icc_dst;
     57  skcms_ICCProfile profile_src, profile_dst;
     58 #else
     59  void* lcms_transform;
     60 #endif
     61 
     62  // These fields are used when the HLG OOTF or inverse OOTF must be applied.
     63  bool apply_hlg_ootf;
     64  size_t hlg_ootf_num_channels;
     65  // Y component of the primaries.
     66  std::array<float, 3> hlg_ootf_luminances;
     67 
     68  size_t channels_src;
     69  size_t channels_dst;
     70 
     71  std::vector<float> src_storage;
     72  std::vector<float*> buf_src;
     73  std::vector<float> dst_storage;
     74  std::vector<float*> buf_dst;
     75 
     76  float intensity_target;
     77  bool skip_lcms = false;
     78  ExtraTF preprocess = ExtraTF::kNone;
     79  ExtraTF postprocess = ExtraTF::kNone;
     80 };
     81 
     82 Status ApplyHlgOotf(JxlCms* t, float* JXL_RESTRICT buf, size_t xsize,
     83                    bool forward);
     84 }  // namespace
     85 }  // namespace jxl
     86 
     87 #endif  // LIB_JXL_JXL_CMS_CC
     88 
     89 HWY_BEFORE_NAMESPACE();
     90 namespace jxl {
     91 namespace HWY_NAMESPACE {
     92 
     93 #if JXL_CMS_VERBOSE >= 2
     94 const size_t kX = 0;  // pixel index, multiplied by 3 for RGB
     95 #endif
     96 
     97 // xform_src = UndoGammaCompression(buf_src).
     98 Status BeforeTransform(JxlCms* t, const float* buf_src, float* xform_src,
     99                       size_t buf_size) {
    100  switch (t->preprocess) {
    101    case ExtraTF::kNone:
    102      JXL_ENSURE(false);  // unreachable
    103      break;
    104 
    105    case ExtraTF::kPQ: {
    106      HWY_FULL(float) df;
    107      TF_PQ tf_pq(t->intensity_target);
    108      for (size_t i = 0; i < buf_size; i += Lanes(df)) {
    109        const auto val = Load(df, buf_src + i);
    110        const auto result = tf_pq.DisplayFromEncoded(df, val);
    111        Store(result, df, xform_src + i);
    112      }
    113 #if JXL_CMS_VERBOSE >= 2
    114      printf("pre in %.4f %.4f %.4f undoPQ %.4f %.4f %.4f\n", buf_src[3 * kX],
    115             buf_src[3 * kX + 1], buf_src[3 * kX + 2], xform_src[3 * kX],
    116             xform_src[3 * kX + 1], xform_src[3 * kX + 2]);
    117 #endif
    118      break;
    119    }
    120 
    121    case ExtraTF::kHLG:
    122      for (size_t i = 0; i < buf_size; ++i) {
    123        xform_src[i] = static_cast<float>(
    124            TF_HLG_Base::DisplayFromEncoded(static_cast<double>(buf_src[i])));
    125      }
    126      if (t->apply_hlg_ootf) {
    127        JXL_RETURN_IF_ERROR(
    128            ApplyHlgOotf(t, xform_src, buf_size, /*forward=*/true));
    129      }
    130 #if JXL_CMS_VERBOSE >= 2
    131      printf("pre in %.4f %.4f %.4f undoHLG %.4f %.4f %.4f\n", buf_src[3 * kX],
    132             buf_src[3 * kX + 1], buf_src[3 * kX + 2], xform_src[3 * kX],
    133             xform_src[3 * kX + 1], xform_src[3 * kX + 2]);
    134 #endif
    135      break;
    136 
    137    case ExtraTF::kSRGB:
    138      HWY_FULL(float) df;
    139      for (size_t i = 0; i < buf_size; i += Lanes(df)) {
    140        const auto val = Load(df, buf_src + i);
    141        const auto result = TF_SRGB().DisplayFromEncoded(val);
    142        Store(result, df, xform_src + i);
    143      }
    144 #if JXL_CMS_VERBOSE >= 2
    145      printf("pre in %.4f %.4f %.4f undoSRGB %.4f %.4f %.4f\n", buf_src[3 * kX],
    146             buf_src[3 * kX + 1], buf_src[3 * kX + 2], xform_src[3 * kX],
    147             xform_src[3 * kX + 1], xform_src[3 * kX + 2]);
    148 #endif
    149      break;
    150  }
    151  return true;
    152 }
    153 
    154 // Applies gamma compression in-place.
    155 Status AfterTransform(JxlCms* t, float* JXL_RESTRICT buf_dst, size_t buf_size) {
    156  switch (t->postprocess) {
    157    case ExtraTF::kNone:
    158      JXL_DEBUG_ABORT("Unreachable");
    159      break;
    160    case ExtraTF::kPQ: {
    161      HWY_FULL(float) df;
    162      TF_PQ tf_pq(t->intensity_target);
    163      for (size_t i = 0; i < buf_size; i += Lanes(df)) {
    164        const auto val = Load(df, buf_dst + i);
    165        const auto result = tf_pq.EncodedFromDisplay(df, val);
    166        Store(result, df, buf_dst + i);
    167      }
    168 #if JXL_CMS_VERBOSE >= 2
    169      printf("after PQ enc %.4f %.4f %.4f\n", buf_dst[3 * kX],
    170             buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]);
    171 #endif
    172      break;
    173    }
    174    case ExtraTF::kHLG:
    175      if (t->apply_hlg_ootf) {
    176        JXL_RETURN_IF_ERROR(
    177            ApplyHlgOotf(t, buf_dst, buf_size, /*forward=*/false));
    178      }
    179      for (size_t i = 0; i < buf_size; ++i) {
    180        buf_dst[i] = static_cast<float>(
    181            TF_HLG_Base::EncodedFromDisplay(static_cast<double>(buf_dst[i])));
    182      }
    183 #if JXL_CMS_VERBOSE >= 2
    184      printf("after HLG enc %.4f %.4f %.4f\n", buf_dst[3 * kX],
    185             buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]);
    186 #endif
    187      break;
    188    case ExtraTF::kSRGB:
    189      HWY_FULL(float) df;
    190      for (size_t i = 0; i < buf_size; i += Lanes(df)) {
    191        const auto val = Load(df, buf_dst + i);
    192        const auto result = TF_SRGB().EncodedFromDisplay(df, val);
    193        Store(result, df, buf_dst + i);
    194      }
    195 #if JXL_CMS_VERBOSE >= 2
    196      printf("after SRGB enc %.4f %.4f %.4f\n", buf_dst[3 * kX],
    197             buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]);
    198 #endif
    199      break;
    200  }
    201  return true;
    202 }
    203 
    204 Status DoColorSpaceTransform(void* cms_data, const size_t thread,
    205                             const float* buf_src, float* buf_dst,
    206                             size_t xsize) {
    207  // No lock needed.
    208  JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
    209 
    210  const float* xform_src = buf_src;  // Read-only.
    211  if (t->preprocess != ExtraTF::kNone) {
    212    float* mutable_xform_src = t->buf_src[thread];  // Writable buffer.
    213    JXL_RETURN_IF_ERROR(BeforeTransform(t, buf_src, mutable_xform_src,
    214                                        xsize * t->channels_src));
    215    xform_src = mutable_xform_src;
    216  }
    217 
    218 #if JPEGXL_ENABLE_SKCMS
    219  if (t->channels_src == 1 && !t->skip_lcms) {
    220    // Expand from 1 to 3 channels, starting from the end in case
    221    // xform_src == t->buf_src[thread].
    222    float* mutable_xform_src = t->buf_src[thread];
    223    for (size_t i = 0; i < xsize; ++i) {
    224      const size_t x = xsize - i - 1;
    225      mutable_xform_src[x * 3] = mutable_xform_src[x * 3 + 1] =
    226          mutable_xform_src[x * 3 + 2] = xform_src[x];
    227    }
    228    xform_src = mutable_xform_src;
    229  }
    230 #else
    231  if (t->channels_src == 4 && !t->skip_lcms) {
    232    // LCMS does CMYK in a weird way: 0 = white, 100 = max ink
    233    float* mutable_xform_src = t->buf_src[thread];
    234    for (size_t x = 0; x < xsize * 4; ++x) {
    235      mutable_xform_src[x] = 100.f - 100.f * mutable_xform_src[x];
    236    }
    237    xform_src = mutable_xform_src;
    238  }
    239 #endif
    240 
    241 #if JXL_CMS_VERBOSE >= 2
    242  // Save inputs for printing before in-place transforms overwrite them.
    243  const float in0 = xform_src[3 * kX + 0];
    244  const float in1 = xform_src[3 * kX + 1];
    245  const float in2 = xform_src[3 * kX + 2];
    246 #endif
    247 
    248  if (t->skip_lcms) {
    249    if (buf_dst != xform_src) {
    250      memcpy(buf_dst, xform_src, xsize * t->channels_src * sizeof(*buf_dst));
    251    }  // else: in-place, no need to copy
    252  } else {
    253 #if JPEGXL_ENABLE_SKCMS
    254    JXL_ENSURE(
    255        skcms_Transform(xform_src,
    256                        (t->channels_src == 4 ? skcms_PixelFormat_RGBA_ffff
    257                                              : skcms_PixelFormat_RGB_fff),
    258                        skcms_AlphaFormat_Opaque, &t->profile_src, buf_dst,
    259                        skcms_PixelFormat_RGB_fff, skcms_AlphaFormat_Opaque,
    260                        &t->profile_dst, xsize));
    261 #else   // JPEGXL_ENABLE_SKCMS
    262    cmsDoTransform(t->lcms_transform, xform_src, buf_dst,
    263                   static_cast<cmsUInt32Number>(xsize));
    264 #endif  // JPEGXL_ENABLE_SKCMS
    265  }
    266 #if JXL_CMS_VERBOSE >= 2
    267  printf("xform skip%d: %.4f %.4f %.4f (%p) -> (%p) %.4f %.4f %.4f\n",
    268         t->skip_lcms, in0, in1, in2, xform_src, buf_dst, buf_dst[3 * kX],
    269         buf_dst[3 * kX + 1], buf_dst[3 * kX + 2]);
    270 #endif
    271 
    272 #if JPEGXL_ENABLE_SKCMS
    273  if (t->channels_dst == 1 && !t->skip_lcms) {
    274    // Contract back from 3 to 1 channel, this time forward.
    275    float* grayscale_buf_dst = t->buf_dst[thread];
    276    for (size_t x = 0; x < xsize; ++x) {
    277      grayscale_buf_dst[x] = buf_dst[x * 3];
    278    }
    279    buf_dst = grayscale_buf_dst;
    280  }
    281 #endif
    282 
    283  if (t->postprocess != ExtraTF::kNone) {
    284    JXL_RETURN_IF_ERROR(AfterTransform(t, buf_dst, xsize * t->channels_dst));
    285  }
    286  return true;
    287 }
    288 
    289 // NOLINTNEXTLINE(google-readability-namespace-comments)
    290 }  // namespace HWY_NAMESPACE
    291 }  // namespace jxl
    292 HWY_AFTER_NAMESPACE();
    293 
    294 #if HWY_ONCE
    295 namespace jxl {
    296 namespace {
    297 
    298 HWY_EXPORT(DoColorSpaceTransform);
    299 int DoColorSpaceTransform(void* t, size_t thread, const float* buf_src,
    300                          float* buf_dst, size_t xsize) {
    301  return HWY_DYNAMIC_DISPATCH(DoColorSpaceTransform)(t, thread, buf_src,
    302                                                     buf_dst, xsize);
    303 }
    304 
    305 // Define to 1 on OS X as a workaround for older LCMS lacking MD5.
    306 #define JXL_CMS_OLD_VERSION 0
    307 
    308 #if JPEGXL_ENABLE_SKCMS
    309 
    310 JXL_MUST_USE_RESULT CIExy CIExyFromXYZ(const Color& XYZ) {
    311  const float factor = 1.f / (XYZ[0] + XYZ[1] + XYZ[2]);
    312  CIExy xy;
    313  xy.x = XYZ[0] * factor;
    314  xy.y = XYZ[1] * factor;
    315  return xy;
    316 }
    317 
    318 #else  // JPEGXL_ENABLE_SKCMS
    319 // (LCMS interface requires xyY but we omit the Y for white points/primaries.)
    320 
    321 JXL_MUST_USE_RESULT CIExy CIExyFromxyY(const cmsCIExyY& xyY) {
    322  CIExy xy;
    323  xy.x = xyY.x;
    324  xy.y = xyY.y;
    325  return xy;
    326 }
    327 
    328 JXL_MUST_USE_RESULT CIExy CIExyFromXYZ(const cmsCIEXYZ& XYZ) {
    329  cmsCIExyY xyY;
    330  cmsXYZ2xyY(/*Dest=*/&xyY, /*Source=*/&XYZ);
    331  return CIExyFromxyY(xyY);
    332 }
    333 
    334 JXL_MUST_USE_RESULT cmsCIEXYZ D50_XYZ() {
    335  // Quantized D50 as stored in ICC profiles.
    336  return {0.96420288, 1.0, 0.82490540};
    337 }
    338 
    339 // RAII
    340 
    341 struct ProfileDeleter {
    342  void operator()(void* p) { cmsCloseProfile(p); }
    343 };
    344 using Profile = std::unique_ptr<void, ProfileDeleter>;
    345 
    346 struct TransformDeleter {
    347  void operator()(void* p) { cmsDeleteTransform(p); }
    348 };
    349 using Transform = std::unique_ptr<void, TransformDeleter>;
    350 
    351 struct CurveDeleter {
    352  void operator()(cmsToneCurve* p) { cmsFreeToneCurve(p); }
    353 };
    354 using Curve = std::unique_ptr<cmsToneCurve, CurveDeleter>;
    355 
    356 Status CreateProfileXYZ(const cmsContext context,
    357                        Profile* JXL_RESTRICT profile) {
    358  profile->reset(cmsCreateXYZProfileTHR(context));
    359  if (profile->get() == nullptr) return JXL_FAILURE("Failed to create XYZ");
    360  return true;
    361 }
    362 
    363 #endif  // !JPEGXL_ENABLE_SKCMS
    364 
    365 #if JPEGXL_ENABLE_SKCMS
    366 // IMPORTANT: icc must outlive profile.
    367 Status DecodeProfile(const uint8_t* icc, size_t size,
    368                     skcms_ICCProfile* const profile) {
    369  if (!skcms_Parse(icc, size, profile)) {
    370    return JXL_FAILURE("Failed to parse ICC profile with %" PRIuS " bytes",
    371                       size);
    372  }
    373  return true;
    374 }
    375 #else   // JPEGXL_ENABLE_SKCMS
    376 Status DecodeProfile(const cmsContext context, Span<const uint8_t> icc,
    377                     Profile* profile) {
    378  profile->reset(cmsOpenProfileFromMemTHR(context, icc.data(), icc.size()));
    379  if (profile->get() == nullptr) {
    380    return JXL_FAILURE("Failed to decode profile");
    381  }
    382 
    383  // WARNING: due to the LCMS MD5 issue mentioned above, many existing
    384  // profiles have incorrect MD5, so do not even bother checking them nor
    385  // generating warning clutter.
    386 
    387  return true;
    388 }
    389 #endif  // JPEGXL_ENABLE_SKCMS
    390 
    391 #if JPEGXL_ENABLE_SKCMS
    392 
    393 ColorSpace ColorSpaceFromProfile(const skcms_ICCProfile& profile) {
    394  switch (profile.data_color_space) {
    395    case skcms_Signature_RGB:
    396    case skcms_Signature_CMYK:
    397      // spec says CMYK is encoded as RGB (the kBlack extra channel signals that
    398      // it is actually CMYK)
    399      return ColorSpace::kRGB;
    400    case skcms_Signature_Gray:
    401      return ColorSpace::kGray;
    402    default:
    403      return ColorSpace::kUnknown;
    404  }
    405 }
    406 
    407 // vector_out := matmul(matrix, vector_in)
    408 void MatrixProduct(const skcms_Matrix3x3& matrix, const Color& vector_in,
    409                   Color& vector_out) {
    410  for (int i = 0; i < 3; ++i) {
    411    vector_out[i] = 0;
    412    for (int j = 0; j < 3; ++j) {
    413      vector_out[i] += matrix.vals[i][j] * vector_in[j];
    414    }
    415  }
    416 }
    417 
    418 // Returns white point that was specified when creating the profile.
    419 JXL_MUST_USE_RESULT Status UnadaptedWhitePoint(const skcms_ICCProfile& profile,
    420                                               CIExy* out) {
    421  Color media_white_point_XYZ;
    422  if (!skcms_GetWTPT(&profile, media_white_point_XYZ.data())) {
    423    return JXL_FAILURE("ICC profile does not contain WhitePoint tag");
    424  }
    425  skcms_Matrix3x3 CHAD;
    426  if (!skcms_GetCHAD(&profile, &CHAD)) {
    427    // If there is no chromatic adaptation matrix, it means that the white point
    428    // is already unadapted.
    429    *out = CIExyFromXYZ(media_white_point_XYZ);
    430    return true;
    431  }
    432  // Otherwise, it has been adapted to the PCS white point using said matrix,
    433  // and the adaptation needs to be undone.
    434  skcms_Matrix3x3 inverse_CHAD;
    435  if (!skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)) {
    436    return JXL_FAILURE("Non-invertible ChromaticAdaptation matrix");
    437  }
    438  Color unadapted_white_point_XYZ;
    439  MatrixProduct(inverse_CHAD, media_white_point_XYZ, unadapted_white_point_XYZ);
    440  *out = CIExyFromXYZ(unadapted_white_point_XYZ);
    441  return true;
    442 }
    443 
    444 Status IdentifyPrimaries(const skcms_ICCProfile& profile,
    445                         const CIExy& wp_unadapted, ColorEncoding* c) {
    446  if (!c->HasPrimaries()) return true;
    447 
    448  skcms_Matrix3x3 CHAD;
    449  skcms_Matrix3x3 inverse_CHAD;
    450  if (skcms_GetCHAD(&profile, &CHAD)) {
    451    JXL_RETURN_IF_ERROR(skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD));
    452  } else {
    453    static constexpr skcms_Matrix3x3 kLMSFromXYZ = {
    454        {{0.8951, 0.2664, -0.1614},
    455         {-0.7502, 1.7135, 0.0367},
    456         {0.0389, -0.0685, 1.0296}}};
    457    static constexpr skcms_Matrix3x3 kXYZFromLMS = {
    458        {{0.9869929, -0.1470543, 0.1599627},
    459         {0.4323053, 0.5183603, 0.0492912},
    460         {-0.0085287, 0.0400428, 0.9684867}}};
    461    static constexpr Color kWpD50XYZ{0.96420288, 1.0, 0.82490540};
    462    Color wp_unadapted_XYZ;
    463    JXL_RETURN_IF_ERROR(
    464        CIEXYZFromWhiteCIExy(wp_unadapted.x, wp_unadapted.y, wp_unadapted_XYZ));
    465    Color wp_D50_LMS;
    466    Color wp_unadapted_LMS;
    467    MatrixProduct(kLMSFromXYZ, kWpD50XYZ, wp_D50_LMS);
    468    MatrixProduct(kLMSFromXYZ, wp_unadapted_XYZ, wp_unadapted_LMS);
    469    inverse_CHAD = {{{wp_unadapted_LMS[0] / wp_D50_LMS[0], 0, 0},
    470                     {0, wp_unadapted_LMS[1] / wp_D50_LMS[1], 0},
    471                     {0, 0, wp_unadapted_LMS[2] / wp_D50_LMS[2]}}};
    472    inverse_CHAD = skcms_Matrix3x3_concat(&kXYZFromLMS, &inverse_CHAD);
    473    inverse_CHAD = skcms_Matrix3x3_concat(&inverse_CHAD, &kLMSFromXYZ);
    474  }
    475 
    476  Color XYZ;
    477  PrimariesCIExy primaries;
    478  CIExy* const chromaticities[] = {&primaries.r, &primaries.g, &primaries.b};
    479  for (int i = 0; i < 3; ++i) {
    480    float RGB[3] = {};
    481    RGB[i] = 1;
    482    skcms_Transform(RGB, skcms_PixelFormat_RGB_fff, skcms_AlphaFormat_Opaque,
    483                    &profile, XYZ.data(), skcms_PixelFormat_RGB_fff,
    484                    skcms_AlphaFormat_Opaque, skcms_XYZD50_profile(), 1);
    485    Color unadapted_XYZ;
    486    MatrixProduct(inverse_CHAD, XYZ, unadapted_XYZ);
    487    *chromaticities[i] = CIExyFromXYZ(unadapted_XYZ);
    488  }
    489  return c->SetPrimaries(primaries);
    490 }
    491 
    492 bool IsApproximatelyEqual(const skcms_ICCProfile& profile,
    493                          const ColorEncoding& JXL_RESTRICT c) {
    494  IccBytes bytes;
    495  if (!MaybeCreateProfile(c.ToExternal(), &bytes)) {
    496    return false;
    497  }
    498 
    499  skcms_ICCProfile profile_test;
    500  if (!DecodeProfile(bytes.data(), bytes.size(), &profile_test)) {
    501    return false;
    502  }
    503 
    504  if (!skcms_ApproximatelyEqualProfiles(&profile_test, &profile)) {
    505    return false;
    506  }
    507 
    508  return true;
    509 }
    510 
    511 Status DetectTransferFunction(const skcms_ICCProfile& profile,
    512                              ColorEncoding* JXL_RESTRICT c) {
    513  JXL_ENSURE(c->color_space != ColorSpace::kXYB);
    514 
    515  float gamma[3] = {};
    516  if (profile.has_trc) {
    517    const auto IsGamma = [](const skcms_TransferFunction& tf) {
    518      return tf.a == 1 && tf.b == 0 &&
    519             /* if b and d are zero, it is fine for c not to be */ tf.d == 0 &&
    520             tf.e == 0 && tf.f == 0;
    521    };
    522    for (int i = 0; i < 3; ++i) {
    523      if (profile.trc[i].table_entries == 0 &&
    524          IsGamma(profile.trc->parametric)) {
    525        gamma[i] = 1.f / profile.trc->parametric.g;
    526      } else {
    527        skcms_TransferFunction approximate_tf;
    528        float max_error;
    529        if (skcms_ApproximateCurve(&profile.trc[i], &approximate_tf,
    530                                   &max_error)) {
    531          if (IsGamma(approximate_tf)) {
    532            gamma[i] = 1.f / approximate_tf.g;
    533          }
    534        }
    535      }
    536    }
    537  }
    538  if (gamma[0] != 0 && std::abs(gamma[0] - gamma[1]) < 1e-4f &&
    539      std::abs(gamma[1] - gamma[2]) < 1e-4f) {
    540    if (c->tf.SetGamma(gamma[0])) {
    541      if (IsApproximatelyEqual(profile, *c)) return true;
    542    }
    543  }
    544 
    545  for (TransferFunction tf : Values<TransferFunction>()) {
    546    // Can only create profile from known transfer function.
    547    if (tf == TransferFunction::kUnknown) continue;
    548    c->tf.SetTransferFunction(tf);
    549    if (IsApproximatelyEqual(profile, *c)) return true;
    550  }
    551 
    552  c->tf.SetTransferFunction(TransferFunction::kUnknown);
    553  return true;
    554 }
    555 
    556 #else  // JPEGXL_ENABLE_SKCMS
    557 
    558 uint32_t Type32(const ColorEncoding& c, bool cmyk) {
    559  if (cmyk) return TYPE_CMYK_FLT;
    560  if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_FLT;
    561  return TYPE_RGB_FLT;
    562 }
    563 
    564 uint32_t Type64(const ColorEncoding& c) {
    565  if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_DBL;
    566  return TYPE_RGB_DBL;
    567 }
    568 
    569 ColorSpace ColorSpaceFromProfile(const Profile& profile) {
    570  switch (cmsGetColorSpace(profile.get())) {
    571    case cmsSigRgbData:
    572    case cmsSigCmykData:
    573      return ColorSpace::kRGB;
    574    case cmsSigGrayData:
    575      return ColorSpace::kGray;
    576    default:
    577      return ColorSpace::kUnknown;
    578  }
    579 }
    580 
    581 // "profile1" is pre-decoded to save time in DetectTransferFunction.
    582 Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
    583                              const IccBytes& icc, const ColorEncoding& c) {
    584  const uint32_t type_src = Type64(c);
    585 
    586  Profile profile2;
    587  JXL_RETURN_IF_ERROR(DecodeProfile(context, Bytes(icc), &profile2));
    588 
    589  Profile profile_xyz;
    590  JXL_RETURN_IF_ERROR(CreateProfileXYZ(context, &profile_xyz));
    591 
    592  const uint32_t intent = INTENT_RELATIVE_COLORIMETRIC;
    593  const uint32_t flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_BLACKPOINTCOMPENSATION |
    594                         cmsFLAGS_HIGHRESPRECALC;
    595  Transform xform1(cmsCreateTransformTHR(context, profile1.get(), type_src,
    596                                         profile_xyz.get(), TYPE_XYZ_DBL,
    597                                         intent, flags));
    598  Transform xform2(cmsCreateTransformTHR(context, profile2.get(), type_src,
    599                                         profile_xyz.get(), TYPE_XYZ_DBL,
    600                                         intent, flags));
    601  if (xform1 == nullptr || xform2 == nullptr) {
    602    return JXL_FAILURE("Failed to create transform");
    603  }
    604 
    605  double in[3];
    606  double out1[3];
    607  double out2[3];
    608 
    609  // Uniformly spaced samples from very dark to almost fully bright.
    610  const double init = 1E-3;
    611  const double step = 0.2;
    612 
    613  if (c.color_space == ColorSpace::kGray) {
    614    // Finer sampling and replicate each component.
    615    for (in[0] = init; in[0] < 1.0; in[0] += step / 8) {
    616      cmsDoTransform(xform1.get(), in, out1, 1);
    617      cmsDoTransform(xform2.get(), in, out2, 1);
    618      if (!cms::ApproxEq(out1[0], out2[0], 2E-4)) {
    619        return false;
    620      }
    621    }
    622  } else {
    623    for (in[0] = init; in[0] < 1.0; in[0] += step) {
    624      for (in[1] = init; in[1] < 1.0; in[1] += step) {
    625        for (in[2] = init; in[2] < 1.0; in[2] += step) {
    626          cmsDoTransform(xform1.get(), in, out1, 1);
    627          cmsDoTransform(xform2.get(), in, out2, 1);
    628          for (size_t i = 0; i < 3; ++i) {
    629            if (!cms::ApproxEq(out1[i], out2[i], 2E-4)) {
    630              return false;
    631            }
    632          }
    633        }
    634      }
    635    }
    636  }
    637 
    638  return true;
    639 }
    640 
    641 // Returns white point that was specified when creating the profile.
    642 // NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation
    643 // differs between ICC versions.
    644 JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context,
    645                                                  const Profile& profile,
    646                                                  const ColorEncoding& c) {
    647  const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>(
    648      cmsReadTag(profile.get(), cmsSigMediaWhitePointTag));
    649  if (white_point != nullptr &&
    650      cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag) == nullptr) {
    651    // No chromatic adaptation matrix: the white point is already unadapted.
    652    return *white_point;
    653  }
    654 
    655  cmsCIEXYZ XYZ = {1.0, 1.0, 1.0};
    656  Profile profile_xyz;
    657  if (!CreateProfileXYZ(context, &profile_xyz)) return XYZ;
    658  // Array arguments are one per profile.
    659  cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()};
    660  // Leave white point unchanged - that is what we're trying to extract.
    661  cmsUInt32Number intents[2] = {INTENT_ABSOLUTE_COLORIMETRIC,
    662                                INTENT_ABSOLUTE_COLORIMETRIC};
    663  cmsBool black_compensation[2] = {0, 0};
    664  cmsFloat64Number adaption[2] = {0.0, 0.0};
    665  // Only transforming a single pixel, so skip expensive optimizations.
    666  cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC;
    667  Transform xform(cmsCreateExtendedTransform(
    668      context, 2, profiles, black_compensation, intents, adaption, nullptr, 0,
    669      Type64(c), TYPE_XYZ_DBL, flags));
    670  if (!xform) return XYZ;  // TODO(lode): return error
    671 
    672  // xy are relative, so magnitude does not matter if we ignore output Y.
    673  const cmsFloat64Number in[3] = {1.0, 1.0, 1.0};
    674  cmsDoTransform(xform.get(), in, &XYZ.X, 1);
    675  return XYZ;
    676 }
    677 
    678 Status IdentifyPrimaries(const cmsContext context, const Profile& profile,
    679                         const cmsCIEXYZ& wp_unadapted, ColorEncoding* c) {
    680  if (!c->HasPrimaries()) return true;
    681  if (ColorSpaceFromProfile(profile) == ColorSpace::kUnknown) return true;
    682 
    683  // These were adapted to the profile illuminant before storing in the profile.
    684  const cmsCIEXYZ* adapted_r = static_cast<const cmsCIEXYZ*>(
    685      cmsReadTag(profile.get(), cmsSigRedColorantTag));
    686  const cmsCIEXYZ* adapted_g = static_cast<const cmsCIEXYZ*>(
    687      cmsReadTag(profile.get(), cmsSigGreenColorantTag));
    688  const cmsCIEXYZ* adapted_b = static_cast<const cmsCIEXYZ*>(
    689      cmsReadTag(profile.get(), cmsSigBlueColorantTag));
    690 
    691  cmsCIEXYZ converted_rgb[3];
    692  if (adapted_r == nullptr || adapted_g == nullptr || adapted_b == nullptr) {
    693    // No colorant tag, determine the XYZ coordinates of the primaries by
    694    // converting from the colorspace.
    695    Profile profile_xyz;
    696    if (!CreateProfileXYZ(context, &profile_xyz)) {
    697      return JXL_FAILURE("Failed to retrieve colorants");
    698    }
    699    // Array arguments are one per profile.
    700    cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()};
    701    cmsUInt32Number intents[2] = {INTENT_RELATIVE_COLORIMETRIC,
    702                                  INTENT_RELATIVE_COLORIMETRIC};
    703    cmsBool black_compensation[2] = {0, 0};
    704    cmsFloat64Number adaption[2] = {0.0, 0.0};
    705    // Only transforming three pixels, so skip expensive optimizations.
    706    cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC;
    707    Transform xform(cmsCreateExtendedTransform(
    708        context, 2, profiles, black_compensation, intents, adaption, nullptr, 0,
    709        Type64(*c), TYPE_XYZ_DBL, flags));
    710    if (!xform) return JXL_FAILURE("Failed to retrieve colorants");
    711 
    712    const cmsFloat64Number in[9] = {1.0, 0.0, 0.0, 0.0, 1.0,
    713                                    0.0, 0.0, 0.0, 1.0};
    714    cmsDoTransform(xform.get(), in, &converted_rgb->X, 3);
    715    adapted_r = &converted_rgb[0];
    716    adapted_g = &converted_rgb[1];
    717    adapted_b = &converted_rgb[2];
    718  }
    719 
    720  // TODO(janwas): no longer assume Bradford and D50.
    721  // Undo the chromatic adaptation.
    722  const cmsCIEXYZ d50 = D50_XYZ();
    723 
    724  cmsCIEXYZ r, g, b;
    725  cmsAdaptToIlluminant(&r, &d50, &wp_unadapted, adapted_r);
    726  cmsAdaptToIlluminant(&g, &d50, &wp_unadapted, adapted_g);
    727  cmsAdaptToIlluminant(&b, &d50, &wp_unadapted, adapted_b);
    728 
    729  const PrimariesCIExy rgb = {CIExyFromXYZ(r), CIExyFromXYZ(g),
    730                              CIExyFromXYZ(b)};
    731  return c->SetPrimaries(rgb);
    732 }
    733 
    734 Status DetectTransferFunction(const cmsContext context, const Profile& profile,
    735                              ColorEncoding* JXL_RESTRICT c) {
    736  JXL_ENSURE(c->color_space != ColorSpace::kXYB);
    737 
    738  float gamma = 0;
    739  if (const auto* gray_trc = reinterpret_cast<const cmsToneCurve*>(
    740          cmsReadTag(profile.get(), cmsSigGrayTRCTag))) {
    741    const double estimated_gamma =
    742        cmsEstimateGamma(gray_trc, /*precision=*/1e-4);
    743    if (estimated_gamma > 0) {
    744      gamma = 1. / estimated_gamma;
    745    }
    746  } else {
    747    float rgb_gamma[3] = {};
    748    int i = 0;
    749    for (const auto tag :
    750         {cmsSigRedTRCTag, cmsSigGreenTRCTag, cmsSigBlueTRCTag}) {
    751      if (const auto* trc = reinterpret_cast<const cmsToneCurve*>(
    752              cmsReadTag(profile.get(), tag))) {
    753        const double estimated_gamma =
    754            cmsEstimateGamma(trc, /*precision=*/1e-4);
    755        if (estimated_gamma > 0) {
    756          rgb_gamma[i] = 1. / estimated_gamma;
    757        }
    758      }
    759      ++i;
    760    }
    761    if (rgb_gamma[0] != 0 && std::abs(rgb_gamma[0] - rgb_gamma[1]) < 1e-4f &&
    762        std::abs(rgb_gamma[1] - rgb_gamma[2]) < 1e-4f) {
    763      gamma = rgb_gamma[0];
    764    }
    765  }
    766 
    767  if (gamma != 0 && c->tf.SetGamma(gamma)) {
    768    IccBytes icc_test;
    769    if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
    770        ProfileEquivalentToICC(context, profile, icc_test, *c)) {
    771      return true;
    772    }
    773  }
    774 
    775  for (TransferFunction tf : Values<TransferFunction>()) {
    776    // Can only create profile from known transfer function.
    777    if (tf == TransferFunction::kUnknown) continue;
    778 
    779    c->tf.SetTransferFunction(tf);
    780 
    781    IccBytes icc_test;
    782    if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
    783        ProfileEquivalentToICC(context, profile, icc_test, *c)) {
    784      return true;
    785    }
    786  }
    787 
    788  c->tf.SetTransferFunction(TransferFunction::kUnknown);
    789  return true;
    790 }
    791 
    792 void ErrorHandler(cmsContext context, cmsUInt32Number code, const char* text) {
    793  JXL_WARNING("LCMS error %u: %s", code, text);
    794 }
    795 
    796 // Returns a context for the current thread, creating it if necessary.
    797 cmsContext GetContext() {
    798  static thread_local void* context_;
    799  if (context_ == nullptr) {
    800    context_ = cmsCreateContext(nullptr, nullptr);
    801    JXL_DASSERT(context_ != nullptr);
    802 
    803    cmsSetLogErrorHandlerTHR(static_cast<cmsContext>(context_), &ErrorHandler);
    804  }
    805  return static_cast<cmsContext>(context_);
    806 }
    807 
    808 #endif  // JPEGXL_ENABLE_SKCMS
    809 
    810 Status GetPrimariesLuminances(const ColorEncoding& encoding,
    811                              float luminances[3]) {
    812  // Explanation:
    813  // We know that the three primaries must sum to white:
    814  //
    815  // [Xr, Xg, Xb;     [1;     [Xw;
    816  //  Yr, Yg, Yb;  ×   1;  =   Yw;
    817  //  Zr, Zg, Zb]      1]      Zw]
    818  //
    819  // By noting that X = x·(X+Y+Z), Y = y·(X+Y+Z) and Z = z·(X+Y+Z) (note the
    820  // lower case indicating chromaticity), and factoring the totals (X+Y+Z) out
    821  // of the left matrix and into the all-ones vector, we get:
    822  //
    823  // [xr, xg, xb;     [Xr + Yr + Zr;     [Xw;
    824  //  yr, yg, yb;  ×   Xg + Yg + Zg;  =   Yw;
    825  //  zr, zg, zb]      Xb + Yb + Zb]      Zw]
    826  //
    827  // Which makes it apparent that we can compute those totals as:
    828  //
    829  //                  [Xr + Yr + Zr;     inv([xr, xg, xb;      [Xw;
    830  //                   Xg + Yg + Zg;  =       yr, yg, yb;   ×   Yw;
    831  //                   Xb + Yb + Zb]          zr, zg, zb])      Zw]
    832  //
    833  // From there, by multiplying each total by its corresponding y, we get Y for
    834  // that primary.
    835 
    836  Color white_XYZ;
    837  CIExy wp = encoding.GetWhitePoint();
    838  JXL_RETURN_IF_ERROR(CIEXYZFromWhiteCIExy(wp.x, wp.y, white_XYZ));
    839 
    840  PrimariesCIExy primaries;
    841  JXL_RETURN_IF_ERROR(encoding.GetPrimaries(primaries));
    842  Matrix3x3d chromaticities{
    843      {{primaries.r.x, primaries.g.x, primaries.b.x},
    844       {primaries.r.y, primaries.g.y, primaries.b.y},
    845       {1 - primaries.r.x - primaries.r.y, 1 - primaries.g.x - primaries.g.y,
    846        1 - primaries.b.x - primaries.b.y}}};
    847  JXL_RETURN_IF_ERROR(Inv3x3Matrix(chromaticities));
    848  const double ys[3] = {primaries.r.y, primaries.g.y, primaries.b.y};
    849  for (size_t i = 0; i < 3; ++i) {
    850    luminances[i] = ys[i] * (chromaticities[i][0] * white_XYZ[0] +
    851                             chromaticities[i][1] * white_XYZ[1] +
    852                             chromaticities[i][2] * white_XYZ[2]);
    853  }
    854  return true;
    855 }
    856 
    857 Status ApplyHlgOotf(JxlCms* t, float* JXL_RESTRICT buf, size_t xsize,
    858                    bool forward) {
    859  if (295 <= t->intensity_target && t->intensity_target <= 305) {
    860    // The gamma is approximately 1 so this can essentially be skipped.
    861    return true;
    862  }
    863  float gamma = 1.2f * std::pow(1.111f, std::log2(t->intensity_target * 1e-3f));
    864  if (!forward) gamma = 1.f / gamma;
    865 
    866  switch (t->hlg_ootf_num_channels) {
    867    case 1:
    868      for (size_t x = 0; x < xsize; ++x) {
    869        buf[x] = std::pow(buf[x], gamma);
    870      }
    871      break;
    872 
    873    case 3:
    874      for (size_t x = 0; x < xsize; x += 3) {
    875        const float luminance = buf[x] * t->hlg_ootf_luminances[0] +
    876                                buf[x + 1] * t->hlg_ootf_luminances[1] +
    877                                buf[x + 2] * t->hlg_ootf_luminances[2];
    878        const float ratio = std::pow(luminance, gamma - 1);
    879        if (std::isfinite(ratio)) {
    880          buf[x] *= ratio;
    881          buf[x + 1] *= ratio;
    882          buf[x + 2] *= ratio;
    883          if (forward && gamma < 1) {
    884            // If gamma < 1, the ratio above will be > 1 which can push bright
    885            // saturated highlights out of gamut. There are several possible
    886            // ways to bring them back in-gamut; this one preserves hue and
    887            // saturation at the slight expense of luminance. If !forward, the
    888            // previously-applied forward OOTF with gamma > 1 already pushed
    889            // those highlights down and we are simply putting them back where
    890            // they were so this is not necessary.
    891            const float maximum =
    892                std::max(buf[x], std::max(buf[x + 1], buf[x + 2]));
    893            if (maximum > 1) {
    894              const float normalizer = 1.f / maximum;
    895              buf[x] *= normalizer;
    896              buf[x + 1] *= normalizer;
    897              buf[x + 2] *= normalizer;
    898            }
    899          }
    900        }
    901      }
    902      break;
    903 
    904    default:
    905      return JXL_FAILURE("HLG OOTF not implemented for %" PRIuS " channels",
    906                         t->hlg_ootf_num_channels);
    907  }
    908  return true;
    909 }
    910 
    911 bool IsKnownTransferFunction(jxl::cms::TransferFunction tf) {
    912  using TF = jxl::cms::TransferFunction;
    913  // All but kUnknown
    914  return tf == TF::k709 || tf == TF::kLinear || tf == TF::kSRGB ||
    915         tf == TF::kPQ || tf == TF::kDCI || tf == TF::kHLG;
    916 }
    917 
    918 constexpr uint8_t kColorPrimariesP3_D65 = 12;
    919 
    920 bool IsKnownColorPrimaries(uint8_t color_primaries) {
    921  using P = jxl::cms::Primaries;
    922  // All but kCustom
    923  if (color_primaries == kColorPrimariesP3_D65) return true;
    924  const auto p = static_cast<Primaries>(color_primaries);
    925  return p == P::kSRGB || p == P::k2100 || p == P::kP3;
    926 }
    927 
    928 bool ApplyCICP(const uint8_t color_primaries,
    929               const uint8_t transfer_characteristics,
    930               const uint8_t matrix_coefficients, const uint8_t full_range,
    931               ColorEncoding* JXL_RESTRICT c) {
    932  if (matrix_coefficients != 0) return false;
    933  if (full_range != 1) return false;
    934 
    935  const auto primaries = static_cast<Primaries>(color_primaries);
    936  const auto tf = static_cast<TransferFunction>(transfer_characteristics);
    937  if (!IsKnownTransferFunction(tf)) return false;
    938  if (!IsKnownColorPrimaries(color_primaries)) return false;
    939  c->color_space = ColorSpace::kRGB;
    940  c->tf.SetTransferFunction(tf);
    941  if (primaries == Primaries::kP3) {
    942    c->white_point = WhitePoint::kDCI;
    943    c->primaries = Primaries::kP3;
    944  } else if (color_primaries == kColorPrimariesP3_D65) {
    945    c->white_point = WhitePoint::kD65;
    946    c->primaries = Primaries::kP3;
    947  } else {
    948    c->white_point = WhitePoint::kD65;
    949    c->primaries = primaries;
    950  }
    951  return true;
    952 }
    953 
    954 JXL_BOOL JxlCmsSetFieldsFromICC(void* user_data, const uint8_t* icc_data,
    955                                size_t icc_size, JxlColorEncoding* c,
    956                                JXL_BOOL* cmyk) {
    957  if (c == nullptr) return JXL_FALSE;
    958  if (cmyk == nullptr) return JXL_FALSE;
    959 
    960  *cmyk = JXL_FALSE;
    961 
    962  // In case parsing fails, mark the ColorEncoding as invalid.
    963  c->color_space = JXL_COLOR_SPACE_UNKNOWN;
    964  c->transfer_function = JXL_TRANSFER_FUNCTION_UNKNOWN;
    965 
    966  if (icc_size == 0) return JXL_FAILURE("Empty ICC profile");
    967 
    968  ColorEncoding c_enc;
    969 
    970 #if JPEGXL_ENABLE_SKCMS
    971  if (icc_size < 128) {
    972    return JXL_FAILURE("ICC file too small");
    973  }
    974 
    975  skcms_ICCProfile profile;
    976  JXL_RETURN_IF_ERROR(skcms_Parse(icc_data, icc_size, &profile));
    977 
    978  // skcms does not return the rendering intent, so get it from the file. It
    979  // should be encoded as big-endian 32-bit integer in bytes 60..63.
    980  uint32_t big_endian_rendering_intent = icc_data[67] + (icc_data[66] << 8) +
    981                                         (icc_data[65] << 16) +
    982                                         (icc_data[64] << 24);
    983  // Some files encode rendering intent as little endian, which is not spec
    984  // compliant. However we accept those with a warning.
    985  uint32_t little_endian_rendering_intent = (icc_data[67] << 24) +
    986                                            (icc_data[66] << 16) +
    987                                            (icc_data[65] << 8) + icc_data[64];
    988  uint32_t candidate_rendering_intent =
    989      std::min(big_endian_rendering_intent, little_endian_rendering_intent);
    990  if (candidate_rendering_intent != big_endian_rendering_intent) {
    991    JXL_WARNING(
    992        "Invalid rendering intent bytes: [0x%02X 0x%02X 0x%02X 0x%02X], "
    993        "assuming %u was meant",
    994        icc_data[64], icc_data[65], icc_data[66], icc_data[67],
    995        candidate_rendering_intent);
    996  }
    997  if (candidate_rendering_intent > 3) {
    998    return JXL_FAILURE("Invalid rendering intent %u\n",
    999                       candidate_rendering_intent);
   1000  }
   1001  // ICC and RenderingIntent have the same values (0..3).
   1002  c_enc.rendering_intent =
   1003      static_cast<RenderingIntent>(candidate_rendering_intent);
   1004 
   1005  if (profile.has_CICP &&
   1006      ApplyCICP(profile.CICP.color_primaries,
   1007                profile.CICP.transfer_characteristics,
   1008                profile.CICP.matrix_coefficients,
   1009                profile.CICP.video_full_range_flag, &c_enc)) {
   1010    *c = c_enc.ToExternal();
   1011    return JXL_TRUE;
   1012  }
   1013 
   1014  c_enc.color_space = ColorSpaceFromProfile(profile);
   1015  *cmyk = TO_JXL_BOOL(profile.data_color_space == skcms_Signature_CMYK);
   1016 
   1017  CIExy wp_unadapted;
   1018  JXL_RETURN_IF_ERROR(UnadaptedWhitePoint(profile, &wp_unadapted));
   1019  JXL_RETURN_IF_ERROR(c_enc.SetWhitePoint(wp_unadapted));
   1020 
   1021  // Relies on color_space.
   1022  JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, &c_enc));
   1023 
   1024  // Relies on color_space/white point/primaries being set already.
   1025  JXL_RETURN_IF_ERROR(DetectTransferFunction(profile, &c_enc));
   1026 #else  // JPEGXL_ENABLE_SKCMS
   1027 
   1028  const cmsContext context = GetContext();
   1029 
   1030  Profile profile;
   1031  JXL_RETURN_IF_ERROR(
   1032      DecodeProfile(context, Bytes(icc_data, icc_size), &profile));
   1033 
   1034  const cmsUInt32Number rendering_intent32 =
   1035      cmsGetHeaderRenderingIntent(profile.get());
   1036  if (rendering_intent32 > 3) {
   1037    return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent32);
   1038  }
   1039  // ICC and RenderingIntent have the same values (0..3).
   1040  c_enc.rendering_intent = static_cast<RenderingIntent>(rendering_intent32);
   1041 
   1042  static constexpr size_t kCICPSize = 12;
   1043  static constexpr auto kCICPSignature =
   1044      static_cast<cmsTagSignature>(0x63696370);
   1045  uint8_t cicp_buffer[kCICPSize];
   1046  if (cmsReadRawTag(profile.get(), kCICPSignature, cicp_buffer, kCICPSize) ==
   1047          kCICPSize &&
   1048      ApplyCICP(cicp_buffer[8], cicp_buffer[9], cicp_buffer[10],
   1049                cicp_buffer[11], &c_enc)) {
   1050    *c = c_enc.ToExternal();
   1051    return JXL_TRUE;
   1052  }
   1053 
   1054  c_enc.color_space = ColorSpaceFromProfile(profile);
   1055  if (cmsGetColorSpace(profile.get()) == cmsSigCmykData) {
   1056    *cmyk = JXL_TRUE;
   1057    *c = c_enc.ToExternal();
   1058    return JXL_TRUE;
   1059  }
   1060 
   1061  const cmsCIEXYZ wp_unadapted = UnadaptedWhitePoint(context, profile, c_enc);
   1062  JXL_RETURN_IF_ERROR(c_enc.SetWhitePoint(CIExyFromXYZ(wp_unadapted)));
   1063 
   1064  // Relies on color_space.
   1065  JXL_RETURN_IF_ERROR(
   1066      IdentifyPrimaries(context, profile, wp_unadapted, &c_enc));
   1067 
   1068  // Relies on color_space/white point/primaries being set already.
   1069  JXL_RETURN_IF_ERROR(DetectTransferFunction(context, profile, &c_enc));
   1070 
   1071 #endif  // JPEGXL_ENABLE_SKCMS
   1072 
   1073  *c = c_enc.ToExternal();
   1074  return JXL_TRUE;
   1075 }
   1076 
   1077 }  // namespace
   1078 
   1079 namespace {
   1080 
   1081 void JxlCmsDestroy(void* cms_data) {
   1082  if (cms_data == nullptr) return;
   1083  JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
   1084 #if !JPEGXL_ENABLE_SKCMS
   1085  TransformDeleter()(t->lcms_transform);
   1086 #endif
   1087  delete t;
   1088 }
   1089 
   1090 void AllocateBuffer(size_t length, size_t num_threads,
   1091                    std::vector<float>* storage, std::vector<float*>* view) {
   1092  constexpr size_t kAlign = 128 / sizeof(float);
   1093  size_t stride = RoundUpTo(length, kAlign);
   1094  storage->resize(stride * num_threads + kAlign);
   1095  intptr_t addr = reinterpret_cast<intptr_t>(storage->data());
   1096  size_t offset =
   1097      (RoundUpTo(addr, kAlign * sizeof(float)) - addr) / sizeof(float);
   1098  view->clear();
   1099  view->reserve(num_threads);
   1100  for (size_t i = 0; i < num_threads; ++i) {
   1101    view->emplace_back(storage->data() + offset + i * stride);
   1102  }
   1103 }
   1104 
   1105 void* JxlCmsInit(void* init_data, size_t num_threads, size_t xsize,
   1106                 const JxlColorProfile* input, const JxlColorProfile* output,
   1107                 float intensity_target) {
   1108  if (init_data == nullptr) {
   1109    JXL_NOTIFY_ERROR("JxlCmsInit: init_data is nullptr");
   1110    return nullptr;
   1111  }
   1112  const auto* cms = static_cast<const JxlCmsInterface*>(init_data);
   1113  auto t = jxl::make_unique<JxlCms>();
   1114  IccBytes icc_src;
   1115  IccBytes icc_dst;
   1116  if (input->icc.size == 0) {
   1117    JXL_NOTIFY_ERROR("JxlCmsInit: empty input ICC");
   1118    return nullptr;
   1119  }
   1120  if (output->icc.size == 0) {
   1121    JXL_NOTIFY_ERROR("JxlCmsInit: empty OUTPUT ICC");
   1122    return nullptr;
   1123  }
   1124  icc_src.assign(input->icc.data, input->icc.data + input->icc.size);
   1125  ColorEncoding c_src;
   1126  if (!c_src.SetFieldsFromICC(std::move(icc_src), *cms)) {
   1127    JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse input ICC");
   1128    return nullptr;
   1129  }
   1130  icc_dst.assign(output->icc.data, output->icc.data + output->icc.size);
   1131  ColorEncoding c_dst;
   1132  if (!c_dst.SetFieldsFromICC(std::move(icc_dst), *cms)) {
   1133    JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse output ICC");
   1134    return nullptr;
   1135  }
   1136 #if JXL_CMS_VERBOSE
   1137  printf("%s -> %s\n", Description(c_src).c_str(), Description(c_dst).c_str());
   1138 #endif
   1139 
   1140 #if JPEGXL_ENABLE_SKCMS
   1141  if (!DecodeProfile(input->icc.data, input->icc.size, &t->profile_src)) {
   1142    JXL_NOTIFY_ERROR("JxlCmsInit: skcms failed to parse input ICC");
   1143    return nullptr;
   1144  }
   1145  if (!DecodeProfile(output->icc.data, output->icc.size, &t->profile_dst)) {
   1146    JXL_NOTIFY_ERROR("JxlCmsInit: skcms failed to parse output ICC");
   1147    return nullptr;
   1148  }
   1149 #else   // JPEGXL_ENABLE_SKCMS
   1150  const cmsContext context = GetContext();
   1151  Profile profile_src, profile_dst;
   1152  if (!DecodeProfile(context, Bytes(c_src.icc), &profile_src)) {
   1153    JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse input ICC");
   1154    return nullptr;
   1155  }
   1156  if (!DecodeProfile(context, Bytes(c_dst.icc), &profile_dst)) {
   1157    JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse output ICC");
   1158    return nullptr;
   1159  }
   1160 #endif  // JPEGXL_ENABLE_SKCMS
   1161 
   1162  t->skip_lcms = false;
   1163  if (c_src.SameColorEncoding(c_dst)) {
   1164    t->skip_lcms = true;
   1165 #if JXL_CMS_VERBOSE
   1166    printf("Skip CMS\n");
   1167 #endif
   1168  }
   1169 
   1170  t->apply_hlg_ootf = c_src.tf.IsHLG() != c_dst.tf.IsHLG();
   1171  if (t->apply_hlg_ootf) {
   1172    const ColorEncoding* c_hlg = c_src.tf.IsHLG() ? &c_src : &c_dst;
   1173    t->hlg_ootf_num_channels = c_hlg->Channels();
   1174    if (t->hlg_ootf_num_channels == 3 &&
   1175        !GetPrimariesLuminances(*c_hlg, t->hlg_ootf_luminances.data())) {
   1176      JXL_NOTIFY_ERROR(
   1177          "JxlCmsInit: failed to compute the luminances of primaries");
   1178      return nullptr;
   1179    }
   1180  }
   1181 
   1182  // Special-case SRGB <=> linear if the primaries / white point are the same,
   1183  // or any conversion where PQ or HLG is involved:
   1184  bool src_linear = c_src.tf.IsLinear();
   1185  const bool dst_linear = c_dst.tf.IsLinear();
   1186 
   1187  if (c_src.tf.IsPQ() || c_src.tf.IsHLG() ||
   1188      (c_src.tf.IsSRGB() && dst_linear && c_src.SameColorSpace(c_dst))) {
   1189    // Construct new profile as if the data were already/still linear.
   1190    ColorEncoding c_linear_src = c_src;
   1191    c_linear_src.tf.SetTransferFunction(TransferFunction::kLinear);
   1192 #if JPEGXL_ENABLE_SKCMS
   1193    skcms_ICCProfile new_src;
   1194 #else   // JPEGXL_ENABLE_SKCMS
   1195    Profile new_src;
   1196 #endif  // JPEGXL_ENABLE_SKCMS
   1197        // Only enable ExtraTF if profile creation succeeded.
   1198    if (MaybeCreateProfile(c_linear_src.ToExternal(), &icc_src) &&
   1199 #if JPEGXL_ENABLE_SKCMS
   1200        DecodeProfile(icc_src.data(), icc_src.size(), &new_src)) {
   1201 #else   // JPEGXL_ENABLE_SKCMS
   1202        DecodeProfile(context, Bytes(icc_src), &new_src)) {
   1203 #endif  // JPEGXL_ENABLE_SKCMS
   1204 #if JXL_CMS_VERBOSE
   1205      printf("Special HLG/PQ/sRGB -> linear\n");
   1206 #endif
   1207 #if JPEGXL_ENABLE_SKCMS
   1208      t->icc_src = std::move(icc_src);
   1209      t->profile_src = new_src;
   1210 #else   // JPEGXL_ENABLE_SKCMS
   1211      profile_src.swap(new_src);
   1212 #endif  // JPEGXL_ENABLE_SKCMS
   1213      t->preprocess = c_src.tf.IsSRGB()
   1214                          ? ExtraTF::kSRGB
   1215                          : (c_src.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG);
   1216      c_src = c_linear_src;
   1217      src_linear = true;
   1218    } else {
   1219      if (t->apply_hlg_ootf) {
   1220        JXL_NOTIFY_ERROR(
   1221            "Failed to create extra linear source profile, and HLG OOTF "
   1222            "required");
   1223        return nullptr;
   1224      }
   1225      JXL_WARNING("Failed to create extra linear destination profile");
   1226    }
   1227  }
   1228 
   1229  if (c_dst.tf.IsPQ() || c_dst.tf.IsHLG() ||
   1230      (c_dst.tf.IsSRGB() && src_linear && c_src.SameColorSpace(c_dst))) {
   1231    ColorEncoding c_linear_dst = c_dst;
   1232    c_linear_dst.tf.SetTransferFunction(TransferFunction::kLinear);
   1233 #if JPEGXL_ENABLE_SKCMS
   1234    skcms_ICCProfile new_dst;
   1235 #else   // JPEGXL_ENABLE_SKCMS
   1236    Profile new_dst;
   1237 #endif  // JPEGXL_ENABLE_SKCMS
   1238    // Only enable ExtraTF if profile creation succeeded.
   1239    if (MaybeCreateProfile(c_linear_dst.ToExternal(), &icc_dst) &&
   1240 #if JPEGXL_ENABLE_SKCMS
   1241        DecodeProfile(icc_dst.data(), icc_dst.size(), &new_dst)) {
   1242 #else   // JPEGXL_ENABLE_SKCMS
   1243        DecodeProfile(context, Bytes(icc_dst), &new_dst)) {
   1244 #endif  // JPEGXL_ENABLE_SKCMS
   1245 #if JXL_CMS_VERBOSE
   1246      printf("Special linear -> HLG/PQ/sRGB\n");
   1247 #endif
   1248 #if JPEGXL_ENABLE_SKCMS
   1249      t->icc_dst = std::move(icc_dst);
   1250      t->profile_dst = new_dst;
   1251 #else   // JPEGXL_ENABLE_SKCMS
   1252      profile_dst.swap(new_dst);
   1253 #endif  // JPEGXL_ENABLE_SKCMS
   1254      t->postprocess = c_dst.tf.IsSRGB()
   1255                           ? ExtraTF::kSRGB
   1256                           : (c_dst.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG);
   1257      c_dst = c_linear_dst;
   1258    } else {
   1259      if (t->apply_hlg_ootf) {
   1260        JXL_NOTIFY_ERROR(
   1261            "Failed to create extra linear destination profile, and inverse "
   1262            "HLG OOTF required");
   1263        return nullptr;
   1264      }
   1265      JXL_WARNING("Failed to create extra linear destination profile");
   1266    }
   1267  }
   1268 
   1269  if (c_src.SameColorEncoding(c_dst)) {
   1270 #if JXL_CMS_VERBOSE
   1271    printf("Same intermediary linear profiles, skipping CMS\n");
   1272 #endif
   1273    t->skip_lcms = true;
   1274  }
   1275 
   1276 #if JPEGXL_ENABLE_SKCMS
   1277  if (!skcms_MakeUsableAsDestination(&t->profile_dst)) {
   1278    JXL_NOTIFY_ERROR(
   1279        "Failed to make %s usable as a color transform destination",
   1280        ColorEncodingDescription(c_dst.ToExternal()).c_str());
   1281    return nullptr;
   1282  }
   1283 #endif  // JPEGXL_ENABLE_SKCMS
   1284 
   1285  // Not including alpha channel (copied separately).
   1286  const size_t channels_src = (c_src.cmyk ? 4 : c_src.Channels());
   1287  const size_t channels_dst = c_dst.Channels();
   1288 #if JXL_CMS_VERBOSE
   1289  printf("Channels: %" PRIuS "; Threads: %" PRIuS "\n", channels_src,
   1290         num_threads);
   1291 #endif
   1292 
   1293 #if !JPEGXL_ENABLE_SKCMS
   1294  // Type includes color space (XYZ vs RGB), so can be different.
   1295  const uint32_t type_src = Type32(c_src, channels_src == 4);
   1296  const uint32_t type_dst = Type32(c_dst, false);
   1297  const uint32_t intent = static_cast<uint32_t>(c_dst.rendering_intent);
   1298  // Use cmsFLAGS_NOCACHE to disable the 1-pixel cache and make calling
   1299  // cmsDoTransform() thread-safe.
   1300  const uint32_t flags = cmsFLAGS_NOCACHE | cmsFLAGS_BLACKPOINTCOMPENSATION |
   1301                         cmsFLAGS_HIGHRESPRECALC;
   1302  t->lcms_transform =
   1303      cmsCreateTransformTHR(context, profile_src.get(), type_src,
   1304                            profile_dst.get(), type_dst, intent, flags);
   1305  if (t->lcms_transform == nullptr) {
   1306    JXL_NOTIFY_ERROR("Failed to create transform");
   1307    return nullptr;
   1308  }
   1309 #endif  // !JPEGXL_ENABLE_SKCMS
   1310 
   1311  // Ideally LCMS would convert directly from External to Image3. However,
   1312  // cmsDoTransformLineStride only accepts 32-bit BytesPerPlaneIn, whereas our
   1313  // planes can be more than 4 GiB apart. Hence, transform inputs/outputs must
   1314  // be interleaved. Calling cmsDoTransform for each pixel is expensive
   1315  // (indirect call). We therefore transform rows, which requires per-thread
   1316  // buffers. To avoid separate allocations, we use the rows of an image.
   1317  // Because LCMS apparently also cannot handle <= 16 bit inputs and 32-bit
   1318  // outputs (or vice versa), we use floating point input/output.
   1319  t->channels_src = channels_src;
   1320  t->channels_dst = channels_dst;
   1321 #if !JPEGXL_ENABLE_SKCMS
   1322  size_t actual_channels_src = channels_src;
   1323  size_t actual_channels_dst = channels_dst;
   1324 #else
   1325  // SkiaCMS doesn't support grayscale float buffers, so we create space for RGB
   1326  // float buffers anyway.
   1327  size_t actual_channels_src = (channels_src == 4 ? 4 : 3);
   1328  size_t actual_channels_dst = 3;
   1329 #endif
   1330  AllocateBuffer(xsize * actual_channels_src, num_threads, &t->src_storage,
   1331                 &t->buf_src);
   1332  AllocateBuffer(xsize * actual_channels_dst, num_threads, &t->dst_storage,
   1333                 &t->buf_dst);
   1334  t->intensity_target = intensity_target;
   1335  return t.release();
   1336 }
   1337 
   1338 float* JxlCmsGetSrcBuf(void* cms_data, size_t thread) {
   1339  JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
   1340  return t->buf_src[thread];
   1341 }
   1342 
   1343 float* JxlCmsGetDstBuf(void* cms_data, size_t thread) {
   1344  JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
   1345  return t->buf_dst[thread];
   1346 }
   1347 
   1348 }  // namespace
   1349 
   1350 extern "C" {
   1351 
   1352 JXL_CMS_EXPORT const JxlCmsInterface* JxlGetDefaultCms() {
   1353  static constexpr JxlCmsInterface kInterface = {
   1354      /*set_fields_data=*/nullptr,
   1355      /*set_fields_from_icc=*/&JxlCmsSetFieldsFromICC,
   1356      /*init_data=*/const_cast<void*>(static_cast<const void*>(&kInterface)),
   1357      /*init=*/&JxlCmsInit,
   1358      /*get_src_buf=*/&JxlCmsGetSrcBuf,
   1359      /*get_dst_buf=*/&JxlCmsGetDstBuf,
   1360      /*run=*/&DoColorSpaceTransform,
   1361      /*destroy=*/&JxlCmsDestroy};
   1362  return &kInterface;
   1363 }
   1364 
   1365 }  // extern "C"
   1366 
   1367 }  // namespace jxl
   1368 #endif  // HWY_ONCE