From cfa4429965054d698c003eca721dad9af05b9b2d Mon Sep 17 00:00:00 2001 From: Philipp Lutz Date: Sat, 18 Apr 2026 11:43:10 +0200 Subject: [PATCH 1/8] =?UTF-8?q?LJpeg:=20support=20predictor=20modes=202?= =?UTF-8?q?=E2=80=937?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: rawspeed's LJpeg decoder previously supported only predictor mode 1 (left neighbor). ITU-T T.81 defines seven predictor modes, and several camera manufacturers — notably DJI (Mavic 3S, Mavic 3 Pro, etc.) and Blackmagic — use mode 6 in their DNG files. This change adds support for all seven modes and handles the associated tile geometry those cameras use. Predictor modes 2–7 * Added a computePrediction(mode, Ra, Rb, Rc) helper that computes the seven ITU-T T.81 predictions. Arithmetic is done in int32_t to avoid overflow in modes 4–6, where Ra + Rb - Rc can transiently exceed the 16-bit range; the final result wraps via uint16_t cast per the standard. * decodeRowN() is extended to accept predMode and prevStripe. For mode 1, the existing fast path (no memory access to the previous row) is preserved. For modes 2–7, the three 2D neighbors (Ra = left, Rb = above, Rc = above-left) are looked up from prevStripe; at the start of a row the above-left neighbor is not available, so Rc = Rb per the JPEG specification. * decodeN() now tracks isFirstRow per restart interval. The first row of each interval always uses predictor mode 1 (per T.81: the standard mandates horizontal prediction for the first row). For subsequent rows, prevStripe is a CroppedArray2DRef into the already-decoded portion of the output image. No extra allocation is needed. Inverted tile reshape (LJpegDecoder) Inverted tile reshape DJI DNGs present tiles in a non-standard geometry: the JPEG SOF3 frame is wider than the declared tile (e.g. 8000×1500 for a 4000×3000 tile). Each JPEG row encodes two tile rows concatenated side-by-side — a widthPack = jpegFrameDim.x / tileW packing. This is the inverse of the Adobe-style reshape rawspeed already handles (MCU > 1×1). Design trade-offs: Separate decode-then-deinterleave vs. in-place: Reusing the existing LJpegDecompressor in-place would require threading the tile geometry inversion through all MCU addressing logic. Instead, the inverted-reshape path decodes into a temporary RawImage at the JPEG frame dimensions, then copies/deinterleaves rows into the real output image. The extra allocation is bounded by a single tile's worth of data and avoids touching the hot-path decompressor. widthPack validation: The factor is computed from the ratio jpegFrameDim.x / maxRes.x and validated against widthPack * jpegFrameDim.y == maxRes.y before decoding begins. An upper bound of 4 is enforced as a sanity check. The inverted reshape is restricted to single-component LJpeg (N_COMP == 1), which is the only case seen in practice for this layout. Fuzz harness The LJpegDecompressor fuzz entry point is updated to source a predictorMode byte from the fuzzer input, covering the new code paths. --- .../decompressors/LJpegDecompressor.cpp | 3 +- .../decompressors/LJpegDecoder.cpp | 115 +++++++++++++++--- .../decompressors/LJpegDecompressor.cpp | 87 ++++++++++++- .../decompressors/LJpegDecompressor.h | 3 + 4 files changed, 189 insertions(+), 19 deletions(-) diff --git a/fuzz/librawspeed/decompressors/LJpegDecompressor.cpp b/fuzz/librawspeed/decompressors/LJpegDecompressor.cpp index aa7b7154c..83e406abb 100644 --- a/fuzz/librawspeed/decompressors/LJpegDecompressor.cpp +++ b/fuzz/librawspeed/decompressors/LJpegDecompressor.cpp @@ -88,10 +88,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* Data, size_t Size) { }); const int numLJpegRowsPerRestartInterval = bs.getI32(); + const int predictorMode = bs.getByte(); rawspeed::LJpegDecompressor d( mRaw, rawspeed::iRectangle2D(mRaw->dim.x, mRaw->dim.y), frame, rec, - numLJpegRowsPerRestartInterval, + numLJpegRowsPerRestartInterval, predictorMode, bs.getSubStream(/*offset=*/0).peekRemainingBuffer().getAsArray1DRef()); mRaw->createData(); (void)d.decode(); diff --git a/src/librawspeed/decompressors/LJpegDecoder.cpp b/src/librawspeed/decompressors/LJpegDecoder.cpp index a4efc03bf..b4fad06b9 100644 --- a/src/librawspeed/decompressors/LJpegDecoder.cpp +++ b/src/librawspeed/decompressors/LJpegDecoder.cpp @@ -32,8 +32,10 @@ #include #include #include +#include #include #include +#include #include using std::copy_n; @@ -104,7 +106,7 @@ void LJpegDecoder::decode(uint32_t offsetX, uint32_t offsetY, uint32_t width, Buffer::size_type LJpegDecoder::decodeScan() { invariant(frame.cps > 0); - if (predictorMode != 1) + if (predictorMode < 1 || predictorMode > 7) ThrowRDE("Unsupported predictor mode: %u", predictorMode); for (uint32_t i = 0; i < frame.cps; i++) @@ -123,9 +125,6 @@ Buffer::size_type LJpegDecoder::decodeScan() { return {*hts[i], initPred[i]}; }); - const iRectangle2D imgFrame = { - {static_cast(offX), static_cast(offY)}, - {static_cast(w), static_cast(h)}}; const auto jpegFrameDim = iPoint2D(frame.w, frame.h); if (implicit_cast(maxDim.x) * implicit_cast(mRaw->getCpp()) > @@ -137,31 +136,119 @@ Buffer::size_type LJpegDecoder::decodeScan() { if (maxRes.area() != N_COMP * jpegFrameDim.area()) ThrowRDE("LJpeg frame area does not match maximal tile area"); - if (maxRes.x % jpegFrameDim.x != 0 || maxRes.y % jpegFrameDim.y != 0) - ThrowRDE("Maximal output tile size is not a multiple of LJpeg frame size"); + // Detect whether the JPEG frame uses an inverted reshape (e.g. DJI/Blackmagic + // CinemaDNG): JPEG frame is wider than tile and shorter, with packed rows. + // Standard (Adobe): maxRes.x >= jpegFrameDim.x (tile is wider/equal) + // Inverted (DJI): jpegFrameDim.x > maxRes.x (JPEG frame is wider) + bool invertedReshape = (jpegFrameDim.x > maxRes.x); + + if (!invertedReshape) { + // Standard case: tile width is a multiple of JPEG frame width. + if (maxRes.x % jpegFrameDim.x != 0 || maxRes.y % jpegFrameDim.y != 0) + ThrowRDE( + "Maximal output tile size is not a multiple of LJpeg frame size"); + + auto MCUSize = + iPoint2D{maxRes.x / jpegFrameDim.x, maxRes.y / jpegFrameDim.y}; + if (MCUSize.area() != implicit_cast(N_COMP)) + ThrowRDE("Unexpected MCU size, does not match LJpeg component count"); + + const iRectangle2D imgFrame = { + {static_cast(offX), static_cast(offY)}, + {static_cast(w), static_cast(h)}}; + const LJpegDecompressor::Frame jpegFrame = {MCUSize, jpegFrameDim}; + + int numLJpegRowsPerRestartInterval; + if (numMCUsPerRestartInterval == 0) { + numLJpegRowsPerRestartInterval = jpegFrameDim.y; + } else { + const int numMCUsPerRow = jpegFrameDim.x; + if (numMCUsPerRestartInterval % numMCUsPerRow != 0) + ThrowRDE("Restart interval is not a multiple of frame row size"); + numLJpegRowsPerRestartInterval = + numMCUsPerRestartInterval / numMCUsPerRow; + } + + LJpegDecompressor d(mRaw, imgFrame, jpegFrame, rec, + numLJpegRowsPerRestartInterval, + implicit_cast(predictorMode), + input.peekRemainingBuffer().getAsArray1DRef()); + return d.decode(); + } + + // Inverted reshape case (DJI/Blackmagic CinemaDNG): + // JPEG frame is wider than tile, e.g. JPEG=8000x1500 1-comp, tile=4000x3000. + // Each JPEG row contains 'widthPack' tile rows concatenated. + if (N_COMP != 1) + ThrowRDE("Inverted reshape only supported for single-component LJpeg"); + + if (jpegFrameDim.x % maxRes.x != 0) + ThrowRDE("LJpeg frame width is not a multiple of tile width"); + if (maxRes.y % jpegFrameDim.y != 0) + ThrowRDE("Tile height is not a multiple of LJpeg frame height"); + + const int widthPack = jpegFrameDim.x / maxRes.x; + if (widthPack * jpegFrameDim.y != maxRes.y) + ThrowRDE("Inverted reshape dimensions mismatch"); - auto MCUSize = iPoint2D{maxRes.x / jpegFrameDim.x, maxRes.y / jpegFrameDim.y}; - if (MCUSize.area() != implicit_cast(N_COMP)) - ThrowRDE("Unexpected MCU size, does not match LJpeg component count"); + if (widthPack < 1 || widthPack > 4) + ThrowRDE("Unexpected row packing factor: %d", widthPack); + // Decode into a temporary buffer at JPEG frame dimensions. + // MCU is {1,1} since we have a single component. + const auto MCUSize = iPoint2D{1, 1}; + + // Create a temporary raw image to decode the JPEG into. + // RawImage::create with dimensions already calls createData() internally. + RawImage tmpRaw = RawImage::create( + iPoint2D(jpegFrameDim.x, jpegFrameDim.y), RawImageType::UINT16, 1); + + const iRectangle2D tmpFrame = { + {0, 0}, {jpegFrameDim.x, jpegFrameDim.y}}; const LJpegDecompressor::Frame jpegFrame = {MCUSize, jpegFrameDim}; int numLJpegRowsPerRestartInterval; if (numMCUsPerRestartInterval == 0) { - // Restart interval not enabled, so all of the rows - // are contained in the first (implicit) restart interval. numLJpegRowsPerRestartInterval = jpegFrameDim.y; } else { const int numMCUsPerRow = jpegFrameDim.x; if (numMCUsPerRestartInterval % numMCUsPerRow != 0) ThrowRDE("Restart interval is not a multiple of frame row size"); - numLJpegRowsPerRestartInterval = numMCUsPerRestartInterval / numMCUsPerRow; + numLJpegRowsPerRestartInterval = + numMCUsPerRestartInterval / numMCUsPerRow; } - LJpegDecompressor d(mRaw, imgFrame, jpegFrame, rec, + LJpegDecompressor d(tmpRaw, tmpFrame, jpegFrame, rec, numLJpegRowsPerRestartInterval, + implicit_cast(predictorMode), input.peekRemainingBuffer().getAsArray1DRef()); - return d.decode(); + auto consumed = d.decode(); + + // Deinterleave: each JPEG row of width (widthPack * tileW) maps to + // widthPack consecutive tile rows of width tileW. + const auto tmpData = tmpRaw->getU16DataAsUncroppedArray2DRef(); + const auto outData = mRaw->getU16DataAsUncroppedArray2DRef(); + + const int tileW = implicit_cast(w); + const int cpp = implicit_cast(mRaw->getCpp()); + const int outRowPixels = cpp * tileW; + + for (int jpegRow = 0; jpegRow < jpegFrameDim.y; ++jpegRow) { + for (int pack = 0; pack < widthPack; ++pack) { + const int tileRow = + implicit_cast(offY) + jpegRow * widthPack + pack; + if (tileRow >= mRaw->dim.y) + continue; + const int srcCol = pack * outRowPixels; + const int dstCol = cpp * implicit_cast(offX); + for (int col = 0; col < outRowPixels && (srcCol + col) < jpegFrameDim.x; + ++col) { + outData(tileRow, dstCol + col) = tmpData(jpegRow, srcCol + col); + } + } + } + + return consumed; } } // namespace rawspeed diff --git a/src/librawspeed/decompressors/LJpegDecompressor.cpp b/src/librawspeed/decompressors/LJpegDecompressor.cpp index c13618297..337168705 100644 --- a/src/librawspeed/decompressors/LJpegDecompressor.cpp +++ b/src/librawspeed/decompressors/LJpegDecompressor.cpp @@ -53,10 +53,12 @@ LJpegDecompressor::LJpegDecompressor(RawImage img, iRectangle2D imgFrame_, Frame frame_, std::vector rec_, int numLJpegRowsPerRestartInterval_, + int predictorMode_, Array1DRef input_) : mRaw(std::move(img)), input(input_), imgFrame(imgFrame_), frame(std::move(frame_)), rec(std::move(rec_)), - numLJpegRowsPerRestartInterval(numLJpegRowsPerRestartInterval_) { + numLJpegRowsPerRestartInterval(numLJpegRowsPerRestartInterval_), + predictorMode(predictorMode_) { if (mRaw->getDataType() != RawImageType::UINT16) ThrowRDE("Unexpected data type (%u)", @@ -181,9 +183,39 @@ constexpr iPoint2D MCU = {MCUWidth, MCUHeight}; } // namespace +namespace { + +// Compute the LJpeg prediction value given predictor mode and neighbor values. +// Ra = left, Rb = above, Rc = above-left. +// All arithmetic done in int32_t to avoid overflow in modes 4-6. +// Result is modulo 2^16 per ITU-T T.81. +inline int computePrediction(int predMode, int Ra, int Rb, int Rc) { + switch (predMode) { + case 1: + return Ra; + case 2: + return Rb; + case 3: + return Rc; + case 4: + return Ra + Rb - Rc; + case 5: + return Ra + ((Rb - Rc) >> 1); + case 6: + return Rb + ((Ra - Rc) >> 1); + case 7: + return (Ra + Rb) >> 1; + default: + __builtin_unreachable(); + } +} + +} // namespace + template void LJpegDecompressor::decodeRowN( Array2DRef outStripe, Array2DRef pred, + int predMode, Array2DRef prevStripe, std::array>, N_COMP> ht, BitStreamerJPEG& bs) const { invariant(MCUSize.area() == N_COMP); @@ -207,7 +239,21 @@ void LJpegDecompressor::decodeRowN( for (int MCURow = 0; MCURow != MCUSize.y; ++MCURow) { for (int MCUСol = 0; MCUСol != MCUSize.x; ++MCUСol) { int c = (MCUSize.x * MCURow) + MCUСol; - int prediction = pred(MCURow, MCUСol); + int prediction; + if (predMode == 1) { + // Fast path for the common case (mode 1 = left neighbor). + prediction = pred(MCURow, MCUСol); + } else { + // For modes 2-7, compute Ra, Rb, Rc. + int Ra = pred(MCURow, MCUСol); // left neighbor + int stripeCol = MCUSize.x * mcuIdx + MCUСol; + int stripeRow = MCURow; + int Rb = prevStripe(stripeRow, stripeCol); + int Rc = (stripeCol >= MCUSize.x) + ? prevStripe(stripeRow, stripeCol - MCUSize.x) + : Rb; // First column: Rc = Rb + prediction = computePrediction(predMode, Ra, Rb, Rc); + } int diff = (static_cast&>(ht[c])) .decodeDifference(bs); int pix = prediction + diff; @@ -230,7 +276,21 @@ void LJpegDecompressor::decodeRowN( for (int MCURow = 0; MCURow != MCUSize.y; ++MCURow) { for (int MCUСol = 0; MCUСol != MCUSize.x; ++MCUСol) { int c = (MCUSize.x * MCURow) + MCUСol; - int prediction = pred(MCURow, MCUСol); + int prediction; + if (predMode == 1) { + prediction = pred(MCURow, MCUСol); + } else { + int Ra = pred(MCURow, MCUСol); + int stripeCol = MCUSize.x * mcuIdx + MCUСol; + int stripeRow = MCURow; + int Rb = (stripeCol < prevStripe.width()) + ? prevStripe(stripeRow, stripeCol) + : Ra; + int Rc = (stripeCol >= MCUSize.x && stripeCol < prevStripe.width()) + ? prevStripe(stripeRow, stripeCol - MCUSize.x) + : Rb; + prediction = computePrediction(predMode, Ra, Rb, Rc); + } int diff = (static_cast&>(ht[c])) .decodeDifference(bs); int pix = prediction + diff; @@ -284,6 +344,7 @@ ByteStream::size_type LJpegDecompressor::decodeN() const { restartIntervalIndex != numRestartIntervals; ++restartIntervalIndex) { auto predStorage = getInitialPreds(); auto pred = Array2DRef(predStorage.data(), MCU.x, MCU.y); + bool isFirstRow = true; if (restartIntervalIndex != 0) { auto marker = peekMarker(inputStream); @@ -321,7 +382,24 @@ ByteStream::size_type LJpegDecompressor::decodeN() const { /*croppedHeight=*/frame.mcu.y) .getAsArray2DRef(); - decodeRowN(outStripe, pred, ht, bs); + // For predictor modes 2-7, we need the previous row (stripe). + // For the first row of each restart interval, use predictor mode 1 + // (per ITU-T T.81: first row always uses horizontal prediction). + // For the first row, prevStripe points to outStripe itself (unused + // since predMode will be 1). + const int predMode = isFirstRow ? 1 : predictorMode; + const Array2DRef prevStripe = + isFirstRow + ? Array2DRef(outStripe) + : CroppedArray2DRef( + img, + /*offsetCols=*/0, + /*offsetRows=*/row - frame.mcu.y, + /*croppedWidth=*/img.width(), + /*croppedHeight=*/frame.mcu.y) + .getAsArray2DRef(); + + decodeRowN(outStripe, pred, predMode, prevStripe, ht, bs); // The predictor for the next line is the start of this line. pred = CroppedArray2DRef(outStripe, @@ -330,6 +408,7 @@ ByteStream::size_type LJpegDecompressor::decodeN() const { /*croppedWidth=*/MCU.x, /*croppedHeight=*/MCU.y) .getAsArray2DRef(); + isFirstRow = false; } inputStream.skipBytes(bs.getStreamPosition()); diff --git a/src/librawspeed/decompressors/LJpegDecompressor.h b/src/librawspeed/decompressors/LJpegDecompressor.h index 69c77739f..3c1f43527 100644 --- a/src/librawspeed/decompressors/LJpegDecompressor.h +++ b/src/librawspeed/decompressors/LJpegDecompressor.h @@ -59,6 +59,7 @@ class LJpegDecompressor final { const Frame frame; const std::vector rec; const int numLJpegRowsPerRestartInterval; + const int predictorMode; int numFullMCUs = 0; int trailingPixels = 0; @@ -79,6 +80,7 @@ class LJpegDecompressor final { template __attribute__((always_inline)) inline void decodeRowN( Array2DRef outStripe, Array2DRef pred, + int predMode, Array2DRef prevStripe, std::array>, N_COMP> ht, BitStreamerJPEG& bs) const; @@ -89,6 +91,7 @@ class LJpegDecompressor final { LJpegDecompressor(RawImage img, iRectangle2D imgFrame, Frame frame, std::vector rec, int numLJpegRowsPerRestartInterval_, + int predictorMode_, Array1DRef input); [[nodiscard]] ByteStream::size_type decode() const; From 1d3a262388dfb51f79d94b423cc61428bed2c32e Mon Sep 17 00:00:00 2001 From: Philipp Lutz Date: Sat, 18 Apr 2026 12:00:15 +0200 Subject: [PATCH 2/8] LJpeg: eliminate per-pixel branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminate per-pixel predMode branch (performance) decodeRowN gains a bool Use2DPred template parameter. The mode-1 (left-neighbor) path and the 2D-predictor path are now separate compile-time instantiations selected with if constexpr. The runtime if (predMode == 1) that previously executed for every pixel in every tile is gone entirely. Mode-1 files — the overwhelming majority of existing DNG content — are completely unaffected at the generated-code level; the compiler produces the same tight loop as before. For modes 2–7, the computePrediction switch is also folded away since predictorMode is now only evaluated once at the per-row dispatch in decodeN(), not per-pixel. The first-column Rc check is also simplified: stripeCol >= MCUSize.x (computed per-pixel) is replaced with mcuIdx > 0 (a loop-index comparison available for free), and the redundant stripeRow alias is removed. Replace scalar deinterleave with memcpy (performance) In the inverted-reshape path (LJpegDecoder), the per-pixel operator() copy loop is replaced by a single std::memcpy per row-segment. The previous loop also carried a data-dependent bounds check (srcCol + col < jpegFrameDim.x) that blocked auto-vectorisation; the check is provably redundant given the dimension validation already performed, so it is removed. memcpy allows the compiler to emit an optimal vector store. --- .../decompressors/LJpegDecoder.cpp | 19 +++-- .../decompressors/LJpegDecompressor.cpp | 73 +++++++++---------- .../decompressors/LJpegDecompressor.h | 4 +- 3 files changed, 46 insertions(+), 50 deletions(-) diff --git a/src/librawspeed/decompressors/LJpegDecoder.cpp b/src/librawspeed/decompressors/LJpegDecoder.cpp index b4fad06b9..b8b2145cf 100644 --- a/src/librawspeed/decompressors/LJpegDecoder.cpp +++ b/src/librawspeed/decompressors/LJpegDecoder.cpp @@ -113,7 +113,7 @@ Buffer::size_type LJpegDecoder::decodeScan() { if (frame.compInfo[i].superH != 1 || frame.compInfo[i].superV != 1) ThrowRDE("Unsupported subsampling"); - int N_COMP = frame.cps; + const int N_COMP = frame.cps; std::vector rec; rec.reserve(N_COMP); @@ -131,7 +131,7 @@ Buffer::size_type LJpegDecoder::decodeScan() { std::numeric_limits::max()) ThrowRDE("Maximal output tile is too large"); - auto maxRes = + const auto maxRes = iPoint2D(implicit_cast(mRaw->getCpp()) * maxDim.x, maxDim.y); if (maxRes.area() != N_COMP * jpegFrameDim.area()) ThrowRDE("LJpeg frame area does not match maximal tile area"); @@ -140,15 +140,14 @@ Buffer::size_type LJpegDecoder::decodeScan() { // CinemaDNG): JPEG frame is wider than tile and shorter, with packed rows. // Standard (Adobe): maxRes.x >= jpegFrameDim.x (tile is wider/equal) // Inverted (DJI): jpegFrameDim.x > maxRes.x (JPEG frame is wider) - bool invertedReshape = (jpegFrameDim.x > maxRes.x); - + const bool invertedReshape = (jpegFrameDim.x > maxRes.x); if (!invertedReshape) { // Standard case: tile width is a multiple of JPEG frame width. if (maxRes.x % jpegFrameDim.x != 0 || maxRes.y % jpegFrameDim.y != 0) ThrowRDE( "Maximal output tile size is not a multiple of LJpeg frame size"); - auto MCUSize = + const auto MCUSize = iPoint2D{maxRes.x / jpegFrameDim.x, maxRes.y / jpegFrameDim.y}; if (MCUSize.area() != implicit_cast(N_COMP)) ThrowRDE("Unexpected MCU size, does not match LJpeg component count"); @@ -222,7 +221,7 @@ Buffer::size_type LJpegDecoder::decodeScan() { numLJpegRowsPerRestartInterval, implicit_cast(predictorMode), input.peekRemainingBuffer().getAsArray1DRef()); - auto consumed = d.decode(); + const auto consumed = d.decode(); // Deinterleave: each JPEG row of width (widthPack * tileW) maps to // widthPack consecutive tile rows of width tileW. @@ -241,10 +240,10 @@ Buffer::size_type LJpegDecoder::decodeScan() { continue; const int srcCol = pack * outRowPixels; const int dstCol = cpp * implicit_cast(offX); - for (int col = 0; col < outRowPixels && (srcCol + col) < jpegFrameDim.x; - ++col) { - outData(tileRow, dstCol + col) = tmpData(jpegRow, srcCol + col); - } + // Contiguous row-segment copy. Bounds guaranteed by validation: + // srcCol + outRowPixels <= widthPack * outRowPixels <= jpegFrameDim.x + std::memcpy(&outData(tileRow, dstCol), &tmpData(jpegRow, srcCol), + sizeof(uint16_t) * outRowPixels); } } diff --git a/src/librawspeed/decompressors/LJpegDecompressor.cpp b/src/librawspeed/decompressors/LJpegDecompressor.cpp index 337168705..b5f2bcb1a 100644 --- a/src/librawspeed/decompressors/LJpegDecompressor.cpp +++ b/src/librawspeed/decompressors/LJpegDecompressor.cpp @@ -212,10 +212,10 @@ inline int computePrediction(int predMode, int Ra, int Rb, int Rc) { } // namespace -template +template void LJpegDecompressor::decodeRowN( Array2DRef outStripe, Array2DRef pred, - int predMode, Array2DRef prevStripe, + Array2DRef prevStripe, std::array>, N_COMP> ht, BitStreamerJPEG& bs) const { invariant(MCUSize.area() == N_COMP); @@ -238,25 +238,22 @@ void LJpegDecompressor::decodeRowN( .getAsArray2DRef(); for (int MCURow = 0; MCURow != MCUSize.y; ++MCURow) { for (int MCUСol = 0; MCUСol != MCUSize.x; ++MCUСol) { - int c = (MCUSize.x * MCURow) + MCUСol; + const int c = (MCUSize.x * MCURow) + MCUСol; int prediction; - if (predMode == 1) { - // Fast path for the common case (mode 1 = left neighbor). + if constexpr (!Use2DPred) { prediction = pred(MCURow, MCUСol); } else { - // For modes 2-7, compute Ra, Rb, Rc. - int Ra = pred(MCURow, MCUСol); // left neighbor - int stripeCol = MCUSize.x * mcuIdx + MCUСol; - int stripeRow = MCURow; - int Rb = prevStripe(stripeRow, stripeCol); - int Rc = (stripeCol >= MCUSize.x) - ? prevStripe(stripeRow, stripeCol - MCUSize.x) - : Rb; // First column: Rc = Rb - prediction = computePrediction(predMode, Ra, Rb, Rc); + const int Ra = pred(MCURow, MCUСol); + const int stripeCol = MCUSize.x * mcuIdx + MCUСol; + const int Rb = prevStripe(MCURow, stripeCol); + const int Rc = (mcuIdx > 0) + ? prevStripe(MCURow, stripeCol - MCUSize.x) + : Rb; + prediction = computePrediction(predictorMode, Ra, Rb, Rc); } - int diff = (static_cast&>(ht[c])) - .decodeDifference(bs); - int pix = prediction + diff; + const int diff = (static_cast&>(ht[c])) + .decodeDifference(bs); + const int pix = prediction + diff; outTile(MCURow, MCUСol) = uint16_t(pix); } } @@ -275,29 +272,27 @@ void LJpegDecompressor::decodeRowN( // We may end up needing just part of last N_COMP pixels. for (int MCURow = 0; MCURow != MCUSize.y; ++MCURow) { for (int MCUСol = 0; MCUСol != MCUSize.x; ++MCUСol) { - int c = (MCUSize.x * MCURow) + MCUСol; + const int c = (MCUSize.x * MCURow) + MCUСol; int prediction; - if (predMode == 1) { + if constexpr (!Use2DPred) { prediction = pred(MCURow, MCUСol); } else { - int Ra = pred(MCURow, MCUСol); - int stripeCol = MCUSize.x * mcuIdx + MCUСol; - int stripeRow = MCURow; - int Rb = (stripeCol < prevStripe.width()) - ? prevStripe(stripeRow, stripeCol) - : Ra; - int Rc = (stripeCol >= MCUSize.x && stripeCol < prevStripe.width()) - ? prevStripe(stripeRow, stripeCol - MCUSize.x) - : Rb; - prediction = computePrediction(predMode, Ra, Rb, Rc); + const int Ra = pred(MCURow, MCUСol); + const int stripeCol = MCUSize.x * mcuIdx + MCUСol; + const int Rb = (stripeCol < prevStripe.width()) + ? prevStripe(MCURow, stripeCol) + : Ra; + const int Rc = (mcuIdx > 0 && stripeCol < prevStripe.width()) + ? prevStripe(MCURow, stripeCol - MCUSize.x) + : Rb; + prediction = computePrediction(predictorMode, Ra, Rb, Rc); } - int diff = (static_cast&>(ht[c])) - .decodeDifference(bs); - int pix = prediction + diff; - int stripeRow = MCURow; - int stripeCol = (MCUSize.x * mcuIdx) + MCUСol; + const int diff = (static_cast&>(ht[c])) + .decodeDifference(bs); + const int pix = prediction + diff; + const int stripeCol = (MCUSize.x * mcuIdx) + MCUСol; if (stripeCol < outStripe.width()) - outStripe(stripeRow, stripeCol) = uint16_t(pix); + outStripe(MCURow, stripeCol) = uint16_t(pix); } } ++mcuIdx; // We did just process one more MCU. @@ -386,8 +381,7 @@ ByteStream::size_type LJpegDecompressor::decodeN() const { // For the first row of each restart interval, use predictor mode 1 // (per ITU-T T.81: first row always uses horizontal prediction). // For the first row, prevStripe points to outStripe itself (unused - // since predMode will be 1). - const int predMode = isFirstRow ? 1 : predictorMode; + // since Use2DPred will be false). const Array2DRef prevStripe = isFirstRow ? Array2DRef(outStripe) @@ -399,7 +393,10 @@ ByteStream::size_type LJpegDecompressor::decodeN() const { /*croppedHeight=*/frame.mcu.y) .getAsArray2DRef(); - decodeRowN(outStripe, pred, predMode, prevStripe, ht, bs); + if (!isFirstRow && predictorMode != 1) + decodeRowN(outStripe, pred, prevStripe, ht, bs); + else + decodeRowN(outStripe, pred, prevStripe, ht, bs); // The predictor for the next line is the start of this line. pred = CroppedArray2DRef(outStripe, diff --git a/src/librawspeed/decompressors/LJpegDecompressor.h b/src/librawspeed/decompressors/LJpegDecompressor.h index 3c1f43527..a35dd79c6 100644 --- a/src/librawspeed/decompressors/LJpegDecompressor.h +++ b/src/librawspeed/decompressors/LJpegDecompressor.h @@ -77,10 +77,10 @@ class LJpegDecompressor final { template [[nodiscard]] std::array getInitialPreds() const; - template + template __attribute__((always_inline)) inline void decodeRowN( Array2DRef outStripe, Array2DRef pred, - int predMode, Array2DRef prevStripe, + Array2DRef prevStripe, std::array>, N_COMP> ht, BitStreamerJPEG& bs) const; From fb89fea813f3ae5d931635c2d0a6c0e5679da471 Mon Sep 17 00:00:00 2001 From: Philipp Lutz Date: Sat, 18 Apr 2026 12:07:09 +0200 Subject: [PATCH 3/8] Apply clang-format --- .../decompressors/LJpegDecoder.cpp | 13 +++++------ .../decompressors/LJpegDecompressor.cpp | 22 +++++++++---------- .../decompressors/LJpegDecompressor.h | 3 +-- 3 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/librawspeed/decompressors/LJpegDecoder.cpp b/src/librawspeed/decompressors/LJpegDecoder.cpp index b8b2145cf..a42aa8d92 100644 --- a/src/librawspeed/decompressors/LJpegDecoder.cpp +++ b/src/librawspeed/decompressors/LJpegDecoder.cpp @@ -199,11 +199,10 @@ Buffer::size_type LJpegDecoder::decodeScan() { // Create a temporary raw image to decode the JPEG into. // RawImage::create with dimensions already calls createData() internally. - RawImage tmpRaw = RawImage::create( - iPoint2D(jpegFrameDim.x, jpegFrameDim.y), RawImageType::UINT16, 1); + RawImage tmpRaw = RawImage::create(iPoint2D(jpegFrameDim.x, jpegFrameDim.y), + RawImageType::UINT16, 1); - const iRectangle2D tmpFrame = { - {0, 0}, {jpegFrameDim.x, jpegFrameDim.y}}; + const iRectangle2D tmpFrame = {{0, 0}, {jpegFrameDim.x, jpegFrameDim.y}}; const LJpegDecompressor::Frame jpegFrame = {MCUSize, jpegFrameDim}; int numLJpegRowsPerRestartInterval; @@ -213,8 +212,7 @@ Buffer::size_type LJpegDecoder::decodeScan() { const int numMCUsPerRow = jpegFrameDim.x; if (numMCUsPerRestartInterval % numMCUsPerRow != 0) ThrowRDE("Restart interval is not a multiple of frame row size"); - numLJpegRowsPerRestartInterval = - numMCUsPerRestartInterval / numMCUsPerRow; + numLJpegRowsPerRestartInterval = numMCUsPerRestartInterval / numMCUsPerRow; } LJpegDecompressor d(tmpRaw, tmpFrame, jpegFrame, rec, @@ -234,8 +232,7 @@ Buffer::size_type LJpegDecoder::decodeScan() { for (int jpegRow = 0; jpegRow < jpegFrameDim.y; ++jpegRow) { for (int pack = 0; pack < widthPack; ++pack) { - const int tileRow = - implicit_cast(offY) + jpegRow * widthPack + pack; + const int tileRow = implicit_cast(offY) + jpegRow * widthPack + pack; if (tileRow >= mRaw->dim.y) continue; const int srcCol = pack * outRowPixels; diff --git a/src/librawspeed/decompressors/LJpegDecompressor.cpp b/src/librawspeed/decompressors/LJpegDecompressor.cpp index b5f2bcb1a..ee3899168 100644 --- a/src/librawspeed/decompressors/LJpegDecompressor.cpp +++ b/src/librawspeed/decompressors/LJpegDecompressor.cpp @@ -246,9 +246,8 @@ void LJpegDecompressor::decodeRowN( const int Ra = pred(MCURow, MCUСol); const int stripeCol = MCUSize.x * mcuIdx + MCUСol; const int Rb = prevStripe(MCURow, stripeCol); - const int Rc = (mcuIdx > 0) - ? prevStripe(MCURow, stripeCol - MCUSize.x) - : Rb; + const int Rc = + (mcuIdx > 0) ? prevStripe(MCURow, stripeCol - MCUSize.x) : Rb; prediction = computePrediction(predictorMode, Ra, Rb, Rc); } const int diff = (static_cast&>(ht[c])) @@ -383,15 +382,14 @@ ByteStream::size_type LJpegDecompressor::decodeN() const { // For the first row, prevStripe points to outStripe itself (unused // since Use2DPred will be false). const Array2DRef prevStripe = - isFirstRow - ? Array2DRef(outStripe) - : CroppedArray2DRef( - img, - /*offsetCols=*/0, - /*offsetRows=*/row - frame.mcu.y, - /*croppedWidth=*/img.width(), - /*croppedHeight=*/frame.mcu.y) - .getAsArray2DRef(); + isFirstRow ? Array2DRef(outStripe) + : CroppedArray2DRef( + img, + /*offsetCols=*/0, + /*offsetRows=*/row - frame.mcu.y, + /*croppedWidth=*/img.width(), + /*croppedHeight=*/frame.mcu.y) + .getAsArray2DRef(); if (!isFirstRow && predictorMode != 1) decodeRowN(outStripe, pred, prevStripe, ht, bs); diff --git a/src/librawspeed/decompressors/LJpegDecompressor.h b/src/librawspeed/decompressors/LJpegDecompressor.h index a35dd79c6..672249c2d 100644 --- a/src/librawspeed/decompressors/LJpegDecompressor.h +++ b/src/librawspeed/decompressors/LJpegDecompressor.h @@ -90,8 +90,7 @@ class LJpegDecompressor final { public: LJpegDecompressor(RawImage img, iRectangle2D imgFrame, Frame frame, std::vector rec, - int numLJpegRowsPerRestartInterval_, - int predictorMode_, + int numLJpegRowsPerRestartInterval_, int predictorMode_, Array1DRef input); [[nodiscard]] ByteStream::size_type decode() const; From d8577b1229438acac1afb0c4937e44cf7f04f6a5 Mon Sep 17 00:00:00 2001 From: Philipp Lutz Date: Sat, 18 Apr 2026 13:27:01 +0200 Subject: [PATCH 4/8] Fix MCU{1,2} layout decoding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some DNG files encode 2-component tiles (e.g., 592×158) as JPEG frames with the component dimension packed horizontally into a narrower, taller JPEG (592×79, 2 components). The JPEG SOF reports half the tile height, with each row encoding two consecutive output rows' worth of data — components are always interleaved horizontally, so the effective decoded width per JPEG row is jpegFrameDim.x × N_COMP = 1184. LJpegDecompressor.cpp — allow MCU{1,2} construction and dispatch The constructor's MCU allowlist and decode()'s dispatch table gain {1,2}. This is needed so the decompressor can be instantiated when the caller passes MCU{1,2} (it will never be used for actual pixel output after the decoder fix below, but the validation and dispatch must not reject it at an early stage). LJpegDecoder.cpp — detect vertical MCU and route to the inverted reshape path Two complementary changes: * In the standard (non-inverted) path, after computing MCUSize = maxRes / jpegFrameDim, gate the direct-decode branch on MCUSize.x >= MCUSize.y. When MCUSize is purely vertical (e.g., {1,2}), fall through instead of decoding in-place. * Generalize the inverted reshape path to handle N_COMP > 1: * effectiveJpegWidth = jpegFrameDim.x × N_COMP replaces jpegFrameDim.x everywhere in width calculations. * The temporary decode buffer is sized effectiveJpegWidth × jpegFrameDim.y. * MCU is set to {N_COMP, 1} (horizontal interleaving), not hardcoded {1,1}. * The N_COMP != 1 guard is removed. * widthPack is derived from effectiveJpegWidth / maxRes.x, so for the 2-comp example: 1184 / 592 = 2, matching the tile's 158 / 79 = 2× height ratio. This correctly reconstructs the full-width, full-height tile from the narrower, shorter JPEG frame, yielding proper CFA Bayer output instead of the full-image purple/orange vertical stripe pattern that occurred when the two components were written to separate rows. --- .../decompressors/LJpegDecoder.cpp | 87 +++++++++++-------- .../decompressors/LJpegDecompressor.cpp | 7 +- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/src/librawspeed/decompressors/LJpegDecoder.cpp b/src/librawspeed/decompressors/LJpegDecoder.cpp index a42aa8d92..082b20e0b 100644 --- a/src/librawspeed/decompressors/LJpegDecoder.cpp +++ b/src/librawspeed/decompressors/LJpegDecoder.cpp @@ -152,57 +152,68 @@ Buffer::size_type LJpegDecoder::decodeScan() { if (MCUSize.area() != implicit_cast(N_COMP)) ThrowRDE("Unexpected MCU size, does not match LJpeg component count"); - const iRectangle2D imgFrame = { - {static_cast(offX), static_cast(offY)}, - {static_cast(w), static_cast(h)}}; - const LJpegDecompressor::Frame jpegFrame = {MCUSize, jpegFrameDim}; - - int numLJpegRowsPerRestartInterval; - if (numMCUsPerRestartInterval == 0) { - numLJpegRowsPerRestartInterval = jpegFrameDim.y; - } else { - const int numMCUsPerRow = jpegFrameDim.x; - if (numMCUsPerRestartInterval % numMCUsPerRow != 0) - ThrowRDE("Restart interval is not a multiple of frame row size"); - numLJpegRowsPerRestartInterval = - numMCUsPerRestartInterval / numMCUsPerRow; + // Standard MCU layouts have MCU.x >= MCU.y: {1,1}, {2,1}, {3,1}, + // {4,1}, {2,2}. If the MCU is purely vertical (e.g., {1,2}), the + // encoder uses horizontal component interleaving with wider effective + // JPEG rows that must be reshaped. Fall through to inverted reshape. + if (MCUSize.x >= MCUSize.y) { + const iRectangle2D imgFrame = { + {static_cast(offX), static_cast(offY)}, + {static_cast(w), static_cast(h)}}; + const LJpegDecompressor::Frame jpegFrame = {MCUSize, jpegFrameDim}; + + int numLJpegRowsPerRestartInterval; + if (numMCUsPerRestartInterval == 0) { + numLJpegRowsPerRestartInterval = jpegFrameDim.y; + } else { + const int numMCUsPerRow = jpegFrameDim.x; + if (numMCUsPerRestartInterval % numMCUsPerRow != 0) + ThrowRDE("Restart interval is not a multiple of frame row size"); + numLJpegRowsPerRestartInterval = + numMCUsPerRestartInterval / numMCUsPerRow; + } + + LJpegDecompressor d(mRaw, imgFrame, jpegFrame, rec, + numLJpegRowsPerRestartInterval, + implicit_cast(predictorMode), + input.peekRemainingBuffer().getAsArray1DRef()); + return d.decode(); } - - LJpegDecompressor d(mRaw, imgFrame, jpegFrame, rec, - numLJpegRowsPerRestartInterval, - implicit_cast(predictorMode), - input.peekRemainingBuffer().getAsArray1DRef()); - return d.decode(); } - // Inverted reshape case (DJI/Blackmagic CinemaDNG): - // JPEG frame is wider than tile, e.g. JPEG=8000x1500 1-comp, tile=4000x3000. - // Each JPEG row contains 'widthPack' tile rows concatenated. - if (N_COMP != 1) - ThrowRDE("Inverted reshape only supported for single-component LJpeg"); - - if (jpegFrameDim.x % maxRes.x != 0) - ThrowRDE("LJpeg frame width is not a multiple of tile width"); + // Inverted reshape case: + // The effective decoded width per JPEG row exceeds the tile width. + // This occurs when: + // (a) The JPEG frame is wider than the tile (DJI/Blackmagic CinemaDNG): + // e.g., JPEG=8000x1500 1-comp, tile=4000x3000. + // (b) Multi-component JPEG with vertical MCU ratio: + // e.g., JPEG=592x79 2-comp, tile=592x158 (effectiveWidth=1184). + // In both cases, components are interleaved horizontally, and each JPEG row + // contains 'widthPack' tile rows of pixel data concatenated. + const int effectiveJpegWidth = jpegFrameDim.x * N_COMP; + + if (effectiveJpegWidth % maxRes.x != 0) + ThrowRDE("Effective JPEG width is not a multiple of tile width"); if (maxRes.y % jpegFrameDim.y != 0) ThrowRDE("Tile height is not a multiple of LJpeg frame height"); - const int widthPack = jpegFrameDim.x / maxRes.x; + const int widthPack = effectiveJpegWidth / maxRes.x; if (widthPack * jpegFrameDim.y != maxRes.y) ThrowRDE("Inverted reshape dimensions mismatch"); if (widthPack < 1 || widthPack > 4) ThrowRDE("Unexpected row packing factor: %d", widthPack); - // Decode into a temporary buffer at JPEG frame dimensions. - // MCU is {1,1} since we have a single component. - const auto MCUSize = iPoint2D{1, 1}; + // Decode into a temporary buffer at effective decoded dimensions. + // Components are always interleaved horizontally: MCU{N_COMP, 1}. + const auto MCUSize = iPoint2D{N_COMP, 1}; // Create a temporary raw image to decode the JPEG into. // RawImage::create with dimensions already calls createData() internally. - RawImage tmpRaw = RawImage::create(iPoint2D(jpegFrameDim.x, jpegFrameDim.y), - RawImageType::UINT16, 1); + RawImage tmpRaw = RawImage::create( + iPoint2D(effectiveJpegWidth, jpegFrameDim.y), RawImageType::UINT16, 1); - const iRectangle2D tmpFrame = {{0, 0}, {jpegFrameDim.x, jpegFrameDim.y}}; + const iRectangle2D tmpFrame = {{0, 0}, {effectiveJpegWidth, jpegFrameDim.y}}; const LJpegDecompressor::Frame jpegFrame = {MCUSize, jpegFrameDim}; int numLJpegRowsPerRestartInterval; @@ -226,8 +237,8 @@ Buffer::size_type LJpegDecoder::decodeScan() { const auto tmpData = tmpRaw->getU16DataAsUncroppedArray2DRef(); const auto outData = mRaw->getU16DataAsUncroppedArray2DRef(); - const int tileW = implicit_cast(w); - const int cpp = implicit_cast(mRaw->getCpp()); + const auto tileW = implicit_cast(w); + const auto cpp = implicit_cast(mRaw->getCpp()); const int outRowPixels = cpp * tileW; for (int jpegRow = 0; jpegRow < jpegFrameDim.y; ++jpegRow) { @@ -238,7 +249,7 @@ Buffer::size_type LJpegDecoder::decodeScan() { const int srcCol = pack * outRowPixels; const int dstCol = cpp * implicit_cast(offX); // Contiguous row-segment copy. Bounds guaranteed by validation: - // srcCol + outRowPixels <= widthPack * outRowPixels <= jpegFrameDim.x + // srcCol + outRowPixels <= widthPack * outRowPixels <= effectiveJpegWidth std::memcpy(&outData(tileRow, dstCol), &tmpData(jpegRow, srcCol), sizeof(uint16_t) * outRowPixels); } diff --git a/src/librawspeed/decompressors/LJpegDecompressor.cpp b/src/librawspeed/decompressors/LJpegDecompressor.cpp index ee3899168..5ef2d66ff 100644 --- a/src/librawspeed/decompressors/LJpegDecompressor.cpp +++ b/src/librawspeed/decompressors/LJpegDecompressor.cpp @@ -102,8 +102,8 @@ LJpegDecompressor::LJpegDecompressor(RawImage img, iRectangle2D imgFrame_, ThrowRDE("Frame has zero size"); if (iPoint2D{1, 1} != frame.mcu && iPoint2D{2, 1} != frame.mcu && - iPoint2D{3, 1} != frame.mcu && iPoint2D{4, 1} != frame.mcu && - iPoint2D{2, 2} != frame.mcu) + iPoint2D{1, 2} != frame.mcu && iPoint2D{3, 1} != frame.mcu && + iPoint2D{4, 1} != frame.mcu && iPoint2D{2, 2} != frame.mcu) ThrowRDE("Unexpected MCU size: {%i, %i}", frame.mcu.x, frame.mcu.y); if (rec.size() != static_cast(frame.mcu.area())) @@ -423,6 +423,9 @@ ByteStream::size_type LJpegDecompressor::decode() const { if (frame.mcu == MCU<2, 1>) { return decodeN>(); } + if (frame.mcu == MCU<1, 2>) { + return decodeN>(); + } break; case 3: if (frame.mcu == MCU<3, 1>) { From aeb171913a3d980cd32cd35fbecc395dae7360ed Mon Sep 17 00:00:00 2001 From: Philipp Lutz Date: Sun, 19 Apr 2026 13:13:56 +0200 Subject: [PATCH 5/8] Refactor & clean-up to satisfy clang-tidy --- .../decompressors/LJpegDecompressor.cpp | 4 +- .../decompressors/LJpegDecoder.cpp | 289 +++++++++--------- .../decompressors/LJpegDecompressor.cpp | 7 +- .../decompressors/LJpegDecompressor.h | 7 +- 4 files changed, 160 insertions(+), 147 deletions(-) diff --git a/fuzz/librawspeed/decompressors/LJpegDecompressor.cpp b/fuzz/librawspeed/decompressors/LJpegDecompressor.cpp index 83e406abb..2d1b1a63a 100644 --- a/fuzz/librawspeed/decompressors/LJpegDecompressor.cpp +++ b/fuzz/librawspeed/decompressors/LJpegDecompressor.cpp @@ -90,9 +90,11 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* Data, size_t Size) { const int numLJpegRowsPerRestartInterval = bs.getI32(); const int predictorMode = bs.getByte(); + const rawspeed::LJpegDecompressor::DecodeSettings settings{ + numLJpegRowsPerRestartInterval, predictorMode}; rawspeed::LJpegDecompressor d( mRaw, rawspeed::iRectangle2D(mRaw->dim.x, mRaw->dim.y), frame, rec, - numLJpegRowsPerRestartInterval, predictorMode, + settings, bs.getSubStream(/*offset=*/0).peekRemainingBuffer().getAsArray1DRef()); mRaw->createData(); (void)d.decode(); diff --git a/src/librawspeed/decompressors/LJpegDecoder.cpp b/src/librawspeed/decompressors/LJpegDecoder.cpp index 082b20e0b..1e3deb08c 100644 --- a/src/librawspeed/decompressors/LJpegDecoder.cpp +++ b/src/librawspeed/decompressors/LJpegDecoder.cpp @@ -30,18 +30,139 @@ #include "io/Buffer.h" #include "io/ByteStream.h" #include -#include #include #include #include #include -#include #include -using std::copy_n; - namespace rawspeed { +namespace { + +using PerCompRecipeVec = std::vector; + +struct ScanSettings final { + RawImage raw; + iRectangle2D imgFrame; + iPoint2D jpegFrameDim; + iPoint2D maxRes; + LJpegDecompressor::DecodeSettings decode; + Array1DRef input; +}; + +[[nodiscard]] int +getNumLJpegRowsPerRestartInterval(uint32_t numMCUsPerRestartInterval, + iPoint2D jpegFrameDim) { + if (numMCUsPerRestartInterval == 0) + return jpegFrameDim.y; + + const int numMCUsPerRow = jpegFrameDim.x; + if (numMCUsPerRestartInterval % numMCUsPerRow != 0) + ThrowRDE("Restart interval is not a multiple of frame row size"); + return implicit_cast(numMCUsPerRestartInterval) / numMCUsPerRow; +} + +[[nodiscard]] iPoint2D getMaxResolution(const RawImage& raw, iPoint2D maxDim, + int numComponents, + iPoint2D jpegFrameDim) { + if (implicit_cast(maxDim.x) * implicit_cast(raw->getCpp()) > + std::numeric_limits::max()) + ThrowRDE("Maximal output tile is too large"); + + const auto maxRes = + iPoint2D(implicit_cast(raw->getCpp()) * maxDim.x, maxDim.y); + if (maxRes.area() != numComponents * jpegFrameDim.area()) + ThrowRDE("LJpeg frame area does not match maximal tile area"); + + return maxRes; +} + +[[nodiscard]] iPoint2D +getStandardMCUSize(int numComponents, iPoint2D jpegFrameDim, iPoint2D maxRes) { + if (jpegFrameDim.x > maxRes.x) + return {}; + + if (maxRes.x % jpegFrameDim.x != 0 || maxRes.y % jpegFrameDim.y != 0) + ThrowRDE("Maximal output tile size is not a multiple of LJpeg frame size"); + + const auto mcuSize = + iPoint2D{maxRes.x / jpegFrameDim.x, maxRes.y / jpegFrameDim.y}; + if (mcuSize.area() != implicit_cast(numComponents)) + ThrowRDE("Unexpected MCU size, does not match LJpeg component count"); + + if (mcuSize.x < mcuSize.y) + return {}; + + return mcuSize; +} + +[[nodiscard]] ByteStream::size_type +decodeStandardScan(const ScanSettings& settings, iPoint2D mcuSize, + const PerCompRecipeVec& rec) { + const LJpegDecompressor::Frame jpegFrame = {mcuSize, settings.jpegFrameDim}; + LJpegDecompressor d(settings.raw, settings.imgFrame, jpegFrame, rec, + settings.decode, settings.input); + return d.decode(); +} + +void copyDeinterleavedRows(const RawImage& raw, RawImage tmpRaw, uint32_t offX, + uint32_t offY, uint32_t tileWidth, int widthPack) { + const auto tmpData = tmpRaw->getU16DataAsUncroppedArray2DRef(); + const auto outData = raw->getU16DataAsUncroppedArray2DRef(); + + const auto cpp = implicit_cast(raw->getCpp()); + const int outRowPixels = cpp * implicit_cast(tileWidth); + + for (int jpegRow = 0; jpegRow < tmpRaw->dim.y; ++jpegRow) { + for (int pack = 0; pack < widthPack; ++pack) { + const int tileRow = implicit_cast(offY) + jpegRow * widthPack + pack; + if (tileRow >= raw->dim.y) + continue; + + const int srcCol = pack * outRowPixels; + const int dstCol = cpp * implicit_cast(offX); + std::memcpy(&outData(tileRow, dstCol), &tmpData(jpegRow, srcCol), + sizeof(uint16_t) * outRowPixels); + } + } +} + +[[nodiscard]] ByteStream::size_type +decodeInvertedScan(const ScanSettings& settings, int numComponents, + uint32_t offX, uint32_t offY, uint32_t tileWidth, + const PerCompRecipeVec& rec) { + const int effectiveJpegWidth = settings.jpegFrameDim.x * numComponents; + + if (effectiveJpegWidth % settings.maxRes.x != 0) + ThrowRDE("Effective JPEG width is not a multiple of tile width"); + if (settings.maxRes.y % settings.jpegFrameDim.y != 0) + ThrowRDE("Tile height is not a multiple of LJpeg frame height"); + + const int widthPack = effectiveJpegWidth / settings.maxRes.x; + if (widthPack * settings.jpegFrameDim.y != settings.maxRes.y) + ThrowRDE("Inverted reshape dimensions mismatch"); + if (widthPack < 1 || widthPack > 4) + ThrowRDE("Unexpected row packing factor: %d", widthPack); + + const auto mcuSize = iPoint2D{numComponents, 1}; + RawImage tmpRaw = + RawImage::create(iPoint2D(effectiveJpegWidth, settings.jpegFrameDim.y), + RawImageType::UINT16, 1); + const iRectangle2D tmpFrame = {{0, 0}, + {effectiveJpegWidth, settings.jpegFrameDim.y}}; + const LJpegDecompressor::Frame jpegFrame = {mcuSize, settings.jpegFrameDim}; + + LJpegDecompressor d(tmpRaw, tmpFrame, jpegFrame, rec, settings.decode, + settings.input); + const auto consumed = d.decode(); + + copyDeinterleavedRows(settings.raw, tmpRaw, offX, offY, tileWidth, widthPack); + return consumed; +} + +} // namespace + LJpegDecoder::LJpegDecoder(ByteStream bs, const RawImage& img) : AbstractLJpegDecoder(bs, img) { if (mRaw->getDataType() != RawImageType::UINT16) @@ -113,149 +234,37 @@ Buffer::size_type LJpegDecoder::decodeScan() { if (frame.compInfo[i].superH != 1 || frame.compInfo[i].superV != 1) ThrowRDE("Unsupported subsampling"); - const int N_COMP = frame.cps; + const int numComponents = frame.cps; - std::vector rec; - rec.reserve(N_COMP); - std::generate_n(std::back_inserter(rec), N_COMP, - [&rec, hts = getPrefixCodeDecoders(N_COMP), - initPred = getInitialPredictors( - N_COMP)]() -> LJpegDecompressor::PerComponentRecipe { + PerCompRecipeVec rec; + rec.reserve(numComponents); + std::generate_n(std::back_inserter(rec), numComponents, + [&rec, hts = getPrefixCodeDecoders(numComponents), + initPred = getInitialPredictors(numComponents)]() + -> LJpegDecompressor::PerComponentRecipe { const auto i = implicit_cast(rec.size()); return {*hts[i], initPred[i]}; }); const auto jpegFrameDim = iPoint2D(frame.w, frame.h); - - if (implicit_cast(maxDim.x) * implicit_cast(mRaw->getCpp()) > - std::numeric_limits::max()) - ThrowRDE("Maximal output tile is too large"); - const auto maxRes = - iPoint2D(implicit_cast(mRaw->getCpp()) * maxDim.x, maxDim.y); - if (maxRes.area() != N_COMP * jpegFrameDim.area()) - ThrowRDE("LJpeg frame area does not match maximal tile area"); - - // Detect whether the JPEG frame uses an inverted reshape (e.g. DJI/Blackmagic - // CinemaDNG): JPEG frame is wider than tile and shorter, with packed rows. - // Standard (Adobe): maxRes.x >= jpegFrameDim.x (tile is wider/equal) - // Inverted (DJI): jpegFrameDim.x > maxRes.x (JPEG frame is wider) - const bool invertedReshape = (jpegFrameDim.x > maxRes.x); - if (!invertedReshape) { - // Standard case: tile width is a multiple of JPEG frame width. - if (maxRes.x % jpegFrameDim.x != 0 || maxRes.y % jpegFrameDim.y != 0) - ThrowRDE( - "Maximal output tile size is not a multiple of LJpeg frame size"); - - const auto MCUSize = - iPoint2D{maxRes.x / jpegFrameDim.x, maxRes.y / jpegFrameDim.y}; - if (MCUSize.area() != implicit_cast(N_COMP)) - ThrowRDE("Unexpected MCU size, does not match LJpeg component count"); - - // Standard MCU layouts have MCU.x >= MCU.y: {1,1}, {2,1}, {3,1}, - // {4,1}, {2,2}. If the MCU is purely vertical (e.g., {1,2}), the - // encoder uses horizontal component interleaving with wider effective - // JPEG rows that must be reshaped. Fall through to inverted reshape. - if (MCUSize.x >= MCUSize.y) { - const iRectangle2D imgFrame = { - {static_cast(offX), static_cast(offY)}, - {static_cast(w), static_cast(h)}}; - const LJpegDecompressor::Frame jpegFrame = {MCUSize, jpegFrameDim}; - - int numLJpegRowsPerRestartInterval; - if (numMCUsPerRestartInterval == 0) { - numLJpegRowsPerRestartInterval = jpegFrameDim.y; - } else { - const int numMCUsPerRow = jpegFrameDim.x; - if (numMCUsPerRestartInterval % numMCUsPerRow != 0) - ThrowRDE("Restart interval is not a multiple of frame row size"); - numLJpegRowsPerRestartInterval = - numMCUsPerRestartInterval / numMCUsPerRow; - } - - LJpegDecompressor d(mRaw, imgFrame, jpegFrame, rec, - numLJpegRowsPerRestartInterval, - implicit_cast(predictorMode), - input.peekRemainingBuffer().getAsArray1DRef()); - return d.decode(); - } - } - - // Inverted reshape case: - // The effective decoded width per JPEG row exceeds the tile width. - // This occurs when: - // (a) The JPEG frame is wider than the tile (DJI/Blackmagic CinemaDNG): - // e.g., JPEG=8000x1500 1-comp, tile=4000x3000. - // (b) Multi-component JPEG with vertical MCU ratio: - // e.g., JPEG=592x79 2-comp, tile=592x158 (effectiveWidth=1184). - // In both cases, components are interleaved horizontally, and each JPEG row - // contains 'widthPack' tile rows of pixel data concatenated. - const int effectiveJpegWidth = jpegFrameDim.x * N_COMP; - - if (effectiveJpegWidth % maxRes.x != 0) - ThrowRDE("Effective JPEG width is not a multiple of tile width"); - if (maxRes.y % jpegFrameDim.y != 0) - ThrowRDE("Tile height is not a multiple of LJpeg frame height"); - - const int widthPack = effectiveJpegWidth / maxRes.x; - if (widthPack * jpegFrameDim.y != maxRes.y) - ThrowRDE("Inverted reshape dimensions mismatch"); - - if (widthPack < 1 || widthPack > 4) - ThrowRDE("Unexpected row packing factor: %d", widthPack); - - // Decode into a temporary buffer at effective decoded dimensions. - // Components are always interleaved horizontally: MCU{N_COMP, 1}. - const auto MCUSize = iPoint2D{N_COMP, 1}; - - // Create a temporary raw image to decode the JPEG into. - // RawImage::create with dimensions already calls createData() internally. - RawImage tmpRaw = RawImage::create( - iPoint2D(effectiveJpegWidth, jpegFrameDim.y), RawImageType::UINT16, 1); - - const iRectangle2D tmpFrame = {{0, 0}, {effectiveJpegWidth, jpegFrameDim.y}}; - const LJpegDecompressor::Frame jpegFrame = {MCUSize, jpegFrameDim}; - - int numLJpegRowsPerRestartInterval; - if (numMCUsPerRestartInterval == 0) { - numLJpegRowsPerRestartInterval = jpegFrameDim.y; - } else { - const int numMCUsPerRow = jpegFrameDim.x; - if (numMCUsPerRestartInterval % numMCUsPerRow != 0) - ThrowRDE("Restart interval is not a multiple of frame row size"); - numLJpegRowsPerRestartInterval = numMCUsPerRestartInterval / numMCUsPerRow; - } - - LJpegDecompressor d(tmpRaw, tmpFrame, jpegFrame, rec, - numLJpegRowsPerRestartInterval, - implicit_cast(predictorMode), - input.peekRemainingBuffer().getAsArray1DRef()); - const auto consumed = d.decode(); - - // Deinterleave: each JPEG row of width (widthPack * tileW) maps to - // widthPack consecutive tile rows of width tileW. - const auto tmpData = tmpRaw->getU16DataAsUncroppedArray2DRef(); - const auto outData = mRaw->getU16DataAsUncroppedArray2DRef(); - - const auto tileW = implicit_cast(w); - const auto cpp = implicit_cast(mRaw->getCpp()); - const int outRowPixels = cpp * tileW; - - for (int jpegRow = 0; jpegRow < jpegFrameDim.y; ++jpegRow) { - for (int pack = 0; pack < widthPack; ++pack) { - const int tileRow = implicit_cast(offY) + jpegRow * widthPack + pack; - if (tileRow >= mRaw->dim.y) - continue; - const int srcCol = pack * outRowPixels; - const int dstCol = cpp * implicit_cast(offX); - // Contiguous row-segment copy. Bounds guaranteed by validation: - // srcCol + outRowPixels <= widthPack * outRowPixels <= effectiveJpegWidth - std::memcpy(&outData(tileRow, dstCol), &tmpData(jpegRow, srcCol), - sizeof(uint16_t) * outRowPixels); - } - } - - return consumed; + getMaxResolution(mRaw, maxDim, numComponents, jpegFrameDim); + const ScanSettings settings{mRaw, + {{static_cast(offX), static_cast(offY)}, + {static_cast(w), static_cast(h)}}, + jpegFrameDim, + maxRes, + {getNumLJpegRowsPerRestartInterval( + numMCUsPerRestartInterval, jpegFrameDim), + implicit_cast(predictorMode)}, + input.peekRemainingBuffer().getAsArray1DRef()}; + + if (const auto mcuSize = + getStandardMCUSize(numComponents, jpegFrameDim, maxRes); + mcuSize.hasPositiveArea()) + return decodeStandardScan(settings, mcuSize, rec); + + return decodeInvertedScan(settings, numComponents, offX, offY, w, rec); } } // namespace rawspeed diff --git a/src/librawspeed/decompressors/LJpegDecompressor.cpp b/src/librawspeed/decompressors/LJpegDecompressor.cpp index 5ef2d66ff..376ddd3a7 100644 --- a/src/librawspeed/decompressors/LJpegDecompressor.cpp +++ b/src/librawspeed/decompressors/LJpegDecompressor.cpp @@ -52,13 +52,12 @@ namespace rawspeed { LJpegDecompressor::LJpegDecompressor(RawImage img, iRectangle2D imgFrame_, Frame frame_, std::vector rec_, - int numLJpegRowsPerRestartInterval_, - int predictorMode_, + DecodeSettings settings, Array1DRef input_) : mRaw(std::move(img)), input(input_), imgFrame(imgFrame_), frame(std::move(frame_)), rec(std::move(rec_)), - numLJpegRowsPerRestartInterval(numLJpegRowsPerRestartInterval_), - predictorMode(predictorMode_) { + numLJpegRowsPerRestartInterval(settings.numLJpegRowsPerRestartInterval), + predictorMode(settings.predictorMode) { if (mRaw->getDataType() != RawImageType::UINT16) ThrowRDE("Unexpected data type (%u)", diff --git a/src/librawspeed/decompressors/LJpegDecompressor.h b/src/librawspeed/decompressors/LJpegDecompressor.h index 672249c2d..11caad318 100644 --- a/src/librawspeed/decompressors/LJpegDecompressor.h +++ b/src/librawspeed/decompressors/LJpegDecompressor.h @@ -45,6 +45,10 @@ class LJpegDecompressor final { const iPoint2D mcu; const iPoint2D dim; }; + struct DecodeSettings final { + const int numLJpegRowsPerRestartInterval; + const int predictorMode; + }; struct PerComponentRecipe final { const PrefixCodeDecoder<>& ht; const uint16_t initPred; @@ -90,8 +94,7 @@ class LJpegDecompressor final { public: LJpegDecompressor(RawImage img, iRectangle2D imgFrame, Frame frame, std::vector rec, - int numLJpegRowsPerRestartInterval_, int predictorMode_, - Array1DRef input); + DecodeSettings settings, Array1DRef input); [[nodiscard]] ByteStream::size_type decode() const; }; From a9dbfad873923c802a7c156e83899d5a5df19019 Mon Sep 17 00:00:00 2001 From: Philipp Lutz Date: Sun, 19 Apr 2026 14:19:22 +0200 Subject: [PATCH 6/8] Support UniqueCameraModel Exif tag for DNG MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blackmagic CinemaDNG files don't include TIFF Make/Model tags — they only have UniqueCameraModel (tag 50708), which is valid per the DNG spec. In decodeMetaDataInternal(), the code called getID() unconditionally. Instead of calling getID() in a try/catch (which logged a spurious error), check for MAKE+MODEL existence first. If present, call getID() as before. If absent, fall back to UNIQUECAMERAMODEL for both make and model fields — mirroring the approach already used in checkSupportInternal(). --- src/librawspeed/decoders/DngDecoder.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/librawspeed/decoders/DngDecoder.cpp b/src/librawspeed/decoders/DngDecoder.cpp index ecd119898..ae972d2a5 100644 --- a/src/librawspeed/decoders/DngDecoder.cpp +++ b/src/librawspeed/decoders/DngDecoder.cpp @@ -699,12 +699,20 @@ void DngDecoder::decodeMetaDataInternal(const CameraMetaData* meta) { TiffID id; - try { + if (mRootIFD->hasEntryRecursive(TiffTag::MAKE) && + mRootIFD->hasEntryRecursive(TiffTag::MODEL)) { id = mRootIFD->getID(); - } catch (const RawspeedException& e) { - mRaw->setError(e.what()); - // not all dngs have MAKE/MODEL entries, - // will be dealt with by using UNIQUECAMERAMODEL below + } else if (mRootIFD->hasEntryRecursive(TiffTag::UNIQUECAMERAMODEL)) { + // Not all DNGs have MAKE/MODEL entries (e.g. Blackmagic CinemaDNG). + // Fall back to UNIQUECAMERAMODEL for identification. + std::string unique = + mRootIFD->getEntryRecursive(TiffTag::UNIQUECAMERAMODEL)->getString(); + if (unique.empty()) + ThrowRDE("UNIQUECAMERAMODEL is empty"); + id.make = unique; + id.model = unique; + } else { + ThrowRDE("DNG has neither MAKE/MODEL nor UNIQUECAMERAMODEL"); } // Set the make and model From a167a44f3a5034725be51b941bff7e00e7f751aa Mon Sep 17 00:00:00 2001 From: Philipp Lutz Date: Sun, 19 Apr 2026 14:34:47 +0200 Subject: [PATCH 7/8] Address codeChecker findings --- src/librawspeed/decompressors/LJpegDecoder.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/librawspeed/decompressors/LJpegDecoder.cpp b/src/librawspeed/decompressors/LJpegDecoder.cpp index 1e3deb08c..31d3b8702 100644 --- a/src/librawspeed/decompressors/LJpegDecoder.cpp +++ b/src/librawspeed/decompressors/LJpegDecoder.cpp @@ -20,6 +20,7 @@ */ #include "decompressors/LJpegDecoder.h" +#include "adt/Array1DRef.h" #include "adt/Casts.h" #include "adt/Invariant.h" #include "adt/Point.h" @@ -106,8 +107,9 @@ decodeStandardScan(const ScanSettings& settings, iPoint2D mcuSize, return d.decode(); } -void copyDeinterleavedRows(const RawImage& raw, RawImage tmpRaw, uint32_t offX, - uint32_t offY, uint32_t tileWidth, int widthPack) { +void copyDeinterleavedRows(const RawImage& raw, const RawImage& tmpRaw, + uint32_t offX, uint32_t offY, uint32_t tileWidth, + int widthPack) { const auto tmpData = tmpRaw->getU16DataAsUncroppedArray2DRef(); const auto outData = raw->getU16DataAsUncroppedArray2DRef(); From 8e10cee704b021a6a092da142cf0215ea7aab38c Mon Sep 17 00:00:00 2001 From: Philipp Lutz Date: Sun, 19 Apr 2026 16:25:44 +0200 Subject: [PATCH 8/8] Add more meaningful error message for 12-bit JPG files Some Blackmagic CinemaDNG files (Pocket Cinema Camera 4K files (3)/(4), Micro Cinema Camera (1)) use SOF1 (Extended Sequential DCT) at 12-bit precision but label tiles as TIFF compression=7 (lossless JPEG). The lossless JPEG decoder hit a DQT marker and threw "Not a valid RAW file." On libjpeg-turbo 2.1.5, the error is now "Unsupported JPEG data precision 12" (much clearer). Future work: on systems with libjpeg-turbo 3.0+, we can enable full 12-bit lossy JPEG decoding. --- .../decompressors/AbstractDngDecompressor.cpp | 67 +++++++++++++++++++ .../decompressors/JpegDecompressor.cpp | 4 ++ 2 files changed, 71 insertions(+) diff --git a/src/librawspeed/decompressors/AbstractDngDecompressor.cpp b/src/librawspeed/decompressors/AbstractDngDecompressor.cpp index b828a4fe2..c68044d9c 100644 --- a/src/librawspeed/decompressors/AbstractDngDecompressor.cpp +++ b/src/librawspeed/decompressors/AbstractDngDecompressor.cpp @@ -51,6 +51,64 @@ namespace rawspeed { +namespace { + +// Some DNG files (e.g. Blackmagic CinemaDNG) use TIFF compression=7 +// (lossless JPEG) but the actual tile data contains lossy DCT JPEG +// (SOF0/SOF1/SOF2 with DQT). Detect this by scanning the first few +// JPEG markers in the tile stream. +[[nodiscard]] bool tileContainsLossyJpeg(const ByteStream& bs) { + const auto remaining = bs.getRemainSize(); + if (remaining < 4) + return false; + + // Must start with JPEG SOI marker + if (bs.peekByte(0) != 0xFF || bs.peekByte(1) != 0xD8) + return false; + + // Scan markers after SOI. Stop after a reasonable number of bytes. + const auto limit = + std::min(remaining, static_cast(1024)); + ByteStream::size_type pos = 2; + + while (pos + 3 < limit) { + if (bs.peekByte(pos) != 0xFF) + return false; // Invalid marker - stop scanning + + const uint8_t marker = bs.peekByte(pos + 1); + + // DQT (quantization table) is definitive proof of lossy JPEG + if (marker == 0xDB) // DQT + return true; + + // SOF0/SOF1/SOF2 = lossy DCT-based JPEG + if (marker == 0xC0 || marker == 0xC1 || marker == 0xC2) + return true; + + // SOF3 = lossless - this is what compression=7 should be + if (marker == 0xC3) + return false; + + // SOS = start of scan data - stop scanning + if (marker == 0xDA) + return false; + + // Skip this marker segment + if (pos + 4 > remaining) + return false; + const auto segLen = static_cast( + (static_cast(bs.peekByte(pos + 2)) << 8) | + static_cast(bs.peekByte(pos + 3))); + if (segLen < 2) + return false; + pos += 2 + segLen; + } + + return false; +} + +} // namespace + template <> void AbstractDngDecompressor::decompressThread<1>() const noexcept { #ifdef HAVE_OPENMP #pragma omp for schedule(static) @@ -116,6 +174,15 @@ template <> void AbstractDngDecompressor::decompressThread<7>() const noexcept { for (const auto& e : Array1DRef(slices.data(), implicit_cast(slices.size()))) { try { +#ifdef HAVE_JPEG + // Some cameras (e.g. Blackmagic CinemaDNG) mislabel lossy DCT JPEG + // tiles as compression=7 (lossless JPEG). Detect and redirect. + if (tileContainsLossyJpeg(e.bs)) { + JpegDecompressor j(e.bs.peekBuffer(e.bs.getRemainSize()), mRaw); + j.decode(e.offX, e.offY); + continue; + } +#endif LJpegDecoder d(e.bs, mRaw); d.decode(e.offX, e.offY, e.width, e.height, iPoint2D(e.dsc.tileW, e.dsc.tileH), mFixLjpeg); diff --git a/src/librawspeed/decompressors/JpegDecompressor.cpp b/src/librawspeed/decompressors/JpegDecompressor.cpp index 569bb037a..a0a4da39b 100644 --- a/src/librawspeed/decompressors/JpegDecompressor.cpp +++ b/src/librawspeed/decompressors/JpegDecompressor.cpp @@ -139,6 +139,10 @@ void JpegDecompressor::decode(uint32_t offX, if (JPEG_HEADER_OK != jpeg_read_header(&dinfo, static_cast(true))) ThrowRDE("Unable to read JPEG header"); + if (dinfo.data_precision != 8) + ThrowRDE("Lossy JPEG tiles with %d-bit precision are not yet supported.", + dinfo.data_precision); + jpeg_start_decompress(&dinfo); if (dinfo.output_components != static_cast(mRaw->getCpp())) ThrowRDE("Component count doesn't match");