Coverage Report

Created: 2026-06-14 06:57

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