summaryrefslogtreecommitdiff
path: root/media/libjxl/src/lib/jxl/jxl_test.cc
diff options
context:
space:
mode:
Diffstat (limited to 'media/libjxl/src/lib/jxl/jxl_test.cc')
-rw-r--r--media/libjxl/src/lib/jxl/jxl_test.cc1697
1 files changed, 1697 insertions, 0 deletions
diff --git a/media/libjxl/src/lib/jxl/jxl_test.cc b/media/libjxl/src/lib/jxl/jxl_test.cc
new file mode 100644
index 0000000000..997dea9643
--- /dev/null
+++ b/media/libjxl/src/lib/jxl/jxl_test.cc
@@ -0,0 +1,1697 @@
+// Copyright (c) the JPEG XL Project Authors. All rights reserved.
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+#include <stdint.h>
+#include <stdio.h>
+
+#include <array>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "gtest/gtest.h"
+#include "lib/extras/codec.h"
+#include "lib/jxl/aux_out.h"
+#include "lib/jxl/base/compiler_specific.h"
+#include "lib/jxl/base/data_parallel.h"
+#include "lib/jxl/base/override.h"
+#include "lib/jxl/base/padded_bytes.h"
+#include "lib/jxl/base/printf_macros.h"
+#include "lib/jxl/base/thread_pool_internal.h"
+#include "lib/jxl/codec_in_out.h"
+#include "lib/jxl/codec_y4m_testonly.h"
+#include "lib/jxl/color_encoding_internal.h"
+#include "lib/jxl/color_management.h"
+#include "lib/jxl/dec_file.h"
+#include "lib/jxl/dec_params.h"
+#include "lib/jxl/enc_butteraugli_comparator.h"
+#include "lib/jxl/enc_butteraugli_pnorm.h"
+#include "lib/jxl/enc_cache.h"
+#include "lib/jxl/enc_file.h"
+#include "lib/jxl/enc_params.h"
+#include "lib/jxl/fake_parallel_runner_testonly.h"
+#include "lib/jxl/image.h"
+#include "lib/jxl/image_bundle.h"
+#include "lib/jxl/image_ops.h"
+#include "lib/jxl/image_test_utils.h"
+#include "lib/jxl/jpeg/dec_jpeg_data_writer.h"
+#include "lib/jxl/jpeg/enc_jpeg_data.h"
+#include "lib/jxl/modular/options.h"
+#include "lib/jxl/test_utils.h"
+#include "lib/jxl/testdata.h"
+#include "tools/box/box.h"
+
+namespace jxl {
+namespace {
+using test::Roundtrip;
+
+#define JXL_TEST_NL 0 // Disabled in code
+
+void CreateImage1x1(CodecInOut* io) {
+ Image3F image(1, 1);
+ ZeroFillImage(&image);
+ io->metadata.m.SetUintSamples(8);
+ io->metadata.m.color_encoding = ColorEncoding::SRGB();
+ io->SetFromImage(std::move(image), io->metadata.m.color_encoding);
+}
+
+TEST(JxlTest, HeaderSize) {
+ CodecInOut io;
+ CreateImage1x1(&io);
+
+ CompressParams cparams;
+ cparams.butteraugli_distance = 1.5;
+ DecompressParams dparams;
+ ThreadPool* pool = nullptr;
+
+ {
+ CodecInOut io2;
+ AuxOut aux_out;
+ Roundtrip(&io, cparams, dparams, pool, &io2, &aux_out);
+ EXPECT_LE(aux_out.layers[kLayerHeader].total_bits, 34u);
+ }
+
+ {
+ CodecInOut io2;
+ io.metadata.m.SetAlphaBits(8);
+ ImageF alpha(1, 1);
+ alpha.Row(0)[0] = 1;
+ io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false);
+ AuxOut aux_out;
+ Roundtrip(&io, cparams, dparams, pool, &io2, &aux_out);
+ EXPECT_LE(aux_out.layers[kLayerHeader].total_bits, 46u);
+ }
+}
+
+TEST(JxlTest, RoundtripSinglePixel) {
+ CodecInOut io;
+ CreateImage1x1(&io);
+
+ CompressParams cparams;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+ ThreadPool* pool = nullptr;
+ CodecInOut io2;
+ Roundtrip(&io, cparams, dparams, pool, &io2);
+}
+
+// Changing serialized signature causes Decode to fail.
+#ifndef JXL_CRASH_ON_ERROR
+TEST(JxlTest, RoundtripMarker) {
+ CodecInOut io;
+ CreateImage1x1(&io);
+
+ CompressParams cparams;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+ AuxOut* aux_out = nullptr;
+ ThreadPool* pool = nullptr;
+
+ PassesEncoderState enc_state;
+ for (size_t i = 0; i < 2; ++i) {
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ compressed[i] ^= 0xFF;
+ CodecInOut io2;
+ EXPECT_FALSE(DecodeFile(dparams, compressed, &io2, pool));
+ }
+}
+#endif
+
+TEST(JxlTest, RoundtripTinyFast) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ io.ShrinkTo(32, 32);
+
+ CompressParams cparams;
+ cparams.speed_tier = SpeedTier::kSquirrel;
+ cparams.butteraugli_distance = 4.0f;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ const size_t enc_bytes = Roundtrip(&io, cparams, dparams, pool, &io2);
+ printf("32x32 image size %" PRIuS " bytes\n", enc_bytes);
+}
+
+TEST(JxlTest, RoundtripSmallD1) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
+ CompressParams cparams;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+
+ CodecInOut io_out;
+ size_t compressed_size;
+
+ {
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ io.ShrinkTo(io.xsize() / 8, io.ysize() / 8);
+
+ compressed_size = Roundtrip(&io, cparams, dparams, pool, &io_out);
+ EXPECT_LE(compressed_size, 1000u);
+ EXPECT_THAT(ButteraugliDistance(io, io_out, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.0));
+ }
+
+ {
+ // And then, with a lower intensity target than the default, the bitrate
+ // should be smaller.
+ CodecInOut io_dim;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io_dim, pool));
+ io_dim.metadata.m.SetIntensityTarget(100);
+ io_dim.ShrinkTo(io_dim.xsize() / 8, io_dim.ysize() / 8);
+ EXPECT_LT(Roundtrip(&io_dim, cparams, dparams, pool, &io_out),
+ compressed_size);
+ EXPECT_THAT(
+ ButteraugliDistance(io_dim, io_out, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.5));
+ EXPECT_EQ(io_dim.metadata.m.IntensityTarget(),
+ io_out.metadata.m.IntensityTarget());
+ }
+}
+
+TEST(JxlTest, RoundtripOtherTransforms) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/64px/a2d1un_nkitzmiller_srgb8.png");
+ std::unique_ptr<CodecInOut> io = jxl::make_unique<CodecInOut>();
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), io.get(), pool));
+
+ CompressParams cparams;
+ // Slow modes access linear image for adaptive quant search
+ cparams.speed_tier = SpeedTier::kKitten;
+ cparams.color_transform = ColorTransform::kNone;
+ cparams.butteraugli_distance = 5.0f;
+ DecompressParams dparams;
+
+ std::unique_ptr<CodecInOut> io2 = jxl::make_unique<CodecInOut>();
+ const size_t compressed_size =
+ Roundtrip(io.get(), cparams, dparams, pool, io2.get());
+ EXPECT_LE(compressed_size, 23000u);
+ EXPECT_THAT(ButteraugliDistance(*io, *io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(3.0));
+
+ // Check the consistency when performing another roundtrip.
+ std::unique_ptr<CodecInOut> io3 = jxl::make_unique<CodecInOut>();
+ const size_t compressed_size2 =
+ Roundtrip(io.get(), cparams, dparams, pool, io3.get());
+ EXPECT_LE(compressed_size2, 23000u);
+ EXPECT_THAT(ButteraugliDistance(*io, *io3, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(3.0));
+}
+
+TEST(JxlTest, RoundtripResample2) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ io.ShrinkTo(io.xsize(), io.ysize());
+ CompressParams cparams;
+ cparams.resampling = 2;
+ cparams.speed_tier = SpeedTier::kFalcon;
+ DecompressParams dparams;
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 17000u);
+ EXPECT_THAT(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()),
+ IsSlightlyBelow(90));
+}
+
+TEST(JxlTest, RoundtripResample2MT) {
+ ThreadPoolInternal pool(4);
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ // image has to be large enough to have multiple groups after downsampling
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+ CompressParams cparams;
+ cparams.resampling = 2;
+ cparams.speed_tier = SpeedTier::kFalcon;
+ DecompressParams dparams;
+ CodecInOut io2;
+ // TODO(veluca): Figure out why msan and release produce different
+ // file size.
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 64500u);
+ EXPECT_THAT(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()),
+ IsSlightlyBelow(320));
+}
+
+// Roundtrip the image using a parallel runner that executes single-threaded but
+// in random order.
+TEST(JxlTest, RoundtripOutOfOrderProcessing) {
+ FakeParallelRunner fake_pool(/*order_seed=*/123, /*num_threads=*/8);
+ ThreadPool pool(&JxlFakeParallelRunner, &fake_pool);
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+ // Image size is selected so that the block border needed is larger than the
+ // amount of pixels available on the next block.
+ io.ShrinkTo(513, 515);
+
+ CompressParams cparams;
+ // Force epf so we end up needing a lot of border.
+ cparams.epf = 3;
+
+ DecompressParams dparams;
+ CodecInOut io2;
+ Roundtrip(&io, cparams, dparams, &pool, &io2);
+
+ EXPECT_GE(1.5, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, &pool));
+}
+
+TEST(JxlTest, RoundtripOutOfOrderProcessingBorder) {
+ FakeParallelRunner fake_pool(/*order_seed=*/47, /*num_threads=*/8);
+ ThreadPool pool(&JxlFakeParallelRunner, &fake_pool);
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+ // Image size is selected so that the block border needed is larger than the
+ // amount of pixels available on the next block.
+ io.ShrinkTo(513, 515);
+
+ CompressParams cparams;
+ // Force epf so we end up needing a lot of border.
+ cparams.epf = 3;
+ cparams.resampling = 2;
+
+ DecompressParams dparams;
+ CodecInOut io2;
+ Roundtrip(&io, cparams, dparams, &pool, &io2);
+
+ EXPECT_GE(2.8, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, &pool));
+}
+
+TEST(JxlTest, RoundtripResample4) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ io.ShrinkTo(io.xsize(), io.ysize());
+ CompressParams cparams;
+ cparams.resampling = 4;
+ DecompressParams dparams;
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 6000u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(22));
+}
+
+TEST(JxlTest, RoundtripResample8) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ io.ShrinkTo(io.xsize(), io.ysize());
+ CompressParams cparams;
+ cparams.resampling = 8;
+ DecompressParams dparams;
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2100u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(50));
+}
+
+TEST(JxlTest, RoundtripUnalignedD2) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ io.ShrinkTo(io.xsize() / 12, io.ysize() / 7);
+
+ CompressParams cparams;
+ cparams.butteraugli_distance = 2.0;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 700u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.7));
+}
+
+#if JXL_TEST_NL
+
+TEST(JxlTest, RoundtripMultiGroupNL) {
+ ThreadPoolInternal pool(4);
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+ io.ShrinkTo(600, 1024); // partial X, full Y group
+
+ CompressParams cparams;
+ DecompressParams dparams;
+
+ cparams.fast_mode = true;
+ cparams.butteraugli_distance = 1.0f;
+ CodecInOut io2;
+ Roundtrip(&io, cparams, dparams, &pool, &io2);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, &pool),
+ IsSlightlyBelow(0.9f));
+
+ cparams.butteraugli_distance = 2.0f;
+ CodecInOut io3;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 80000u);
+ EXPECT_THAT(ButteraugliDistance(io, io3, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, &pool),
+ IsSlightlyBelow(1.5f));
+}
+
+#endif
+
+TEST(JxlTest, RoundtripMultiGroup) {
+ ThreadPoolInternal pool(4);
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+ io.ShrinkTo(600, 1024);
+
+ CompressParams cparams;
+ DecompressParams dparams;
+
+ cparams.butteraugli_distance = 1.0f;
+ cparams.speed_tier = SpeedTier::kKitten;
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u);
+ EXPECT_THAT(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()),
+ IsSlightlyBelow(15));
+
+ cparams.butteraugli_distance = 2.0f;
+ cparams.speed_tier = SpeedTier::kWombat;
+ CodecInOut io3;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 22100u);
+ EXPECT_THAT(ComputeDistance2(io.Main(), io3.Main(), GetJxlCms()),
+ IsSlightlyBelow(24));
+}
+
+TEST(JxlTest, RoundtripLargeFast) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+
+ CompressParams cparams;
+ cparams.speed_tier = SpeedTier::kSquirrel;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 265000u);
+}
+
+TEST(JxlTest, RoundtripDotsForceEpf) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+
+ CompressParams cparams;
+ cparams.epf = 2;
+ cparams.dots = Override::kOn;
+ cparams.speed_tier = SpeedTier::kSquirrel;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 265000u);
+}
+
+// Checks for differing size/distance in two consecutive runs of distance 2,
+// which involves additional processing including adaptive reconstruction.
+// Failing this may be a sign of race conditions or invalid memory accesses.
+TEST(JxlTest, RoundtripD2Consistent) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+
+ CompressParams cparams;
+ cparams.speed_tier = SpeedTier::kSquirrel;
+ cparams.butteraugli_distance = 2.0;
+ DecompressParams dparams;
+
+ // Try each xsize mod kBlockDim to verify right border handling.
+ for (size_t xsize = 48; xsize > 40; --xsize) {
+ io.ShrinkTo(xsize, 15);
+
+ CodecInOut io2;
+ const size_t size2 = Roundtrip(&io, cparams, dparams, &pool, &io2);
+
+ CodecInOut io3;
+ const size_t size3 = Roundtrip(&io, cparams, dparams, &pool, &io3);
+
+ // Exact same compressed size.
+ EXPECT_EQ(size2, size3);
+
+ // Exact same distance.
+ const float dist2 = ComputeDistance2(io.Main(), io2.Main(), GetJxlCms());
+ const float dist3 = ComputeDistance2(io.Main(), io3.Main(), GetJxlCms());
+ EXPECT_EQ(dist2, dist3);
+ }
+}
+
+// Same as above, but for full image, testing multiple groups.
+TEST(JxlTest, RoundtripLargeConsistent) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+
+ CompressParams cparams;
+ cparams.speed_tier = SpeedTier::kSquirrel;
+ cparams.butteraugli_distance = 2.0;
+ DecompressParams dparams;
+
+ // Try each xsize mod kBlockDim to verify right border handling.
+ CodecInOut io2;
+ const size_t size2 = Roundtrip(&io, cparams, dparams, &pool, &io2);
+
+ CodecInOut io3;
+ const size_t size3 = Roundtrip(&io, cparams, dparams, &pool, &io3);
+
+ // Exact same compressed size.
+ EXPECT_EQ(size2, size3);
+
+ // Exact same distance.
+ const float dist2 = ComputeDistance2(io.Main(), io2.Main(), GetJxlCms());
+ const float dist3 = ComputeDistance2(io.Main(), io3.Main(), GetJxlCms());
+ EXPECT_EQ(dist2, dist3);
+}
+
+#if JXL_TEST_NL
+
+TEST(JxlTest, RoundtripSmallNL) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ io.ShrinkTo(io.xsize() / 8, io.ysize() / 8);
+
+ CompressParams cparams;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 1500u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.7));
+}
+
+#endif
+
+TEST(JxlTest, RoundtripNoGaborishNoAR) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+
+ CompressParams cparams;
+ cparams.gaborish = Override::kOff;
+ cparams.epf = 0;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 40000u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(2.0));
+}
+
+TEST(JxlTest, RoundtripSmallNoGaborish) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ io.ShrinkTo(io.xsize() / 8, io.ysize() / 8);
+
+ CompressParams cparams;
+ cparams.gaborish = Override::kOff;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 900u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.2));
+}
+
+TEST(JxlTest, RoundtripSmallPatchesAlpha) {
+ ThreadPool* pool = nullptr;
+ CodecInOut io;
+ io.metadata.m.color_encoding = ColorEncoding::LinearSRGB();
+ Image3F black_with_small_lines(256, 256);
+ ImageF alpha(black_with_small_lines.xsize(), black_with_small_lines.ysize());
+ ZeroFillImage(&black_with_small_lines);
+ // This pattern should be picked up by the patch detection heuristics.
+ for (size_t y = 0; y < black_with_small_lines.ysize(); y++) {
+ float* JXL_RESTRICT row = black_with_small_lines.PlaneRow(1, y);
+ for (size_t x = 0; x < black_with_small_lines.xsize(); x++) {
+ if (x % 4 == 0 && (y / 32) % 4 == 0) row[x] = 127.0f;
+ }
+ }
+ io.metadata.m.SetAlphaBits(8);
+ io.SetFromImage(std::move(black_with_small_lines),
+ ColorEncoding::LinearSRGB());
+ FillImage(1.0f, &alpha);
+ io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false);
+
+ CompressParams cparams;
+ cparams.speed_tier = SpeedTier::kSquirrel;
+ cparams.butteraugli_distance = 0.1f;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(0.04f));
+}
+
+TEST(JxlTest, RoundtripSmallPatches) {
+ ThreadPool* pool = nullptr;
+ CodecInOut io;
+ io.metadata.m.color_encoding = ColorEncoding::LinearSRGB();
+ Image3F black_with_small_lines(256, 256);
+ ZeroFillImage(&black_with_small_lines);
+ // This pattern should be picked up by the patch detection heuristics.
+ for (size_t y = 0; y < black_with_small_lines.ysize(); y++) {
+ float* JXL_RESTRICT row = black_with_small_lines.PlaneRow(1, y);
+ for (size_t x = 0; x < black_with_small_lines.xsize(); x++) {
+ if (x % 4 == 0 && (y / 32) % 4 == 0) row[x] = 127.0f;
+ }
+ }
+ io.SetFromImage(std::move(black_with_small_lines),
+ ColorEncoding::LinearSRGB());
+
+ CompressParams cparams;
+ cparams.speed_tier = SpeedTier::kSquirrel;
+ cparams.butteraugli_distance = 0.1f;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 2000u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(0.04f));
+}
+
+// Test header encoding of original bits per sample
+TEST(JxlTest, RoundtripImageBundleOriginalBits) {
+ ThreadPool* pool = nullptr;
+
+ // Image does not matter, only io.metadata.m and io2.metadata.m are tested.
+ Image3F image(1, 1);
+ ZeroFillImage(&image);
+ CodecInOut io;
+ io.metadata.m.color_encoding = ColorEncoding::LinearSRGB();
+ io.SetFromImage(std::move(image), ColorEncoding::LinearSRGB());
+
+ CompressParams cparams;
+ DecompressParams dparams;
+
+ // Test unsigned integers from 1 to 32 bits
+ for (uint32_t bit_depth = 1; bit_depth <= 32; bit_depth++) {
+ if (bit_depth == 32) {
+ // TODO(lode): allow testing 32, however the code below ends up in
+ // enc_modular which does not support 32. We only want to test the header
+ // encoding though, so try without modular.
+ break;
+ }
+
+ io.metadata.m.SetUintSamples(bit_depth);
+ CodecInOut io2;
+ Roundtrip(&io, cparams, dparams, pool, &io2);
+
+ EXPECT_EQ(bit_depth, io2.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample);
+ EXPECT_EQ(0u, io2.metadata.m.GetAlphaBits());
+ }
+
+ // Test various existing and non-existing floating point formats
+ for (uint32_t bit_depth = 8; bit_depth <= 32; bit_depth++) {
+ if (bit_depth != 32) {
+ // TODO: test other float types once they work
+ break;
+ }
+
+ uint32_t exponent_bit_depth;
+ if (bit_depth < 10) {
+ exponent_bit_depth = 2;
+ } else if (bit_depth < 12) {
+ exponent_bit_depth = 3;
+ } else if (bit_depth < 16) {
+ exponent_bit_depth = 4;
+ } else if (bit_depth < 20) {
+ exponent_bit_depth = 5;
+ } else if (bit_depth < 24) {
+ exponent_bit_depth = 6;
+ } else if (bit_depth < 28) {
+ exponent_bit_depth = 7;
+ } else {
+ exponent_bit_depth = 8;
+ }
+
+ io.metadata.m.bit_depth.bits_per_sample = bit_depth;
+ io.metadata.m.bit_depth.floating_point_sample = true;
+ io.metadata.m.bit_depth.exponent_bits_per_sample = exponent_bit_depth;
+
+ CodecInOut io2;
+ Roundtrip(&io, cparams, dparams, pool, &io2);
+
+ EXPECT_EQ(bit_depth, io2.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_TRUE(io2.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(exponent_bit_depth,
+ io2.metadata.m.bit_depth.exponent_bits_per_sample);
+ EXPECT_EQ(0u, io2.metadata.m.GetAlphaBits());
+ }
+}
+
+TEST(JxlTest, RoundtripGrayscale) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ ASSERT_NE(io.xsize(), 0u);
+ io.ShrinkTo(128, 128);
+ EXPECT_TRUE(io.Main().IsGray());
+ EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+ EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB());
+
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+
+ {
+ CompressParams cparams;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
+ EXPECT_TRUE(io2.Main().IsGray());
+
+ EXPECT_LE(compressed.size(), 7000u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.6));
+ }
+
+ // Test with larger butteraugli distance and other settings enabled so
+ // different jxl codepaths trigger.
+ {
+ CompressParams cparams;
+ cparams.butteraugli_distance = 8.0;
+ DecompressParams dparams;
+
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
+ EXPECT_TRUE(io2.Main().IsGray());
+
+ EXPECT_LE(compressed.size(), 1300u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(6.0));
+ }
+}
+
+TEST(JxlTest, RoundtripAlpha) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+
+ ASSERT_NE(io.xsize(), 0u);
+ ASSERT_TRUE(io.metadata.m.HasAlpha());
+ ASSERT_TRUE(io.Main().HasAlpha());
+ io.ShrinkTo(300, 300);
+
+ CompressParams cparams;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+
+ EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+ EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB());
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
+
+ EXPECT_LE(compressed.size(), 10077u);
+
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.2));
+}
+
+TEST(JxlTest, RoundtripAlphaPremultiplied) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
+ CodecInOut io, io_nopremul;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io_nopremul, pool));
+
+ ASSERT_NE(io.xsize(), 0u);
+ ASSERT_TRUE(io.metadata.m.HasAlpha());
+ ASSERT_TRUE(io.Main().HasAlpha());
+ io.ShrinkTo(300, 300);
+ io_nopremul.ShrinkTo(300, 300);
+
+ CompressParams cparams;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+
+ io.PremultiplyAlpha();
+ EXPECT_TRUE(io.Main().AlphaIsPremultiplied());
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
+
+ EXPECT_LE(compressed.size(), 10000u);
+
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.2));
+ io2.Main().UnpremultiplyAlpha();
+ EXPECT_THAT(
+ ButteraugliDistance(io_nopremul, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.35));
+}
+
+TEST(JxlTest, RoundtripAlphaResampling) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+
+ ASSERT_NE(io.xsize(), 0u);
+ ASSERT_TRUE(io.metadata.m.HasAlpha());
+ ASSERT_TRUE(io.Main().HasAlpha());
+
+ CompressParams cparams;
+ cparams.resampling = 2;
+ cparams.ec_resampling = 2;
+ cparams.butteraugli_distance = 1.0;
+ cparams.speed_tier = SpeedTier::kHare;
+ DecompressParams dparams;
+
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
+
+ EXPECT_LE(compressed.size(), 15000u);
+
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(4.7));
+}
+
+TEST(JxlTest, RoundtripAlphaResamplingOnlyAlpha) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+
+ ASSERT_NE(io.xsize(), 0u);
+ ASSERT_TRUE(io.metadata.m.HasAlpha());
+ ASSERT_TRUE(io.Main().HasAlpha());
+
+ CompressParams cparams;
+ cparams.ec_resampling = 2;
+ cparams.butteraugli_distance = 1.0;
+ cparams.speed_tier = SpeedTier::kFalcon;
+ DecompressParams dparams;
+
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
+
+ EXPECT_LE(compressed.size(), 34200u);
+
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.85));
+}
+
+TEST(JxlTest, RoundtripAlphaNonMultipleOf8) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+
+ ASSERT_NE(io.xsize(), 0u);
+ ASSERT_TRUE(io.metadata.m.HasAlpha());
+ ASSERT_TRUE(io.Main().HasAlpha());
+ io.ShrinkTo(12, 12);
+
+ CompressParams cparams;
+ cparams.butteraugli_distance = 1.0;
+ DecompressParams dparams;
+
+ EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+ EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB());
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
+
+ EXPECT_LE(compressed.size(), 180u);
+
+ // TODO(robryk): Fix the following line in presence of different alpha_bits in
+ // the two contexts.
+ // EXPECT_TRUE(SamePixels(io.Main().alpha(), io2.Main().alpha()));
+ // TODO(robryk): Fix the distance estimate used in the encoder.
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(0.9));
+}
+
+TEST(JxlTest, RoundtripAlpha16) {
+ ThreadPoolInternal pool(4);
+
+ size_t xsize = 1200, ysize = 160;
+ Image3F color(xsize, ysize);
+ ImageF alpha(xsize, ysize);
+ // Generate 16-bit pattern that uses various colors and alpha values.
+ for (size_t y = 0; y < ysize; y++) {
+ for (size_t x = 0; x < xsize; x++) {
+ color.PlaneRow(0, y)[x] = (y * 65535 / ysize) * (1.0f / 65535);
+ color.PlaneRow(1, y)[x] = (x * 65535 / xsize) * (1.0f / 65535);
+ color.PlaneRow(2, y)[x] =
+ ((y + x) * 65535 / (xsize + ysize)) * (1.0f / 65535);
+ alpha.Row(y)[x] = (x * 65535 / xsize) * (1.0f / 65535);
+ }
+ }
+ const bool is_gray = false;
+ CodecInOut io;
+ io.metadata.m.SetUintSamples(16);
+ io.metadata.m.SetAlphaBits(16);
+ io.metadata.m.color_encoding = ColorEncoding::SRGB(is_gray);
+ io.SetFromImage(std::move(color), io.metadata.m.color_encoding);
+ io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false);
+
+ // The image is wider than 512 pixels to ensure multiple groups are tested.
+
+ ASSERT_NE(io.xsize(), 0u);
+ ASSERT_TRUE(io.metadata.m.HasAlpha());
+ ASSERT_TRUE(io.Main().HasAlpha());
+
+ CompressParams cparams;
+ cparams.butteraugli_distance = 0.5;
+ cparams.speed_tier = SpeedTier::kWombat;
+ DecompressParams dparams;
+
+ io.metadata.m.SetUintSamples(16);
+ EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB());
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, &pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, &pool));
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, &pool),
+ IsSlightlyBelow(0.8));
+}
+
+namespace {
+CompressParams CParamsForLossless() {
+ CompressParams cparams;
+ cparams.modular_mode = true;
+ cparams.color_transform = jxl::ColorTransform::kNone;
+ cparams.butteraugli_distance = 0.f;
+ cparams.options.predictor = {Predictor::Weighted};
+ return cparams;
+}
+} // namespace
+
+TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+
+ CompressParams cparams = CParamsForLossless();
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u);
+ // If this test fails with a very close to 0.0 but not exactly 0.0 butteraugli
+ // distance, then there is likely a floating point issue, that could be
+ // happening either in io or io2. The values of io are generated by
+ // external_image.cc, and those in io2 by the jxl decoder. If they use
+ // slightly different floating point operations (say, one casts int to float
+ // while other divides the int through 255.0f and later multiplies it by
+ // 255 again) they will get slightly different values. To fix, ensure both
+ // sides do the following formula for converting integer range 0-255 to
+ // floating point range 0.0f-255.0f: static_cast<float>(i)
+ // without any further intermediate operations.
+ // Note that this precision issue is not a problem in practice if the values
+ // are equal when rounded to 8-bit int, but currently full exact precision is
+ // tested.
+ EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0);
+}
+
+TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathWP)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+
+ CompressParams cparams = CParamsForLossless();
+ cparams.speed_tier = SpeedTier::kFalcon;
+ cparams.options.skip_encoder_fast_path = true;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u);
+ EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0);
+}
+
+TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderFastPathGradient)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+
+ CompressParams cparams = CParamsForLossless();
+ cparams.speed_tier = SpeedTier::kThunder;
+ cparams.options.skip_encoder_fast_path = true;
+ cparams.options.predictor = {Predictor::Gradient};
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u);
+ EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0);
+}
+
+TEST(JxlTest, JXL_SLOW_TEST(RoundtripLosslessNoEncoderVeryFastPathGradient)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+
+ CompressParams cparams = CParamsForLossless();
+ cparams.speed_tier = SpeedTier::kLightning;
+ cparams.options.skip_encoder_fast_path = true;
+ cparams.options.predictor = {Predictor::Gradient};
+ DecompressParams dparams;
+
+ CodecInOut io2, io3;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u);
+ EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0);
+ cparams.options.skip_encoder_fast_path = false;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io3), 3500000u);
+ EXPECT_EQ(ComputeDistance2(io.Main(), io3.Main(), GetJxlCms()), 0.0);
+}
+
+TEST(JxlTest, JXL_SLOW_TEST(RoundtripLossless8Falcon)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+
+ CompressParams cparams = CParamsForLossless();
+ cparams.speed_tier = SpeedTier::kFalcon;
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 3500000u);
+ EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, &pool));
+}
+
+TEST(JxlTest, RoundtripLossless8Alpha) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/tmshre_riaphotographs_alpha.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ EXPECT_EQ(8u, io.metadata.m.GetAlphaBits());
+ EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+
+ CompressParams cparams = CParamsForLossless();
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 350000u);
+ // If fails, see note about floating point in RoundtripLossless8.
+ EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0);
+ EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha()));
+ EXPECT_EQ(8u, io2.metadata.m.GetAlphaBits());
+ EXPECT_EQ(8u, io2.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample);
+}
+
+TEST(JxlTest, RoundtripLossless16Alpha) {
+ ThreadPool* pool = nullptr;
+
+ size_t xsize = 1200, ysize = 160;
+ Image3F color(xsize, ysize);
+ ImageF alpha(xsize, ysize);
+ // Generate 16-bit pattern that uses various colors and alpha values.
+ for (size_t y = 0; y < ysize; y++) {
+ for (size_t x = 0; x < xsize; x++) {
+ color.PlaneRow(0, y)[x] = (y * 65535 / ysize) * (1.0f / 65535);
+ color.PlaneRow(1, y)[x] = (x * 65535 / xsize) * (1.0f / 65535);
+ color.PlaneRow(2, y)[x] =
+ ((y + x) * 65535 / (xsize + ysize)) * (1.0f / 65535);
+ alpha.Row(y)[x] = (x * 65535 / xsize) * (1.0f / 65535);
+ }
+ }
+ const bool is_gray = false;
+ CodecInOut io;
+ io.metadata.m.SetUintSamples(16);
+ io.metadata.m.SetAlphaBits(16);
+ io.metadata.m.color_encoding = ColorEncoding::SRGB(is_gray);
+ io.SetFromImage(std::move(color), io.metadata.m.color_encoding);
+ io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false);
+
+ EXPECT_EQ(16u, io.metadata.m.GetAlphaBits());
+ EXPECT_EQ(16u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+
+ CompressParams cparams = CParamsForLossless();
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 7100u);
+ // If this test fails with a very close to 0.0 but not exactly 0.0 butteraugli
+ // distance, then there is likely a floating point issue, that could be
+ // happening either in io or io2. The values of io are generated by
+ // external_image.cc, and those in io2 by the jxl decoder. If they use
+ // slightly different floating point operations (say, one does "i / 257.0f"
+ // while the other does "i * (1.0f / 257)" they will get slightly different
+ // values. To fix, ensure both sides do the following formula for converting
+ // integer range 0-65535 to Image3F floating point range 0.0f-255.0f:
+ // "i * (1.0f / 257)".
+ // Note that this precision issue is not a problem in practice if the values
+ // are equal when rounded to 16-bit int, but currently full exact precision is
+ // tested.
+ EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0);
+ EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha()));
+ EXPECT_EQ(16u, io2.metadata.m.GetAlphaBits());
+ EXPECT_EQ(16u, io2.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample);
+}
+
+TEST(JxlTest, RoundtripLossless16AlphaNotMisdetectedAs8Bit) {
+ ThreadPool* pool = nullptr;
+
+ size_t xsize = 128, ysize = 128;
+ Image3F color(xsize, ysize);
+ ImageF alpha(xsize, ysize);
+ // All 16-bit values, both color and alpha, of this image are below 64.
+ // This allows testing if a code path wrongly concludes it's an 8-bit instead
+ // of 16-bit image (or even 6-bit).
+ for (size_t y = 0; y < ysize; y++) {
+ for (size_t x = 0; x < xsize; x++) {
+ color.PlaneRow(0, y)[x] = (y * 64 / ysize) * (1.0f / 65535);
+ color.PlaneRow(1, y)[x] = (x * 64 / xsize) * (1.0f / 65535);
+ color.PlaneRow(2, y)[x] =
+ ((y + x) * 64 / (xsize + ysize)) * (1.0f / 65535);
+ alpha.Row(y)[x] = (64 * x / xsize) * (1.0f / 65535);
+ }
+ }
+ const bool is_gray = false;
+ CodecInOut io;
+ io.metadata.m.SetUintSamples(16);
+ io.metadata.m.SetAlphaBits(16);
+ io.metadata.m.color_encoding = ColorEncoding::SRGB(is_gray);
+ io.SetFromImage(std::move(color), io.metadata.m.color_encoding);
+ io.Main().SetAlpha(std::move(alpha), /*alpha_is_premultiplied=*/false);
+
+ EXPECT_EQ(16u, io.metadata.m.GetAlphaBits());
+ EXPECT_EQ(16u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+
+ CompressParams cparams = CParamsForLossless();
+ DecompressParams dparams;
+
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 3100u);
+ EXPECT_EQ(16u, io2.metadata.m.GetAlphaBits());
+ EXPECT_EQ(16u, io2.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample);
+ // If fails, see note about floating point in RoundtripLossless8.
+ EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0.0);
+ EXPECT_TRUE(SamePixels(*io.Main().alpha(), *io2.Main().alpha()));
+}
+
+TEST(JxlTest, RoundtripYCbCr420) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ const PaddedBytes yuv420 = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.ffmpeg.y4m");
+ CodecInOut io2;
+ ASSERT_TRUE(test::DecodeImageY4M(Span<const uint8_t>(yuv420), &io2));
+
+ CompressParams cparams = CParamsForLossless();
+ cparams.speed_tier = SpeedTier::kThunder;
+ DecompressParams dparams;
+
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io2, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io3;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io3, pool));
+
+ EXPECT_LE(compressed.size(), 1325000u);
+
+ // we're comparing an original PNG with a YCbCr 4:2:0 version
+ EXPECT_THAT(ComputeDistance2(io.Main(), io3.Main(), GetJxlCms()),
+ IsSlightlyBelow(8.5));
+}
+
+TEST(JxlTest, RoundtripDots) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+
+ ASSERT_NE(io.xsize(), 0u);
+
+ CompressParams cparams;
+ cparams.dots = Override::kOn;
+ cparams.butteraugli_distance = 0.04;
+ cparams.speed_tier = SpeedTier::kSquirrel;
+ DecompressParams dparams;
+
+ EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB());
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
+
+ EXPECT_LE(compressed.size(), 400000u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(0.3));
+}
+
+TEST(JxlTest, RoundtripNoise) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig =
+ ReadTestData("third_party/wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+
+ ASSERT_NE(io.xsize(), 0u);
+
+ CompressParams cparams;
+ cparams.noise = Override::kOn;
+ cparams.speed_tier = SpeedTier::kSquirrel;
+ DecompressParams dparams;
+
+ EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_TRUE(io.metadata.m.color_encoding.tf.IsSRGB());
+ PassesEncoderState enc_state;
+ AuxOut* aux_out = nullptr;
+ PaddedBytes compressed;
+ EXPECT_TRUE(EncodeFile(cparams, &io, &enc_state, &compressed, GetJxlCms(),
+ aux_out, pool));
+ CodecInOut io2;
+ EXPECT_TRUE(DecodeFile(dparams, compressed, &io2, pool));
+
+ EXPECT_LE(compressed.size(), 40000u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.6));
+}
+
+TEST(JxlTest, RoundtripLossless8Gray) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData(
+ "third_party/wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+
+ CompressParams cparams = CParamsForLossless();
+ DecompressParams dparams;
+
+ EXPECT_TRUE(io.Main().IsGray());
+ EXPECT_EQ(8u, io.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io.metadata.m.bit_depth.exponent_bits_per_sample);
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 130000u);
+ // If fails, see note about floating point in RoundtripLossless8.
+ EXPECT_EQ(ComputeDistance2(io.Main(), io2.Main(), GetJxlCms()), 0);
+ EXPECT_EQ(0.0, ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool));
+ EXPECT_TRUE(io2.Main().IsGray());
+ EXPECT_EQ(8u, io2.metadata.m.bit_depth.bits_per_sample);
+ EXPECT_FALSE(io2.metadata.m.bit_depth.floating_point_sample);
+ EXPECT_EQ(0u, io2.metadata.m.bit_depth.exponent_bits_per_sample);
+}
+
+#if JPEGXL_ENABLE_GIF
+
+TEST(JxlTest, RoundtripAnimation) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData("jxl/traffic_light.gif");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ ASSERT_EQ(4u, io.frames.size());
+
+ CompressParams cparams;
+ DecompressParams dparams;
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 3000u);
+
+ EXPECT_EQ(io2.frames.size(), io.frames.size());
+ test::CoalesceGIFAnimationWithAlpha(&io);
+ EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+#if JXL_HIGH_PRECISION
+ 1.55);
+#else
+ 1.75);
+#endif
+}
+
+TEST(JxlTest, RoundtripLosslessAnimation) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData("jxl/traffic_light.gif");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ ASSERT_EQ(4u, io.frames.size());
+
+ CompressParams cparams = CParamsForLossless();
+ DecompressParams dparams;
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 1200u);
+
+ EXPECT_EQ(io2.frames.size(), io.frames.size());
+ test::CoalesceGIFAnimationWithAlpha(&io);
+ EXPECT_LE(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ 5e-4);
+}
+
+TEST(JxlTest, RoundtripAnimationPatches) {
+ ThreadPool* pool = nullptr;
+ const PaddedBytes orig = ReadTestData("jxl/animation_patches.gif");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, pool));
+ ASSERT_EQ(2u, io.frames.size());
+
+ CompressParams cparams;
+ cparams.patches = Override::kOn;
+ DecompressParams dparams;
+ CodecInOut io2;
+ // 40k with no patches, 27k with patch frames encoded multiple times.
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, pool, &io2), 24000u);
+
+ EXPECT_EQ(io2.frames.size(), io.frames.size());
+ // >10 with broken patches
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, pool),
+ IsSlightlyBelow(1.5));
+}
+
+#endif // JPEGXL_ENABLE_GIF
+
+namespace {
+
+jxl::Status DecompressJxlToJPEGForTest(
+ const jpegxl::tools::JpegXlContainer& container, jxl::ThreadPool* pool,
+ jxl::PaddedBytes* output) {
+ output->clear();
+ jxl::Span<const uint8_t> compressed(container.codestream);
+
+ JXL_RETURN_IF_ERROR(compressed.size() >= 2);
+
+ // JXL case
+ // Decode to DCT when possible and generate a JPG file.
+ jxl::CodecInOut io;
+ jxl::DecompressParams params;
+ params.keep_dct = true;
+ if (!jpegxl::tools::DecodeJpegXlToJpeg(params, container, &io, pool)) {
+ return JXL_FAILURE("Failed to decode JXL to JPEG");
+ }
+ if (!jpeg::EncodeImageJPGCoefficients(&io, output)) {
+ return JXL_FAILURE("Failed to generate JPEG");
+ }
+ return true;
+}
+
+} // namespace
+
+size_t RoundtripJpeg(const PaddedBytes& jpeg_in, ThreadPool* pool) {
+ CodecInOut io;
+ EXPECT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(jpeg_in), &io));
+ CompressParams cparams;
+ cparams.color_transform = jxl::ColorTransform::kYCbCr;
+
+ PassesEncoderState passes_enc_state;
+ PaddedBytes compressed, codestream;
+
+ EXPECT_TRUE(EncodeFile(cparams, &io, &passes_enc_state, &codestream,
+ GetJxlCms(),
+ /*aux_out=*/nullptr, pool));
+ jpegxl::tools::JpegXlContainer enc_container;
+ enc_container.codestream = std::move(codestream);
+ jpeg::JPEGData data_in = *io.Main().jpeg_data;
+ jxl::PaddedBytes jpeg_data;
+ EXPECT_TRUE(EncodeJPEGData(data_in, &jpeg_data, cparams));
+ enc_container.jpeg_reconstruction = jpeg_data.data();
+ enc_container.jpeg_reconstruction_size = jpeg_data.size();
+ EXPECT_TRUE(EncodeJpegXlContainerOneShot(enc_container, &compressed));
+
+ jpegxl::tools::JpegXlContainer container;
+ EXPECT_TRUE(DecodeJpegXlContainerOneShot(compressed.data(), compressed.size(),
+ &container));
+ PaddedBytes out;
+ EXPECT_TRUE(DecompressJxlToJPEGForTest(container, pool, &out));
+ EXPECT_EQ(out.size(), jpeg_in.size());
+ size_t failures = 0;
+ for (size_t i = 0; i < std::min(out.size(), jpeg_in.size()); i++) {
+ if (out[i] != jpeg_in[i]) {
+ EXPECT_EQ(out[i], jpeg_in[i])
+ << "byte mismatch " << i << " " << out[i] << " != " << jpeg_in[i];
+ if (++failures > 4) {
+ return compressed.size();
+ }
+ }
+ }
+ return compressed.size();
+}
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression444)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg");
+ // JPEG size is 326'916 bytes.
+ EXPECT_LE(RoundtripJpeg(orig, &pool), 256000u);
+}
+
+#if JPEGXL_ENABLE_JPEG
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.im_q85_444.jpg");
+ CodecInOut io;
+ ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
+
+ CodecInOut io2;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io2, &pool));
+
+ CompressParams cparams;
+ cparams.color_transform = jxl::ColorTransform::kYCbCr;
+
+ DecompressParams dparams;
+
+ CodecInOut io3;
+ Roundtrip(&io, cparams, dparams, &pool, &io3);
+
+ // TODO(eustas): investigate, why SJPEG and JpegRecompression pixels are
+ // different.
+ EXPECT_THAT(ComputeDistance2(io2.Main(), io3.Main(), GetJxlCms()),
+ IsSlightlyBelow(12));
+}
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg");
+ CodecInOut io;
+ ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
+
+ CodecInOut io2;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io2, &pool));
+
+ CompressParams cparams;
+ cparams.color_transform = jxl::ColorTransform::kYCbCr;
+
+ DecompressParams dparams;
+
+ CodecInOut io3;
+ Roundtrip(&io, cparams, dparams, &pool, &io3);
+
+ EXPECT_THAT(ComputeDistance2(io2.Main(), io3.Main(), GetJxlCms()),
+ IsSlightlyBelow(11));
+}
+
+TEST(JxlTest,
+ JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420EarlyFlush)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg");
+ CodecInOut io;
+ ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
+
+ CodecInOut io2;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io2, &pool));
+
+ CompressParams cparams;
+ cparams.color_transform = jxl::ColorTransform::kYCbCr;
+
+ DecompressParams dparams;
+ dparams.max_downsampling = 8;
+
+ CodecInOut io3;
+ Roundtrip(&io, cparams, dparams, &pool, &io3);
+
+ EXPECT_THAT(ComputeDistance2(io2.Main(), io3.Main(), GetJxlCms()),
+ IsSlightlyBelow(650));
+}
+
+TEST(JxlTest,
+ JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels420Mul16)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon_cropped.jpg");
+ CodecInOut io;
+ ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
+
+ CodecInOut io2;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io2, &pool));
+
+ CompressParams cparams;
+ cparams.color_transform = jxl::ColorTransform::kYCbCr;
+
+ DecompressParams dparams;
+
+ CodecInOut io3;
+ Roundtrip(&io, cparams, dparams, &pool, &io3);
+
+ EXPECT_THAT(ComputeDistance2(io2.Main(), io3.Main(), GetJxlCms()),
+ IsSlightlyBelow(4));
+}
+
+TEST(JxlTest,
+ JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionToPixels_asymmetric)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/"
+ "flower_foveon.png.im_q85_asymmetric.jpg");
+ CodecInOut io;
+ ASSERT_TRUE(jpeg::DecodeImageJPG(Span<const uint8_t>(orig), &io));
+
+ CodecInOut io2;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io2, &pool));
+
+ CompressParams cparams;
+ cparams.color_transform = jxl::ColorTransform::kYCbCr;
+
+ DecompressParams dparams;
+
+ CodecInOut io3;
+ Roundtrip(&io, cparams, dparams, &pool, &io3);
+
+ EXPECT_THAT(ComputeDistance2(io2.Main(), io3.Main(), GetJxlCms()),
+ IsSlightlyBelow(10));
+}
+
+#endif
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompressionGray)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.im_q85_gray.jpg");
+ // JPEG size is 167'025 bytes.
+ EXPECT_LE(RoundtripJpeg(orig, &pool), 140000u);
+}
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression420)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.im_q85_420.jpg");
+ // JPEG size is 226'018 bytes.
+ EXPECT_LE(RoundtripJpeg(orig, &pool), 181050u);
+}
+
+TEST(JxlTest,
+ JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression_luma_subsample)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/"
+ "flower_foveon.png.im_q85_luma_subsample.jpg");
+ // JPEG size is 216'069 bytes.
+ EXPECT_LE(RoundtripJpeg(orig, &pool), 181000u);
+}
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression444_12)) {
+ // 444 JPEG that has an interesting sampling-factor (1x2, 1x2, 1x2).
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.im_q85_444_1x2.jpg");
+ // JPEG size is 329'942 bytes.
+ EXPECT_LE(RoundtripJpeg(orig, &pool), 256000u);
+}
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression422)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.im_q85_422.jpg");
+ // JPEG size is 265'590 bytes.
+ EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u);
+}
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression440)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/flower_foveon.png.im_q85_440.jpg");
+ // JPEG size is 262'249 bytes.
+ EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u);
+}
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression_asymmetric)) {
+ // 2x vertical downsample of one chroma channel, 2x horizontal downsample of
+ // the other.
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/"
+ "flower_foveon.png.im_q85_asymmetric.jpg");
+ // JPEG size is 262'249 bytes.
+ EXPECT_LE(RoundtripJpeg(orig, &pool), 209000u);
+}
+
+TEST(JxlTest, JXL_TRANSCODE_JPEG_TEST(RoundtripJpegRecompression420Progr)) {
+ ThreadPoolInternal pool(8);
+ const PaddedBytes orig = ReadTestData(
+ "third_party/imagecompression.info/"
+ "flower_foveon.png.im_q85_420_progr.jpg");
+ EXPECT_LE(RoundtripJpeg(orig, &pool), 181000u);
+}
+
+TEST(JxlTest, RoundtripProgressive) {
+ ThreadPoolInternal pool(4);
+ const PaddedBytes orig =
+ ReadTestData("third_party/imagecompression.info/flower_foveon.png");
+ CodecInOut io;
+ ASSERT_TRUE(SetFromBytes(Span<const uint8_t>(orig), &io, &pool));
+ io.ShrinkTo(600, 1024);
+
+ CompressParams cparams;
+ DecompressParams dparams;
+
+ cparams.butteraugli_distance = 1.0f;
+ cparams.progressive_dc = true;
+ cparams.responsive = true;
+ cparams.progressive_mode = true;
+ CodecInOut io2;
+ EXPECT_LE(Roundtrip(&io, cparams, dparams, &pool, &io2), 40000u);
+ EXPECT_THAT(ButteraugliDistance(io, io2, cparams.ba_params, GetJxlCms(),
+ /*distmap=*/nullptr, &pool),
+ IsSlightlyBelow(1.1f));
+}
+
+} // namespace
+} // namespace jxl