From ca7e333f86543f4310c2a96006e2aa7c0f385621 Mon Sep 17 00:00:00 2001 From: Mayk Thewessen Date: Sun, 21 Jun 2026 22:57:17 +0200 Subject: [PATCH 1/5] Add JPEG XL (DNG 1.7 / Compression 52546) decompressor DNG 1.7 stores the raw image with JPEG XL compression (TIFF Compression tag 52546) -- as used by Apple ProRAW on the 48MP main camera of iPhone 16 Pro and 17 Pro (PhotometricInterpretation LinearRaw, 3 channels, 10-bit, tiled). Add a libjxl-backed JpegXlDecompressor modelled on the existing lossy-JPEG path, wired into DngDecoder chunk acceptance and AbstractDngDecompressor dispatch. Gated behind a new WITH_JPEGXL CMake option (default ON, mirroring WITH_JPEG / WITH_ZLIB); libjxl is discovered via pkg-config. When disabled, the compression is reported unsupported via a #pragma message. --- CMakeLists.txt | 1 + cmake/src-dependencies.cmake | 23 +++ src/config.h.in | 2 + src/librawspeed/decoders/DngDecoder.cpp | 14 +- .../decompressors/AbstractDngDecompressor.cpp | 35 +++++ src/librawspeed/decompressors/CMakeLists.txt | 6 + .../decompressors/JpegXlDecompressor.cpp | 140 ++++++++++++++++++ .../decompressors/JpegXlDecompressor.h | 56 +++++++ 8 files changed, 276 insertions(+), 1 deletion(-) create mode 100644 src/librawspeed/decompressors/JpegXlDecompressor.cpp create mode 100644 src/librawspeed/decompressors/JpegXlDecompressor.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2346db8d4..af15a8181 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,6 +80,7 @@ else() set(ALLOW_DOWNLOADING_PUGIXML OFF CACHE BOOL "If pugixml src tree is not found in location specified by PUGIXML_PATH, do fetch the archive from internet" FORCE) endif() option(WITH_JPEG "Enable JPEG support for DNG Lossy JPEG support" ON) +option(WITH_JPEGXL "Enable JPEG XL support for DNG 1.7 JPEG XL compression" ON) option(WITH_ZLIB "Enable ZLIB support for DNG deflate support" ON) if(WITH_ZLIB) option(USE_BUNDLED_ZLIB "Build and use zlib in-tree" OFF) diff --git a/cmake/src-dependencies.cmake b/cmake/src-dependencies.cmake index a9a887e6d..59619ea8e 100644 --- a/cmake/src-dependencies.cmake +++ b/cmake/src-dependencies.cmake @@ -184,6 +184,29 @@ else() endif() add_feature_info("Lossy JPEG decoding" HAVE_JPEG "used for DNG Lossy JPEG compression decoding") +unset(HAVE_JPEGXL) +if(WITH_JPEGXL) + message(STATUS "Looking for JPEG XL (libjxl)") + find_package(PkgConfig QUIET) + if(PkgConfig_FOUND) + pkg_check_modules(libjxl IMPORTED_TARGET libjxl) + endif() + if(NOT libjxl_FOUND) + message(SEND_ERROR "Did not find libjxl! Either install jpeg-xl, or pass -DWITH_JPEGXL=OFF to disable JPEG XL.") + else() + message(STATUS "Looking for JPEG XL - found (${libjxl_VERSION})") + set(HAVE_JPEGXL 1) + target_link_libraries(rawspeed PRIVATE PkgConfig::libjxl) + set_package_properties(libjxl PROPERTIES + TYPE RECOMMENDED + DESCRIPTION "JPEG XL reference codec library" + PURPOSE "Used for decoding DNG JPEG XL (DNG 1.7) compression") + endif() +else() + message(STATUS "JPEG XL is disabled, DNG JPEG XL (DNG 1.7) support won't be available.") +endif() +add_feature_info("JPEG XL decoding" HAVE_JPEGXL "used for DNG JPEG XL (DNG 1.7) compression decoding") + unset(HAVE_ZLIB) if (WITH_ZLIB) message(STATUS "Looking for ZLIB") diff --git a/src/config.h.in b/src/config.h.in index 623d14174..0e42746e9 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -65,6 +65,8 @@ static_assert(RAWSPEED_LARGEPAGESIZE >= RAWSPEED_PAGESIZE, #cmakedefine HAVE_JPEG #cmakedefine HAVE_JPEG_MEM_SRC +#cmakedefine HAVE_JPEGXL + #cmakedefine HAVE_CXX_THREAD_LOCAL #cmakedefine HAVE_GCC_THREAD_LOCAL diff --git a/src/librawspeed/decoders/DngDecoder.cpp b/src/librawspeed/decoders/DngDecoder.cpp index ecd119898..a59cbb10e 100644 --- a/src/librawspeed/decoders/DngDecoder.cpp +++ b/src/librawspeed/decoders/DngDecoder.cpp @@ -120,7 +120,10 @@ void DngDecoder::dropUnsuportedChunks(std::vector* data) { #ifdef HAVE_JPEG case 0x884c: // lossy JPEG #endif - // no change, if supported, then is still supported. +#ifdef HAVE_JPEGXL + case 52546: // JPEG XL (DNG 1.7) +#endif + // no change, if supported, then is still supported. break; #ifndef HAVE_ZLIB @@ -140,6 +143,15 @@ void DngDecoder::dropUnsuportedChunks(std::vector* data) { "chunk, but the jpeg support was " "disabled at build!"); [[clang::fallthrough]]; +#endif +#ifndef HAVE_JPEGXL + case 52546: // JPEG XL (DNG 1.7) +#pragma message \ + "JPEG XL is not present! DNG JPEG XL compression will not be supported!" + writeLog(DEBUG_PRIO::WARNING, "DNG Decoder: found JPEG XL-encoded " + "chunk, but JPEG XL support was " + "disabled at build!"); + [[clang::fallthrough]]; #endif default: supported = false; diff --git a/src/librawspeed/decompressors/AbstractDngDecompressor.cpp b/src/librawspeed/decompressors/AbstractDngDecompressor.cpp index b828a4fe2..83b27beae 100644 --- a/src/librawspeed/decompressors/AbstractDngDecompressor.cpp +++ b/src/librawspeed/decompressors/AbstractDngDecompressor.cpp @@ -49,6 +49,10 @@ #include "decompressors/JpegDecompressor.h" #endif +#ifdef HAVE_JPEGXL +#include "decompressors/JpegXlDecompressor.h" +#endif + namespace rawspeed { template <> void AbstractDngDecompressor::decompressThread<1>() const noexcept { @@ -201,6 +205,29 @@ void AbstractDngDecompressor::decompressThread<0x884c>() const noexcept { } #endif +#ifdef HAVE_JPEGXL +template <> +void AbstractDngDecompressor::decompressThread<52546>() const noexcept { +#ifdef HAVE_OPENMP +#pragma omp for schedule(static) +#endif + for (const auto& e : + Array1DRef(slices.data(), implicit_cast(slices.size()))) { + try { + JpegXlDecompressor j(e.bs.peekBuffer(e.bs.getRemainSize()), mRaw); + j.decode(e.offX, e.offY); + } catch (const RawDecoderException& err) { + mRaw->setError(err.what()); + } catch (const IOException& err) { + mRaw->setError(err.what()); + } catch (...) { + // We should not get any other exception type here. + __builtin_unreachable(); + } + } +} +#endif + void AbstractDngDecompressor::decompressThread() const noexcept { invariant(mRaw->dim.x > 0); invariant(mRaw->dim.y > 0); @@ -232,6 +259,14 @@ void AbstractDngDecompressor::decompressThread() const noexcept { #else #pragma message "JPEG is not present! Lossy JPEG DNG will not be supported!" mRaw->setError("jpeg support is disabled."); +#endif + } else if (compression == 52546) { + /* JPEG XL (DNG 1.7) */ +#ifdef HAVE_JPEGXL + decompressThread<52546>(); +#else +#pragma message "JPEG XL is not present! DNG JPEG XL will not be supported!" + mRaw->setError("JPEG XL support is disabled."); #endif } else { mRaw->setError("AbstractDngDecompressor: Unknown compression"); diff --git a/src/librawspeed/decompressors/CMakeLists.txt b/src/librawspeed/decompressors/CMakeLists.txt index 8933a3ad1..20627c348 100644 --- a/src/librawspeed/decompressors/CMakeLists.txt +++ b/src/librawspeed/decompressors/CMakeLists.txt @@ -26,6 +26,8 @@ FILE(GLOB SOURCES "JpegDecompressor.cpp" "JpegDecompressor.h" "JpegMarkers.h" + "JpegXlDecompressor.cpp" + "JpegXlDecompressor.h" "KodakDecompressor.cpp" "KodakDecompressor.h" "LJpegDecoder.cpp" @@ -82,4 +84,8 @@ if(WITH_JPEG AND TARGET JPEG::JPEG) target_link_libraries(rawspeed_decompressors PUBLIC JPEG::JPEG) endif() +if(WITH_JPEGXL AND TARGET PkgConfig::libjxl) + target_link_libraries(rawspeed_decompressors PUBLIC PkgConfig::libjxl) +endif() + target_link_libraries(rawspeed PRIVATE rawspeed_decompressors) diff --git a/src/librawspeed/decompressors/JpegXlDecompressor.cpp b/src/librawspeed/decompressors/JpegXlDecompressor.cpp new file mode 100644 index 000000000..7b7b47026 --- /dev/null +++ b/src/librawspeed/decompressors/JpegXlDecompressor.cpp @@ -0,0 +1,140 @@ +/* + RawSpeed - RAW file decoder. + + Copyright (C) 2026 darktable developers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "rawspeedconfig.h" // IWYU pragma: keep + +#ifdef HAVE_JPEGXL + +#include "adt/Array2DRef.h" +#include "adt/Point.h" +#include "decoders/RawDecoderException.h" +#include "decompressors/JpegXlDecompressor.h" +#include +#include +#include +#include +#include + +using std::min; + +namespace rawspeed { + +namespace { +// RAII wrapper so JxlDecoderDestroy runs on every exit path (incl. throws). +struct JxlDecoderGuard final { + JxlDecoder* dec; + explicit JxlDecoderGuard(JxlDecoder* d) : dec(d) {} + JxlDecoderGuard(const JxlDecoderGuard&) = delete; + JxlDecoderGuard(JxlDecoderGuard&&) = delete; + JxlDecoderGuard& operator=(const JxlDecoderGuard&) = delete; + JxlDecoderGuard& operator=(JxlDecoderGuard&&) = delete; + ~JxlDecoderGuard() { JxlDecoderDestroy(dec); } +}; +} // namespace + +void JpegXlDecompressor::decode(uint32_t offX, uint32_t offY) { + JxlDecoder* dec = JxlDecoderCreate(nullptr); + if (dec == nullptr) + ThrowRDE("JXL: JxlDecoderCreate failed"); + JxlDecoderGuard guard(dec); + + if (JXL_DEC_SUCCESS != + JxlDecoderSubscribeEvents(dec, JXL_DEC_BASIC_INFO | JXL_DEC_FULL_IMAGE)) + ThrowRDE("JXL: JxlDecoderSubscribeEvents failed"); + + // rawspeed/darktable handle orientation themselves; do not auto-rotate. + if (JXL_DEC_SUCCESS != JxlDecoderSetKeepOrientation(dec, JXL_TRUE)) + ThrowRDE("JXL: JxlDecoderSetKeepOrientation failed"); + + if (JXL_DEC_SUCCESS != + JxlDecoderSetInput(dec, input.begin(), input.getSize())) + ThrowRDE("JXL: JxlDecoderSetInput failed"); + JxlDecoderCloseInput(dec); + + const uint32_t cpp = mRaw->getCpp(); + const JxlPixelFormat fmt = {/*num_channels=*/cpp, + /*data_type=*/JXL_TYPE_UINT16, + /*endianness=*/JXL_LITTLE_ENDIAN, + /*align=*/0}; + + JxlBasicInfo info = {}; + uint32_t jxl_w = 0; + uint32_t jxl_h = 0; + std::vector pixels; + + for (;;) { + const JxlDecoderStatus status = JxlDecoderProcessInput(dec); + if (status == JXL_DEC_ERROR) + ThrowRDE("JXL: decoding error"); + if (status == JXL_DEC_NEED_MORE_INPUT) + ThrowRDE("JXL: needs more input (truncated tile?)"); + if (status == JXL_DEC_BASIC_INFO) { + if (JXL_DEC_SUCCESS != JxlDecoderGetBasicInfo(dec, &info)) + ThrowRDE("JXL: JxlDecoderGetBasicInfo failed"); + jxl_w = info.xsize; + jxl_h = info.ysize; + if (info.num_color_channels != cpp) + ThrowRDE("JXL: color channel count %u does not match cpp %u", + info.num_color_channels, cpp); + continue; + } + if (status == JXL_DEC_NEED_IMAGE_OUT_BUFFER) { + size_t buf_size = 0; + if (JXL_DEC_SUCCESS != JxlDecoderImageOutBufferSize(dec, &fmt, &buf_size)) + ThrowRDE("JXL: JxlDecoderImageOutBufferSize failed"); + pixels.resize(buf_size / sizeof(uint16_t)); + if (JXL_DEC_SUCCESS != + JxlDecoderSetImageOutBuffer(dec, &fmt, pixels.data(), buf_size)) + ThrowRDE("JXL: JxlDecoderSetImageOutBuffer failed"); + continue; + } + if (status == JXL_DEC_FULL_IMAGE) + continue; // image now in `pixels` + if (status == JXL_DEC_SUCCESS) + break; + ThrowRDE("JXL: unexpected decoder status %d", static_cast(status)); + } + + if (pixels.empty() || jxl_w == 0 || jxl_h == 0) + ThrowRDE("JXL: no pixel data decoded"); + + // Copy the decoded interleaved uint16 tile into the raw buffer at + // (offX,offY). + const uint32_t copy_w = min(static_cast(mRaw->dim.x) - offX, jxl_w); + const uint32_t copy_h = min(static_cast(mRaw->dim.y) - offY, jxl_h); + + const Array2DRef out(mRaw->getU16DataAsUncroppedArray2DRef()); + for (uint32_t row = 0; row < copy_h; ++row) { + for (uint32_t col = 0; col < cpp * copy_w; ++col) { + out(static_cast(row + offY), static_cast(cpp * offX + col)) = + pixels[(static_cast(row) * jxl_w * cpp) + col]; + } + } +} + +} // namespace rawspeed + +#else + +#pragma message \ + "JPEG XL is not present! DNG JPEG XL (DNG 1.7) compression will not be " \ + "supported!" + +#endif diff --git a/src/librawspeed/decompressors/JpegXlDecompressor.h b/src/librawspeed/decompressors/JpegXlDecompressor.h new file mode 100644 index 000000000..7cbcef083 --- /dev/null +++ b/src/librawspeed/decompressors/JpegXlDecompressor.h @@ -0,0 +1,56 @@ +/* + RawSpeed - RAW file decoder. + + Copyright (C) 2026 darktable developers + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include "rawspeedconfig.h" + +#ifdef HAVE_JPEGXL + +#include "common/RawImage.h" +#include "decompressors/AbstractDecompressor.h" +#include "io/Buffer.h" +#include +#include + +namespace rawspeed { + +// Decodes a single DNG tile whose data is a self-contained JPEG XL codestream +// (TIFF Compression tag 52546, as used by Apple ProRAW on iPhone 16 / DNG 1.7). +class JpegXlDecompressor final : public AbstractDecompressor { + Buffer input; + RawImage mRaw; + +public: + JpegXlDecompressor(Buffer bs, RawImage img) + : input(bs), mRaw(std::move(img)) {} + + void decode(uint32_t offsetX, uint32_t offsetY); +}; + +} // namespace rawspeed + +#else + +#pragma message \ + "JPEG XL is not present! DNG JPEG XL (DNG 1.7) compression will not be " \ + "supported!" + +#endif From 8277b5542883a3367f0f0dbb06b12c344cad2f94 Mon Sep 17 00:00:00 2001 From: Mayk Thewessen Date: Tue, 30 Jun 2026 17:38:48 +0200 Subject: [PATCH 2/5] DngDecoder: honor Row/ColumnInterleaveFactor (DNG 1.7) DNG 1.7 CFA files may store the Bayer mosaic as RowInterleaveFactor x ColumnInterleaveFactor stacked color-plane "fields" (each a smooth half-res subimage that compresses far better under lossy JPEG XL), signalled by tags 0xC71F / 0xCD43. rawspeed assembled the tiles but never wove the fields back together, emitting a scrambled mosaic (four quadrant planes stacked instead of RGGB). Add a whole-frame de-interleave post-pass in decodeData(), run after slices.decompress() and before handleMetadata() applies ActiveArea / DefaultCropOrigin. It operates on the uncropped assembled buffer, one whole pixel (getBpp() bytes) at a time, so it is type-generic over UINT16 and F32. A single JXL tile can straddle a field boundary, so this cannot be done per-tile. The stored->final index math (dngDeinterleaveFieldMap) follows the Adobe DNG 1.7.1.0 spec and the dng_sdk dng_read_image.cpp reference: field f's rows scatter to final rows f, f+R, f+2R, ...; non-divisible sizes let earlier fields absorb the remainder. Factored into a small pure header (DngDeinterleave.h) so the index mapping is unit-testable. R==1 && C==1 fast-paths to a no-op. Also adds the missing COLUMNINTERLEAVEFACTOR (0xCD43) tag. Co-Authored-By: Claude Opus 4.8 --- src/librawspeed/decoders/DngDecoder.cpp | 65 ++++++++++++++++++ src/librawspeed/decoders/DngDecoder.h | 1 + src/librawspeed/decoders/DngDeinterleave.h | 77 ++++++++++++++++++++++ src/librawspeed/tiff/TiffTag.h | 1 + 4 files changed, 144 insertions(+) create mode 100644 src/librawspeed/decoders/DngDeinterleave.h diff --git a/src/librawspeed/decoders/DngDecoder.cpp b/src/librawspeed/decoders/DngDecoder.cpp index a59cbb10e..0af7ccda6 100644 --- a/src/librawspeed/decoders/DngDecoder.cpp +++ b/src/librawspeed/decoders/DngDecoder.cpp @@ -31,6 +31,7 @@ #include "common/DngOpcodes.h" #include "common/RawImage.h" #include "decoders/AbstractTiffDecoder.h" +#include "decoders/DngDeinterleave.h" #include "decoders/RawDecoderException.h" #include "decompressors/AbstractDngDecompressor.h" #include "io/Buffer.h" @@ -45,7 +46,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -455,6 +458,68 @@ void DngDecoder::decodeData(const TiffIFD* raw, uint32_t sample_format) const { mRaw->createData(); slices.decompress(); + + // DNG 1.7 may store the (already assembled) frame with its color-plane + // fields stacked, signalled by Row/ColumnInterleaveFactor. This is a + // whole-frame post-pass over the fully assembled buffer, applied BEFORE + // ActiveArea/DefaultCropOrigin (handleMetadata) crop the image. A single + // (e.g. JXL) tile can straddle a field boundary, so this can NOT be done + // per-tile. + deinterleaveFields(raw); +} + +void DngDecoder::deinterleaveFields(const TiffIFD* raw) const { + uint32_t rowFactor = 1; + if (raw->hasEntry(TiffTag::ROWINTERLEAVEFACTOR)) + rowFactor = raw->getEntry(TiffTag::ROWINTERLEAVEFACTOR)->getU32(); + + uint32_t colFactor = 1; + if (raw->hasEntry(TiffTag::COLUMNINTERLEAVEFACTOR)) + colFactor = raw->getEntry(TiffTag::COLUMNINTERLEAVEFACTOR)->getU32(); + + if (rowFactor == 0 || colFactor == 0) + ThrowRDE("Invalid interleave factor (%u, %u)", rowFactor, colFactor); + + // Fast path: nothing to do. + if (rowFactor == 1 && colFactor == 1) + return; + + const int storedH = mRaw->dim.y; + const int storedW = mRaw->dim.x; + + if (rowFactor > static_cast(storedH) || + colFactor > static_cast(storedW)) + ThrowRDE("Interleave factor (%u, %u) larger than image dimensions (%i, %i)", + rowFactor, colFactor, storedW, storedH); + + // stored-row -> final-row and stored-col -> final-col lookup tables. + const std::vector rowMap = + dngDeinterleaveFieldMap(storedH, implicit_cast(rowFactor)); + const std::vector colMap = + dngDeinterleaveFieldMap(storedW, implicit_cast(colFactor)); + + // Operate on raw bytes so the same scatter works for both UINT16 and F32 + // buffers. `bpp` is the size of one whole pixel (all channels) in bytes; the + // byte Array2DRef indexes columns in bytes. + const int bpp = implicit_cast(mRaw->getBpp()); + const Array2DRef img = mRaw->getByteDataAsUncroppedArray2DRef(); + + // A temporary copy of the assembled (stored-order) frame to scatter from. + std::vector tmp(static_cast(storedH) * storedW * bpp); + const Array2DRef src(tmp.data(), storedW * bpp, storedH); + for (int sy = 0; sy < storedH; ++sy) + std::memcpy(&src(sy, 0), &img(sy, 0), + static_cast(storedW) * bpp); + + // Scatter src(sy,sx) -> img(fy,fx), one whole pixel (bpp bytes) at a time. + for (int sy = 0; sy < storedH; ++sy) { + const int fy = rowMap[static_cast(sy)]; + for (int sx = 0; sx < storedW; ++sx) { + const int fx = colMap[static_cast(sx)]; + std::memcpy(&img(fy, bpp * fx), &src(sy, bpp * sx), + static_cast(bpp)); + } + } } RawImage DngDecoder::decodeRawInternal() { diff --git a/src/librawspeed/decoders/DngDecoder.h b/src/librawspeed/decoders/DngDecoder.h index 4558be552..c3fea09c9 100644 --- a/src/librawspeed/decoders/DngDecoder.h +++ b/src/librawspeed/decoders/DngDecoder.h @@ -53,6 +53,7 @@ class DngDecoder final : public AbstractTiffDecoder { void parseWhiteBalance() const; DngTilingDescription getTilingDescription(const TiffIFD* raw) const; void decodeData(const TiffIFD* raw, uint32_t sample_format) const; + void deinterleaveFields(const TiffIFD* raw) const; void handleMetadata(const TiffIFD* raw); bool decodeMaskedAreas(const TiffIFD* raw) const; bool decodeBlackLevels(const TiffIFD* raw) const; diff --git a/src/librawspeed/decoders/DngDeinterleave.h b/src/librawspeed/decoders/DngDeinterleave.h new file mode 100644 index 000000000..ab6cc8bee --- /dev/null +++ b/src/librawspeed/decoders/DngDeinterleave.h @@ -0,0 +1,77 @@ +/* + RawSpeed - RAW file decoder. + + Copyright (C) 2026 Mayk Thewessen + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#pragma once + +#include +#include +#include + +namespace rawspeed { + +// Pixel de-interleave for DNG 1.7 RowInterleaveFactor (0xC71F) / +// ColumnInterleaveFactor (0xCD43). +// +// A CFA DNG may store its mosaic as `R` x `C` stacked "fields" (color-plane +// subimages), each of which compresses far better under lossy JPEG XL than the +// raw mosaic. De-interleaving REORDERS pixels only; it never resizes, so the +// final dimensions equal the stored (decoded) dimensions. +// +// Field f's rows scatter to final rows f, f+R, f+2R, ... i.e. the forward map +// stored->final is `fy = within_field_row * R + field_row_index`, and +// symmetrically for columns with C. Non-divisible sizes are handled exactly +// like the dng_sdk reference (dng_read_image.cpp): earlier fields absorb the +// remainder. +// +// This matches the Adobe DNG 1.7.1.0 specification and the dng_sdk reference +// implementation. + +// Build the stored-row -> final-row (or stored-col -> final-col) lookup table. +// +// `total` is the stored extent (height for rows, width for columns) and +// `factor` is the corresponding interleave factor (R or C, >= 1). +// +// Returns a vector `map` of size `total` such that the pixel stored at index +// `s` belongs at final index `map[s]`. +[[nodiscard]] inline std::vector +dngDeinterleaveFieldMap(int total, int factor) { + assert(total >= 0); + assert(factor >= 1); + + std::vector map(static_cast(total)); + + int acc = 0; + for (int f = 0; f < factor; ++f) { + // Number of rows/cols in this field. Earlier fields absorb the remainder, + // matching dng_sdk: rows[f] = (total - f + factor - 1) / factor. + const int fieldExtent = (total - f + factor - 1) / factor; + for (int within = 0; within < fieldExtent; ++within) { + const int storedIdx = acc + within; + assert(storedIdx < total); + map[static_cast(storedIdx)] = within * factor + f; + } + acc += fieldExtent; + } + assert(acc == total); + + return map; +} + +} // namespace rawspeed diff --git a/src/librawspeed/tiff/TiffTag.h b/src/librawspeed/tiff/TiffTag.h index b4d20750a..8a8163cc8 100644 --- a/src/librawspeed/tiff/TiffTag.h +++ b/src/librawspeed/tiff/TiffTag.h @@ -327,6 +327,7 @@ enum class TiffTag : uint16_t { ORIGINALRAWFILEDIGEST = 0xC71D, SUBTILEBLOCKSIZE = 0xC71E, ROWINTERLEAVEFACTOR = 0xC71F, + COLUMNINTERLEAVEFACTOR = 0xCD43, PROFILELOOKTABLEDIMS = 0xC725, PROFILELOOKTABLEDATA = 0xC726, OPCODELIST1 = 0xC740, From c1c5cf201b9b05f1c36f11b7b6bbced281efbdb4 Mon Sep 17 00:00:00 2001 From: Mayk Thewessen Date: Tue, 30 Jun 2026 17:38:55 +0200 Subject: [PATCH 3/5] test: cover DNG Row/ColumnInterleaveFactor de-interleave math Unit-test dngDeinterleaveFieldMap, including the verified 4x4 R=C=2 worked example (quadrant-constant stored buffer must de-interleave to the RGGB-phase mosaic), the per-pixel stored->final map, the factor-1 identity fast path, and the non-divisible remainder rule from dng_sdk (total=5,factor=2 and total=7,factor=3). Co-Authored-By: Claude Opus 4.8 --- test/librawspeed/CMakeLists.txt | 1 + test/librawspeed/decoders/CMakeLists.txt | 7 + .../decoders/DngDeinterleaveTest.cpp | 136 ++++++++++++++++++ 3 files changed, 144 insertions(+) create mode 100644 test/librawspeed/decoders/CMakeLists.txt create mode 100644 test/librawspeed/decoders/DngDeinterleaveTest.cpp diff --git a/test/librawspeed/CMakeLists.txt b/test/librawspeed/CMakeLists.txt index fe5234773..47161eb3d 100644 --- a/test/librawspeed/CMakeLists.txt +++ b/test/librawspeed/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory(adt) add_subdirectory(bitstreams) add_subdirectory(codes) add_subdirectory(common) +add_subdirectory(decoders) add_subdirectory(io) add_subdirectory(metadata) add_subdirectory(test) diff --git a/test/librawspeed/decoders/CMakeLists.txt b/test/librawspeed/decoders/CMakeLists.txt new file mode 100644 index 000000000..2cb182180 --- /dev/null +++ b/test/librawspeed/decoders/CMakeLists.txt @@ -0,0 +1,7 @@ +FILE(GLOB RAWSPEED_TEST_SOURCES + "DngDeinterleaveTest.cpp" +) + +foreach(SRC ${RAWSPEED_TEST_SOURCES}) + add_rs_test("${SRC}") +endforeach() diff --git a/test/librawspeed/decoders/DngDeinterleaveTest.cpp b/test/librawspeed/decoders/DngDeinterleaveTest.cpp new file mode 100644 index 000000000..c5d0dd403 --- /dev/null +++ b/test/librawspeed/decoders/DngDeinterleaveTest.cpp @@ -0,0 +1,136 @@ +/* + RawSpeed - RAW file decoder. + + Copyright (C) 2026 Mayk Thewessen + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#include "decoders/DngDeinterleave.h" +#include +#include + +using rawspeed::dngDeinterleaveFieldMap; + +namespace rawspeed_test { + +namespace { + +// Apply the row/col field maps to a stored (row-major, single-channel) image, +// mirroring DngDecoder::deinterleaveFields for verification of the index math. +std::vector applyDeinterleave(const std::vector& stored, int storedH, + int storedW, int rowFactor, int colFactor) { + const std::vector rowMap = + dngDeinterleaveFieldMap(storedH, rowFactor); + const std::vector colMap = + dngDeinterleaveFieldMap(storedW, colFactor); + + std::vector out(stored.size()); + for (int sy = 0; sy < storedH; ++sy) { + const int fy = rowMap[sy]; + for (int sx = 0; sx < storedW; ++sx) { + const int fx = colMap[sx]; + out[fy * storedW + fx] = stored[sy * storedW + sx]; + } + } + return out; +} + +} // namespace + +// Identity: factor 1 must leave the map untouched. +TEST(DngDeinterleaveTest, FactorOneIsIdentity) { + const std::vector map = dngDeinterleaveFieldMap(7, 1); + ASSERT_EQ(map.size(), 7U); + for (int i = 0; i < 7; ++i) + EXPECT_EQ(map[i], i); +} + +// The exact stored->final row map for R=2 on 4 rows. +// Field 0 occupies stored rows {0,1} -> final {0,2}. +// Field 1 occupies stored rows {2,3} -> final {1,3}. +TEST(DngDeinterleaveTest, RowMap2x4) { + const std::vector map = dngDeinterleaveFieldMap(4, 2); + const std::vector expected = {0, 2, 1, 3}; + EXPECT_EQ(map, expected); +} + +// Non-divisible case: earlier fields absorb the remainder (dng_sdk rule). +// total=5, factor=2: field0 has rows {0,1,2}, field1 has rows {3,4}. +// stored 0 -> 0, 1 -> 2, 2 -> 4 (field 0, *2+0) +// stored 3 -> 1, 4 -> 3 (field 1, *2+1) +TEST(DngDeinterleaveTest, NonDivisibleRemainder) { + const std::vector map = dngDeinterleaveFieldMap(5, 2); + const std::vector expected = {0, 2, 4, 1, 3}; + EXPECT_EQ(map, expected); +} + +// total=7, factor=3: field0 {0,1,2}(3), field1 {3,4}(2), field2 {5,6}(2). +// field0: 0->0, 1->3, 2->6 +// field1: 3->1, 4->4 +// field2: 5->2, 6->5 +TEST(DngDeinterleaveTest, NonDivisibleThreeFields) { + const std::vector map = dngDeinterleaveFieldMap(7, 3); + const std::vector expected = {0, 3, 6, 1, 4, 2, 5}; + EXPECT_EQ(map, expected); +} + +// The full VERIFIED 4x4, R=C=2 worked example. +// A stored buffer whose 4 quadrants are constant (TL=10 "R", TR=20 "G", +// BL=30 "G", BR=40 "B") must de-interleave to the RGGB-phase mosaic: +// 10 20 10 20 / 30 40 30 40 / 10 20 10 20 / 30 40 30 40 +TEST(DngDeinterleaveTest, WorkedExample4x4Quadrants) { + // clang-format off + const std::vector stored = { + 10, 10, 20, 20, + 10, 10, 20, 20, + 30, 30, 40, 40, + 30, 30, 40, 40, + }; + const std::vector expected = { + 10, 20, 10, 20, + 30, 40, 30, 40, + 10, 20, 10, 20, + 30, 40, 30, 40, + }; + // clang-format on + + const std::vector out = + applyDeinterleave(stored, /*storedH=*/4, /*storedW=*/4, + /*rowFactor=*/2, /*colFactor=*/2); + EXPECT_EQ(out, expected); +} + +// Per-pixel stored->final mapping from the spec, every pixel of the 4x4. +TEST(DngDeinterleaveTest, WorkedExample4x4PerPixelMap) { + const std::vector rowMap = dngDeinterleaveFieldMap(4, 2); + const std::vector colMap = dngDeinterleaveFieldMap(4, 2); + + // (stored_y, stored_x) -> (final_y, final_x) + const int expected[4][4][2] = { + {{0, 0}, {0, 2}, {0, 1}, {0, 3}}, + {{2, 0}, {2, 2}, {2, 1}, {2, 3}}, + {{1, 0}, {1, 2}, {1, 1}, {1, 3}}, + {{3, 0}, {3, 2}, {3, 1}, {3, 3}}, + }; + for (int sy = 0; sy < 4; ++sy) { + for (int sx = 0; sx < 4; ++sx) { + EXPECT_EQ(rowMap[sy], expected[sy][sx][0]) << "sy=" << sy << " sx=" << sx; + EXPECT_EQ(colMap[sx], expected[sy][sx][1]) << "sy=" << sy << " sx=" << sx; + } + } +} + +} // namespace rawspeed_test From 94bb2afbd8cb88127ba25d0b9a945a7bea3110a1 Mon Sep 17 00:00:00 2001 From: Mayk Thewessen Date: Tue, 30 Jun 2026 23:28:42 +0200 Subject: [PATCH 4/5] JpegXlDecompressor: add F32 output path for float DNGs The decompressor unconditionally wrote the decoded tile through mRaw->getU16DataAsUncroppedArray2DRef() and requested JXL_TYPE_UINT16, which asserts (dataType == RawImageType::UINT16) and fails to decode for float (F32) JPEG XL DNGs such as Adobe's 02_jxl_linear_raw_float.dng. Pick the JxlPixelFormat sample type from mRaw->getDataType(): request JXL_TYPE_FLOAT and write via getF32DataAsUncroppedArray2DRef() for F32 images, keeping the JXL_TYPE_UINT16 path for integer images. The decoded tile now lands in a type-agnostic byte buffer (libjxl reports the size in bytes) and a templated copyTile() shares the interleaved copy loop. Verified both 01_jxl_linear_raw_integer.dng and 02_jxl_linear_raw_float.dng decode without crashing via rstest. Co-Authored-By: Claude Opus 4.8 --- .../decompressors/JpegXlDecompressor.cpp | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/src/librawspeed/decompressors/JpegXlDecompressor.cpp b/src/librawspeed/decompressors/JpegXlDecompressor.cpp index 7b7b47026..afdc2ecf2 100644 --- a/src/librawspeed/decompressors/JpegXlDecompressor.cpp +++ b/src/librawspeed/decompressors/JpegXlDecompressor.cpp @@ -24,6 +24,7 @@ #include "adt/Array2DRef.h" #include "adt/Point.h" +#include "common/RawImage.h" #include "decoders/RawDecoderException.h" #include "decompressors/JpegXlDecompressor.h" #include @@ -47,6 +48,21 @@ struct JxlDecoderGuard final { JxlDecoderGuard& operator=(JxlDecoderGuard&&) = delete; ~JxlDecoderGuard() { JxlDecoderDestroy(dec); } }; + +// Copy the decoded, interleaved JXL tile into the raw buffer at (offX,offY). +// Templated on the sample type so the integer (uint16_t) and float (float) +// output paths share one implementation. +template +void copyTile(const Array2DRef out, const T* pixels, uint32_t jxl_w, + uint32_t copy_w, uint32_t copy_h, uint32_t cpp, uint32_t offX, + uint32_t offY) { + for (uint32_t row = 0; row < copy_h; ++row) { + for (uint32_t col = 0; col < cpp * copy_w; ++col) { + out(static_cast(row + offY), static_cast(cpp * offX + col)) = + pixels[(static_cast(row) * jxl_w * cpp) + col]; + } + } +} } // namespace void JpegXlDecompressor::decode(uint32_t offX, uint32_t offY) { @@ -69,15 +85,21 @@ void JpegXlDecompressor::decode(uint32_t offX, uint32_t offY) { JxlDecoderCloseInput(dec); const uint32_t cpp = mRaw->getCpp(); + // Float DNGs (e.g. linear raw float) store an F32 buffer; integer DNGs store + // uint16. Decode JXL directly into whichever sample type mRaw expects, so the + // tile lands in the matching typed view below. + const bool isFloat = mRaw->getDataType() == RawImageType::F32; const JxlPixelFormat fmt = {/*num_channels=*/cpp, - /*data_type=*/JXL_TYPE_UINT16, + /*data_type=*/isFloat ? JXL_TYPE_FLOAT + : JXL_TYPE_UINT16, /*endianness=*/JXL_LITTLE_ENDIAN, /*align=*/0}; JxlBasicInfo info = {}; uint32_t jxl_w = 0; uint32_t jxl_h = 0; - std::vector pixels; + // Type-agnostic byte buffer; libjxl reports the required size in bytes. + std::vector pixels; for (;;) { const JxlDecoderStatus status = JxlDecoderProcessInput(dec); @@ -99,7 +121,7 @@ void JpegXlDecompressor::decode(uint32_t offX, uint32_t offY) { size_t buf_size = 0; if (JXL_DEC_SUCCESS != JxlDecoderImageOutBufferSize(dec, &fmt, &buf_size)) ThrowRDE("JXL: JxlDecoderImageOutBufferSize failed"); - pixels.resize(buf_size / sizeof(uint16_t)); + pixels.resize(buf_size); if (JXL_DEC_SUCCESS != JxlDecoderSetImageOutBuffer(dec, &fmt, pixels.data(), buf_size)) ThrowRDE("JXL: JxlDecoderSetImageOutBuffer failed"); @@ -115,17 +137,17 @@ void JpegXlDecompressor::decode(uint32_t offX, uint32_t offY) { if (pixels.empty() || jxl_w == 0 || jxl_h == 0) ThrowRDE("JXL: no pixel data decoded"); - // Copy the decoded interleaved uint16 tile into the raw buffer at - // (offX,offY). const uint32_t copy_w = min(static_cast(mRaw->dim.x) - offX, jxl_w); const uint32_t copy_h = min(static_cast(mRaw->dim.y) - offY, jxl_h); - const Array2DRef out(mRaw->getU16DataAsUncroppedArray2DRef()); - for (uint32_t row = 0; row < copy_h; ++row) { - for (uint32_t col = 0; col < cpp * copy_w; ++col) { - out(static_cast(row + offY), static_cast(cpp * offX + col)) = - pixels[(static_cast(row) * jxl_w * cpp) + col]; - } + if (isFloat) { + copyTile(mRaw->getF32DataAsUncroppedArray2DRef(), + reinterpret_cast(pixels.data()), jxl_w, copy_w, + copy_h, cpp, offX, offY); + } else { + copyTile(mRaw->getU16DataAsUncroppedArray2DRef(), + reinterpret_cast(pixels.data()), jxl_w, + copy_w, copy_h, cpp, offX, offY); } } From fc433de40c6abce065b7cd8593757f5e72cfe0ee Mon Sep 17 00:00:00 2001 From: Mayk Thewessen Date: Wed, 1 Jul 2026 09:20:28 +0200 Subject: [PATCH 5/5] Applying clang-format --- src/librawspeed/decoders/DngDecoder.cpp | 3 +-- src/librawspeed/decoders/DngDeinterleave.h | 4 ++-- .../decompressors/JpegXlDecompressor.cpp | 14 +++++++------- test/librawspeed/decoders/DngDeinterleaveTest.cpp | 6 ++---- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/librawspeed/decoders/DngDecoder.cpp b/src/librawspeed/decoders/DngDecoder.cpp index 0af7ccda6..9251184b4 100644 --- a/src/librawspeed/decoders/DngDecoder.cpp +++ b/src/librawspeed/decoders/DngDecoder.cpp @@ -508,8 +508,7 @@ void DngDecoder::deinterleaveFields(const TiffIFD* raw) const { std::vector tmp(static_cast(storedH) * storedW * bpp); const Array2DRef src(tmp.data(), storedW * bpp, storedH); for (int sy = 0; sy < storedH; ++sy) - std::memcpy(&src(sy, 0), &img(sy, 0), - static_cast(storedW) * bpp); + std::memcpy(&src(sy, 0), &img(sy, 0), static_cast(storedW) * bpp); // Scatter src(sy,sx) -> img(fy,fx), one whole pixel (bpp bytes) at a time. for (int sy = 0; sy < storedH; ++sy) { diff --git a/src/librawspeed/decoders/DngDeinterleave.h b/src/librawspeed/decoders/DngDeinterleave.h index ab6cc8bee..27021c087 100644 --- a/src/librawspeed/decoders/DngDeinterleave.h +++ b/src/librawspeed/decoders/DngDeinterleave.h @@ -50,8 +50,8 @@ namespace rawspeed { // // Returns a vector `map` of size `total` such that the pixel stored at index // `s` belongs at final index `map[s]`. -[[nodiscard]] inline std::vector -dngDeinterleaveFieldMap(int total, int factor) { +[[nodiscard]] inline std::vector dngDeinterleaveFieldMap(int total, + int factor) { assert(total >= 0); assert(factor >= 1); diff --git a/src/librawspeed/decompressors/JpegXlDecompressor.cpp b/src/librawspeed/decompressors/JpegXlDecompressor.cpp index afdc2ecf2..5ba983141 100644 --- a/src/librawspeed/decompressors/JpegXlDecompressor.cpp +++ b/src/librawspeed/decompressors/JpegXlDecompressor.cpp @@ -89,11 +89,11 @@ void JpegXlDecompressor::decode(uint32_t offX, uint32_t offY) { // uint16. Decode JXL directly into whichever sample type mRaw expects, so the // tile lands in the matching typed view below. const bool isFloat = mRaw->getDataType() == RawImageType::F32; - const JxlPixelFormat fmt = {/*num_channels=*/cpp, - /*data_type=*/isFloat ? JXL_TYPE_FLOAT - : JXL_TYPE_UINT16, - /*endianness=*/JXL_LITTLE_ENDIAN, - /*align=*/0}; + const JxlPixelFormat fmt = { + /*num_channels=*/cpp, + /*data_type=*/isFloat ? JXL_TYPE_FLOAT : JXL_TYPE_UINT16, + /*endianness=*/JXL_LITTLE_ENDIAN, + /*align=*/0}; JxlBasicInfo info = {}; uint32_t jxl_w = 0; @@ -142,8 +142,8 @@ void JpegXlDecompressor::decode(uint32_t offX, uint32_t offY) { if (isFloat) { copyTile(mRaw->getF32DataAsUncroppedArray2DRef(), - reinterpret_cast(pixels.data()), jxl_w, copy_w, - copy_h, cpp, offX, offY); + reinterpret_cast(pixels.data()), jxl_w, + copy_w, copy_h, cpp, offX, offY); } else { copyTile(mRaw->getU16DataAsUncroppedArray2DRef(), reinterpret_cast(pixels.data()), jxl_w, diff --git a/test/librawspeed/decoders/DngDeinterleaveTest.cpp b/test/librawspeed/decoders/DngDeinterleaveTest.cpp index c5d0dd403..1c8b5c33f 100644 --- a/test/librawspeed/decoders/DngDeinterleaveTest.cpp +++ b/test/librawspeed/decoders/DngDeinterleaveTest.cpp @@ -32,10 +32,8 @@ namespace { // mirroring DngDecoder::deinterleaveFields for verification of the index math. std::vector applyDeinterleave(const std::vector& stored, int storedH, int storedW, int rowFactor, int colFactor) { - const std::vector rowMap = - dngDeinterleaveFieldMap(storedH, rowFactor); - const std::vector colMap = - dngDeinterleaveFieldMap(storedW, colFactor); + const std::vector rowMap = dngDeinterleaveFieldMap(storedH, rowFactor); + const std::vector colMap = dngDeinterleaveFieldMap(storedW, colFactor); std::vector out(stored.size()); for (int sy = 0; sy < storedH; ++sy) {