diff options
Diffstat (limited to 'third_party/jpeg-xl/lib/jpegli/test_utils.cc')
-rw-r--r-- | third_party/jpeg-xl/lib/jpegli/test_utils.cc | 1240 |
1 files changed, 1240 insertions, 0 deletions
diff --git a/third_party/jpeg-xl/lib/jpegli/test_utils.cc b/third_party/jpeg-xl/lib/jpegli/test_utils.cc new file mode 100644 index 0000000000..1ae5483d4a --- /dev/null +++ b/third_party/jpeg-xl/lib/jpegli/test_utils.cc @@ -0,0 +1,1240 @@ +// 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/jpegli/test_utils.h" + +#include <cmath> + +#include "lib/jpegli/decode.h" +#include "lib/jpegli/encode.h" +#include "lib/jxl/base/byte_order.h" +#include "lib/jxl/base/file_io.h" +#include "lib/jxl/base/printf_macros.h" +#include "lib/jxl/base/status.h" +#include "lib/jxl/sanitizers.h" + +#if !defined(TEST_DATA_PATH) +#include "tools/cpp/runfiles/runfiles.h" +#endif + +namespace jpegli { + +#if defined(TEST_DATA_PATH) +std::string GetTestDataPath(const std::string& filename) { + return std::string(TEST_DATA_PATH "/") + filename; +} +#else +using bazel::tools::cpp::runfiles::Runfiles; +const std::unique_ptr<Runfiles> kRunfiles(Runfiles::Create("")); +std::string GetTestDataPath(const std::string& filename) { + std::string root(JPEGXL_ROOT_PACKAGE "/testdata/"); + return kRunfiles->Rlocation(root + filename); +} +#endif + +std::vector<uint8_t> ReadTestData(const std::string& filename) { + std::string full_path = GetTestDataPath(filename); + std::vector<uint8_t> data; + fprintf(stderr, "ReadTestData %s\n", full_path.c_str()); + JXL_CHECK(jxl::ReadFile(full_path, &data)); + printf("Test data %s is %d bytes long.\n", filename.c_str(), + static_cast<int>(data.size())); + return data; +} + +void CustomQuantTable::Generate() { + basic_table.resize(DCTSIZE2); + quantval.resize(DCTSIZE2); + switch (table_type) { + case 0: { + for (int k = 0; k < DCTSIZE2; ++k) { + basic_table[k] = k + 1; + } + break; + } + default: + for (int k = 0; k < DCTSIZE2; ++k) { + basic_table[k] = table_type; + } + } + for (int k = 0; k < DCTSIZE2; ++k) { + quantval[k] = (basic_table[k] * scale_factor + 50U) / 100U; + quantval[k] = std::max(quantval[k], 1U); + quantval[k] = std::min(quantval[k], 65535U); + if (!add_raw) { + quantval[k] = std::min(quantval[k], force_baseline ? 255U : 32767U); + } + } +} + +bool PNMParser::ParseHeader(const uint8_t** pos, size_t* xsize, size_t* ysize, + size_t* num_channels, size_t* bitdepth) { + if (pos_[0] != 'P' || (pos_[1] != '5' && pos_[1] != '6')) { + fprintf(stderr, "Invalid PNM header."); + return false; + } + *num_channels = (pos_[1] == '5' ? 1 : 3); + pos_ += 2; + + size_t maxval; + if (!SkipWhitespace() || !ParseUnsigned(xsize) || !SkipWhitespace() || + !ParseUnsigned(ysize) || !SkipWhitespace() || !ParseUnsigned(&maxval) || + !SkipWhitespace()) { + return false; + } + if (maxval == 0 || maxval >= 65536) { + fprintf(stderr, "Invalid maxval value.\n"); + return false; + } + bool found_bitdepth = false; + for (int bits = 1; bits <= 16; ++bits) { + if (maxval == (1u << bits) - 1) { + *bitdepth = bits; + found_bitdepth = true; + break; + } + } + if (!found_bitdepth) { + fprintf(stderr, "Invalid maxval value.\n"); + return false; + } + + *pos = pos_; + return true; +} + +bool PNMParser::ParseUnsigned(size_t* number) { + if (pos_ == end_ || *pos_ < '0' || *pos_ > '9') { + fprintf(stderr, "Expected unsigned number.\n"); + return false; + } + *number = 0; + while (pos_ < end_ && *pos_ >= '0' && *pos_ <= '9') { + *number *= 10; + *number += *pos_ - '0'; + ++pos_; + } + + return true; +} + +bool PNMParser::SkipWhitespace() { + if (pos_ == end_ || !IsWhitespace(*pos_)) { + fprintf(stderr, "Expected whitespace.\n"); + return false; + } + while (pos_ < end_ && IsWhitespace(*pos_)) { + ++pos_; + } + return true; +} + +bool ReadPNM(const std::vector<uint8_t>& data, size_t* xsize, size_t* ysize, + size_t* num_channels, size_t* bitdepth, + std::vector<uint8_t>* pixels) { + if (data.size() < 2) { + fprintf(stderr, "PNM file too small.\n"); + return false; + } + PNMParser parser(data.data(), data.size()); + const uint8_t* pos = nullptr; + if (!parser.ParseHeader(&pos, xsize, ysize, num_channels, bitdepth)) { + return false; + } + pixels->resize(data.data() + data.size() - pos); + memcpy(&(*pixels)[0], pos, pixels->size()); + return true; +} + +std::string ColorSpaceName(J_COLOR_SPACE colorspace) { + switch (colorspace) { + case JCS_UNKNOWN: + return "UNKNOWN"; + case JCS_GRAYSCALE: + return "GRAYSCALE"; + case JCS_RGB: + return "RGB"; + case JCS_YCbCr: + return "YCbCr"; + case JCS_CMYK: + return "CMYK"; + case JCS_YCCK: + return "YCCK"; + default: + return ""; + } +} + +std::string IOMethodName(JpegliDataType data_type, + JpegliEndianness endianness) { + std::string retval; + if (data_type == JPEGLI_TYPE_UINT8) { + return ""; + } else if (data_type == JPEGLI_TYPE_UINT16) { + retval = "UINT16"; + } else if (data_type == JPEGLI_TYPE_FLOAT) { + retval = "FLOAT"; + } + if (endianness == JPEGLI_LITTLE_ENDIAN) { + retval += "LE"; + } else if (endianness == JPEGLI_BIG_ENDIAN) { + retval += "BE"; + } + return retval; +} + +std::string SamplingId(const CompressParams& jparams) { + std::stringstream os; + JXL_CHECK(jparams.h_sampling.size() == jparams.v_sampling.size()); + if (!jparams.h_sampling.empty()) { + size_t len = jparams.h_sampling.size(); + while (len > 1 && jparams.h_sampling[len - 1] == 1 && + jparams.v_sampling[len - 1] == 1) { + --len; + } + os << "SAMP"; + for (size_t i = 0; i < len; ++i) { + if (i > 0) os << "_"; + os << jparams.h_sampling[i] << "x" << jparams.v_sampling[i]; + } + } + return os.str(); +} + +std::ostream& operator<<(std::ostream& os, const TestImage& input) { + os << input.xsize << "x" << input.ysize; + os << IOMethodName(input.data_type, input.endianness); + if (input.color_space != JCS_RGB) { + os << "InputColor" << ColorSpaceName(input.color_space); + } + if (input.color_space == JCS_UNKNOWN) { + os << input.components; + } + return os; +} + +std::ostream& operator<<(std::ostream& os, const CompressParams& jparams) { + os << "Q" << jparams.quality; + os << SamplingId(jparams); + if (jparams.set_jpeg_colorspace) { + os << "JpegColor" << ColorSpaceName(jparams.jpeg_color_space); + } + if (!jparams.comp_ids.empty()) { + os << "CID"; + for (size_t i = 0; i < jparams.comp_ids.size(); ++i) { + os << jparams.comp_ids[i]; + } + } + if (!jparams.quant_indexes.empty()) { + os << "QIDX"; + for (size_t i = 0; i < jparams.quant_indexes.size(); ++i) { + os << jparams.quant_indexes[i]; + } + for (const auto& table : jparams.quant_tables) { + os << "TABLE" << table.slot_idx << "T" << table.table_type << "F" + << table.scale_factor + << (table.add_raw ? "R" + : table.force_baseline ? "B" + : ""); + } + } + if (jparams.progressive_mode >= 0) { + os << "P" << jparams.progressive_mode; + } else if (jparams.simple_progression) { + os << "Psimple"; + } + if (jparams.optimize_coding == 1) { + JXL_CHECK(jparams.progressive_mode <= 0 && !jparams.simple_progression); + os << "OptimizedCode"; + } else if (jparams.optimize_coding == 0) { + JXL_CHECK(jparams.progressive_mode <= 0 && !jparams.simple_progression); + os << "FixedCode"; + if (jparams.use_flat_dc_luma_code) { + os << "FlatDCLuma"; + } else if (jparams.omit_standard_tables) { + os << "OmitDHT"; + } + } + if (!jparams.use_adaptive_quantization) { + os << "NoAQ"; + } + if (jparams.restart_interval > 0) { + os << "R" << jparams.restart_interval; + } + if (jparams.restart_in_rows > 0) { + os << "RR" << jparams.restart_in_rows; + } + if (jparams.xyb_mode) { + os << "XYB"; + } else if (jparams.libjpeg_mode) { + os << "Libjpeg"; + } + if (jparams.override_JFIF >= 0) { + os << (jparams.override_JFIF ? "AddJFIF" : "NoJFIF"); + } + if (jparams.override_Adobe >= 0) { + os << (jparams.override_Adobe ? "AddAdobe" : "NoAdobe"); + } + if (jparams.add_marker) { + os << "AddMarker"; + } + if (!jparams.icc.empty()) { + os << "ICCSize" << jparams.icc.size(); + } + if (jparams.smoothing_factor != 0) { + os << "SF" << jparams.smoothing_factor; + } + return os; +} + +void SetNumChannels(J_COLOR_SPACE colorspace, size_t* channels) { + if (colorspace == JCS_GRAYSCALE) { + *channels = 1; + } else if (colorspace == JCS_RGB || colorspace == JCS_YCbCr) { + *channels = 3; + } else if (colorspace == JCS_CMYK || colorspace == JCS_YCCK) { + *channels = 4; + } else if (colorspace == JCS_UNKNOWN) { + JXL_CHECK(*channels <= 4); + } else { + JXL_ABORT(); + } +} + +void RGBToYCbCr(float r, float g, float b, float* y, float* cb, float* cr) { + *y = 0.299f * r + 0.587f * g + 0.114f * b; + *cb = -0.168736f * r - 0.331264f * g + 0.5f * b + 0.5f; + *cr = 0.5f * r - 0.418688f * g - 0.081312f * b + 0.5f; +} + +void ConvertPixel(const uint8_t* input_rgb, uint8_t* out, + J_COLOR_SPACE colorspace, size_t num_channels, + JpegliDataType data_type = JPEGLI_TYPE_UINT8, + bool swap_endianness = JPEGLI_NATIVE_ENDIAN) { + const float kMul = 255.0f; + float r = input_rgb[0] / kMul; + float g = input_rgb[1] / kMul; + float b = input_rgb[2] / kMul; + uint8_t out8[MAX_COMPONENTS]; + if (colorspace == JCS_GRAYSCALE) { + const float Y = 0.299f * r + 0.587f * g + 0.114f * b; + out8[0] = static_cast<uint8_t>(std::round(Y * kMul)); + } else if (colorspace == JCS_RGB || colorspace == JCS_UNKNOWN) { + for (size_t c = 0; c < num_channels; ++c) { + out8[c] = input_rgb[std::min<size_t>(2, c)]; + } + } else if (colorspace == JCS_YCbCr) { + float Y, Cb, Cr; + RGBToYCbCr(r, g, b, &Y, &Cb, &Cr); + out8[0] = static_cast<uint8_t>(std::round(Y * kMul)); + out8[1] = static_cast<uint8_t>(std::round(Cb * kMul)); + out8[2] = static_cast<uint8_t>(std::round(Cr * kMul)); + } else if (colorspace == JCS_CMYK || colorspace == JCS_YCCK) { + float K = 1.0f - std::max(r, std::max(g, b)); + float scaleK = 1.0f / (1.0f - K); + r *= scaleK; + g *= scaleK; + b *= scaleK; + if (colorspace == JCS_CMYK) { + out8[0] = static_cast<uint8_t>(std::round((1.0f - r) * kMul)); + out8[1] = static_cast<uint8_t>(std::round((1.0f - g) * kMul)); + out8[2] = static_cast<uint8_t>(std::round((1.0f - b) * kMul)); + } else if (colorspace == JCS_YCCK) { + float Y, Cb, Cr; + RGBToYCbCr(r, g, b, &Y, &Cb, &Cr); + out8[0] = static_cast<uint8_t>(std::round(Y * kMul)); + out8[1] = static_cast<uint8_t>(std::round(Cb * kMul)); + out8[2] = static_cast<uint8_t>(std::round(Cr * kMul)); + } + out8[3] = static_cast<uint8_t>(std::round(K * kMul)); + } else { + JXL_ABORT("Colorspace %d not supported", colorspace); + } + if (data_type == JPEGLI_TYPE_UINT8) { + memcpy(out, out8, num_channels); + } else if (data_type == JPEGLI_TYPE_UINT16) { + for (size_t c = 0; c < num_channels; ++c) { + uint16_t val = (out8[c] << 8) + out8[c]; + val |= 0x40; // Make little-endian and big-endian asymmetric + if (swap_endianness) { + val = JXL_BSWAP16(val); + } + memcpy(&out[sizeof(val) * c], &val, sizeof(val)); + } + } else if (data_type == JPEGLI_TYPE_FLOAT) { + for (size_t c = 0; c < num_channels; ++c) { + float val = out8[c] / 255.0f; + if (swap_endianness) { + val = BSwapFloat(val); + } + memcpy(&out[sizeof(val) * c], &val, sizeof(val)); + } + } +} + +void ConvertToGrayscale(TestImage* img) { + if (img->color_space == JCS_GRAYSCALE) return; + JXL_CHECK(img->data_type == JPEGLI_TYPE_UINT8); + for (size_t i = 0; i < img->pixels.size(); i += 3) { + if (img->color_space == JCS_RGB) { + ConvertPixel(&img->pixels[i], &img->pixels[i / 3], JCS_GRAYSCALE, 1); + } else if (img->color_space == JCS_YCbCr) { + img->pixels[i / 3] = img->pixels[i]; + } + } + img->pixels.resize(img->pixels.size() / 3); + img->color_space = JCS_GRAYSCALE; + img->components = 1; +} + +void GeneratePixels(TestImage* img) { + const std::vector<uint8_t> imgdata = ReadTestData("jxl/flower/flower.pnm"); + size_t xsize, ysize, channels, bitdepth; + std::vector<uint8_t> pixels; + JXL_CHECK(ReadPNM(imgdata, &xsize, &ysize, &channels, &bitdepth, &pixels)); + if (img->xsize == 0) img->xsize = xsize; + if (img->ysize == 0) img->ysize = ysize; + JXL_CHECK(img->xsize <= xsize); + JXL_CHECK(img->ysize <= ysize); + JXL_CHECK(3 == channels); + JXL_CHECK(8 == bitdepth); + size_t in_bytes_per_pixel = channels; + size_t in_stride = xsize * in_bytes_per_pixel; + size_t x0 = (xsize - img->xsize) / 2; + size_t y0 = (ysize - img->ysize) / 2; + SetNumChannels(img->color_space, &img->components); + size_t out_bytes_per_pixel = + jpegli_bytes_per_sample(img->data_type) * img->components; + size_t out_stride = img->xsize * out_bytes_per_pixel; + bool swap_endianness = + (img->endianness == JPEGLI_LITTLE_ENDIAN && !IsLittleEndian()) || + (img->endianness == JPEGLI_BIG_ENDIAN && IsLittleEndian()); + img->pixels.resize(img->ysize * out_stride); + for (size_t iy = 0; iy < img->ysize; ++iy) { + size_t y = y0 + iy; + for (size_t ix = 0; ix < img->xsize; ++ix) { + size_t x = x0 + ix; + size_t idx_in = y * in_stride + x * in_bytes_per_pixel; + size_t idx_out = iy * out_stride + ix * out_bytes_per_pixel; + ConvertPixel(&pixels[idx_in], &img->pixels[idx_out], img->color_space, + img->components, img->data_type, swap_endianness); + } + } +} + +void GenerateRawData(const CompressParams& jparams, TestImage* img) { + for (size_t c = 0; c < img->components; ++c) { + size_t xsize = jparams.comp_width(*img, c); + size_t ysize = jparams.comp_height(*img, c); + size_t factor_y = jparams.max_v_sample() / jparams.v_samp(c); + size_t factor_x = jparams.max_h_sample() / jparams.h_samp(c); + size_t factor = factor_x * factor_y; + std::vector<uint8_t> plane(ysize * xsize); + size_t bytes_per_pixel = img->components; + for (size_t y = 0; y < ysize; ++y) { + for (size_t x = 0; x < xsize; ++x) { + int result = 0; + for (size_t iy = 0; iy < factor_y; ++iy) { + size_t yy = std::min(y * factor_y + iy, img->ysize - 1); + for (size_t ix = 0; ix < factor_x; ++ix) { + size_t xx = std::min(x * factor_x + ix, img->xsize - 1); + size_t pixel_ix = (yy * img->xsize + xx) * bytes_per_pixel + c; + result += img->pixels[pixel_ix]; + } + } + result = static_cast<uint8_t>((result + factor / 2) / factor); + plane[y * xsize + x] = result; + } + } + img->raw_data.emplace_back(std::move(plane)); + } +} + +void GenerateCoeffs(const CompressParams& jparams, TestImage* img) { + for (size_t c = 0; c < img->components; ++c) { + int xsize_blocks = jparams.comp_width(*img, c) / DCTSIZE; + int ysize_blocks = jparams.comp_height(*img, c) / DCTSIZE; + std::vector<JCOEF> plane(ysize_blocks * xsize_blocks * DCTSIZE2); + for (int by = 0; by < ysize_blocks; ++by) { + for (int bx = 0; bx < xsize_blocks; ++bx) { + JCOEF* block = &plane[(by * xsize_blocks + bx) * DCTSIZE2]; + for (int k = 0; k < DCTSIZE2; ++k) { + block[k] = (bx - by) / (k + 1); + } + } + } + img->coeffs.emplace_back(std::move(plane)); + } +} + +void EncodeWithJpegli(const TestImage& input, const CompressParams& jparams, + j_compress_ptr cinfo) { + cinfo->image_width = input.xsize; + cinfo->image_height = input.ysize; + cinfo->input_components = input.components; + if (jparams.xyb_mode) { + jpegli_set_xyb_mode(cinfo); + } + if (jparams.libjpeg_mode) { + jpegli_enable_adaptive_quantization(cinfo, FALSE); + jpegli_use_standard_quant_tables(cinfo); + jpegli_set_progressive_level(cinfo, 0); + } + jpegli_set_defaults(cinfo); + cinfo->in_color_space = input.color_space; + jpegli_default_colorspace(cinfo); + if (jparams.override_JFIF >= 0) { + cinfo->write_JFIF_header = jparams.override_JFIF; + } + if (jparams.override_Adobe >= 0) { + cinfo->write_Adobe_marker = jparams.override_Adobe; + } + if (jparams.set_jpeg_colorspace) { + jpegli_set_colorspace(cinfo, jparams.jpeg_color_space); + } + if (!jparams.comp_ids.empty()) { + for (int c = 0; c < cinfo->num_components; ++c) { + cinfo->comp_info[c].component_id = jparams.comp_ids[c]; + } + } + if (!jparams.h_sampling.empty()) { + for (int c = 0; c < cinfo->num_components; ++c) { + cinfo->comp_info[c].h_samp_factor = jparams.h_sampling[c]; + cinfo->comp_info[c].v_samp_factor = jparams.v_sampling[c]; + } + } + jpegli_set_quality(cinfo, jparams.quality, TRUE); + if (!jparams.quant_indexes.empty()) { + for (int c = 0; c < cinfo->num_components; ++c) { + cinfo->comp_info[c].quant_tbl_no = jparams.quant_indexes[c]; + } + for (const auto& table : jparams.quant_tables) { + if (table.add_raw) { + cinfo->quant_tbl_ptrs[table.slot_idx] = + jpegli_alloc_quant_table((j_common_ptr)cinfo); + for (int k = 0; k < DCTSIZE2; ++k) { + cinfo->quant_tbl_ptrs[table.slot_idx]->quantval[k] = + table.quantval[k]; + } + cinfo->quant_tbl_ptrs[table.slot_idx]->sent_table = FALSE; + } else { + jpegli_add_quant_table(cinfo, table.slot_idx, &table.basic_table[0], + table.scale_factor, table.force_baseline); + } + } + } + if (jparams.simple_progression) { + jpegli_simple_progression(cinfo); + JXL_CHECK(jparams.progressive_mode == -1); + } + if (jparams.progressive_mode > 2) { + const ScanScript& script = kTestScript[jparams.progressive_mode - 3]; + cinfo->scan_info = script.scans; + cinfo->num_scans = script.num_scans; + } else if (jparams.progressive_mode >= 0) { + jpegli_set_progressive_level(cinfo, jparams.progressive_mode); + } + jpegli_set_input_format(cinfo, input.data_type, input.endianness); + jpegli_enable_adaptive_quantization(cinfo, jparams.use_adaptive_quantization); + cinfo->restart_interval = jparams.restart_interval; + cinfo->restart_in_rows = jparams.restart_in_rows; + cinfo->smoothing_factor = jparams.smoothing_factor; + if (jparams.optimize_coding == 1) { + cinfo->optimize_coding = TRUE; + } else if (jparams.optimize_coding == 0) { + cinfo->optimize_coding = FALSE; + } + cinfo->raw_data_in = !input.raw_data.empty(); + if (jparams.optimize_coding == 0 && jparams.use_flat_dc_luma_code) { + JHUFF_TBL* tbl = cinfo->dc_huff_tbl_ptrs[0]; + memset(tbl, 0, sizeof(*tbl)); + tbl->bits[4] = 15; + for (int i = 0; i < 15; ++i) tbl->huffval[i] = i; + } + if (input.coeffs.empty()) { + bool write_all_tables = TRUE; + if (jparams.optimize_coding == 0 && !jparams.use_flat_dc_luma_code && + jparams.omit_standard_tables) { + write_all_tables = FALSE; + cinfo->dc_huff_tbl_ptrs[0]->sent_table = TRUE; + cinfo->dc_huff_tbl_ptrs[1]->sent_table = TRUE; + cinfo->ac_huff_tbl_ptrs[0]->sent_table = TRUE; + cinfo->ac_huff_tbl_ptrs[1]->sent_table = TRUE; + } + jpegli_start_compress(cinfo, write_all_tables); + if (jparams.add_marker) { + jpegli_write_marker(cinfo, kSpecialMarker0, kMarkerData, + sizeof(kMarkerData)); + jpegli_write_m_header(cinfo, kSpecialMarker1, sizeof(kMarkerData)); + for (size_t p = 0; p < sizeof(kMarkerData); ++p) { + jpegli_write_m_byte(cinfo, kMarkerData[p]); + } + for (size_t i = 0; i < kMarkerSequenceLen; ++i) { + jpegli_write_marker(cinfo, kMarkerSequence[i], kMarkerData, + ((i + 2) % sizeof(kMarkerData))); + } + } + if (!jparams.icc.empty()) { + jpegli_write_icc_profile(cinfo, jparams.icc.data(), jparams.icc.size()); + } + } + if (cinfo->raw_data_in) { + // Need to copy because jpeg API requires non-const pointers. + std::vector<std::vector<uint8_t>> raw_data = input.raw_data; + size_t max_lines = jparams.max_v_sample() * DCTSIZE; + std::vector<std::vector<JSAMPROW>> rowdata(cinfo->num_components); + std::vector<JSAMPARRAY> data(cinfo->num_components); + for (int c = 0; c < cinfo->num_components; ++c) { + rowdata[c].resize(jparams.v_samp(c) * DCTSIZE); + data[c] = &rowdata[c][0]; + } + while (cinfo->next_scanline < cinfo->image_height) { + for (int c = 0; c < cinfo->num_components; ++c) { + size_t cwidth = cinfo->comp_info[c].width_in_blocks * DCTSIZE; + size_t cheight = cinfo->comp_info[c].height_in_blocks * DCTSIZE; + size_t num_lines = jparams.v_samp(c) * DCTSIZE; + size_t y0 = (cinfo->next_scanline / max_lines) * num_lines; + for (size_t i = 0; i < num_lines; ++i) { + rowdata[c][i] = + (y0 + i < cheight ? &raw_data[c][(y0 + i) * cwidth] : nullptr); + } + } + size_t num_lines = jpegli_write_raw_data(cinfo, &data[0], max_lines); + JXL_CHECK(num_lines == max_lines); + } + } else if (!input.coeffs.empty()) { + j_common_ptr comptr = reinterpret_cast<j_common_ptr>(cinfo); + jvirt_barray_ptr* coef_arrays = reinterpret_cast<jvirt_barray_ptr*>(( + *cinfo->mem->alloc_small)( + comptr, JPOOL_IMAGE, cinfo->num_components * sizeof(jvirt_barray_ptr))); + for (int c = 0; c < cinfo->num_components; ++c) { + size_t xsize_blocks = jparams.comp_width(input, c) / DCTSIZE; + size_t ysize_blocks = jparams.comp_height(input, c) / DCTSIZE; + coef_arrays[c] = (*cinfo->mem->request_virt_barray)( + comptr, JPOOL_IMAGE, FALSE, xsize_blocks, ysize_blocks, + cinfo->comp_info[c].v_samp_factor); + } + jpegli_write_coefficients(cinfo, coef_arrays); + if (jparams.add_marker) { + jpegli_write_marker(cinfo, kSpecialMarker0, kMarkerData, + sizeof(kMarkerData)); + jpegli_write_m_header(cinfo, kSpecialMarker1, sizeof(kMarkerData)); + for (size_t p = 0; p < sizeof(kMarkerData); ++p) { + jpegli_write_m_byte(cinfo, kMarkerData[p]); + } + } + for (int c = 0; c < cinfo->num_components; ++c) { + jpeg_component_info* comp = &cinfo->comp_info[c]; + for (size_t by = 0; by < comp->height_in_blocks; ++by) { + JBLOCKARRAY ba = (*cinfo->mem->access_virt_barray)( + comptr, coef_arrays[c], by, 1, true); + size_t stride = comp->width_in_blocks * sizeof(JBLOCK); + size_t offset = by * comp->width_in_blocks * DCTSIZE2; + memcpy(ba[0], &input.coeffs[c][offset], stride); + } + } + } else { + size_t stride = cinfo->image_width * cinfo->input_components * + jpegli_bytes_per_sample(input.data_type); + std::vector<uint8_t> row_bytes(stride); + for (size_t y = 0; y < cinfo->image_height; ++y) { + memcpy(&row_bytes[0], &input.pixels[y * stride], stride); + JSAMPROW row[] = {row_bytes.data()}; + jpegli_write_scanlines(cinfo, row, 1); + } + } + jpegli_finish_compress(cinfo); +} + +bool EncodeWithJpegli(const TestImage& input, const CompressParams& jparams, + std::vector<uint8_t>* compressed) { + uint8_t* buffer = nullptr; + unsigned long buffer_size = 0; + jpeg_compress_struct cinfo; + const auto try_catch_block = [&]() -> bool { + ERROR_HANDLER_SETUP(jpegli); + jpegli_create_compress(&cinfo); + jpegli_mem_dest(&cinfo, &buffer, &buffer_size); + EncodeWithJpegli(input, jparams, &cinfo); + return true; + }; + bool success = try_catch_block(); + jpegli_destroy_compress(&cinfo); + if (success) { + compressed->resize(buffer_size); + std::copy_n(buffer, buffer_size, compressed->data()); + } + if (buffer) std::free(buffer); + return success; +} + +void SetScanDecompressParams(const DecompressParams& dparams, + j_decompress_ptr cinfo, int scan_number, + bool is_jpegli) { + const ScanDecompressParams* sparams = nullptr; + for (const auto& sp : dparams.scan_params) { + if (scan_number <= sp.max_scan_number) { + sparams = &sp; + break; + } + } + if (sparams == nullptr) { + return; + } + if (dparams.quantize_colors) { + cinfo->dither_mode = sparams->dither_mode; + if (sparams->color_quant_mode == CQUANT_1PASS) { + cinfo->two_pass_quantize = FALSE; + cinfo->colormap = nullptr; + } else if (sparams->color_quant_mode == CQUANT_2PASS) { + JXL_CHECK(cinfo->out_color_space = JCS_RGB); + cinfo->two_pass_quantize = TRUE; + cinfo->colormap = nullptr; + } else if (sparams->color_quant_mode == CQUANT_EXTERNAL) { + JXL_CHECK(cinfo->out_color_space = JCS_RGB); + cinfo->two_pass_quantize = FALSE; + bool have_colormap = cinfo->colormap != nullptr; + cinfo->actual_number_of_colors = kTestColorMapNumColors; + cinfo->colormap = (*cinfo->mem->alloc_sarray)( + reinterpret_cast<j_common_ptr>(cinfo), JPOOL_IMAGE, + cinfo->actual_number_of_colors, 3); + jxl::msan::UnpoisonMemory(cinfo->colormap, 3 * sizeof(JSAMPROW)); + for (int i = 0; i < kTestColorMapNumColors; ++i) { + cinfo->colormap[0][i] = (kTestColorMap[i] >> 16) & 0xff; + cinfo->colormap[1][i] = (kTestColorMap[i] >> 8) & 0xff; + cinfo->colormap[2][i] = (kTestColorMap[i] >> 0) & 0xff; + } + if (have_colormap) { + if (is_jpegli) { + jpegli_new_colormap(cinfo); + } else { + jpeg_new_colormap(cinfo); + } + } + } else if (sparams->color_quant_mode == CQUANT_REUSE) { + JXL_CHECK(cinfo->out_color_space = JCS_RGB); + JXL_CHECK(cinfo->colormap); + } + } +} + +void SetDecompressParams(const DecompressParams& dparams, + j_decompress_ptr cinfo, bool is_jpegli) { + cinfo->do_block_smoothing = dparams.do_block_smoothing; + cinfo->do_fancy_upsampling = dparams.do_fancy_upsampling; + if (dparams.output_mode == RAW_DATA) { + cinfo->raw_data_out = TRUE; + } + if (dparams.set_out_color_space) { + cinfo->out_color_space = dparams.out_color_space; + if (dparams.out_color_space == JCS_UNKNOWN) { + cinfo->jpeg_color_space = JCS_UNKNOWN; + } + } + cinfo->scale_num = dparams.scale_num; + cinfo->scale_denom = dparams.scale_denom; + cinfo->quantize_colors = dparams.quantize_colors; + cinfo->desired_number_of_colors = dparams.desired_number_of_colors; + if (!dparams.scan_params.empty()) { + if (cinfo->buffered_image) { + for (const auto& sparams : dparams.scan_params) { + if (sparams.color_quant_mode == CQUANT_1PASS) { + cinfo->enable_1pass_quant = TRUE; + } else if (sparams.color_quant_mode == CQUANT_2PASS) { + cinfo->enable_2pass_quant = TRUE; + } else if (sparams.color_quant_mode == CQUANT_EXTERNAL) { + cinfo->enable_external_quant = TRUE; + } + } + SetScanDecompressParams(dparams, cinfo, 1, is_jpegli); + } else { + SetScanDecompressParams(dparams, cinfo, kLastScan, is_jpegli); + } + } + if (is_jpegli) { + jpegli_set_output_format(cinfo, dparams.data_type, dparams.endianness); + } +} + +void CheckMarkerPresent(j_decompress_ptr cinfo, uint8_t marker_type) { + bool marker_found = false; + for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != nullptr; + marker = marker->next) { + jxl::msan::UnpoisonMemory(marker, sizeof(*marker)); + jxl::msan::UnpoisonMemory(marker->data, marker->data_length); + if (marker->marker == marker_type && + marker->data_length == sizeof(kMarkerData) && + memcmp(marker->data, kMarkerData, sizeof(kMarkerData)) == 0) { + marker_found = true; + } + } + JXL_CHECK(marker_found); +} + +void VerifyHeader(const CompressParams& jparams, j_decompress_ptr cinfo) { + if (jparams.set_jpeg_colorspace) { + JXL_CHECK(cinfo->jpeg_color_space == jparams.jpeg_color_space); + } + if (jparams.override_JFIF >= 0) { + JXL_CHECK(cinfo->saw_JFIF_marker == jparams.override_JFIF); + } + if (jparams.override_Adobe >= 0) { + JXL_CHECK(cinfo->saw_Adobe_marker == jparams.override_Adobe); + } + if (jparams.add_marker) { + CheckMarkerPresent(cinfo, kSpecialMarker0); + CheckMarkerPresent(cinfo, kSpecialMarker1); + } + jxl::msan::UnpoisonMemory( + cinfo->comp_info, cinfo->num_components * sizeof(cinfo->comp_info[0])); + int max_h_samp_factor = 1; + int max_v_samp_factor = 1; + for (int i = 0; i < cinfo->num_components; ++i) { + jpeg_component_info* comp = &cinfo->comp_info[i]; + if (!jparams.comp_ids.empty()) { + JXL_CHECK(comp->component_id == jparams.comp_ids[i]); + } + if (!jparams.h_sampling.empty()) { + JXL_CHECK(comp->h_samp_factor == jparams.h_sampling[i]); + } + if (!jparams.v_sampling.empty()) { + JXL_CHECK(comp->v_samp_factor == jparams.v_sampling[i]); + } + if (!jparams.quant_indexes.empty()) { + JXL_CHECK(comp->quant_tbl_no == jparams.quant_indexes[i]); + } + max_h_samp_factor = std::max(max_h_samp_factor, comp->h_samp_factor); + max_v_samp_factor = std::max(max_v_samp_factor, comp->v_samp_factor); + } + JXL_CHECK(max_h_samp_factor == cinfo->max_h_samp_factor); + JXL_CHECK(max_v_samp_factor == cinfo->max_v_samp_factor); + int referenced_tables[NUM_QUANT_TBLS] = {}; + for (int i = 0; i < cinfo->num_components; ++i) { + jpeg_component_info* comp = &cinfo->comp_info[i]; + JXL_CHECK(comp->width_in_blocks == + DivCeil(cinfo->image_width * comp->h_samp_factor, + max_h_samp_factor * DCTSIZE)); + JXL_CHECK(comp->height_in_blocks == + DivCeil(cinfo->image_height * comp->v_samp_factor, + max_v_samp_factor * DCTSIZE)); + referenced_tables[comp->quant_tbl_no] = 1; + } + for (const auto& table : jparams.quant_tables) { + JQUANT_TBL* quant_table = cinfo->quant_tbl_ptrs[table.slot_idx]; + if (!referenced_tables[table.slot_idx]) { + JXL_CHECK(quant_table == nullptr); + continue; + } + JXL_CHECK(quant_table != nullptr); + jxl::msan::UnpoisonMemory(quant_table, sizeof(*quant_table)); + for (int k = 0; k < DCTSIZE2; ++k) { + JXL_CHECK(quant_table->quantval[k] == table.quantval[k]); + } + } +} + +void VerifyScanHeader(const CompressParams& jparams, j_decompress_ptr cinfo) { + JXL_CHECK(cinfo->input_scan_number > 0); + if (cinfo->progressive_mode) { + JXL_CHECK(cinfo->Ss != 0 || cinfo->Se != 63); + } else { + JXL_CHECK(cinfo->Ss == 0 && cinfo->Se == 63); + } + if (jparams.progressive_mode > 2) { + JXL_CHECK(jparams.progressive_mode < 3 + kNumTestScripts); + const ScanScript& script = kTestScript[jparams.progressive_mode - 3]; + JXL_CHECK(cinfo->input_scan_number <= script.num_scans); + const jpeg_scan_info& scan = script.scans[cinfo->input_scan_number - 1]; + JXL_CHECK(cinfo->comps_in_scan == scan.comps_in_scan); + for (int i = 0; i < cinfo->comps_in_scan; ++i) { + JXL_CHECK(cinfo->cur_comp_info[i]->component_index == + scan.component_index[i]); + } + JXL_CHECK(cinfo->Ss == scan.Ss); + JXL_CHECK(cinfo->Se == scan.Se); + JXL_CHECK(cinfo->Ah == scan.Ah); + JXL_CHECK(cinfo->Al == scan.Al); + } + if (jparams.restart_interval > 0) { + JXL_CHECK(cinfo->restart_interval == jparams.restart_interval); + } else if (jparams.restart_in_rows > 0) { + JXL_CHECK(cinfo->restart_interval == + jparams.restart_in_rows * cinfo->MCUs_per_row); + } + if (jparams.progressive_mode == 0 && jparams.optimize_coding == 0) { + if (cinfo->jpeg_color_space == JCS_RGB) { + JXL_CHECK(cinfo->comp_info[0].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[1].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[2].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[0].ac_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[1].ac_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[2].ac_tbl_no == 0); + } else if (cinfo->jpeg_color_space == JCS_YCbCr) { + JXL_CHECK(cinfo->comp_info[0].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[1].dc_tbl_no == 1); + JXL_CHECK(cinfo->comp_info[2].dc_tbl_no == 1); + JXL_CHECK(cinfo->comp_info[0].ac_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[1].ac_tbl_no == 1); + JXL_CHECK(cinfo->comp_info[2].ac_tbl_no == 1); + } else if (cinfo->jpeg_color_space == JCS_CMYK) { + JXL_CHECK(cinfo->comp_info[0].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[1].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[2].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[3].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[0].ac_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[1].ac_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[2].ac_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[3].ac_tbl_no == 0); + } else if (cinfo->jpeg_color_space == JCS_YCCK) { + JXL_CHECK(cinfo->comp_info[0].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[1].dc_tbl_no == 1); + JXL_CHECK(cinfo->comp_info[2].dc_tbl_no == 1); + JXL_CHECK(cinfo->comp_info[3].dc_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[0].ac_tbl_no == 0); + JXL_CHECK(cinfo->comp_info[1].ac_tbl_no == 1); + JXL_CHECK(cinfo->comp_info[2].ac_tbl_no == 1); + JXL_CHECK(cinfo->comp_info[3].ac_tbl_no == 0); + } + if (jparams.use_flat_dc_luma_code) { + JHUFF_TBL* tbl = cinfo->dc_huff_tbl_ptrs[0]; + jxl::msan::UnpoisonMemory(tbl, sizeof(*tbl)); + for (int i = 0; i < 15; ++i) { + JXL_CHECK(tbl->huffval[i] == i); + } + } + } +} + +void UnmapColors(uint8_t* row, size_t xsize, int components, + JSAMPARRAY colormap, size_t num_colors) { + JXL_CHECK(colormap != nullptr); + std::vector<uint8_t> tmp(xsize * components); + for (size_t x = 0; x < xsize; ++x) { + JXL_CHECK(row[x] < num_colors); + for (int c = 0; c < components; ++c) { + tmp[x * components + c] = colormap[c][row[x]]; + } + } + memcpy(row, tmp.data(), tmp.size()); +} + +void ReadOutputPass(j_decompress_ptr cinfo, const DecompressParams& dparams, + TestImage* output) { + JDIMENSION xoffset = 0; + JDIMENSION yoffset = 0; + JDIMENSION xsize_cropped = cinfo->output_width; + JDIMENSION ysize_cropped = cinfo->output_height; + if (dparams.crop_output) { + xoffset = xsize_cropped = cinfo->output_width / 3; + yoffset = ysize_cropped = cinfo->output_height / 3; + jpeg_crop_scanline(cinfo, &xoffset, &xsize_cropped); + JXL_CHECK(xsize_cropped == cinfo->output_width); + } + output->xsize = xsize_cropped; + output->ysize = ysize_cropped; + output->components = cinfo->out_color_components; + if (cinfo->quantize_colors) { + jxl::msan::UnpoisonMemory(cinfo->colormap, cinfo->out_color_components * + sizeof(cinfo->colormap[0])); + for (int c = 0; c < cinfo->out_color_components; ++c) { + jxl::msan::UnpoisonMemory( + cinfo->colormap[c], + cinfo->actual_number_of_colors * sizeof(cinfo->colormap[c][0])); + } + } + if (!cinfo->raw_data_out) { + size_t stride = output->xsize * output->components; + output->pixels.resize(output->ysize * stride); + output->color_space = cinfo->out_color_space; + if (yoffset > 0) { + jpeg_skip_scanlines(cinfo, yoffset); + } + for (size_t y = 0; y < output->ysize; ++y) { + JSAMPROW rows[] = { + reinterpret_cast<JSAMPLE*>(&output->pixels[y * stride])}; + JXL_CHECK(1 == jpeg_read_scanlines(cinfo, rows, 1)); + jxl::msan::UnpoisonMemory( + rows[0], sizeof(JSAMPLE) * cinfo->output_components * output->xsize); + if (cinfo->quantize_colors) { + UnmapColors(rows[0], cinfo->output_width, cinfo->out_color_components, + cinfo->colormap, cinfo->actual_number_of_colors); + } + } + if (cinfo->output_scanline < cinfo->output_height) { + jpeg_skip_scanlines(cinfo, cinfo->output_height - cinfo->output_scanline); + } + } else { + output->color_space = cinfo->jpeg_color_space; + for (int c = 0; c < cinfo->num_components; ++c) { + size_t xsize = cinfo->comp_info[c].width_in_blocks * DCTSIZE; + size_t ysize = cinfo->comp_info[c].height_in_blocks * DCTSIZE; + std::vector<uint8_t> plane(ysize * xsize); + output->raw_data.emplace_back(std::move(plane)); + } + while (cinfo->output_scanline < cinfo->output_height) { + size_t iMCU_height = cinfo->max_v_samp_factor * DCTSIZE; + JXL_CHECK(cinfo->output_scanline == cinfo->output_iMCU_row * iMCU_height); + std::vector<std::vector<JSAMPROW>> rowdata(cinfo->num_components); + std::vector<JSAMPARRAY> data(cinfo->num_components); + for (int c = 0; c < cinfo->num_components; ++c) { + size_t xsize = cinfo->comp_info[c].width_in_blocks * DCTSIZE; + size_t ysize = cinfo->comp_info[c].height_in_blocks * DCTSIZE; + size_t num_lines = cinfo->comp_info[c].v_samp_factor * DCTSIZE; + rowdata[c].resize(num_lines); + size_t y0 = cinfo->output_iMCU_row * num_lines; + for (size_t i = 0; i < num_lines; ++i) { + rowdata[c][i] = + y0 + i < ysize ? &output->raw_data[c][(y0 + i) * xsize] : nullptr; + } + data[c] = &rowdata[c][0]; + } + JXL_CHECK(iMCU_height == + jpeg_read_raw_data(cinfo, &data[0], iMCU_height)); + } + } + JXL_CHECK(cinfo->total_iMCU_rows == + DivCeil(cinfo->image_height, cinfo->max_v_samp_factor * DCTSIZE)); +} + +void CopyCoefficients(j_decompress_ptr cinfo, jvirt_barray_ptr* coef_arrays, + TestImage* output) { + output->xsize = cinfo->image_width; + output->ysize = cinfo->image_height; + output->components = cinfo->num_components; + output->color_space = cinfo->out_color_space; + j_common_ptr comptr = reinterpret_cast<j_common_ptr>(cinfo); + for (int c = 0; c < cinfo->num_components; ++c) { + jpeg_component_info* comp = &cinfo->comp_info[c]; + std::vector<JCOEF> coeffs(comp->width_in_blocks * comp->height_in_blocks * + DCTSIZE2); + for (size_t by = 0; by < comp->height_in_blocks; ++by) { + JBLOCKARRAY ba = (*cinfo->mem->access_virt_barray)(comptr, coef_arrays[c], + by, 1, true); + size_t stride = comp->width_in_blocks * sizeof(JBLOCK); + size_t offset = by * comp->width_in_blocks * DCTSIZE2; + memcpy(&coeffs[offset], ba[0], stride); + } + output->coeffs.emplace_back(std::move(coeffs)); + } +} + +// Verifies that an image encoded with libjpegli can be decoded with libjpeg, +// and checks that the jpeg coding metadata matches jparams. +void DecodeAllScansWithLibjpeg(const CompressParams& jparams, + const DecompressParams& dparams, + const std::vector<uint8_t>& compressed, + std::vector<TestImage>* output_progression) { + jpeg_decompress_struct cinfo = {}; + const auto try_catch_block = [&]() { + ERROR_HANDLER_SETUP(jpeg); + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, compressed.data(), compressed.size()); + if (jparams.add_marker) { + jpeg_save_markers(&cinfo, kSpecialMarker0, 0xffff); + jpeg_save_markers(&cinfo, kSpecialMarker1, 0xffff); + } + JXL_CHECK(JPEG_REACHED_SOS == + jpeg_read_header(&cinfo, /*require_image=*/TRUE)); + cinfo.buffered_image = TRUE; + SetDecompressParams(dparams, &cinfo, /*is_jpegli=*/false); + VerifyHeader(jparams, &cinfo); + JXL_CHECK(jpeg_start_decompress(&cinfo)); + // start decompress should not read the whole input in buffered image mode + JXL_CHECK(!jpeg_input_complete(&cinfo)); + JXL_CHECK(cinfo.output_scan_number == 0); + int sos_marker_cnt = 1; // read header reads the first SOS marker + while (!jpeg_input_complete(&cinfo)) { + JXL_CHECK(cinfo.input_scan_number == sos_marker_cnt); + if (dparams.skip_scans && (cinfo.input_scan_number % 2) != 1) { + int result = JPEG_SUSPENDED; + while (result != JPEG_REACHED_SOS && result != JPEG_REACHED_EOI) { + result = jpeg_consume_input(&cinfo); + } + if (result == JPEG_REACHED_SOS) ++sos_marker_cnt; + continue; + } + SetScanDecompressParams(dparams, &cinfo, cinfo.input_scan_number, + /*is_jpegli=*/false); + JXL_CHECK(jpeg_start_output(&cinfo, cinfo.input_scan_number)); + // start output sets output_scan_number, but does not change + // input_scan_number + JXL_CHECK(cinfo.output_scan_number == cinfo.input_scan_number); + JXL_CHECK(cinfo.input_scan_number == sos_marker_cnt); + VerifyScanHeader(jparams, &cinfo); + TestImage output; + ReadOutputPass(&cinfo, dparams, &output); + output_progression->emplace_back(std::move(output)); + // read scanlines/read raw data does not change input/output scan number + if (!cinfo.progressive_mode) { + JXL_CHECK(cinfo.input_scan_number == sos_marker_cnt); + JXL_CHECK(cinfo.output_scan_number == cinfo.input_scan_number); + } + JXL_CHECK(jpeg_finish_output(&cinfo)); + ++sos_marker_cnt; // finish output reads the next SOS marker or EOI + if (dparams.output_mode == COEFFICIENTS) { + jvirt_barray_ptr* coef_arrays = jpeg_read_coefficients(&cinfo); + JXL_CHECK(coef_arrays != nullptr); + CopyCoefficients(&cinfo, coef_arrays, &output_progression->back()); + } + } + JXL_CHECK(jpeg_finish_decompress(&cinfo)); + return true; + }; + JXL_CHECK(try_catch_block()); + jpeg_destroy_decompress(&cinfo); +} + +void DecodeWithLibjpeg(const CompressParams& jparams, + const DecompressParams& dparams, j_decompress_ptr cinfo, + TestImage* output) { + if (jparams.add_marker) { + jpeg_save_markers(cinfo, kSpecialMarker0, 0xffff); + jpeg_save_markers(cinfo, kSpecialMarker1, 0xffff); + } + if (!jparams.icc.empty()) { + jpeg_save_markers(cinfo, JPEG_APP0 + 2, 0xffff); + } + JXL_CHECK(JPEG_REACHED_SOS == + jpeg_read_header(cinfo, /*require_image=*/TRUE)); + if (!jparams.icc.empty()) { + uint8_t* icc_data = nullptr; + unsigned int icc_len; + JXL_CHECK(jpeg_read_icc_profile(cinfo, &icc_data, &icc_len)); + JXL_CHECK(icc_data); + jxl::msan::UnpoisonMemory(icc_data, icc_len); + JXL_CHECK(0 == memcmp(jparams.icc.data(), icc_data, icc_len)); + free(icc_data); + } + SetDecompressParams(dparams, cinfo, /*is_jpegli=*/false); + VerifyHeader(jparams, cinfo); + if (dparams.output_mode == COEFFICIENTS) { + jvirt_barray_ptr* coef_arrays = jpeg_read_coefficients(cinfo); + JXL_CHECK(coef_arrays != nullptr); + CopyCoefficients(cinfo, coef_arrays, output); + } else { + JXL_CHECK(jpeg_start_decompress(cinfo)); + VerifyScanHeader(jparams, cinfo); + ReadOutputPass(cinfo, dparams, output); + } + JXL_CHECK(jpeg_finish_decompress(cinfo)); +} + +void DecodeWithLibjpeg(const CompressParams& jparams, + const DecompressParams& dparams, + const std::vector<uint8_t>& compressed, + TestImage* output) { + jpeg_decompress_struct cinfo = {}; + const auto try_catch_block = [&]() { + ERROR_HANDLER_SETUP(jpeg); + jpeg_create_decompress(&cinfo); + jpeg_mem_src(&cinfo, compressed.data(), compressed.size()); + DecodeWithLibjpeg(jparams, dparams, &cinfo, output); + return true; + }; + JXL_CHECK(try_catch_block()); + jpeg_destroy_decompress(&cinfo); +} + +void DumpImage(const TestImage& image, const std::string fn) { + JXL_CHECK(image.components == 1 || image.components == 3); + jxl::FileWrapper f(fn.c_str(), "wb"); + size_t bytes_per_sample = jpegli_bytes_per_sample(image.data_type); + uint32_t maxval = (1u << (8 * bytes_per_sample)) - 1; + char type = image.components == 1 ? '5' : '6'; + fprintf(f, "P%c\n%" PRIuS " %" PRIuS "\n%u\n", type, image.xsize, image.ysize, + maxval); + fwrite(image.pixels.data(), 1, image.pixels.size(), f); +} + +double DistanceRms(const TestImage& input, const TestImage& output, + size_t start_line, size_t num_lines, double* max_diff) { + size_t stride = input.xsize * input.components; + size_t start_offset = start_line * stride; + auto get_sample = [&](const TestImage& im, const std::vector<uint8_t>& data, + size_t idx) -> double { + size_t bytes_per_sample = jpegli_bytes_per_sample(im.data_type); + bool is_little_endian = + (im.endianness == JPEGLI_LITTLE_ENDIAN || + (im.endianness == JPEGLI_NATIVE_ENDIAN && IsLittleEndian())); + size_t offset = start_offset + idx * bytes_per_sample; + JXL_CHECK(offset < data.size()); + const uint8_t* p = &data[offset]; + if (im.data_type == JPEGLI_TYPE_UINT8) { + static const double mul8 = 1.0 / 255.0; + return p[0] * mul8; + } else if (im.data_type == JPEGLI_TYPE_UINT16) { + static const double mul16 = 1.0 / 65535.0; + return (is_little_endian ? LoadLE16(p) : LoadBE16(p)) * mul16; + } else if (im.data_type == JPEGLI_TYPE_FLOAT) { + return (is_little_endian ? LoadLEFloat(p) : LoadBEFloat(p)); + } + return 0.0; + }; + double diff2 = 0.0; + size_t num_samples = 0; + if (max_diff) *max_diff = 0.0; + if (!input.pixels.empty() && !output.pixels.empty()) { + num_samples = num_lines * stride; + for (size_t i = 0; i < num_samples; ++i) { + double sample_orig = get_sample(input, input.pixels, i); + double sample_output = get_sample(output, output.pixels, i); + double diff = sample_orig - sample_output; + if (max_diff) *max_diff = std::max(*max_diff, 255.0 * std::abs(diff)); + diff2 += diff * diff; + } + } else { + JXL_CHECK(!input.raw_data.empty()); + JXL_CHECK(!output.raw_data.empty()); + for (size_t c = 0; c < input.raw_data.size(); ++c) { + JXL_CHECK(c < output.raw_data.size()); + num_samples += input.raw_data[c].size(); + for (size_t i = 0; i < input.raw_data[c].size(); ++i) { + double sample_orig = get_sample(input, input.raw_data[c], i); + double sample_output = get_sample(output, output.raw_data[c], i); + double diff = sample_orig - sample_output; + if (max_diff) *max_diff = std::max(*max_diff, 255.0 * std::abs(diff)); + diff2 += diff * diff; + } + } + } + return std::sqrt(diff2 / num_samples) * 255.0; +} + +double DistanceRms(const TestImage& input, const TestImage& output, + double* max_diff) { + return DistanceRms(input, output, 0, output.ysize, max_diff); +} + +void VerifyOutputImage(const TestImage& input, const TestImage& output, + size_t start_line, size_t num_lines, double max_rms, + double max_diff) { + double max_d; + double rms = DistanceRms(input, output, start_line, num_lines, &max_d); + printf("rms: %f, max_rms: %f, max_d: %f, max_diff: %f\n", rms, max_rms, + max_d, max_diff); + JXL_CHECK(rms <= max_rms); + JXL_CHECK(max_d <= max_diff); +} + +void VerifyOutputImage(const TestImage& input, const TestImage& output, + double max_rms, double max_diff) { + JXL_CHECK(output.xsize == input.xsize); + JXL_CHECK(output.ysize == input.ysize); + JXL_CHECK(output.components == input.components); + JXL_CHECK(output.color_space == input.color_space); + if (!input.coeffs.empty()) { + JXL_CHECK(input.coeffs.size() == input.components); + JXL_CHECK(output.coeffs.size() == input.components); + for (size_t c = 0; c < input.components; ++c) { + JXL_CHECK(output.coeffs[c].size() == input.coeffs[c].size()); + JXL_CHECK(0 == memcmp(input.coeffs[c].data(), output.coeffs[c].data(), + input.coeffs[c].size())); + } + } else { + VerifyOutputImage(input, output, 0, output.ysize, max_rms, max_diff); + } +} + +} // namespace jpegli |