libjxl

FORK: libjxl patches used on blog
git clone https://git.neptards.moe/blog/libjxl.git
Log | Files | Refs | Submodules | README | LICENSE

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_