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