From 48a7abd4eafd6bffa448d675f38c897fbc286082 Mon Sep 17 00:00:00 2001 From: Chema Gonzalez Date: Tue, 7 Apr 2026 13:53:35 -0700 Subject: [PATCH] Support non-full-depth uint16 PNM input (10/12/14-bit) The encoder rejected uint16 input unless bits_per_sample was exactly 16, causing failures for valid PGM files with maxval other than 65535 (e.g. 10-bit maxval=1023, 12-bit maxval=4095, 14-bit maxval=16383). Fix by converting non-full-depth uint16 data to float with proper normalization (value / (2^N - 1)) before feeding to jpegli, which internally works in float anyway. Tested encode/decode roundtrip (cjpegli -q 90 + djpegli) with all 8-16 bit depth PNM files in testdata/. PSNR and per-pixel error (in 8-bit units) are consistent across all depths: Grayscale (PGM) - flower_small Depth PSNR (dB) Mean Median StdDev Max ------------------------------------------------------------------------ 8 45.88 0.946 1.000 0.885 9.000 9 45.79 1.018 0.804 0.822 9.603 10 45.86 1.007 0.827 0.820 8.654 11 45.88 0.981 0.921 0.846 8.217 12 45.89 0.964 0.952 0.864 8.941 13 45.88 0.956 0.983 0.874 8.979 14 45.88 0.951 0.992 0.880 8.982 15 45.88 0.948 0.999 0.883 8.999 16 45.88 0.946 1.000 0.885 9.000 RGB (PPM) - flower_small Depth PSNR (dB) Mean Median StdDev Max ------------------------------------------------------------------------ 8 41.58 1.542 1.000 1.464 32.000 9 41.51 1.597 1.231 1.430 32.740 10 41.56 1.584 1.226 1.426 32.361 11 41.57 1.567 1.114 1.441 32.047 12 41.58 1.554 1.055 1.451 32.077 13 41.58 1.548 1.027 1.457 32.061 14 41.58 1.545 1.013 1.460 32.022 15 41.58 1.543 1.004 1.463 32.010 16 41.58 1.542 1.000 1.464 32.000 --- lib/extras/enc/jpegli.cc | 35 ++++++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/lib/extras/enc/jpegli.cc b/lib/extras/enc/jpegli.cc index 14a7a398..7bbf9323 100644 --- a/lib/extras/enc/jpegli.cc +++ b/lib/extras/enc/jpegli.cc @@ -66,10 +66,8 @@ Status VerifyInput(const PackedPixelFile& ppf) { JXL_RETURN_IF_ERROR(Encoder::VerifyBitDepth(image.format.data_type, info.bits_per_sample, info.exponent_bits_per_sample)); - if ((image.format.data_type == JXL_TYPE_UINT8 && info.bits_per_sample != 8) || - (image.format.data_type == JXL_TYPE_UINT16 && - info.bits_per_sample != 16)) { - return JXL_FAILURE("Only full bit depth unsigned types are supported."); + if (image.format.data_type == JXL_TYPE_UINT8 && info.bits_per_sample != 8) { + return JXL_FAILURE("Only full bit depth uint8 type is supported."); } return true; } @@ -391,6 +389,7 @@ Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings, unsigned char* output_buffer = nullptr; unsigned long output_size = 0; // NOLINT std::vector row_bytes; + std::vector float_row; const size_t max_vector_size = MaxVectorSize(); size_t rowlen = RoundUpTo(ppf.info.xsize, max_vector_size); hwy::AlignedFreeUniquePtr xyb_tmp = @@ -479,7 +478,11 @@ Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings, cinfo.write_Adobe_marker = JXL_FALSE; } const PackedImage& image = ppf.frames[0].color; - if (jpeg_settings.xyb) { + const bool needs_float_conversion = + !jpeg_settings.xyb && + image.format.data_type == JXL_TYPE_UINT16 && + info.bits_per_sample != 16; + if (jpeg_settings.xyb || needs_float_conversion) { jpegli_set_input_format(&cinfo, JPEGLI_TYPE_FLOAT, JPEGLI_NATIVE_ENDIAN); } else { jpegli_set_input_format(&cinfo, ConvertDataType(image.format.data_type), @@ -530,6 +533,28 @@ Status EncodeJpeg(const PackedPixelFile& ppf, const JpegSettings& jpeg_settings, JSAMPROW row[] = {reinterpret_cast(row_out)}; jpegli_write_scanlines(&cinfo, row, 1); } + } else if (needs_float_conversion) { + // Convert non-full-depth uint16 to float [0.0, 1.0] for jpegli. + const float scale = 1.0f / ((1u << info.bits_per_sample) - 1); + const bool is_little_endian = + (image.format.endianness == JXL_LITTLE_ENDIAN || + (image.format.endianness == JXL_NATIVE_ENDIAN && IsLittleEndian())); + const size_t c_in = image.format.num_channels; + const size_t c_out = cinfo.num_components; + float_row.resize(info.xsize * c_out); + for (size_t y = 0; y < info.ysize; ++y) { + const uint8_t* row_in = pixels + y * image.stride; + for (size_t x = 0; x < info.xsize; ++x) { + for (size_t c = 0; c < c_out; ++c) { + const size_t ix = c_in * x + c; + uint16_t val = is_little_endian ? LoadLE16(&row_in[2 * ix]) + : LoadBE16(&row_in[2 * ix]); + float_row[c_out * x + c] = val * scale; + } + } + JSAMPROW row[] = {reinterpret_cast(float_row.data())}; + jpegli_write_scanlines(&cinfo, row, 1); + } } else { row_bytes.resize(image.stride); if (cinfo.num_components == static_cast(image.format.num_channels)) {