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