/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 |