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..9251184b4 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 @@ -120,7 +123,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 +146,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; @@ -443,6 +458,67 @@ 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..27021c087 --- /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/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..5ba983141 --- /dev/null +++ b/src/librawspeed/decompressors/JpegXlDecompressor.cpp @@ -0,0 +1,162 @@ +/* + 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 "common/RawImage.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); } +}; + +// 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) { + 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(); + // 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=*/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; + // Type-agnostic byte buffer; libjxl reports the required size in bytes. + 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); + 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"); + + 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); + + 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); + } +} + +} // 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 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..1c8b5c33f --- /dev/null +++ b/test/librawspeed/decoders/DngDeinterleaveTest.cpp @@ -0,0 +1,134 @@ +/* + 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