Coverage Report

Created: 2026-07-25 07:03

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