color_encoding_cms.h (19960B)
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 #ifndef LIB_JXL_CMS_COLOR_ENCODING_CMS_H_ 7 #define LIB_JXL_CMS_COLOR_ENCODING_CMS_H_ 8 9 #include <jxl/cms_interface.h> 10 #include <jxl/color_encoding.h> 11 #include <jxl/types.h> 12 13 #include <cmath> 14 #include <cstdint> 15 #include <cstring> 16 #include <utility> 17 #include <vector> 18 19 #include "lib/jxl/base/status.h" 20 21 namespace jxl { 22 namespace cms { 23 24 using IccBytes = std::vector<uint8_t>; 25 26 // Returns whether the two inputs are approximately equal. 27 static inline bool ApproxEq(const double a, const double b, 28 double max_l1 = 1E-3) { 29 // Threshold should be sufficient for ICC's 15-bit fixed-point numbers. 30 // We have seen differences of 7.1E-5 with lcms2 and 1E-3 with skcms. 31 return std::abs(a - b) <= max_l1; 32 } 33 34 // (All CIE units are for the standard 1931 2 degree observer) 35 36 // Color space the color pixel data is encoded in. The color pixel data is 37 // 3-channel in all cases except in case of kGray, where it uses only 1 channel. 38 // This also determines the amount of channels used in modular encoding. 39 enum class ColorSpace : uint32_t { 40 // Trichromatic color data. This also includes CMYK if a kBlack 41 // ExtraChannelInfo is present. This implies, if there is an ICC profile, that 42 // the ICC profile uses a 3-channel color space if no kBlack extra channel is 43 // present, or uses color space 'CMYK' if a kBlack extra channel is present. 44 kRGB, 45 // Single-channel data. This implies, if there is an ICC profile, that the ICC 46 // profile also represents single-channel data and has the appropriate color 47 // space ('GRAY'). 48 kGray, 49 // Like kRGB, but implies fixed values for primaries etc. 50 kXYB, 51 // For non-RGB/gray data, e.g. from non-electro-optical sensors. Otherwise 52 // the same conditions as kRGB apply. 53 kUnknown 54 // NB: don't forget to update EnumBits! 55 }; 56 57 // Values from CICP ColourPrimaries. 58 enum class WhitePoint : uint32_t { 59 kD65 = 1, // sRGB/BT.709/Display P3/BT.2020 60 kCustom = 2, // Actual values encoded in separate fields 61 kE = 10, // XYZ 62 kDCI = 11, // DCI-P3 63 // NB: don't forget to update EnumBits! 64 }; 65 66 // Values from CICP ColourPrimaries 67 enum class Primaries : uint32_t { 68 kSRGB = 1, // Same as BT.709 69 kCustom = 2, // Actual values encoded in separate fields 70 k2100 = 9, // Same as BT.2020 71 kP3 = 11, 72 // NB: don't forget to update EnumBits! 73 }; 74 75 // Values from CICP TransferCharacteristics 76 enum class TransferFunction : uint32_t { 77 k709 = 1, 78 kUnknown = 2, 79 kLinear = 8, 80 kSRGB = 13, 81 kPQ = 16, // from BT.2100 82 kDCI = 17, // from SMPTE RP 431-2 reference projector 83 kHLG = 18, // from BT.2100 84 // NB: don't forget to update EnumBits! 85 }; 86 87 enum class RenderingIntent : uint32_t { 88 // Values match ICC sRGB encodings. 89 kPerceptual = 0, // good for photos, requires a profile with LUT. 90 kRelative, // good for logos. 91 kSaturation, // perhaps useful for CG with fully saturated colors. 92 kAbsolute, // leaves white point unchanged; good for proofing. 93 // NB: don't forget to update EnumBits! 94 }; 95 96 // Chromaticity (Y is omitted because it is 1 for white points and implicit for 97 // primaries) 98 struct CIExy { 99 double x = 0.0; 100 double y = 0.0; 101 }; 102 103 struct PrimariesCIExy { 104 CIExy r; 105 CIExy g; 106 CIExy b; 107 }; 108 109 // Serializable form of CIExy. 110 struct Customxy { 111 static constexpr uint32_t kMul = 1000000; 112 static constexpr double kRoughLimit = 4.0; 113 static constexpr int32_t kMin = -0x200000; 114 static constexpr int32_t kMax = 0x1FFFFF; 115 116 int32_t x = 0; 117 int32_t y = 0; 118 119 CIExy GetValue() const { 120 CIExy xy; 121 xy.x = x * (1.0 / kMul); 122 xy.y = y * (1.0 / kMul); 123 return xy; 124 } 125 126 Status SetValue(const CIExy& xy) { 127 bool ok = (std::abs(xy.x) < kRoughLimit) && (std::abs(xy.y) < kRoughLimit); 128 if (!ok) return JXL_FAILURE("X or Y is out of bounds"); 129 x = static_cast<int32_t>(roundf(xy.x * kMul)); 130 if (x < kMin || x > kMax) return JXL_FAILURE("X is out of bounds"); 131 y = static_cast<int32_t>(roundf(xy.y * kMul)); 132 if (y < kMin || y > kMax) return JXL_FAILURE("Y is out of bounds"); 133 return true; 134 } 135 136 bool IsSame(const Customxy& other) const { 137 return (x == other.x) && (y == other.y); 138 } 139 }; 140 141 static inline Status WhitePointFromExternal(const JxlWhitePoint external, 142 WhitePoint* out) { 143 switch (external) { 144 case JXL_WHITE_POINT_D65: 145 *out = WhitePoint::kD65; 146 return true; 147 case JXL_WHITE_POINT_CUSTOM: 148 *out = WhitePoint::kCustom; 149 return true; 150 case JXL_WHITE_POINT_E: 151 *out = WhitePoint::kE; 152 return true; 153 case JXL_WHITE_POINT_DCI: 154 *out = WhitePoint::kDCI; 155 return true; 156 } 157 return JXL_FAILURE("Invalid WhitePoint enum value %d", 158 static_cast<int>(external)); 159 } 160 161 static inline Status PrimariesFromExternal(const JxlPrimaries external, 162 Primaries* out) { 163 switch (external) { 164 case JXL_PRIMARIES_SRGB: 165 *out = Primaries::kSRGB; 166 return true; 167 case JXL_PRIMARIES_CUSTOM: 168 *out = Primaries::kCustom; 169 return true; 170 case JXL_PRIMARIES_2100: 171 *out = Primaries::k2100; 172 return true; 173 case JXL_PRIMARIES_P3: 174 *out = Primaries::kP3; 175 return true; 176 } 177 return JXL_FAILURE("Invalid Primaries enum value"); 178 } 179 180 static inline Status RenderingIntentFromExternal( 181 const JxlRenderingIntent external, RenderingIntent* out) { 182 switch (external) { 183 case JXL_RENDERING_INTENT_PERCEPTUAL: 184 *out = RenderingIntent::kPerceptual; 185 return true; 186 case JXL_RENDERING_INTENT_RELATIVE: 187 *out = RenderingIntent::kRelative; 188 return true; 189 case JXL_RENDERING_INTENT_SATURATION: 190 *out = RenderingIntent::kSaturation; 191 return true; 192 case JXL_RENDERING_INTENT_ABSOLUTE: 193 *out = RenderingIntent::kAbsolute; 194 return true; 195 } 196 return JXL_FAILURE("Invalid RenderingIntent enum value"); 197 } 198 199 struct CustomTransferFunction { 200 // Highest reasonable value for the gamma of a transfer curve. 201 static constexpr uint32_t kMaxGamma = 8192; 202 static constexpr uint32_t kGammaMul = 10000000; 203 204 bool have_gamma = false; 205 206 // OETF exponent to go from linear to gamma-compressed. 207 uint32_t gamma = 0; // Only used if have_gamma_. 208 209 // Can be kUnknown. 210 TransferFunction transfer_function = 211 TransferFunction::kSRGB; // Only used if !have_gamma_. 212 213 TransferFunction GetTransferFunction() const { 214 JXL_ASSERT(!have_gamma); 215 return transfer_function; 216 } 217 void SetTransferFunction(const TransferFunction tf) { 218 have_gamma = false; 219 transfer_function = tf; 220 } 221 222 bool IsUnknown() const { 223 return !have_gamma && (transfer_function == TransferFunction::kUnknown); 224 } 225 bool IsSRGB() const { 226 return !have_gamma && (transfer_function == TransferFunction::kSRGB); 227 } 228 bool IsLinear() const { 229 return !have_gamma && (transfer_function == TransferFunction::kLinear); 230 } 231 bool IsPQ() const { 232 return !have_gamma && (transfer_function == TransferFunction::kPQ); 233 } 234 bool IsHLG() const { 235 return !have_gamma && (transfer_function == TransferFunction::kHLG); 236 } 237 bool Is709() const { 238 return !have_gamma && (transfer_function == TransferFunction::k709); 239 } 240 bool IsDCI() const { 241 return !have_gamma && (transfer_function == TransferFunction::kDCI); 242 } 243 244 double GetGamma() const { 245 JXL_ASSERT(have_gamma); 246 return gamma * (1.0 / kGammaMul); // (0, 1) 247 } 248 Status SetGamma(double new_gamma) { 249 if (new_gamma < (1.0 / kMaxGamma) || new_gamma > 1.0) { 250 return JXL_FAILURE("Invalid gamma %f", new_gamma); 251 } 252 253 have_gamma = false; 254 if (ApproxEq(new_gamma, 1.0)) { 255 transfer_function = TransferFunction::kLinear; 256 return true; 257 } 258 if (ApproxEq(new_gamma, 1.0 / 2.6)) { 259 transfer_function = TransferFunction::kDCI; 260 return true; 261 } 262 // Don't translate 0.45.. to kSRGB nor k709 - that might change pixel 263 // values because those curves also have a linear part. 264 265 have_gamma = true; 266 gamma = roundf(new_gamma * kGammaMul); 267 transfer_function = TransferFunction::kUnknown; 268 return true; 269 } 270 271 bool IsSame(const CustomTransferFunction& other) const { 272 if (have_gamma != other.have_gamma) { 273 return false; 274 } 275 if (have_gamma) { 276 if (gamma != other.gamma) { 277 return false; 278 } 279 } else { 280 if (transfer_function != other.transfer_function) { 281 return false; 282 } 283 } 284 return true; 285 } 286 }; 287 288 static inline Status ConvertExternalToInternalTransferFunction( 289 const JxlTransferFunction external, TransferFunction* internal) { 290 switch (external) { 291 case JXL_TRANSFER_FUNCTION_709: 292 *internal = TransferFunction::k709; 293 return true; 294 case JXL_TRANSFER_FUNCTION_UNKNOWN: 295 *internal = TransferFunction::kUnknown; 296 return true; 297 case JXL_TRANSFER_FUNCTION_LINEAR: 298 *internal = TransferFunction::kLinear; 299 return true; 300 case JXL_TRANSFER_FUNCTION_SRGB: 301 *internal = TransferFunction::kSRGB; 302 return true; 303 case JXL_TRANSFER_FUNCTION_PQ: 304 *internal = TransferFunction::kPQ; 305 return true; 306 case JXL_TRANSFER_FUNCTION_DCI: 307 *internal = TransferFunction::kDCI; 308 return true; 309 case JXL_TRANSFER_FUNCTION_HLG: 310 *internal = TransferFunction::kHLG; 311 return true; 312 case JXL_TRANSFER_FUNCTION_GAMMA: 313 return JXL_FAILURE("Gamma should be handled separately"); 314 } 315 return JXL_FAILURE("Invalid TransferFunction enum value"); 316 } 317 318 // Compact encoding of data required to interpret and translate pixels to a 319 // known color space. Stored in Metadata. Thread-compatible. 320 struct ColorEncoding { 321 // Only valid if HaveFields() 322 WhitePoint white_point = WhitePoint::kD65; 323 Primaries primaries = Primaries::kSRGB; // Only valid if HasPrimaries() 324 RenderingIntent rendering_intent = RenderingIntent::kRelative; 325 326 // When false, fields such as white_point and tf are invalid and must not be 327 // used. This occurs after setting a raw bytes-only ICC profile, only the 328 // ICC bytes may be used. The color_space_ field is still valid. 329 bool have_fields = true; 330 331 IccBytes icc; // Valid ICC profile 332 333 ColorSpace color_space = ColorSpace::kRGB; // Can be kUnknown 334 bool cmyk = false; 335 336 // "late sync" fields 337 CustomTransferFunction tf; 338 Customxy white; // Only used if white_point == kCustom 339 Customxy red; // Only used if primaries == kCustom 340 Customxy green; // Only used if primaries == kCustom 341 Customxy blue; // Only used if primaries == kCustom 342 343 // Returns false if the field is invalid and unusable. 344 bool HasPrimaries() const { 345 return (color_space != ColorSpace::kGray) && 346 (color_space != ColorSpace::kXYB); 347 } 348 349 size_t Channels() const { return (color_space == ColorSpace::kGray) ? 1 : 3; } 350 351 PrimariesCIExy GetPrimaries() const { 352 JXL_DASSERT(have_fields); 353 JXL_ASSERT(HasPrimaries()); 354 PrimariesCIExy xy; 355 switch (primaries) { 356 case Primaries::kCustom: 357 xy.r = red.GetValue(); 358 xy.g = green.GetValue(); 359 xy.b = blue.GetValue(); 360 return xy; 361 362 case Primaries::kSRGB: 363 xy.r.x = 0.639998686; 364 xy.r.y = 0.330010138; 365 xy.g.x = 0.300003784; 366 xy.g.y = 0.600003357; 367 xy.b.x = 0.150002046; 368 xy.b.y = 0.059997204; 369 return xy; 370 371 case Primaries::k2100: 372 xy.r.x = 0.708; 373 xy.r.y = 0.292; 374 xy.g.x = 0.170; 375 xy.g.y = 0.797; 376 xy.b.x = 0.131; 377 xy.b.y = 0.046; 378 return xy; 379 380 case Primaries::kP3: 381 xy.r.x = 0.680; 382 xy.r.y = 0.320; 383 xy.g.x = 0.265; 384 xy.g.y = 0.690; 385 xy.b.x = 0.150; 386 xy.b.y = 0.060; 387 return xy; 388 } 389 JXL_UNREACHABLE("Invalid Primaries %u", static_cast<uint32_t>(primaries)); 390 } 391 392 Status SetPrimaries(const PrimariesCIExy& xy) { 393 JXL_DASSERT(have_fields); 394 JXL_ASSERT(HasPrimaries()); 395 if (xy.r.x == 0.0 || xy.r.y == 0.0 || xy.g.x == 0.0 || xy.g.y == 0.0 || 396 xy.b.x == 0.0 || xy.b.y == 0.0) { 397 return JXL_FAILURE("Invalid primaries %f %f %f %f %f %f", xy.r.x, xy.r.y, 398 xy.g.x, xy.g.y, xy.b.x, xy.b.y); 399 } 400 401 if (ApproxEq(xy.r.x, 0.64) && ApproxEq(xy.r.y, 0.33) && 402 ApproxEq(xy.g.x, 0.30) && ApproxEq(xy.g.y, 0.60) && 403 ApproxEq(xy.b.x, 0.15) && ApproxEq(xy.b.y, 0.06)) { 404 primaries = Primaries::kSRGB; 405 return true; 406 } 407 408 if (ApproxEq(xy.r.x, 0.708) && ApproxEq(xy.r.y, 0.292) && 409 ApproxEq(xy.g.x, 0.170) && ApproxEq(xy.g.y, 0.797) && 410 ApproxEq(xy.b.x, 0.131) && ApproxEq(xy.b.y, 0.046)) { 411 primaries = Primaries::k2100; 412 return true; 413 } 414 if (ApproxEq(xy.r.x, 0.680) && ApproxEq(xy.r.y, 0.320) && 415 ApproxEq(xy.g.x, 0.265) && ApproxEq(xy.g.y, 0.690) && 416 ApproxEq(xy.b.x, 0.150) && ApproxEq(xy.b.y, 0.060)) { 417 primaries = Primaries::kP3; 418 return true; 419 } 420 421 primaries = Primaries::kCustom; 422 JXL_RETURN_IF_ERROR(red.SetValue(xy.r)); 423 JXL_RETURN_IF_ERROR(green.SetValue(xy.g)); 424 JXL_RETURN_IF_ERROR(blue.SetValue(xy.b)); 425 return true; 426 } 427 428 CIExy GetWhitePoint() const { 429 JXL_DASSERT(have_fields); 430 CIExy xy; 431 switch (white_point) { 432 case WhitePoint::kCustom: 433 return white.GetValue(); 434 435 case WhitePoint::kD65: 436 xy.x = 0.3127; 437 xy.y = 0.3290; 438 return xy; 439 440 case WhitePoint::kDCI: 441 // From https://ieeexplore.ieee.org/document/7290729 C.2 page 11 442 xy.x = 0.314; 443 xy.y = 0.351; 444 return xy; 445 446 case WhitePoint::kE: 447 xy.x = xy.y = 1.0 / 3; 448 return xy; 449 } 450 JXL_UNREACHABLE("Invalid WhitePoint %u", 451 static_cast<uint32_t>(white_point)); 452 } 453 454 Status SetWhitePoint(const CIExy& xy) { 455 JXL_DASSERT(have_fields); 456 if (xy.x == 0.0 || xy.y == 0.0) { 457 return JXL_FAILURE("Invalid white point %f %f", xy.x, xy.y); 458 } 459 if (ApproxEq(xy.x, 0.3127) && ApproxEq(xy.y, 0.3290)) { 460 white_point = WhitePoint::kD65; 461 return true; 462 } 463 if (ApproxEq(xy.x, 1.0 / 3) && ApproxEq(xy.y, 1.0 / 3)) { 464 white_point = WhitePoint::kE; 465 return true; 466 } 467 if (ApproxEq(xy.x, 0.314) && ApproxEq(xy.y, 0.351)) { 468 white_point = WhitePoint::kDCI; 469 return true; 470 } 471 white_point = WhitePoint::kCustom; 472 return white.SetValue(xy); 473 } 474 475 // Checks if the color spaces (including white point / primaries) are the 476 // same, but ignores the transfer function, rendering intent and ICC bytes. 477 bool SameColorSpace(const ColorEncoding& other) const { 478 if (color_space != other.color_space) return false; 479 480 if (white_point != other.white_point) return false; 481 if (white_point == WhitePoint::kCustom) { 482 if (!white.IsSame(other.white)) { 483 return false; 484 } 485 } 486 487 if (HasPrimaries() != other.HasPrimaries()) return false; 488 if (HasPrimaries()) { 489 if (primaries != other.primaries) return false; 490 if (primaries == Primaries::kCustom) { 491 if (!red.IsSame(other.red)) return false; 492 if (!green.IsSame(other.green)) return false; 493 if (!blue.IsSame(other.blue)) return false; 494 } 495 } 496 return true; 497 } 498 499 // Checks if the color space and transfer function are the same, ignoring 500 // rendering intent and ICC bytes 501 bool SameColorEncoding(const ColorEncoding& other) const { 502 return SameColorSpace(other) && tf.IsSame(other.tf); 503 } 504 505 // Returns true if all fields have been initialized (possibly to kUnknown). 506 // Returns false if the ICC profile is invalid or decoding it fails. 507 Status SetFieldsFromICC(IccBytes&& new_icc, const JxlCmsInterface& cms) { 508 // In case parsing fails, mark the ColorEncoding as invalid. 509 JXL_ASSERT(!new_icc.empty()); 510 color_space = ColorSpace::kUnknown; 511 tf.transfer_function = TransferFunction::kUnknown; 512 icc.clear(); 513 514 JxlColorEncoding external; 515 JXL_BOOL new_cmyk; 516 JXL_RETURN_IF_ERROR(cms.set_fields_from_icc(cms.set_fields_data, 517 new_icc.data(), new_icc.size(), 518 &external, &new_cmyk)); 519 cmyk = static_cast<bool>(new_cmyk); 520 JXL_RETURN_IF_ERROR(FromExternal(external)); 521 icc = std::move(new_icc); 522 return true; 523 } 524 525 JxlColorEncoding ToExternal() const { 526 JxlColorEncoding external = {}; 527 if (!have_fields) { 528 external.color_space = JXL_COLOR_SPACE_UNKNOWN; 529 external.primaries = JXL_PRIMARIES_CUSTOM; 530 external.rendering_intent = JXL_RENDERING_INTENT_PERCEPTUAL; //? 531 external.transfer_function = JXL_TRANSFER_FUNCTION_UNKNOWN; 532 external.white_point = JXL_WHITE_POINT_CUSTOM; 533 return external; 534 } 535 external.color_space = static_cast<JxlColorSpace>(color_space); 536 537 external.white_point = static_cast<JxlWhitePoint>(white_point); 538 539 CIExy wp = GetWhitePoint(); 540 external.white_point_xy[0] = wp.x; 541 external.white_point_xy[1] = wp.y; 542 543 if (external.color_space == JXL_COLOR_SPACE_RGB || 544 external.color_space == JXL_COLOR_SPACE_UNKNOWN) { 545 external.primaries = static_cast<JxlPrimaries>(primaries); 546 PrimariesCIExy p = GetPrimaries(); 547 external.primaries_red_xy[0] = p.r.x; 548 external.primaries_red_xy[1] = p.r.y; 549 external.primaries_green_xy[0] = p.g.x; 550 external.primaries_green_xy[1] = p.g.y; 551 external.primaries_blue_xy[0] = p.b.x; 552 external.primaries_blue_xy[1] = p.b.y; 553 } 554 555 if (tf.have_gamma) { 556 external.transfer_function = JXL_TRANSFER_FUNCTION_GAMMA; 557 external.gamma = tf.GetGamma(); 558 } else { 559 external.transfer_function = 560 static_cast<JxlTransferFunction>(tf.GetTransferFunction()); 561 external.gamma = 0; 562 } 563 564 external.rendering_intent = 565 static_cast<JxlRenderingIntent>(rendering_intent); 566 return external; 567 } 568 569 // NB: does not create ICC. 570 Status FromExternal(const JxlColorEncoding& external) { 571 // TODO(eustas): update non-serializable on call-site 572 color_space = static_cast<ColorSpace>(external.color_space); 573 574 JXL_RETURN_IF_ERROR( 575 WhitePointFromExternal(external.white_point, &white_point)); 576 if (external.white_point == JXL_WHITE_POINT_CUSTOM) { 577 CIExy wp; 578 wp.x = external.white_point_xy[0]; 579 wp.y = external.white_point_xy[1]; 580 JXL_RETURN_IF_ERROR(SetWhitePoint(wp)); 581 } 582 583 if (external.color_space == JXL_COLOR_SPACE_RGB || 584 external.color_space == JXL_COLOR_SPACE_UNKNOWN) { 585 JXL_RETURN_IF_ERROR( 586 PrimariesFromExternal(external.primaries, &primaries)); 587 if (external.primaries == JXL_PRIMARIES_CUSTOM) { 588 PrimariesCIExy primaries; 589 primaries.r.x = external.primaries_red_xy[0]; 590 primaries.r.y = external.primaries_red_xy[1]; 591 primaries.g.x = external.primaries_green_xy[0]; 592 primaries.g.y = external.primaries_green_xy[1]; 593 primaries.b.x = external.primaries_blue_xy[0]; 594 primaries.b.y = external.primaries_blue_xy[1]; 595 JXL_RETURN_IF_ERROR(SetPrimaries(primaries)); 596 } 597 } 598 CustomTransferFunction tf; 599 if (external.transfer_function == JXL_TRANSFER_FUNCTION_GAMMA) { 600 JXL_RETURN_IF_ERROR(tf.SetGamma(external.gamma)); 601 } else { 602 TransferFunction tf_enum; 603 // JXL_TRANSFER_FUNCTION_GAMMA is not handled by this function since 604 // there's no internal enum value for it. 605 JXL_RETURN_IF_ERROR(ConvertExternalToInternalTransferFunction( 606 external.transfer_function, &tf_enum)); 607 tf.SetTransferFunction(tf_enum); 608 } 609 this->tf = tf; 610 611 JXL_RETURN_IF_ERROR(RenderingIntentFromExternal(external.rendering_intent, 612 &rendering_intent)); 613 614 icc.clear(); 615 616 return true; 617 } 618 }; 619 620 } // namespace cms 621 } // namespace jxl 622 623 #endif // LIB_JXL_CMS_COLOR_ENCODING_CMS_H_