Coverage Report

Created: 2026-11-12 07:35

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
1.53k
                       size_t buf_size) {
106
1.53k
  HWY_FULL(float) df;
107
1.53k
  size_t tail = buf_size % Lanes(df);
108
1.53k
  size_t vec_buf_end = buf_size - tail;
109
1.53k
  switch (t->preprocess) {
110
0
    case ExtraTF::kNone:
111
0
      JXL_ENSURE(false);  // unreachable
112
0
      break;
113
114
320
    case ExtraTF::kPQ: {
115
320
      TF_PQ tf_pq(t->intensity_target);
116
928
      for (size_t i = 0; i < vec_buf_end; i += Lanes(df)) {
117
608
        const auto val = Load(df, buf_src + i);
118
608
        const auto result = tf_pq.DisplayFromEncoded(df, val);
119
608
        Store(result, df, xform_src + i);
120
608
      }
121
320
      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
320
      break;
132
0
    }
133
134
1.21k
    case ExtraTF::kHLG:
135
456k
      for (size_t i = 0; i < buf_size; ++i) {
136
455k
        xform_src[i] = static_cast<float>(
137
455k
            TF_HLG_Base::DisplayFromEncoded(static_cast<double>(buf_src[i])));
138
455k
      }
139
1.21k
      if (t->apply_hlg_ootf) {
140
1.21k
        JXL_RETURN_IF_ERROR(
141
1.21k
            ApplyHlgOotf(t, xform_src, buf_size, /*forward=*/true));
142
1.21k
      }
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
1.21k
      break;
149
150
1.21k
    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
1.53k
  }
168
1.53k
  return true;
169
1.53k
}
Unexecuted instantiation: jxl_cms.cc:jxl::N_SSE4::BeforeTransform(jxl::(anonymous namespace)::JxlCms*, float const*, float*, unsigned long)
jxl_cms.cc:jxl::N_AVX2::BeforeTransform(jxl::(anonymous namespace)::JxlCms*, float const*, float*, unsigned long)
Line
Count
Source
105
1.53k
                       size_t buf_size) {
106
1.53k
  HWY_FULL(float) df;
107
1.53k
  size_t tail = buf_size % Lanes(df);
108
1.53k
  size_t vec_buf_end = buf_size - tail;
109
1.53k
  switch (t->preprocess) {
110
0
    case ExtraTF::kNone:
111
0
      JXL_ENSURE(false);  // unreachable
112
0
      break;
113
114
320
    case ExtraTF::kPQ: {
115
320
      TF_PQ tf_pq(t->intensity_target);
116
928
      for (size_t i = 0; i < vec_buf_end; i += Lanes(df)) {
117
608
        const auto val = Load(df, buf_src + i);
118
608
        const auto result = tf_pq.DisplayFromEncoded(df, val);
119
608
        Store(result, df, xform_src + i);
120
608
      }
121
320
      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
320
      break;
132
0
    }
133
134
1.21k
    case ExtraTF::kHLG:
135
456k
      for (size_t i = 0; i < buf_size; ++i) {
136
455k
        xform_src[i] = static_cast<float>(
137
455k
            TF_HLG_Base::DisplayFromEncoded(static_cast<double>(buf_src[i])));
138
455k
      }
139
1.21k
      if (t->apply_hlg_ootf) {
140
1.21k
        JXL_RETURN_IF_ERROR(
141
1.21k
            ApplyHlgOotf(t, xform_src, buf_size, /*forward=*/true));
142
1.21k
      }
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
1.21k
      break;
149
150
1.21k
    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
1.53k
  }
168
1.53k
  return true;
169
1.53k
}
Unexecuted instantiation: jxl_cms.cc:jxl::N_SSE2::BeforeTransform(jxl::(anonymous namespace)::JxlCms*, float const*, float*, unsigned long)
170
171
// Applies gamma compression in-place.
172
0
Status AfterTransform(JxlCms* t, float* JXL_RESTRICT buf_dst, size_t buf_size) {
173
0
  HWY_FULL(float) df;
174
0
  size_t tail = buf_size % Lanes(df);
175
0
  size_t vec_buf_end = buf_size - tail;
176
0
  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
0
    case ExtraTF::kSRGB:
213
0
      for (size_t i = 0; i < vec_buf_end; i += Lanes(df)) {
214
0
        const auto val = Load(df, buf_dst + i);
215
0
        const auto result = TF_SRGB().EncodedFromDisplay(df, val);
216
0
        Store(result, df, buf_dst + i);
217
0
      }
218
0
      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
0
      break;
228
0
  }
229
0
  return true;
230
0
}
Unexecuted instantiation: jxl_cms.cc:jxl::N_SSE4::AfterTransform(jxl::(anonymous namespace)::JxlCms*, float*, unsigned long)
Unexecuted instantiation: jxl_cms.cc:jxl::N_AVX2::AfterTransform(jxl::(anonymous namespace)::JxlCms*, float*, unsigned long)
Unexecuted instantiation: jxl_cms.cc:jxl::N_SSE2::AfterTransform(jxl::(anonymous namespace)::JxlCms*, float*, unsigned long)
231
232
Status DoColorSpaceTransform(void* cms_data, const size_t thread,
233
                             const float* buf_src, float* buf_dst,
234
23.8k
                             size_t xsize) {
235
  // No lock needed.
236
23.8k
  JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
237
238
23.8k
  const float* xform_src = buf_src;  // Read-only.
239
23.8k
  if (t->preprocess != ExtraTF::kNone) {
240
1.53k
    float* mutable_xform_src = t->buf_src[thread];  // Writable buffer.
241
1.53k
    JXL_RETURN_IF_ERROR(BeforeTransform(t, buf_src, mutable_xform_src,
242
1.53k
                                        xsize * t->channels_src));
243
1.53k
    xform_src = mutable_xform_src;
244
1.53k
  }
245
246
#if JPEGXL_ENABLE_SKCMS
247
  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
    float* mutable_xform_src = t->buf_src[thread];
251
    for (size_t i = 0; i < xsize; ++i) {
252
      const size_t x = xsize - i - 1;
253
      mutable_xform_src[x * 3] = mutable_xform_src[x * 3 + 1] =
254
          mutable_xform_src[x * 3 + 2] = xform_src[x];
255
    }
256
    xform_src = mutable_xform_src;
257
  }
258
#else
259
23.8k
  if (t->channels_src == 4 && !t->skip_lcms) {
260
    // LCMS does CMYK in a weird way: 0 = white, 100 = max ink
261
0
    float* mutable_xform_src = t->buf_src[thread];
262
0
    for (size_t x = 0; x < xsize * 4; ++x) {
263
0
      mutable_xform_src[x] = 100.f - 100.f * mutable_xform_src[x];
264
0
    }
265
0
    xform_src = mutable_xform_src;
266
0
  }
267
23.8k
#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
23.8k
  if (t->skip_lcms) {
277
376
    if (buf_dst != xform_src) {
278
376
      memcpy(buf_dst, xform_src, xsize * t->channels_src * sizeof(*buf_dst));
279
376
    }  // else: in-place, no need to copy
280
23.4k
  } else {
281
#if JPEGXL_ENABLE_SKCMS
282
    float* xform_dst = (t->channels_dst == 1) ? t->buf_dst[thread] : buf_dst;
283
    JXL_ENSURE(
284
        skcms_Transform(xform_src,
285
                        (t->channels_src == 4 ? skcms_PixelFormat_RGBA_ffff
286
                                              : skcms_PixelFormat_RGB_fff),
287
                        skcms_AlphaFormat_Opaque, &t->profile_src, xform_dst,
288
                        skcms_PixelFormat_RGB_fff, skcms_AlphaFormat_Opaque,
289
                        &t->profile_dst, xsize));
290
    if (t->channels_dst == 1) {
291
      // Contract back from 3 to 1 channel, this time forward.
292
      for (size_t x = 0; x < xsize; ++x) {
293
        buf_dst[x] = xform_dst[x * 3];
294
      }
295
    }
296
#else   // JPEGXL_ENABLE_SKCMS
297
23.4k
    cmsDoTransform(t->lcms_transform, xform_src, buf_dst,
298
23.4k
                   static_cast<cmsUInt32Number>(xsize));
299
23.4k
#endif  // JPEGXL_ENABLE_SKCMS
300
23.4k
  }
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
23.8k
  if (t->postprocess != ExtraTF::kNone) {
308
0
    JXL_RETURN_IF_ERROR(AfterTransform(t, buf_dst, xsize * t->channels_dst));
309
0
  }
310
23.8k
  return true;
311
23.8k
}
Unexecuted instantiation: jxl::N_SSE4::DoColorSpaceTransform(void*, unsigned long, float const*, float*, unsigned long)
jxl::N_AVX2::DoColorSpaceTransform(void*, unsigned long, float const*, float*, unsigned long)
Line
Count
Source
234
23.8k
                             size_t xsize) {
235
  // No lock needed.
236
23.8k
  JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
237
238
23.8k
  const float* xform_src = buf_src;  // Read-only.
239
23.8k
  if (t->preprocess != ExtraTF::kNone) {
240
1.53k
    float* mutable_xform_src = t->buf_src[thread];  // Writable buffer.
241
1.53k
    JXL_RETURN_IF_ERROR(BeforeTransform(t, buf_src, mutable_xform_src,
242
1.53k
                                        xsize * t->channels_src));
243
1.53k
    xform_src = mutable_xform_src;
244
1.53k
  }
245
246
#if JPEGXL_ENABLE_SKCMS
247
  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
    float* mutable_xform_src = t->buf_src[thread];
251
    for (size_t i = 0; i < xsize; ++i) {
252
      const size_t x = xsize - i - 1;
253
      mutable_xform_src[x * 3] = mutable_xform_src[x * 3 + 1] =
254
          mutable_xform_src[x * 3 + 2] = xform_src[x];
255
    }
256
    xform_src = mutable_xform_src;
257
  }
258
#else
259
23.8k
  if (t->channels_src == 4 && !t->skip_lcms) {
260
    // LCMS does CMYK in a weird way: 0 = white, 100 = max ink
261
0
    float* mutable_xform_src = t->buf_src[thread];
262
0
    for (size_t x = 0; x < xsize * 4; ++x) {
263
0
      mutable_xform_src[x] = 100.f - 100.f * mutable_xform_src[x];
264
0
    }
265
0
    xform_src = mutable_xform_src;
266
0
  }
267
23.8k
#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
23.8k
  if (t->skip_lcms) {
277
376
    if (buf_dst != xform_src) {
278
376
      memcpy(buf_dst, xform_src, xsize * t->channels_src * sizeof(*buf_dst));
279
376
    }  // else: in-place, no need to copy
280
23.4k
  } else {
281
#if JPEGXL_ENABLE_SKCMS
282
    float* xform_dst = (t->channels_dst == 1) ? t->buf_dst[thread] : buf_dst;
283
    JXL_ENSURE(
284
        skcms_Transform(xform_src,
285
                        (t->channels_src == 4 ? skcms_PixelFormat_RGBA_ffff
286
                                              : skcms_PixelFormat_RGB_fff),
287
                        skcms_AlphaFormat_Opaque, &t->profile_src, xform_dst,
288
                        skcms_PixelFormat_RGB_fff, skcms_AlphaFormat_Opaque,
289
                        &t->profile_dst, xsize));
290
    if (t->channels_dst == 1) {
291
      // Contract back from 3 to 1 channel, this time forward.
292
      for (size_t x = 0; x < xsize; ++x) {
293
        buf_dst[x] = xform_dst[x * 3];
294
      }
295
    }
296
#else   // JPEGXL_ENABLE_SKCMS
297
23.4k
    cmsDoTransform(t->lcms_transform, xform_src, buf_dst,
298
23.4k
                   static_cast<cmsUInt32Number>(xsize));
299
23.4k
#endif  // JPEGXL_ENABLE_SKCMS
300
23.4k
  }
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
23.8k
  if (t->postprocess != ExtraTF::kNone) {
308
0
    JXL_RETURN_IF_ERROR(AfterTransform(t, buf_dst, xsize * t->channels_dst));
309
0
  }
310
23.8k
  return true;
311
23.8k
}
Unexecuted instantiation: jxl::N_SSE2::DoColorSpaceTransform(void*, unsigned long, float const*, float*, unsigned long)
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
23.8k
                          float* buf_dst, size_t xsize) {
325
23.8k
  return HWY_DYNAMIC_DISPATCH(DoColorSpaceTransform)(t, thread, buf_src,
326
23.8k
                                                     buf_dst, xsize);
327
23.8k
}
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
JXL_MUST_USE_RESULT CIExy CIExyFromXYZ(const Color& XYZ) {
335
  const double factor = 1.0 / static_cast<double>(XYZ[0] + XYZ[1] + XYZ[2]);
336
  CIExy xy;
337
  xy.x = XYZ[0] * factor;
338
  xy.y = XYZ[1] * factor;
339
  return xy;
340
}
341
342
#else  // JPEGXL_ENABLE_SKCMS
343
// (LCMS interface requires xyY but we omit the Y for white points/primaries.)
344
345
5.02k
JXL_MUST_USE_RESULT CIExy CIExyFromxyY(const cmsCIExyY& xyY) {
346
5.02k
  CIExy xy;
347
5.02k
  xy.x = xyY.x;
348
5.02k
  xy.y = xyY.y;
349
5.02k
  return xy;
350
5.02k
}
351
352
5.02k
JXL_MUST_USE_RESULT CIExy CIExyFromXYZ(const cmsCIEXYZ& XYZ) {
353
5.02k
  cmsCIExyY xyY;
354
5.02k
  cmsXYZ2xyY(/*Dest=*/&xyY, /*Source=*/&XYZ);
355
5.02k
  return CIExyFromxyY(xyY);
356
5.02k
}
357
358
1.98k
JXL_MUST_USE_RESULT cmsCIEXYZ D50_XYZ() {
359
  // Quantized D50 as stored in ICC profiles.
360
1.98k
  return {0.96420288, 1.0, 0.82490540};
361
1.98k
}
362
363
// RAII
364
365
struct ContextDeleter {
366
1
  void operator()(void* p) { cmsDeleteContext(static_cast<cmsContext>(p)); }
367
};
368
using Context = std::unique_ptr<void, ContextDeleter>;
369
370
struct ProfileDeleter {
371
19.8k
  void operator()(void* p) { cmsCloseProfile(p); }
372
};
373
using Profile = std::unique_ptr<void, ProfileDeleter>;
374
375
struct TransformDeleter {
376
9.67k
  void operator()(void* p) { cmsDeleteTransform(p); }
377
};
378
using Transform = std::unique_ptr<void, TransformDeleter>;
379
380
struct CurveDeleter {
381
0
  void operator()(cmsToneCurve* p) { cmsFreeToneCurve(p); }
382
};
383
using Curve = std::unique_ptr<cmsToneCurve, CurveDeleter>;
384
385
Status CreateProfileXYZ(const cmsContext context,
386
6.05k
                        Profile* JXL_RESTRICT profile) {
387
6.05k
  profile->reset(cmsCreateXYZProfileTHR(context));
388
6.05k
  if (profile->get() == nullptr) return JXL_FAILURE("Failed to create XYZ");
389
6.05k
  return true;
390
6.05k
}
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
                     skcms_ICCProfile* const profile) {
398
  if (!skcms_Parse(icc, size, profile)) {
399
    return JXL_FAILURE("Failed to parse ICC profile with %" PRIuS " bytes",
400
                       size);
401
  }
402
  return true;
403
}
404
#else   // JPEGXL_ENABLE_SKCMS
405
Status DecodeProfile(const cmsContext context, Span<const uint8_t> icc,
406
13.7k
                     Profile* profile) {
407
13.7k
  profile->reset(cmsOpenProfileFromMemTHR(context, icc.data(), icc.size()));
408
13.7k
  if (profile->get() == nullptr) {
409
5
    return JXL_FAILURE("Failed to decode profile");
410
5
  }
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
13.7k
  return true;
417
13.7k
}
418
#endif  // JPEGXL_ENABLE_SKCMS
419
420
#if JPEGXL_ENABLE_SKCMS
421
422
ColorSpace ColorSpaceFromProfile(const skcms_ICCProfile& profile) {
423
  switch (profile.data_color_space) {
424
    case skcms_Signature_RGB:
425
    case skcms_Signature_CMYK:
426
      // spec says CMYK is encoded as RGB (the kBlack extra channel signals that
427
      // it is actually CMYK)
428
      return ColorSpace::kRGB;
429
    case skcms_Signature_Gray:
430
      return ColorSpace::kGray;
431
    default:
432
      return ColorSpace::kUnknown;
433
  }
434
}
435
436
// vector_out := matmul(matrix, vector_in)
437
void MatrixProduct(const skcms_Matrix3x3& matrix, const Color& vector_in,
438
                   Color& vector_out) {
439
  for (int i = 0; i < 3; ++i) {
440
    vector_out[i] = 0;
441
    for (int j = 0; j < 3; ++j) {
442
      vector_out[i] += matrix.vals[i][j] * vector_in[j];
443
    }
444
  }
445
}
446
447
bool IsAcceptableCHAD(const skcms_Matrix3x3& chad) {
448
  Matrix3x3d chad_double;
449
  for (int i = 0; i < 3; ++i) {
450
    for (int j = 0; j < 3; ++j) {
451
      chad_double[i][j] = chad.vals[i][j];
452
    }
453
  }
454
  return Inv3x3Matrix(chad_double);
455
}
456
457
bool HasICCVersion4DisplayClass(const skcms_ICCProfile& profile) {
458
  return profile.buffer != nullptr && profile.size >= 128 &&
459
         profile.buffer[8] == 4 && profile.buffer[12] == 'm' &&
460
         profile.buffer[13] == 'n' && profile.buffer[14] == 't' &&
461
         profile.buffer[15] == 'r' && profile.buffer[36] == 'a' &&
462
         profile.buffer[37] == 'c' && profile.buffer[38] == 's' &&
463
         profile.buffer[39] == 'p';
464
}
465
466
bool HasTag(const skcms_ICCProfile& profile, const char* signature) {
467
  constexpr size_t kTagCountOffset = 128;
468
  constexpr size_t kTagTableOffset = 132;
469
  constexpr size_t kTagRecordSize = 12;
470
  if (profile.buffer == nullptr || profile.size < kTagCountOffset + 4) {
471
    return false;
472
  }
473
  const uint32_t tag_count =
474
      (static_cast<uint32_t>(profile.buffer[kTagCountOffset]) << 24) |
475
      (static_cast<uint32_t>(profile.buffer[kTagCountOffset + 1]) << 16) |
476
      (static_cast<uint32_t>(profile.buffer[kTagCountOffset + 2]) << 8) |
477
      static_cast<uint32_t>(profile.buffer[kTagCountOffset + 3]);
478
  if (tag_count > (profile.size - kTagTableOffset) / kTagRecordSize) {
479
    return false;
480
  }
481
  for (uint32_t i = 0; i < tag_count; ++i) {
482
    const size_t offset = kTagTableOffset + kTagRecordSize * i;
483
    if (std::memcmp(profile.buffer + offset, signature, 4) == 0) {
484
      return true;
485
    }
486
  }
487
  return false;
488
}
489
490
bool HasLUTTags(const skcms_ICCProfile& profile) {
491
  for (const char* signature :
492
       {"A2B0", "A2B1", "A2B2", "B2A0", "B2A1", "B2A2", "D2B0", "D2B1", "D2B2",
493
        "D2B3", "B2D0", "B2D1", "B2D2", "B2D3"}) {
494
    if (HasTag(profile, signature)) return true;
495
  }
496
  return false;
497
}
498
499
bool HasParametricTRCs(const skcms_ICCProfile& profile) {
500
  return profile.trc[0].table_entries == 0 &&
501
         profile.trc[1].table_entries == 0 && profile.trc[2].table_entries == 0;
502
}
503
504
bool IsApproximatelyD50(const Color& XYZ) {
505
  static constexpr Color kD50XYZ{0.96420288f, 1.0f, 0.82490540f};
506
  constexpr float kTolerance = 1e-3f;
507
  return std::abs(XYZ[0] - kD50XYZ[0]) < kTolerance &&
508
         std::abs(XYZ[1] - kD50XYZ[1]) < kTolerance &&
509
         std::abs(XYZ[2] - kD50XYZ[2]) < kTolerance;
510
}
511
512
bool GetNonconformingMatrixProfileWhitePoint(const skcms_ICCProfile& profile,
513
                                             const Color& media_white_point_XYZ,
514
                                             Color& adapted_white_point_XYZ) {
515
  // Some v4 display profiles contain a CHAD but retain the pre-v4 media
516
  // white point. For a matrix/TRC profile, the colorant column sum is the
517
  // adapted neutral and is a more reliable source for undoing that CHAD.
518
  if (!HasICCVersion4DisplayClass(profile) ||
519
      profile.data_color_space != skcms_Signature_RGB ||
520
      profile.pcs != skcms_Signature_XYZ || !profile.has_trc ||
521
      !profile.has_toXYZD50 || !HasTag(profile, "chad") ||
522
      HasLUTTags(profile) || IsApproximatelyD50(media_white_point_XYZ)) {
523
    return false;
524
  }
525
  Color matrix_white_point_XYZ;
526
  for (int i = 0; i < 3; ++i) {
527
    matrix_white_point_XYZ[i] = profile.toXYZD50.vals[i][0] +
528
                                profile.toXYZD50.vals[i][1] +
529
                                profile.toXYZD50.vals[i][2];
530
  }
531
  if (!IsApproximatelyD50(matrix_white_point_XYZ)) return false;
532
  adapted_white_point_XYZ = matrix_white_point_XYZ;
533
  return true;
534
}
535
536
bool RecoverMatrixProfileWhitePoint(const skcms_ICCProfile& profile,
537
                                    const Color& media_white_point_XYZ,
538
                                    Color& adapted_white_point_XYZ) {
539
  skcms_Matrix3x3 CHAD;
540
  return HasParametricTRCs(profile) &&
541
         GetNonconformingMatrixProfileWhitePoint(profile, media_white_point_XYZ,
542
                                                 adapted_white_point_XYZ) &&
543
         skcms_GetCHAD(&profile, &CHAD) && IsAcceptableCHAD(CHAD);
544
}
545
546
// Returns white point that was specified when creating the profile.
547
JXL_MUST_USE_RESULT Status UnadaptedWhitePoint(const skcms_ICCProfile& profile,
548
                                               CIExy* out) {
549
  Color media_white_point_XYZ;
550
  if (!skcms_GetWTPT(&profile, media_white_point_XYZ.data())) {
551
    return JXL_FAILURE("ICC profile does not contain WhitePoint tag");
552
  }
553
  skcms_Matrix3x3 CHAD;
554
  if (!skcms_GetCHAD(&profile, &CHAD)) {
555
    // If there is no chromatic adaptation matrix, it means that the white point
556
    // is already unadapted.
557
    *out = CIExyFromXYZ(media_white_point_XYZ);
558
    return true;
559
  }
560
  Color adapted_white_point_XYZ;
561
  if (RecoverMatrixProfileWhitePoint(profile, media_white_point_XYZ,
562
                                     adapted_white_point_XYZ)) {
563
    media_white_point_XYZ = adapted_white_point_XYZ;
564
  }
565
  // The media white point is expressed in the PCS after the chromatic
566
  // adaptation in the profile. Undo that adaptation to recover the source
567
  // white point. The compatibility path above replaces the inconsistent media
568
  // white point with the adapted neutral before applying the same operation.
569
  skcms_Matrix3x3 inverse_CHAD;
570
  if (!skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD)) {
571
    return JXL_FAILURE("Non-invertible ChromaticAdaptation matrix");
572
  }
573
  Color unadapted_white_point_XYZ;
574
  MatrixProduct(inverse_CHAD, media_white_point_XYZ, unadapted_white_point_XYZ);
575
  *out = CIExyFromXYZ(unadapted_white_point_XYZ);
576
  return true;
577
}
578
579
Status IdentifyPrimaries(const skcms_ICCProfile& profile,
580
                         const CIExy& wp_unadapted, ColorEncoding* c) {
581
  if (!c->HasPrimaries()) return true;
582
583
  skcms_Matrix3x3 CHAD;
584
  skcms_Matrix3x3 inverse_CHAD;
585
  if (skcms_GetCHAD(&profile, &CHAD)) {
586
    JXL_RETURN_IF_ERROR(skcms_Matrix3x3_invert(&CHAD, &inverse_CHAD));
587
  } else {
588
    static constexpr skcms_Matrix3x3 kLMSFromXYZ = {
589
        {{0.8951, 0.2664, -0.1614},
590
         {-0.7502, 1.7135, 0.0367},
591
         {0.0389, -0.0685, 1.0296}}};
592
    static constexpr skcms_Matrix3x3 kXYZFromLMS = {
593
        {{0.9869929, -0.1470543, 0.1599627},
594
         {0.4323053, 0.5183603, 0.0492912},
595
         {-0.0085287, 0.0400428, 0.9684867}}};
596
    static constexpr Color kWpD50XYZ{0.96420288, 1.0, 0.82490540};
597
    Color wp_unadapted_XYZ;
598
    JXL_RETURN_IF_ERROR(
599
        CIEXYZFromWhiteCIExy(wp_unadapted.x, wp_unadapted.y, wp_unadapted_XYZ));
600
    Color wp_D50_LMS;
601
    Color wp_unadapted_LMS;
602
    MatrixProduct(kLMSFromXYZ, kWpD50XYZ, wp_D50_LMS);
603
    MatrixProduct(kLMSFromXYZ, wp_unadapted_XYZ, wp_unadapted_LMS);
604
    inverse_CHAD = {{{wp_unadapted_LMS[0] / wp_D50_LMS[0], 0, 0},
605
                     {0, wp_unadapted_LMS[1] / wp_D50_LMS[1], 0},
606
                     {0, 0, wp_unadapted_LMS[2] / wp_D50_LMS[2]}}};
607
    inverse_CHAD = skcms_Matrix3x3_concat(&kXYZFromLMS, &inverse_CHAD);
608
    inverse_CHAD = skcms_Matrix3x3_concat(&inverse_CHAD, &kLMSFromXYZ);
609
  }
610
611
  Color XYZ;
612
  PrimariesCIExy primaries;
613
  CIExy* const chromaticities[] = {&primaries.r, &primaries.g, &primaries.b};
614
  for (int i = 0; i < 3; ++i) {
615
    float RGB[3] = {};
616
    RGB[i] = 1;
617
    skcms_Transform(RGB, skcms_PixelFormat_RGB_fff, skcms_AlphaFormat_Opaque,
618
                    &profile, XYZ.data(), skcms_PixelFormat_RGB_fff,
619
                    skcms_AlphaFormat_Opaque, skcms_XYZD50_profile(), 1);
620
    Color unadapted_XYZ;
621
    MatrixProduct(inverse_CHAD, XYZ, unadapted_XYZ);
622
    *chromaticities[i] = CIExyFromXYZ(unadapted_XYZ);
623
  }
624
  return c->SetPrimaries(primaries);
625
}
626
627
bool IsApproximatelyEqual(const skcms_ICCProfile& profile,
628
                          const ColorEncoding& JXL_RESTRICT c) {
629
  IccBytes bytes;
630
  if (!MaybeCreateProfile(c.ToExternal(), &bytes)) {
631
    return false;
632
  }
633
634
  skcms_ICCProfile profile_test;
635
  if (!DecodeProfile(bytes.data(), bytes.size(), &profile_test)) {
636
    return false;
637
  }
638
639
  if (!skcms_ApproximatelyEqualProfiles(&profile_test, &profile)) {
640
    return false;
641
  }
642
643
  return true;
644
}
645
646
Status DetectTransferFunction(const skcms_ICCProfile& profile,
647
                              ColorEncoding* JXL_RESTRICT c) {
648
  JXL_ENSURE(c->color_space != ColorSpace::kXYB);
649
650
  Color media_white_point_XYZ;
651
  Color adapted_white_point_XYZ;
652
  if (!HasParametricTRCs(profile) &&
653
      skcms_GetWTPT(&profile, media_white_point_XYZ.data()) &&
654
      GetNonconformingMatrixProfileWhitePoint(profile, media_white_point_XYZ,
655
                                              adapted_white_point_XYZ)) {
656
    c->tf.SetTransferFunction(TransferFunction::kUnknown);
657
    return true;
658
  }
659
660
  float gamma[3] = {};
661
  if (profile.has_trc) {
662
    const auto IsGamma = [](const skcms_TransferFunction& tf) {
663
      return tf.a == 1 && tf.b == 0 &&
664
             /* if b and d are zero, it is fine for c not to be */ tf.d == 0 &&
665
             tf.e == 0 && tf.f == 0;
666
    };
667
    for (int i = 0; i < 3; ++i) {
668
      if (profile.trc[i].table_entries == 0 &&
669
          IsGamma(profile.trc->parametric)) {
670
        gamma[i] = 1.f / profile.trc->parametric.g;
671
      } else {
672
        skcms_TransferFunction approximate_tf;
673
        float max_error;
674
        if (skcms_ApproximateCurve(&profile.trc[i], &approximate_tf,
675
                                   &max_error)) {
676
          if (IsGamma(approximate_tf)) {
677
            gamma[i] = 1.f / approximate_tf.g;
678
          }
679
        }
680
      }
681
    }
682
  }
683
  if (gamma[0] != 0 && std::abs(gamma[0] - gamma[1]) < 1e-4f &&
684
      std::abs(gamma[1] - gamma[2]) < 1e-4f) {
685
    if (c->tf.SetGamma(gamma[0])) {
686
      if (IsApproximatelyEqual(profile, *c)) return true;
687
    }
688
  }
689
690
  for (TransferFunction tf : Values<TransferFunction>()) {
691
    // Can only create profile from known transfer function.
692
    if (tf == TransferFunction::kUnknown) continue;
693
    c->tf.SetTransferFunction(tf);
694
    if (IsApproximatelyEqual(profile, *c)) return true;
695
  }
696
697
  c->tf.SetTransferFunction(TransferFunction::kUnknown);
698
  return true;
699
}
700
701
#else  // JPEGXL_ENABLE_SKCMS
702
703
1.01k
uint32_t Type32(const ColorEncoding& c, bool cmyk) {
704
1.01k
  if (cmyk) return TYPE_CMYK_FLT;
705
1.01k
  if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_FLT;
706
696
  return TYPE_RGB_FLT;
707
1.01k
}
708
709
6.05k
uint32_t Type64(const ColorEncoding& c) {
710
6.05k
  if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_DBL;
711
4.87k
  return TYPE_RGB_DBL;
712
6.05k
}
713
714
2.86k
ColorSpace ColorSpaceFromProfile(const Profile& profile) {
715
2.86k
  switch (cmsGetColorSpace(profile.get())) {
716
2.20k
    case cmsSigRgbData:
717
2.20k
    case cmsSigCmykData:
718
2.20k
      return ColorSpace::kRGB;
719
662
    case cmsSigGrayData:
720
662
      return ColorSpace::kGray;
721
0
    default:
722
0
      return ColorSpace::kUnknown;
723
2.86k
  }
724
2.86k
}
725
726
// "profile1" is pre-decoded to save time in DetectTransferFunction.
727
constexpr double kIccEquivalenceTolerance = 2E-4;
728
constexpr double kLegacyMatrixProfileEquivalenceTolerance = 3E-4;
729
730
Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1,
731
                              const IccBytes& icc, const ColorEncoding& c,
732
5.64k
                              const double tolerance) {
733
5.64k
  const uint32_t type_src = Type64(c);
734
735
5.64k
  Profile profile2;
736
5.64k
  JXL_RETURN_IF_ERROR(DecodeProfile(context, Bytes(icc), &profile2));
737
738
5.64k
  Profile profile_xyz;
739
5.64k
  JXL_RETURN_IF_ERROR(CreateProfileXYZ(context, &profile_xyz));
740
741
5.64k
  const uint32_t intent = INTENT_RELATIVE_COLORIMETRIC;
742
5.64k
  const uint32_t flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_BLACKPOINTCOMPENSATION |
743
5.64k
                         cmsFLAGS_HIGHRESPRECALC;
744
5.64k
  Transform xform1(cmsCreateTransformTHR(context, profile1.get(), type_src,
745
5.64k
                                         profile_xyz.get(), TYPE_XYZ_DBL,
746
5.64k
                                         intent, flags));
747
5.64k
  Transform xform2(cmsCreateTransformTHR(context, profile2.get(), type_src,
748
5.64k
                                         profile_xyz.get(), TYPE_XYZ_DBL,
749
5.64k
                                         intent, flags));
750
5.64k
  if (xform1 == nullptr || xform2 == nullptr) {
751
2.28k
    return JXL_FAILURE("Failed to create transform");
752
2.28k
  }
753
754
3.35k
  double in[3];
755
3.35k
  double out1[3];
756
3.35k
  double out2[3];
757
758
  // Uniformly spaced samples from very dark to almost fully bright.
759
3.35k
  const double init = 1E-3;
760
3.35k
  const double step = 0.2;
761
762
3.35k
  if (c.color_space == ColorSpace::kGray) {
763
    // Finer sampling and replicate each component.
764
25.8k
    for (in[0] = init; in[0] < 1.0; in[0] += step / 8) {
765
25.2k
      cmsDoTransform(xform1.get(), in, out1, 1);
766
25.2k
      cmsDoTransform(xform2.get(), in, out2, 1);
767
25.2k
      if (!cms::ApproxEq(out1[0], out2[0], tolerance)) {
768
574
        return false;
769
574
      }
770
25.2k
    }
771
2.18k
  } else {
772
2.65k
    for (in[0] = init; in[0] < 1.0; in[0] += step) {
773
5.08k
      for (in[1] = init; in[1] < 1.0; in[1] += step) {
774
18.4k
        for (in[2] = init; in[2] < 1.0; in[2] += step) {
775
15.9k
          cmsDoTransform(xform1.get(), in, out1, 1);
776
15.9k
          cmsDoTransform(xform2.get(), in, out2, 1);
777
58.0k
          for (size_t i = 0; i < 3; ++i) {
778
44.1k
            if (!cms::ApproxEq(out1[i], out2[i], tolerance)) {
779
2.09k
              return false;
780
2.09k
            }
781
44.1k
          }
782
15.9k
        }
783
4.60k
      }
784
2.56k
    }
785
2.18k
  }
786
787
688
  return true;
788
3.35k
}
789
790
894
bool HasLUTTags(const Profile& profile) {
791
894
  for (const auto tag :
792
894
       {cmsSigAToB0Tag, cmsSigAToB1Tag, cmsSigAToB2Tag, cmsSigBToA0Tag,
793
894
        cmsSigBToA1Tag, cmsSigBToA2Tag, cmsSigDToB0Tag, cmsSigDToB1Tag,
794
894
        cmsSigDToB2Tag, cmsSigDToB3Tag, cmsSigBToD0Tag, cmsSigBToD1Tag,
795
12.5k
        cmsSigBToD2Tag, cmsSigBToD3Tag}) {
796
12.5k
    if (cmsIsTag(profile.get(), tag)) return true;
797
12.5k
  }
798
894
  return false;
799
894
}
800
801
1.42k
bool HasParametricTRCs(const Profile& profile) {
802
1.42k
  for (const auto tag :
803
2.49k
       {cmsSigRedTRCTag, cmsSigGreenTRCTag, cmsSigBlueTRCTag}) {
804
2.49k
    const auto* trc =
805
2.49k
        static_cast<const cmsToneCurve*>(cmsReadTag(profile.get(), tag));
806
2.49k
    if (trc == nullptr || cmsGetToneCurveParametricType(trc) <= 0) return false;
807
2.49k
  }
808
387
  return true;
809
1.42k
}
810
811
894
bool IsApproximatelyD50(const cmsCIEXYZ& XYZ) {
812
894
  const cmsCIEXYZ d50 = D50_XYZ();
813
894
  constexpr cmsFloat64Number kTolerance = 1e-3;
814
894
  return std::abs(XYZ.X - d50.X) < kTolerance &&
815
894
         std::abs(XYZ.Y - d50.Y) < kTolerance &&
816
894
         std::abs(XYZ.Z - d50.Z) < kTolerance;
817
894
}
818
819
0
bool GetInverseCHAD(const Profile& profile, Matrix3x3d* inverse_chad) {
820
0
  const cmsMAT3* chad = static_cast<const cmsMAT3*>(
821
0
      cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag));
822
0
  if (chad == nullptr) return false;
823
0
  for (int i = 0; i < 3; ++i) {
824
0
    for (int j = 0; j < 3; ++j) {
825
0
      (*inverse_chad)[i][j] = chad->v[i].n[j];
826
0
    }
827
0
  }
828
0
  return Inv3x3Matrix(*inverse_chad);
829
0
}
830
831
bool GetNonconformingMatrixProfileWhitePoint(const Profile& profile,
832
                                             const cmsCIEXYZ& media_white_point,
833
972
                                             cmsCIEXYZ* matrix_white_point) {
834
972
  const double version = cmsGetProfileVersion(profile.get());
835
972
  if (version < 4.0 || version >= 5.0 ||
836
972
      cmsGetDeviceClass(profile.get()) != cmsSigDisplayClass ||
837
894
      cmsGetColorSpace(profile.get()) != cmsSigRgbData ||
838
894
      cmsGetPCS(profile.get()) != cmsSigXYZData ||
839
894
      cmsReadTag(profile.get(), cmsSigRedColorantTag) == nullptr ||
840
894
      cmsReadTag(profile.get(), cmsSigGreenColorantTag) == nullptr ||
841
894
      cmsReadTag(profile.get(), cmsSigBlueColorantTag) == nullptr ||
842
894
      cmsReadTag(profile.get(), cmsSigRedTRCTag) == nullptr ||
843
894
      cmsReadTag(profile.get(), cmsSigGreenTRCTag) == nullptr ||
844
894
      cmsReadTag(profile.get(), cmsSigBlueTRCTag) == nullptr ||
845
972
      HasLUTTags(profile) || IsApproximatelyD50(media_white_point)) {
846
972
    return false;
847
972
  }
848
849
0
  const cmsCIEXYZ* adapted_r = static_cast<const cmsCIEXYZ*>(
850
0
      cmsReadTag(profile.get(), cmsSigRedColorantTag));
851
0
  const cmsCIEXYZ* adapted_g = static_cast<const cmsCIEXYZ*>(
852
0
      cmsReadTag(profile.get(), cmsSigGreenColorantTag));
853
0
  const cmsCIEXYZ* adapted_b = static_cast<const cmsCIEXYZ*>(
854
0
      cmsReadTag(profile.get(), cmsSigBlueColorantTag));
855
0
  *matrix_white_point = {adapted_r->X + adapted_g->X + adapted_b->X,
856
0
                         adapted_r->Y + adapted_g->Y + adapted_b->Y,
857
0
                         adapted_r->Z + adapted_g->Z + adapted_b->Z};
858
0
  return IsApproximatelyD50(*matrix_white_point);
859
972
}
860
861
bool RecoverMatrixProfileWhitePoint(const Profile& profile,
862
                                    const cmsCIEXYZ& media_white_point,
863
339
                                    cmsCIEXYZ* unadapted_white_point) {
864
339
  cmsCIEXYZ matrix_white_point;
865
339
  if (!HasParametricTRCs(profile) ||
866
150
      !GetNonconformingMatrixProfileWhitePoint(profile, media_white_point,
867
339
                                               &matrix_white_point)) {
868
339
    return false;
869
339
  }
870
871
0
  Matrix3x3d inverse_chad = {};
872
0
  if (!GetInverseCHAD(profile, &inverse_chad)) return false;
873
0
  const Vector3d adapted = {matrix_white_point.X, matrix_white_point.Y,
874
0
                            matrix_white_point.Z};
875
0
  Vector3d unadapted = {};
876
0
  Mul3x3Vector(inverse_chad, adapted, unadapted);
877
0
  unadapted_white_point->X = unadapted[0];
878
0
  unadapted_white_point->Y = unadapted[1];
879
0
  unadapted_white_point->Z = unadapted[2];
880
0
  return true;
881
0
}
882
883
3.66k
bool HasNonconformingMatrixProfileShape(const Profile& profile) {
884
3.66k
  const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>(
885
3.66k
      cmsReadTag(profile.get(), cmsSigMediaWhitePointTag));
886
3.66k
  const cmsMAT3* chad = static_cast<const cmsMAT3*>(
887
3.66k
      cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag));
888
3.66k
  cmsCIEXYZ matrix_white_point;
889
3.66k
  return white_point != nullptr && chad != nullptr &&
890
822
         GetNonconformingMatrixProfileWhitePoint(profile, *white_point,
891
822
                                                 &matrix_white_point);
892
3.66k
}
893
894
1.95k
bool IsMatrixProfileWithNonconformingWhitePoint(const Profile& profile) {
895
1.95k
  Matrix3x3d inverse_chad = {};
896
1.95k
  return HasNonconformingMatrixProfileShape(profile) &&
897
0
         GetInverseCHAD(profile, &inverse_chad);
898
1.95k
}
899
900
// Returns white point that was specified when creating the profile.
901
// NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation
902
// differs between ICC versions.
903
JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context,
904
                                                  const Profile& profile,
905
1.76k
                                                  const ColorEncoding& c) {
906
1.76k
  const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>(
907
1.76k
      cmsReadTag(profile.get(), cmsSigMediaWhitePointTag));
908
1.76k
  const cmsMAT3* chad = static_cast<const cmsMAT3*>(
909
1.76k
      cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag));
910
1.76k
  if (white_point != nullptr && chad == nullptr) {
911
    // No chromatic adaptation matrix: the white point is already unadapted.
912
1.41k
    return *white_point;
913
1.41k
  }
914
356
  if (white_point != nullptr && chad != nullptr) {
915
339
    cmsCIEXYZ unadapted_white_point;
916
339
    if (RecoverMatrixProfileWhitePoint(profile, *white_point,
917
339
                                       &unadapted_white_point)) {
918
0
      return unadapted_white_point;
919
0
    }
920
339
  }
921
922
356
  cmsCIEXYZ XYZ = {1.0, 1.0, 1.0};
923
356
  Profile profile_xyz;
924
356
  if (!CreateProfileXYZ(context, &profile_xyz)) return XYZ;
925
  // Array arguments are one per profile.
926
356
  cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()};
927
  // Leave white point unchanged - that is what we're trying to extract.
928
356
  cmsUInt32Number intents[2] = {INTENT_ABSOLUTE_COLORIMETRIC,
929
356
                                INTENT_ABSOLUTE_COLORIMETRIC};
930
356
  cmsBool black_compensation[2] = {0, 0};
931
356
  cmsFloat64Number adaption[2] = {0.0, 0.0};
932
  // Only transforming a single pixel, so skip expensive optimizations.
933
356
  cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC;
934
356
  Transform xform(cmsCreateExtendedTransform(
935
356
      context, 2, profiles, black_compensation, intents, adaption, nullptr, 0,
936
356
      Type64(c), TYPE_XYZ_DBL, flags));
937
356
  if (!xform) return XYZ;  // TODO(lode): return error
938
939
  // xy are relative, so magnitude does not matter if we ignore output Y.
940
336
  const cmsFloat64Number in[3] = {1.0, 1.0, 1.0};
941
336
  cmsDoTransform(xform.get(), in, &XYZ.X, 1);
942
336
  return XYZ;
943
356
}
944
945
Status IdentifyPrimaries(const cmsContext context, const Profile& profile,
946
1.74k
                         const cmsCIEXYZ& wp_unadapted, ColorEncoding* c) {
947
1.74k
  if (!c->HasPrimaries()) return true;
948
1.09k
  if (ColorSpaceFromProfile(profile) == ColorSpace::kUnknown) return true;
949
950
  // These were adapted to the profile illuminant before storing in the profile.
951
1.09k
  const cmsCIEXYZ* adapted_r = static_cast<const cmsCIEXYZ*>(
952
1.09k
      cmsReadTag(profile.get(), cmsSigRedColorantTag));
953
1.09k
  const cmsCIEXYZ* adapted_g = static_cast<const cmsCIEXYZ*>(
954
1.09k
      cmsReadTag(profile.get(), cmsSigGreenColorantTag));
955
1.09k
  const cmsCIEXYZ* adapted_b = static_cast<const cmsCIEXYZ*>(
956
1.09k
      cmsReadTag(profile.get(), cmsSigBlueColorantTag));
957
958
1.09k
  cmsCIEXYZ converted_rgb[3];
959
1.09k
  if (adapted_r == nullptr || adapted_g == nullptr || adapted_b == nullptr) {
960
    // No colorant tag, determine the XYZ coordinates of the primaries by
961
    // converting from the colorspace.
962
49
    Profile profile_xyz;
963
49
    if (!CreateProfileXYZ(context, &profile_xyz)) {
964
0
      return JXL_FAILURE("Failed to retrieve colorants");
965
0
    }
966
    // Array arguments are one per profile.
967
49
    cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()};
968
49
    cmsUInt32Number intents[2] = {INTENT_RELATIVE_COLORIMETRIC,
969
49
                                  INTENT_RELATIVE_COLORIMETRIC};
970
49
    cmsBool black_compensation[2] = {0, 0};
971
49
    cmsFloat64Number adaption[2] = {0.0, 0.0};
972
    // Only transforming three pixels, so skip expensive optimizations.
973
49
    cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC;
974
49
    Transform xform(cmsCreateExtendedTransform(
975
49
        context, 2, profiles, black_compensation, intents, adaption, nullptr, 0,
976
49
        Type64(*c), TYPE_XYZ_DBL, flags));
977
49
    if (!xform) return JXL_FAILURE("Failed to retrieve colorants");
978
979
39
    const cmsFloat64Number in[9] = {1.0, 0.0, 0.0, 0.0, 1.0,
980
39
                                    0.0, 0.0, 0.0, 1.0};
981
39
    cmsDoTransform(xform.get(), in, &converted_rgb->X, 3);
982
39
    adapted_r = &converted_rgb[0];
983
39
    adapted_g = &converted_rgb[1];
984
39
    adapted_b = &converted_rgb[2];
985
39
  }
986
987
1.08k
  cmsCIEXYZ r, g, b;
988
1.08k
  Matrix3x3d inverse_chad = {};
989
1.08k
  if (HasParametricTRCs(profile) &&
990
237
      IsMatrixProfileWithNonconformingWhitePoint(profile) &&
991
0
      GetInverseCHAD(profile, &inverse_chad)) {
992
0
    const cmsCIEXYZ* adapted[] = {adapted_r, adapted_g, adapted_b};
993
0
    cmsCIEXYZ* unadapted[] = {&r, &g, &b};
994
0
    for (size_t i = 0; i < 3; ++i) {
995
0
      const Vector3d in = {adapted[i]->X, adapted[i]->Y, adapted[i]->Z};
996
0
      Vector3d out = {};
997
0
      Mul3x3Vector(inverse_chad, in, out);
998
0
      *unadapted[i] = {out[0], out[1], out[2]};
999
0
    }
1000
1.08k
  } else {
1001
    // TODO(janwas): no longer assume Bradford and D50 for other profiles.
1002
1.08k
    const cmsCIEXYZ d50 = D50_XYZ();
1003
1.08k
    cmsAdaptToIlluminant(&r, &d50, &wp_unadapted, adapted_r);
1004
1.08k
    cmsAdaptToIlluminant(&g, &d50, &wp_unadapted, adapted_g);
1005
1.08k
    cmsAdaptToIlluminant(&b, &d50, &wp_unadapted, adapted_b);
1006
1.08k
  }
1007
1008
1.08k
  const PrimariesCIExy rgb = {CIExyFromXYZ(r), CIExyFromXYZ(g),
1009
1.08k
                              CIExyFromXYZ(b)};
1010
1.08k
  return c->SetPrimaries(rgb);
1011
1.09k
}
1012
1013
Status DetectTransferFunction(const cmsContext context, const Profile& profile,
1014
1.71k
                              ColorEncoding* JXL_RESTRICT c) {
1015
1.71k
  JXL_ENSURE(c->color_space != ColorSpace::kXYB);
1016
1017
1.71k
  const bool nonconforming_matrix_profile =
1018
1.71k
      HasNonconformingMatrixProfileShape(profile);
1019
1.71k
  const bool compatibility_profile =
1020
1.71k
      IsMatrixProfileWithNonconformingWhitePoint(profile);
1021
1.71k
  if (nonconforming_matrix_profile &&
1022
0
      (!compatibility_profile || !HasParametricTRCs(profile))) {
1023
0
    c->tf.SetTransferFunction(TransferFunction::kUnknown);
1024
0
    return true;
1025
0
  }
1026
  // Matrix/TRC coefficients and the canonical D50 white point are quantized
1027
  // independently in ICC. Allow that small difference for this compatibility
1028
  // case while retaining the functional equivalence check.
1029
1.71k
  const double equivalence_tolerance =
1030
1.71k
      compatibility_profile ? kLegacyMatrixProfileEquivalenceTolerance
1031
1.71k
                            : kIccEquivalenceTolerance;
1032
1033
1.71k
  float gamma = 0;
1034
1.71k
  if (const auto* gray_trc = reinterpret_cast<const cmsToneCurve*>(
1035
1.71k
          cmsReadTag(profile.get(), cmsSigGrayTRCTag))) {
1036
648
    const double estimated_gamma =
1037
648
        cmsEstimateGamma(gray_trc, /*precision=*/1e-4);
1038
648
    if (estimated_gamma > 0) {
1039
361
      gamma = 1. / estimated_gamma;
1040
361
    }
1041
1.06k
  } else {
1042
1.06k
    float rgb_gamma[3] = {};
1043
1.06k
    int i = 0;
1044
1.06k
    for (const auto tag :
1045
3.19k
         {cmsSigRedTRCTag, cmsSigGreenTRCTag, cmsSigBlueTRCTag}) {
1046
3.19k
      if (const auto* trc = reinterpret_cast<const cmsToneCurve*>(
1047
3.19k
              cmsReadTag(profile.get(), tag))) {
1048
1.44k
        const double estimated_gamma =
1049
1.44k
            cmsEstimateGamma(trc, /*precision=*/1e-4);
1050
1.44k
        if (estimated_gamma > 0) {
1051
393
          rgb_gamma[i] = 1. / estimated_gamma;
1052
393
        }
1053
1.44k
      }
1054
3.19k
      ++i;
1055
3.19k
    }
1056
1.06k
    if (rgb_gamma[0] != 0 && std::abs(rgb_gamma[0] - rgb_gamma[1]) < 1e-4f &&
1057
142
        std::abs(rgb_gamma[1] - rgb_gamma[2]) < 1e-4f) {
1058
105
      gamma = rgb_gamma[0];
1059
105
    }
1060
1.06k
  }
1061
1062
1.71k
  if (gamma != 0 && c->tf.SetGamma(gamma)) {
1063
456
    IccBytes icc_test;
1064
456
    if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
1065
449
        ProfileEquivalentToICC(context, profile, icc_test, *c,
1066
449
                               equivalence_tolerance)) {
1067
395
      return true;
1068
395
    }
1069
456
  }
1070
1071
8.11k
  for (TransferFunction tf : Values<TransferFunction>()) {
1072
    // Can only create profile from known transfer function.
1073
8.11k
    if (tf == TransferFunction::kUnknown) continue;
1074
1075
6.94k
    c->tf.SetTransferFunction(tf);
1076
1077
6.94k
    IccBytes icc_test;
1078
6.94k
    if (MaybeCreateProfile(c->ToExternal(), &icc_test) &&
1079
5.19k
        ProfileEquivalentToICC(context, profile, icc_test, *c,
1080
5.19k
                               equivalence_tolerance)) {
1081
293
      return true;
1082
293
    }
1083
6.94k
  }
1084
1085
1.02k
  c->tf.SetTransferFunction(TransferFunction::kUnknown);
1086
1.02k
  return true;
1087
1.31k
}
1088
1089
10.7k
void ErrorHandler(cmsContext context, cmsUInt32Number code, const char* text) {
1090
10.7k
  JXL_WARNING("LCMS error %u: %s", code, text);
1091
10.7k
}
1092
1093
// Returns a context for the current thread, creating it if necessary.
1094
7.60k
cmsContext GetContext() {
1095
7.60k
  static thread_local Context context_;
1096
7.60k
  if (context_ == nullptr) {
1097
1
    context_.reset(cmsCreateContext(nullptr, nullptr));
1098
1
    JXL_DASSERT(context_ != nullptr);
1099
1100
1
    cmsSetLogErrorHandlerTHR(static_cast<cmsContext>(context_.get()),
1101
1
                             &ErrorHandler);
1102
1
  }
1103
7.60k
  return static_cast<cmsContext>(context_.get());
1104
7.60k
}
1105
1106
#endif  // JPEGXL_ENABLE_SKCMS
1107
1108
Status GetPrimariesLuminances(const ColorEncoding& encoding,
1109
14
                              float luminances[3]) {
1110
  // Explanation:
1111
  // We know that the three primaries must sum to white:
1112
  //
1113
  // [Xr, Xg, Xb;     [1;     [Xw;
1114
  //  Yr, Yg, Yb;  ×   1;  =   Yw;
1115
  //  Zr, Zg, Zb]      1]      Zw]
1116
  //
1117
  // By noting that X = x·(X+Y+Z), Y = y·(X+Y+Z) and Z = z·(X+Y+Z) (note the
1118
  // lower case indicating chromaticity), and factoring the totals (X+Y+Z) out
1119
  // of the left matrix and into the all-ones vector, we get:
1120
  //
1121
  // [xr, xg, xb;     [Xr + Yr + Zr;     [Xw;
1122
  //  yr, yg, yb;  ×   Xg + Yg + Zg;  =   Yw;
1123
  //  zr, zg, zb]      Xb + Yb + Zb]      Zw]
1124
  //
1125
  // Which makes it apparent that we can compute those totals as:
1126
  //
1127
  //                  [Xr + Yr + Zr;     inv([xr, xg, xb;      [Xw;
1128
  //                   Xg + Yg + Zg;  =       yr, yg, yb;   ×   Yw;
1129
  //                   Xb + Yb + Zb]          zr, zg, zb])      Zw]
1130
  //
1131
  // From there, by multiplying each total by its corresponding y, we get Y for
1132
  // that primary.
1133
1134
14
  Color white_XYZ;
1135
14
  CIExy wp = encoding.GetWhitePoint();
1136
14
  JXL_RETURN_IF_ERROR(CIEXYZFromWhiteCIExy(wp.x, wp.y, white_XYZ));
1137
1138
14
  PrimariesCIExy primaries;
1139
14
  JXL_RETURN_IF_ERROR(encoding.GetPrimaries(primaries));
1140
14
  Matrix3x3d chromaticities{
1141
14
      {{primaries.r.x, primaries.g.x, primaries.b.x},
1142
14
       {primaries.r.y, primaries.g.y, primaries.b.y},
1143
14
       {1 - primaries.r.x - primaries.r.y, 1 - primaries.g.x - primaries.g.y,
1144
14
        1 - primaries.b.x - primaries.b.y}}};
1145
14
  JXL_RETURN_IF_ERROR(Inv3x3Matrix(chromaticities));
1146
14
  const double ys[3] = {primaries.r.y, primaries.g.y, primaries.b.y};
1147
56
  for (size_t i = 0; i < 3; ++i) {
1148
42
    luminances[i] = ys[i] * (chromaticities[i][0] * white_XYZ[0] +
1149
42
                             chromaticities[i][1] * white_XYZ[1] +
1150
42
                             chromaticities[i][2] * white_XYZ[2]);
1151
42
  }
1152
14
  return true;
1153
14
}
1154
1155
Status ApplyHlgOotf(JxlCms* t, float* JXL_RESTRICT buf, size_t xsize,
1156
1.21k
                    bool forward) {
1157
1.21k
  if (295 <= t->intensity_target && t->intensity_target <= 305) {
1158
    // The gamma is approximately 1 so this can essentially be skipped.
1159
0
    return true;
1160
0
  }
1161
1.21k
  float gamma = 1.2f * std::pow(1.111f, std::log2(t->intensity_target * 1e-3f));
1162
1.21k
  if (!forward) gamma = 1.f / gamma;
1163
1164
1.21k
  switch (t->hlg_ootf_num_channels) {
1165
489
    case 1:
1166
83.0k
      for (size_t x = 0; x < xsize; ++x) {
1167
82.5k
        buf[x] = std::pow(buf[x], gamma);
1168
82.5k
      }
1169
489
      break;
1170
1171
721
    case 3:
1172
125k
      for (size_t x = 0; x < xsize; x += 3) {
1173
124k
        const float luminance = buf[x] * t->hlg_ootf_luminances[0] +
1174
124k
                                buf[x + 1] * t->hlg_ootf_luminances[1] +
1175
124k
                                buf[x + 2] * t->hlg_ootf_luminances[2];
1176
124k
        const float ratio = std::pow(luminance, gamma - 1);
1177
124k
        if (std::isfinite(ratio)) {
1178
123k
          buf[x] *= ratio;
1179
123k
          buf[x + 1] *= ratio;
1180
123k
          buf[x + 2] *= ratio;
1181
123k
          if (forward && gamma < 1) {
1182
            // If gamma < 1, the ratio above will be > 1 which can push bright
1183
            // saturated highlights out of gamut. There are several possible
1184
            // ways to bring them back in-gamut; this one preserves hue and
1185
            // saturation at the slight expense of luminance. If !forward, the
1186
            // previously-applied forward OOTF with gamma > 1 already pushed
1187
            // those highlights down and we are simply putting them back where
1188
            // they were so this is not necessary.
1189
0
            const float maximum =
1190
0
                std::max(buf[x], std::max(buf[x + 1], buf[x + 2]));
1191
0
            if (maximum > 1) {
1192
0
              const float normalizer = 1.f / maximum;
1193
0
              buf[x] *= normalizer;
1194
0
              buf[x + 1] *= normalizer;
1195
0
              buf[x + 2] *= normalizer;
1196
0
            }
1197
0
          }
1198
123k
        }
1199
124k
      }
1200
721
      break;
1201
1202
0
    default:
1203
0
      return JXL_FAILURE("HLG OOTF not implemented for %" PRIuS " channels",
1204
1.21k
                         t->hlg_ootf_num_channels);
1205
1.21k
  }
1206
1.21k
  return true;
1207
1.21k
}
1208
1209
5.32k
bool IsKnownTransferFunction(jxl::cms::TransferFunction tf) {
1210
5.32k
  using TF = jxl::cms::TransferFunction;
1211
  // All but kUnknown
1212
5.32k
  return tf == TF::k709 || tf == TF::kLinear || tf == TF::kSRGB ||
1213
0
         tf == TF::kPQ || tf == TF::kDCI || tf == TF::kHLG;
1214
5.32k
}
1215
1216
constexpr uint8_t kColorPrimariesP3_D65 = 12;
1217
1218
5.32k
bool IsKnownColorPrimaries(uint8_t color_primaries) {
1219
5.32k
  using P = jxl::cms::Primaries;
1220
  // All but kCustom
1221
5.32k
  if (color_primaries == kColorPrimariesP3_D65) return true;
1222
5.32k
  const auto p = static_cast<Primaries>(color_primaries);
1223
5.32k
  return p == P::kSRGB || p == P::k2100 || p == P::kP3;
1224
5.32k
}
1225
1226
bool ApplyCICP(const uint8_t color_primaries,
1227
               const uint8_t transfer_characteristics,
1228
               const uint8_t matrix_coefficients, const uint8_t full_range,
1229
5.32k
               ColorEncoding* JXL_RESTRICT c) {
1230
5.32k
  if (matrix_coefficients != 0) return false;
1231
5.32k
  if (full_range != 1) return false;
1232
1233
5.32k
  const auto primaries = static_cast<Primaries>(color_primaries);
1234
5.32k
  const auto tf = static_cast<TransferFunction>(transfer_characteristics);
1235
5.32k
  if (!IsKnownTransferFunction(tf)) return false;
1236
5.32k
  if (!IsKnownColorPrimaries(color_primaries)) return false;
1237
5.32k
  c->color_space = ColorSpace::kRGB;
1238
5.32k
  c->tf.SetTransferFunction(tf);
1239
5.32k
  if (primaries == Primaries::kP3) {
1240
0
    c->white_point = WhitePoint::kDCI;
1241
0
    c->primaries = Primaries::kP3;
1242
5.32k
  } else if (color_primaries == kColorPrimariesP3_D65) {
1243
0
    c->white_point = WhitePoint::kD65;
1244
0
    c->primaries = Primaries::kP3;
1245
5.32k
  } else {
1246
5.32k
    c->white_point = WhitePoint::kD65;
1247
5.32k
    c->primaries = primaries;
1248
5.32k
  }
1249
5.32k
  return true;
1250
5.32k
}
1251
1252
JXL_BOOL JxlCmsSetFieldsFromICC(void* user_data, const uint8_t* icc_data,
1253
                                size_t icc_size, JxlColorEncoding* c,
1254
7.10k
                                JXL_BOOL* cmyk) {
1255
7.10k
  if (c == nullptr) return JXL_FALSE;
1256
7.10k
  if (cmyk == nullptr) return JXL_FALSE;
1257
1258
7.10k
  *cmyk = JXL_FALSE;
1259
1260
  // In case parsing fails, mark the ColorEncoding as invalid.
1261
7.10k
  c->color_space = JXL_COLOR_SPACE_UNKNOWN;
1262
7.10k
  c->transfer_function = JXL_TRANSFER_FUNCTION_UNKNOWN;
1263
1264
7.10k
  if (icc_size == 0) return JXL_FAILURE("Empty ICC profile");
1265
1266
7.10k
  ColorEncoding c_enc;
1267
1268
#if JPEGXL_ENABLE_SKCMS
1269
  if (icc_size < 128) {
1270
    return JXL_FAILURE("ICC file too small");
1271
  }
1272
1273
  skcms_ICCProfile profile;
1274
  JXL_RETURN_IF_ERROR(skcms_Parse(icc_data, icc_size, &profile));
1275
1276
  // skcms does not return the rendering intent, so get it from the file. It
1277
  // should be encoded as big-endian 32-bit integer in bytes 60..63.
1278
  uint32_t big_endian_rendering_intent = icc_data[67] + (icc_data[66] << 8) +
1279
                                         (icc_data[65] << 16) +
1280
                                         (icc_data[64] << 24);
1281
  // Some files encode rendering intent as little endian, which is not spec
1282
  // compliant. However we accept those with a warning.
1283
  uint32_t little_endian_rendering_intent = (icc_data[67] << 24) +
1284
                                            (icc_data[66] << 16) +
1285
                                            (icc_data[65] << 8) + icc_data[64];
1286
  uint32_t candidate_rendering_intent =
1287
      std::min(big_endian_rendering_intent, little_endian_rendering_intent);
1288
  if (candidate_rendering_intent != big_endian_rendering_intent) {
1289
    JXL_WARNING(
1290
        "Invalid rendering intent bytes: [0x%02X 0x%02X 0x%02X 0x%02X], "
1291
        "assuming %u was meant",
1292
        icc_data[64], icc_data[65], icc_data[66], icc_data[67],
1293
        candidate_rendering_intent);
1294
  }
1295
  if (candidate_rendering_intent > 3) {
1296
    return JXL_FAILURE("Invalid rendering intent %u\n",
1297
                       candidate_rendering_intent);
1298
  }
1299
  // ICC and RenderingIntent have the same values (0..3).
1300
  c_enc.rendering_intent =
1301
      static_cast<RenderingIntent>(candidate_rendering_intent);
1302
1303
  if (profile.has_CICP &&
1304
      ApplyCICP(profile.CICP.color_primaries,
1305
                profile.CICP.transfer_characteristics,
1306
                profile.CICP.matrix_coefficients,
1307
                profile.CICP.video_full_range_flag, &c_enc)) {
1308
    *c = c_enc.ToExternal();
1309
    return JXL_TRUE;
1310
  }
1311
1312
  c_enc.color_space = ColorSpaceFromProfile(profile);
1313
  *cmyk = TO_JXL_BOOL(profile.data_color_space == skcms_Signature_CMYK);
1314
1315
  CIExy wp_unadapted;
1316
  JXL_RETURN_IF_ERROR(UnadaptedWhitePoint(profile, &wp_unadapted));
1317
  JXL_RETURN_IF_ERROR(c_enc.SetWhitePoint(wp_unadapted));
1318
1319
  // Relies on color_space.
1320
  JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, &c_enc));
1321
1322
  // Relies on color_space/white point/primaries being set already.
1323
  JXL_RETURN_IF_ERROR(DetectTransferFunction(profile, &c_enc));
1324
#else  // JPEGXL_ENABLE_SKCMS
1325
1326
7.10k
  const cmsContext context = GetContext();
1327
1328
7.10k
  Profile profile;
1329
7.10k
  JXL_RETURN_IF_ERROR(
1330
7.10k
      DecodeProfile(context, Bytes(icc_data, icc_size), &profile));
1331
1332
7.09k
  const cmsUInt32Number rendering_intent32 =
1333
7.09k
      cmsGetHeaderRenderingIntent(profile.get());
1334
7.09k
  if (rendering_intent32 > 3) {
1335
6
    return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent32);
1336
6
  }
1337
  // ICC and RenderingIntent have the same values (0..3).
1338
7.09k
  c_enc.rendering_intent = static_cast<RenderingIntent>(rendering_intent32);
1339
1340
7.09k
  static constexpr size_t kCICPSize = 12;
1341
7.09k
  static constexpr auto kCICPSignature =
1342
7.09k
      static_cast<cmsTagSignature>(0x63696370);
1343
7.09k
  uint8_t cicp_buffer[kCICPSize];
1344
7.09k
  if (cmsReadRawTag(profile.get(), kCICPSignature, cicp_buffer, kCICPSize) ==
1345
7.09k
          kCICPSize &&
1346
5.32k
      ApplyCICP(cicp_buffer[8], cicp_buffer[9], cicp_buffer[10],
1347
5.32k
                cicp_buffer[11], &c_enc)) {
1348
5.32k
    *c = c_enc.ToExternal();
1349
5.32k
    return JXL_TRUE;
1350
5.32k
  }
1351
1352
1.76k
  c_enc.color_space = ColorSpaceFromProfile(profile);
1353
1.76k
  if (cmsGetColorSpace(profile.get()) == cmsSigCmykData) {
1354
0
    *cmyk = JXL_TRUE;
1355
0
    *c = c_enc.ToExternal();
1356
0
    return JXL_TRUE;
1357
0
  }
1358
1359
1.76k
  const cmsCIEXYZ wp_unadapted = UnadaptedWhitePoint(context, profile, c_enc);
1360
1.76k
  JXL_RETURN_IF_ERROR(c_enc.SetWhitePoint(CIExyFromXYZ(wp_unadapted)));
1361
1362
  // Relies on color_space.
1363
1.74k
  JXL_RETURN_IF_ERROR(
1364
1.74k
      IdentifyPrimaries(context, profile, wp_unadapted, &c_enc));
1365
1366
  // Relies on color_space/white point/primaries being set already.
1367
1.71k
  JXL_RETURN_IF_ERROR(DetectTransferFunction(context, profile, &c_enc));
1368
1369
1.71k
#endif  // JPEGXL_ENABLE_SKCMS
1370
1371
1.71k
  *c = c_enc.ToExternal();
1372
1.71k
  return JXL_TRUE;
1373
1.71k
}
1374
1375
}  // namespace
1376
1377
namespace {
1378
1379
296
void JxlCmsDestroy(void* cms_data) {
1380
296
  if (cms_data == nullptr) return;
1381
296
  JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
1382
296
#if !JPEGXL_ENABLE_SKCMS
1383
296
  TransformDeleter()(t->lcms_transform);
1384
296
#endif
1385
296
  delete t;
1386
296
}
1387
1388
void AllocateBuffer(size_t length, size_t num_threads,
1389
592
                    std::vector<float>* storage, std::vector<float*>* view) {
1390
592
  constexpr size_t kAlign = 128 / sizeof(float);
1391
592
  size_t stride = RoundUpTo(length, kAlign);
1392
592
  storage->resize(stride * num_threads + kAlign);
1393
592
  intptr_t addr = reinterpret_cast<intptr_t>(storage->data());
1394
592
  size_t offset =
1395
592
      (RoundUpTo(addr, kAlign * sizeof(float)) - addr) / sizeof(float);
1396
592
  view->clear();
1397
592
  view->reserve(num_threads);
1398
1.18k
  for (size_t i = 0; i < num_threads; ++i) {
1399
592
    view->emplace_back(storage->data() + offset + i * stride);
1400
592
  }
1401
592
}
1402
1403
void* JxlCmsInit(void* init_data, size_t num_threads, size_t xsize,
1404
                 const JxlColorProfile* input, const JxlColorProfile* output,
1405
506
                 float intensity_target) {
1406
506
  if (init_data == nullptr) {
1407
0
    JXL_NOTIFY_ERROR("JxlCmsInit: init_data is nullptr");
1408
0
    return nullptr;
1409
0
  }
1410
506
  const auto* cms = static_cast<const JxlCmsInterface*>(init_data);
1411
506
  auto t = jxl::make_unique<JxlCms>();
1412
506
  IccBytes icc_src;
1413
506
  IccBytes icc_dst;
1414
506
  if (input->icc.size == 0) {
1415
0
    JXL_NOTIFY_ERROR("JxlCmsInit: empty input ICC");
1416
0
    return nullptr;
1417
0
  }
1418
506
  if (output->icc.size == 0) {
1419
0
    JXL_NOTIFY_ERROR("JxlCmsInit: empty OUTPUT ICC");
1420
0
    return nullptr;
1421
0
  }
1422
506
  icc_src.assign(input->icc.data, input->icc.data + input->icc.size);
1423
506
  ColorEncoding c_src;
1424
506
  if (!c_src.SetFieldsFromICC(std::move(icc_src), *cms)) {
1425
0
    JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse input ICC");
1426
0
    return nullptr;
1427
0
  }
1428
506
  icc_dst.assign(output->icc.data, output->icc.data + output->icc.size);
1429
506
  ColorEncoding c_dst;
1430
506
  if (!c_dst.SetFieldsFromICC(std::move(icc_dst), *cms)) {
1431
0
    JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse output ICC");
1432
0
    return nullptr;
1433
0
  }
1434
#if JXL_CMS_VERBOSE
1435
  printf("%s -> %s\n", Description(c_src).c_str(), Description(c_dst).c_str());
1436
#endif
1437
1438
#if JPEGXL_ENABLE_SKCMS
1439
  if (!DecodeProfile(input->icc.data, input->icc.size, &t->profile_src)) {
1440
    JXL_NOTIFY_ERROR("JxlCmsInit: skcms failed to parse input ICC");
1441
    return nullptr;
1442
  }
1443
  if (!DecodeProfile(output->icc.data, output->icc.size, &t->profile_dst)) {
1444
    JXL_NOTIFY_ERROR("JxlCmsInit: skcms failed to parse output ICC");
1445
    return nullptr;
1446
  }
1447
#else   // JPEGXL_ENABLE_SKCMS
1448
506
  const cmsContext context = GetContext();
1449
506
  Profile profile_src, profile_dst;
1450
506
  if (!DecodeProfile(context, Bytes(c_src.icc), &profile_src)) {
1451
0
    JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse input ICC");
1452
0
    return nullptr;
1453
0
  }
1454
506
  if (!DecodeProfile(context, Bytes(c_dst.icc), &profile_dst)) {
1455
0
    JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse output ICC");
1456
0
    return nullptr;
1457
0
  }
1458
506
#endif  // JPEGXL_ENABLE_SKCMS
1459
1460
506
  t->skip_lcms = false;
1461
506
  if (c_src.SameColorEncoding(c_dst)) {
1462
0
    t->skip_lcms = true;
1463
#if JXL_CMS_VERBOSE
1464
    printf("Skip CMS\n");
1465
#endif
1466
0
  }
1467
1468
506
  t->apply_hlg_ootf = c_src.tf.IsHLG() != c_dst.tf.IsHLG();
1469
506
  if (t->apply_hlg_ootf) {
1470
22
    const ColorEncoding* c_hlg = c_src.tf.IsHLG() ? &c_src : &c_dst;
1471
22
    t->hlg_ootf_num_channels = c_hlg->Channels();
1472
22
    if (t->hlg_ootf_num_channels == 3 &&
1473
14
        !GetPrimariesLuminances(*c_hlg, t->hlg_ootf_luminances.data())) {
1474
0
      JXL_NOTIFY_ERROR(
1475
0
          "JxlCmsInit: failed to compute the luminances of primaries");
1476
0
      return nullptr;
1477
0
    }
1478
22
  }
1479
1480
  // Special-case SRGB <=> linear if the primaries / white point are the same,
1481
  // or any conversion where PQ or HLG is involved:
1482
506
  bool src_linear = c_src.tf.IsLinear();
1483
506
  const bool dst_linear = c_dst.tf.IsLinear();
1484
1485
506
  if (c_src.tf.IsPQ() || c_src.tf.IsHLG() ||
1486
480
      (c_src.tf.IsSRGB() && dst_linear && c_src.SameColorSpace(c_dst))) {
1487
    // Construct new profile as if the data were already/still linear.
1488
26
    ColorEncoding c_linear_src = c_src;
1489
26
    c_linear_src.tf.SetTransferFunction(TransferFunction::kLinear);
1490
#if JPEGXL_ENABLE_SKCMS
1491
    skcms_ICCProfile new_src;
1492
#else   // JPEGXL_ENABLE_SKCMS
1493
26
    Profile new_src;
1494
26
#endif  // JPEGXL_ENABLE_SKCMS
1495
        // Only enable ExtraTF if profile creation succeeded.
1496
26
    if (MaybeCreateProfile(c_linear_src.ToExternal(), &icc_src) &&
1497
#if JPEGXL_ENABLE_SKCMS
1498
        DecodeProfile(icc_src.data(), icc_src.size(), &new_src)) {
1499
#else   // JPEGXL_ENABLE_SKCMS
1500
26
        DecodeProfile(context, Bytes(icc_src), &new_src)) {
1501
26
#endif  // JPEGXL_ENABLE_SKCMS
1502
#if JXL_CMS_VERBOSE
1503
      printf("Special HLG/PQ/sRGB -> linear\n");
1504
#endif
1505
#if JPEGXL_ENABLE_SKCMS
1506
      t->icc_src = std::move(icc_src);
1507
      t->profile_src = new_src;
1508
#else   // JPEGXL_ENABLE_SKCMS
1509
26
      profile_src.swap(new_src);
1510
26
#endif  // JPEGXL_ENABLE_SKCMS
1511
26
      t->preprocess = c_src.tf.IsSRGB()
1512
26
                          ? ExtraTF::kSRGB
1513
26
                          : (c_src.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG);
1514
26
      c_src = c_linear_src;
1515
26
      src_linear = true;
1516
26
    } else {
1517
0
      if (t->apply_hlg_ootf) {
1518
0
        JXL_NOTIFY_ERROR(
1519
0
            "Failed to create extra linear source profile, and HLG OOTF "
1520
0
            "required");
1521
0
        return nullptr;
1522
0
      }
1523
0
      JXL_WARNING("Failed to create extra linear destination profile");
1524
0
    }
1525
26
  }
1526
1527
506
  if (c_dst.tf.IsPQ() || c_dst.tf.IsHLG() ||
1528
506
      (c_dst.tf.IsSRGB() && src_linear && c_src.SameColorSpace(c_dst))) {
1529
0
    ColorEncoding c_linear_dst = c_dst;
1530
0
    c_linear_dst.tf.SetTransferFunction(TransferFunction::kLinear);
1531
#if JPEGXL_ENABLE_SKCMS
1532
    skcms_ICCProfile new_dst;
1533
#else   // JPEGXL_ENABLE_SKCMS
1534
0
    Profile new_dst;
1535
0
#endif  // JPEGXL_ENABLE_SKCMS
1536
    // Only enable ExtraTF if profile creation succeeded.
1537
0
    if (MaybeCreateProfile(c_linear_dst.ToExternal(), &icc_dst) &&
1538
#if JPEGXL_ENABLE_SKCMS
1539
        DecodeProfile(icc_dst.data(), icc_dst.size(), &new_dst)) {
1540
#else   // JPEGXL_ENABLE_SKCMS
1541
0
        DecodeProfile(context, Bytes(icc_dst), &new_dst)) {
1542
0
#endif  // JPEGXL_ENABLE_SKCMS
1543
#if JXL_CMS_VERBOSE
1544
      printf("Special linear -> HLG/PQ/sRGB\n");
1545
#endif
1546
#if JPEGXL_ENABLE_SKCMS
1547
      t->icc_dst = std::move(icc_dst);
1548
      t->profile_dst = new_dst;
1549
#else   // JPEGXL_ENABLE_SKCMS
1550
0
      profile_dst.swap(new_dst);
1551
0
#endif  // JPEGXL_ENABLE_SKCMS
1552
0
      t->postprocess = c_dst.tf.IsSRGB()
1553
0
                           ? ExtraTF::kSRGB
1554
0
                           : (c_dst.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG);
1555
0
      c_dst = c_linear_dst;
1556
0
    } else {
1557
0
      if (t->apply_hlg_ootf) {
1558
0
        JXL_NOTIFY_ERROR(
1559
0
            "Failed to create extra linear destination profile, and inverse "
1560
0
            "HLG OOTF required");
1561
0
        return nullptr;
1562
0
      }
1563
0
      JXL_WARNING("Failed to create extra linear destination profile");
1564
0
    }
1565
0
  }
1566
1567
506
  if (c_src.SameColorEncoding(c_dst)) {
1568
#if JXL_CMS_VERBOSE
1569
    printf("Same intermediary linear profiles, skipping CMS\n");
1570
#endif
1571
5
    t->skip_lcms = true;
1572
5
  }
1573
1574
#if JPEGXL_ENABLE_SKCMS
1575
  if (!skcms_MakeUsableAsDestination(&t->profile_dst)) {
1576
    JXL_NOTIFY_ERROR(
1577
        "Failed to make %s usable as a color transform destination",
1578
        ColorEncodingDescription(c_dst.ToExternal()).c_str());
1579
    return nullptr;
1580
  }
1581
#endif  // JPEGXL_ENABLE_SKCMS
1582
1583
  // Not including alpha channel (copied separately).
1584
506
  const size_t channels_src = (c_src.cmyk ? 4 : c_src.Channels());
1585
506
  const size_t channels_dst = c_dst.Channels();
1586
#if JXL_CMS_VERBOSE
1587
  printf("Channels: %" PRIuS "; Threads: %" PRIuS "\n", channels_src,
1588
         num_threads);
1589
#endif
1590
1591
506
#if !JPEGXL_ENABLE_SKCMS
1592
  // Type includes color space (XYZ vs RGB), so can be different.
1593
506
  const uint32_t type_src = Type32(c_src, channels_src == 4);
1594
506
  const uint32_t type_dst = Type32(c_dst, false);
1595
506
  const uint32_t intent = static_cast<uint32_t>(c_dst.rendering_intent);
1596
  // Use cmsFLAGS_NOCACHE to disable the 1-pixel cache and make calling
1597
  // cmsDoTransform() thread-safe.
1598
506
  const uint32_t flags = cmsFLAGS_NOCACHE | cmsFLAGS_BLACKPOINTCOMPENSATION |
1599
506
                         cmsFLAGS_HIGHRESPRECALC;
1600
506
  t->lcms_transform =
1601
506
      cmsCreateTransformTHR(context, profile_src.get(), type_src,
1602
506
                            profile_dst.get(), type_dst, intent, flags);
1603
506
  if (t->lcms_transform == nullptr) {
1604
210
    JXL_NOTIFY_ERROR("Failed to create transform");
1605
210
    return nullptr;
1606
210
  }
1607
296
#endif  // !JPEGXL_ENABLE_SKCMS
1608
1609
  // Ideally LCMS would convert directly from External to Image3. However,
1610
  // cmsDoTransformLineStride only accepts 32-bit BytesPerPlaneIn, whereas our
1611
  // planes can be more than 4 GiB apart. Hence, transform inputs/outputs must
1612
  // be interleaved. Calling cmsDoTransform for each pixel is expensive
1613
  // (indirect call). We therefore transform rows, which requires per-thread
1614
  // buffers. To avoid separate allocations, we use the rows of an image.
1615
  // Because LCMS apparently also cannot handle <= 16 bit inputs and 32-bit
1616
  // outputs (or vice versa), we use floating point input/output.
1617
296
  t->channels_src = channels_src;
1618
296
  t->channels_dst = channels_dst;
1619
296
#if !JPEGXL_ENABLE_SKCMS
1620
296
  size_t actual_channels_src = channels_src;
1621
296
  size_t actual_channels_dst = channels_dst;
1622
#else
1623
  // SkiaCMS doesn't support grayscale float buffers, so we create space for RGB
1624
  // float buffers anyway.
1625
  size_t actual_channels_src = (channels_src == 4 ? 4 : 3);
1626
  size_t actual_channels_dst = 3;
1627
#endif
1628
296
  AllocateBuffer(xsize * actual_channels_src, num_threads, &t->src_storage,
1629
296
                 &t->buf_src);
1630
296
  AllocateBuffer(xsize * actual_channels_dst, num_threads, &t->dst_storage,
1631
296
                 &t->buf_dst);
1632
296
  t->intensity_target = intensity_target;
1633
296
  return t.release();
1634
506
}
1635
1636
23.8k
float* JxlCmsGetSrcBuf(void* cms_data, size_t thread) {
1637
23.8k
  JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
1638
23.8k
  return t->buf_src[thread];
1639
23.8k
}
1640
1641
23.8k
float* JxlCmsGetDstBuf(void* cms_data, size_t thread) {
1642
23.8k
  JxlCms* t = reinterpret_cast<JxlCms*>(cms_data);
1643
23.8k
  return t->buf_dst[thread];
1644
23.8k
}
1645
1646
}  // namespace
1647
1648
extern "C" {
1649
1650
3.10k
JXL_CMS_EXPORT const JxlCmsInterface* JxlGetDefaultCms() {
1651
3.10k
  static constexpr JxlCmsInterface kInterface = {
1652
3.10k
      /*set_fields_data=*/nullptr,
1653
3.10k
      /*set_fields_from_icc=*/&JxlCmsSetFieldsFromICC,
1654
3.10k
      /*init_data=*/const_cast<void*>(static_cast<const void*>(&kInterface)),
1655
3.10k
      /*init=*/&JxlCmsInit,
1656
3.10k
      /*get_src_buf=*/&JxlCmsGetSrcBuf,
1657
3.10k
      /*get_dst_buf=*/&JxlCmsGetDstBuf,
1658
3.10k
      /*run=*/&DoColorSpaceTransform,
1659
3.10k
      /*destroy=*/&JxlCmsDestroy};
1660
3.10k
  return &kInterface;
1661
3.10k
}
1662
1663
}  // extern "C"
1664
1665
}  // namespace jxl
1666
#endif  // HWY_ONCE