tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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_