/src/libjxl/lib/jxl/test_utils.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/test_utils.h" |
7 | | |
8 | | #include <jxl/cms.h> |
9 | | #include <jxl/cms_interface.h> |
10 | | #include <jxl/memory_manager.h> |
11 | | #include <jxl/types.h> |
12 | | |
13 | | #include <cstddef> |
14 | | #include <fstream> |
15 | | #include <memory> |
16 | | #include <string> |
17 | | #include <utility> |
18 | | #include <vector> |
19 | | |
20 | | #include "lib/extras/metrics.h" |
21 | | #include "lib/extras/packed_image_convert.h" |
22 | | #include "lib/jxl/base/compiler_specific.h" |
23 | | #include "lib/jxl/base/data_parallel.h" |
24 | | #include "lib/jxl/base/float.h" |
25 | | #include "lib/jxl/base/printf_macros.h" |
26 | | #include "lib/jxl/base/status.h" |
27 | | #include "lib/jxl/codec_in_out.h" |
28 | | #include "lib/jxl/enc_aux_out.h" |
29 | | #include "lib/jxl/enc_bit_writer.h" |
30 | | #include "lib/jxl/enc_butteraugli_comparator.h" |
31 | | #include "lib/jxl/enc_cache.h" |
32 | | #include "lib/jxl/enc_external_image.h" |
33 | | #include "lib/jxl/enc_fields.h" |
34 | | #include "lib/jxl/enc_frame.h" |
35 | | #include "lib/jxl/enc_icc_codec.h" |
36 | | #include "lib/jxl/enc_params.h" |
37 | | #include "lib/jxl/frame_header.h" |
38 | | #include "lib/jxl/icc_codec.h" |
39 | | #include "lib/jxl/image.h" |
40 | | #include "lib/jxl/image_bundle.h" |
41 | | #include "lib/jxl/padded_bytes.h" |
42 | | |
43 | | #if !defined(TEST_DATA_PATH) |
44 | | #include "tools/cpp/runfiles/runfiles.h" |
45 | | #endif |
46 | | |
47 | | namespace jxl { |
48 | | namespace test { |
49 | | |
50 | | #if defined(TEST_DATA_PATH) |
51 | 0 | std::string GetTestDataPath(const std::string& filename) { |
52 | 0 | return std::string(TEST_DATA_PATH "/") + filename; |
53 | 0 | } |
54 | | #else |
55 | | using bazel::tools::cpp::runfiles::Runfiles; |
56 | | const std::unique_ptr<Runfiles> kRunfiles(Runfiles::Create("")); |
57 | | std::string GetTestDataPath(const std::string& filename) { |
58 | | std::string root(JPEGXL_ROOT_PACKAGE "/testdata/"); |
59 | | return kRunfiles->Rlocation(root + filename); |
60 | | } |
61 | | #endif |
62 | | |
63 | 0 | std::vector<uint8_t> ReadTestData(const std::string& filename) { |
64 | 0 | std::string full_path = GetTestDataPath(filename); |
65 | 0 | fprintf(stderr, "ReadTestData %s\n", full_path.c_str()); |
66 | 0 | std::ifstream file(full_path, std::ios::binary); |
67 | 0 | std::vector<char> str((std::istreambuf_iterator<char>(file)), |
68 | 0 | std::istreambuf_iterator<char>()); |
69 | 0 | JXL_CHECK(file.good()); |
70 | 0 | const uint8_t* raw = reinterpret_cast<const uint8_t*>(str.data()); |
71 | 0 | std::vector<uint8_t> data(raw, raw + str.size()); |
72 | 0 | printf("Test data %s is %d bytes long.\n", filename.c_str(), |
73 | 0 | static_cast<int>(data.size())); |
74 | 0 | return data; |
75 | 0 | } |
76 | | |
77 | 0 | void DefaultAcceptedFormats(extras::JXLDecompressParams& dparams) { |
78 | 0 | if (dparams.accepted_formats.empty()) { |
79 | 0 | for (const uint32_t num_channels : {1, 2, 3, 4}) { |
80 | 0 | dparams.accepted_formats.push_back( |
81 | 0 | {num_channels, JXL_TYPE_FLOAT, JXL_LITTLE_ENDIAN, /*align=*/0}); |
82 | 0 | } |
83 | 0 | } |
84 | 0 | } |
85 | | |
86 | | Status DecodeFile(extras::JXLDecompressParams dparams, |
87 | | const Span<const uint8_t> file, CodecInOut* JXL_RESTRICT io, |
88 | 0 | ThreadPool* pool) { |
89 | 0 | DefaultAcceptedFormats(dparams); |
90 | 0 | SetThreadParallelRunner(dparams, pool); |
91 | 0 | extras::PackedPixelFile ppf; |
92 | 0 | JXL_RETURN_IF_ERROR(DecodeImageJXL(file.data(), file.size(), dparams, |
93 | 0 | /*decoded_bytes=*/nullptr, &ppf)); |
94 | 0 | JXL_RETURN_IF_ERROR(ConvertPackedPixelFileToCodecInOut(ppf, pool, io)); |
95 | 0 | return true; |
96 | 0 | } |
97 | | |
98 | | void JxlBasicInfoSetFromPixelFormat(JxlBasicInfo* basic_info, |
99 | 0 | const JxlPixelFormat* pixel_format) { |
100 | 0 | JxlEncoderInitBasicInfo(basic_info); |
101 | 0 | switch (pixel_format->data_type) { |
102 | 0 | case JXL_TYPE_FLOAT: |
103 | 0 | basic_info->bits_per_sample = 32; |
104 | 0 | basic_info->exponent_bits_per_sample = 8; |
105 | 0 | break; |
106 | 0 | case JXL_TYPE_FLOAT16: |
107 | 0 | basic_info->bits_per_sample = 16; |
108 | 0 | basic_info->exponent_bits_per_sample = 5; |
109 | 0 | break; |
110 | 0 | case JXL_TYPE_UINT8: |
111 | 0 | basic_info->bits_per_sample = 8; |
112 | 0 | basic_info->exponent_bits_per_sample = 0; |
113 | 0 | break; |
114 | 0 | case JXL_TYPE_UINT16: |
115 | 0 | basic_info->bits_per_sample = 16; |
116 | 0 | basic_info->exponent_bits_per_sample = 0; |
117 | 0 | break; |
118 | 0 | default: |
119 | 0 | JXL_ABORT("Unhandled JxlDataType"); |
120 | 0 | } |
121 | 0 | if (pixel_format->num_channels < 3) { |
122 | 0 | basic_info->num_color_channels = 1; |
123 | 0 | } else { |
124 | 0 | basic_info->num_color_channels = 3; |
125 | 0 | } |
126 | 0 | if (pixel_format->num_channels == 2 || pixel_format->num_channels == 4) { |
127 | 0 | basic_info->alpha_exponent_bits = basic_info->exponent_bits_per_sample; |
128 | 0 | basic_info->alpha_bits = basic_info->bits_per_sample; |
129 | 0 | basic_info->num_extra_channels = 1; |
130 | 0 | } else { |
131 | 0 | basic_info->alpha_exponent_bits = 0; |
132 | 0 | basic_info->alpha_bits = 0; |
133 | 0 | } |
134 | 0 | } |
135 | | |
136 | 0 | ColorEncoding ColorEncodingFromDescriptor(const ColorEncodingDescriptor& desc) { |
137 | 0 | ColorEncoding c; |
138 | 0 | c.SetColorSpace(desc.color_space); |
139 | 0 | if (desc.color_space != ColorSpace::kXYB) { |
140 | 0 | JXL_CHECK(c.SetWhitePointType(desc.white_point)); |
141 | 0 | if (desc.color_space != ColorSpace::kGray) { |
142 | 0 | JXL_CHECK(c.SetPrimariesType(desc.primaries)); |
143 | 0 | } |
144 | 0 | c.Tf().SetTransferFunction(desc.tf); |
145 | 0 | } |
146 | 0 | c.SetRenderingIntent(desc.rendering_intent); |
147 | 0 | JXL_CHECK(c.CreateICC()); |
148 | 0 | return c; |
149 | 0 | } |
150 | | |
151 | | namespace { |
152 | | void CheckSameEncodings(const std::vector<ColorEncoding>& a, |
153 | | const std::vector<ColorEncoding>& b, |
154 | | const std::string& check_name, |
155 | 0 | std::stringstream& failures) { |
156 | 0 | JXL_CHECK(a.size() == b.size()); |
157 | 0 | for (size_t i = 0; i < a.size(); ++i) { |
158 | 0 | if ((a[i].ICC() == b[i].ICC()) || |
159 | 0 | ((a[i].GetPrimariesType() == b[i].GetPrimariesType()) && |
160 | 0 | a[i].Tf().IsSame(b[i].Tf()))) { |
161 | 0 | continue; |
162 | 0 | } |
163 | 0 | failures << "CheckSameEncodings " << check_name << ": " << i |
164 | 0 | << "-th encoding mismatch\n"; |
165 | 0 | } |
166 | 0 | } |
167 | | } // namespace |
168 | | |
169 | | bool Roundtrip(const CodecInOut* io, const CompressParams& cparams, |
170 | | extras::JXLDecompressParams dparams, |
171 | | CodecInOut* JXL_RESTRICT io2, std::stringstream& failures, |
172 | 0 | size_t* compressed_size, ThreadPool* pool) { |
173 | 0 | DefaultAcceptedFormats(dparams); |
174 | 0 | if (compressed_size) { |
175 | 0 | *compressed_size = static_cast<size_t>(-1); |
176 | 0 | } |
177 | 0 | std::vector<uint8_t> compressed; |
178 | |
|
179 | 0 | std::vector<ColorEncoding> original_metadata_encodings; |
180 | 0 | std::vector<ColorEncoding> original_current_encodings; |
181 | 0 | std::vector<ColorEncoding> metadata_encodings_1; |
182 | 0 | std::vector<ColorEncoding> metadata_encodings_2; |
183 | 0 | std::vector<ColorEncoding> current_encodings_2; |
184 | 0 | original_metadata_encodings.reserve(io->frames.size()); |
185 | 0 | original_current_encodings.reserve(io->frames.size()); |
186 | 0 | metadata_encodings_1.reserve(io->frames.size()); |
187 | 0 | metadata_encodings_2.reserve(io->frames.size()); |
188 | 0 | current_encodings_2.reserve(io->frames.size()); |
189 | |
|
190 | 0 | for (const ImageBundle& ib : io->frames) { |
191 | | // Remember original encoding, will be returned by decoder. |
192 | 0 | original_metadata_encodings.push_back(ib.metadata()->color_encoding); |
193 | | // c_current should not change during encoding. |
194 | 0 | original_current_encodings.push_back(ib.c_current()); |
195 | 0 | } |
196 | |
|
197 | 0 | JXL_CHECK(test::EncodeFile(cparams, io, &compressed, pool)); |
198 | | |
199 | 0 | for (const ImageBundle& ib1 : io->frames) { |
200 | 0 | metadata_encodings_1.push_back(ib1.metadata()->color_encoding); |
201 | 0 | } |
202 | | |
203 | | // Should still be in the same color space after encoding. |
204 | 0 | CheckSameEncodings(metadata_encodings_1, original_metadata_encodings, |
205 | 0 | "original vs after encoding", failures); |
206 | |
|
207 | 0 | JXL_CHECK(DecodeFile(dparams, Bytes(compressed), io2, pool)); |
208 | 0 | JXL_CHECK(io2->frames.size() == io->frames.size()); |
209 | | |
210 | 0 | for (const ImageBundle& ib2 : io2->frames) { |
211 | 0 | metadata_encodings_2.push_back(ib2.metadata()->color_encoding); |
212 | 0 | current_encodings_2.push_back(ib2.c_current()); |
213 | 0 | } |
214 | | |
215 | | // We always produce the original color encoding if a color transform hook is |
216 | | // set. |
217 | 0 | CheckSameEncodings(current_encodings_2, original_current_encodings, |
218 | 0 | "current: original vs decoded", failures); |
219 | | |
220 | | // Decoder returns the originals passed to the encoder. |
221 | 0 | CheckSameEncodings(metadata_encodings_2, original_metadata_encodings, |
222 | 0 | "metadata: original vs decoded", failures); |
223 | |
|
224 | 0 | if (compressed_size) { |
225 | 0 | *compressed_size = compressed.size(); |
226 | 0 | } |
227 | |
|
228 | 0 | return failures.str().empty(); |
229 | 0 | } |
230 | | |
231 | | size_t Roundtrip(const extras::PackedPixelFile& ppf_in, |
232 | | const extras::JXLCompressParams& cparams, |
233 | | extras::JXLDecompressParams dparams, ThreadPool* pool, |
234 | 0 | extras::PackedPixelFile* ppf_out) { |
235 | 0 | DefaultAcceptedFormats(dparams); |
236 | 0 | SetThreadParallelRunner(cparams, pool); |
237 | 0 | SetThreadParallelRunner(dparams, pool); |
238 | 0 | std::vector<uint8_t> compressed; |
239 | 0 | JXL_CHECK(extras::EncodeImageJXL(cparams, ppf_in, /*jpeg_bytes=*/nullptr, |
240 | 0 | &compressed)); |
241 | 0 | size_t decoded_bytes = 0; |
242 | 0 | JXL_CHECK(extras::DecodeImageJXL(compressed.data(), compressed.size(), |
243 | 0 | dparams, &decoded_bytes, ppf_out)); |
244 | 0 | JXL_CHECK(decoded_bytes == compressed.size()); |
245 | 0 | return compressed.size(); |
246 | 0 | } |
247 | | |
248 | 0 | std::vector<ColorEncodingDescriptor> AllEncodings() { |
249 | 0 | std::vector<ColorEncodingDescriptor> all_encodings; |
250 | 0 | all_encodings.reserve(300); |
251 | |
|
252 | 0 | for (ColorSpace cs : Values<ColorSpace>()) { |
253 | 0 | if (cs == ColorSpace::kUnknown || cs == ColorSpace::kXYB || |
254 | 0 | cs == ColorSpace::kGray) { |
255 | 0 | continue; |
256 | 0 | } |
257 | | |
258 | 0 | for (WhitePoint wp : Values<WhitePoint>()) { |
259 | 0 | if (wp == WhitePoint::kCustom) continue; |
260 | 0 | for (Primaries primaries : Values<Primaries>()) { |
261 | 0 | if (primaries == Primaries::kCustom) continue; |
262 | 0 | for (TransferFunction tf : Values<TransferFunction>()) { |
263 | 0 | if (tf == TransferFunction::kUnknown) continue; |
264 | 0 | for (RenderingIntent ri : Values<RenderingIntent>()) { |
265 | 0 | ColorEncodingDescriptor cdesc; |
266 | 0 | cdesc.color_space = cs; |
267 | 0 | cdesc.white_point = wp; |
268 | 0 | cdesc.primaries = primaries; |
269 | 0 | cdesc.tf = tf; |
270 | 0 | cdesc.rendering_intent = ri; |
271 | 0 | all_encodings.push_back(cdesc); |
272 | 0 | } |
273 | 0 | } |
274 | 0 | } |
275 | 0 | } |
276 | 0 | } |
277 | |
|
278 | 0 | return all_encodings; |
279 | 0 | } |
280 | | |
281 | | jxl::CodecInOut SomeTestImageToCodecInOut(const std::vector<uint8_t>& buf, |
282 | | size_t num_channels, size_t xsize, |
283 | 0 | size_t ysize) { |
284 | 0 | JxlMemoryManager* memory_manager = jxl::test::MemoryManager(); |
285 | 0 | jxl::CodecInOut io{memory_manager}; |
286 | 0 | io.SetSize(xsize, ysize); |
287 | 0 | io.metadata.m.SetAlphaBits(16); |
288 | 0 | io.metadata.m.color_encoding = jxl::ColorEncoding::SRGB( |
289 | 0 | /*is_gray=*/num_channels == 1 || num_channels == 2); |
290 | 0 | JxlPixelFormat format = {static_cast<uint32_t>(num_channels), JXL_TYPE_UINT16, |
291 | 0 | JXL_BIG_ENDIAN, 0}; |
292 | 0 | JXL_CHECK(ConvertFromExternal( |
293 | 0 | jxl::Bytes(buf.data(), buf.size()), xsize, ysize, |
294 | 0 | jxl::ColorEncoding::SRGB(/*is_gray=*/num_channels < 3), |
295 | 0 | /*bits_per_sample=*/16, format, |
296 | 0 | /*pool=*/nullptr, |
297 | 0 | /*ib=*/&io.Main())); |
298 | 0 | return io; |
299 | 0 | } |
300 | | |
301 | 0 | bool Near(double expected, double value, double max_dist) { |
302 | 0 | double dist = expected > value ? expected - value : value - expected; |
303 | 0 | return dist <= max_dist; |
304 | 0 | } |
305 | | |
306 | 0 | float LoadLEFloat16(const uint8_t* p) { |
307 | 0 | uint16_t bits16 = LoadLE16(p); |
308 | 0 | return detail::LoadFloat16(bits16); |
309 | 0 | } |
310 | | |
311 | 0 | float LoadBEFloat16(const uint8_t* p) { |
312 | 0 | uint16_t bits16 = LoadBE16(p); |
313 | 0 | return detail::LoadFloat16(bits16); |
314 | 0 | } |
315 | | |
316 | 0 | size_t GetPrecision(JxlDataType data_type) { |
317 | 0 | switch (data_type) { |
318 | 0 | case JXL_TYPE_UINT8: |
319 | 0 | return 8; |
320 | 0 | case JXL_TYPE_UINT16: |
321 | 0 | return 16; |
322 | 0 | case JXL_TYPE_FLOAT: |
323 | | // Floating point mantissa precision |
324 | 0 | return 24; |
325 | 0 | case JXL_TYPE_FLOAT16: |
326 | 0 | return 11; |
327 | 0 | default: |
328 | 0 | JXL_ABORT("Unhandled JxlDataType"); |
329 | 0 | } |
330 | 0 | } |
331 | | |
332 | 0 | size_t GetDataBits(JxlDataType data_type) { |
333 | 0 | switch (data_type) { |
334 | 0 | case JXL_TYPE_UINT8: |
335 | 0 | return 8; |
336 | 0 | case JXL_TYPE_UINT16: |
337 | 0 | return 16; |
338 | 0 | case JXL_TYPE_FLOAT: |
339 | 0 | return 32; |
340 | 0 | case JXL_TYPE_FLOAT16: |
341 | 0 | return 16; |
342 | 0 | default: |
343 | 0 | JXL_ABORT("Unhandled JxlDataType"); |
344 | 0 | } |
345 | 0 | } |
346 | | |
347 | | std::vector<double> ConvertToRGBA32(const uint8_t* pixels, size_t xsize, |
348 | | size_t ysize, const JxlPixelFormat& format, |
349 | 0 | double factor) { |
350 | 0 | std::vector<double> result(xsize * ysize * 4); |
351 | 0 | size_t num_channels = format.num_channels; |
352 | 0 | bool gray = num_channels == 1 || num_channels == 2; |
353 | 0 | bool alpha = num_channels == 2 || num_channels == 4; |
354 | 0 | JxlEndianness endianness = format.endianness; |
355 | | // Compute actual type: |
356 | 0 | if (endianness == JXL_NATIVE_ENDIAN) { |
357 | 0 | endianness = IsLittleEndian() ? JXL_LITTLE_ENDIAN : JXL_BIG_ENDIAN; |
358 | 0 | } |
359 | |
|
360 | 0 | size_t stride = |
361 | 0 | xsize * jxl::DivCeil(GetDataBits(format.data_type) * num_channels, |
362 | 0 | jxl::kBitsPerByte); |
363 | 0 | if (format.align > 1) stride = jxl::RoundUpTo(stride, format.align); |
364 | |
|
365 | 0 | if (format.data_type == JXL_TYPE_UINT8) { |
366 | | // Multiplier to bring to 0-1.0 range |
367 | 0 | double mul = factor > 0.0 ? factor : 1.0 / 255.0; |
368 | 0 | for (size_t y = 0; y < ysize; ++y) { |
369 | 0 | for (size_t x = 0; x < xsize; ++x) { |
370 | 0 | size_t j = (y * xsize + x) * 4; |
371 | 0 | size_t i = y * stride + x * num_channels; |
372 | 0 | double r = pixels[i]; |
373 | 0 | double g = gray ? r : pixels[i + 1]; |
374 | 0 | double b = gray ? r : pixels[i + 2]; |
375 | 0 | double a = alpha ? pixels[i + num_channels - 1] : 255; |
376 | 0 | result[j + 0] = r * mul; |
377 | 0 | result[j + 1] = g * mul; |
378 | 0 | result[j + 2] = b * mul; |
379 | 0 | result[j + 3] = a * mul; |
380 | 0 | } |
381 | 0 | } |
382 | 0 | } else if (format.data_type == JXL_TYPE_UINT16) { |
383 | 0 | JXL_ASSERT(endianness != JXL_NATIVE_ENDIAN); |
384 | | // Multiplier to bring to 0-1.0 range |
385 | 0 | double mul = factor > 0.0 ? factor : 1.0 / 65535.0; |
386 | 0 | for (size_t y = 0; y < ysize; ++y) { |
387 | 0 | for (size_t x = 0; x < xsize; ++x) { |
388 | 0 | size_t j = (y * xsize + x) * 4; |
389 | 0 | size_t i = y * stride + x * num_channels * 2; |
390 | 0 | double r; |
391 | 0 | double g; |
392 | 0 | double b; |
393 | 0 | double a; |
394 | 0 | if (endianness == JXL_BIG_ENDIAN) { |
395 | 0 | r = (pixels[i + 0] << 8) + pixels[i + 1]; |
396 | 0 | g = gray ? r : (pixels[i + 2] << 8) + pixels[i + 3]; |
397 | 0 | b = gray ? r : (pixels[i + 4] << 8) + pixels[i + 5]; |
398 | 0 | a = alpha ? (pixels[i + num_channels * 2 - 2] << 8) + |
399 | 0 | pixels[i + num_channels * 2 - 1] |
400 | 0 | : 65535; |
401 | 0 | } else { |
402 | 0 | r = (pixels[i + 1] << 8) + pixels[i + 0]; |
403 | 0 | g = gray ? r : (pixels[i + 3] << 8) + pixels[i + 2]; |
404 | 0 | b = gray ? r : (pixels[i + 5] << 8) + pixels[i + 4]; |
405 | 0 | a = alpha ? (pixels[i + num_channels * 2 - 1] << 8) + |
406 | 0 | pixels[i + num_channels * 2 - 2] |
407 | 0 | : 65535; |
408 | 0 | } |
409 | 0 | result[j + 0] = r * mul; |
410 | 0 | result[j + 1] = g * mul; |
411 | 0 | result[j + 2] = b * mul; |
412 | 0 | result[j + 3] = a * mul; |
413 | 0 | } |
414 | 0 | } |
415 | 0 | } else if (format.data_type == JXL_TYPE_FLOAT) { |
416 | 0 | JXL_ASSERT(endianness != JXL_NATIVE_ENDIAN); |
417 | 0 | for (size_t y = 0; y < ysize; ++y) { |
418 | 0 | for (size_t x = 0; x < xsize; ++x) { |
419 | 0 | size_t j = (y * xsize + x) * 4; |
420 | 0 | size_t i = y * stride + x * num_channels * 4; |
421 | 0 | double r; |
422 | 0 | double g; |
423 | 0 | double b; |
424 | 0 | double a; |
425 | 0 | if (endianness == JXL_BIG_ENDIAN) { |
426 | 0 | r = LoadBEFloat(pixels + i); |
427 | 0 | g = gray ? r : LoadBEFloat(pixels + i + 4); |
428 | 0 | b = gray ? r : LoadBEFloat(pixels + i + 8); |
429 | 0 | a = alpha ? LoadBEFloat(pixels + i + num_channels * 4 - 4) : 1.0; |
430 | 0 | } else { |
431 | 0 | r = LoadLEFloat(pixels + i); |
432 | 0 | g = gray ? r : LoadLEFloat(pixels + i + 4); |
433 | 0 | b = gray ? r : LoadLEFloat(pixels + i + 8); |
434 | 0 | a = alpha ? LoadLEFloat(pixels + i + num_channels * 4 - 4) : 1.0; |
435 | 0 | } |
436 | 0 | result[j + 0] = r; |
437 | 0 | result[j + 1] = g; |
438 | 0 | result[j + 2] = b; |
439 | 0 | result[j + 3] = a; |
440 | 0 | } |
441 | 0 | } |
442 | 0 | } else if (format.data_type == JXL_TYPE_FLOAT16) { |
443 | 0 | JXL_ASSERT(endianness != JXL_NATIVE_ENDIAN); |
444 | 0 | for (size_t y = 0; y < ysize; ++y) { |
445 | 0 | for (size_t x = 0; x < xsize; ++x) { |
446 | 0 | size_t j = (y * xsize + x) * 4; |
447 | 0 | size_t i = y * stride + x * num_channels * 2; |
448 | 0 | double r; |
449 | 0 | double g; |
450 | 0 | double b; |
451 | 0 | double a; |
452 | 0 | if (endianness == JXL_BIG_ENDIAN) { |
453 | 0 | r = LoadBEFloat16(pixels + i); |
454 | 0 | g = gray ? r : LoadBEFloat16(pixels + i + 2); |
455 | 0 | b = gray ? r : LoadBEFloat16(pixels + i + 4); |
456 | 0 | a = alpha ? LoadBEFloat16(pixels + i + num_channels * 2 - 2) : 1.0; |
457 | 0 | } else { |
458 | 0 | r = LoadLEFloat16(pixels + i); |
459 | 0 | g = gray ? r : LoadLEFloat16(pixels + i + 2); |
460 | 0 | b = gray ? r : LoadLEFloat16(pixels + i + 4); |
461 | 0 | a = alpha ? LoadLEFloat16(pixels + i + num_channels * 2 - 2) : 1.0; |
462 | 0 | } |
463 | 0 | result[j + 0] = r; |
464 | 0 | result[j + 1] = g; |
465 | 0 | result[j + 2] = b; |
466 | 0 | result[j + 3] = a; |
467 | 0 | } |
468 | 0 | } |
469 | 0 | } else { |
470 | 0 | JXL_ASSERT(false); // Unsupported type |
471 | 0 | } |
472 | 0 | return result; |
473 | 0 | } |
474 | | |
475 | | size_t ComparePixels(const uint8_t* a, const uint8_t* b, size_t xsize, |
476 | | size_t ysize, const JxlPixelFormat& format_a, |
477 | | const JxlPixelFormat& format_b, |
478 | 0 | double threshold_multiplier) { |
479 | | // Convert both images to equal full precision for comparison. |
480 | 0 | std::vector<double> a_full = ConvertToRGBA32(a, xsize, ysize, format_a); |
481 | 0 | std::vector<double> b_full = ConvertToRGBA32(b, xsize, ysize, format_b); |
482 | 0 | bool gray_a = format_a.num_channels < 3; |
483 | 0 | bool gray_b = format_b.num_channels < 3; |
484 | 0 | bool alpha_a = ((format_a.num_channels & 1) == 0); |
485 | 0 | bool alpha_b = ((format_b.num_channels & 1) == 0); |
486 | 0 | size_t bits_a = GetPrecision(format_a.data_type); |
487 | 0 | size_t bits_b = GetPrecision(format_b.data_type); |
488 | 0 | size_t bits = std::min(bits_a, bits_b); |
489 | | // How much distance is allowed in case of pixels with lower bit depths, given |
490 | | // that the double precision float images use range 0-1.0. |
491 | | // E.g. in case of 1-bit this is 0.5 since 0.499 must map to 0 and 0.501 must |
492 | | // map to 1. |
493 | 0 | double precision = 0.5 * threshold_multiplier / ((1ull << bits) - 1ull); |
494 | 0 | if (format_a.data_type == JXL_TYPE_FLOAT16 || |
495 | 0 | format_b.data_type == JXL_TYPE_FLOAT16) { |
496 | | // Lower the precision for float16, because it currently looks like the |
497 | | // scalar and wasm implementations of hwy have 1 less bit of precision |
498 | | // than the x86 implementations. |
499 | | // TODO(lode): Set the required precision back to 11 bits when possible. |
500 | 0 | precision = 0.5 * threshold_multiplier / ((1ull << (bits - 1)) - 1ull); |
501 | 0 | } |
502 | 0 | if (format_b.data_type == JXL_TYPE_UINT8) { |
503 | | // Increase the threshold by the maximum difference introduced by dithering. |
504 | 0 | precision += 63.0 / 128.0; |
505 | 0 | } |
506 | 0 | size_t numdiff = 0; |
507 | 0 | for (size_t y = 0; y < ysize; y++) { |
508 | 0 | for (size_t x = 0; x < xsize; x++) { |
509 | 0 | size_t i = (y * xsize + x) * 4; |
510 | 0 | bool ok = true; |
511 | 0 | if (gray_a || gray_b) { |
512 | 0 | if (!Near(a_full[i + 0], b_full[i + 0], precision)) ok = false; |
513 | | // If the input was grayscale and the output not, then the output must |
514 | | // have all channels equal. |
515 | 0 | if (gray_a && b_full[i + 0] != b_full[i + 1] && |
516 | 0 | b_full[i + 2] != b_full[i + 2]) { |
517 | 0 | ok = false; |
518 | 0 | } |
519 | 0 | } else { |
520 | 0 | if (!Near(a_full[i + 0], b_full[i + 0], precision) || |
521 | 0 | !Near(a_full[i + 1], b_full[i + 1], precision) || |
522 | 0 | !Near(a_full[i + 2], b_full[i + 2], precision)) { |
523 | 0 | ok = false; |
524 | 0 | } |
525 | 0 | } |
526 | 0 | if (alpha_a && alpha_b) { |
527 | 0 | if (!Near(a_full[i + 3], b_full[i + 3], precision)) ok = false; |
528 | 0 | } else { |
529 | | // If the input had no alpha channel, the output should be opaque |
530 | | // after roundtrip. |
531 | 0 | if (alpha_b && !Near(1.0, b_full[i + 3], precision)) ok = false; |
532 | 0 | } |
533 | 0 | if (!ok) numdiff++; |
534 | 0 | } |
535 | 0 | } |
536 | 0 | return numdiff; |
537 | 0 | } |
538 | | |
539 | | double DistanceRMS(const uint8_t* a, const uint8_t* b, size_t xsize, |
540 | 0 | size_t ysize, const JxlPixelFormat& format) { |
541 | | // Convert both images to equal full precision for comparison. |
542 | 0 | std::vector<double> a_full = ConvertToRGBA32(a, xsize, ysize, format); |
543 | 0 | std::vector<double> b_full = ConvertToRGBA32(b, xsize, ysize, format); |
544 | 0 | double sum = 0.0; |
545 | 0 | for (size_t y = 0; y < ysize; y++) { |
546 | 0 | double row_sum = 0.0; |
547 | 0 | for (size_t x = 0; x < xsize; x++) { |
548 | 0 | size_t i = (y * xsize + x) * 4; |
549 | 0 | for (size_t c = 0; c < format.num_channels; ++c) { |
550 | 0 | double diff = a_full[i + c] - b_full[i + c]; |
551 | 0 | row_sum += diff * diff; |
552 | 0 | } |
553 | 0 | } |
554 | 0 | sum += row_sum; |
555 | 0 | } |
556 | 0 | sum /= (xsize * ysize); |
557 | 0 | return sqrt(sum); |
558 | 0 | } |
559 | | |
560 | | float ButteraugliDistance(const extras::PackedPixelFile& a, |
561 | 0 | const extras::PackedPixelFile& b, ThreadPool* pool) { |
562 | 0 | JxlMemoryManager* memory_manager = jxl::test::MemoryManager(); |
563 | 0 | CodecInOut io0{memory_manager}; |
564 | 0 | JXL_CHECK(ConvertPackedPixelFileToCodecInOut(a, pool, &io0)); |
565 | 0 | CodecInOut io1{memory_manager}; |
566 | 0 | JXL_CHECK(ConvertPackedPixelFileToCodecInOut(b, pool, &io1)); |
567 | | // TODO(eustas): simplify? |
568 | 0 | return ButteraugliDistance(io0.frames, io1.frames, ButteraugliParams(), |
569 | 0 | *JxlGetDefaultCms(), |
570 | 0 | /*distmap=*/nullptr, pool); |
571 | 0 | } |
572 | | |
573 | | float ButteraugliDistance(const ImageBundle& rgb0, const ImageBundle& rgb1, |
574 | | const ButteraugliParams& params, |
575 | | const JxlCmsInterface& cms, ImageF* distmap, |
576 | 0 | ThreadPool* pool, bool ignore_alpha) { |
577 | 0 | JxlButteraugliComparator comparator(params, cms); |
578 | 0 | float distance; |
579 | 0 | JXL_CHECK(ComputeScore(rgb0, rgb1, &comparator, cms, &distance, distmap, pool, |
580 | 0 | ignore_alpha)); |
581 | 0 | return distance; |
582 | 0 | } |
583 | | |
584 | | float ButteraugliDistance(const std::vector<ImageBundle>& frames0, |
585 | | const std::vector<ImageBundle>& frames1, |
586 | | const ButteraugliParams& params, |
587 | | const JxlCmsInterface& cms, ImageF* distmap, |
588 | 0 | ThreadPool* pool) { |
589 | 0 | JxlButteraugliComparator comparator(params, cms); |
590 | 0 | JXL_ASSERT(frames0.size() == frames1.size()); |
591 | 0 | float max_dist = 0.0f; |
592 | 0 | for (size_t i = 0; i < frames0.size(); ++i) { |
593 | 0 | float frame_score; |
594 | 0 | JXL_CHECK(ComputeScore(frames0[i], frames1[i], &comparator, cms, |
595 | 0 | &frame_score, distmap, pool)); |
596 | 0 | max_dist = std::max(max_dist, frame_score); |
597 | 0 | } |
598 | 0 | return max_dist; |
599 | 0 | } |
600 | | |
601 | | float Butteraugli3Norm(const extras::PackedPixelFile& a, |
602 | 0 | const extras::PackedPixelFile& b, ThreadPool* pool) { |
603 | 0 | JxlMemoryManager* memory_manager = jxl::test::MemoryManager(); |
604 | 0 | CodecInOut io0{memory_manager}; |
605 | 0 | JXL_CHECK(ConvertPackedPixelFileToCodecInOut(a, pool, &io0)); |
606 | 0 | CodecInOut io1{memory_manager}; |
607 | 0 | JXL_CHECK(ConvertPackedPixelFileToCodecInOut(b, pool, &io1)); |
608 | 0 | ButteraugliParams ba; |
609 | 0 | ImageF distmap; |
610 | 0 | ButteraugliDistance(io0.frames, io1.frames, ba, *JxlGetDefaultCms(), &distmap, |
611 | 0 | pool); |
612 | 0 | return ComputeDistanceP(distmap, ba, 3); |
613 | 0 | } |
614 | | |
615 | | float ComputeDistance2(const extras::PackedPixelFile& a, |
616 | 0 | const extras::PackedPixelFile& b) { |
617 | 0 | JxlMemoryManager* memory_manager = jxl::test::MemoryManager(); |
618 | 0 | CodecInOut io0{memory_manager}; |
619 | 0 | JXL_CHECK(ConvertPackedPixelFileToCodecInOut(a, nullptr, &io0)); |
620 | 0 | CodecInOut io1{memory_manager}; |
621 | 0 | JXL_CHECK(ConvertPackedPixelFileToCodecInOut(b, nullptr, &io1)); |
622 | 0 | return ComputeDistance2(io0.Main(), io1.Main(), *JxlGetDefaultCms()); |
623 | 0 | } |
624 | | |
625 | | float ComputePSNR(const extras::PackedPixelFile& a, |
626 | 0 | const extras::PackedPixelFile& b) { |
627 | 0 | JxlMemoryManager* memory_manager = jxl::test::MemoryManager(); |
628 | 0 | CodecInOut io0{memory_manager}; |
629 | 0 | JXL_CHECK(ConvertPackedPixelFileToCodecInOut(a, nullptr, &io0)); |
630 | 0 | CodecInOut io1{memory_manager}; |
631 | 0 | JXL_CHECK(ConvertPackedPixelFileToCodecInOut(b, nullptr, &io1)); |
632 | 0 | return ComputePSNR(io0.Main(), io1.Main(), *JxlGetDefaultCms()); |
633 | 0 | } |
634 | | |
635 | | bool SameAlpha(const extras::PackedPixelFile& a, |
636 | 0 | const extras::PackedPixelFile& b) { |
637 | 0 | JXL_CHECK(a.info.xsize == b.info.xsize); |
638 | 0 | JXL_CHECK(a.info.ysize == b.info.ysize); |
639 | 0 | JXL_CHECK(a.info.alpha_bits == b.info.alpha_bits); |
640 | 0 | JXL_CHECK(a.info.alpha_exponent_bits == b.info.alpha_exponent_bits); |
641 | 0 | JXL_CHECK(a.info.alpha_bits > 0); |
642 | 0 | JXL_CHECK(a.frames.size() == b.frames.size()); |
643 | 0 | for (size_t i = 0; i < a.frames.size(); ++i) { |
644 | 0 | const extras::PackedImage& color_a = a.frames[i].color; |
645 | 0 | const extras::PackedImage& color_b = b.frames[i].color; |
646 | 0 | JXL_CHECK(color_a.format.num_channels == color_b.format.num_channels); |
647 | 0 | JXL_CHECK(color_a.format.data_type == color_b.format.data_type); |
648 | 0 | JXL_CHECK(color_a.format.endianness == color_b.format.endianness); |
649 | 0 | JXL_CHECK(color_a.pixels_size == color_b.pixels_size); |
650 | 0 | size_t pwidth = |
651 | 0 | extras::PackedImage::BitsPerChannel(color_a.format.data_type) / 8; |
652 | 0 | size_t num_color = color_a.format.num_channels < 3 ? 1 : 3; |
653 | 0 | const uint8_t* p_a = reinterpret_cast<const uint8_t*>(color_a.pixels()); |
654 | 0 | const uint8_t* p_b = reinterpret_cast<const uint8_t*>(color_b.pixels()); |
655 | 0 | for (size_t y = 0; y < a.info.ysize; ++y) { |
656 | 0 | for (size_t x = 0; x < a.info.xsize; ++x) { |
657 | 0 | size_t idx = |
658 | 0 | ((y * a.info.xsize + x) * color_a.format.num_channels + num_color) * |
659 | 0 | pwidth; |
660 | 0 | if (memcmp(&p_a[idx], &p_b[idx], pwidth) != 0) { |
661 | 0 | return false; |
662 | 0 | } |
663 | 0 | } |
664 | 0 | } |
665 | 0 | } |
666 | 0 | return true; |
667 | 0 | } |
668 | | |
669 | 0 | bool SamePixels(const extras::PackedImage& a, const extras::PackedImage& b) { |
670 | 0 | JXL_CHECK(a.xsize == b.xsize); |
671 | 0 | JXL_CHECK(a.ysize == b.ysize); |
672 | 0 | JXL_CHECK(a.format.num_channels == b.format.num_channels); |
673 | 0 | JXL_CHECK(a.format.data_type == b.format.data_type); |
674 | 0 | JXL_CHECK(a.format.endianness == b.format.endianness); |
675 | 0 | JXL_CHECK(a.pixels_size == b.pixels_size); |
676 | 0 | const uint8_t* p_a = reinterpret_cast<const uint8_t*>(a.pixels()); |
677 | 0 | const uint8_t* p_b = reinterpret_cast<const uint8_t*>(b.pixels()); |
678 | 0 | for (size_t y = 0; y < a.ysize; ++y) { |
679 | 0 | for (size_t x = 0; x < a.xsize; ++x) { |
680 | 0 | size_t idx = (y * a.xsize + x) * a.pixel_stride(); |
681 | 0 | if (memcmp(&p_a[idx], &p_b[idx], a.pixel_stride()) != 0) { |
682 | 0 | printf("Mismatch at row %" PRIuS " col %" PRIuS "\n", y, x); |
683 | 0 | printf(" a: "); |
684 | 0 | for (size_t j = 0; j < a.pixel_stride(); ++j) { |
685 | 0 | printf(" %3u", p_a[idx + j]); |
686 | 0 | } |
687 | 0 | printf("\n b: "); |
688 | 0 | for (size_t j = 0; j < a.pixel_stride(); ++j) { |
689 | 0 | printf(" %3u", p_b[idx + j]); |
690 | 0 | } |
691 | 0 | printf("\n"); |
692 | 0 | return false; |
693 | 0 | } |
694 | 0 | } |
695 | 0 | } |
696 | 0 | return true; |
697 | 0 | } |
698 | | |
699 | | bool SamePixels(const extras::PackedPixelFile& a, |
700 | 0 | const extras::PackedPixelFile& b) { |
701 | 0 | JXL_CHECK(a.info.xsize == b.info.xsize); |
702 | 0 | JXL_CHECK(a.info.ysize == b.info.ysize); |
703 | 0 | JXL_CHECK(a.info.bits_per_sample == b.info.bits_per_sample); |
704 | 0 | JXL_CHECK(a.info.exponent_bits_per_sample == b.info.exponent_bits_per_sample); |
705 | 0 | JXL_CHECK(a.frames.size() == b.frames.size()); |
706 | 0 | for (size_t i = 0; i < a.frames.size(); ++i) { |
707 | 0 | const auto& frame_a = a.frames[i]; |
708 | 0 | const auto& frame_b = b.frames[i]; |
709 | 0 | if (!SamePixels(frame_a.color, frame_b.color)) { |
710 | 0 | return false; |
711 | 0 | } |
712 | 0 | JXL_CHECK(frame_a.extra_channels.size() == frame_b.extra_channels.size()); |
713 | 0 | for (size_t j = 0; j < frame_a.extra_channels.size(); ++j) { |
714 | 0 | if (!SamePixels(frame_a.extra_channels[i], frame_b.extra_channels[i])) { |
715 | 0 | return false; |
716 | 0 | } |
717 | 0 | } |
718 | 0 | } |
719 | 0 | return true; |
720 | 0 | } |
721 | | |
722 | | Status ReadICC(BitReader* JXL_RESTRICT reader, |
723 | 0 | std::vector<uint8_t>* JXL_RESTRICT icc, size_t output_limit) { |
724 | 0 | JxlMemoryManager* memort_manager = jxl::test::MemoryManager(); |
725 | 0 | icc->clear(); |
726 | 0 | ICCReader icc_reader{memort_manager}; |
727 | 0 | PaddedBytes icc_buffer{memort_manager}; |
728 | 0 | JXL_RETURN_IF_ERROR(icc_reader.Init(reader, output_limit)); |
729 | 0 | JXL_RETURN_IF_ERROR(icc_reader.Process(reader, &icc_buffer)); |
730 | 0 | Bytes(icc_buffer).AppendTo(*icc); |
731 | 0 | return true; |
732 | 0 | } |
733 | | |
734 | | namespace { // For EncodeFile |
735 | | Status PrepareCodecMetadataFromIO(const CompressParams& cparams, |
736 | | const CodecInOut* io, |
737 | 0 | CodecMetadata* metadata) { |
738 | 0 | *metadata = io->metadata; |
739 | 0 | size_t ups = 1; |
740 | 0 | if (cparams.already_downsampled) ups = cparams.resampling; |
741 | |
|
742 | 0 | JXL_RETURN_IF_ERROR(metadata->size.Set(io->xsize() * ups, io->ysize() * ups)); |
743 | | |
744 | | // Keep ICC profile in lossless modes because a reconstructed profile may be |
745 | | // slightly different (quantization). |
746 | | // Also keep ICC in JPEG reconstruction mode as we need byte-exact profiles. |
747 | 0 | if (!cparams.IsLossless() && !io->Main().IsJPEG() && cparams.cms_set) { |
748 | 0 | metadata->m.color_encoding.DecideIfWantICC(cparams.cms); |
749 | 0 | } |
750 | |
|
751 | 0 | metadata->m.xyb_encoded = |
752 | 0 | cparams.color_transform == ColorTransform::kXYB ? true : false; |
753 | | |
754 | | // TODO(firsching): move this EncodeFile to test_utils / re-implement this |
755 | | // using API functions |
756 | 0 | return true; |
757 | 0 | } |
758 | | |
759 | | Status EncodePreview(const CompressParams& cparams, const ImageBundle& ib, |
760 | | const CodecMetadata* metadata, const JxlCmsInterface& cms, |
761 | 0 | ThreadPool* pool, BitWriter* JXL_RESTRICT writer) { |
762 | 0 | JxlMemoryManager* memory_manager = jxl::test::MemoryManager(); |
763 | 0 | BitWriter preview_writer{memory_manager}; |
764 | | // TODO(janwas): also support generating preview by downsampling |
765 | 0 | if (ib.HasColor()) { |
766 | 0 | AuxOut aux_out; |
767 | | // TODO(lode): check if we want all extra channels and matching xyb_encoded |
768 | | // for the preview, such that using the main ImageMetadata object for |
769 | | // encoding this frame is warrented. |
770 | 0 | FrameInfo frame_info; |
771 | 0 | frame_info.is_preview = true; |
772 | 0 | JXL_RETURN_IF_ERROR(EncodeFrame(memory_manager, cparams, frame_info, |
773 | 0 | metadata, ib, cms, pool, &preview_writer, |
774 | 0 | &aux_out)); |
775 | 0 | preview_writer.ZeroPadToByte(); |
776 | 0 | } |
777 | | |
778 | 0 | if (preview_writer.BitsWritten() != 0) { |
779 | 0 | writer->ZeroPadToByte(); |
780 | 0 | writer->AppendByteAligned(preview_writer); |
781 | 0 | } |
782 | |
|
783 | 0 | return true; |
784 | 0 | } |
785 | | |
786 | | } // namespace |
787 | | |
788 | | Status EncodeFile(const CompressParams& params, const CodecInOut* io, |
789 | 0 | std::vector<uint8_t>* compressed, ThreadPool* pool) { |
790 | 0 | JxlMemoryManager* memory_manager = jxl::test::MemoryManager(); |
791 | 0 | compressed->clear(); |
792 | 0 | const JxlCmsInterface& cms = *JxlGetDefaultCms(); |
793 | 0 | io->CheckMetadata(); |
794 | 0 | BitWriter writer{memory_manager}; |
795 | |
|
796 | 0 | CompressParams cparams = params; |
797 | 0 | if (io->Main().color_transform != ColorTransform::kNone) { |
798 | | // Set the color transform to YCbCr or XYB if the original image is such. |
799 | 0 | cparams.color_transform = io->Main().color_transform; |
800 | 0 | } |
801 | |
|
802 | 0 | JXL_RETURN_IF_ERROR(ParamsPostInit(&cparams)); |
803 | | |
804 | 0 | std::unique_ptr<CodecMetadata> metadata = jxl::make_unique<CodecMetadata>(); |
805 | 0 | JXL_RETURN_IF_ERROR(PrepareCodecMetadataFromIO(cparams, io, metadata.get())); |
806 | 0 | JXL_RETURN_IF_ERROR( |
807 | 0 | WriteCodestreamHeaders(metadata.get(), &writer, /*aux_out*/ nullptr)); |
808 | | |
809 | | // Only send ICC (at least several hundred bytes) if fields aren't enough. |
810 | 0 | if (metadata->m.color_encoding.WantICC()) { |
811 | 0 | JXL_RETURN_IF_ERROR(WriteICC(metadata->m.color_encoding.ICC(), &writer, |
812 | 0 | kLayerHeader, /* aux_out */ nullptr)); |
813 | 0 | } |
814 | | |
815 | 0 | if (metadata->m.have_preview) { |
816 | 0 | JXL_RETURN_IF_ERROR(EncodePreview(cparams, io->preview_frame, |
817 | 0 | metadata.get(), cms, pool, &writer)); |
818 | 0 | } |
819 | | |
820 | | // Each frame should start on byte boundaries. |
821 | 0 | BitWriter::Allotment allotment(&writer, 8); |
822 | 0 | writer.ZeroPadToByte(); |
823 | 0 | allotment.ReclaimAndCharge(&writer, kLayerHeader, /* aux_out */ nullptr); |
824 | |
|
825 | 0 | for (size_t i = 0; i < io->frames.size(); i++) { |
826 | 0 | FrameInfo info; |
827 | 0 | info.is_last = i == io->frames.size() - 1; |
828 | 0 | if (io->frames[i].use_for_next_frame) { |
829 | 0 | info.save_as_reference = 1; |
830 | 0 | } |
831 | 0 | JXL_RETURN_IF_ERROR(EncodeFrame(memory_manager, cparams, info, |
832 | 0 | metadata.get(), io->frames[i], cms, pool, |
833 | 0 | &writer, |
834 | 0 | /* aux_out */ nullptr)); |
835 | 0 | } |
836 | | |
837 | 0 | PaddedBytes output = std::move(writer).TakeBytes(); |
838 | 0 | Bytes(output).AppendTo(*compressed); |
839 | 0 | return true; |
840 | 0 | } |
841 | | |
842 | | namespace { |
843 | 921k | void* TestAlloc(void* /* opaque*/, size_t size) { return malloc(size); } |
844 | 921k | void TestFree(void* /* opaque*/, void* address) { free(address); } |
845 | | JxlMemoryManager kMemoryManager{nullptr, &TestAlloc, &TestFree}; |
846 | | } // namespace |
847 | | |
848 | 6.06k | JxlMemoryManager* MemoryManager() { return &kMemoryManager; }; |
849 | | |
850 | | } // namespace test |
851 | | |
852 | 0 | bool operator==(const jxl::Bytes& a, const jxl::Bytes& b) { |
853 | 0 | if (a.size() != b.size()) return false; |
854 | 0 | if (memcmp(a.data(), b.data(), a.size()) != 0) return false; |
855 | 0 | return true; |
856 | 0 | } |
857 | | |
858 | | // Allow using EXPECT_EQ on jxl::Bytes |
859 | 0 | bool operator!=(const jxl::Bytes& a, const jxl::Bytes& b) { return !(a == b); } |
860 | | |
861 | | } // namespace jxl |