libjxl

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

jxl_cms.cc (47857B)


      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/span.h"
     31 #include "lib/jxl/base/status.h"
     32 #include "lib/jxl/cms/jxl_cms_internal.h"
     33 #include "lib/jxl/cms/transfer_functions-inl.h"
     34 #include "lib/jxl/color_encoding_internal.h"
     35 #if JPEGXL_ENABLE_SKCMS
     36 #include "skcms.h"
     37 #else  // JPEGXL_ENABLE_SKCMS
     38 #include "lcms2.h"
     39 #include "lcms2_plugin.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_DASSERT(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_DASSERT(false);  // 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_CHECK(
    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 float XYZ[3]) {
    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 float vector_in[3],
    409                    float vector_out[3]) {
    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   float media_white_point_XYZ[3];
    422   if (!skcms_GetWTPT(&profile, media_white_point_XYZ)) {
    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   float unadapted_white_point_XYZ[3];
    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 float kWpD50XYZ[3] = {0.96420288, 1.0, 0.82490540};
    462     float wp_unadapted_XYZ[3];
    463     JXL_RETURN_IF_ERROR(
    464         CIEXYZFromWhiteCIExy(wp_unadapted.x, wp_unadapted.y, wp_unadapted_XYZ));
    465     float wp_D50_LMS[3];
    466     float wp_unadapted_LMS[3];
    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   float XYZ[3];
    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, skcms_PixelFormat_RGB_fff,
    484                     skcms_AlphaFormat_Opaque, skcms_XYZD50_profile(), 1);
    485     float unadapted_XYZ[3];
    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 void DetectTransferFunction(const skcms_ICCProfile& profile,
    512                             ColorEncoding* JXL_RESTRICT c) {
    513   JXL_CHECK(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;
    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;
    550   }
    551 
    552   c->tf.SetTransferFunction(TransferFunction::kUnknown);
    553 }
    554 
    555 #else  // JPEGXL_ENABLE_SKCMS
    556 
    557 uint32_t Type32(const ColorEncoding& c, bool cmyk) {
    558   if (cmyk) return TYPE_CMYK_FLT;
    559   if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_FLT;
    560   return TYPE_RGB_FLT;
    561 }
    562 
    563 uint32_t Type64(const ColorEncoding& c) {
    564   if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_DBL;
    565   return TYPE_RGB_DBL;
    566 }
    567 
    568 ColorSpace ColorSpaceFromProfile(const Profile& profile) {
    569   switch (cmsGetColorSpace(profile.get())) {
    570     case cmsSigRgbData:
    571     case cmsSigCmykData:
    572       return ColorSpace::kRGB;
    573     case cmsSigGrayData:
    574       return ColorSpace::kGray;
    575     default:
    576       return ColorSpace::kUnknown;
    577   }
    578 }
    579 
    580 // "profile1" is pre-decoded to save time in DetectTransferFunction.
    581 Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
    582                               const IccBytes& icc, const ColorEncoding& c) {
    583   const uint32_t type_src = Type64(c);
    584 
    585   Profile profile2;
    586   JXL_RETURN_IF_ERROR(DecodeProfile(context, Bytes(icc), &profile2));
    587 
    588   Profile profile_xyz;
    589   JXL_RETURN_IF_ERROR(CreateProfileXYZ(context, &profile_xyz));
    590 
    591   const uint32_t intent = INTENT_RELATIVE_COLORIMETRIC;
    592   const uint32_t flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_BLACKPOINTCOMPENSATION |
    593                          cmsFLAGS_HIGHRESPRECALC;
    594   Transform xform1(cmsCreateTransformTHR(context, profile1.get(), type_src,
    595                                          profile_xyz.get(), TYPE_XYZ_DBL,
    596                                          intent, flags));
    597   Transform xform2(cmsCreateTransformTHR(context, profile2.get(), type_src,
    598                                          profile_xyz.get(), TYPE_XYZ_DBL,
    599                                          intent, flags));
    600   if (xform1 == nullptr || xform2 == nullptr) {
    601     return JXL_FAILURE("Failed to create transform");
    602   }
    603 
    604   double in[3];
    605   double out1[3];
    606   double out2[3];
    607 
    608   // Uniformly spaced samples from very dark to almost fully bright.
    609   const double init = 1E-3;
    610   const double step = 0.2;
    611 
    612   if (c.color_space == ColorSpace::kGray) {
    613     // Finer sampling and replicate each component.
    614     for (in[0] = init; in[0] < 1.0; in[0] += step / 8) {
    615       cmsDoTransform(xform1.get(), in, out1, 1);
    616       cmsDoTransform(xform2.get(), in, out2, 1);
    617       if (!cms::ApproxEq(out1[0], out2[0], 2E-4)) {
    618         return false;
    619       }
    620     }
    621   } else {
    622     for (in[0] = init; in[0] < 1.0; in[0] += step) {
    623       for (in[1] = init; in[1] < 1.0; in[1] += step) {
    624         for (in[2] = init; in[2] < 1.0; in[2] += step) {
    625           cmsDoTransform(xform1.get(), in, out1, 1);
    626           cmsDoTransform(xform2.get(), in, out2, 1);
    627           for (size_t i = 0; i < 3; ++i) {
    628             if (!cms::ApproxEq(out1[i], out2[i], 2E-4)) {
    629               return false;
    630             }
    631           }
    632         }
    633       }
    634     }
    635   }
    636 
    637   return true;
    638 }
    639 
    640 // Returns white point that was specified when creating the profile.
    641 // NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation
    642 // differs between ICC versions.
    643 JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context,
    644                                                   const Profile& profile,
    645                                                   const ColorEncoding& c) {
    646   const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>(
    647       cmsReadTag(profile.get(), cmsSigMediaWhitePointTag));
    648   if (white_point != nullptr &&
    649       cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag) == nullptr) {
    650     // No chromatic adaptation matrix: the white point is already unadapted.
    651     return *white_point;
    652   }
    653 
    654   cmsCIEXYZ XYZ = {1.0, 1.0, 1.0};
    655   Profile profile_xyz;
    656   if (!CreateProfileXYZ(context, &profile_xyz)) return XYZ;
    657   // Array arguments are one per profile.
    658   cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()};
    659   // Leave white point unchanged - that is what we're trying to extract.
    660   cmsUInt32Number intents[2] = {INTENT_ABSOLUTE_COLORIMETRIC,
    661                                 INTENT_ABSOLUTE_COLORIMETRIC};
    662   cmsBool black_compensation[2] = {0, 0};
    663   cmsFloat64Number adaption[2] = {0.0, 0.0};
    664   // Only transforming a single pixel, so skip expensive optimizations.
    665   cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC;
    666   Transform xform(cmsCreateExtendedTransform(
    667       context, 2, profiles, black_compensation, intents, adaption, nullptr, 0,
    668       Type64(c), TYPE_XYZ_DBL, flags));
    669   if (!xform) return XYZ;  // TODO(lode): return error
    670 
    671   // xy are relative, so magnitude does not matter if we ignore output Y.
    672   const cmsFloat64Number in[3] = {1.0, 1.0, 1.0};
    673   cmsDoTransform(xform.get(), in, &XYZ.X, 1);
    674   return XYZ;
    675 }
    676 
    677 Status IdentifyPrimaries(const cmsContext context, const Profile& profile,
    678                          const cmsCIEXYZ& wp_unadapted, ColorEncoding* c) {
    679   if (!c->HasPrimaries()) return true;
    680   if (ColorSpaceFromProfile(profile) == ColorSpace::kUnknown) return true;
    681 
    682   // These were adapted to the profile illuminant before storing in the profile.
    683   const cmsCIEXYZ* adapted_r = static_cast<const cmsCIEXYZ*>(
    684       cmsReadTag(profile.get(), cmsSigRedColorantTag));
    685   const cmsCIEXYZ* adapted_g = static_cast<const cmsCIEXYZ*>(
    686       cmsReadTag(profile.get(), cmsSigGreenColorantTag));
    687   const cmsCIEXYZ* adapted_b = static_cast<const cmsCIEXYZ*>(
    688       cmsReadTag(profile.get(), cmsSigBlueColorantTag));
    689 
    690   cmsCIEXYZ converted_rgb[3];
    691   if (adapted_r == nullptr || adapted_g == nullptr || adapted_b == nullptr) {
    692     // No colorant tag, determine the XYZ coordinates of the primaries by
    693     // converting from the colorspace.
    694     Profile profile_xyz;
    695     if (!CreateProfileXYZ(context, &profile_xyz)) {
    696       return JXL_FAILURE("Failed to retrieve colorants");
    697     }
    698     // Array arguments are one per profile.
    699     cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()};
    700     cmsUInt32Number intents[2] = {INTENT_RELATIVE_COLORIMETRIC,
    701                                   INTENT_RELATIVE_COLORIMETRIC};
    702     cmsBool black_compensation[2] = {0, 0};
    703     cmsFloat64Number adaption[2] = {0.0, 0.0};
    704     // Only transforming three pixels, so skip expensive optimizations.
    705     cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC;
    706     Transform xform(cmsCreateExtendedTransform(
    707         context, 2, profiles, black_compensation, intents, adaption, nullptr, 0,
    708         Type64(*c), TYPE_XYZ_DBL, flags));
    709     if (!xform) return JXL_FAILURE("Failed to retrieve colorants");
    710 
    711     const cmsFloat64Number in[9] = {1.0, 0.0, 0.0, 0.0, 1.0,
    712                                     0.0, 0.0, 0.0, 1.0};
    713     cmsDoTransform(xform.get(), in, &converted_rgb->X, 3);
    714     adapted_r = &converted_rgb[0];
    715     adapted_g = &converted_rgb[1];
    716     adapted_b = &converted_rgb[2];
    717   }
    718 
    719   // TODO(janwas): no longer assume Bradford and D50.
    720   // Undo the chromatic adaptation.
    721   const cmsCIEXYZ d50 = D50_XYZ();
    722 
    723   cmsCIEXYZ r, g, b;
    724   cmsAdaptToIlluminant(&r, &d50, &wp_unadapted, adapted_r);
    725   cmsAdaptToIlluminant(&g, &d50, &wp_unadapted, adapted_g);
    726   cmsAdaptToIlluminant(&b, &d50, &wp_unadapted, adapted_b);
    727 
    728   const PrimariesCIExy rgb = {CIExyFromXYZ(r), CIExyFromXYZ(g),
    729                               CIExyFromXYZ(b)};
    730   return c->SetPrimaries(rgb);
    731 }
    732 
    733 void DetectTransferFunction(const cmsContext context, const Profile& profile,
    734                             ColorEncoding* JXL_RESTRICT c) {
    735   JXL_CHECK(c->color_space != ColorSpace::kXYB);
    736 
    737   float gamma = 0;
    738   if (const auto* gray_trc = reinterpret_cast<const cmsToneCurve*>(
    739           cmsReadTag(profile.get(), cmsSigGrayTRCTag))) {
    740     const double estimated_gamma =
    741         cmsEstimateGamma(gray_trc, /*precision=*/1e-4);
    742     if (estimated_gamma > 0) {
    743       gamma = 1. / estimated_gamma;
    744     }
    745   } else {
    746     float rgb_gamma[3] = {};
    747     int i = 0;
    748     for (const auto tag :
    749          {cmsSigRedTRCTag, cmsSigGreenTRCTag, cmsSigBlueTRCTag}) {
    750       if (const auto* trc = reinterpret_cast<const cmsToneCurve*>(
    751               cmsReadTag(profile.get(), tag))) {
    752         const double estimated_gamma =
    753             cmsEstimateGamma(trc, /*precision=*/1e-4);
    754         if (estimated_gamma > 0) {
    755           rgb_gamma[i] = 1. / estimated_gamma;
    756         }
    757       }
    758       ++i;
    759     }
    760     if (rgb_gamma[0] != 0 && std::abs(rgb_gamma[0] - rgb_gamma[1]) < 1e-4f &&
    761         std::abs(rgb_gamma[1] - rgb_gamma[2]) < 1e-4f) {
    762       gamma = rgb_gamma[0];
    763     }
    764   }
    765 
    766   if (gamma != 0 && c->tf.SetGamma(gamma)) {
    767     IccBytes icc_test;
    768     if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
    769         ProfileEquivalentToICC(context, profile, icc_test, *c)) {
    770       return;
    771     }
    772   }
    773 
    774   for (TransferFunction tf : Values<TransferFunction>()) {
    775     // Can only create profile from known transfer function.
    776     if (tf == TransferFunction::kUnknown) continue;
    777 
    778     c->tf.SetTransferFunction(tf);
    779 
    780     IccBytes icc_test;
    781     if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
    782         ProfileEquivalentToICC(context, profile, icc_test, *c)) {
    783       return;
    784     }
    785   }
    786 
    787   c->tf.SetTransferFunction(TransferFunction::kUnknown);
    788 }
    789 
    790 void ErrorHandler(cmsContext context, cmsUInt32Number code, const char* text) {
    791   JXL_WARNING("LCMS error %u: %s", code, text);
    792 }
    793 
    794 // Returns a context for the current thread, creating it if necessary.
    795 cmsContext GetContext() {
    796   static thread_local void* context_;
    797   if (context_ == nullptr) {
    798     context_ = cmsCreateContext(nullptr, nullptr);
    799     JXL_ASSERT(context_ != nullptr);
    800 
    801     cmsSetLogErrorHandlerTHR(static_cast<cmsContext>(context_), &ErrorHandler);
    802   }
    803   return static_cast<cmsContext>(context_);
    804 }
    805 
    806 #endif  // JPEGXL_ENABLE_SKCMS
    807 
    808 Status GetPrimariesLuminances(const ColorEncoding& encoding,
    809                               float luminances[3]) {
    810   // Explanation:
    811   // We know that the three primaries must sum to white:
    812   //
    813   // [Xr, Xg, Xb;     [1;     [Xw;
    814   //  Yr, Yg, Yb;  ×   1;  =   Yw;
    815   //  Zr, Zg, Zb]      1]      Zw]
    816   //
    817   // By noting that X = x·(X+Y+Z), Y = y·(X+Y+Z) and Z = z·(X+Y+Z) (note the
    818   // lower case indicating chromaticity), and factoring the totals (X+Y+Z) out
    819   // of the left matrix and into the all-ones vector, we get:
    820   //
    821   // [xr, xg, xb;     [Xr + Yr + Zr;     [Xw;
    822   //  yr, yg, yb;  ×   Xg + Yg + Zg;  =   Yw;
    823   //  zr, zg, zb]      Xb + Yb + Zb]      Zw]
    824   //
    825   // Which makes it apparent that we can compute those totals as:
    826   //
    827   //                  [Xr + Yr + Zr;     inv([xr, xg, xb;      [Xw;
    828   //                   Xg + Yg + Zg;  =       yr, yg, yb;   ×   Yw;
    829   //                   Xb + Yb + Zb]          zr, zg, zb])      Zw]
    830   //
    831   // From there, by multiplying each total by its corresponding y, we get Y for
    832   // that primary.
    833 
    834   float white_XYZ[3];
    835   CIExy wp = encoding.GetWhitePoint();
    836   JXL_RETURN_IF_ERROR(CIEXYZFromWhiteCIExy(wp.x, wp.y, white_XYZ));
    837 
    838   const PrimariesCIExy primaries = encoding.GetPrimaries();
    839   double chromaticities[3][3] = {
    840       {primaries.r.x, primaries.g.x, primaries.b.x},
    841       {primaries.r.y, primaries.g.y, primaries.b.y},
    842       {1 - primaries.r.x - primaries.r.y, 1 - primaries.g.x - primaries.g.y,
    843        1 - primaries.b.x - primaries.b.y}};
    844   JXL_RETURN_IF_ERROR(Inv3x3Matrix(&chromaticities[0][0]));
    845   const double ys[3] = {primaries.r.y, primaries.g.y, primaries.b.y};
    846   for (size_t i = 0; i < 3; ++i) {
    847     luminances[i] = ys[i] * (chromaticities[i][0] * white_XYZ[0] +
    848                              chromaticities[i][1] * white_XYZ[1] +
    849                              chromaticities[i][2] * white_XYZ[2]);
    850   }
    851   return true;
    852 }
    853 
    854 Status ApplyHlgOotf(JxlCms* t, float* JXL_RESTRICT buf, size_t xsize,
    855                     bool forward) {
    856   if (295 <= t->intensity_target && t->intensity_target <= 305) {
    857     // The gamma is approximately 1 so this can essentially be skipped.
    858     return true;
    859   }
    860   float gamma = 1.2f * std::pow(1.111f, std::log2(t->intensity_target * 1e-3f));
    861   if (!forward) gamma = 1.f / gamma;
    862 
    863   switch (t->hlg_ootf_num_channels) {
    864     case 1:
    865       for (size_t x = 0; x < xsize; ++x) {
    866         buf[x] = std::pow(buf[x], gamma);
    867       }
    868       break;
    869 
    870     case 3:
    871       for (size_t x = 0; x < xsize; x += 3) {
    872         const float luminance = buf[x] * t->hlg_ootf_luminances[0] +
    873                                 buf[x + 1] * t->hlg_ootf_luminances[1] +
    874                                 buf[x + 2] * t->hlg_ootf_luminances[2];
    875         const float ratio = std::pow(luminance, gamma - 1);
    876         if (std::isfinite(ratio)) {
    877           buf[x] *= ratio;
    878           buf[x + 1] *= ratio;
    879           buf[x + 2] *= ratio;
    880           if (forward && gamma < 1) {
    881             // If gamma < 1, the ratio above will be > 1 which can push bright
    882             // saturated highlights out of gamut. There are several possible
    883             // ways to bring them back in-gamut; this one preserves hue and
    884             // saturation at the slight expense of luminance. If !forward, the
    885             // previously-applied forward OOTF with gamma > 1 already pushed
    886             // those highlights down and we are simply putting them back where
    887             // they were so this is not necessary.
    888             const float maximum =
    889                 std::max(buf[x], std::max(buf[x + 1], buf[x + 2]));
    890             if (maximum > 1) {
    891               const float normalizer = 1.f / maximum;
    892               buf[x] *= normalizer;
    893               buf[x + 1] *= normalizer;
    894               buf[x + 2] *= normalizer;
    895             }
    896           }
    897         }
    898       }
    899       break;
    900 
    901     default:
    902       return JXL_FAILURE("HLG OOTF not implemented for %" PRIuS " channels",
    903                          t->hlg_ootf_num_channels);
    904   }
    905   return true;
    906 }
    907 
    908 bool IsKnownTransferFunction(jxl::cms::TransferFunction tf) {
    909   using TF = jxl::cms::TransferFunction;
    910   // All but kUnknown
    911   return tf == TF::k709 || tf == TF::kLinear || tf == TF::kSRGB ||
    912          tf == TF::kPQ || tf == TF::kDCI || tf == TF::kHLG;
    913 }
    914 
    915 constexpr uint8_t kColorPrimariesP3_D65 = 12;
    916 
    917 bool IsKnownColorPrimaries(uint8_t color_primaries) {
    918   using P = jxl::cms::Primaries;
    919   // All but kCustom
    920   if (color_primaries == kColorPrimariesP3_D65) return true;
    921   const auto p = static_cast<Primaries>(color_primaries);
    922   return p == P::kSRGB || p == P::k2100 || p == P::kP3;
    923 }
    924 
    925 bool ApplyCICP(const uint8_t color_primaries,
    926                const uint8_t transfer_characteristics,
    927                const uint8_t matrix_coefficients, const uint8_t full_range,
    928                ColorEncoding* JXL_RESTRICT c) {
    929   if (matrix_coefficients != 0) return false;
    930   if (full_range != 1) return false;
    931 
    932   const auto primaries = static_cast<Primaries>(color_primaries);
    933   const auto tf = static_cast<TransferFunction>(transfer_characteristics);
    934   if (!IsKnownTransferFunction(tf)) return false;
    935   if (!IsKnownColorPrimaries(color_primaries)) return false;
    936   c->color_space = ColorSpace::kRGB;
    937   c->tf.SetTransferFunction(tf);
    938   if (primaries == Primaries::kP3) {
    939     c->white_point = WhitePoint::kDCI;
    940     c->primaries = Primaries::kP3;
    941   } else if (color_primaries == kColorPrimariesP3_D65) {
    942     c->white_point = WhitePoint::kD65;
    943     c->primaries = Primaries::kP3;
    944   } else {
    945     c->white_point = WhitePoint::kD65;
    946     c->primaries = primaries;
    947   }
    948   return true;
    949 }
    950 
    951 JXL_BOOL JxlCmsSetFieldsFromICC(void* user_data, const uint8_t* icc_data,
    952                                 size_t icc_size, JxlColorEncoding* c,
    953                                 JXL_BOOL* cmyk) {
    954   if (c == nullptr) return JXL_FALSE;
    955   if (cmyk == nullptr) return JXL_FALSE;
    956 
    957   *cmyk = JXL_FALSE;
    958 
    959   // In case parsing fails, mark the ColorEncoding as invalid.
    960   c->color_space = JXL_COLOR_SPACE_UNKNOWN;
    961   c->transfer_function = JXL_TRANSFER_FUNCTION_UNKNOWN;
    962 
    963   if (icc_size == 0) return JXL_FAILURE("Empty ICC profile");
    964 
    965   ColorEncoding c_enc;
    966 
    967 #if JPEGXL_ENABLE_SKCMS
    968   if (icc_size < 128) {
    969     return JXL_FAILURE("ICC file too small");
    970   }
    971 
    972   skcms_ICCProfile profile;
    973   JXL_RETURN_IF_ERROR(skcms_Parse(icc_data, icc_size, &profile));
    974 
    975   // skcms does not return the rendering intent, so get it from the file. It
    976   // should be encoded as big-endian 32-bit integer in bytes 60..63.
    977   uint32_t big_endian_rendering_intent = icc_data[67] + (icc_data[66] << 8) +
    978                                          (icc_data[65] << 16) +
    979                                          (icc_data[64] << 24);
    980   // Some files encode rendering intent as little endian, which is not spec
    981   // compliant. However we accept those with a warning.
    982   uint32_t little_endian_rendering_intent = (icc_data[67] << 24) +
    983                                             (icc_data[66] << 16) +
    984                                             (icc_data[65] << 8) + icc_data[64];
    985   uint32_t candidate_rendering_intent =
    986       std::min(big_endian_rendering_intent, little_endian_rendering_intent);
    987   if (candidate_rendering_intent != big_endian_rendering_intent) {
    988     JXL_WARNING(
    989         "Invalid rendering intent bytes: [0x%02X 0x%02X 0x%02X 0x%02X], "
    990         "assuming %u was meant",
    991         icc_data[64], icc_data[65], icc_data[66], icc_data[67],
    992         candidate_rendering_intent);
    993   }
    994   if (candidate_rendering_intent > 3) {
    995     return JXL_FAILURE("Invalid rendering intent %u\n",
    996                        candidate_rendering_intent);
    997   }
    998   // ICC and RenderingIntent have the same values (0..3).
    999   c_enc.rendering_intent =
   1000       static_cast<RenderingIntent>(candidate_rendering_intent);
   1001 
   1002   if (profile.has_CICP &&
   1003       ApplyCICP(profile.CICP.color_primaries,
   1004                 profile.CICP.transfer_characteristics,
   1005                 profile.CICP.matrix_coefficients,
   1006                 profile.CICP.video_full_range_flag, &c_enc)) {
   1007     *c = c_enc.ToExternal();
   1008     return JXL_TRUE;
   1009   }
   1010 
   1011   c_enc.color_space = ColorSpaceFromProfile(profile);
   1012   *cmyk = TO_JXL_BOOL(profile.data_color_space == skcms_Signature_CMYK);
   1013 
   1014   CIExy wp_unadapted;
   1015   JXL_RETURN_IF_ERROR(UnadaptedWhitePoint(profile, &wp_unadapted));
   1016   JXL_RETURN_IF_ERROR(c_enc.SetWhitePoint(wp_unadapted));
   1017 
   1018   // Relies on color_space.
   1019   JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, &c_enc));
   1020 
   1021   // Relies on color_space/white point/primaries being set already.
   1022   DetectTransferFunction(profile, &c_enc);
   1023 #else  // JPEGXL_ENABLE_SKCMS
   1024 
   1025   const cmsContext context = GetContext();
   1026 
   1027   Profile profile;
   1028   JXL_RETURN_IF_ERROR(
   1029       DecodeProfile(context, Bytes(icc_data, icc_size), &profile));
   1030 
   1031   const cmsUInt32Number rendering_intent32 =
   1032       cmsGetHeaderRenderingIntent(profile.get());
   1033   if (rendering_intent32 > 3) {
   1034     return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent32);
   1035   }
   1036   // ICC and RenderingIntent have the same values (0..3).
   1037   c_enc.rendering_intent = static_cast<RenderingIntent>(rendering_intent32);
   1038 
   1039   static constexpr size_t kCICPSize = 12;
   1040   static constexpr auto kCICPSignature =
   1041       static_cast<cmsTagSignature>(0x63696370);
   1042   uint8_t cicp_buffer[kCICPSize];
   1043   if (cmsReadRawTag(profile.get(), kCICPSignature, cicp_buffer, kCICPSize) ==
   1044           kCICPSize &&
   1045       ApplyCICP(cicp_buffer[8], cicp_buffer[9], cicp_buffer[10],
   1046                 cicp_buffer[11], &c_enc)) {
   1047     *c = c_enc.ToExternal();
   1048     return JXL_TRUE;
   1049   }
   1050 
   1051   c_enc.color_space = ColorSpaceFromProfile(profile);
   1052   if (cmsGetColorSpace(profile.get()) == cmsSigCmykData) {
   1053     *cmyk = JXL_TRUE;
   1054     *c = c_enc.ToExternal();
   1055     return JXL_TRUE;
   1056   }
   1057 
   1058   const cmsCIEXYZ wp_unadapted = UnadaptedWhitePoint(context, profile, c_enc);
   1059   JXL_RETURN_IF_ERROR(c_enc.SetWhitePoint(CIExyFromXYZ(wp_unadapted)));
   1060 
   1061   // Relies on color_space.
   1062   JXL_RETURN_IF_ERROR(
   1063       IdentifyPrimaries(context, profile, wp_unadapted, &c_enc));
   1064 
   1065   // Relies on color_space/white point/primaries being set already.
   1066   DetectTransferFunction(context, profile, &c_enc);
   1067 
   1068 #endif  // JPEGXL_ENABLE_SKCMS
   1069 
   1070   *c = c_enc.ToExternal();
   1071   return JXL_TRUE;
   1072 }
   1073 
   1074 }  // namespace
   1075 
   1076 namespace {
   1077 
   1078 void JxlCmsDestroy(void* cms_data) {
   1079   if (cms_data == nullptr) return;
   1080   JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
   1081 #if !JPEGXL_ENABLE_SKCMS
   1082   TransformDeleter()(t->lcms_transform);
   1083 #endif
   1084   delete t;
   1085 }
   1086 
   1087 void AllocateBuffer(size_t length, size_t num_threads,
   1088                     std::vector<float>* storage, std::vector<float*>* view) {
   1089   constexpr size_t kAlign = 128 / sizeof(float);
   1090   size_t stride = RoundUpTo(length, kAlign);
   1091   storage->resize(stride * num_threads + kAlign);
   1092   intptr_t addr = reinterpret_cast<intptr_t>(storage->data());
   1093   size_t offset =
   1094       (RoundUpTo(addr, kAlign * sizeof(float)) - addr) / sizeof(float);
   1095   view->clear();
   1096   view->reserve(num_threads);
   1097   for (size_t i = 0; i < num_threads; ++i) {
   1098     view->emplace_back(storage->data() + offset + i * stride);
   1099   }
   1100 }
   1101 
   1102 void* JxlCmsInit(void* init_data, size_t num_threads, size_t xsize,
   1103                  const JxlColorProfile* input, const JxlColorProfile* output,
   1104                  float intensity_target) {
   1105   JXL_ASSERT(init_data != nullptr);
   1106   const auto* cms = static_cast<const JxlCmsInterface*>(init_data);
   1107   auto t = jxl::make_unique<JxlCms>();
   1108   IccBytes icc_src;
   1109   IccBytes icc_dst;
   1110   if (input->icc.size == 0) {
   1111     JXL_NOTIFY_ERROR("JxlCmsInit: empty input ICC");
   1112     return nullptr;
   1113   }
   1114   if (output->icc.size == 0) {
   1115     JXL_NOTIFY_ERROR("JxlCmsInit: empty OUTPUT ICC");
   1116     return nullptr;
   1117   }
   1118   icc_src.assign(input->icc.data, input->icc.data + input->icc.size);
   1119   ColorEncoding c_src;
   1120   if (!c_src.SetFieldsFromICC(std::move(icc_src), *cms)) {
   1121     JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse input ICC");
   1122     return nullptr;
   1123   }
   1124   icc_dst.assign(output->icc.data, output->icc.data + output->icc.size);
   1125   ColorEncoding c_dst;
   1126   if (!c_dst.SetFieldsFromICC(std::move(icc_dst), *cms)) {
   1127     JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse output ICC");
   1128     return nullptr;
   1129   }
   1130 #if JXL_CMS_VERBOSE
   1131   printf("%s -> %s\n", Description(c_src).c_str(), Description(c_dst).c_str());
   1132 #endif
   1133 
   1134 #if JPEGXL_ENABLE_SKCMS
   1135   if (!DecodeProfile(input->icc.data, input->icc.size, &t->profile_src)) {
   1136     JXL_NOTIFY_ERROR("JxlCmsInit: skcms failed to parse input ICC");
   1137     return nullptr;
   1138   }
   1139   if (!DecodeProfile(output->icc.data, output->icc.size, &t->profile_dst)) {
   1140     JXL_NOTIFY_ERROR("JxlCmsInit: skcms failed to parse output ICC");
   1141     return nullptr;
   1142   }
   1143 #else   // JPEGXL_ENABLE_SKCMS
   1144   const cmsContext context = GetContext();
   1145   Profile profile_src, profile_dst;
   1146   if (!DecodeProfile(context, Bytes(c_src.icc), &profile_src)) {
   1147     JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse input ICC");
   1148     return nullptr;
   1149   }
   1150   if (!DecodeProfile(context, Bytes(c_dst.icc), &profile_dst)) {
   1151     JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse output ICC");
   1152     return nullptr;
   1153   }
   1154 #endif  // JPEGXL_ENABLE_SKCMS
   1155 
   1156   t->skip_lcms = false;
   1157   if (c_src.SameColorEncoding(c_dst)) {
   1158     t->skip_lcms = true;
   1159 #if JXL_CMS_VERBOSE
   1160     printf("Skip CMS\n");
   1161 #endif
   1162   }
   1163 
   1164   t->apply_hlg_ootf = c_src.tf.IsHLG() != c_dst.tf.IsHLG();
   1165   if (t->apply_hlg_ootf) {
   1166     const ColorEncoding* c_hlg = c_src.tf.IsHLG() ? &c_src : &c_dst;
   1167     t->hlg_ootf_num_channels = c_hlg->Channels();
   1168     if (t->hlg_ootf_num_channels == 3 &&
   1169         !GetPrimariesLuminances(*c_hlg, t->hlg_ootf_luminances.data())) {
   1170       JXL_NOTIFY_ERROR(
   1171           "JxlCmsInit: failed to compute the luminances of primaries");
   1172       return nullptr;
   1173     }
   1174   }
   1175 
   1176   // Special-case SRGB <=> linear if the primaries / white point are the same,
   1177   // or any conversion where PQ or HLG is involved:
   1178   bool src_linear = c_src.tf.IsLinear();
   1179   const bool dst_linear = c_dst.tf.IsLinear();
   1180 
   1181   if (c_src.tf.IsPQ() || c_src.tf.IsHLG() ||
   1182       (c_src.tf.IsSRGB() && dst_linear && c_src.SameColorSpace(c_dst))) {
   1183     // Construct new profile as if the data were already/still linear.
   1184     ColorEncoding c_linear_src = c_src;
   1185     c_linear_src.tf.SetTransferFunction(TransferFunction::kLinear);
   1186 #if JPEGXL_ENABLE_SKCMS
   1187     skcms_ICCProfile new_src;
   1188 #else   // JPEGXL_ENABLE_SKCMS
   1189     Profile new_src;
   1190 #endif  // JPEGXL_ENABLE_SKCMS
   1191         // Only enable ExtraTF if profile creation succeeded.
   1192     if (MaybeCreateProfile(c_linear_src.ToExternal(), &icc_src) &&
   1193 #if JPEGXL_ENABLE_SKCMS
   1194         DecodeProfile(icc_src.data(), icc_src.size(), &new_src)) {
   1195 #else   // JPEGXL_ENABLE_SKCMS
   1196         DecodeProfile(context, Bytes(icc_src), &new_src)) {
   1197 #endif  // JPEGXL_ENABLE_SKCMS
   1198 #if JXL_CMS_VERBOSE
   1199       printf("Special HLG/PQ/sRGB -> linear\n");
   1200 #endif
   1201 #if JPEGXL_ENABLE_SKCMS
   1202       t->icc_src = std::move(icc_src);
   1203       t->profile_src = new_src;
   1204 #else   // JPEGXL_ENABLE_SKCMS
   1205       profile_src.swap(new_src);
   1206 #endif  // JPEGXL_ENABLE_SKCMS
   1207       t->preprocess = c_src.tf.IsSRGB()
   1208                           ? ExtraTF::kSRGB
   1209                           : (c_src.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG);
   1210       c_src = c_linear_src;
   1211       src_linear = true;
   1212     } else {
   1213       if (t->apply_hlg_ootf) {
   1214         JXL_NOTIFY_ERROR(
   1215             "Failed to create extra linear source profile, and HLG OOTF "
   1216             "required");
   1217         return nullptr;
   1218       }
   1219       JXL_WARNING("Failed to create extra linear destination profile");
   1220     }
   1221   }
   1222 
   1223   if (c_dst.tf.IsPQ() || c_dst.tf.IsHLG() ||
   1224       (c_dst.tf.IsSRGB() && src_linear && c_src.SameColorSpace(c_dst))) {
   1225     ColorEncoding c_linear_dst = c_dst;
   1226     c_linear_dst.tf.SetTransferFunction(TransferFunction::kLinear);
   1227 #if JPEGXL_ENABLE_SKCMS
   1228     skcms_ICCProfile new_dst;
   1229 #else   // JPEGXL_ENABLE_SKCMS
   1230     Profile new_dst;
   1231 #endif  // JPEGXL_ENABLE_SKCMS
   1232     // Only enable ExtraTF if profile creation succeeded.
   1233     if (MaybeCreateProfile(c_linear_dst.ToExternal(), &icc_dst) &&
   1234 #if JPEGXL_ENABLE_SKCMS
   1235         DecodeProfile(icc_dst.data(), icc_dst.size(), &new_dst)) {
   1236 #else   // JPEGXL_ENABLE_SKCMS
   1237         DecodeProfile(context, Bytes(icc_dst), &new_dst)) {
   1238 #endif  // JPEGXL_ENABLE_SKCMS
   1239 #if JXL_CMS_VERBOSE
   1240       printf("Special linear -> HLG/PQ/sRGB\n");
   1241 #endif
   1242 #if JPEGXL_ENABLE_SKCMS
   1243       t->icc_dst = std::move(icc_dst);
   1244       t->profile_dst = new_dst;
   1245 #else   // JPEGXL_ENABLE_SKCMS
   1246       profile_dst.swap(new_dst);
   1247 #endif  // JPEGXL_ENABLE_SKCMS
   1248       t->postprocess = c_dst.tf.IsSRGB()
   1249                            ? ExtraTF::kSRGB
   1250                            : (c_dst.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG);
   1251       c_dst = c_linear_dst;
   1252     } else {
   1253       if (t->apply_hlg_ootf) {
   1254         JXL_NOTIFY_ERROR(
   1255             "Failed to create extra linear destination profile, and inverse "
   1256             "HLG OOTF required");
   1257         return nullptr;
   1258       }
   1259       JXL_WARNING("Failed to create extra linear destination profile");
   1260     }
   1261   }
   1262 
   1263   if (c_src.SameColorEncoding(c_dst)) {
   1264 #if JXL_CMS_VERBOSE
   1265     printf("Same intermediary linear profiles, skipping CMS\n");
   1266 #endif
   1267     t->skip_lcms = true;
   1268   }
   1269 
   1270 #if JPEGXL_ENABLE_SKCMS
   1271   if (!skcms_MakeUsableAsDestination(&t->profile_dst)) {
   1272     JXL_NOTIFY_ERROR(
   1273         "Failed to make %s usable as a color transform destination",
   1274         ColorEncodingDescription(c_dst.ToExternal()).c_str());
   1275     return nullptr;
   1276   }
   1277 #endif  // JPEGXL_ENABLE_SKCMS
   1278 
   1279   // Not including alpha channel (copied separately).
   1280   const size_t channels_src = (c_src.cmyk ? 4 : c_src.Channels());
   1281   const size_t channels_dst = c_dst.Channels();
   1282   JXL_CHECK(channels_src == channels_dst ||
   1283             (channels_src == 4 && channels_dst == 3));
   1284 #if JXL_CMS_VERBOSE
   1285   printf("Channels: %" PRIuS "; Threads: %" PRIuS "\n", channels_src,
   1286          num_threads);
   1287 #endif
   1288 
   1289 #if !JPEGXL_ENABLE_SKCMS
   1290   // Type includes color space (XYZ vs RGB), so can be different.
   1291   const uint32_t type_src = Type32(c_src, channels_src == 4);
   1292   const uint32_t type_dst = Type32(c_dst, false);
   1293   const uint32_t intent = static_cast<uint32_t>(c_dst.rendering_intent);
   1294   // Use cmsFLAGS_NOCACHE to disable the 1-pixel cache and make calling
   1295   // cmsDoTransform() thread-safe.
   1296   const uint32_t flags = cmsFLAGS_NOCACHE | cmsFLAGS_BLACKPOINTCOMPENSATION |
   1297                          cmsFLAGS_HIGHRESPRECALC;
   1298   t->lcms_transform =
   1299       cmsCreateTransformTHR(context, profile_src.get(), type_src,
   1300                             profile_dst.get(), type_dst, intent, flags);
   1301   if (t->lcms_transform == nullptr) {
   1302     JXL_NOTIFY_ERROR("Failed to create transform");
   1303     return nullptr;
   1304   }
   1305 #endif  // !JPEGXL_ENABLE_SKCMS
   1306 
   1307   // Ideally LCMS would convert directly from External to Image3. However,
   1308   // cmsDoTransformLineStride only accepts 32-bit BytesPerPlaneIn, whereas our
   1309   // planes can be more than 4 GiB apart. Hence, transform inputs/outputs must
   1310   // be interleaved. Calling cmsDoTransform for each pixel is expensive
   1311   // (indirect call). We therefore transform rows, which requires per-thread
   1312   // buffers. To avoid separate allocations, we use the rows of an image.
   1313   // Because LCMS apparently also cannot handle <= 16 bit inputs and 32-bit
   1314   // outputs (or vice versa), we use floating point input/output.
   1315   t->channels_src = channels_src;
   1316   t->channels_dst = channels_dst;
   1317 #if !JPEGXL_ENABLE_SKCMS
   1318   size_t actual_channels_src = channels_src;
   1319   size_t actual_channels_dst = channels_dst;
   1320 #else
   1321   // SkiaCMS doesn't support grayscale float buffers, so we create space for RGB
   1322   // float buffers anyway.
   1323   size_t actual_channels_src = (channels_src == 4 ? 4 : 3);
   1324   size_t actual_channels_dst = 3;
   1325 #endif
   1326   AllocateBuffer(xsize * actual_channels_src, num_threads, &t->src_storage,
   1327                  &t->buf_src);
   1328   AllocateBuffer(xsize * actual_channels_dst, num_threads, &t->dst_storage,
   1329                  &t->buf_dst);
   1330   t->intensity_target = intensity_target;
   1331   return t.release();
   1332 }
   1333 
   1334 float* JxlCmsGetSrcBuf(void* cms_data, size_t thread) {
   1335   JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
   1336   return t->buf_src[thread];
   1337 }
   1338 
   1339 float* JxlCmsGetDstBuf(void* cms_data, size_t thread) {
   1340   JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
   1341   return t->buf_dst[thread];
   1342 }
   1343 
   1344 }  // namespace
   1345 
   1346 extern "C" {
   1347 
   1348 JXL_CMS_EXPORT const JxlCmsInterface* JxlGetDefaultCms() {
   1349   static constexpr JxlCmsInterface kInterface = {
   1350       /*set_fields_data=*/nullptr,
   1351       /*set_fields_from_icc=*/&JxlCmsSetFieldsFromICC,
   1352       /*init_data=*/const_cast<void*>(static_cast<const void*>(&kInterface)),
   1353       /*init=*/&JxlCmsInit,
   1354       /*get_src_buf=*/&JxlCmsGetSrcBuf,
   1355       /*get_dst_buf=*/&JxlCmsGetDstBuf,
   1356       /*run=*/&DoColorSpaceTransform,
   1357       /*destroy=*/&JxlCmsDestroy};
   1358   return &kInterface;
   1359 }
   1360 
   1361 }  // extern "C"
   1362 
   1363 }  // namespace jxl
   1364 #endif  // HWY_ONCE