codec_test.cc (16375B)
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 <jxl/codestream_header.h> 7 #include <jxl/color_encoding.h> 8 #include <jxl/encode.h> 9 #include <jxl/types.h> 10 #include <stddef.h> 11 12 #include <algorithm> 13 #include <cstdint> 14 #include <cstdio> 15 #include <cstring> 16 #include <memory> 17 #include <sstream> 18 #include <string> 19 #include <utility> 20 #include <vector> 21 22 #include "lib/extras/common.h" 23 #include "lib/extras/dec/color_hints.h" 24 #include "lib/extras/dec/decode.h" 25 #include "lib/extras/dec/pnm.h" 26 #include "lib/extras/enc/encode.h" 27 #include "lib/extras/packed_image.h" 28 #include "lib/jxl/base/byte_order.h" 29 #include "lib/jxl/base/random.h" 30 #include "lib/jxl/base/span.h" 31 #include "lib/jxl/base/status.h" 32 #include "lib/jxl/color_encoding_internal.h" 33 #include "lib/jxl/test_utils.h" 34 #include "lib/jxl/testing.h" 35 36 namespace jxl { 37 38 using test::ThreadPoolForTests; 39 40 namespace extras { 41 namespace { 42 43 using ::testing::AllOf; 44 using ::testing::Contains; 45 using ::testing::Field; 46 using ::testing::IsEmpty; 47 using ::testing::SizeIs; 48 49 std::string ExtensionFromCodec(Codec codec, const bool is_gray, 50 const bool has_alpha, 51 const size_t bits_per_sample) { 52 switch (codec) { 53 case Codec::kJPG: 54 return ".jpg"; 55 case Codec::kPGX: 56 return ".pgx"; 57 case Codec::kPNG: 58 return ".png"; 59 case Codec::kPNM: 60 if (bits_per_sample == 32) return ".pfm"; 61 if (has_alpha) return ".pam"; 62 return is_gray ? ".pgm" : ".ppm"; 63 case Codec::kEXR: 64 return ".exr"; 65 default: 66 return std::string(); 67 } 68 } 69 70 void VerifySameImage(const PackedImage& im0, size_t bits_per_sample0, 71 const PackedImage& im1, size_t bits_per_sample1, 72 bool lossless = true) { 73 ASSERT_EQ(im0.xsize, im1.xsize); 74 ASSERT_EQ(im0.ysize, im1.ysize); 75 ASSERT_EQ(im0.format.num_channels, im1.format.num_channels); 76 auto get_factor = [](JxlPixelFormat f, size_t bits) -> double { 77 return 1.0 / ((1u << std::min(test::GetPrecision(f.data_type), bits)) - 1); 78 }; 79 double factor0 = get_factor(im0.format, bits_per_sample0); 80 double factor1 = get_factor(im1.format, bits_per_sample1); 81 const auto* pixels0 = static_cast<const uint8_t*>(im0.pixels()); 82 const auto* pixels1 = static_cast<const uint8_t*>(im1.pixels()); 83 auto rgba0 = 84 test::ConvertToRGBA32(pixels0, im0.xsize, im0.ysize, im0.format, factor0); 85 auto rgba1 = 86 test::ConvertToRGBA32(pixels1, im1.xsize, im1.ysize, im1.format, factor1); 87 double tolerance = 88 lossless ? 0.5 * std::min(factor0, factor1) : 3.0f / 255.0f; 89 if (bits_per_sample0 == 32 || bits_per_sample1 == 32) { 90 tolerance = 0.5 * std::max(factor0, factor1); 91 } 92 for (size_t y = 0; y < im0.ysize; ++y) { 93 for (size_t x = 0; x < im0.xsize; ++x) { 94 for (size_t c = 0; c < im0.format.num_channels; ++c) { 95 size_t ix = (y * im0.xsize + x) * 4 + c; 96 double val0 = rgba0[ix]; 97 double val1 = rgba1[ix]; 98 ASSERT_NEAR(val1, val0, tolerance) 99 << "y = " << y << " x = " << x << " c = " << c; 100 } 101 } 102 } 103 } 104 105 JxlColorEncoding CreateTestColorEncoding(bool is_gray) { 106 JxlColorEncoding c; 107 c.color_space = is_gray ? JXL_COLOR_SPACE_GRAY : JXL_COLOR_SPACE_RGB; 108 c.white_point = JXL_WHITE_POINT_D65; 109 c.primaries = JXL_PRIMARIES_P3; 110 c.rendering_intent = JXL_RENDERING_INTENT_RELATIVE; 111 c.transfer_function = JXL_TRANSFER_FUNCTION_LINEAR; 112 // Roundtrip through internal color encoding to fill in primaries and white 113 // point CIE xy coordinates. 114 ColorEncoding c_internal; 115 JXL_CHECK(c_internal.FromExternal(c)); 116 c = c_internal.ToExternal(); 117 return c; 118 } 119 120 std::vector<uint8_t> GenerateICC(JxlColorEncoding color_encoding) { 121 ColorEncoding c; 122 JXL_CHECK(c.FromExternal(color_encoding)); 123 JXL_CHECK(!c.ICC().empty()); 124 return c.ICC(); 125 } 126 127 void StoreRandomValue(uint8_t* out, Rng* rng, JxlPixelFormat format, 128 size_t bits_per_sample) { 129 uint64_t max_val = (1ull << bits_per_sample) - 1; 130 if (format.data_type == JXL_TYPE_UINT8) { 131 *out = rng->UniformU(0, max_val); 132 } else if (format.data_type == JXL_TYPE_UINT16) { 133 uint32_t val = rng->UniformU(0, max_val); 134 if (format.endianness == JXL_BIG_ENDIAN) { 135 StoreBE16(val, out); 136 } else { 137 StoreLE16(val, out); 138 } 139 } else { 140 ASSERT_EQ(format.data_type, JXL_TYPE_FLOAT); 141 float val = rng->UniformF(0.0, 1.0); 142 uint32_t uval; 143 memcpy(&uval, &val, 4); 144 if (format.endianness == JXL_BIG_ENDIAN) { 145 StoreBE32(uval, out); 146 } else { 147 StoreLE32(uval, out); 148 } 149 } 150 } 151 152 void FillPackedImage(size_t bits_per_sample, PackedImage* image) { 153 JxlPixelFormat format = image->format; 154 size_t bytes_per_channel = PackedImage::BitsPerChannel(format.data_type) / 8; 155 uint8_t* out = static_cast<uint8_t*>(image->pixels()); 156 size_t stride = image->xsize * format.num_channels * bytes_per_channel; 157 ASSERT_EQ(image->pixels_size, image->ysize * stride); 158 Rng rng(129); 159 for (size_t y = 0; y < image->ysize; ++y) { 160 for (size_t x = 0; x < image->xsize; ++x) { 161 for (size_t c = 0; c < format.num_channels; ++c) { 162 StoreRandomValue(out, &rng, format, bits_per_sample); 163 out += bytes_per_channel; 164 } 165 } 166 } 167 } 168 169 struct TestImageParams { 170 Codec codec; 171 size_t xsize; 172 size_t ysize; 173 size_t bits_per_sample; 174 bool is_gray; 175 bool add_alpha; 176 bool big_endian; 177 bool add_extra_channels; 178 179 bool ShouldTestRoundtrip() const { 180 if (codec == Codec::kPNG) { 181 return bits_per_sample <= 16; 182 } else if (codec == Codec::kPNM) { 183 // TODO(szabadka) Make PNM encoder endianness-aware. 184 return ((bits_per_sample <= 16 && big_endian) || 185 (bits_per_sample == 32 && !add_alpha && !big_endian)); 186 } else if (codec == Codec::kPGX) { 187 return ((bits_per_sample == 8 || bits_per_sample == 16) && is_gray && 188 !add_alpha); 189 } else if (codec == Codec::kEXR) { 190 #if defined(ADDRESS_SANITIZER) || defined(MEMORY_SANITIZER) || \ 191 defined(THREAD_SANITIZER) 192 // OpenEXR 2.3 has a memory leak in IlmThread_2_3::ThreadPool 193 return false; 194 #else 195 return bits_per_sample == 32 && !is_gray; 196 #endif 197 } else if (codec == Codec::kJPG) { 198 return bits_per_sample == 8 && !add_alpha; 199 } else { 200 return false; 201 } 202 } 203 204 JxlPixelFormat PixelFormat() const { 205 JxlPixelFormat format; 206 format.num_channels = (is_gray ? 1 : 3) + (add_alpha ? 1 : 0); 207 format.data_type = (bits_per_sample == 32 ? JXL_TYPE_FLOAT 208 : bits_per_sample > 8 ? JXL_TYPE_UINT16 209 : JXL_TYPE_UINT8); 210 format.endianness = big_endian ? JXL_BIG_ENDIAN : JXL_LITTLE_ENDIAN; 211 format.align = 0; 212 return format; 213 } 214 215 std::string DebugString() const { 216 std::ostringstream os; 217 os << "bps:" << bits_per_sample << " gr:" << is_gray << " al:" << add_alpha 218 << " be: " << big_endian << " ec: " << add_extra_channels; 219 return os.str(); 220 } 221 }; 222 223 void CreateTestImage(const TestImageParams& params, PackedPixelFile* ppf) { 224 ppf->info.xsize = params.xsize; 225 ppf->info.ysize = params.ysize; 226 ppf->info.bits_per_sample = params.bits_per_sample; 227 ppf->info.exponent_bits_per_sample = params.bits_per_sample == 32 ? 8 : 0; 228 ppf->info.num_color_channels = params.is_gray ? 1 : 3; 229 ppf->info.alpha_bits = params.add_alpha ? params.bits_per_sample : 0; 230 ppf->info.alpha_premultiplied = TO_JXL_BOOL(params.codec == Codec::kEXR); 231 232 JxlColorEncoding color_encoding = CreateTestColorEncoding(params.is_gray); 233 ppf->icc = GenerateICC(color_encoding); 234 ppf->color_encoding = color_encoding; 235 236 PackedFrame frame(params.xsize, params.ysize, params.PixelFormat()); 237 FillPackedImage(params.bits_per_sample, &frame.color); 238 if (params.add_extra_channels) { 239 for (size_t i = 0; i < 7; ++i) { 240 JxlPixelFormat ec_format = params.PixelFormat(); 241 ec_format.num_channels = 1; 242 PackedImage ec(params.xsize, params.ysize, ec_format); 243 FillPackedImage(params.bits_per_sample, &ec); 244 frame.extra_channels.emplace_back(std::move(ec)); 245 PackedExtraChannel pec; 246 pec.ec_info.bits_per_sample = params.bits_per_sample; 247 pec.ec_info.type = static_cast<JxlExtraChannelType>(i); 248 ppf->extra_channels_info.emplace_back(std::move(pec)); 249 } 250 } 251 ppf->frames.emplace_back(std::move(frame)); 252 } 253 254 // Ensures reading a newly written file leads to the same image pixels. 255 void TestRoundTrip(const TestImageParams& params, ThreadPool* pool) { 256 if (!params.ShouldTestRoundtrip()) return; 257 258 std::string extension = ExtensionFromCodec( 259 params.codec, params.is_gray, params.add_alpha, params.bits_per_sample); 260 printf("Codec %s %s\n", extension.c_str(), params.DebugString().c_str()); 261 262 PackedPixelFile ppf_in; 263 CreateTestImage(params, &ppf_in); 264 265 EncodedImage encoded; 266 auto encoder = Encoder::FromExtension(extension); 267 if (!encoder) { 268 fprintf(stderr, "Skipping test because of missing codec support.\n"); 269 return; 270 } 271 ASSERT_TRUE(encoder->Encode(ppf_in, &encoded, pool)); 272 ASSERT_EQ(encoded.bitstreams.size(), 1); 273 274 PackedPixelFile ppf_out; 275 ColorHints color_hints; 276 if (params.codec == Codec::kPNM || params.codec == Codec::kPGX) { 277 color_hints.Add("color_space", 278 params.is_gray ? "Gra_D65_Rel_SRG" : "RGB_D65_SRG_Rel_SRG"); 279 } 280 ASSERT_TRUE(DecodeBytes(Bytes(encoded.bitstreams[0]), color_hints, &ppf_out)); 281 if (params.codec == Codec::kPNG && ppf_out.icc.empty()) { 282 // Decoding a PNG may drop the ICC profile if there's a valid cICP chunk. 283 // Rendering intent is not preserved in this case. 284 EXPECT_EQ(ppf_in.color_encoding.color_space, 285 ppf_out.color_encoding.color_space); 286 EXPECT_EQ(ppf_in.color_encoding.white_point, 287 ppf_out.color_encoding.white_point); 288 if (ppf_in.color_encoding.color_space != JXL_COLOR_SPACE_GRAY) { 289 EXPECT_EQ(ppf_in.color_encoding.primaries, 290 ppf_out.color_encoding.primaries); 291 } 292 EXPECT_EQ(ppf_in.color_encoding.transfer_function, 293 ppf_out.color_encoding.transfer_function); 294 EXPECT_EQ(ppf_out.color_encoding.rendering_intent, 295 JXL_RENDERING_INTENT_RELATIVE); 296 } else if (params.codec != Codec::kPNM && params.codec != Codec::kPGX && 297 params.codec != Codec::kEXR) { 298 EXPECT_EQ(ppf_in.icc, ppf_out.icc); 299 } 300 301 ASSERT_EQ(ppf_out.frames.size(), 1); 302 const auto& frame_in = ppf_in.frames[0]; 303 const auto& frame_out = ppf_out.frames[0]; 304 VerifySameImage(frame_in.color, ppf_in.info.bits_per_sample, frame_out.color, 305 ppf_out.info.bits_per_sample, 306 /*lossless=*/params.codec != Codec::kJPG); 307 ASSERT_EQ(frame_in.extra_channels.size(), frame_out.extra_channels.size()); 308 ASSERT_EQ(ppf_out.extra_channels_info.size(), 309 frame_out.extra_channels.size()); 310 for (size_t i = 0; i < frame_in.extra_channels.size(); ++i) { 311 VerifySameImage(frame_in.extra_channels[i], ppf_in.info.bits_per_sample, 312 frame_out.extra_channels[i], ppf_out.info.bits_per_sample, 313 /*lossless=*/true); 314 EXPECT_EQ(ppf_out.extra_channels_info[i].ec_info.type, 315 ppf_in.extra_channels_info[i].ec_info.type); 316 } 317 } 318 319 TEST(CodecTest, TestRoundTrip) { 320 ThreadPoolForTests pool(12); 321 322 TestImageParams params; 323 params.xsize = 7; 324 params.ysize = 4; 325 326 for (Codec codec : 327 {Codec::kPNG, Codec::kPNM, Codec::kPGX, Codec::kEXR, Codec::kJPG}) { 328 for (int bits_per_sample : {4, 8, 10, 12, 16, 32}) { 329 for (bool is_gray : {false, true}) { 330 for (bool add_alpha : {false, true}) { 331 for (bool big_endian : {false, true}) { 332 params.codec = codec; 333 params.bits_per_sample = static_cast<size_t>(bits_per_sample); 334 params.is_gray = is_gray; 335 params.add_alpha = add_alpha; 336 params.big_endian = big_endian; 337 params.add_extra_channels = false; 338 TestRoundTrip(params, &pool); 339 if (codec == Codec::kPNM && add_alpha) { 340 params.add_extra_channels = true; 341 TestRoundTrip(params, &pool); 342 } 343 } 344 } 345 } 346 } 347 } 348 } 349 350 TEST(CodecTest, LosslessPNMRoundtrip) { 351 ThreadPoolForTests pool(12); 352 353 static const char* kChannels[] = {"", "g", "ga", "rgb", "rgba"}; 354 static const char* kExtension[] = {"", ".pgm", ".pam", ".ppm", ".pam"}; 355 for (size_t bit_depth = 1; bit_depth <= 16; ++bit_depth) { 356 for (size_t channels = 1; channels <= 4; ++channels) { 357 if (bit_depth == 1 && (channels == 2 || channels == 4)) continue; 358 std::string extension(kExtension[channels]); 359 std::string filename = "jxl/flower/flower_small." + 360 std::string(kChannels[channels]) + ".depth" + 361 std::to_string(bit_depth) + extension; 362 const std::vector<uint8_t> orig = jxl::test::ReadTestData(filename); 363 364 PackedPixelFile ppf; 365 ColorHints color_hints; 366 color_hints.Add("color_space", 367 channels < 3 ? "Gra_D65_Rel_SRG" : "RGB_D65_SRG_Rel_SRG"); 368 ASSERT_TRUE( 369 DecodeBytes(Bytes(orig.data(), orig.size()), color_hints, &ppf)); 370 371 EncodedImage encoded; 372 auto encoder = Encoder::FromExtension(extension); 373 ASSERT_TRUE(encoder.get()); 374 ASSERT_TRUE(encoder->Encode(ppf, &encoded, &pool)); 375 ASSERT_EQ(encoded.bitstreams.size(), 1); 376 ASSERT_EQ(orig.size(), encoded.bitstreams[0].size()); 377 EXPECT_EQ(0, 378 memcmp(orig.data(), encoded.bitstreams[0].data(), orig.size())); 379 } 380 } 381 } 382 383 TEST(CodecTest, TestPNM) { TestCodecPNM(); } 384 385 TEST(CodecTest, FormatNegotiation) { 386 const std::vector<JxlPixelFormat> accepted_formats = { 387 {/*num_channels=*/4, 388 /*data_type=*/JXL_TYPE_UINT16, 389 /*endianness=*/JXL_NATIVE_ENDIAN, 390 /*align=*/0}, 391 {/*num_channels=*/3, 392 /*data_type=*/JXL_TYPE_UINT8, 393 /*endianness=*/JXL_NATIVE_ENDIAN, 394 /*align=*/0}, 395 {/*num_channels=*/3, 396 /*data_type=*/JXL_TYPE_UINT16, 397 /*endianness=*/JXL_NATIVE_ENDIAN, 398 /*align=*/0}, 399 {/*num_channels=*/1, 400 /*data_type=*/JXL_TYPE_UINT8, 401 /*endianness=*/JXL_NATIVE_ENDIAN, 402 /*align=*/0}, 403 }; 404 405 JxlBasicInfo info; 406 JxlEncoderInitBasicInfo(&info); 407 info.bits_per_sample = 12; 408 info.num_color_channels = 2; 409 410 JxlPixelFormat format; 411 EXPECT_FALSE(SelectFormat(accepted_formats, info, &format)); 412 413 info.num_color_channels = 3; 414 ASSERT_TRUE(SelectFormat(accepted_formats, info, &format)); 415 EXPECT_EQ(format.num_channels, info.num_color_channels); 416 // 16 is the smallest accepted format that can accommodate the 12-bit data. 417 EXPECT_EQ(format.data_type, JXL_TYPE_UINT16); 418 } 419 420 TEST(CodecTest, EncodeToPNG) { 421 ThreadPool* const pool = nullptr; 422 423 std::unique_ptr<Encoder> png_encoder = Encoder::FromExtension(".png"); 424 if (!png_encoder) { 425 fprintf(stderr, "Skipping test because of missing codec support.\n"); 426 return; 427 } 428 429 const std::vector<uint8_t> original_png = jxl::test::ReadTestData( 430 "external/wesaturate/500px/tmshre_riaphotographs_srgb8.png"); 431 PackedPixelFile ppf; 432 ASSERT_TRUE(extras::DecodeBytes(Bytes(original_png), ColorHints(), &ppf)); 433 434 const JxlPixelFormat& format = ppf.frames.front().color.format; 435 ASSERT_THAT( 436 png_encoder->AcceptedFormats(), 437 Contains(AllOf(Field(&JxlPixelFormat::num_channels, format.num_channels), 438 Field(&JxlPixelFormat::data_type, format.data_type), 439 Field(&JxlPixelFormat::endianness, format.endianness)))); 440 EncodedImage encoded_png; 441 ASSERT_TRUE(png_encoder->Encode(ppf, &encoded_png, pool)); 442 EXPECT_THAT(encoded_png.icc, IsEmpty()); 443 ASSERT_THAT(encoded_png.bitstreams, SizeIs(1)); 444 445 PackedPixelFile decoded_ppf; 446 ASSERT_TRUE(extras::DecodeBytes(Bytes(encoded_png.bitstreams.front()), 447 ColorHints(), &decoded_ppf)); 448 449 ASSERT_EQ(decoded_ppf.info.bits_per_sample, ppf.info.bits_per_sample); 450 ASSERT_EQ(decoded_ppf.frames.size(), 1); 451 VerifySameImage(ppf.frames[0].color, ppf.info.bits_per_sample, 452 decoded_ppf.frames[0].color, 453 decoded_ppf.info.bits_per_sample); 454 } 455 456 } // namespace 457 } // namespace extras 458 } // namespace jxl