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/extras/codec.h"
7
8 #include <stddef.h>
9 #include <stdio.h>
10
11 #include <algorithm>
12 #include <random>
13 #include <utility>
14 #include <vector>
15
16 #include "gtest/gtest.h"
17 #include "lib/extras/codec_pgx.h"
18 #include "lib/extras/codec_pnm.h"
19 #include "lib/jxl/base/thread_pool_internal.h"
20 #include "lib/jxl/color_management.h"
21 #include "lib/jxl/image.h"
22 #include "lib/jxl/image_bundle.h"
23 #include "lib/jxl/image_test_utils.h"
24 #include "lib/jxl/luminance.h"
25 #include "lib/jxl/testdata.h"
26
27 namespace jxl {
28 namespace extras {
29 namespace {
30
CreateTestImage(const size_t xsize,const size_t ysize,const bool is_gray,const bool add_alpha,const size_t bits_per_sample,const ColorEncoding & c_native)31 CodecInOut CreateTestImage(const size_t xsize, const size_t ysize,
32 const bool is_gray, const bool add_alpha,
33 const size_t bits_per_sample,
34 const ColorEncoding& c_native) {
35 Image3F image(xsize, ysize);
36 std::mt19937_64 rng(129);
37 std::uniform_real_distribution<float> dist(0.0f, 1.0f);
38 if (is_gray) {
39 for (size_t y = 0; y < ysize; ++y) {
40 float* JXL_RESTRICT row0 = image.PlaneRow(0, y);
41 float* JXL_RESTRICT row1 = image.PlaneRow(1, y);
42 float* JXL_RESTRICT row2 = image.PlaneRow(2, y);
43 for (size_t x = 0; x < xsize; ++x) {
44 row0[x] = row1[x] = row2[x] = dist(rng);
45 }
46 }
47 } else {
48 RandomFillImage(&image, 1.0f);
49 }
50 CodecInOut io;
51
52 if (bits_per_sample == 32) {
53 io.metadata.m.SetFloat32Samples();
54 } else {
55 io.metadata.m.SetUintSamples(bits_per_sample);
56 }
57 io.metadata.m.color_encoding = c_native;
58 io.SetFromImage(std::move(image), c_native);
59 if (add_alpha) {
60 ImageF alpha(xsize, ysize);
61 RandomFillImage(&alpha, 1.f);
62 io.metadata.m.SetAlphaBits(bits_per_sample <= 8 ? 8 : 16);
63 io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false);
64 }
65 return io;
66 }
67
68 // Ensures reading a newly written file leads to the same image pixels.
TestRoundTrip(Codec codec,const size_t xsize,const size_t ysize,const bool is_gray,const bool add_alpha,const size_t bits_per_sample,ThreadPool * pool)69 void TestRoundTrip(Codec codec, const size_t xsize, const size_t ysize,
70 const bool is_gray, const bool add_alpha,
71 const size_t bits_per_sample, ThreadPool* pool) {
72 // JPEG encoding is not lossless.
73 if (codec == Codec::kJPG) return;
74 if (codec == Codec::kPNM && add_alpha) return;
75 // Our EXR codec always uses 16-bit premultiplied alpha, does not support
76 // grayscale, and somehow does not have sufficient precision for this test.
77 if (codec == Codec::kEXR) return;
78 printf("Codec %s bps:%zu gr:%d al:%d\n",
79 ExtensionFromCodec(codec, is_gray, bits_per_sample).c_str(),
80 bits_per_sample, is_gray, add_alpha);
81
82 ColorEncoding c_native;
83 c_native.SetColorSpace(is_gray ? ColorSpace::kGray : ColorSpace::kRGB);
84 // Note: this must not be wider than c_external, otherwise gamut clipping
85 // will cause large round-trip errors.
86 c_native.primaries = Primaries::kP3;
87 c_native.tf.SetTransferFunction(TransferFunction::kLinear);
88 JXL_CHECK(c_native.CreateICC());
89
90 // Generally store same color space to reduce round trip errors..
91 ColorEncoding c_external = c_native;
92 // .. unless we have enough precision for some transforms.
93 if (bits_per_sample >= 16) {
94 c_external.white_point = WhitePoint::kE;
95 c_external.primaries = Primaries::k2100;
96 c_external.tf.SetTransferFunction(TransferFunction::kSRGB);
97 }
98 JXL_CHECK(c_external.CreateICC());
99
100 const CodecInOut io = CreateTestImage(xsize, ysize, is_gray, add_alpha,
101 bits_per_sample, c_native);
102 const ImageBundle& ib1 = io.Main();
103
104 PaddedBytes encoded;
105 JXL_CHECK(Encode(io, codec, c_external, bits_per_sample, &encoded, pool));
106
107 CodecInOut io2;
108 ColorHints color_hints;
109 io2.target_nits = io.metadata.m.IntensityTarget();
110 // Only for PNM because PNG will warn about ignoring them.
111 if (codec == Codec::kPNM) {
112 color_hints.Add("color_space", Description(c_external));
113 }
114 JXL_CHECK(SetFromBytes(Span<const uint8_t>(encoded), color_hints, &io2, pool,
115 nullptr));
116 ImageBundle& ib2 = io2.Main();
117
118 EXPECT_EQ(Description(c_external),
119 Description(io2.metadata.m.color_encoding));
120
121 // See c_external above - for low bits_per_sample the encoded space is
122 // already the same.
123 if (bits_per_sample < 16) {
124 EXPECT_EQ(Description(ib1.c_current()), Description(ib2.c_current()));
125 }
126
127 if (add_alpha) {
128 EXPECT_TRUE(SamePixels(ib1.alpha(), *ib2.alpha()));
129 }
130
131 JXL_CHECK(ib2.TransformTo(ib1.c_current(), pool));
132
133 double max_l1, max_rel;
134 // Round-trip tolerances must be higher than in external_image_test because
135 // codecs do not support unbounded ranges.
136 #if JPEGXL_ENABLE_SKCMS
137 if (bits_per_sample <= 12) {
138 max_l1 = 0.5;
139 max_rel = 6E-3;
140 } else {
141 max_l1 = 1E-3;
142 max_rel = 5E-4;
143 }
144 #else // JPEGXL_ENABLE_SKCMS
145 if (bits_per_sample <= 12) {
146 max_l1 = 0.5;
147 max_rel = 6E-3;
148 } else if (bits_per_sample == 16) {
149 max_l1 = 3E-3;
150 max_rel = 1E-4;
151 } else {
152 #ifdef __ARM_ARCH
153 // pow() implementation in arm is a bit less precise than in x86 and
154 // therefore we need a bigger error margin in this case.
155 max_l1 = 1E-7;
156 max_rel = 1E-4;
157 #else
158 max_l1 = 1E-7;
159 max_rel = 1E-5;
160 #endif
161 }
162 #endif // JPEGXL_ENABLE_SKCMS
163
164 VerifyRelativeError(ib1.color(), *ib2.color(), max_l1, max_rel);
165 }
166
167 #if 0
168 TEST(CodecTest, TestRoundTrip) {
169 ThreadPoolInternal pool(12);
170
171 const size_t xsize = 7;
172 const size_t ysize = 4;
173
174 for (Codec codec : Values<Codec>()) {
175 for (int bits_per_sample : {8, 10, 12, 16, 32}) {
176 for (bool is_gray : {false, true}) {
177 for (bool add_alpha : {false, true}) {
178 TestRoundTrip(codec, xsize, ysize, is_gray, add_alpha,
179 static_cast<size_t>(bits_per_sample), &pool);
180 }
181 }
182 }
183 }
184 }
185 #endif
186
DecodeRoundtrip(const std::string & pathname,Codec expected_codec,ThreadPool * pool,const ColorHints & color_hints=ColorHints ())187 CodecInOut DecodeRoundtrip(const std::string& pathname, Codec expected_codec,
188 ThreadPool* pool,
189 const ColorHints& color_hints = ColorHints()) {
190 CodecInOut io;
191 const PaddedBytes orig = ReadTestData(pathname);
192 JXL_CHECK(
193 SetFromBytes(Span<const uint8_t>(orig), color_hints, &io, pool, nullptr));
194 const ImageBundle& ib1 = io.Main();
195
196 // Encode/Decode again to make sure Encode carries through all metadata.
197 PaddedBytes encoded;
198 JXL_CHECK(Encode(io, expected_codec, io.metadata.m.color_encoding,
199 io.metadata.m.bit_depth.bits_per_sample, &encoded, pool));
200
201 CodecInOut io2;
202 JXL_CHECK(SetFromBytes(Span<const uint8_t>(encoded), color_hints, &io2, pool,
203 nullptr));
204 const ImageBundle& ib2 = io2.Main();
205 EXPECT_EQ(Description(ib1.metadata()->color_encoding),
206 Description(ib2.metadata()->color_encoding));
207 EXPECT_EQ(Description(ib1.c_current()), Description(ib2.c_current()));
208
209 size_t bits_per_sample = io2.metadata.m.bit_depth.bits_per_sample;
210
211 // "Same" pixels?
212 double max_l1 = bits_per_sample <= 12 ? 1.3 : 2E-3;
213 double max_rel = bits_per_sample <= 12 ? 6E-3 : 1E-4;
214 if (ib1.metadata()->color_encoding.IsGray()) {
215 max_rel *= 2.0;
216 } else if (ib1.metadata()->color_encoding.primaries != Primaries::kSRGB) {
217 // Need more tolerance for large gamuts (anything but sRGB)
218 max_l1 *= 1.5;
219 max_rel *= 3.0;
220 }
221 VerifyRelativeError(ib1.color(), ib2.color(), max_l1, max_rel);
222
223 // Simulate the encoder removing profile and decoder restoring it.
224 if (!ib2.metadata()->color_encoding.WantICC()) {
225 io2.metadata.m.color_encoding.InternalRemoveICC();
226 EXPECT_TRUE(io2.metadata.m.color_encoding.CreateICC());
227 }
228
229 return io2;
230 }
231
232 #if 0
233 TEST(CodecTest, TestMetadataSRGB) {
234 ThreadPoolInternal pool(12);
235
236 const char* paths[] = {"raw.pixls/DJI-FC6310-16bit_srgb8_v4_krita.png",
237 "raw.pixls/Google-Pixel2XL-16bit_srgb8_v4_krita.png",
238 "raw.pixls/HUAWEI-EVA-L09-16bit_srgb8_dt.png",
239 "raw.pixls/Nikon-D300-12bit_srgb8_dt.png",
240 "raw.pixls/Sony-DSC-RX1RM2-14bit_srgb8_v4_krita.png"};
241 for (const char* relative_pathname : paths) {
242 const CodecInOut io =
243 DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool);
244 EXPECT_EQ(8, io.metadata.m.bit_depth.bits_per_sample);
245 EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
246 EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample);
247
248 EXPECT_EQ(64, io.xsize());
249 EXPECT_EQ(64, io.ysize());
250 EXPECT_FALSE(io.metadata.m.HasAlpha());
251
252 const ColorEncoding& c_original = io.metadata.m.color_encoding;
253 EXPECT_FALSE(c_original.ICC().empty());
254 EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
255 EXPECT_EQ(WhitePoint::kD65, c_original.white_point);
256 EXPECT_EQ(Primaries::kSRGB, c_original.primaries);
257 EXPECT_TRUE(c_original.tf.IsSRGB());
258 }
259 }
260
261 TEST(CodecTest, TestMetadataLinear) {
262 ThreadPoolInternal pool(12);
263
264 const char* paths[3] = {
265 "raw.pixls/Google-Pixel2XL-16bit_acescg_g1_v4_krita.png",
266 "raw.pixls/HUAWEI-EVA-L09-16bit_709_g1_dt.png",
267 "raw.pixls/Nikon-D300-12bit_2020_g1_dt.png",
268 };
269 const WhitePoint white_points[3] = {WhitePoint::kCustom, WhitePoint::kD65,
270 WhitePoint::kD65};
271 const Primaries primaries[3] = {Primaries::kCustom, Primaries::kSRGB,
272 Primaries::k2100};
273
274 for (size_t i = 0; i < 3; ++i) {
275 const CodecInOut io = DecodeRoundtrip(paths[i], Codec::kPNG, &pool);
276 EXPECT_EQ(16, io.metadata.m.bit_depth.bits_per_sample);
277 EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
278 EXPECT_EQ(0, io.metadata.m.bit_depth.exponent_bits_per_sample);
279
280 EXPECT_EQ(64, io.xsize());
281 EXPECT_EQ(64, io.ysize());
282 EXPECT_FALSE(io.metadata.m.HasAlpha());
283
284 const ColorEncoding& c_original = io.metadata.m.color_encoding;
285 EXPECT_FALSE(c_original.ICC().empty());
286 EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
287 EXPECT_EQ(white_points[i], c_original.white_point);
288 EXPECT_EQ(primaries[i], c_original.primaries);
289 EXPECT_TRUE(c_original.tf.IsLinear());
290 }
291 }
292
293 TEST(CodecTest, TestMetadataICC) {
294 ThreadPoolInternal pool(12);
295
296 const char* paths[] = {
297 "raw.pixls/DJI-FC6310-16bit_709_v4_krita.png",
298 "raw.pixls/Sony-DSC-RX1RM2-14bit_709_v4_krita.png",
299 };
300 for (const char* relative_pathname : paths) {
301 const CodecInOut io =
302 DecodeRoundtrip(relative_pathname, Codec::kPNG, &pool);
303 EXPECT_GE(16, io.metadata.m.bit_depth.bits_per_sample);
304 EXPECT_LE(14, io.metadata.m.bit_depth.bits_per_sample);
305
306 EXPECT_EQ(64, io.xsize());
307 EXPECT_EQ(64, io.ysize());
308 EXPECT_FALSE(io.metadata.m.HasAlpha());
309
310 const ColorEncoding& c_original = io.metadata.m.color_encoding;
311 EXPECT_FALSE(c_original.ICC().empty());
312 EXPECT_EQ(RenderingIntent::kPerceptual, c_original.rendering_intent);
313 EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
314 EXPECT_EQ(WhitePoint::kD65, c_original.white_point);
315 EXPECT_EQ(Primaries::kSRGB, c_original.primaries);
316 EXPECT_EQ(TransferFunction::k709, c_original.tf.GetTransferFunction());
317 }
318 }
319
320 TEST(CodecTest, TestPNGSuite) {
321 ThreadPoolInternal pool(12);
322
323 // Ensure we can load PNG with text, japanese UTF-8, compressed text.
324 (void)DecodeRoundtrip("pngsuite/ct1n0g04.png", Codec::kPNG, &pool);
325 (void)DecodeRoundtrip("pngsuite/ctjn0g04.png", Codec::kPNG, &pool);
326 (void)DecodeRoundtrip("pngsuite/ctzn0g04.png", Codec::kPNG, &pool);
327
328 // Extract gAMA
329 const CodecInOut b1 =
330 DecodeRoundtrip("pngsuite/g10n3p04.png", Codec::kPNG, &pool);
331 EXPECT_TRUE(b1.metadata.color_encoding.tf.IsLinear());
332
333 // Extract cHRM
334 const CodecInOut b_p =
335 DecodeRoundtrip("pngsuite/ccwn2c08.png", Codec::kPNG, &pool);
336 EXPECT_EQ(Primaries::kSRGB, b_p.metadata.color_encoding.primaries);
337 EXPECT_EQ(WhitePoint::kD65, b_p.metadata.color_encoding.white_point);
338
339 // Extract EXIF from (new-style) dedicated chunk
340 const CodecInOut b_exif =
341 DecodeRoundtrip("pngsuite/exif2c08.png", Codec::kPNG, &pool);
342 EXPECT_EQ(978, b_exif.blobs.exif.size());
343 }
344 #endif
345
VerifyWideGamutMetadata(const std::string & relative_pathname,const Primaries primaries,ThreadPool * pool)346 void VerifyWideGamutMetadata(const std::string& relative_pathname,
347 const Primaries primaries, ThreadPool* pool) {
348 const CodecInOut io = DecodeRoundtrip(relative_pathname, Codec::kPNG, pool);
349
350 EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
351 EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
352 EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
353
354 const ColorEncoding& c_original = io.metadata.m.color_encoding;
355 EXPECT_FALSE(c_original.ICC().empty());
356 EXPECT_EQ(RenderingIntent::kAbsolute, c_original.rendering_intent);
357 EXPECT_EQ(ColorSpace::kRGB, c_original.GetColorSpace());
358 EXPECT_EQ(WhitePoint::kD65, c_original.white_point);
359 EXPECT_EQ(primaries, c_original.primaries);
360 }
361
TEST(CodecTest,TestWideGamut)362 TEST(CodecTest, TestWideGamut) {
363 ThreadPoolInternal pool(12);
364 // VerifyWideGamutMetadata("wide-gamut-tests/P3-sRGB-color-bars.png",
365 // Primaries::kP3, &pool);
366 VerifyWideGamutMetadata("wide-gamut-tests/P3-sRGB-color-ring.png",
367 Primaries::kP3, &pool);
368 // VerifyWideGamutMetadata("wide-gamut-tests/R2020-sRGB-color-bars.png",
369 // Primaries::k2100, &pool);
370 // VerifyWideGamutMetadata("wide-gamut-tests/R2020-sRGB-color-ring.png",
371 // Primaries::k2100, &pool);
372 }
373
TEST(CodecTest,TestPNM)374 TEST(CodecTest, TestPNM) { TestCodecPNM(); }
375
376 } // namespace
377 } // namespace extras
378 } // namespace jxl
379