jxl_cms.cc (47929B)
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/status.h" 31 #include "lib/jxl/cms/jxl_cms_internal.h" 32 #include "lib/jxl/cms/transfer_functions-inl.h" 33 #include "lib/jxl/color_encoding_internal.h" 34 #if JPEGXL_ENABLE_SKCMS 35 #include "skcms.h" 36 #else // JPEGXL_ENABLE_SKCMS 37 #include "lcms2.h" 38 #include "lcms2_plugin.h" 39 #include "lib/jxl/base/span.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_ENSURE(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_DEBUG_ABORT("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_ENSURE( 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 Color& XYZ) { 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 Color& vector_in, 409 Color& vector_out) { 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 Color media_white_point_XYZ; 422 if (!skcms_GetWTPT(&profile, media_white_point_XYZ.data())) { 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 Color unadapted_white_point_XYZ; 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 Color kWpD50XYZ{0.96420288, 1.0, 0.82490540}; 462 Color wp_unadapted_XYZ; 463 JXL_RETURN_IF_ERROR( 464 CIEXYZFromWhiteCIExy(wp_unadapted.x, wp_unadapted.y, wp_unadapted_XYZ)); 465 Color wp_D50_LMS; 466 Color wp_unadapted_LMS; 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 Color XYZ; 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.data(), skcms_PixelFormat_RGB_fff, 484 skcms_AlphaFormat_Opaque, skcms_XYZD50_profile(), 1); 485 Color unadapted_XYZ; 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 Status DetectTransferFunction(const skcms_ICCProfile& profile, 512 ColorEncoding* JXL_RESTRICT c) { 513 JXL_ENSURE(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 true; 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 true; 550 } 551 552 c->tf.SetTransferFunction(TransferFunction::kUnknown); 553 return true; 554 } 555 556 #else // JPEGXL_ENABLE_SKCMS 557 558 uint32_t Type32(const ColorEncoding& c, bool cmyk) { 559 if (cmyk) return TYPE_CMYK_FLT; 560 if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_FLT; 561 return TYPE_RGB_FLT; 562 } 563 564 uint32_t Type64(const ColorEncoding& c) { 565 if (c.color_space == ColorSpace::kGray) return TYPE_GRAY_DBL; 566 return TYPE_RGB_DBL; 567 } 568 569 ColorSpace ColorSpaceFromProfile(const Profile& profile) { 570 switch (cmsGetColorSpace(profile.get())) { 571 case cmsSigRgbData: 572 case cmsSigCmykData: 573 return ColorSpace::kRGB; 574 case cmsSigGrayData: 575 return ColorSpace::kGray; 576 default: 577 return ColorSpace::kUnknown; 578 } 579 } 580 581 // "profile1" is pre-decoded to save time in DetectTransferFunction. 582 Status ProfileEquivalentToICC(const cmsContext context, const Profile& profile1, 583 const IccBytes& icc, const ColorEncoding& c) { 584 const uint32_t type_src = Type64(c); 585 586 Profile profile2; 587 JXL_RETURN_IF_ERROR(DecodeProfile(context, Bytes(icc), &profile2)); 588 589 Profile profile_xyz; 590 JXL_RETURN_IF_ERROR(CreateProfileXYZ(context, &profile_xyz)); 591 592 const uint32_t intent = INTENT_RELATIVE_COLORIMETRIC; 593 const uint32_t flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_BLACKPOINTCOMPENSATION | 594 cmsFLAGS_HIGHRESPRECALC; 595 Transform xform1(cmsCreateTransformTHR(context, profile1.get(), type_src, 596 profile_xyz.get(), TYPE_XYZ_DBL, 597 intent, flags)); 598 Transform xform2(cmsCreateTransformTHR(context, profile2.get(), type_src, 599 profile_xyz.get(), TYPE_XYZ_DBL, 600 intent, flags)); 601 if (xform1 == nullptr || xform2 == nullptr) { 602 return JXL_FAILURE("Failed to create transform"); 603 } 604 605 double in[3]; 606 double out1[3]; 607 double out2[3]; 608 609 // Uniformly spaced samples from very dark to almost fully bright. 610 const double init = 1E-3; 611 const double step = 0.2; 612 613 if (c.color_space == ColorSpace::kGray) { 614 // Finer sampling and replicate each component. 615 for (in[0] = init; in[0] < 1.0; in[0] += step / 8) { 616 cmsDoTransform(xform1.get(), in, out1, 1); 617 cmsDoTransform(xform2.get(), in, out2, 1); 618 if (!cms::ApproxEq(out1[0], out2[0], 2E-4)) { 619 return false; 620 } 621 } 622 } else { 623 for (in[0] = init; in[0] < 1.0; in[0] += step) { 624 for (in[1] = init; in[1] < 1.0; in[1] += step) { 625 for (in[2] = init; in[2] < 1.0; in[2] += step) { 626 cmsDoTransform(xform1.get(), in, out1, 1); 627 cmsDoTransform(xform2.get(), in, out2, 1); 628 for (size_t i = 0; i < 3; ++i) { 629 if (!cms::ApproxEq(out1[i], out2[i], 2E-4)) { 630 return false; 631 } 632 } 633 } 634 } 635 } 636 } 637 638 return true; 639 } 640 641 // Returns white point that was specified when creating the profile. 642 // NOTE: we can't just use cmsSigMediaWhitePointTag because its interpretation 643 // differs between ICC versions. 644 JXL_MUST_USE_RESULT cmsCIEXYZ UnadaptedWhitePoint(const cmsContext context, 645 const Profile& profile, 646 const ColorEncoding& c) { 647 const cmsCIEXYZ* white_point = static_cast<const cmsCIEXYZ*>( 648 cmsReadTag(profile.get(), cmsSigMediaWhitePointTag)); 649 if (white_point != nullptr && 650 cmsReadTag(profile.get(), cmsSigChromaticAdaptationTag) == nullptr) { 651 // No chromatic adaptation matrix: the white point is already unadapted. 652 return *white_point; 653 } 654 655 cmsCIEXYZ XYZ = {1.0, 1.0, 1.0}; 656 Profile profile_xyz; 657 if (!CreateProfileXYZ(context, &profile_xyz)) return XYZ; 658 // Array arguments are one per profile. 659 cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()}; 660 // Leave white point unchanged - that is what we're trying to extract. 661 cmsUInt32Number intents[2] = {INTENT_ABSOLUTE_COLORIMETRIC, 662 INTENT_ABSOLUTE_COLORIMETRIC}; 663 cmsBool black_compensation[2] = {0, 0}; 664 cmsFloat64Number adaption[2] = {0.0, 0.0}; 665 // Only transforming a single pixel, so skip expensive optimizations. 666 cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC; 667 Transform xform(cmsCreateExtendedTransform( 668 context, 2, profiles, black_compensation, intents, adaption, nullptr, 0, 669 Type64(c), TYPE_XYZ_DBL, flags)); 670 if (!xform) return XYZ; // TODO(lode): return error 671 672 // xy are relative, so magnitude does not matter if we ignore output Y. 673 const cmsFloat64Number in[3] = {1.0, 1.0, 1.0}; 674 cmsDoTransform(xform.get(), in, &XYZ.X, 1); 675 return XYZ; 676 } 677 678 Status IdentifyPrimaries(const cmsContext context, const Profile& profile, 679 const cmsCIEXYZ& wp_unadapted, ColorEncoding* c) { 680 if (!c->HasPrimaries()) return true; 681 if (ColorSpaceFromProfile(profile) == ColorSpace::kUnknown) return true; 682 683 // These were adapted to the profile illuminant before storing in the profile. 684 const cmsCIEXYZ* adapted_r = static_cast<const cmsCIEXYZ*>( 685 cmsReadTag(profile.get(), cmsSigRedColorantTag)); 686 const cmsCIEXYZ* adapted_g = static_cast<const cmsCIEXYZ*>( 687 cmsReadTag(profile.get(), cmsSigGreenColorantTag)); 688 const cmsCIEXYZ* adapted_b = static_cast<const cmsCIEXYZ*>( 689 cmsReadTag(profile.get(), cmsSigBlueColorantTag)); 690 691 cmsCIEXYZ converted_rgb[3]; 692 if (adapted_r == nullptr || adapted_g == nullptr || adapted_b == nullptr) { 693 // No colorant tag, determine the XYZ coordinates of the primaries by 694 // converting from the colorspace. 695 Profile profile_xyz; 696 if (!CreateProfileXYZ(context, &profile_xyz)) { 697 return JXL_FAILURE("Failed to retrieve colorants"); 698 } 699 // Array arguments are one per profile. 700 cmsHPROFILE profiles[2] = {profile.get(), profile_xyz.get()}; 701 cmsUInt32Number intents[2] = {INTENT_RELATIVE_COLORIMETRIC, 702 INTENT_RELATIVE_COLORIMETRIC}; 703 cmsBool black_compensation[2] = {0, 0}; 704 cmsFloat64Number adaption[2] = {0.0, 0.0}; 705 // Only transforming three pixels, so skip expensive optimizations. 706 cmsUInt32Number flags = cmsFLAGS_NOOPTIMIZE | cmsFLAGS_HIGHRESPRECALC; 707 Transform xform(cmsCreateExtendedTransform( 708 context, 2, profiles, black_compensation, intents, adaption, nullptr, 0, 709 Type64(*c), TYPE_XYZ_DBL, flags)); 710 if (!xform) return JXL_FAILURE("Failed to retrieve colorants"); 711 712 const cmsFloat64Number in[9] = {1.0, 0.0, 0.0, 0.0, 1.0, 713 0.0, 0.0, 0.0, 1.0}; 714 cmsDoTransform(xform.get(), in, &converted_rgb->X, 3); 715 adapted_r = &converted_rgb[0]; 716 adapted_g = &converted_rgb[1]; 717 adapted_b = &converted_rgb[2]; 718 } 719 720 // TODO(janwas): no longer assume Bradford and D50. 721 // Undo the chromatic adaptation. 722 const cmsCIEXYZ d50 = D50_XYZ(); 723 724 cmsCIEXYZ r, g, b; 725 cmsAdaptToIlluminant(&r, &d50, &wp_unadapted, adapted_r); 726 cmsAdaptToIlluminant(&g, &d50, &wp_unadapted, adapted_g); 727 cmsAdaptToIlluminant(&b, &d50, &wp_unadapted, adapted_b); 728 729 const PrimariesCIExy rgb = {CIExyFromXYZ(r), CIExyFromXYZ(g), 730 CIExyFromXYZ(b)}; 731 return c->SetPrimaries(rgb); 732 } 733 734 Status DetectTransferFunction(const cmsContext context, const Profile& profile, 735 ColorEncoding* JXL_RESTRICT c) { 736 JXL_ENSURE(c->color_space != ColorSpace::kXYB); 737 738 float gamma = 0; 739 if (const auto* gray_trc = reinterpret_cast<const cmsToneCurve*>( 740 cmsReadTag(profile.get(), cmsSigGrayTRCTag))) { 741 const double estimated_gamma = 742 cmsEstimateGamma(gray_trc, /*precision=*/1e-4); 743 if (estimated_gamma > 0) { 744 gamma = 1. / estimated_gamma; 745 } 746 } else { 747 float rgb_gamma[3] = {}; 748 int i = 0; 749 for (const auto tag : 750 {cmsSigRedTRCTag, cmsSigGreenTRCTag, cmsSigBlueTRCTag}) { 751 if (const auto* trc = reinterpret_cast<const cmsToneCurve*>( 752 cmsReadTag(profile.get(), tag))) { 753 const double estimated_gamma = 754 cmsEstimateGamma(trc, /*precision=*/1e-4); 755 if (estimated_gamma > 0) { 756 rgb_gamma[i] = 1. / estimated_gamma; 757 } 758 } 759 ++i; 760 } 761 if (rgb_gamma[0] != 0 && std::abs(rgb_gamma[0] - rgb_gamma[1]) < 1e-4f && 762 std::abs(rgb_gamma[1] - rgb_gamma[2]) < 1e-4f) { 763 gamma = rgb_gamma[0]; 764 } 765 } 766 767 if (gamma != 0 && c->tf.SetGamma(gamma)) { 768 IccBytes icc_test; 769 if (MaybeCreateProfile(c->ToExternal(), &icc_test) && 770 ProfileEquivalentToICC(context, profile, icc_test, *c)) { 771 return true; 772 } 773 } 774 775 for (TransferFunction tf : Values<TransferFunction>()) { 776 // Can only create profile from known transfer function. 777 if (tf == TransferFunction::kUnknown) continue; 778 779 c->tf.SetTransferFunction(tf); 780 781 IccBytes icc_test; 782 if (MaybeCreateProfile(c->ToExternal(), &icc_test) && 783 ProfileEquivalentToICC(context, profile, icc_test, *c)) { 784 return true; 785 } 786 } 787 788 c->tf.SetTransferFunction(TransferFunction::kUnknown); 789 return true; 790 } 791 792 void ErrorHandler(cmsContext context, cmsUInt32Number code, const char* text) { 793 JXL_WARNING("LCMS error %u: %s", code, text); 794 } 795 796 // Returns a context for the current thread, creating it if necessary. 797 cmsContext GetContext() { 798 static thread_local void* context_; 799 if (context_ == nullptr) { 800 context_ = cmsCreateContext(nullptr, nullptr); 801 JXL_DASSERT(context_ != nullptr); 802 803 cmsSetLogErrorHandlerTHR(static_cast<cmsContext>(context_), &ErrorHandler); 804 } 805 return static_cast<cmsContext>(context_); 806 } 807 808 #endif // JPEGXL_ENABLE_SKCMS 809 810 Status GetPrimariesLuminances(const ColorEncoding& encoding, 811 float luminances[3]) { 812 // Explanation: 813 // We know that the three primaries must sum to white: 814 // 815 // [Xr, Xg, Xb; [1; [Xw; 816 // Yr, Yg, Yb; × 1; = Yw; 817 // Zr, Zg, Zb] 1] Zw] 818 // 819 // By noting that X = x·(X+Y+Z), Y = y·(X+Y+Z) and Z = z·(X+Y+Z) (note the 820 // lower case indicating chromaticity), and factoring the totals (X+Y+Z) out 821 // of the left matrix and into the all-ones vector, we get: 822 // 823 // [xr, xg, xb; [Xr + Yr + Zr; [Xw; 824 // yr, yg, yb; × Xg + Yg + Zg; = Yw; 825 // zr, zg, zb] Xb + Yb + Zb] Zw] 826 // 827 // Which makes it apparent that we can compute those totals as: 828 // 829 // [Xr + Yr + Zr; inv([xr, xg, xb; [Xw; 830 // Xg + Yg + Zg; = yr, yg, yb; × Yw; 831 // Xb + Yb + Zb] zr, zg, zb]) Zw] 832 // 833 // From there, by multiplying each total by its corresponding y, we get Y for 834 // that primary. 835 836 Color white_XYZ; 837 CIExy wp = encoding.GetWhitePoint(); 838 JXL_RETURN_IF_ERROR(CIEXYZFromWhiteCIExy(wp.x, wp.y, white_XYZ)); 839 840 PrimariesCIExy primaries; 841 JXL_RETURN_IF_ERROR(encoding.GetPrimaries(primaries)); 842 Matrix3x3d chromaticities{ 843 {{primaries.r.x, primaries.g.x, primaries.b.x}, 844 {primaries.r.y, primaries.g.y, primaries.b.y}, 845 {1 - primaries.r.x - primaries.r.y, 1 - primaries.g.x - primaries.g.y, 846 1 - primaries.b.x - primaries.b.y}}}; 847 JXL_RETURN_IF_ERROR(Inv3x3Matrix(chromaticities)); 848 const double ys[3] = {primaries.r.y, primaries.g.y, primaries.b.y}; 849 for (size_t i = 0; i < 3; ++i) { 850 luminances[i] = ys[i] * (chromaticities[i][0] * white_XYZ[0] + 851 chromaticities[i][1] * white_XYZ[1] + 852 chromaticities[i][2] * white_XYZ[2]); 853 } 854 return true; 855 } 856 857 Status ApplyHlgOotf(JxlCms* t, float* JXL_RESTRICT buf, size_t xsize, 858 bool forward) { 859 if (295 <= t->intensity_target && t->intensity_target <= 305) { 860 // The gamma is approximately 1 so this can essentially be skipped. 861 return true; 862 } 863 float gamma = 1.2f * std::pow(1.111f, std::log2(t->intensity_target * 1e-3f)); 864 if (!forward) gamma = 1.f / gamma; 865 866 switch (t->hlg_ootf_num_channels) { 867 case 1: 868 for (size_t x = 0; x < xsize; ++x) { 869 buf[x] = std::pow(buf[x], gamma); 870 } 871 break; 872 873 case 3: 874 for (size_t x = 0; x < xsize; x += 3) { 875 const float luminance = buf[x] * t->hlg_ootf_luminances[0] + 876 buf[x + 1] * t->hlg_ootf_luminances[1] + 877 buf[x + 2] * t->hlg_ootf_luminances[2]; 878 const float ratio = std::pow(luminance, gamma - 1); 879 if (std::isfinite(ratio)) { 880 buf[x] *= ratio; 881 buf[x + 1] *= ratio; 882 buf[x + 2] *= ratio; 883 if (forward && gamma < 1) { 884 // If gamma < 1, the ratio above will be > 1 which can push bright 885 // saturated highlights out of gamut. There are several possible 886 // ways to bring them back in-gamut; this one preserves hue and 887 // saturation at the slight expense of luminance. If !forward, the 888 // previously-applied forward OOTF with gamma > 1 already pushed 889 // those highlights down and we are simply putting them back where 890 // they were so this is not necessary. 891 const float maximum = 892 std::max(buf[x], std::max(buf[x + 1], buf[x + 2])); 893 if (maximum > 1) { 894 const float normalizer = 1.f / maximum; 895 buf[x] *= normalizer; 896 buf[x + 1] *= normalizer; 897 buf[x + 2] *= normalizer; 898 } 899 } 900 } 901 } 902 break; 903 904 default: 905 return JXL_FAILURE("HLG OOTF not implemented for %" PRIuS " channels", 906 t->hlg_ootf_num_channels); 907 } 908 return true; 909 } 910 911 bool IsKnownTransferFunction(jxl::cms::TransferFunction tf) { 912 using TF = jxl::cms::TransferFunction; 913 // All but kUnknown 914 return tf == TF::k709 || tf == TF::kLinear || tf == TF::kSRGB || 915 tf == TF::kPQ || tf == TF::kDCI || tf == TF::kHLG; 916 } 917 918 constexpr uint8_t kColorPrimariesP3_D65 = 12; 919 920 bool IsKnownColorPrimaries(uint8_t color_primaries) { 921 using P = jxl::cms::Primaries; 922 // All but kCustom 923 if (color_primaries == kColorPrimariesP3_D65) return true; 924 const auto p = static_cast<Primaries>(color_primaries); 925 return p == P::kSRGB || p == P::k2100 || p == P::kP3; 926 } 927 928 bool ApplyCICP(const uint8_t color_primaries, 929 const uint8_t transfer_characteristics, 930 const uint8_t matrix_coefficients, const uint8_t full_range, 931 ColorEncoding* JXL_RESTRICT c) { 932 if (matrix_coefficients != 0) return false; 933 if (full_range != 1) return false; 934 935 const auto primaries = static_cast<Primaries>(color_primaries); 936 const auto tf = static_cast<TransferFunction>(transfer_characteristics); 937 if (!IsKnownTransferFunction(tf)) return false; 938 if (!IsKnownColorPrimaries(color_primaries)) return false; 939 c->color_space = ColorSpace::kRGB; 940 c->tf.SetTransferFunction(tf); 941 if (primaries == Primaries::kP3) { 942 c->white_point = WhitePoint::kDCI; 943 c->primaries = Primaries::kP3; 944 } else if (color_primaries == kColorPrimariesP3_D65) { 945 c->white_point = WhitePoint::kD65; 946 c->primaries = Primaries::kP3; 947 } else { 948 c->white_point = WhitePoint::kD65; 949 c->primaries = primaries; 950 } 951 return true; 952 } 953 954 JXL_BOOL JxlCmsSetFieldsFromICC(void* user_data, const uint8_t* icc_data, 955 size_t icc_size, JxlColorEncoding* c, 956 JXL_BOOL* cmyk) { 957 if (c == nullptr) return JXL_FALSE; 958 if (cmyk == nullptr) return JXL_FALSE; 959 960 *cmyk = JXL_FALSE; 961 962 // In case parsing fails, mark the ColorEncoding as invalid. 963 c->color_space = JXL_COLOR_SPACE_UNKNOWN; 964 c->transfer_function = JXL_TRANSFER_FUNCTION_UNKNOWN; 965 966 if (icc_size == 0) return JXL_FAILURE("Empty ICC profile"); 967 968 ColorEncoding c_enc; 969 970 #if JPEGXL_ENABLE_SKCMS 971 if (icc_size < 128) { 972 return JXL_FAILURE("ICC file too small"); 973 } 974 975 skcms_ICCProfile profile; 976 JXL_RETURN_IF_ERROR(skcms_Parse(icc_data, icc_size, &profile)); 977 978 // skcms does not return the rendering intent, so get it from the file. It 979 // should be encoded as big-endian 32-bit integer in bytes 60..63. 980 uint32_t big_endian_rendering_intent = icc_data[67] + (icc_data[66] << 8) + 981 (icc_data[65] << 16) + 982 (icc_data[64] << 24); 983 // Some files encode rendering intent as little endian, which is not spec 984 // compliant. However we accept those with a warning. 985 uint32_t little_endian_rendering_intent = (icc_data[67] << 24) + 986 (icc_data[66] << 16) + 987 (icc_data[65] << 8) + icc_data[64]; 988 uint32_t candidate_rendering_intent = 989 std::min(big_endian_rendering_intent, little_endian_rendering_intent); 990 if (candidate_rendering_intent != big_endian_rendering_intent) { 991 JXL_WARNING( 992 "Invalid rendering intent bytes: [0x%02X 0x%02X 0x%02X 0x%02X], " 993 "assuming %u was meant", 994 icc_data[64], icc_data[65], icc_data[66], icc_data[67], 995 candidate_rendering_intent); 996 } 997 if (candidate_rendering_intent > 3) { 998 return JXL_FAILURE("Invalid rendering intent %u\n", 999 candidate_rendering_intent); 1000 } 1001 // ICC and RenderingIntent have the same values (0..3). 1002 c_enc.rendering_intent = 1003 static_cast<RenderingIntent>(candidate_rendering_intent); 1004 1005 if (profile.has_CICP && 1006 ApplyCICP(profile.CICP.color_primaries, 1007 profile.CICP.transfer_characteristics, 1008 profile.CICP.matrix_coefficients, 1009 profile.CICP.video_full_range_flag, &c_enc)) { 1010 *c = c_enc.ToExternal(); 1011 return JXL_TRUE; 1012 } 1013 1014 c_enc.color_space = ColorSpaceFromProfile(profile); 1015 *cmyk = TO_JXL_BOOL(profile.data_color_space == skcms_Signature_CMYK); 1016 1017 CIExy wp_unadapted; 1018 JXL_RETURN_IF_ERROR(UnadaptedWhitePoint(profile, &wp_unadapted)); 1019 JXL_RETURN_IF_ERROR(c_enc.SetWhitePoint(wp_unadapted)); 1020 1021 // Relies on color_space. 1022 JXL_RETURN_IF_ERROR(IdentifyPrimaries(profile, wp_unadapted, &c_enc)); 1023 1024 // Relies on color_space/white point/primaries being set already. 1025 JXL_RETURN_IF_ERROR(DetectTransferFunction(profile, &c_enc)); 1026 #else // JPEGXL_ENABLE_SKCMS 1027 1028 const cmsContext context = GetContext(); 1029 1030 Profile profile; 1031 JXL_RETURN_IF_ERROR( 1032 DecodeProfile(context, Bytes(icc_data, icc_size), &profile)); 1033 1034 const cmsUInt32Number rendering_intent32 = 1035 cmsGetHeaderRenderingIntent(profile.get()); 1036 if (rendering_intent32 > 3) { 1037 return JXL_FAILURE("Invalid rendering intent %u\n", rendering_intent32); 1038 } 1039 // ICC and RenderingIntent have the same values (0..3). 1040 c_enc.rendering_intent = static_cast<RenderingIntent>(rendering_intent32); 1041 1042 static constexpr size_t kCICPSize = 12; 1043 static constexpr auto kCICPSignature = 1044 static_cast<cmsTagSignature>(0x63696370); 1045 uint8_t cicp_buffer[kCICPSize]; 1046 if (cmsReadRawTag(profile.get(), kCICPSignature, cicp_buffer, kCICPSize) == 1047 kCICPSize && 1048 ApplyCICP(cicp_buffer[8], cicp_buffer[9], cicp_buffer[10], 1049 cicp_buffer[11], &c_enc)) { 1050 *c = c_enc.ToExternal(); 1051 return JXL_TRUE; 1052 } 1053 1054 c_enc.color_space = ColorSpaceFromProfile(profile); 1055 if (cmsGetColorSpace(profile.get()) == cmsSigCmykData) { 1056 *cmyk = JXL_TRUE; 1057 *c = c_enc.ToExternal(); 1058 return JXL_TRUE; 1059 } 1060 1061 const cmsCIEXYZ wp_unadapted = UnadaptedWhitePoint(context, profile, c_enc); 1062 JXL_RETURN_IF_ERROR(c_enc.SetWhitePoint(CIExyFromXYZ(wp_unadapted))); 1063 1064 // Relies on color_space. 1065 JXL_RETURN_IF_ERROR( 1066 IdentifyPrimaries(context, profile, wp_unadapted, &c_enc)); 1067 1068 // Relies on color_space/white point/primaries being set already. 1069 JXL_RETURN_IF_ERROR(DetectTransferFunction(context, profile, &c_enc)); 1070 1071 #endif // JPEGXL_ENABLE_SKCMS 1072 1073 *c = c_enc.ToExternal(); 1074 return JXL_TRUE; 1075 } 1076 1077 } // namespace 1078 1079 namespace { 1080 1081 void JxlCmsDestroy(void* cms_data) { 1082 if (cms_data == nullptr) return; 1083 JxlCms* t = reinterpret_cast<JxlCms*>(cms_data); 1084 #if !JPEGXL_ENABLE_SKCMS 1085 TransformDeleter()(t->lcms_transform); 1086 #endif 1087 delete t; 1088 } 1089 1090 void AllocateBuffer(size_t length, size_t num_threads, 1091 std::vector<float>* storage, std::vector<float*>* view) { 1092 constexpr size_t kAlign = 128 / sizeof(float); 1093 size_t stride = RoundUpTo(length, kAlign); 1094 storage->resize(stride * num_threads + kAlign); 1095 intptr_t addr = reinterpret_cast<intptr_t>(storage->data()); 1096 size_t offset = 1097 (RoundUpTo(addr, kAlign * sizeof(float)) - addr) / sizeof(float); 1098 view->clear(); 1099 view->reserve(num_threads); 1100 for (size_t i = 0; i < num_threads; ++i) { 1101 view->emplace_back(storage->data() + offset + i * stride); 1102 } 1103 } 1104 1105 void* JxlCmsInit(void* init_data, size_t num_threads, size_t xsize, 1106 const JxlColorProfile* input, const JxlColorProfile* output, 1107 float intensity_target) { 1108 if (init_data == nullptr) { 1109 JXL_NOTIFY_ERROR("JxlCmsInit: init_data is nullptr"); 1110 return nullptr; 1111 } 1112 const auto* cms = static_cast<const JxlCmsInterface*>(init_data); 1113 auto t = jxl::make_unique<JxlCms>(); 1114 IccBytes icc_src; 1115 IccBytes icc_dst; 1116 if (input->icc.size == 0) { 1117 JXL_NOTIFY_ERROR("JxlCmsInit: empty input ICC"); 1118 return nullptr; 1119 } 1120 if (output->icc.size == 0) { 1121 JXL_NOTIFY_ERROR("JxlCmsInit: empty OUTPUT ICC"); 1122 return nullptr; 1123 } 1124 icc_src.assign(input->icc.data, input->icc.data + input->icc.size); 1125 ColorEncoding c_src; 1126 if (!c_src.SetFieldsFromICC(std::move(icc_src), *cms)) { 1127 JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse input ICC"); 1128 return nullptr; 1129 } 1130 icc_dst.assign(output->icc.data, output->icc.data + output->icc.size); 1131 ColorEncoding c_dst; 1132 if (!c_dst.SetFieldsFromICC(std::move(icc_dst), *cms)) { 1133 JXL_NOTIFY_ERROR("JxlCmsInit: failed to parse output ICC"); 1134 return nullptr; 1135 } 1136 #if JXL_CMS_VERBOSE 1137 printf("%s -> %s\n", Description(c_src).c_str(), Description(c_dst).c_str()); 1138 #endif 1139 1140 #if JPEGXL_ENABLE_SKCMS 1141 if (!DecodeProfile(input->icc.data, input->icc.size, &t->profile_src)) { 1142 JXL_NOTIFY_ERROR("JxlCmsInit: skcms failed to parse input ICC"); 1143 return nullptr; 1144 } 1145 if (!DecodeProfile(output->icc.data, output->icc.size, &t->profile_dst)) { 1146 JXL_NOTIFY_ERROR("JxlCmsInit: skcms failed to parse output ICC"); 1147 return nullptr; 1148 } 1149 #else // JPEGXL_ENABLE_SKCMS 1150 const cmsContext context = GetContext(); 1151 Profile profile_src, profile_dst; 1152 if (!DecodeProfile(context, Bytes(c_src.icc), &profile_src)) { 1153 JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse input ICC"); 1154 return nullptr; 1155 } 1156 if (!DecodeProfile(context, Bytes(c_dst.icc), &profile_dst)) { 1157 JXL_NOTIFY_ERROR("JxlCmsInit: lcms failed to parse output ICC"); 1158 return nullptr; 1159 } 1160 #endif // JPEGXL_ENABLE_SKCMS 1161 1162 t->skip_lcms = false; 1163 if (c_src.SameColorEncoding(c_dst)) { 1164 t->skip_lcms = true; 1165 #if JXL_CMS_VERBOSE 1166 printf("Skip CMS\n"); 1167 #endif 1168 } 1169 1170 t->apply_hlg_ootf = c_src.tf.IsHLG() != c_dst.tf.IsHLG(); 1171 if (t->apply_hlg_ootf) { 1172 const ColorEncoding* c_hlg = c_src.tf.IsHLG() ? &c_src : &c_dst; 1173 t->hlg_ootf_num_channels = c_hlg->Channels(); 1174 if (t->hlg_ootf_num_channels == 3 && 1175 !GetPrimariesLuminances(*c_hlg, t->hlg_ootf_luminances.data())) { 1176 JXL_NOTIFY_ERROR( 1177 "JxlCmsInit: failed to compute the luminances of primaries"); 1178 return nullptr; 1179 } 1180 } 1181 1182 // Special-case SRGB <=> linear if the primaries / white point are the same, 1183 // or any conversion where PQ or HLG is involved: 1184 bool src_linear = c_src.tf.IsLinear(); 1185 const bool dst_linear = c_dst.tf.IsLinear(); 1186 1187 if (c_src.tf.IsPQ() || c_src.tf.IsHLG() || 1188 (c_src.tf.IsSRGB() && dst_linear && c_src.SameColorSpace(c_dst))) { 1189 // Construct new profile as if the data were already/still linear. 1190 ColorEncoding c_linear_src = c_src; 1191 c_linear_src.tf.SetTransferFunction(TransferFunction::kLinear); 1192 #if JPEGXL_ENABLE_SKCMS 1193 skcms_ICCProfile new_src; 1194 #else // JPEGXL_ENABLE_SKCMS 1195 Profile new_src; 1196 #endif // JPEGXL_ENABLE_SKCMS 1197 // Only enable ExtraTF if profile creation succeeded. 1198 if (MaybeCreateProfile(c_linear_src.ToExternal(), &icc_src) && 1199 #if JPEGXL_ENABLE_SKCMS 1200 DecodeProfile(icc_src.data(), icc_src.size(), &new_src)) { 1201 #else // JPEGXL_ENABLE_SKCMS 1202 DecodeProfile(context, Bytes(icc_src), &new_src)) { 1203 #endif // JPEGXL_ENABLE_SKCMS 1204 #if JXL_CMS_VERBOSE 1205 printf("Special HLG/PQ/sRGB -> linear\n"); 1206 #endif 1207 #if JPEGXL_ENABLE_SKCMS 1208 t->icc_src = std::move(icc_src); 1209 t->profile_src = new_src; 1210 #else // JPEGXL_ENABLE_SKCMS 1211 profile_src.swap(new_src); 1212 #endif // JPEGXL_ENABLE_SKCMS 1213 t->preprocess = c_src.tf.IsSRGB() 1214 ? ExtraTF::kSRGB 1215 : (c_src.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG); 1216 c_src = c_linear_src; 1217 src_linear = true; 1218 } else { 1219 if (t->apply_hlg_ootf) { 1220 JXL_NOTIFY_ERROR( 1221 "Failed to create extra linear source profile, and HLG OOTF " 1222 "required"); 1223 return nullptr; 1224 } 1225 JXL_WARNING("Failed to create extra linear destination profile"); 1226 } 1227 } 1228 1229 if (c_dst.tf.IsPQ() || c_dst.tf.IsHLG() || 1230 (c_dst.tf.IsSRGB() && src_linear && c_src.SameColorSpace(c_dst))) { 1231 ColorEncoding c_linear_dst = c_dst; 1232 c_linear_dst.tf.SetTransferFunction(TransferFunction::kLinear); 1233 #if JPEGXL_ENABLE_SKCMS 1234 skcms_ICCProfile new_dst; 1235 #else // JPEGXL_ENABLE_SKCMS 1236 Profile new_dst; 1237 #endif // JPEGXL_ENABLE_SKCMS 1238 // Only enable ExtraTF if profile creation succeeded. 1239 if (MaybeCreateProfile(c_linear_dst.ToExternal(), &icc_dst) && 1240 #if JPEGXL_ENABLE_SKCMS 1241 DecodeProfile(icc_dst.data(), icc_dst.size(), &new_dst)) { 1242 #else // JPEGXL_ENABLE_SKCMS 1243 DecodeProfile(context, Bytes(icc_dst), &new_dst)) { 1244 #endif // JPEGXL_ENABLE_SKCMS 1245 #if JXL_CMS_VERBOSE 1246 printf("Special linear -> HLG/PQ/sRGB\n"); 1247 #endif 1248 #if JPEGXL_ENABLE_SKCMS 1249 t->icc_dst = std::move(icc_dst); 1250 t->profile_dst = new_dst; 1251 #else // JPEGXL_ENABLE_SKCMS 1252 profile_dst.swap(new_dst); 1253 #endif // JPEGXL_ENABLE_SKCMS 1254 t->postprocess = c_dst.tf.IsSRGB() 1255 ? ExtraTF::kSRGB 1256 : (c_dst.tf.IsPQ() ? ExtraTF::kPQ : ExtraTF::kHLG); 1257 c_dst = c_linear_dst; 1258 } else { 1259 if (t->apply_hlg_ootf) { 1260 JXL_NOTIFY_ERROR( 1261 "Failed to create extra linear destination profile, and inverse " 1262 "HLG OOTF required"); 1263 return nullptr; 1264 } 1265 JXL_WARNING("Failed to create extra linear destination profile"); 1266 } 1267 } 1268 1269 if (c_src.SameColorEncoding(c_dst)) { 1270 #if JXL_CMS_VERBOSE 1271 printf("Same intermediary linear profiles, skipping CMS\n"); 1272 #endif 1273 t->skip_lcms = true; 1274 } 1275 1276 #if JPEGXL_ENABLE_SKCMS 1277 if (!skcms_MakeUsableAsDestination(&t->profile_dst)) { 1278 JXL_NOTIFY_ERROR( 1279 "Failed to make %s usable as a color transform destination", 1280 ColorEncodingDescription(c_dst.ToExternal()).c_str()); 1281 return nullptr; 1282 } 1283 #endif // JPEGXL_ENABLE_SKCMS 1284 1285 // Not including alpha channel (copied separately). 1286 const size_t channels_src = (c_src.cmyk ? 4 : c_src.Channels()); 1287 const size_t channels_dst = c_dst.Channels(); 1288 #if JXL_CMS_VERBOSE 1289 printf("Channels: %" PRIuS "; Threads: %" PRIuS "\n", channels_src, 1290 num_threads); 1291 #endif 1292 1293 #if !JPEGXL_ENABLE_SKCMS 1294 // Type includes color space (XYZ vs RGB), so can be different. 1295 const uint32_t type_src = Type32(c_src, channels_src == 4); 1296 const uint32_t type_dst = Type32(c_dst, false); 1297 const uint32_t intent = static_cast<uint32_t>(c_dst.rendering_intent); 1298 // Use cmsFLAGS_NOCACHE to disable the 1-pixel cache and make calling 1299 // cmsDoTransform() thread-safe. 1300 const uint32_t flags = cmsFLAGS_NOCACHE | cmsFLAGS_BLACKPOINTCOMPENSATION | 1301 cmsFLAGS_HIGHRESPRECALC; 1302 t->lcms_transform = 1303 cmsCreateTransformTHR(context, profile_src.get(), type_src, 1304 profile_dst.get(), type_dst, intent, flags); 1305 if (t->lcms_transform == nullptr) { 1306 JXL_NOTIFY_ERROR("Failed to create transform"); 1307 return nullptr; 1308 } 1309 #endif // !JPEGXL_ENABLE_SKCMS 1310 1311 // Ideally LCMS would convert directly from External to Image3. However, 1312 // cmsDoTransformLineStride only accepts 32-bit BytesPerPlaneIn, whereas our 1313 // planes can be more than 4 GiB apart. Hence, transform inputs/outputs must 1314 // be interleaved. Calling cmsDoTransform for each pixel is expensive 1315 // (indirect call). We therefore transform rows, which requires per-thread 1316 // buffers. To avoid separate allocations, we use the rows of an image. 1317 // Because LCMS apparently also cannot handle <= 16 bit inputs and 32-bit 1318 // outputs (or vice versa), we use floating point input/output. 1319 t->channels_src = channels_src; 1320 t->channels_dst = channels_dst; 1321 #if !JPEGXL_ENABLE_SKCMS 1322 size_t actual_channels_src = channels_src; 1323 size_t actual_channels_dst = channels_dst; 1324 #else 1325 // SkiaCMS doesn't support grayscale float buffers, so we create space for RGB 1326 // float buffers anyway. 1327 size_t actual_channels_src = (channels_src == 4 ? 4 : 3); 1328 size_t actual_channels_dst = 3; 1329 #endif 1330 AllocateBuffer(xsize * actual_channels_src, num_threads, &t->src_storage, 1331 &t->buf_src); 1332 AllocateBuffer(xsize * actual_channels_dst, num_threads, &t->dst_storage, 1333 &t->buf_dst); 1334 t->intensity_target = intensity_target; 1335 return t.release(); 1336 } 1337 1338 float* JxlCmsGetSrcBuf(void* cms_data, size_t thread) { 1339 JxlCms* t = reinterpret_cast<JxlCms*>(cms_data); 1340 return t->buf_src[thread]; 1341 } 1342 1343 float* JxlCmsGetDstBuf(void* cms_data, size_t thread) { 1344 JxlCms* t = reinterpret_cast<JxlCms*>(cms_data); 1345 return t->buf_dst[thread]; 1346 } 1347 1348 } // namespace 1349 1350 extern "C" { 1351 1352 JXL_CMS_EXPORT const JxlCmsInterface* JxlGetDefaultCms() { 1353 static constexpr JxlCmsInterface kInterface = { 1354 /*set_fields_data=*/nullptr, 1355 /*set_fields_from_icc=*/&JxlCmsSetFieldsFromICC, 1356 /*init_data=*/const_cast<void*>(static_cast<const void*>(&kInterface)), 1357 /*init=*/&JxlCmsInit, 1358 /*get_src_buf=*/&JxlCmsGetSrcBuf, 1359 /*get_dst_buf=*/&JxlCmsGetDstBuf, 1360 /*run=*/&DoColorSpaceTransform, 1361 /*destroy=*/&JxlCmsDestroy}; 1362 return &kInterface; 1363 } 1364 1365 } // extern "C" 1366 1367 } // namespace jxl 1368 #endif // HWY_ONCE