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