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