Coverage Report

Created: 2025-12-03 07:54

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/libjxl/lib/jxl/test_image.cc
Line
Count
Source
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/test_image.h"
7
8
#include <jxl/codestream_header.h>
9
#include <jxl/color_encoding.h>
10
#include <jxl/encode.h>
11
#include <jxl/types.h>
12
13
#include <algorithm>
14
#include <cstddef>
15
#include <cstdint>
16
#include <cstring>
17
#include <string>
18
#include <utility>
19
#include <vector>
20
21
#include "lib/extras/dec/color_description.h"
22
#include "lib/extras/dec/color_hints.h"
23
#include "lib/extras/dec/decode.h"
24
#include "lib/extras/packed_image.h"
25
#include "lib/jxl/base/byte_order.h"
26
#include "lib/jxl/base/random.h"
27
#include "lib/jxl/base/span.h"
28
#include "lib/jxl/base/status.h"
29
#include "lib/jxl/color_encoding_internal.h"
30
#include "lib/jxl/test_utils.h"
31
32
namespace jxl {
33
namespace test {
34
35
namespace {
36
37
void StoreValue(float val, size_t bits_per_sample, JxlPixelFormat format,
38
0
                uint8_t** out) {
39
0
  const float mul = (1u << bits_per_sample) - 1;
40
0
  if (format.data_type == JXL_TYPE_UINT8) {
41
0
    **out = val * mul;
42
0
  } else if (format.data_type == JXL_TYPE_UINT16) {
43
0
    uint16_t uval = val * mul;
44
0
    if (SwapEndianness(format.endianness)) {
45
0
      uval = JXL_BSWAP16(uval);
46
0
    }
47
0
    memcpy(*out, &uval, 2);
48
0
  } else if (format.data_type == JXL_TYPE_FLOAT) {
49
    // TODO(szabadka) Add support for custom bits / exponent bits floats.
50
0
    if (SwapEndianness(format.endianness)) {
51
0
      val = BSwapFloat(val);
52
0
    }
53
0
    memcpy(*out, &val, 4);
54
0
  } else {
55
    // TODO(szabadka) Add support for FLOAT16.
56
0
  }
57
0
  *out += extras::PackedImage::BitsPerChannel(format.data_type) / 8;
58
0
}
59
60
void FillPackedImage(size_t bits_per_sample, uint16_t seed,
61
0
                     extras::PackedImage* image) {
62
0
  const size_t xsize = image->xsize;
63
0
  const size_t ysize = image->ysize;
64
0
  const JxlPixelFormat format = image->format;
65
66
  // Cause more significant image difference for successive seeds.
67
0
  Rng generator(seed);
68
69
  // Returns random integer in interval [0, max_value)
70
0
  auto rngu = [&generator](size_t max_value) -> size_t {
71
0
    return generator.UniformU(0, max_value);
72
0
  };
73
74
  // Returns random float in interval [0.0, max_value)
75
0
  auto rngf = [&generator](float max_value) {
76
0
    return generator.UniformF(0.0f, max_value);
77
0
  };
78
79
  // Dark background gradient color
80
0
  float r0 = rngf(0.5f);
81
0
  float g0 = rngf(0.5f);
82
0
  float b0 = rngf(0.5f);
83
0
  float a0 = rngf(0.5f);
84
0
  float r1 = rngf(0.5f);
85
0
  float g1 = rngf(0.5f);
86
0
  float b1 = rngf(0.5f);
87
0
  float a1 = rngf(0.5f);
88
89
  // Circle with different color
90
0
  size_t circle_x = rngu(xsize);
91
0
  size_t circle_y = rngu(ysize);
92
0
  size_t circle_r = rngu(std::min(xsize, ysize));
93
94
  // Rectangle with random noise
95
0
  size_t rect_x0 = rngu(xsize);
96
0
  size_t rect_y0 = rngu(ysize);
97
0
  size_t rect_x1 = rngu(xsize);
98
0
  size_t rect_y1 = rngu(ysize);
99
0
  if (rect_x1 < rect_x0) std::swap(rect_x0, rect_x1);
100
0
  if (rect_y1 < rect_y0) std::swap(rect_y0, rect_y1);
101
102
  // Create pixel content to test, actual content does not matter as long as it
103
  // can be compared after roundtrip.
104
0
  const float imul16 = 1.0f / 65536.0f;
105
0
  for (size_t y = 0; y < ysize; y++) {
106
0
    uint8_t* out =
107
0
        reinterpret_cast<uint8_t*>(image->pixels()) + y * image->stride;
108
0
    for (size_t x = 0; x < xsize; x++) {
109
0
      float r = r0 * (ysize - y - 1) / ysize + r1 * y / ysize;
110
0
      float g = g0 * (ysize - y - 1) / ysize + g1 * y / ysize;
111
0
      float b = b0 * (ysize - y - 1) / ysize + b1 * y / ysize;
112
0
      float a = a0 * (ysize - y - 1) / ysize + a1 * y / ysize;
113
      // put some shape in there for visual debugging
114
0
      if ((x - circle_x) * (x - circle_x) + (y - circle_y) * (y - circle_y) <
115
0
          circle_r * circle_r) {
116
0
        r = std::min(1.0f, ((65535 - x * y) ^ seed) * imul16);
117
0
        g = std::min(1.0f, ((x << 8) + y + seed) * imul16);
118
0
        b = std::min(1.0f, ((y << 8) + x * seed) * imul16);
119
0
        a = std::min(1.0f, (32768 + x * 256 - y) * imul16);
120
0
      } else if (x > rect_x0 && x < rect_x1 && y > rect_y0 && y < rect_y1) {
121
0
        r = rngf(1.0f);
122
0
        g = rngf(1.0f);
123
0
        b = rngf(1.0f);
124
0
        a = rngf(1.0f);
125
0
      }
126
0
      if (format.num_channels == 1) {
127
0
        StoreValue(g, bits_per_sample, format, &out);
128
0
      } else if (format.num_channels == 2) {
129
0
        StoreValue(g, bits_per_sample, format, &out);
130
0
        StoreValue(a, bits_per_sample, format, &out);
131
0
      } else if (format.num_channels == 3) {
132
0
        StoreValue(r, bits_per_sample, format, &out);
133
0
        StoreValue(g, bits_per_sample, format, &out);
134
0
        StoreValue(b, bits_per_sample, format, &out);
135
0
      } else if (format.num_channels == 4) {
136
0
        StoreValue(r, bits_per_sample, format, &out);
137
0
        StoreValue(g, bits_per_sample, format, &out);
138
0
        StoreValue(b, bits_per_sample, format, &out);
139
0
        StoreValue(a, bits_per_sample, format, &out);
140
0
      }
141
0
    }
142
0
  }
143
0
}
144
145
}  // namespace
146
147
std::vector<uint8_t> GetSomeTestImage(size_t xsize, size_t ysize,
148
224
                                      size_t num_channels, uint16_t seed) {
149
  // Cause more significant image difference for successive seeds.
150
224
  Rng generator(seed);
151
152
  // Returns random integer in interval [0, max_value)
153
3.36k
  auto rng = [&generator](size_t max_value) -> size_t {
154
3.36k
    return generator.UniformU(0, max_value);
155
3.36k
  };
156
157
  // Dark background gradient color
158
224
  uint16_t r0 = rng(32768);
159
224
  uint16_t g0 = rng(32768);
160
224
  uint16_t b0 = rng(32768);
161
224
  uint16_t a0 = rng(32768);
162
224
  uint16_t r1 = rng(32768);
163
224
  uint16_t g1 = rng(32768);
164
224
  uint16_t b1 = rng(32768);
165
224
  uint16_t a1 = rng(32768);
166
167
  // Circle with different color
168
224
  size_t circle_x = rng(xsize);
169
224
  size_t circle_y = rng(ysize);
170
224
  size_t circle_r = rng(std::min(xsize, ysize));
171
172
  // Rectangle with random noise
173
224
  size_t rect_x0 = rng(xsize);
174
224
  size_t rect_y0 = rng(ysize);
175
224
  size_t rect_x1 = rng(xsize);
176
224
  size_t rect_y1 = rng(ysize);
177
224
  if (rect_x1 < rect_x0) std::swap(rect_x0, rect_x1);
178
224
  if (rect_y1 < rect_y0) std::swap(rect_y0, rect_y1);
179
180
224
  size_t num_pixels = xsize * ysize;
181
  // 16 bits per channel, big endian, 4 channels
182
224
  std::vector<uint8_t> pixels(num_pixels * num_channels * 2);
183
  // Create pixel content to test, actual content does not matter as long as it
184
  // can be compared after roundtrip.
185
448
  for (size_t y = 0; y < ysize; y++) {
186
448
    for (size_t x = 0; x < xsize; x++) {
187
224
      uint16_t r = r0 * (ysize - y - 1) / ysize + r1 * y / ysize;
188
224
      uint16_t g = g0 * (ysize - y - 1) / ysize + g1 * y / ysize;
189
224
      uint16_t b = b0 * (ysize - y - 1) / ysize + b1 * y / ysize;
190
224
      uint16_t a = a0 * (ysize - y - 1) / ysize + a1 * y / ysize;
191
      // put some shape in there for visual debugging
192
224
      if ((x - circle_x) * (x - circle_x) + (y - circle_y) * (y - circle_y) <
193
224
          circle_r * circle_r) {
194
0
        r = (65535 - x * y) ^ seed;
195
0
        g = (x << 8) + y + seed;
196
0
        b = (y << 8) + x * seed;
197
0
        a = 32768 + x * 256 - y;
198
224
      } else if (x > rect_x0 && x < rect_x1 && y > rect_y0 && y < rect_y1) {
199
0
        r = rng(65536);
200
0
        g = rng(65536);
201
0
        b = rng(65536);
202
0
        a = rng(65536);
203
0
      }
204
224
      size_t i = (y * xsize + x) * 2 * num_channels;
205
224
      pixels[i + 0] = (r >> 8);
206
224
      pixels[i + 1] = (r & 255);
207
224
      if (num_channels >= 2) {
208
        // This may store what is called 'g' in the alpha channel of a 2-channel
209
        // image, but that's ok since the content is arbitrary
210
224
        pixels[i + 2] = (g >> 8);
211
224
        pixels[i + 3] = (g & 255);
212
224
      }
213
224
      if (num_channels >= 3) {
214
224
        pixels[i + 4] = (b >> 8);
215
224
        pixels[i + 5] = (b & 255);
216
224
      }
217
224
      if (num_channels >= 4) {
218
0
        pixels[i + 6] = (a >> 8);
219
0
        pixels[i + 7] = (a & 255);
220
0
      }
221
224
    }
222
224
  }
223
224
  return pixels;
224
224
}
225
226
0
TestImage::TestImage() {
227
0
  Check(SetChannels(3));
228
0
  SetAllBitDepths(8);
229
0
  Check(SetColorEncoding("RGB_D65_SRG_Rel_SRG"));
230
0
}
231
232
0
Status TestImage::DecodeFromBytes(const std::vector<uint8_t>& bytes) {
233
0
  ColorEncoding c_enc;
234
0
  JXL_RETURN_IF_ERROR(c_enc.FromExternal(ppf_.color_encoding));
235
0
  extras::ColorHints color_hints;
236
0
  color_hints.Add("color_space", Description(c_enc));
237
0
  JXL_RETURN_IF_ERROR(extras::DecodeBytes(Bytes(bytes), color_hints, &ppf_));
238
0
  return true;
239
0
}
240
241
0
TestImage& TestImage::ClearMetadata() {
242
0
  ppf_.metadata = extras::PackedMetadata();
243
0
  return *this;
244
0
}
245
246
0
Status TestImage::SetDimensions(size_t xsize, size_t ysize) {
247
0
  if (xsize <= ppf_.info.xsize && ysize <= ppf_.info.ysize) {
248
0
    for (auto& frame : ppf_.frames) {
249
0
      CropLayerInfo(xsize, ysize, &frame.frame_info.layer_info);
250
0
      CropImage(xsize, ysize, &frame.color);
251
0
      for (auto& ec : frame.extra_channels) {
252
0
        CropImage(xsize, ysize, &ec);
253
0
      }
254
0
    }
255
0
  } else {
256
0
    JXL_ENSURE(ppf_.info.xsize == 0 && ppf_.info.ysize == 0);
257
0
  }
258
0
  ppf_.info.xsize = xsize;
259
0
  ppf_.info.ysize = ysize;
260
0
  return true;
261
0
}
262
263
0
Status TestImage::SetChannels(size_t num_channels) {
264
0
  JXL_ENSURE(ppf_.frames.empty());
265
0
  JXL_ENSURE(!ppf_.preview_frame);
266
0
  ppf_.info.num_color_channels = num_channels < 3 ? 1 : 3;
267
0
  ppf_.info.num_extra_channels = num_channels - ppf_.info.num_color_channels;
268
0
  if (ppf_.info.num_extra_channels > 0 && ppf_.info.alpha_bits == 0) {
269
0
    ppf_.info.alpha_bits = ppf_.info.bits_per_sample;
270
0
    ppf_.info.alpha_exponent_bits = ppf_.info.exponent_bits_per_sample;
271
0
  }
272
0
  ppf_.extra_channels_info.clear();
273
0
  for (size_t i = 1; i < ppf_.info.num_extra_channels; ++i) {
274
0
    extras::PackedExtraChannel ec;
275
0
    ec.index = i;
276
0
    JxlEncoderInitExtraChannelInfo(JXL_CHANNEL_ALPHA, &ec.ec_info);
277
0
    if (ec.ec_info.bits_per_sample == 0) {
278
0
      ec.ec_info.bits_per_sample = ppf_.info.bits_per_sample;
279
0
      ec.ec_info.exponent_bits_per_sample = ppf_.info.exponent_bits_per_sample;
280
0
    }
281
0
    ppf_.extra_channels_info.emplace_back(std::move(ec));
282
0
  }
283
0
  format_.num_channels = std::min(static_cast<size_t>(4), num_channels);
284
0
  if (ppf_.info.num_color_channels == 1 &&
285
0
      ppf_.color_encoding.color_space != JXL_COLOR_SPACE_GRAY) {
286
0
    JXL_RETURN_IF_ERROR(SetColorEncoding("Gra_D65_Rel_SRG"));
287
0
  }
288
0
  return true;
289
0
}
290
291
// Sets the same bit depth on color, alpha and all extra channels.
292
TestImage& TestImage::SetAllBitDepths(uint32_t bits_per_sample,
293
0
                                      uint32_t exponent_bits_per_sample) {
294
0
  ppf_.info.bits_per_sample = bits_per_sample;
295
0
  ppf_.info.exponent_bits_per_sample = exponent_bits_per_sample;
296
0
  if (ppf_.info.num_extra_channels > 0) {
297
0
    ppf_.info.alpha_bits = bits_per_sample;
298
0
    ppf_.info.alpha_exponent_bits = exponent_bits_per_sample;
299
0
  }
300
0
  for (auto& ec : ppf_.extra_channels_info) {
301
0
    ec.ec_info.bits_per_sample = bits_per_sample;
302
0
    ec.ec_info.exponent_bits_per_sample = exponent_bits_per_sample;
303
0
  }
304
0
  format_.data_type = DefaultDataType(ppf_.info);
305
0
  return *this;
306
0
}
307
308
0
TestImage& TestImage::SetDataType(JxlDataType data_type) {
309
0
  format_.data_type = data_type;
310
0
  return *this;
311
0
}
312
313
0
TestImage& TestImage::SetEndianness(JxlEndianness endianness) {
314
0
  format_.endianness = endianness;
315
0
  return *this;
316
0
}
317
318
0
TestImage& TestImage::SetRowAlignment(size_t align) {
319
0
  format_.align = align;
320
0
  return *this;
321
0
}
322
323
0
Status TestImage::SetColorEncoding(const std::string& description) {
324
0
  JXL_RETURN_IF_ERROR(ParseDescription(description, &ppf_.color_encoding));
325
0
  ColorEncoding c_enc;
326
0
  JXL_RETURN_IF_ERROR(c_enc.FromExternal(ppf_.color_encoding));
327
0
  IccBytes icc = c_enc.ICC();
328
0
  JXL_ENSURE(!icc.empty());
329
0
  ppf_.icc.assign(icc.begin(), icc.end());
330
0
  return true;
331
0
}
332
333
0
Status TestImage::CoalesceGIFAnimationWithAlpha() {
334
0
  JXL_ASSIGN_OR_RETURN(extras::PackedFrame canvas, ppf_.frames[0].Copy());
335
0
  JXL_ENSURE(canvas.color.format.num_channels == 3);
336
0
  JXL_ENSURE(canvas.color.format.data_type == JXL_TYPE_UINT8);
337
0
  JXL_ENSURE(canvas.extra_channels.size() == 1);
338
0
  for (size_t i = 1; i < ppf_.frames.size(); i++) {
339
0
    const extras::PackedFrame& frame = ppf_.frames[i];
340
0
    JXL_ENSURE(frame.extra_channels.size() == 1);
341
0
    const JxlLayerInfo& layer_info = frame.frame_info.layer_info;
342
0
    JXL_ASSIGN_OR_RETURN(extras::PackedFrame rendered, canvas.Copy());
343
0
    uint8_t* pixels_rendered =
344
0
        reinterpret_cast<uint8_t*>(rendered.color.pixels());
345
0
    const uint8_t* pixels_frame =
346
0
        reinterpret_cast<const uint8_t*>(frame.color.pixels());
347
0
    uint8_t* alpha_rendered =
348
0
        reinterpret_cast<uint8_t*>(rendered.extra_channels[0].pixels());
349
0
    const uint8_t* alpha_frame =
350
0
        reinterpret_cast<const uint8_t*>(frame.extra_channels[0].pixels());
351
0
    for (size_t y = 0; y < frame.color.ysize; y++) {
352
0
      for (size_t x = 0; x < frame.color.xsize; x++) {
353
0
        size_t idx_frame = y * frame.color.xsize + x;
354
0
        size_t idx_rendered = ((layer_info.crop_y0 + y) * rendered.color.xsize +
355
0
                               (layer_info.crop_x0 + x));
356
0
        if (alpha_frame[idx_frame] != 0) {
357
0
          memcpy(&pixels_rendered[idx_rendered * 3],
358
0
                 &pixels_frame[idx_frame * 3], 3);
359
0
          alpha_rendered[idx_rendered] = alpha_frame[idx_frame];
360
0
        }
361
0
      }
362
0
    }
363
0
    if (layer_info.save_as_reference != 0) {
364
0
      JXL_ASSIGN_OR_RETURN(canvas, rendered.Copy());
365
0
    }
366
0
    ppf_.frames[i] = std::move(rendered);
367
0
  }
368
0
  return true;
369
0
}
370
371
TestImage::Frame::Frame(TestImage* parent, bool is_preview, size_t index)
372
0
    : parent_(parent), is_preview_(is_preview), index_(index) {}
373
374
0
void TestImage::Frame::ZeroFill() {
375
0
  memset(frame().color.pixels(), 0, frame().color.pixels_size);
376
0
  for (auto& ec : frame().extra_channels) {
377
0
    memset(ec.pixels(), 0, ec.pixels_size);
378
0
  }
379
0
}
380
381
0
void TestImage::Frame::RandomFill(uint16_t seed) {
382
0
  FillPackedImage(ppf().info.bits_per_sample, seed, &frame().color);
383
0
  for (size_t i = 0; i < ppf().extra_channels_info.size(); ++i) {
384
0
    FillPackedImage(ppf().extra_channels_info[i].ec_info.bits_per_sample,
385
0
                    seed + 1 + i, &frame().extra_channels[i]);
386
0
  }
387
0
}
388
389
0
Status TestImage::Frame::SetValue(size_t y, size_t x, size_t c, float val) {
390
0
  const extras::PackedImage& color = frame().color;
391
0
  JxlPixelFormat format = color.format;
392
0
  JXL_ENSURE(y < ppf().info.ysize);
393
0
  JXL_ENSURE(x < ppf().info.xsize);
394
0
  JXL_ENSURE(c < format.num_channels);
395
0
  size_t pwidth = extras::PackedImage::BitsPerChannel(format.data_type) / 8;
396
0
  size_t idx = ((y * color.xsize + x) * format.num_channels + c) * pwidth;
397
0
  uint8_t* pixels = reinterpret_cast<uint8_t*>(frame().color.pixels());
398
0
  uint8_t* p = pixels + idx;
399
0
  StoreValue(val, ppf().info.bits_per_sample, frame().color.format, &p);
400
0
  return true;
401
0
}
402
403
0
StatusOr<TestImage::Frame> TestImage::AddFrame() {
404
0
  size_t index = ppf_.frames.size();
405
0
  JXL_ASSIGN_OR_RETURN(
406
0
      extras::PackedFrame frame,
407
0
      extras::PackedFrame::Create(ppf_.info.xsize, ppf_.info.ysize, format_));
408
0
  for (size_t i = 0; i < ppf_.extra_channels_info.size(); ++i) {
409
0
    JxlPixelFormat ec_format = {1, format_.data_type, format_.endianness, 0};
410
0
    JXL_ASSIGN_OR_RETURN(extras::PackedImage image,
411
0
                         extras::PackedImage::Create(
412
0
                             ppf_.info.xsize, ppf_.info.ysize, ec_format));
413
0
    frame.extra_channels.emplace_back(std::move(image));
414
0
  }
415
0
  ppf_.frames.emplace_back(std::move(frame));
416
0
  return Frame(this, false, index);
417
0
}
418
419
0
void TestImage::CropLayerInfo(size_t xsize, size_t ysize, JxlLayerInfo* info) {
420
0
  if (info->crop_x0 < static_cast<ptrdiff_t>(xsize)) {
421
0
    info->xsize = std::min<size_t>(info->xsize, xsize - info->crop_x0);
422
0
  } else {
423
0
    info->xsize = 0;
424
0
  }
425
0
  if (info->crop_y0 < static_cast<ptrdiff_t>(ysize)) {
426
0
    info->ysize = std::min<size_t>(info->ysize, ysize - info->crop_y0);
427
0
  } else {
428
0
    info->ysize = 0;
429
0
  }
430
0
}
431
432
void TestImage::CropImage(size_t xsize, size_t ysize,
433
0
                          extras::PackedImage* image) {
434
0
  size_t new_stride = (image->stride / image->xsize) * xsize;
435
0
  uint8_t* buf = reinterpret_cast<uint8_t*>(image->pixels());
436
0
  for (size_t y = 0; y < ysize; ++y) {
437
0
    memmove(&buf[y * new_stride], &buf[y * image->stride], new_stride);
438
0
  }
439
0
  image->xsize = xsize;
440
0
  image->ysize = ysize;
441
0
  image->stride = new_stride;
442
0
  image->pixels_size = ysize * new_stride;
443
0
}
444
445
0
JxlDataType TestImage::DefaultDataType(const JxlBasicInfo& info) {
446
0
  if (info.bits_per_sample == 16 && info.exponent_bits_per_sample == 5) {
447
0
    return JXL_TYPE_FLOAT16;
448
0
  } else if (info.exponent_bits_per_sample > 0 || info.bits_per_sample > 16) {
449
0
    return JXL_TYPE_FLOAT;
450
0
  } else if (info.bits_per_sample > 8) {
451
0
    return JXL_TYPE_UINT16;
452
0
  } else {
453
0
    return JXL_TYPE_UINT8;
454
0
  }
455
0
}
456
457
}  // namespace test
458
}  // namespace jxl