// 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 "lib/extras/dec/jxl.h"

#include <jxl/cms.h>
#include <jxl/color_encoding.h>
#include <jxl/encode.h>
#include <jxl/memory_manager.h>
#include <jxl/types.h>

#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <future>
#include <ostream>
#include <string>
#include <tuple>
#include <vector>

#include "lib/extras/codec.h"
#include "lib/extras/codec_in_out.h"
#include "lib/extras/dec/decode.h"
#include "lib/extras/enc/encode.h"
#include "lib/extras/enc/jxl.h"
#include "lib/extras/packed_image.h"
#include "lib/jxl/alpha.h"
#include "lib/jxl/base/common.h"
#include "lib/jxl/base/compiler_specific.h"
#include "lib/jxl/base/data_parallel.h"
#include "lib/jxl/base/span.h"
#include "lib/jxl/base/status.h"
#include "lib/jxl/butteraugli/butteraugli.h"
#include "lib/jxl/color_encoding_internal.h"
#include "lib/jxl/common.h"  // JXL_HIGH_PRECISION
#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_metadata.h"
#include "lib/jxl/test_image.h"
#include "lib/jxl/test_memory_manager.h"
#include "lib/jxl/test_utils.h"
#include "lib/jxl/testing.h"

namespace jxl {

struct AuxOut;

namespace {
using ::jxl::extras::JXLCompressParams;
using ::jxl::extras::JXLDecompressParams;
using ::jxl::extras::PackedPixelFile;
using ::jxl::test::ButteraugliDistance;
using ::jxl::test::ComputeDistance2;
using ::jxl::test::ReadTestData;
using ::jxl::test::Roundtrip;
using ::jxl::test::TestImage;
using ::jxl::test::ThreadPoolForTests;

#define JXL_TEST_NL 0  // Disabled in code

TEST(JxlTest, RoundtripSinglePixel) {
  TestImage t;
  ASSERT_TRUE(t.SetDimensions(1, 1));
  JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
  frame.ZeroFill();
  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), {}, {}, nullptr, &ppf_out), 54, 10);
}

TEST(JxlTest, RoundtripSinglePixelWithAlpha) {
  TestImage t;
  ASSERT_TRUE(t.SetDimensions(1, 1));
  ASSERT_TRUE(t.SetChannels(4));
  JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
  frame.ZeroFill();
  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), {}, {}, nullptr, &ppf_out), 57, 10);
}

// Changing serialized signature causes Decode to fail.
TEST(JxlTest, RoundtripMarker) {
  if (JXL_CRASH_ON_ERROR) {
    GTEST_SKIP() << "Skipping due to JXL_CRASH_ON_ERROR";
  }
  TestImage t;
  ASSERT_TRUE(t.SetDimensions(1, 1));
  JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
  frame.ZeroFill();
  for (size_t i = 0; i < 2; ++i) {
    std::vector<uint8_t> compressed;
    EXPECT_TRUE(extras::EncodeImageJXL({}, t.ppf(), /*jpeg_bytes=*/nullptr,
                                       &compressed));
    compressed[i] ^= 0xFF;
    PackedPixelFile ppf_out;
    EXPECT_FALSE(extras::DecodeImageJXL(compressed.data(), compressed.size(),
                                        {}, /* decoded_bytes */ nullptr,
                                        &ppf_out));
  }
}

TEST(JxlTest, RoundtripTinyFast) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_TRUE(t.SetDimensions(32, 32));

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);
  cparams.distance = 4.0f;
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 181, 15);
}

TEST(JxlTest, RoundtripSmallD1) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  size_t xsize = t.ppf().info.xsize / 8;
  size_t ysize = t.ppf().info.ysize / 8;
  ASSERT_TRUE(t.SetDimensions(xsize, ysize));

  {
    PackedPixelFile ppf_out;
    EXPECT_NEAR(Roundtrip(t.ppf(), {}, {}, pool, &ppf_out), 916, 40);
    EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 0.92);
  }

  // With a lower intensity target than the default, the bitrate should be
  // smaller.
  t.ppf().info.intensity_target = 100.0f;

  {
    PackedPixelFile ppf_out;
    EXPECT_NEAR(Roundtrip(t.ppf(), {}, {}, pool, &ppf_out), 747, 20);
    EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 1.3);
    EXPECT_EQ(ppf_out.info.intensity_target, t.ppf().info.intensity_target);
  }
}
TEST(JxlTest, RoundtripResample2) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESAMPLING, 2);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 3);  // kFalcon
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 17300, 500);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 90);
}

TEST(JxlTest, RoundtripResample2Slow) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESAMPLING, 2);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 9);  // kTortoise
  cparams.distance = 10.0;
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 3988, 300);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 270);
}

TEST(JxlTest, RoundtripResample2MT) {
  ThreadPoolForTests pool(4);
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  // image has to be large enough to have multiple groups after downsampling
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESAMPLING, 2);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 3);  // kFalcon
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out),
              206917, 2000);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 340);
}

// 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 std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  // Image size is selected so that the block border needed is larger than the
  // amount of pixels available on the next block.
  ASSERT_TRUE(t.SetDimensions(513, 515));

  JXLCompressParams cparams;
  // Force epf so we end up needing a lot of border.
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EPF, 3);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, &pool, &ppf_out), 23666,
              400);
  EXPECT_LE(ButteraugliDistance(t.ppf(), ppf_out), 1.35);
}

TEST(JxlTest, RoundtripOutOfOrderProcessingBorder) {
  FakeParallelRunner fake_pool(/*order_seed=*/47, /*num_threads=*/8);
  ThreadPool pool(&JxlFakeParallelRunner, &fake_pool);
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  // Image size is selected so that the block border needed is larger than the
  // amount of pixels available on the next block.
  ASSERT_TRUE(t.SetDimensions(513, 515));

  JXLCompressParams cparams;
  // Force epf so we end up needing a lot of border.
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EPF, 3);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESAMPLING, 2);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, &pool, &ppf_out), 9479, 200);
  EXPECT_LE(ButteraugliDistance(t.ppf(), ppf_out), 2.9);
}

TEST(JxlTest, RoundtripResample4) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESAMPLING, 4);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 5888, 100);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 22);
}

TEST(JxlTest, RoundtripResample8) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESAMPLING, 8);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 2036, 50);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 50);
}

TEST(JxlTest, RoundtripUnalignedD2) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  size_t xsize = t.ppf().info.xsize / 12;
  size_t ysize = t.ppf().info.ysize / 7;
  ASSERT_TRUE(t.SetDimensions(xsize, ysize));

  JXLCompressParams cparams;
  cparams.distance = 2.0;
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 540, 37);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 1.72);
}

TEST(JxlTest, RoundtripMultiGroup) {
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_TRUE(t.SetDimensions(600, 1024));

  auto test = [&](jxl::SpeedTier speed_tier, float target_distance,
                  size_t expected_size, float expected_distance) {
    ThreadPoolForTests pool(4);
    JXLCompressParams cparams;
    int64_t effort = 10 - static_cast<int>(speed_tier);
    cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, effort);
    cparams.distance = target_distance;
    extras::JXLDecompressParams dparams;

    PackedPixelFile ppf_out;
    EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out),
                expected_size, 814);
    EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out),
                          expected_distance);
  };

  auto run_kitten = std::async(std::launch::async, test, SpeedTier::kKitten,
                               1.0f, 58737u, 9.1);
  auto run_wombat = std::async(std::launch::async, test, SpeedTier::kWombat,
                               2.0f, 35561u, 16.5);
}

TEST(JxlTest, RoundtripRGBToGrayscale) {
  JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
  ThreadPoolForTests pool(4);
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  auto io = jxl::make_unique<jxl::CodecInOut>(memory_manager);
  ASSERT_TRUE(SetFromBytes(Bytes(orig), io.get(), pool.get()));
  ASSERT_TRUE(io->ShrinkTo(600, 1024));

  CompressParams cparams;
  cparams.butteraugli_distance = 1.0f;
  cparams.speed_tier = SpeedTier::kFalcon;

  JXLDecompressParams dparams;
  dparams.color_space = "Gra_D65_Rel_SRG";

  auto io2 = jxl::make_unique<jxl::CodecInOut>(memory_manager);
  EXPECT_FALSE(io->Main().IsGray());
  size_t compressed_size;
  JXL_EXPECT_OK(Roundtrip(io.get(), cparams, dparams, io2.get(), _,
                          &compressed_size, pool.get()));
  EXPECT_LE(compressed_size, 65000u);
  EXPECT_TRUE(io2->Main().IsGray());

  // Convert original to grayscale here, because TransformTo refuses to
  // convert between grayscale and RGB.
  ColorEncoding srgb_lin = ColorEncoding::LinearSRGB(/*is_gray=*/false);
  ASSERT_TRUE(io->frames[0].TransformTo(srgb_lin, *JxlGetDefaultCms()));
  Image3F* color = io->Main().color();
  for (size_t y = 0; y < color->ysize(); ++y) {
    float* row_r = color->PlaneRow(0, y);
    float* row_g = color->PlaneRow(1, y);
    float* row_b = color->PlaneRow(2, y);
    for (size_t x = 0; x < color->xsize(); ++x) {
      float luma = 0.2126 * row_r[x] + 0.7152 * row_g[x] + 0.0722 * row_b[x];
      row_r[x] = row_g[x] = row_b[x] = luma;
    }
  }
  ColorEncoding srgb_gamma = ColorEncoding::SRGB(/*is_gray=*/false);
  ASSERT_TRUE(io->frames[0].TransformTo(srgb_gamma, *JxlGetDefaultCms()));
  io->metadata.m.color_encoding = io2->Main().c_current();
  io->Main().OverrideProfile(io2->Main().c_current());
  EXPECT_SLIGHTLY_BELOW(
      ButteraugliDistance(io->frames, io2->frames, ButteraugliParams(),
                          *JxlGetDefaultCms(),
                          /*distmap=*/nullptr, pool.get()),
      1.4);
}

TEST(JxlTest, RoundtripLargeFast) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out),
              488000, 12000);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 78);
}

TEST(JxlTest, RoundtripLargeEmptyModular) {
  ThreadPoolForTests pool(8);
  TestImage t;
  // On 32-bit machines memory could be limited to 2GiB. Current use factor
  // is about 20x, that means hat 6MPx image is about the max available size.
  constexpr bool kSkinny = (sizeof(size_t) == 4);
  constexpr size_t kDim = kSkinny ? 2400 : 4096;
  const size_t kLim = std::min<size_t>(kDim, 1024);
  ASSERT_TRUE(t.SetDimensions(kDim, kDim));
  t.SetDataType(JXL_TYPE_UINT8);
  ASSERT_TRUE(t.SetChannels(4));
  JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
  frame.ZeroFill();
  for (size_t c = 0; c < 4; ++c) {
    for (size_t y = 0; y < kLim; y += (c + 1)) {
      for (size_t x = 0; x < kLim; x += ((y % 4) + 3)) {
        ASSERT_TRUE(frame.SetValue(y, x, c, 0.88));
      }
    }
  }

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 1);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_MODULAR, 1);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESPONSIVE, 1);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_DECODING_SPEED, 2);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  constexpr size_t kTargetSize = kSkinny ? 539100 : 620200;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out),
              kTargetSize, 10000);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 0.21);
}

TEST(JxlTest, RoundtripOutputColorSpace) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel

  JXLDecompressParams dparams;
  dparams.color_space = "RGB_D65_DCI_Rel_709";
  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out),
              487000, 12000);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 78);
}

TEST(JxlTest, RoundtripDotsForceEpf) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EPF, 2);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_DOTS, 1);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out), 41777,
              700);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 18);
}

// 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) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel
  cparams.distance = 2.0;
  extras::JXLDecompressParams dparams;

  // Try each xsize mod kBlockDim to verify right border handling.
  for (size_t xsize = 48; xsize > 40; --xsize) {
    ASSERT_TRUE(t.SetDimensions(xsize, 15));

    PackedPixelFile ppf2;
    const size_t size2 =
        Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf2);

    PackedPixelFile ppf3;
    const size_t size3 =
        Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf3);

    // Exact same compressed size.
    EXPECT_EQ(size2, size3);

    // Exact same distance.
    const float dist2 = ComputeDistance2(t.ppf(), ppf2);
    const float dist3 = ComputeDistance2(t.ppf(), ppf3);
    EXPECT_EQ(dist2, dist3);
  }
}

// Same as above, but for full image, testing multiple groups.
TEST(JxlTest, RoundtripLargeConsistent) {
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel
  cparams.distance = 2.0;
  extras::JXLDecompressParams dparams;

  auto roundtrip_and_compare = [&]() {
    ThreadPoolForTests pool(8);
    PackedPixelFile ppf2;
    size_t size = Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf2);
    double dist = ComputeDistance2(t.ppf(), ppf2);
    return std::tuple<size_t, double>(size, dist);
  };

  // Try each xsize mod kBlockDim to verify right border handling.
  auto future2 = std::async(std::launch::async, roundtrip_and_compare);
  auto future3 = std::async(std::launch::async, roundtrip_and_compare);

  const auto result2 = future2.get();
  const auto result3 = future3.get();

  // Exact same compressed size.
  EXPECT_EQ(std::get<0>(result2), std::get<0>(result3));

  // Exact same distance.
  EXPECT_EQ(std::get<1>(result2), std::get<1>(result3));
}

TEST(JxlTest, RoundtripSmallNL) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  size_t xsize = t.ppf().info.xsize / 8;
  size_t ysize = t.ppf().info.ysize / 8;
  ASSERT_TRUE(t.SetDimensions(xsize, ysize));

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), {}, {}, pool, &ppf_out), 916, 45);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 0.92);
}

TEST(JxlTest, RoundtripNoGaborishNoAR) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EPF, 0);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_GABORISH, 0);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 44555, 400);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 1.55);
}

TEST(JxlTest, RoundtripSmallNoGaborish) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  size_t xsize = t.ppf().info.xsize / 8;
  size_t ysize = t.ppf().info.ysize / 8;
  ASSERT_TRUE(t.SetDimensions(xsize, ysize));

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_GABORISH, 0);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 1066, 20);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 1.0);
}

TEST(JxlTest, RoundtripSmallPatchesAlpha) {
  ThreadPool* pool = nullptr;
  TestImage t;
  ASSERT_TRUE(t.SetDimensions(256, 256));
  ASSERT_TRUE(t.SetChannels(4));
  ASSERT_TRUE(t.SetColorEncoding("RGB_D65_SRG_Rel_Lin"));
  JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
  frame.ZeroFill();
  // This pattern should be picked up by the patch detection heuristics.
  for (size_t y = 0; y < t.ppf().info.ysize; ++y) {
    for (size_t x = 0; x < t.ppf().info.xsize; ++x) {
      if (x % 4 == 0 && (y / 32) % 4 == 0) {
        ASSERT_TRUE(frame.SetValue(y, x, 1, 127.0f / 255.0f));
      }
      ASSERT_TRUE(frame.SetValue(y, x, 3, 1.0f));
    }
  }

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel
  cparams.distance = 0.1f;
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 462, 100);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 0.016f);
}

TEST(JxlTest, RoundtripSmallPatches) {
  ThreadPool* pool = nullptr;
  TestImage t;
  ASSERT_TRUE(t.SetDimensions(256, 256));
  ASSERT_TRUE(t.SetColorEncoding("RGB_D65_SRG_Rel_Lin"));
  JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
  frame.ZeroFill();
  // This pattern should be picked up by the patch detection heuristics.
  for (size_t y = 0; y < t.ppf().info.ysize; ++y) {
    for (size_t x = 0; x < t.ppf().info.xsize; ++x) {
      if (x % 4 == 0 && (y / 32) % 4 == 0) {
        ASSERT_TRUE(frame.SetValue(y, x, 1, 127.0f / 255.0f));
      }
    }
  }

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel
  cparams.distance = 0.1f;
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 363, 100);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 0.015f);
}

// TODO(szabadka) Add encoder and decoder API functions that accept frame
// buffers in arbitrary unsigned and floating point formats, and then roundtrip
// test the lossless codepath to make sure the exact binary representations
// are preserved.
#if JXL_FALSE
TEST(JxlTest, RoundtripImageBundleOriginalBits) {
  JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
  // Image does not matter, only io.metadata.m and io2.metadata.m are tested.
  JXL_TEST_ASSIGN_OR_DIE(Image3F image, Image3F::Create(memory_manager, 1, 1));
  ZeroFillImage(&image);
  auto io = jxl::make_unique<CodecInOut>(memory_manager);
  io->metadata.m.color_encoding = ColorEncoding::LinearSRGB();
  JXL_EXPECT_OK(
      io->SetFromImage(std::move(image), ColorEncoding::LinearSRGB()));

  CompressParams cparams;
  extras::JXLDecompressParams 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);
    auto io2 = jxl::make_unique<CodecInOut>(memory_manager);
    JXL_EXPECT_OK(Roundtrip(io.get(), cparams, dparams, io2.get(), _));

    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(user): 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;

    auto io2 = jxl::make_unique<CodecInOut>(memory_manager);
    JXL_EXPECT_OK(Roundtrip(io.get(), cparams, dparams, io2.get()));

    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());
  }
}
#endif

TEST(JxlTest, RoundtripGrayscale) {
  JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
  const std::vector<uint8_t> orig = ReadTestData(
      "external/wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
  auto io = jxl::make_unique<CodecInOut>(memory_manager);
  ASSERT_TRUE(SetFromBytes(Bytes(orig), io.get()));
  ASSERT_NE(io->xsize(), 0u);
  ASSERT_TRUE(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());

  {
    CompressParams cparams;
    cparams.butteraugli_distance = 1.0;

    std::vector<uint8_t> compressed;
    EXPECT_TRUE(test::EncodeFile(cparams, io.get(), &compressed));
    auto io2 = jxl::make_unique<CodecInOut>(memory_manager);
    EXPECT_TRUE(test::DecodeFile({}, Bytes(compressed), io2.get()));
    EXPECT_TRUE(io2->Main().IsGray());

    EXPECT_LE(compressed.size(), 7000u);
    EXPECT_SLIGHTLY_BELOW(
        ButteraugliDistance(io->frames, io2->frames, ButteraugliParams(),
                            *JxlGetDefaultCms(),
                            /*distmap=*/nullptr),
        1.6);
  }

  // Test with larger butteraugli distance and other settings enabled so
  // different jxl codepaths trigger.
  {
    CompressParams cparams;
    cparams.butteraugli_distance = 8.0;

    std::vector<uint8_t> compressed;
    EXPECT_TRUE(test::EncodeFile(cparams, io.get(), &compressed));
    auto io2 = jxl::make_unique<CodecInOut>(memory_manager);
    EXPECT_TRUE(test::DecodeFile({}, Bytes(compressed), io2.get()));
    EXPECT_TRUE(io2->Main().IsGray());

    EXPECT_LE(compressed.size(), 1300u);
    EXPECT_SLIGHTLY_BELOW(
        ButteraugliDistance(io->frames, io2->frames, ButteraugliParams(),
                            *JxlGetDefaultCms(),
                            /*distmap=*/nullptr),
        6.7);
  }

  {
    CompressParams cparams;
    cparams.butteraugli_distance = 1.0;

    std::vector<uint8_t> compressed;
    EXPECT_TRUE(test::EncodeFile(cparams, io.get(), &compressed));

    auto io2 = jxl::make_unique<CodecInOut>(memory_manager);
    JXLDecompressParams dparams;
    dparams.color_space = "RGB_D65_SRG_Rel_SRG";
    EXPECT_TRUE(test::DecodeFile(dparams, Bytes(compressed), io2.get()));
    EXPECT_FALSE(io2->Main().IsGray());

    EXPECT_LE(compressed.size(), 7000u);
    EXPECT_SLIGHTLY_BELOW(
        ButteraugliDistance(io->frames, io2->frames, ButteraugliParams(),
                            *JxlGetDefaultCms(),
                            /*distmap=*/nullptr),
        1.6);
  }
}

TEST(JxlTest, RoundtripAlpha) {
  JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png");
  auto io = jxl::make_unique<CodecInOut>(memory_manager);
  ASSERT_TRUE(SetFromBytes(Bytes(orig), io.get()));

  ASSERT_NE(io->xsize(), 0u);
  ASSERT_TRUE(io->metadata.m.HasAlpha());
  ASSERT_TRUE(io->Main().HasAlpha());
  ASSERT_TRUE(io->ShrinkTo(300, 300));

  CompressParams cparams;
  cparams.butteraugli_distance = 1.0;

  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());
  std::vector<uint8_t> compressed;
  EXPECT_TRUE(test::EncodeFile(cparams, io.get(), &compressed));

  EXPECT_LE(compressed.size(), 20000u);

  for (bool use_image_callback : {false, true}) {
    for (bool unpremul_alpha : {false, true}) {
      auto io2 = jxl::make_unique<CodecInOut>(memory_manager);
      JXLDecompressParams dparams;
      dparams.use_image_callback = use_image_callback;
      dparams.unpremultiply_alpha = unpremul_alpha;
      EXPECT_TRUE(test::DecodeFile(dparams, Bytes(compressed), io2.get()));
      EXPECT_SLIGHTLY_BELOW(
          ButteraugliDistance(io->frames, io2->frames, ButteraugliParams(),
                              *JxlGetDefaultCms(),
                              /*distmap=*/nullptr),
          1.05);
    }
  }
}

namespace {
// Performs "PremultiplyAlpha" for each ImageBundle (preview/frames).
Status PremultiplyAlpha(CodecInOut& io) {
  const auto doPremultiplyAlpha = [](ImageBundle& bundle) -> Status {
    if (!bundle.HasAlpha()) return true;
    if (!bundle.HasColor()) return true;
    auto* color = bundle.color();
    const auto* alpha = bundle.alpha();
    JXL_ENSURE(color->ysize() == alpha->ysize());
    JXL_ENSURE(color->xsize() == alpha->xsize());
    for (size_t y = 0; y < color->ysize(); y++) {
      ::jxl::PremultiplyAlpha(color->PlaneRow(0, y), color->PlaneRow(1, y),
                              color->PlaneRow(2, y), alpha->Row(y),
                              color->xsize());
    }
    return true;
  };
  ExtraChannelInfo* eci = io.metadata.m.Find(ExtraChannel::kAlpha);
  JXL_ENSURE(eci != nullptr && !eci->alpha_associated);
  if (io.metadata.m.have_preview) {
    JXL_RETURN_IF_ERROR(doPremultiplyAlpha(io.preview_frame));
  }
  for (ImageBundle& ib : io.frames) {
    JXL_RETURN_IF_ERROR(doPremultiplyAlpha(ib));
  }
  eci->alpha_associated = true;
  return true;
}

Status UnpremultiplyAlpha(CodecInOut& io) {
  const auto doUnpremultiplyAlpha = [](ImageBundle& bundle) -> Status {
    if (!bundle.HasAlpha()) return true;
    if (!bundle.HasColor()) return true;
    auto* color = bundle.color();
    const auto* alpha = bundle.alpha();
    JXL_ENSURE(color->ysize() == alpha->ysize());
    JXL_ENSURE(color->xsize() == alpha->xsize());
    for (size_t y = 0; y < color->ysize(); y++) {
      ::jxl::UnpremultiplyAlpha(color->PlaneRow(0, y), color->PlaneRow(1, y),
                                color->PlaneRow(2, y), alpha->Row(y),
                                color->xsize());
    }
    return true;
  };
  ExtraChannelInfo* eci = io.metadata.m.Find(ExtraChannel::kAlpha);
  JXL_ENSURE(eci != nullptr && eci->alpha_associated);
  if (io.metadata.m.have_preview) {
    JXL_RETURN_IF_ERROR(doUnpremultiplyAlpha(io.preview_frame));
  }
  for (ImageBundle& ib : io.frames) {
    JXL_RETURN_IF_ERROR(doUnpremultiplyAlpha(ib));
  }
  eci->alpha_associated = false;
  return true;
}
}  // namespace

TEST(JxlTest, RoundtripAlphaPremultiplied) {
  JxlMemoryManager* memory_manager = jxl::test::MemoryManager();
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png");
  auto io = jxl::make_unique<CodecInOut>(memory_manager);
  auto io_nopremul = jxl::make_unique<CodecInOut>(memory_manager);
  ASSERT_TRUE(SetFromBytes(Bytes(orig), io.get()));
  ASSERT_TRUE(SetFromBytes(Bytes(orig), io_nopremul.get()));

  ASSERT_NE(io->xsize(), 0u);
  ASSERT_TRUE(io->metadata.m.HasAlpha());
  ASSERT_TRUE(io->Main().HasAlpha());
  ASSERT_TRUE(io->ShrinkTo(300, 300));
  ASSERT_TRUE(io_nopremul->ShrinkTo(300, 300));

  CompressParams cparams;
  cparams.butteraugli_distance = 1.0;
  cparams.SetCms(*JxlGetDefaultCms());

  EXPECT_FALSE(io->Main().AlphaIsPremultiplied());
  EXPECT_TRUE(PremultiplyAlpha(*io));
  EXPECT_TRUE(io->Main().AlphaIsPremultiplied());

  EXPECT_FALSE(io_nopremul->Main().AlphaIsPremultiplied());

  std::vector<uint8_t> compressed;
  EXPECT_TRUE(test::EncodeFile(cparams, io.get(), &compressed));
  EXPECT_LE(compressed.size(), 18613u);

  for (bool use_image_callback : {false, true}) {
    for (bool unpremul_alpha : {false, true}) {
      for (bool use_uint8 : {false, true}) {
        printf(
            "Testing premultiplied alpha using %s %s requesting "
            "%spremultiplied output.\n",
            use_uint8 ? "uint8" : "float",
            use_image_callback ? "image callback" : "image_buffer",
            unpremul_alpha ? "un" : "");
        auto io2 = jxl::make_unique<CodecInOut>(memory_manager);
        JXLDecompressParams dparams;
        dparams.use_image_callback = use_image_callback;
        dparams.unpremultiply_alpha = unpremul_alpha;
        if (use_uint8) {
          dparams.accepted_formats.push_back(
              {4, JXL_TYPE_UINT8, JXL_LITTLE_ENDIAN, 0});
        }
        EXPECT_TRUE(test::DecodeFile(dparams, Bytes(compressed), io2.get()));

        EXPECT_EQ(unpremul_alpha, !io2->Main().AlphaIsPremultiplied());
        if (!unpremul_alpha) {
          EXPECT_SLIGHTLY_BELOW(
              ButteraugliDistance(io->frames, io2->frames, ButteraugliParams(),
                                  *JxlGetDefaultCms(),
                                  /*distmap=*/nullptr),
              1.111);
          EXPECT_TRUE(UnpremultiplyAlpha(*io2));
          EXPECT_FALSE(io2->Main().AlphaIsPremultiplied());
        }
        EXPECT_SLIGHTLY_BELOW(
            ButteraugliDistance(io_nopremul->frames, io2->frames,
                                ButteraugliParams(), *JxlGetDefaultCms(),
                                /*distmap=*/nullptr),
            1.1);
      }
    }
  }
}

TEST(JxlTest, RoundtripAlphaResampling) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_NE(t.ppf().info.xsize, 0u);
  ASSERT_TRUE(t.ppf().info.alpha_bits > 0);

  JXLCompressParams cparams;
  cparams.alpha_distance = 1.0;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 5);  // kHare
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESAMPLING, 2);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING, 2);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 13700, 130);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 4.85);
}

TEST(JxlTest, RoundtripAlphaResamplingOnlyAlpha) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_NE(t.ppf().info.xsize, 0u);
  ASSERT_TRUE(t.ppf().info.alpha_bits > 0);

  JXLCompressParams cparams;
  cparams.alpha_distance = 1.0;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 3);  // kFalcon
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EXTRA_CHANNEL_RESAMPLING, 2);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 33136,
              1000);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 1.52);
}

TEST(JxlTest, RoundtripAlphaNonMultipleOf8) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_TRUE(t.SetDimensions(12, 12));
  ASSERT_NE(t.ppf().info.xsize, 0u);
  ASSERT_TRUE(t.ppf().info.alpha_bits > 0);
  EXPECT_EQ(t.ppf().frames[0].color.format.data_type, JXL_TYPE_UINT8);

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), {}, {}, pool, &ppf_out), 107, 10);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 0.007);
}

TEST(JxlTest, RoundtripAlpha16) {
  ThreadPoolForTests pool(4);
  // The image is wider than 512 pixels to ensure multiple groups are tested.
  size_t xsize = 1200;
  size_t ysize = 160;
  TestImage t;
  ASSERT_TRUE(t.SetDimensions(xsize, ysize));
  ASSERT_TRUE(t.SetChannels(4));
  t.SetAllBitDepths(16);
  JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
  // Generate 16-bit pattern that uses various colors and alpha values.
  const float mul = 1.0f / 65535;
  for (size_t y = 0; y < ysize; y++) {
    for (size_t x = 0; x < xsize; x++) {
      uint16_t r = y * 65535 / ysize;
      uint16_t g = x * 65535 / xsize;
      uint16_t b = (y + x) * 65535 / (xsize + ysize);
      ASSERT_TRUE(frame.SetValue(y, x, 0, r * mul));
      ASSERT_TRUE(frame.SetValue(y, x, 1, g * mul));
      ASSERT_TRUE(frame.SetValue(y, x, 2, b * mul));
      ASSERT_TRUE(frame.SetValue(y, x, 3, g * mul));
    }
  }

  ASSERT_NE(t.ppf().info.xsize, 0u);
  ASSERT_EQ(t.ppf().info.alpha_bits, 16u);

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 6);  // kWombat
  cparams.distance = 0.5;
  cparams.alpha_distance = 0.5;
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  // TODO(szabadka) Investigate big size difference on i686
  // This still keeps happening (2023-04-18).
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out), 3810,
              120);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 0.6);
}

JXL_SLOW_TEST(JxlTest, RoundtripLossless8) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams = test::CompressParamsForLossless();
  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  size_t compressed_size =
      Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out);
  EXPECT_EQ(compressed_size, 218284u);
  EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
}

JXL_SLOW_TEST(JxlTest, RoundtripLossless8ThunderGradient) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams = test::CompressParamsForLossless();
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 2);             // kThunder
  cparams.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_PREDICTOR, 5);  // Gradient
  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  EXPECT_EQ(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out),
            261613u);
  EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
}

JXL_SLOW_TEST(JxlTest, RoundtripLossless8LightningGradient) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams = test::CompressParamsForLossless();
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 1);  // kLightning
  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  // Lax comparison because different SIMD will cause different compression.
  EXPECT_SLIGHTLY_BELOW(
      Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out), 286848u);
  EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
}

JXL_SLOW_TEST(JxlTest, RoundtripLossless8Falcon) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams = test::CompressParamsForLossless();
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 3);  // kFalcon
  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  EXPECT_EQ(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out),
            230697u);
  EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
}

TEST(JxlTest, RoundtripLossless8Alpha) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/tmshre_riaphotographs_alpha.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_EQ(t.ppf().info.alpha_bits, 8u);
  EXPECT_EQ(t.ppf().frames[0].color.format.data_type, JXL_TYPE_UINT8);

  JXLCompressParams cparams = test::CompressParamsForLossless();

  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  size_t compressed_size = Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out);
  EXPECT_EQ(compressed_size, 246071u);
  EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
  EXPECT_EQ(ppf_out.info.alpha_bits, 8u);
  EXPECT_TRUE(test::SameAlpha(t.ppf(), ppf_out));
}

TEST(JxlTest, RoundtripLossless16Alpha) {
  ThreadPool* pool = nullptr;
  size_t xsize = 1200;
  size_t ysize = 160;
  TestImage t;
  ASSERT_TRUE(t.SetDimensions(xsize, ysize));
  ASSERT_TRUE(t.SetChannels(4));
  t.SetAllBitDepths(16);
  JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
  frame.ZeroFill();
  // Generate 16-bit pattern that uses various colors and alpha values.
  const float mul = 1.0f / 65535;
  for (size_t y = 0; y < ysize; y++) {
    for (size_t x = 0; x < xsize; x++) {
      uint16_t r = y * 65535 / ysize;
      uint16_t g = x * 65535 / xsize + 37;
      uint16_t b = (y + x) * 65535 / (xsize + ysize);
      ASSERT_TRUE(frame.SetValue(y, x, 0, r * mul));
      ASSERT_TRUE(frame.SetValue(y, x, 1, g * mul));
      ASSERT_TRUE(frame.SetValue(y, x, 2, b * mul));
      ASSERT_TRUE(frame.SetValue(y, x, 3, g * mul));
    }
  }
  ASSERT_EQ(t.ppf().info.bits_per_sample, 16u);
  ASSERT_EQ(t.ppf().info.alpha_bits, 16u);

  JXLCompressParams cparams = test::CompressParamsForLossless();

  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  // TODO(szabadka) Investigate big size difference on i686
  size_t compressed_size = Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out);
  EXPECT_NEAR(compressed_size, 3334u, 100u);
  EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
  EXPECT_EQ(ppf_out.info.alpha_bits, 16u);
  EXPECT_TRUE(test::SameAlpha(t.ppf(), ppf_out));
}

TEST(JxlTest, RoundtripLossless16AlphaNotMisdetectedAs8Bit) {
  ThreadPool* pool = nullptr;
  size_t xsize = 128;
  size_t ysize = 128;
  TestImage t;
  ASSERT_TRUE(t.SetDimensions(xsize, ysize));
  ASSERT_TRUE(t.SetChannels(4));
  t.SetAllBitDepths(16);
  JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
  // 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).
  const float mul = 1.0f / 65535;
  for (size_t y = 0; y < ysize; y++) {
    for (size_t x = 0; x < xsize; x++) {
      uint16_t r = y * 64 / ysize;
      uint16_t g = x * 64 / xsize + 37;
      uint16_t b = (y + x) * 64 / (xsize + ysize);
      ASSERT_TRUE(frame.SetValue(y, x, 0, r * mul));
      ASSERT_TRUE(frame.SetValue(y, x, 1, g * mul));
      ASSERT_TRUE(frame.SetValue(y, x, 2, b * mul));
      ASSERT_TRUE(frame.SetValue(y, x, 3, g * mul));
    }
  }
  ASSERT_EQ(t.ppf().info.bits_per_sample, 16u);
  ASSERT_EQ(t.ppf().info.alpha_bits, 16u);

  JXLCompressParams cparams = test::CompressParamsForLossless();

  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  size_t compressed_size = Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out);
  EXPECT_NEAR(compressed_size, 1278u, 100u);
  EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
  EXPECT_EQ(ppf_out.info.bits_per_sample, 16u);
  EXPECT_EQ(ppf_out.info.alpha_bits, 16u);
  EXPECT_TRUE(test::SameAlpha(t.ppf(), ppf_out));
}

TEST(JxlTest, RoundtripDots) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/cvo9xd_keong_macan_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_NE(t.ppf().info.xsize, 0u);
  EXPECT_EQ(t.ppf().info.bits_per_sample, 8u);
  EXPECT_EQ(t.ppf().color_encoding.transfer_function,
            JXL_TRANSFER_FUNCTION_SRGB);

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel
  cparams.AddOption(JXL_ENC_FRAME_SETTING_DOTS, 1);
  cparams.distance = 0.05;
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 236173u,
              3000u);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 0.165);
}

TEST(JxlTest, RoundtripDisablePerceptual) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_NE(t.ppf().info.xsize, 0u);
  EXPECT_EQ(t.ppf().info.bits_per_sample, 8u);
  EXPECT_EQ(t.ppf().color_encoding.transfer_function,
            JXL_TRANSFER_FUNCTION_SRGB);

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel
  cparams.AddOption(JXL_ENC_FRAME_SETTING_DISABLE_PERCEPTUAL_HEURISTICS, 1);
  cparams.distance = 1.0;
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;

  size_t expected_size = 487778;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out),
              expected_size, 4000);
  // TODO(veluca): figure out why we can't get below this value.
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 11.0);
}

TEST(JxlTest, RoundtripNoise) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig =
      ReadTestData("external/wesaturate/500px/u76c0g_bliznaca_srgb8.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_NE(t.ppf().info.xsize, 0u);
  EXPECT_EQ(t.ppf().info.bits_per_sample, 8u);
  EXPECT_EQ(t.ppf().color_encoding.transfer_function,
            JXL_TRANSFER_FUNCTION_SRGB);

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 7);  // kSquirrel
  cparams.AddOption(JXL_ENC_FRAME_SETTING_NOISE, 1);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 41009, 750);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 1.42);
}

TEST(JxlTest, RoundtripLossless8Gray) {
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig = ReadTestData(
      "external/wesaturate/500px/cvo9xd_keong_macan_grayscale.png");
  TestImage t;
  ASSERT_TRUE(t.SetColorEncoding("Gra_D65_Rel_SRG"));
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  EXPECT_EQ(t.ppf().color_encoding.color_space, JXL_COLOR_SPACE_GRAY);
  EXPECT_EQ(t.ppf().info.bits_per_sample, 8u);

  JXLCompressParams cparams = test::CompressParamsForLossless();

  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  EXPECT_EQ(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out), 92495u);
  EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
  EXPECT_EQ(ppf_out.color_encoding.color_space, JXL_COLOR_SPACE_GRAY);
  EXPECT_EQ(ppf_out.info.bits_per_sample, 8u);
}

TEST(JxlTest, RoundtripAnimation) {
  if (!jxl::extras::CanDecode(jxl::extras::Codec::kGIF)) {
    fprintf(stderr, "Skipping test because of missing GIF decoder.\n");
    return;
  }
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig = ReadTestData("jxl/traffic_light.gif");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  EXPECT_EQ(4u, t.ppf().frames.size());

  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  EXPECT_SLIGHTLY_BELOW(Roundtrip(t.ppf(), {}, dparams, pool, &ppf_out), 2666);

  ASSERT_TRUE(t.CoalesceGIFAnimationWithAlpha());
  ASSERT_EQ(ppf_out.frames.size(), t.ppf().frames.size());
  static constexpr double kMaxButteraugli =
#if JXL_HIGH_PRECISION
      1.55;
#else
      1.75;
#endif
  EXPECT_LE(ButteraugliDistance(t.ppf(), ppf_out), kMaxButteraugli);
}

TEST(JxlTest, RoundtripLosslessAnimation) {
  if (!jxl::extras::CanDecode(jxl::extras::Codec::kGIF)) {
    fprintf(stderr, "Skipping test because of missing GIF decoder.\n");
    return;
  }
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig = ReadTestData("jxl/traffic_light.gif");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  EXPECT_EQ(4u, t.ppf().frames.size());

  JXLCompressParams cparams = test::CompressParamsForLossless();

  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  EXPECT_SLIGHTLY_BELOW(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out),
                        969u);

  ASSERT_TRUE(t.CoalesceGIFAnimationWithAlpha());
  ASSERT_EQ(ppf_out.frames.size(), t.ppf().frames.size());
  EXPECT_LE(ButteraugliDistance(t.ppf(), ppf_out), 5e-4);
}

TEST(JxlTest, RoundtripAnimationPatches) {
  if (!jxl::extras::CanDecode(jxl::extras::Codec::kGIF)) {
    fprintf(stderr, "Skipping test because of missing GIF decoder.\n");
    return;
  }
  ThreadPool* pool = nullptr;
  const std::vector<uint8_t> orig = ReadTestData("jxl/animation_patches.gif");

  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_EQ(2u, t.ppf().frames.size());

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_PATCHES, 1);

  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  // 40k with no patches, 27k with patch frames encoded multiple times.
  EXPECT_SLIGHTLY_BELOW(Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out),
                        21378u);
  EXPECT_EQ(ppf_out.frames.size(), t.ppf().frames.size());
  // >10 with broken patches; not all patches are detected on borders.
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 1.88);
}

size_t RoundtripJpeg(const std::vector<uint8_t>& jpeg_in, ThreadPool* pool) {
  std::vector<uint8_t> compressed;
  EXPECT_TRUE(extras::EncodeImageJXL({}, extras::PackedPixelFile(), &jpeg_in,
                                     &compressed));

  jxl::JXLDecompressParams dparams;
  test::SetThreadParallelRunner(dparams, pool);
  {
    std::vector<uint8_t> out;
    jxl::PackedPixelFile ppf;
    EXPECT_FALSE(DecodeImageJXL(compressed.data(), compressed.size() - 1,
                                dparams, nullptr, &ppf, &out));
  }
  std::vector<uint8_t> out;
  jxl::PackedPixelFile ppf;
  EXPECT_TRUE(DecodeImageJXL(compressed.data(), compressed.size(), dparams,
                             nullptr, &ppf, &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();
}

void RoundtripJpegToPixels(const std::vector<uint8_t>& jpeg_in,
                           JXLDecompressParams dparams, ThreadPool* pool,
                           PackedPixelFile* ppf_out) {
  std::vector<uint8_t> jpeg_bytes(jpeg_in.data(),
                                  jpeg_in.data() + jpeg_in.size());
  std::vector<uint8_t> compressed;
  EXPECT_TRUE(extras::EncodeImageJXL({}, extras::PackedPixelFile(), &jpeg_bytes,
                                     &compressed));

  test::DefaultAcceptedFormats(dparams);
  test::SetThreadParallelRunner(dparams, pool);
  EXPECT_TRUE(DecodeImageJXL(compressed.data(), compressed.size(), dparams,
                             nullptr, ppf_out, nullptr));
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompression444) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_444.jpg");
  // JPEG size is 696,659 bytes.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 568460u, 100);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionToPixels) {
  TEST_LIBJPEG_SUPPORT();
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_444.jpg");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));

  PackedPixelFile ppf_out;
  RoundtripJpegToPixels(orig, {}, pool.get(), &ppf_out);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 12);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionToPixels420) {
  TEST_LIBJPEG_SUPPORT();
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_420.jpg");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));

  PackedPixelFile ppf_out;
  RoundtripJpegToPixels(orig, {}, pool.get(), &ppf_out);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 11);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest,
                        RoundtripJpegRecompressionToPixels420EarlyFlush) {
  TEST_LIBJPEG_SUPPORT();
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_420.jpg");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));

  JXLDecompressParams dparams;
  dparams.max_downsampling = 8u;

  PackedPixelFile ppf_out;
  RoundtripJpegToPixels(orig, dparams, pool.get(), &ppf_out);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 4410);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionToPixels420Mul16) {
  TEST_LIBJPEG_SUPPORT();
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower_cropped.jpg");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));

  PackedPixelFile ppf_out;
  RoundtripJpegToPixels(orig, {}, pool.get(), &ppf_out);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 4);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionToPixelsAsymmetric) {
  TEST_LIBJPEG_SUPPORT();
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_asymmetric.jpg");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));

  PackedPixelFile ppf_out;
  RoundtripJpegToPixels(orig, {}, pool.get(), &ppf_out);
  EXPECT_SLIGHTLY_BELOW(ComputeDistance2(t.ppf(), ppf_out), 10);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionGray) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_gray.jpg");
  // JPEG size is 456,528 bytes.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 386557u, 200);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompression420) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_420.jpg");
  // JPEG size is 546,797 bytes.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 456098u, 20);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionLumaSubsample) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_luma_subsample.jpg");
  // JPEG size is 400,724 bytes.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 324301u, 20);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompression444wh12) {
  // 444 JPEG that has an interesting sampling-factor (1x2, 1x2, 1x2).
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_444_1x2.jpg");
  // JPEG size is 703,874 bytes.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 569154u, 20);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompression422) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_422.jpg");
  // JPEG size is 522,057 bytes.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 499766u, 20);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompression440) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_440.jpg");
  // JPEG size is 603,623 bytes.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 501617u, 20u);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionAsymmetric) {
  // 2x vertical downsample of one chroma channel, 2x horizontal downsample of
  // the other.
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_asymmetric.jpg");
  // JPEG size is 604,601 bytes.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 500974u, 20u);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompression420Progr) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/flower/flower.png.im_q85_420_progr.jpg");
  // JPEG size is 522,057 bytes.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 456037u, 20u);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionMetadata) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/jpeg_reconstruction/1x1_exif_xmp.jpg");
  // JPEG size is 4290 bytes
  // 1370 on 386, so higher margin.
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 1334u, 100u);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionEmptyExif) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig = {
      /* clang-format off */
      // SOI
      0xff, 0xd8,  //
      // APP1
      0xff, 0xe1, 0x00, 0x08, 0x45, 0x78, 0x69, 0x66, 0x00, 0x00,  //
      // DQT
      0xff, 0xdb, 0x00, 0x43, 0x00, 0x03, 0x02, 0x02, 0x03, 0x02,  //
      0x02, 0x03, 0x03, 0x03, 0x03, 0x04, 0x03, 0x03, 0x04, 0x05,  //
      0x08, 0x05, 0x05, 0x04, 0x04, 0x05, 0x0a, 0x07, 0x07, 0x06,  //
      0x08, 0x0c, 0x0a, 0x0c, 0x0c, 0x0b, 0x0a, 0x0b, 0x0b, 0x0d,  //
      0x0e, 0x12, 0x10, 0x0d, 0x0e, 0x11, 0x0e, 0x0b, 0x0b, 0x10,  //
      0x16, 0x10, 0x11, 0x13, 0x14, 0x15, 0x15, 0x15, 0x0c, 0x0f,  //
      0x17, 0x18, 0x16, 0x14, 0x18, 0x12, 0x14, 0x15, 0x14,        //
      // SOF
      0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01,  //
      0x01, 0x11, 0x00,                                            //
      // DHT
      0xff, 0xc4, 0x00, 0xd2, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01,  //
      0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  //
      0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,  //
      0x09, 0x0a, 0x0b, 0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02,  //
      0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d,  //
      0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31,  //
      0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32,  //
      0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52,  //
      0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,  //
      0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a,  //
      0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45,  //
      0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57,  //
      0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,  //
      0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83,  //
      0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94,  //
      0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,  //
      0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,  //
      0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,  //
      0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8,  //
      0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8,  //
      0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,  //
      0xf9, 0xfa,                                                  //
      // SOS
      0xff, 0xda, 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00,  //
      // entropy coded data
      0xfc, 0xaa, 0xaf,  //
      // EOI
      0xff, 0xd9,  //
      /* clang-format on */
  };
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 466u, 100u);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionRestarts) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/jpeg_reconstruction/bicycles_restarts.jpg");
  // JPEG size is 87478 bytes
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 76010u, 30u);
}

JXL_TRANSCODE_JPEG_TEST(JxlTest, RoundtripJpegRecompressionOrientationICC) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig =
      ReadTestData("jxl/jpeg_reconstruction/sideways_bench.jpg");
  // JPEG size is 15252 bytes
  EXPECT_NEAR(RoundtripJpeg(orig, pool.get()), 12000u, 470u);
  // TODO(jon): investigate why 'Cross-compiling i686-linux-gnu' produces a
  // larger result
}

TEST(JxlTest, RoundtripProgressive) {
  ThreadPoolForTests pool(4);
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_TRUE(t.SetDimensions(600, 1024));

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, 1);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC, 1);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESPONSIVE, 1);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out),
              68606u, 500u);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 1.2);
}

TEST(JxlTest, RoundtripProgressiveLevel2Slow) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig = ReadTestData("jxl/flower/flower.png");
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();
  ASSERT_TRUE(t.SetDimensions(600u, 1024u));

  JXLCompressParams cparams;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 9);  // kTortoise
  cparams.AddOption(JXL_ENC_FRAME_SETTING_PROGRESSIVE_DC, 2);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC, 1);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_RESPONSIVE, 1);
  extras::JXLDecompressParams dparams;

  PackedPixelFile ppf_out;
  EXPECT_NEAR(Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out), 71969,
              1000);
  EXPECT_SLIGHTLY_BELOW(ButteraugliDistance(t.ppf(), ppf_out), 1.20);
}

TEST(JxlTest, RoundtripUnsignedCustomBitdepthLossless) {
  ThreadPool* pool = nullptr;
  for (uint32_t num_channels = 1; num_channels < 6; ++num_channels) {
    for (JxlEndianness endianness : {JXL_LITTLE_ENDIAN, JXL_BIG_ENDIAN}) {
      for (uint32_t bitdepth = 3; bitdepth <= 16; ++bitdepth) {
        if (bitdepth <= 8 && endianness == JXL_BIG_ENDIAN) continue;
        printf("Testing %u channel unsigned %u bit %s endian lossless.\n",
               num_channels, bitdepth,
               endianness == JXL_LITTLE_ENDIAN ? "little" : "big");
        TestImage t;
        ASSERT_TRUE(t.SetDimensions(256u, 256u));
        ASSERT_TRUE(t.SetChannels(num_channels));
        t.SetAllBitDepths(bitdepth).SetEndianness(endianness);
        JXL_TEST_ASSIGN_OR_DIE(auto frame, t.AddFrame());
        frame.RandomFill();
        t.ppf().input_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM;

        JXLCompressParams cparams = test::CompressParamsForLossless();

        JXLDecompressParams dparams;
        dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);
        dparams.output_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM;

        PackedPixelFile ppf_out;
        Roundtrip(t.ppf(), cparams, dparams, pool, &ppf_out);

        ASSERT_TRUE(test::SamePixels(t.ppf(), ppf_out));
      }
    }
  }
}

TEST(JxlTest, LosslessPNMRoundtrip) {
  static const char* kChannels[] = {"", "g", "ga", "rgb", "rgba"};
  static const char* kExtension[] = {"", ".pgm", ".pam", ".ppm", ".pam"};
  for (size_t bit_depth = 1; bit_depth <= 16; ++bit_depth) {
    for (size_t channels = 1; channels <= 4; ++channels) {
      if (bit_depth == 1 && (channels == 2 || channels == 4)) continue;
      std::string extension(kExtension[channels]);
      std::string filename = "jxl/flower/flower_small." +
                             std::string(kChannels[channels]) + ".depth" +
                             std::to_string(bit_depth) + extension;
      const std::vector<uint8_t> orig = ReadTestData(filename);
      test::TestImage t;
      if (channels < 3) {
        ASSERT_TRUE(t.SetColorEncoding("Gra_D65_Rel_SRG"));
      }
      ASSERT_TRUE(t.DecodeFromBytes(orig));

      JXLCompressParams cparams = test::CompressParamsForLossless();
      cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 1);  // kLightning

      JXLDecompressParams dparams;
      dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);
      dparams.output_bitdepth.type = JXL_BIT_DEPTH_FROM_CODESTREAM;

      PackedPixelFile ppf_out;
      Roundtrip(t.ppf(), cparams, dparams, nullptr, &ppf_out);

      extras::EncodedImage encoded;
      auto encoder = extras::Encoder::FromExtension(extension);
      ASSERT_TRUE(encoder.get());
      ASSERT_TRUE(encoder->Encode(ppf_out, &encoded, nullptr));
      ASSERT_EQ(encoded.bitstreams.size(), 1u);
      ASSERT_EQ(orig.size(), encoded.bitstreams[0].size());
      EXPECT_EQ(0,
                memcmp(orig.data(), encoded.bitstreams[0].data(), orig.size()));
    }
  }
}

class JxlTest : public ::testing::TestWithParam<const char*> {};

TEST_P(JxlTest, LosslessSmallFewColors) {
  ThreadPoolForTests pool(8);
  const std::vector<uint8_t> orig = ReadTestData(GetParam());
  TestImage t;
  ASSERT_TRUE(t.DecodeFromBytes(orig));
  t.ClearMetadata();

  JXLCompressParams cparams;
  cparams.distance = 0;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, 1);
  JXLDecompressParams dparams;
  dparams.accepted_formats.push_back(t.ppf().frames[0].color.format);

  PackedPixelFile ppf_out;
  Roundtrip(t.ppf(), cparams, dparams, pool.get(), &ppf_out);
  EXPECT_EQ(ComputeDistance2(t.ppf(), ppf_out), 0.0);
}

JXL_GTEST_INSTANTIATE_TEST_SUITE_P(
    ImageTests, JxlTest,
    ::testing::Values("jxl/blending/cropped_traffic_light_frame-0.png",
                      "palette/358colors.png"));

struct StreamingTestParam {
  size_t xsize;
  size_t ysize;
  bool is_grey;
  bool has_alpha;
  int effort;
  bool progressive;

  size_t num_channels() const {
    return (is_grey ? 1 : 3) + (has_alpha ? 1 : 0);
  }

  float max_psnr() const { return is_grey ? 90 : 40; }

  static std::vector<StreamingTestParam> All() {
    std::vector<StreamingTestParam> params;
    for (int e : {1, 3, 4, 7}) {
      for (bool g : {false, true}) {
        params.push_back(StreamingTestParam{357, 517, g, false, e, false});
        params.push_back(StreamingTestParam{2247, 2357, g, false, e, false});
      }
    }
    params.push_back(StreamingTestParam{2247, 2357, false, false, 1, true});
    params.push_back(StreamingTestParam{2247, 2157, false, false, 5, false});
    params.push_back(StreamingTestParam{2247, 2157, false, true, 5, false});
    return params;
  }
};

std::ostream& operator<<(std::ostream& out, StreamingTestParam p) {
  out << (p.is_grey ? "Grey" : "RGB");
  out << p.xsize << "x" << p.ysize;
  out << "e" << p.effort;
  if (p.progressive) {
    out << "Progressive";
  }
  return out;
}

class JxlStreamingTest : public ::testing::TestWithParam<StreamingTestParam> {};

TEST_P(JxlStreamingTest, Roundtrip) {
  const StreamingTestParam& p = GetParam();

  jxl::test::TestImage image;
  ASSERT_TRUE(image.SetDimensions(p.xsize, p.ysize));
  image.SetDataType(JXL_TYPE_UINT8);
  ASSERT_TRUE(image.SetChannels(p.num_channels()));
  image.SetAllBitDepths(8);
  JXL_TEST_ASSIGN_OR_DIE(auto frame, image.AddFrame());
  frame.RandomFill();
  JXLCompressParams cparams;
  cparams.distance = 0.1;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, p.effort);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_BUFFERING, 3);
  if (p.progressive) {
    cparams.AddOption(JXL_ENC_FRAME_SETTING_PROGRESSIVE_AC, 1);
  }
  extras::JXLDecompressParams dparams;

  ThreadPoolForTests pool(8);
  PackedPixelFile ppf_out;
  Roundtrip(image.ppf(), cparams, dparams, pool.get(), &ppf_out);
  EXPECT_GT(jxl::test::ComputePSNR(image.ppf(), ppf_out), p.max_psnr());
}

JXL_GTEST_INSTANTIATE_TEST_SUITE_P(
    JxlStreamingTest, JxlStreamingTest,
    testing::ValuesIn(StreamingTestParam::All()));

struct StreamingEncodingTestParam {
  std::string file;
  int effort;
  float distance;
  int group_size;
  float palette_percent;

  static std::vector<StreamingEncodingTestParam> All() {
    std::vector<StreamingEncodingTestParam> params;
    for (const auto* file :
         {"jxl/flower/flower.png", "jxl/flower/flower_alpha.png"}) {
      for (int effort : {1, 3, 5, 6}) {
        if (effort != 1) {
          params.push_back(
              StreamingEncodingTestParam{file, effort, 1.0, 1, -1});
          params.push_back(
              StreamingEncodingTestParam{file, effort, 4.0, 1, -1});
        }
        for (auto group_size : {-1, 0}) {
          for (float palette_percent : {-1, 50, 100}) {
            params.push_back(StreamingEncodingTestParam{
                file, effort, 0.0, group_size, palette_percent});
          }
        }
      }
    }
    return params;
  }
};

std::ostream& operator<<(std::ostream& out,
                         const StreamingEncodingTestParam& p) {
  out << p.file << "-";
  out << "e" << p.effort;
  if (p.distance == 0) {
    out << "Lossless";
    out << "G" << p.group_size << "P" << p.palette_percent;
  } else {
    out << "D" << p.distance;
  }
  return out;
}

class JxlStreamingEncodingTest
    : public ::testing::TestWithParam<StreamingEncodingTestParam> {};

TEST_P(JxlStreamingEncodingTest, StreamingSamePixels) {
  const auto param = GetParam();

  const std::vector<uint8_t> orig = ReadTestData(param.file);
  jxl::test::TestImage image;
  ASSERT_TRUE(image.DecodeFromBytes(orig));

  JXLCompressParams cparams;
  cparams.distance = param.distance;
  cparams.AddOption(JXL_ENC_FRAME_SETTING_EFFORT, param.effort);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_MODULAR_GROUP_SIZE, param.group_size);
  cparams.AddFloatOption(JXL_ENC_FRAME_SETTING_CHANNEL_COLORS_GROUP_PERCENT,
                         param.palette_percent);
  cparams.AddOption(JXL_ENC_FRAME_SETTING_USE_FULL_IMAGE_HEURISTICS, 0);
  extras::JXLDecompressParams dparams;

  ThreadPoolForTests pool(8);
  PackedPixelFile ppf_out;
  Roundtrip(image.ppf(), cparams, dparams, pool.get(), &ppf_out);

  cparams.AddOption(JXL_ENC_FRAME_SETTING_BUFFERING, 3);
  PackedPixelFile ppf_out_streaming;
  Roundtrip(image.ppf(), cparams, dparams, pool.get(), &ppf_out_streaming);

  EXPECT_TRUE(jxl::test::SamePixels(ppf_out, ppf_out_streaming));
}

JXL_GTEST_INSTANTIATE_TEST_SUITE_P(
    JxlStreamingTest, JxlStreamingEncodingTest,
    testing::ValuesIn(StreamingEncodingTestParam::All()));

}  // namespace
}  // namespace jxl
