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