Coverage Report

Created: 2025-06-22 08:04

/src/libjxl/lib/jxl/enc_icc_codec.cc
Line
Count
Source (jump to first uncovered line)
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
#include "lib/jxl/enc_icc_codec.h"
7
8
#include <jxl/memory_manager.h>
9
10
#include <cstdint>
11
#include <limits>
12
#include <map>
13
#include <vector>
14
15
#include "lib/jxl/base/status.h"
16
#include "lib/jxl/enc_ans.h"
17
#include "lib/jxl/enc_aux_out.h"
18
#include "lib/jxl/fields.h"
19
#include "lib/jxl/icc_codec_common.h"
20
#include "lib/jxl/padded_bytes.h"
21
22
namespace jxl {
23
namespace {
24
25
// Unshuffles or de-interleaves bytes, for example with width 2, turns
26
// "AaBbCcDc" into "ABCDabcd", this for example de-interleaves UTF-16 bytes into
27
// first all the high order bytes, then all the low order bytes.
28
// Transposes a matrix of width columns and ceil(size / width) rows. There are
29
// size elements, size may be < width * height, if so the
30
// last elements of the bottom row are missing, the missing spots are
31
// transposed along with the filled spots, and the result has the missing
32
// elements at the bottom of the rightmost column. The input is the input matrix
33
// in scanline order, the output is the result matrix in scanline order, with
34
// missing elements skipped over (this may occur at multiple positions).
35
Status Unshuffle(JxlMemoryManager* memory_manager, uint8_t* data, size_t size,
36
0
                 size_t width) {
37
0
  size_t height = (size + width - 1) / width;  // amount of rows of input
38
0
  PaddedBytes result(memory_manager);
39
0
  JXL_ASSIGN_OR_RETURN(result,
40
0
                       PaddedBytes::WithInitialSpace(memory_manager, size));
41
42
  // i = input index, j output index
43
0
  size_t s = 0;
44
0
  size_t j = 0;
45
0
  for (size_t i = 0; i < size; i++) {
46
0
    result[j] = data[i];
47
0
    j += height;
48
0
    if (j >= size) j = ++s;
49
0
  }
50
51
0
  for (size_t i = 0; i < size; i++) {
52
0
    data[i] = result[i];
53
0
  }
54
0
  return true;
55
0
}
56
57
// This is performed by the encoder, the encoder must be able to encode any
58
// random byte stream (not just byte streams that are a valid ICC profile), so
59
// an error returned by this function is an implementation error.
60
Status PredictAndShuffle(size_t stride, size_t width, int order, size_t num,
61
                         const uint8_t* data, size_t size, size_t* pos,
62
0
                         PaddedBytes* result) {
63
0
  JXL_RETURN_IF_ERROR(CheckOutOfBounds(*pos, num, size));
64
0
  JxlMemoryManager* memory_manager = result->memory_manager();
65
  // Required by the specification, see decoder. stride * 4 must be < *pos.
66
0
  if (!*pos || ((*pos - 1u) >> 2u) < stride) {
67
0
    return JXL_FAILURE("Invalid stride");
68
0
  }
69
0
  if (*pos < stride * 4) return JXL_FAILURE("Too large stride");
70
0
  size_t start = result->size();
71
0
  for (size_t i = 0; i < num; i++) {
72
0
    uint8_t predicted =
73
0
        LinearPredictICCValue(data, *pos, i, stride, width, order);
74
0
    JXL_RETURN_IF_ERROR(result->push_back(data[*pos + i] - predicted));
75
0
  }
76
0
  *pos += num;
77
0
  if (width > 1) {
78
0
    JXL_RETURN_IF_ERROR(
79
0
        Unshuffle(memory_manager, result->data() + start, num, width));
80
0
  }
81
0
  return true;
82
0
}
83
84
0
inline Status EncodeVarInt(uint64_t value, PaddedBytes* data) {
85
0
  size_t pos = data->size();
86
0
  JXL_RETURN_IF_ERROR(data->resize(data->size() + 9));
87
0
  size_t output_size = data->size();
88
0
  uint8_t* output = data->data();
89
90
  // While more than 7 bits of data are left,
91
  // store 7 bits and set the next byte flag
92
0
  while (value > 127) {
93
    // TODO(eustas): should it be `<` ?
94
0
    JXL_ENSURE(pos <= output_size);
95
    // |128: Set the next byte flag
96
0
    output[pos++] = (static_cast<uint8_t>(value & 127)) | 128;
97
    // Remove the seven bits we just wrote
98
0
    value >>= 7;
99
0
  }
100
  // TODO(eustas): should it be `<` ?
101
0
  JXL_ENSURE(pos <= output_size);
102
0
  output[pos++] = static_cast<uint8_t>(value & 127);
103
104
0
  return data->resize(pos);
105
0
}
106
107
constexpr size_t kSizeLimit = std::numeric_limits<uint32_t>::max() >> 2;
108
109
}  // namespace
110
111
// Outputs a transformed form of the given icc profile. The result itself is
112
// not particularly smaller than the input data in bytes, but it will be in a
113
// form that is easier to compress (more zeroes, ...) and will compress better
114
// with brotli.
115
0
Status PredictICC(const uint8_t* icc, size_t size, PaddedBytes* result) {
116
0
  JxlMemoryManager* memory_manager = result->memory_manager();
117
0
  PaddedBytes commands{memory_manager};
118
0
  PaddedBytes data{memory_manager};
119
120
0
  static_assert(sizeof(size_t) >= 4, "size_t is too short");
121
  // Fuzzer expects that PredictICC can accept any input,
122
  // but 1GB should be enough for any purpose.
123
0
  if (size > kSizeLimit) {
124
0
    return JXL_FAILURE("ICC profile is too large");
125
0
  }
126
127
0
  JXL_RETURN_IF_ERROR(EncodeVarInt(size, result));
128
129
  // Header
130
0
  PaddedBytes header{memory_manager};
131
0
  JXL_RETURN_IF_ERROR(header.append(ICCInitialHeaderPrediction(size)));
132
0
  for (size_t i = 0; i < kICCHeaderSize && i < size; i++) {
133
0
    ICCPredictHeader(icc, size, header.data(), i);
134
0
    JXL_RETURN_IF_ERROR(data.push_back(icc[i] - header[i]));
135
0
  }
136
0
  if (size <= kICCHeaderSize) {
137
0
    JXL_RETURN_IF_ERROR(EncodeVarInt(0, result));  // 0 commands
138
0
    for (uint8_t b : data) {
139
0
      JXL_RETURN_IF_ERROR(result->push_back(b));
140
0
    }
141
0
    return true;
142
0
  }
143
144
0
  std::vector<Tag> tags;
145
0
  std::vector<size_t> tagstarts;
146
0
  std::vector<size_t> tagsizes;
147
0
  std::map<size_t, size_t> tagmap;
148
149
  // Tag list
150
0
  size_t pos = kICCHeaderSize;
151
0
  if (pos + 4 <= size) {
152
0
    uint64_t numtags = DecodeUint32(icc, size, pos);
153
0
    pos += 4;
154
0
    JXL_RETURN_IF_ERROR(EncodeVarInt(numtags + 1, &commands));
155
0
    uint64_t prevtagstart = kICCHeaderSize + numtags * 12;
156
0
    uint32_t prevtagsize = 0;
157
0
    for (size_t i = 0; i < numtags; i++) {
158
0
      if (pos + 12 > size) break;
159
160
0
      Tag tag = DecodeKeyword(icc, size, pos + 0);
161
0
      uint32_t tagstart = DecodeUint32(icc, size, pos + 4);
162
0
      uint32_t tagsize = DecodeUint32(icc, size, pos + 8);
163
0
      pos += 12;
164
165
0
      tags.push_back(tag);
166
0
      tagstarts.push_back(tagstart);
167
0
      tagsizes.push_back(tagsize);
168
0
      tagmap[tagstart] = tags.size() - 1;
169
170
0
      uint8_t tagcode = kCommandTagUnknown;
171
0
      for (size_t j = 0; j < kNumTagStrings; j++) {
172
0
        if (tag == *kTagStrings[j]) {
173
0
          tagcode = j + kCommandTagStringFirst;
174
0
          break;
175
0
        }
176
0
      }
177
178
0
      if (tag == kRtrcTag && pos + 24 < size) {
179
0
        bool ok = true;
180
0
        ok &= DecodeKeyword(icc, size, pos + 0) == kGtrcTag;
181
0
        ok &= DecodeKeyword(icc, size, pos + 12) == kBtrcTag;
182
0
        if (ok) {
183
0
          for (size_t kk = 0; kk < 8; kk++) {
184
0
            if (icc[pos - 8 + kk] != icc[pos + 4 + kk]) ok = false;
185
0
            if (icc[pos - 8 + kk] != icc[pos + 16 + kk]) ok = false;
186
0
          }
187
0
        }
188
0
        if (ok) {
189
0
          tagcode = kCommandTagTRC;
190
0
          pos += 24;
191
0
          i += 2;
192
0
        }
193
0
      }
194
195
0
      if (tag == kRxyzTag && pos + 24 < size) {
196
0
        bool ok = true;
197
0
        ok &= DecodeKeyword(icc, size, pos + 0) == kGxyzTag;
198
0
        ok &= DecodeKeyword(icc, size, pos + 12) == kBxyzTag;
199
0
        uint32_t offsetr = tagstart;
200
0
        uint32_t offsetg = DecodeUint32(icc, size, pos + 4);
201
0
        uint32_t offsetb = DecodeUint32(icc, size, pos + 16);
202
0
        uint32_t sizer = tagsize;
203
0
        uint32_t sizeg = DecodeUint32(icc, size, pos + 8);
204
0
        uint32_t sizeb = DecodeUint32(icc, size, pos + 20);
205
0
        ok &= sizer == 20;
206
0
        ok &= sizeg == 20;
207
0
        ok &= sizeb == 20;
208
0
        ok &= (offsetg == offsetr + 20);
209
0
        ok &= (offsetb == offsetr + 40);
210
0
        if (ok) {
211
0
          tagcode = kCommandTagXYZ;
212
0
          pos += 24;
213
0
          i += 2;
214
0
        }
215
0
      }
216
217
0
      uint8_t command = tagcode;
218
0
      uint64_t predicted_tagstart = prevtagstart + prevtagsize;
219
0
      if (predicted_tagstart != tagstart) command |= kFlagBitOffset;
220
0
      size_t predicted_tagsize = prevtagsize;
221
0
      if (tag == kRxyzTag || tag == kGxyzTag || tag == kBxyzTag ||
222
0
          tag == kKxyzTag || tag == kWtptTag || tag == kBkptTag ||
223
0
          tag == kLumiTag) {
224
0
        predicted_tagsize = 20;
225
0
      }
226
0
      if (predicted_tagsize != tagsize) command |= kFlagBitSize;
227
0
      JXL_RETURN_IF_ERROR(commands.push_back(command));
228
0
      if (tagcode == 1) {
229
0
        JXL_RETURN_IF_ERROR(AppendKeyword(tag, &data));
230
0
      }
231
0
      if (command & kFlagBitOffset)
232
0
        JXL_RETURN_IF_ERROR(EncodeVarInt(tagstart, &commands));
233
0
      if (command & kFlagBitSize)
234
0
        JXL_RETURN_IF_ERROR(EncodeVarInt(tagsize, &commands));
235
236
0
      prevtagstart = tagstart;
237
0
      prevtagsize = tagsize;
238
0
    }
239
0
  }
240
  // Indicate end of tag list or varint indicating there's none
241
0
  JXL_RETURN_IF_ERROR(commands.push_back(0));
242
243
  // Main content
244
  // The main content in a valid ICC profile contains tagged elements, with the
245
  // tag types (4 letter names) given by the tag list above, and the tag list
246
  // pointing to the start and indicating the size of each tagged element. It is
247
  // allowed for tagged elements to overlap, e.g. the curve for R, G and B could
248
  // all point to the same one.
249
0
  Tag tag;
250
0
  size_t tagstart = 0;
251
0
  size_t tagsize = 0;
252
0
  size_t clutstart = 0;
253
254
  // Should always check tag_sane before doing math with tagsize.
255
0
  const auto tag_sane = [&tagsize]() {
256
0
    return (tagsize > 8) && (tagsize < kSizeLimit);
257
0
  };
258
259
0
  size_t last0 = pos;
260
  // This loop appends commands to the output, processing some sub-section of a
261
  // current tagged element each time. We need to keep track of the tagtype of
262
  // the current element, and update it when we encounter the boundary of a
263
  // next one.
264
  // It is not required that the input data is a valid ICC profile, if the
265
  // encoder does not recognize the data it will still be able to output bytes
266
  // but will not predict as well.
267
0
  while (pos <= size) {
268
0
    size_t last1 = pos;
269
0
    PaddedBytes commands_add{memory_manager};
270
0
    PaddedBytes data_add{memory_manager};
271
272
    // This means the loop brought the position beyond the tag end.
273
    // If tagsize is nonsensical, any pos looks "ok-ish".
274
0
    if ((pos > tagstart + tagsize) && (tagsize < kSizeLimit)) {
275
0
      tag = {{0, 0, 0, 0}};  // nonsensical value
276
0
    }
277
278
0
    if (commands_add.empty() && data_add.empty() && tagmap.count(pos) &&
279
0
        pos + 4 <= size) {
280
0
      size_t index = tagmap[pos];
281
0
      tag = DecodeKeyword(icc, size, pos);
282
0
      tagstart = tagstarts[index];
283
0
      tagsize = tagsizes[index];
284
285
0
      if (tag == kMlucTag && tag_sane() && pos + tagsize <= size &&
286
0
          icc[pos + 4] == 0 && icc[pos + 5] == 0 && icc[pos + 6] == 0 &&
287
0
          icc[pos + 7] == 0) {
288
0
        size_t num = tagsize - 8;
289
0
        JXL_RETURN_IF_ERROR(commands_add.push_back(kCommandTypeStartFirst + 3));
290
0
        pos += 8;
291
0
        JXL_RETURN_IF_ERROR(commands_add.push_back(kCommandShuffle2));
292
0
        JXL_RETURN_IF_ERROR(EncodeVarInt(num, &commands_add));
293
0
        size_t start = data_add.size();
294
0
        for (size_t i = 0; i < num; i++) {
295
0
          JXL_RETURN_IF_ERROR(data_add.push_back(icc[pos]));
296
0
          pos++;
297
0
        }
298
0
        JXL_RETURN_IF_ERROR(
299
0
            Unshuffle(memory_manager, data_add.data() + start, num, 2));
300
0
      }
301
302
0
      if (tag == kCurvTag && tag_sane() && pos + tagsize <= size &&
303
0
          icc[pos + 4] == 0 && icc[pos + 5] == 0 && icc[pos + 6] == 0 &&
304
0
          icc[pos + 7] == 0) {
305
0
        size_t num = tagsize - 8;
306
0
        if (num > 16 && num < (1 << 28) && pos + num <= size && pos > 0) {
307
0
          JXL_RETURN_IF_ERROR(
308
0
              commands_add.push_back(kCommandTypeStartFirst + 5));
309
0
          pos += 8;
310
0
          JXL_RETURN_IF_ERROR(commands_add.push_back(kCommandPredict));
311
0
          int order = 1;
312
0
          int width = 2;
313
0
          int stride = width;
314
0
          JXL_RETURN_IF_ERROR(
315
0
              commands_add.push_back((order << 2) | (width - 1)));
316
0
          JXL_RETURN_IF_ERROR(EncodeVarInt(num, &commands_add));
317
0
          JXL_RETURN_IF_ERROR(PredictAndShuffle(stride, width, order, num, icc,
318
0
                                                size, &pos, &data_add));
319
0
        }
320
0
      }
321
0
    }
322
323
0
    if (tag == kMab_Tag || tag == kMba_Tag) {
324
0
      Tag subTag = DecodeKeyword(icc, size, pos);
325
0
      if (pos + 12 < size && (subTag == kCurvTag || subTag == kVcgtTag) &&
326
0
          DecodeUint32(icc, size, pos + 4) == 0) {
327
0
        uint32_t num = DecodeUint32(icc, size, pos + 8) * 2;
328
0
        if (num > 16 && num < (1 << 28) && pos + 12 + num <= size) {
329
0
          pos += 12;
330
0
          last1 = pos;
331
0
          JXL_RETURN_IF_ERROR(commands_add.push_back(kCommandPredict));
332
0
          int order = 1;
333
0
          int width = 2;
334
0
          int stride = width;
335
0
          JXL_RETURN_IF_ERROR(
336
0
              commands_add.push_back((order << 2) | (width - 1)));
337
0
          JXL_RETURN_IF_ERROR(EncodeVarInt(num, &commands_add));
338
0
          JXL_RETURN_IF_ERROR(PredictAndShuffle(stride, width, order, num, icc,
339
0
                                                size, &pos, &data_add));
340
0
        }
341
0
      }
342
343
0
      if (pos == tagstart + 24 && pos + 4 < size) {
344
        // Note that this value can be remembered for next iterations of the
345
        // loop, so the "pos == clutstart" if below can trigger during a later
346
        // iteration.
347
0
        clutstart = tagstart + DecodeUint32(icc, size, pos);
348
0
      }
349
350
0
      if (pos == clutstart && clutstart + 16 < size) {
351
0
        size_t numi = icc[tagstart + 8];
352
0
        size_t numo = icc[tagstart + 9];
353
0
        size_t width = icc[clutstart + 16];
354
0
        size_t stride = width * numo;
355
0
        size_t num = width * numo;
356
0
        for (size_t i = 0; i < numi && clutstart + i < size; i++) {
357
0
          num *= icc[clutstart + i];
358
0
        }
359
0
        if ((width == 1 || width == 2) && num > 64 && num < (1 << 28) &&
360
0
            pos + num <= size && pos > stride * 4) {
361
0
          JXL_RETURN_IF_ERROR(commands_add.push_back(kCommandPredict));
362
0
          int order = 1;
363
0
          uint8_t flags =
364
0
              (order << 2) | (width - 1) | (stride == width ? 0 : 16);
365
0
          JXL_RETURN_IF_ERROR(commands_add.push_back(flags));
366
0
          if (flags & 16) {
367
0
            JXL_RETURN_IF_ERROR(EncodeVarInt(stride, &commands_add));
368
0
          }
369
0
          JXL_RETURN_IF_ERROR(EncodeVarInt(num, &commands_add));
370
0
          JXL_RETURN_IF_ERROR(PredictAndShuffle(stride, width, order, num, icc,
371
0
                                                size, &pos, &data_add));
372
0
        }
373
0
      }
374
0
    }
375
376
0
    if (commands_add.empty() && data_add.empty() && tag == kGbd_Tag &&
377
0
        tag_sane() && pos == tagstart + 8 && pos + tagsize - 8 <= size &&
378
0
        pos > 16) {
379
0
      size_t width = 4;
380
0
      size_t order = 0;
381
0
      size_t stride = width;
382
0
      size_t num = tagsize - 8;
383
0
      uint8_t flags = (order << 2) | (width - 1) | (stride == width ? 0 : 16);
384
0
      JXL_RETURN_IF_ERROR(commands_add.push_back(kCommandPredict));
385
0
      JXL_RETURN_IF_ERROR(commands_add.push_back(flags));
386
0
      if (flags & 16) {
387
0
        JXL_RETURN_IF_ERROR(EncodeVarInt(stride, &commands_add));
388
0
      }
389
0
      JXL_RETURN_IF_ERROR(EncodeVarInt(num, &commands_add));
390
0
      JXL_RETURN_IF_ERROR(PredictAndShuffle(stride, width, order, num, icc,
391
0
                                            size, &pos, &data_add));
392
0
    }
393
394
0
    if (commands_add.empty() && data_add.empty() && pos + 20 <= size) {
395
0
      Tag subTag = DecodeKeyword(icc, size, pos);
396
0
      if (subTag == kXyz_Tag && DecodeUint32(icc, size, pos + 4) == 0) {
397
0
        JXL_RETURN_IF_ERROR(commands_add.push_back(kCommandXYZ));
398
0
        pos += 8;
399
0
        for (size_t j = 0; j < 12; j++) {
400
0
          JXL_RETURN_IF_ERROR(data_add.push_back(icc[pos++]));
401
0
        }
402
0
      }
403
0
    }
404
405
0
    if (commands_add.empty() && data_add.empty() && pos + 8 <= size) {
406
0
      if (DecodeUint32(icc, size, pos + 4) == 0) {
407
0
        Tag subTag = DecodeKeyword(icc, size, pos);
408
0
        for (size_t i = 0; i < kNumTypeStrings; i++) {
409
0
          if (subTag == *kTypeStrings[i]) {
410
0
            JXL_RETURN_IF_ERROR(
411
0
                commands_add.push_back(kCommandTypeStartFirst + i));
412
0
            pos += 8;
413
0
            break;
414
0
          }
415
0
        }
416
0
      }
417
0
    }
418
419
0
    if (!(commands_add.empty() && data_add.empty()) || pos == size) {
420
0
      if (last0 < last1) {
421
0
        JXL_RETURN_IF_ERROR(commands.push_back(kCommandInsert));
422
0
        JXL_RETURN_IF_ERROR(EncodeVarInt(last1 - last0, &commands));
423
0
        while (last0 < last1) {
424
0
          JXL_RETURN_IF_ERROR(data.push_back(icc[last0++]));
425
0
        }
426
0
      }
427
0
      for (uint8_t b : commands_add) {
428
0
        JXL_RETURN_IF_ERROR(commands.push_back(b));
429
0
      }
430
0
      for (uint8_t b : data_add) {
431
0
        JXL_RETURN_IF_ERROR(data.push_back(b));
432
0
      }
433
0
      last0 = pos;
434
0
    }
435
0
    if (commands_add.empty() && data_add.empty()) {
436
0
      pos++;
437
0
    }
438
0
  }
439
440
0
  JXL_RETURN_IF_ERROR(EncodeVarInt(commands.size(), result));
441
0
  for (uint8_t b : commands) {
442
0
    JXL_RETURN_IF_ERROR(result->push_back(b));
443
0
  }
444
0
  for (uint8_t b : data) {
445
0
    JXL_RETURN_IF_ERROR(result->push_back(b));
446
0
  }
447
448
0
  return true;
449
0
}
450
451
Status WriteICC(const Span<const uint8_t> icc, BitWriter* JXL_RESTRICT writer,
452
0
                LayerType layer, AuxOut* JXL_RESTRICT aux_out) {
453
0
  if (icc.empty()) return JXL_FAILURE("ICC must be non-empty");
454
0
  JxlMemoryManager* memory_manager = writer->memory_manager();
455
0
  PaddedBytes enc{memory_manager};
456
0
  JXL_RETURN_IF_ERROR(PredictICC(icc.data(), icc.size(), &enc));
457
0
  std::vector<std::vector<Token>> tokens(1);
458
0
  JXL_RETURN_IF_ERROR(writer->WithMaxBits(128, layer, aux_out, [&] {
459
0
    return U64Coder::Write(enc.size(), writer);
460
0
  }));
461
462
0
  for (size_t i = 0; i < enc.size(); i++) {
463
0
    tokens[0].emplace_back(
464
0
        ICCANSContext(i, i > 0 ? enc[i - 1] : 0, i > 1 ? enc[i - 2] : 0),
465
0
        enc[i]);
466
0
  }
467
0
  HistogramParams params;
468
0
  params.lz77_method = enc.size() < 4096 ? HistogramParams::LZ77Method::kOptimal
469
0
                                         : HistogramParams::LZ77Method::kLZ77;
470
0
  EntropyEncodingData code;
471
0
  std::vector<uint8_t> context_map;
472
0
  params.force_huffman = true;
473
0
  JXL_ASSIGN_OR_RETURN(
474
0
      size_t cost,
475
0
      BuildAndEncodeHistograms(memory_manager, params, kNumICCContexts, tokens,
476
0
                               &code, &context_map, writer, layer, aux_out));
477
0
  (void)cost;
478
0
  JXL_RETURN_IF_ERROR(
479
0
      WriteTokens(tokens[0], code, context_map, 0, writer, layer, aux_out));
480
0
  return true;
481
0
}
482
483
}  // namespace jxl