jxl_cms_internal.h (39095B)
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_JXL_CMS_INTERNAL_H_ 7 #define LIB_JXL_CMS_JXL_CMS_INTERNAL_H_ 8 9 // ICC profiles and color space conversions. 10 11 #include <jxl/color_encoding.h> 12 13 #include <algorithm> 14 #include <cmath> 15 #include <cstddef> 16 #include <cstdint> 17 #include <cstring> 18 #include <string> 19 #include <vector> 20 21 #include "lib/jxl/base/common.h" 22 #include "lib/jxl/base/compiler_specific.h" 23 #include "lib/jxl/base/matrix_ops.h" 24 #include "lib/jxl/base/span.h" // Bytes 25 #include "lib/jxl/base/status.h" 26 #include "lib/jxl/cms/opsin_params.h" 27 #include "lib/jxl/cms/tone_mapping.h" 28 #include "lib/jxl/cms/transfer_functions.h" 29 30 #ifndef JXL_ENABLE_3D_ICC_TONEMAPPING 31 #define JXL_ENABLE_3D_ICC_TONEMAPPING 1 32 #endif 33 34 namespace jxl { 35 36 enum class ExtraTF { 37 kNone, 38 kPQ, 39 kHLG, 40 kSRGB, 41 }; 42 43 static Status PrimariesToXYZ(float rx, float ry, float gx, float gy, float bx, 44 float by, float wx, float wy, Matrix3x3& matrix) { 45 bool ok = (wx >= 0) && (wx <= 1) && (wy > 0) && (wy <= 1); 46 if (!ok) { 47 return JXL_FAILURE("Invalid white point"); 48 } 49 // TODO(lode): also require rx, ry, gx, gy, bx, to be in range 0-1? ICC 50 // profiles in theory forbid negative XYZ values, but in practice the ACES P0 51 // color space uses a negative y for the blue primary. 52 Matrix3x3 primaries{{{rx, gx, bx}, 53 {ry, gy, by}, 54 {1.0f - rx - ry, 1.0f - gx - gy, 1.0f - bx - by}}}; 55 Matrix3x3 primaries_inv; 56 primaries_inv = primaries; 57 JXL_RETURN_IF_ERROR(Inv3x3Matrix(primaries_inv)); 58 59 Vector3 w{wx / wy, 1.0f, (1.0f - wx - wy) / wy}; 60 // 1 / tiny float can still overflow 61 JXL_RETURN_IF_ERROR(std::isfinite(w[0]) && std::isfinite(w[2])); 62 Vector3 xyz; 63 Mul3x3Vector(primaries_inv, w, xyz); 64 65 Matrix3x3 a{{{xyz[0], 0, 0}, {0, xyz[1], 0}, {0, 0, xyz[2]}}}; 66 67 Mul3x3Matrix(primaries, a, matrix); 68 return true; 69 } 70 71 /* Chromatic adaptation matrices*/ 72 constexpr Matrix3x3 kBradford{{{0.8951f, 0.2664f, -0.1614f}, 73 {-0.7502f, 1.7135f, 0.0367f}, 74 {0.0389f, -0.0685f, 1.0296f}}}; 75 constexpr Matrix3x3 kBradfordInv{{{0.9869929f, -0.1470543f, 0.1599627f}, 76 {0.4323053f, 0.5183603f, 0.0492912f}, 77 {-0.0085287f, 0.0400428f, 0.9684867f}}}; 78 79 // Adapts white point x, y to D50 80 static Status AdaptToXYZD50(float wx, float wy, Matrix3x3& matrix) { 81 bool ok = (wx >= 0) && (wx <= 1) && (wy > 0) && (wy <= 1); 82 if (!ok) { 83 // Out of range values can cause division through zero 84 // further down with the bradford adaptation too. 85 return JXL_FAILURE("Invalid white point"); 86 } 87 Vector3 w{wx / wy, 1.0f, (1.0f - wx - wy) / wy}; 88 // 1 / tiny float can still overflow 89 JXL_RETURN_IF_ERROR(std::isfinite(w[0]) && std::isfinite(w[2])); 90 Vector3 w50{0.96422f, 1.0f, 0.82521f}; 91 92 Vector3 lms; 93 Vector3 lms50; 94 95 Mul3x3Vector(kBradford, w, lms); 96 Mul3x3Vector(kBradford, w50, lms50); 97 98 if (lms[0] == 0 || lms[1] == 0 || lms[2] == 0) { 99 return JXL_FAILURE("Invalid white point"); 100 } 101 Matrix3x3 a{{{lms50[0] / lms[0], 0, 0}, 102 {0, lms50[1] / lms[1], 0}, 103 {0, 0, lms50[2] / lms[2]}}}; 104 if (!std::isfinite(a[0][0]) || !std::isfinite(a[1][1]) || 105 !std::isfinite(a[2][2])) { 106 return JXL_FAILURE("Invalid white point"); 107 } 108 109 Matrix3x3 b; 110 Mul3x3Matrix(a, kBradford, b); 111 Mul3x3Matrix(kBradfordInv, b, matrix); 112 113 return true; 114 } 115 116 static Status PrimariesToXYZD50(float rx, float ry, float gx, float gy, 117 float bx, float by, float wx, float wy, 118 Matrix3x3& matrix) { 119 Matrix3x3 toXYZ; 120 JXL_RETURN_IF_ERROR(PrimariesToXYZ(rx, ry, gx, gy, bx, by, wx, wy, toXYZ)); 121 Matrix3x3 d50; 122 JXL_RETURN_IF_ERROR(AdaptToXYZD50(wx, wy, d50)); 123 124 Mul3x3Matrix(d50, toXYZ, matrix); 125 return true; 126 } 127 128 static Status ToneMapPixel(const JxlColorEncoding& c, const float in[3], 129 uint8_t pcslab_out[3]) { 130 Matrix3x3 primaries_XYZ; 131 JXL_RETURN_IF_ERROR(PrimariesToXYZ( 132 c.primaries_red_xy[0], c.primaries_red_xy[1], c.primaries_green_xy[0], 133 c.primaries_green_xy[1], c.primaries_blue_xy[0], c.primaries_blue_xy[1], 134 c.white_point_xy[0], c.white_point_xy[1], primaries_XYZ)); 135 const Vector3 luminances = primaries_XYZ[1]; 136 Color linear; 137 JxlTransferFunction tf = c.transfer_function; 138 if (tf == JXL_TRANSFER_FUNCTION_PQ) { 139 for (size_t i = 0; i < 3; ++i) { 140 linear[i] = TF_PQ_Base::DisplayFromEncoded( 141 /*display_intensity_target=*/10000.0, in[i]); 142 } 143 } else { 144 for (size_t i = 0; i < 3; ++i) { 145 linear[i] = TF_HLG_Base::DisplayFromEncoded(in[i]); 146 } 147 } 148 if (tf == JXL_TRANSFER_FUNCTION_PQ) { 149 Rec2408ToneMapperBase tone_mapper({0.0f, 10000.0f}, {0.0f, 250.0f}, 150 luminances); 151 tone_mapper.ToneMap(linear); 152 } else { 153 HlgOOTF_Base ootf(/*source_luminance=*/300, /*target_luminance=*/80, 154 luminances); 155 ootf.Apply(linear); 156 } 157 GamutMapScalar(linear, luminances, 158 /*preserve_saturation=*/0.3f); 159 160 Matrix3x3 chad; 161 JXL_RETURN_IF_ERROR( 162 AdaptToXYZD50(c.white_point_xy[0], c.white_point_xy[1], chad)); 163 Matrix3x3 to_xyzd50; 164 Mul3x3Matrix(chad, primaries_XYZ, to_xyzd50); 165 166 Vector3 xyz{0, 0, 0}; 167 for (size_t xyz_c = 0; xyz_c < 3; ++xyz_c) { 168 for (size_t rgb_c = 0; rgb_c < 3; ++rgb_c) { 169 xyz[xyz_c] += linear[rgb_c] * to_xyzd50[xyz_c][rgb_c]; 170 } 171 } 172 173 const auto lab_f = [](const float x) { 174 static constexpr float kDelta = 6. / 29; 175 return x <= kDelta * kDelta * kDelta 176 ? x * (1 / (3 * kDelta * kDelta)) + 4.f / 29 177 : std::cbrt(x); 178 }; 179 static constexpr float kXn = 0.964212; 180 static constexpr float kYn = 1; 181 static constexpr float kZn = 0.825188; 182 183 const float f_x = lab_f(xyz[0] / kXn); 184 const float f_y = lab_f(xyz[1] / kYn); 185 const float f_z = lab_f(xyz[2] / kZn); 186 187 pcslab_out[0] = static_cast<uint8_t>( 188 std::lroundf(255.f * Clamp1(1.16f * f_y - .16f, 0.f, 1.f))); 189 pcslab_out[1] = static_cast<uint8_t>( 190 std::lroundf(128.f + Clamp1(500 * (f_x - f_y), -128.f, 127.f))); 191 pcslab_out[2] = static_cast<uint8_t>( 192 std::lroundf(128.f + Clamp1(200 * (f_y - f_z), -128.f, 127.f))); 193 194 return true; 195 } 196 197 template <size_t N, ExtraTF tf> 198 static std::vector<uint16_t> CreateTableCurve(bool tone_map) { 199 // The generated PQ curve will make room for highlights up to this luminance. 200 // TODO(sboukortt): make this variable? 201 static constexpr float kPQIntensityTarget = 10000; 202 203 static_assert(N <= 4096); // ICC MFT2 only allows 4K entries 204 static_assert(tf == ExtraTF::kPQ || tf == ExtraTF::kHLG); 205 206 static constexpr Vector3 kLuminances{1.f / 3, 1.f / 3, 1.f / 3}; 207 Rec2408ToneMapperBase tone_mapper( 208 {0.0f, kPQIntensityTarget}, {0.0f, kDefaultIntensityTarget}, kLuminances); 209 // No point using float - LCMS converts to 16-bit for A2B/MFT. 210 std::vector<uint16_t> table(N); 211 for (uint32_t i = 0; i < N; ++i) { 212 const float x = static_cast<float>(i) / (N - 1); // 1.0 at index N - 1. 213 const double dx = static_cast<double>(x); 214 // LCMS requires EOTF (e.g. 2.4 exponent). 215 double y = (tf == ExtraTF::kHLG) 216 ? TF_HLG_Base::DisplayFromEncoded(dx) 217 : TF_PQ_Base::DisplayFromEncoded(kPQIntensityTarget, dx); 218 if (tone_map && tf == ExtraTF::kPQ && 219 kPQIntensityTarget > kDefaultIntensityTarget) { 220 float l = y * 10000 / kPQIntensityTarget; 221 Color gray{l, l, l}; 222 tone_mapper.ToneMap(gray); 223 y = gray[0]; 224 } 225 JXL_DASSERT(y >= 0.0); 226 // Clamp to table range - necessary for HLG. 227 y = Clamp1(y, 0.0, 1.0); 228 // 1.0 corresponds to table value 0xFFFF. 229 table[i] = static_cast<uint16_t>(roundf(y * 65535.0)); 230 } 231 return table; 232 } 233 234 static Status CIEXYZFromWhiteCIExy(double wx, double wy, Color& XYZ) { 235 // Target Y = 1. 236 if (std::abs(wy) < 1e-12) return JXL_FAILURE("Y value is too small"); 237 const float factor = 1 / wy; 238 XYZ[0] = wx * factor; 239 XYZ[1] = 1; 240 XYZ[2] = (1 - wx - wy) * factor; 241 return true; 242 } 243 244 namespace detail { 245 246 constexpr bool kEnable3DToneMapping = JXL_ENABLE_3D_ICC_TONEMAPPING; 247 248 static bool CanToneMap(const JxlColorEncoding& encoding) { 249 // If the color space cannot be represented by a CICP tag in the ICC profile 250 // then the rest of the profile must unambiguously identify it; we have less 251 // freedom to do use it for tone mapping. 252 JxlTransferFunction tf = encoding.transfer_function; 253 JxlPrimaries p = encoding.primaries; 254 JxlWhitePoint wp = encoding.white_point; 255 return encoding.color_space == JXL_COLOR_SPACE_RGB && 256 (tf == JXL_TRANSFER_FUNCTION_PQ || tf == JXL_TRANSFER_FUNCTION_HLG) && 257 ((p == JXL_PRIMARIES_P3 && 258 (wp == JXL_WHITE_POINT_D65 || wp == JXL_WHITE_POINT_DCI)) || 259 (p != JXL_PRIMARIES_CUSTOM && wp == JXL_WHITE_POINT_D65)); 260 } 261 262 static void ICCComputeMD5(const std::vector<uint8_t>& data, uint8_t sum[16]) 263 JXL_NO_SANITIZE("unsigned-integer-overflow") { 264 std::vector<uint8_t> data64 = data; 265 data64.push_back(128); 266 // Add bytes such that ((size + 8) & 63) == 0. 267 size_t extra = ((64 - ((data64.size() + 8) & 63)) & 63); 268 data64.resize(data64.size() + extra, 0); 269 for (uint64_t i = 0; i < 64; i += 8) { 270 data64.push_back(static_cast<uint64_t>(data.size() << 3u) >> i); 271 } 272 273 static const uint32_t sineparts[64] = { 274 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, 275 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, 276 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, 277 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, 278 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, 279 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, 280 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, 281 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, 282 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, 283 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, 284 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391, 285 }; 286 static const uint32_t shift[64] = { 287 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 288 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 289 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 290 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 291 }; 292 293 uint32_t a0 = 0x67452301; 294 uint32_t b0 = 0xefcdab89; 295 uint32_t c0 = 0x98badcfe; 296 uint32_t d0 = 0x10325476; 297 298 for (size_t i = 0; i < data64.size(); i += 64) { 299 uint32_t a = a0; 300 uint32_t b = b0; 301 uint32_t c = c0; 302 uint32_t d = d0; 303 uint32_t f; 304 uint32_t g; 305 for (size_t j = 0; j < 64; j++) { 306 if (j < 16) { 307 f = (b & c) | ((~b) & d); 308 g = j; 309 } else if (j < 32) { 310 f = (d & b) | ((~d) & c); 311 g = (5 * j + 1) & 0xf; 312 } else if (j < 48) { 313 f = b ^ c ^ d; 314 g = (3 * j + 5) & 0xf; 315 } else { 316 f = c ^ (b | (~d)); 317 g = (7 * j) & 0xf; 318 } 319 uint32_t dg0 = data64[i + g * 4 + 0]; 320 uint32_t dg1 = data64[i + g * 4 + 1]; 321 uint32_t dg2 = data64[i + g * 4 + 2]; 322 uint32_t dg3 = data64[i + g * 4 + 3]; 323 uint32_t u = dg0 | (dg1 << 8u) | (dg2 << 16u) | (dg3 << 24u); 324 f += a + sineparts[j] + u; 325 a = d; 326 d = c; 327 c = b; 328 b += (f << shift[j]) | (f >> (32u - shift[j])); 329 } 330 a0 += a; 331 b0 += b; 332 c0 += c; 333 d0 += d; 334 } 335 sum[0] = a0; 336 sum[1] = a0 >> 8u; 337 sum[2] = a0 >> 16u; 338 sum[3] = a0 >> 24u; 339 sum[4] = b0; 340 sum[5] = b0 >> 8u; 341 sum[6] = b0 >> 16u; 342 sum[7] = b0 >> 24u; 343 sum[8] = c0; 344 sum[9] = c0 >> 8u; 345 sum[10] = c0 >> 16u; 346 sum[11] = c0 >> 24u; 347 sum[12] = d0; 348 sum[13] = d0 >> 8u; 349 sum[14] = d0 >> 16u; 350 sum[15] = d0 >> 24u; 351 } 352 353 static Status CreateICCChadMatrix(double wx, double wy, Matrix3x3& result) { 354 Matrix3x3 m; 355 if (wy == 0) { // WhitePoint can not be pitch-black. 356 return JXL_FAILURE("Invalid WhitePoint"); 357 } 358 JXL_RETURN_IF_ERROR(AdaptToXYZD50(wx, wy, m)); 359 result = m; 360 return true; 361 } 362 363 // Creates RGB to XYZ matrix given RGB primaries and white point in xy. 364 static Status CreateICCRGBMatrix(double rx, double ry, double gx, double gy, 365 double bx, double by, double wx, double wy, 366 Matrix3x3& result) { 367 Matrix3x3 m; 368 JXL_RETURN_IF_ERROR(PrimariesToXYZD50(rx, ry, gx, gy, bx, by, wx, wy, m)); 369 result = m; 370 return true; 371 } 372 373 static void WriteICCUint32(uint32_t value, size_t pos, 374 std::vector<uint8_t>* icc) { 375 if (icc->size() < pos + 4) icc->resize(pos + 4); 376 (*icc)[pos + 0] = (value >> 24u) & 255; 377 (*icc)[pos + 1] = (value >> 16u) & 255; 378 (*icc)[pos + 2] = (value >> 8u) & 255; 379 (*icc)[pos + 3] = value & 255; 380 } 381 382 static void WriteICCUint16(uint16_t value, size_t pos, 383 std::vector<uint8_t>* icc) { 384 if (icc->size() < pos + 2) icc->resize(pos + 2); 385 (*icc)[pos + 0] = (value >> 8u) & 255; 386 (*icc)[pos + 1] = value & 255; 387 } 388 389 static void WriteICCUint8(uint8_t value, size_t pos, 390 std::vector<uint8_t>* icc) { 391 if (icc->size() < pos + 1) icc->resize(pos + 1); 392 (*icc)[pos] = value; 393 } 394 395 // Writes a 4-character tag 396 static void WriteICCTag(const char* value, size_t pos, 397 std::vector<uint8_t>* icc) { 398 if (icc->size() < pos + 4) icc->resize(pos + 4); 399 memcpy(icc->data() + pos, value, 4); 400 } 401 402 static Status WriteICCS15Fixed16(float value, size_t pos, 403 std::vector<uint8_t>* icc) { 404 // "nextafterf" for 32768.0f towards zero are: 405 // 32767.998046875, 32767.99609375, 32767.994140625 406 // Even the first value works well,... 407 bool ok = (-32767.995f <= value) && (value <= 32767.995f); 408 if (!ok) return JXL_FAILURE("ICC value is out of range / NaN"); 409 int32_t i = static_cast<int32_t>(std::lround(value * 65536.0f)); 410 // Use two's complement 411 uint32_t u = static_cast<uint32_t>(i); 412 WriteICCUint32(u, pos, icc); 413 return true; 414 } 415 416 static Status CreateICCHeader(const JxlColorEncoding& c, 417 std::vector<uint8_t>* header) { 418 // TODO(lode): choose color management engine name, e.g. "skia" if 419 // integrated in skia. 420 static const char* kCmm = "jxl "; 421 422 header->resize(128, 0); 423 424 WriteICCUint32(0, 0, header); // size, correct value filled in at end 425 WriteICCTag(kCmm, 4, header); 426 WriteICCUint32(0x04400000u, 8, header); 427 const char* profile_type = 428 c.color_space == JXL_COLOR_SPACE_XYB ? "scnr" : "mntr"; 429 WriteICCTag(profile_type, 12, header); 430 WriteICCTag(c.color_space == JXL_COLOR_SPACE_GRAY ? "GRAY" : "RGB ", 16, 431 header); 432 if (kEnable3DToneMapping && CanToneMap(c)) { 433 // We are going to use a 3D LUT for tone mapping, which will be more compact 434 // with an 8-bit LUT to CIELAB than with a 16-bit LUT to XYZ. 8-bit XYZ 435 // would not be viable due to XYZ being linear, whereas it is fine with 436 // CIELAB's ~cube root. 437 WriteICCTag("Lab ", 20, header); 438 } else { 439 WriteICCTag("XYZ ", 20, header); 440 } 441 442 // Three uint32_t's date/time encoding. 443 // TODO(lode): encode actual date and time, this is a placeholder 444 uint32_t year = 2019; 445 uint32_t month = 12; 446 uint32_t day = 1; 447 uint32_t hour = 0; 448 uint32_t minute = 0; 449 uint32_t second = 0; 450 WriteICCUint16(year, 24, header); 451 WriteICCUint16(month, 26, header); 452 WriteICCUint16(day, 28, header); 453 WriteICCUint16(hour, 30, header); 454 WriteICCUint16(minute, 32, header); 455 WriteICCUint16(second, 34, header); 456 457 WriteICCTag("acsp", 36, header); 458 WriteICCTag("APPL", 40, header); 459 WriteICCUint32(0, 44, header); // flags 460 WriteICCUint32(0, 48, header); // device manufacturer 461 WriteICCUint32(0, 52, header); // device model 462 WriteICCUint32(0, 56, header); // device attributes 463 WriteICCUint32(0, 60, header); // device attributes 464 WriteICCUint32(static_cast<uint32_t>(c.rendering_intent), 64, header); 465 466 // Mandatory D50 white point of profile connection space 467 WriteICCUint32(0x0000f6d6, 68, header); 468 WriteICCUint32(0x00010000, 72, header); 469 WriteICCUint32(0x0000d32d, 76, header); 470 471 WriteICCTag(kCmm, 80, header); 472 473 return true; 474 } 475 476 static void AddToICCTagTable(const char* tag, size_t offset, size_t size, 477 std::vector<uint8_t>* tagtable, 478 std::vector<size_t>* offsets) { 479 WriteICCTag(tag, tagtable->size(), tagtable); 480 // writing true offset deferred to later 481 WriteICCUint32(0, tagtable->size(), tagtable); 482 offsets->push_back(offset); 483 WriteICCUint32(size, tagtable->size(), tagtable); 484 } 485 486 static void FinalizeICCTag(std::vector<uint8_t>* tags, size_t* offset, 487 size_t* size) { 488 while ((tags->size() & 3) != 0) { 489 tags->push_back(0); 490 } 491 *offset += *size; 492 *size = tags->size() - *offset; 493 } 494 495 // The input text must be ASCII, writing other characters to UTF-16 is not 496 // implemented. 497 static void CreateICCMlucTag(const std::string& text, 498 std::vector<uint8_t>* tags) { 499 WriteICCTag("mluc", tags->size(), tags); 500 WriteICCUint32(0, tags->size(), tags); 501 WriteICCUint32(1, tags->size(), tags); 502 WriteICCUint32(12, tags->size(), tags); 503 WriteICCTag("enUS", tags->size(), tags); 504 WriteICCUint32(text.size() * 2, tags->size(), tags); 505 WriteICCUint32(28, tags->size(), tags); 506 for (char c : text) { 507 tags->push_back(0); // prepend 0 for UTF-16 508 tags->push_back(c); 509 } 510 } 511 512 static Status CreateICCXYZTag(const Color& xyz, std::vector<uint8_t>* tags) { 513 WriteICCTag("XYZ ", tags->size(), tags); 514 WriteICCUint32(0, tags->size(), tags); 515 for (size_t i = 0; i < 3; ++i) { 516 JXL_RETURN_IF_ERROR(WriteICCS15Fixed16(xyz[i], tags->size(), tags)); 517 } 518 return true; 519 } 520 521 static Status CreateICCChadTag(const Matrix3x3& chad, 522 std::vector<uint8_t>* tags) { 523 WriteICCTag("sf32", tags->size(), tags); 524 WriteICCUint32(0, tags->size(), tags); 525 for (size_t j = 0; j < 3; j++) { 526 for (size_t i = 0; i < 3; i++) { 527 JXL_RETURN_IF_ERROR(WriteICCS15Fixed16(chad[j][i], tags->size(), tags)); 528 } 529 } 530 return true; 531 } 532 533 static void MaybeCreateICCCICPTag(const JxlColorEncoding& c, 534 std::vector<uint8_t>* tags, size_t* offset, 535 size_t* size, std::vector<uint8_t>* tagtable, 536 std::vector<size_t>* offsets) { 537 if (c.color_space != JXL_COLOR_SPACE_RGB) { 538 return; 539 } 540 uint8_t primaries = 0; 541 if (c.primaries == JXL_PRIMARIES_P3) { 542 if (c.white_point == JXL_WHITE_POINT_D65) { 543 primaries = 12; 544 } else if (c.white_point == JXL_WHITE_POINT_DCI) { 545 primaries = 11; 546 } else { 547 return; 548 } 549 } else if (c.primaries != JXL_PRIMARIES_CUSTOM && 550 c.white_point == JXL_WHITE_POINT_D65) { 551 primaries = static_cast<uint8_t>(c.primaries); 552 } else { 553 return; 554 } 555 JxlTransferFunction tf = c.transfer_function; 556 if (tf == JXL_TRANSFER_FUNCTION_UNKNOWN || 557 tf == JXL_TRANSFER_FUNCTION_GAMMA) { 558 return; 559 } 560 WriteICCTag("cicp", tags->size(), tags); 561 WriteICCUint32(0, tags->size(), tags); 562 WriteICCUint8(primaries, tags->size(), tags); 563 WriteICCUint8(static_cast<uint8_t>(tf), tags->size(), tags); 564 // Matrix 565 WriteICCUint8(0, tags->size(), tags); 566 // Full range 567 WriteICCUint8(1, tags->size(), tags); 568 FinalizeICCTag(tags, offset, size); 569 AddToICCTagTable("cicp", *offset, *size, tagtable, offsets); 570 } 571 572 static void CreateICCCurvCurvTag(const std::vector<uint16_t>& curve, 573 std::vector<uint8_t>* tags) { 574 size_t pos = tags->size(); 575 tags->resize(tags->size() + 12 + curve.size() * 2, 0); 576 WriteICCTag("curv", pos, tags); 577 WriteICCUint32(0, pos + 4, tags); 578 WriteICCUint32(curve.size(), pos + 8, tags); 579 for (size_t i = 0; i < curve.size(); i++) { 580 WriteICCUint16(curve[i], pos + 12 + i * 2, tags); 581 } 582 } 583 584 // Writes 12 + 4*params.size() bytes 585 static Status CreateICCCurvParaTag(const std::vector<float>& params, 586 size_t curve_type, 587 std::vector<uint8_t>* tags) { 588 WriteICCTag("para", tags->size(), tags); 589 WriteICCUint32(0, tags->size(), tags); 590 WriteICCUint16(curve_type, tags->size(), tags); 591 WriteICCUint16(0, tags->size(), tags); 592 for (float param : params) { 593 JXL_RETURN_IF_ERROR(WriteICCS15Fixed16(param, tags->size(), tags)); 594 } 595 return true; 596 } 597 598 static Status CreateICCLutAtoBTagForXYB(std::vector<uint8_t>* tags) { 599 WriteICCTag("mAB ", tags->size(), tags); 600 // 4 reserved bytes set to 0 601 WriteICCUint32(0, tags->size(), tags); 602 // number of input channels 603 WriteICCUint8(3, tags->size(), tags); 604 // number of output channels 605 WriteICCUint8(3, tags->size(), tags); 606 // 2 reserved bytes for padding 607 WriteICCUint16(0, tags->size(), tags); 608 // offset to first B curve 609 WriteICCUint32(32, tags->size(), tags); 610 // offset to matrix 611 WriteICCUint32(244, tags->size(), tags); 612 // offset to first M curve 613 WriteICCUint32(148, tags->size(), tags); 614 // offset to CLUT 615 WriteICCUint32(80, tags->size(), tags); 616 // offset to first A curve 617 // (reuse linear B curves) 618 WriteICCUint32(32, tags->size(), tags); 619 620 // offset = 32 621 // no-op curves 622 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag({1.0f}, 0, tags)); 623 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag({1.0f}, 0, tags)); 624 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag({1.0f}, 0, tags)); 625 // offset = 80 626 // number of grid points for each input channel 627 for (int i = 0; i < 16; ++i) { 628 WriteICCUint8(i < 3 ? 2 : 0, tags->size(), tags); 629 } 630 // precision = 2 631 WriteICCUint8(2, tags->size(), tags); 632 // 3 bytes of padding 633 WriteICCUint8(0, tags->size(), tags); 634 WriteICCUint16(0, tags->size(), tags); 635 // 2*2*2*3 entries of 2 bytes each = 48 bytes 636 const jxl::cms::ColorCube3D& cube = jxl::cms::UnscaledA2BCube(); 637 for (size_t ix = 0; ix < 2; ++ix) { 638 for (size_t iy = 0; iy < 2; ++iy) { 639 for (size_t ib = 0; ib < 2; ++ib) { 640 const jxl::cms::ColorCube0D& out_f = cube[ix][iy][ib]; 641 for (int i = 0; i < 3; ++i) { 642 int32_t val = static_cast<int32_t>(std::lroundf(65535 * out_f[i])); 643 JXL_DASSERT(val >= 0 && val <= 65535); 644 WriteICCUint16(val, tags->size(), tags); 645 } 646 } 647 } 648 } 649 // offset = 148 650 // 3 curves with 5 parameters = 3 * (12 + 5 * 4) = 96 bytes 651 for (size_t i = 0; i < 3; ++i) { 652 const float b = -jxl::cms::kXYBOffset[i] - 653 std::cbrt(jxl::cms::kNegOpsinAbsorbanceBiasRGB[i]); 654 std::vector<float> params = { 655 3, 656 1.0f / jxl::cms::kXYBScale[i], 657 b, 658 0, // unused 659 std::max(0.f, -b * jxl::cms::kXYBScale[i]), // make skcms happy 660 }; 661 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag(params, 3, tags)); 662 } 663 // offset = 244 664 const double matrix[] = {1.5170095, -1.1065225, 0.071623, 665 -0.050022, 0.5683655, -0.018344, 666 -1.387676, 1.1145555, 0.6857255}; 667 // 12 * 4 = 48 bytes 668 for (double v : matrix) { 669 JXL_RETURN_IF_ERROR(WriteICCS15Fixed16(v, tags->size(), tags)); 670 } 671 for (size_t i = 0; i < 3; ++i) { 672 float intercept = 0; 673 for (size_t j = 0; j < 3; ++j) { 674 intercept += matrix[i * 3 + j] * jxl::cms::kNegOpsinAbsorbanceBiasRGB[j]; 675 } 676 JXL_RETURN_IF_ERROR(WriteICCS15Fixed16(intercept, tags->size(), tags)); 677 } 678 return true; 679 } 680 681 static Status CreateICCLutAtoBTagForHDR(JxlColorEncoding c, 682 std::vector<uint8_t>* tags) { 683 static constexpr size_t k3DLutDim = 9; 684 WriteICCTag("mft1", tags->size(), tags); 685 // 4 reserved bytes set to 0 686 WriteICCUint32(0, tags->size(), tags); 687 // number of input channels 688 WriteICCUint8(3, tags->size(), tags); 689 // number of output channels 690 WriteICCUint8(3, tags->size(), tags); 691 // number of CLUT grid points 692 WriteICCUint8(k3DLutDim, tags->size(), tags); 693 // 1 reserved bytes for padding 694 WriteICCUint8(0, tags->size(), tags); 695 696 // Matrix (per specification, must be identity if input is not XYZ) 697 for (size_t i = 0; i < 3; ++i) { 698 for (size_t j = 0; j < 3; ++j) { 699 JXL_RETURN_IF_ERROR( 700 WriteICCS15Fixed16(i == j ? 1.f : 0.f, tags->size(), tags)); 701 } 702 } 703 704 // Input tables 705 for (size_t c = 0; c < 3; ++c) { 706 for (size_t i = 0; i < 256; ++i) { 707 WriteICCUint8(i, tags->size(), tags); 708 } 709 } 710 711 for (size_t ix = 0; ix < k3DLutDim; ++ix) { 712 for (size_t iy = 0; iy < k3DLutDim; ++iy) { 713 for (size_t ib = 0; ib < k3DLutDim; ++ib) { 714 float f[3] = {ix * (1.0f / (k3DLutDim - 1)), 715 iy * (1.0f / (k3DLutDim - 1)), 716 ib * (1.0f / (k3DLutDim - 1))}; 717 uint8_t pcslab_out[3]; 718 JXL_RETURN_IF_ERROR(ToneMapPixel(c, f, pcslab_out)); 719 for (uint8_t val : pcslab_out) { 720 WriteICCUint8(val, tags->size(), tags); 721 } 722 } 723 } 724 } 725 726 // Output tables 727 for (size_t c = 0; c < 3; ++c) { 728 for (size_t i = 0; i < 256; ++i) { 729 WriteICCUint8(i, tags->size(), tags); 730 } 731 } 732 733 return true; 734 } 735 736 // Some software (Apple Safari, Preview) requires this. 737 static Status CreateICCNoOpBToATag(std::vector<uint8_t>* tags) { 738 WriteICCTag("mBA ", tags->size(), tags); // notypo 739 // 4 reserved bytes set to 0 740 WriteICCUint32(0, tags->size(), tags); 741 // number of input channels 742 WriteICCUint8(3, tags->size(), tags); 743 // number of output channels 744 WriteICCUint8(3, tags->size(), tags); 745 // 2 reserved bytes for padding 746 WriteICCUint16(0, tags->size(), tags); 747 // offset to first B curve 748 WriteICCUint32(32, tags->size(), tags); 749 // offset to matrix 750 WriteICCUint32(0, tags->size(), tags); 751 // offset to first M curve 752 WriteICCUint32(0, tags->size(), tags); 753 // offset to CLUT 754 WriteICCUint32(0, tags->size(), tags); 755 // offset to first A curve 756 WriteICCUint32(0, tags->size(), tags); 757 758 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag({1.0f}, 0, tags)); 759 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag({1.0f}, 0, tags)); 760 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag({1.0f}, 0, tags)); 761 762 return true; 763 } 764 765 // These strings are baked into Description - do not change. 766 767 static std::string ToString(JxlColorSpace color_space) { 768 switch (color_space) { 769 case JXL_COLOR_SPACE_RGB: 770 return "RGB"; 771 case JXL_COLOR_SPACE_GRAY: 772 return "Gra"; 773 case JXL_COLOR_SPACE_XYB: 774 return "XYB"; 775 case JXL_COLOR_SPACE_UNKNOWN: 776 return "CS?"; 777 default: 778 // Should not happen - visitor fails if enum is invalid. 779 JXL_DEBUG_ABORT("Invalid ColorSpace %u", 780 static_cast<uint32_t>(color_space)); 781 return "Invalid"; 782 } 783 } 784 785 static std::string ToString(JxlWhitePoint white_point) { 786 switch (white_point) { 787 case JXL_WHITE_POINT_D65: 788 return "D65"; 789 case JXL_WHITE_POINT_CUSTOM: 790 return "Cst"; 791 case JXL_WHITE_POINT_E: 792 return "EER"; 793 case JXL_WHITE_POINT_DCI: 794 return "DCI"; 795 default: 796 // Should not happen - visitor fails if enum is invalid. 797 JXL_DEBUG_ABORT("Invalid WhitePoint %u", 798 static_cast<uint32_t>(white_point)); 799 return "Invalid"; 800 } 801 } 802 803 static std::string ToString(JxlPrimaries primaries) { 804 switch (primaries) { 805 case JXL_PRIMARIES_SRGB: 806 return "SRG"; 807 case JXL_PRIMARIES_2100: 808 return "202"; 809 case JXL_PRIMARIES_P3: 810 return "DCI"; 811 case JXL_PRIMARIES_CUSTOM: 812 return "Cst"; 813 default: 814 // Should not happen - visitor fails if enum is invalid. 815 JXL_DEBUG_ABORT("Invalid Primaries %u", static_cast<uint32_t>(primaries)); 816 return "Invalid"; 817 } 818 } 819 820 static std::string ToString(JxlTransferFunction transfer_function) { 821 switch (transfer_function) { 822 case JXL_TRANSFER_FUNCTION_SRGB: 823 return "SRG"; 824 case JXL_TRANSFER_FUNCTION_LINEAR: 825 return "Lin"; 826 case JXL_TRANSFER_FUNCTION_709: 827 return "709"; 828 case JXL_TRANSFER_FUNCTION_PQ: 829 return "PeQ"; 830 case JXL_TRANSFER_FUNCTION_HLG: 831 return "HLG"; 832 case JXL_TRANSFER_FUNCTION_DCI: 833 return "DCI"; 834 case JXL_TRANSFER_FUNCTION_UNKNOWN: 835 return "TF?"; 836 case JXL_TRANSFER_FUNCTION_GAMMA: 837 JXL_DEBUG_ABORT("Invalid TransferFunction: gamma"); 838 return "Invalid"; 839 default: 840 // Should not happen - visitor fails if enum is invalid. 841 JXL_DEBUG_ABORT("Invalid TransferFunction %u", 842 static_cast<uint32_t>(transfer_function)); 843 return "Invalid"; 844 } 845 } 846 847 static std::string ToString(JxlRenderingIntent rendering_intent) { 848 switch (rendering_intent) { 849 case JXL_RENDERING_INTENT_PERCEPTUAL: 850 return "Per"; 851 case JXL_RENDERING_INTENT_RELATIVE: 852 return "Rel"; 853 case JXL_RENDERING_INTENT_SATURATION: 854 return "Sat"; 855 case JXL_RENDERING_INTENT_ABSOLUTE: 856 return "Abs"; 857 } 858 // Should not happen - visitor fails if enum is invalid. 859 JXL_DEBUG_ABORT("Invalid RenderingIntent %u", 860 static_cast<uint32_t>(rendering_intent)); 861 return "Invalid"; 862 } 863 864 static std::string ColorEncodingDescriptionImpl(const JxlColorEncoding& c) { 865 if (c.color_space == JXL_COLOR_SPACE_RGB && 866 c.white_point == JXL_WHITE_POINT_D65) { 867 if (c.rendering_intent == JXL_RENDERING_INTENT_PERCEPTUAL && 868 c.transfer_function == JXL_TRANSFER_FUNCTION_SRGB) { 869 if (c.primaries == JXL_PRIMARIES_SRGB) return "sRGB"; 870 if (c.primaries == JXL_PRIMARIES_P3) return "DisplayP3"; 871 } 872 if (c.rendering_intent == JXL_RENDERING_INTENT_RELATIVE && 873 c.primaries == JXL_PRIMARIES_2100) { 874 if (c.transfer_function == JXL_TRANSFER_FUNCTION_PQ) return "Rec2100PQ"; 875 if (c.transfer_function == JXL_TRANSFER_FUNCTION_HLG) return "Rec2100HLG"; 876 } 877 } 878 879 std::string d = ToString(c.color_space); 880 881 bool explicit_wp_tf = (c.color_space != JXL_COLOR_SPACE_XYB); 882 if (explicit_wp_tf) { 883 d += '_'; 884 if (c.white_point == JXL_WHITE_POINT_CUSTOM) { 885 d += jxl::ToString(c.white_point_xy[0]) + ';'; 886 d += jxl::ToString(c.white_point_xy[1]); 887 } else { 888 d += ToString(c.white_point); 889 } 890 } 891 892 if ((c.color_space != JXL_COLOR_SPACE_GRAY) && 893 (c.color_space != JXL_COLOR_SPACE_XYB)) { 894 d += '_'; 895 if (c.primaries == JXL_PRIMARIES_CUSTOM) { 896 d += jxl::ToString(c.primaries_red_xy[0]) + ';'; 897 d += jxl::ToString(c.primaries_red_xy[1]) + ';'; 898 d += jxl::ToString(c.primaries_green_xy[0]) + ';'; 899 d += jxl::ToString(c.primaries_green_xy[1]) + ';'; 900 d += jxl::ToString(c.primaries_blue_xy[0]) + ';'; 901 d += jxl::ToString(c.primaries_blue_xy[1]); 902 } else { 903 d += ToString(c.primaries); 904 } 905 } 906 907 d += '_'; 908 d += ToString(c.rendering_intent); 909 910 if (explicit_wp_tf) { 911 JxlTransferFunction tf = c.transfer_function; 912 d += '_'; 913 if (tf == JXL_TRANSFER_FUNCTION_GAMMA) { 914 d += 'g'; 915 d += jxl::ToString(c.gamma); 916 } else { 917 d += ToString(tf); 918 } 919 } 920 return d; 921 } 922 923 static Status MaybeCreateProfileImpl(const JxlColorEncoding& c, 924 std::vector<uint8_t>* icc) { 925 std::vector<uint8_t> header; 926 std::vector<uint8_t> tagtable; 927 std::vector<uint8_t> tags; 928 JxlTransferFunction tf = c.transfer_function; 929 if (c.color_space == JXL_COLOR_SPACE_UNKNOWN || 930 tf == JXL_TRANSFER_FUNCTION_UNKNOWN) { 931 return false; // Not an error 932 } 933 934 switch (c.color_space) { 935 case JXL_COLOR_SPACE_RGB: 936 case JXL_COLOR_SPACE_GRAY: 937 case JXL_COLOR_SPACE_XYB: 938 break; // OK 939 default: 940 return JXL_FAILURE("Invalid CS %u", 941 static_cast<unsigned int>(c.color_space)); 942 } 943 944 if (c.color_space == JXL_COLOR_SPACE_XYB && 945 c.rendering_intent != JXL_RENDERING_INTENT_PERCEPTUAL) { 946 return JXL_FAILURE( 947 "Only perceptual rendering intent implemented for XYB " 948 "ICC profile."); 949 } 950 951 JXL_RETURN_IF_ERROR(CreateICCHeader(c, &header)); 952 953 std::vector<size_t> offsets; 954 // tag count, deferred to later 955 WriteICCUint32(0, tagtable.size(), &tagtable); 956 957 size_t tag_offset = 0; 958 size_t tag_size = 0; 959 960 CreateICCMlucTag(ColorEncodingDescriptionImpl(c), &tags); 961 FinalizeICCTag(&tags, &tag_offset, &tag_size); 962 AddToICCTagTable("desc", tag_offset, tag_size, &tagtable, &offsets); 963 964 const std::string copyright = "CC0"; 965 CreateICCMlucTag(copyright, &tags); 966 FinalizeICCTag(&tags, &tag_offset, &tag_size); 967 AddToICCTagTable("cprt", tag_offset, tag_size, &tagtable, &offsets); 968 969 // TODO(eustas): isn't it the other way round: gray image has d50 WhitePoint? 970 if (c.color_space == JXL_COLOR_SPACE_GRAY) { 971 Color wtpt; 972 JXL_RETURN_IF_ERROR( 973 CIEXYZFromWhiteCIExy(c.white_point_xy[0], c.white_point_xy[1], wtpt)); 974 JXL_RETURN_IF_ERROR(CreateICCXYZTag(wtpt, &tags)); 975 } else { 976 Color d50{0.964203, 1.0, 0.824905}; 977 JXL_RETURN_IF_ERROR(CreateICCXYZTag(d50, &tags)); 978 } 979 FinalizeICCTag(&tags, &tag_offset, &tag_size); 980 AddToICCTagTable("wtpt", tag_offset, tag_size, &tagtable, &offsets); 981 982 if (c.color_space != JXL_COLOR_SPACE_GRAY) { 983 // Chromatic adaptation matrix 984 Matrix3x3 chad; 985 JXL_RETURN_IF_ERROR( 986 CreateICCChadMatrix(c.white_point_xy[0], c.white_point_xy[1], chad)); 987 988 JXL_RETURN_IF_ERROR(CreateICCChadTag(chad, &tags)); 989 FinalizeICCTag(&tags, &tag_offset, &tag_size); 990 AddToICCTagTable("chad", tag_offset, tag_size, &tagtable, &offsets); 991 } 992 993 if (c.color_space == JXL_COLOR_SPACE_RGB) { 994 MaybeCreateICCCICPTag(c, &tags, &tag_offset, &tag_size, &tagtable, 995 &offsets); 996 997 Matrix3x3 m; 998 JXL_RETURN_IF_ERROR(CreateICCRGBMatrix( 999 c.primaries_red_xy[0], c.primaries_red_xy[1], c.primaries_green_xy[0], 1000 c.primaries_green_xy[1], c.primaries_blue_xy[0], c.primaries_blue_xy[1], 1001 c.white_point_xy[0], c.white_point_xy[1], m)); 1002 Color r{m[0][0], m[1][0], m[2][0]}; 1003 Color g{m[0][1], m[1][1], m[2][1]}; 1004 Color b{m[0][2], m[1][2], m[2][2]}; 1005 1006 JXL_RETURN_IF_ERROR(CreateICCXYZTag(r, &tags)); 1007 FinalizeICCTag(&tags, &tag_offset, &tag_size); 1008 AddToICCTagTable("rXYZ", tag_offset, tag_size, &tagtable, &offsets); 1009 1010 JXL_RETURN_IF_ERROR(CreateICCXYZTag(g, &tags)); 1011 FinalizeICCTag(&tags, &tag_offset, &tag_size); 1012 AddToICCTagTable("gXYZ", tag_offset, tag_size, &tagtable, &offsets); 1013 1014 JXL_RETURN_IF_ERROR(CreateICCXYZTag(b, &tags)); 1015 FinalizeICCTag(&tags, &tag_offset, &tag_size); 1016 AddToICCTagTable("bXYZ", tag_offset, tag_size, &tagtable, &offsets); 1017 } 1018 1019 if (c.color_space == JXL_COLOR_SPACE_XYB) { 1020 JXL_RETURN_IF_ERROR(CreateICCLutAtoBTagForXYB(&tags)); 1021 FinalizeICCTag(&tags, &tag_offset, &tag_size); 1022 AddToICCTagTable("A2B0", tag_offset, tag_size, &tagtable, &offsets); 1023 JXL_RETURN_IF_ERROR(CreateICCNoOpBToATag(&tags)); 1024 FinalizeICCTag(&tags, &tag_offset, &tag_size); 1025 AddToICCTagTable("B2A0", tag_offset, tag_size, &tagtable, &offsets); 1026 } else if (kEnable3DToneMapping && CanToneMap(c)) { 1027 JXL_RETURN_IF_ERROR(CreateICCLutAtoBTagForHDR(c, &tags)); 1028 FinalizeICCTag(&tags, &tag_offset, &tag_size); 1029 AddToICCTagTable("A2B0", tag_offset, tag_size, &tagtable, &offsets); 1030 JXL_RETURN_IF_ERROR(CreateICCNoOpBToATag(&tags)); 1031 FinalizeICCTag(&tags, &tag_offset, &tag_size); 1032 AddToICCTagTable("B2A0", tag_offset, tag_size, &tagtable, &offsets); 1033 } else { 1034 if (tf == JXL_TRANSFER_FUNCTION_GAMMA) { 1035 float gamma = 1.0 / c.gamma; 1036 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag({gamma}, 0, &tags)); 1037 } else if (c.color_space != JXL_COLOR_SPACE_XYB) { 1038 switch (tf) { 1039 case JXL_TRANSFER_FUNCTION_HLG: 1040 CreateICCCurvCurvTag( 1041 CreateTableCurve<64, ExtraTF::kHLG>(CanToneMap(c)), &tags); 1042 break; 1043 case JXL_TRANSFER_FUNCTION_PQ: 1044 CreateICCCurvCurvTag( 1045 CreateTableCurve<64, ExtraTF::kPQ>(CanToneMap(c)), &tags); 1046 break; 1047 case JXL_TRANSFER_FUNCTION_SRGB: 1048 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag( 1049 {2.4, 1.0 / 1.055, 0.055 / 1.055, 1.0 / 12.92, 0.04045}, 3, 1050 &tags)); 1051 break; 1052 case JXL_TRANSFER_FUNCTION_709: 1053 JXL_RETURN_IF_ERROR(CreateICCCurvParaTag( 1054 {1.0 / 0.45, 1.0 / 1.099, 0.099 / 1.099, 1.0 / 4.5, 0.081}, 3, 1055 &tags)); 1056 break; 1057 case JXL_TRANSFER_FUNCTION_LINEAR: 1058 JXL_RETURN_IF_ERROR( 1059 CreateICCCurvParaTag({1.0, 1.0, 0.0, 1.0, 0.0}, 3, &tags)); 1060 break; 1061 case JXL_TRANSFER_FUNCTION_DCI: 1062 JXL_RETURN_IF_ERROR( 1063 CreateICCCurvParaTag({2.6, 1.0, 0.0, 1.0, 0.0}, 3, &tags)); 1064 break; 1065 default: 1066 return JXL_UNREACHABLE("unknown TF %u", 1067 static_cast<unsigned int>(tf)); 1068 } 1069 } 1070 FinalizeICCTag(&tags, &tag_offset, &tag_size); 1071 if (c.color_space == JXL_COLOR_SPACE_GRAY) { 1072 AddToICCTagTable("kTRC", tag_offset, tag_size, &tagtable, &offsets); 1073 } else { 1074 AddToICCTagTable("rTRC", tag_offset, tag_size, &tagtable, &offsets); 1075 AddToICCTagTable("gTRC", tag_offset, tag_size, &tagtable, &offsets); 1076 AddToICCTagTable("bTRC", tag_offset, tag_size, &tagtable, &offsets); 1077 } 1078 } 1079 1080 // Tag count 1081 WriteICCUint32(offsets.size(), 0, &tagtable); 1082 for (size_t i = 0; i < offsets.size(); i++) { 1083 WriteICCUint32(offsets[i] + header.size() + tagtable.size(), 4 + 12 * i + 4, 1084 &tagtable); 1085 } 1086 1087 // ICC profile size 1088 WriteICCUint32(header.size() + tagtable.size() + tags.size(), 0, &header); 1089 1090 *icc = header; 1091 Bytes(tagtable).AppendTo(*icc); 1092 Bytes(tags).AppendTo(*icc); 1093 1094 // The MD5 checksum must be computed on the profile with profile flags, 1095 // rendering intent, and region of the checksum itself, set to 0. 1096 // TODO(lode): manually verify with a reliable tool that this creates correct 1097 // signature (profile id) for ICC profiles. 1098 std::vector<uint8_t> icc_sum = *icc; 1099 if (icc_sum.size() >= 64 + 4) { 1100 memset(icc_sum.data() + 44, 0, 4); 1101 memset(icc_sum.data() + 64, 0, 4); 1102 } 1103 uint8_t checksum[16]; 1104 detail::ICCComputeMD5(icc_sum, checksum); 1105 1106 memcpy(icc->data() + 84, checksum, sizeof(checksum)); 1107 1108 return true; 1109 } 1110 1111 } // namespace detail 1112 1113 // Returns a representation of the ColorEncoding fields (not icc). 1114 // Example description: "RGB_D65_SRG_Rel_Lin" 1115 static JXL_MAYBE_UNUSED std::string ColorEncodingDescription( 1116 const JxlColorEncoding& c) { 1117 return detail::ColorEncodingDescriptionImpl(c); 1118 } 1119 1120 // NOTE: for XYB colorspace, the created profile can be used to transform a 1121 // *scaled* XYB image (created by ScaleXYB()) to another colorspace. 1122 static JXL_MAYBE_UNUSED Status MaybeCreateProfile(const JxlColorEncoding& c, 1123 std::vector<uint8_t>* icc) { 1124 return detail::MaybeCreateProfileImpl(c, icc); 1125 } 1126 1127 } // namespace jxl 1128 1129 #endif // LIB_JXL_CMS_JXL_CMS_INTERNAL_H_