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