Skip to content

JXL DNG: honor Row/ColumnInterleaveFactor, add F32 path#1

Merged
MaykThewessen merged 3 commits into
jpegxl-dng17from
jxl-dng17-cfa-deinterleave-and-float
Jul 1, 2026
Merged

JXL DNG: honor Row/ColumnInterleaveFactor, add F32 path#1
MaykThewessen merged 3 commits into
jpegxl-dng17from
jxl-dng17-cfa-deinterleave-and-float

Conversation

@MaykThewessen

Copy link
Copy Markdown
Owner

Stacked on top of darktable-org#971 (base branch jpegxl-dng17). Three correctness fixes for the JPEG XL DNG decoder, found while verifying the feature against Adobe's DNG SDK sample files and a real interleaved-CFA capture.

1. Honor RowInterleaveFactor / ColumnInterleaveFactor (DNG 1.7)

DNG 1.7 lets a CFA mosaic be stored as R x C stacked half-res color-plane subimages (each plane is spatially smooth, so it compresses far better under lossy JXL). The decoder ignored RowInterleaveFactor (0xC71F, was defined but never read) and ColumnInterleaveFactor (0xCD43, was not defined), so any 2x2-interleaved CFA JXL DNG decoded scrambled: the four quadrant planes were left stacked instead of woven back into the Bayer mosaic.

  • Adds COLUMNINTERLEAVEFACTOR = 0xCD43 to TiffTag.h.
  • New DngDeinterleave.h: pure, unit-testable dngDeinterleaveFieldMap(total, factor).
  • DngDecoder::deinterleaveFields(): a whole-frame post-pass after slices.decompress(), before ActiveArea/DefaultCropOrigin. Type-generic (UINT16 + F32) via getByteDataAsUncroppedArray2DRef().

Why whole-frame, not per-tile: Adobe's 03_jxl_bayer_raw_integer.dng is 10240x7168 with 1280x1440 tiles and R=C=2; the row-field boundary (3584) is not a multiple of 1440, so a single JXL tile straddles two color planes. Per-tile de-interleave is geometrically impossible. The index mapping (fy = within_field_row * R + field_row_index, symmetric for columns; non-divisible fields let earlier fields absorb the remainder) matches the DNG 1.7.1.0 spec and dng_sdk's dng_row_interleaved_image::MapRow.

2. Unit test for the de-interleave math

googletest covering factor-1 identity, non-divisible field sizing, and the spec's 4x4 / R=C=2 quadrant worked example (verifies the exact RGGB-phase mosaic).

3. F32 output path for float JXL DNGs

The decoder unconditionally requested JXL_TYPE_UINT16 and wrote through getU16DataAsUncroppedArray2DRef(), which asserts on float (F32) images, so float LinearRaw JXL DNGs (02_jxl_linear_raw_float.dng) failed to decode. Now the JxlPixelFormat sample type and output accessor are picked from mRaw->getDataType() (JXL_TYPE_FLOAT + F32 accessor for float, UINT16 path otherwise) via a templated copyTile().

Verification (libjxl 0.11.2, standalone build)

  • Build clean (275/275); DngDeinterleaveTest 6/6.
  • rstest -c decodes 01_jxl_linear_raw_integer, 02_jxl_linear_raw_float, 03_jxl_bayer_raw_integer with no crash/assertion. 02 confirms the UINT16 assertion is gone.
  • 03 mosaic de-scrambled: mid-line vs interior mean-abs-diff ratio 1.3x (a scrambled 4-quadrant layout shows 5-20x).
  • Non-interleaved LinearRaw is an unaffected fast-path no-op.

Test files: the Adobe DNG SDK sample set (adobe-dng-sdk sample_files), 03_jxl_bayer_raw_integer.dng being the canonical 2x2-interleaved CFA vector.


🤖 Generated with Claude Code

MaykThewessen and others added 3 commits June 30, 2026 23:30
DNG 1.7 CFA files may store the Bayer mosaic as RowInterleaveFactor x
ColumnInterleaveFactor stacked color-plane "fields" (each a smooth
half-res subimage that compresses far better under lossy JPEG XL),
signalled by tags 0xC71F / 0xCD43. rawspeed assembled the tiles but
never wove the fields back together, emitting a scrambled mosaic (four
quadrant planes stacked instead of RGGB).

Add a whole-frame de-interleave post-pass in decodeData(), run after
slices.decompress() and before handleMetadata() applies ActiveArea /
DefaultCropOrigin. It operates on the uncropped assembled buffer, one
whole pixel (getBpp() bytes) at a time, so it is type-generic over
UINT16 and F32. A single JXL tile can straddle a field boundary, so
this cannot be done per-tile.

The stored->final index math (dngDeinterleaveFieldMap) follows the
Adobe DNG 1.7.1.0 spec and the dng_sdk dng_read_image.cpp reference:
field f's rows scatter to final rows f, f+R, f+2R, ...; non-divisible
sizes let earlier fields absorb the remainder. Factored into a small
pure header (DngDeinterleave.h) so the index mapping is unit-testable.
R==1 && C==1 fast-paths to a no-op.

Also adds the missing COLUMNINTERLEAVEFACTOR (0xCD43) tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unit-test dngDeinterleaveFieldMap, including the verified 4x4 R=C=2
worked example (quadrant-constant stored buffer must de-interleave to
the RGGB-phase mosaic), the per-pixel stored->final map, the factor-1
identity fast path, and the non-divisible remainder rule from dng_sdk
(total=5,factor=2 and total=7,factor=3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The decompressor unconditionally wrote the decoded tile through
mRaw->getU16DataAsUncroppedArray2DRef() and requested JXL_TYPE_UINT16,
which asserts (dataType == RawImageType::UINT16) and fails to decode for
float (F32) JPEG XL DNGs such as Adobe's 02_jxl_linear_raw_float.dng.

Pick the JxlPixelFormat sample type from mRaw->getDataType(): request
JXL_TYPE_FLOAT and write via getF32DataAsUncroppedArray2DRef() for F32
images, keeping the JXL_TYPE_UINT16 path for integer images. The decoded
tile now lands in a type-agnostic byte buffer (libjxl reports the size in
bytes) and a templated copyTile() shares the interleaved copy loop.

Verified both 01_jxl_linear_raw_integer.dng and
02_jxl_linear_raw_float.dng decode without crashing via rstest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@MaykThewessen MaykThewessen merged commit 94bb2af into jpegxl-dng17 Jul 1, 2026
@MaykThewessen MaykThewessen deleted the jxl-dng17-cfa-deinterleave-and-float branch July 1, 2026 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant