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/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); } } 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, 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