Coverage Report

Created: 2026-03-31 07:44

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