Skip to content

Add JPEG XL (DNG 1.7 / Compression 52546) decompressor#971

Open
MaykThewessen wants to merge 1 commit into
darktable-org:developfrom
MaykThewessen:jpegxl-dng17
Open

Add JPEG XL (DNG 1.7 / Compression 52546) decompressor#971
MaykThewessen wants to merge 1 commit into
darktable-org:developfrom
MaykThewessen:jpegxl-dng17

Conversation

@MaykThewessen

Copy link
Copy Markdown

What

Adds a libjxl-backed JpegXlDecompressor so rawspeed can decode DNG 1.7 tiles compressed with JPEG XL (TIFF Compression tag 52546). It's wired into DngDecoder chunk acceptance and AbstractDngDecompressor dispatch, modelled on the existing lossy-JPEG path (JpegDecompressor). Gated behind a new WITH_JPEGXL CMake option (default ON, mirroring WITH_JPEG/WITH_ZLIB); libjxl is discovered via pkg-config.

Why

This is what Apple ProRAW uses on the 48 MP main camera of recent iPhones (16 Pro, 17 Pro): PhotometricInterpretation = LinearRaw (34892), 3 channels, 10-bit, tiled. Those files currently fail with No RAW chunks found, because compression 52546 is dropped as unsupported.

Testing

Verified by decoding real iPhone 16 Pro Max and iPhone 17 Pro ProRAW DNGs end-to-end (all tiles, 8064×6048, clean artifact-free output; 10-bit samples scale to the full 16-bit range, whitePoint 65535). Built with -DRAWSPEED_ENABLE_WERROR=ON and the WITH_JPEGXL default (no extra flags); clang-format clean.

Honest scope: this verifies decode-without-error and visually-correct output, not pixel-exact output vs a reference decoder. The 2×2 interleaved-CFA JPEG XL variant is not exercised (my samples are LinearRaw, 1×1).

Relationship to #755 / #516

There's an existing JPEG XL effort: #755 by @kmilos (and issue #516). #755 is a proof-of-concept its author noted was stalled and open for adoption, and it currently hangs on real files due to a JxlDecoderSetImageOutBuffer byte-count-vs-element-count bug (reported on #755). This PR is an independent, self-contained, CI-passing implementation offered in that spirit — not trying to compete. Happy to consolidate however the maintainers prefer: fold this into #755, co-author, or close this in favour of #755. The goal is simply to get JPEG XL ProRAW support landed. Thanks to @kmilos for the original prototype.

Scope

Only the JPEG XL piece. The other Apple ProRAW path — lossless JPEG with predictor mode 7 (iPhone ≤ 15 Pro and the telephoto cameras) — is a separate concern handled by #963.

DNG 1.7 stores the raw image with JPEG XL compression (TIFF Compression tag
52546) -- as used by Apple ProRAW on the 48MP main camera of iPhone 16 Pro and
17 Pro (PhotometricInterpretation LinearRaw, 3 channels, 10-bit, tiled). Add a
libjxl-backed JpegXlDecompressor modelled on the existing lossy-JPEG path, wired
into DngDecoder chunk acceptance and AbstractDngDecompressor dispatch.

Gated behind a new WITH_JPEGXL CMake option (default ON, mirroring WITH_JPEG /
WITH_ZLIB); libjxl is discovered via pkg-config. When disabled, the compression
is reported unsupported via a #pragma message.
@MaykThewessen MaykThewessen requested a review from LebedevRI as a code owner June 21, 2026 21:00
@dholth

dholth commented Jun 27, 2026

Copy link
Copy Markdown

toucan.zip is a sample 2×2 interleaved-CFA JPEG XL DNG compressed to be small for convenience. I took this image. Decompression should work the same whether it's lossy or lossless; then reshape to get the original Bayer pattern.
My camera produces 19M Panasonic .RW2 files, becoming 14M lossless LJPEG-92 DNGs, 12M lossless JPEG XL DNGs.
For some reason Darktable doesn't do the same dead-sensor-pixel processing when I convert to DNG compared to camera-native RW2.

I've learned that my camera is one of a few that marks its bad pixels by setting them to 0 in the RAW instead of e.g. the black threshold of 143. It is possible to translate this list to opcodes in a DNG but Adobe DNG converter doesn't. Many cameras interpolate bad pixels before writing the RAW.

For lossless I've found that the JPEG-XL modular predictor "9=leftleft" is 93% as good as 4-up for CFA data.

@MaykThewessen

MaykThewessen commented Jun 30, 2026

Copy link
Copy Markdown
Author

Follow-up verification: CFA variant + a second real file

The PR description flagged that the CFA JPEG XL variant was not exercised (my original samples were all LinearRaw 1×1). I've now closed that gap by testing against a real single-channel CFA DNG 1.7 / JPEG XL file (Panasonic DMC-GX85, toucan.lossy-d2.00-3ev.dng) alongside the iPhone 17 Pro LinearRaw file, using this branch built standalone with libjxl 0.11.2.

End-to-end decode (full decodeRaw() via rstest -c -d)

File Photometric Framing Decode Output
Panasonic GX85 Color Filter Array (1ch, 16-bit, CFAPattern2 = 2 1 1 0) bare codestream (FF 0A) OK P5 4620×3464
iPhone 17 Pro LinearRaw (3ch) ISOBMFF container (00 00 00 0C 4A 58 4C 20) OK P6 8064×6048

Both decode cleanly through the single JxlDecoderSetInput path. Worth highlighting: two JPEG XL framings occur in the wild — Apple wraps each tile in the ISOBMFF container box, while this Panasonic/Adobe-DNG-Converter file is a bare codestream. JxlDecoder auto-detects both, so the current JxlDecoderSetInput-based approach is correct, but it means a CFA-only test sample (bare codestream) and a container sample exercise different framing branches — both belong in any test corpus.

CFA layout note

This GX85 file stores the CFA as a single-channel, full-resolution mosaic (decodes to 4620×3464, 1 channel), not a 4-channel half-resolution interleaved superpixel image. So there's no de-interleave step for this variant — the decoded plane is the Bayer mosaic directly, indexed by CFAPattern2. A "2×2 interleaved" half-res encoding would be a separate representation; if a converter emits that form too, both layouts are worth covering.

Bit-depth / scaling check

The decoder requests JXL_TYPE_UINT16, which makes libjxl map the codestream's normalized [0,1] range to [0,65535]. I checked this against both files' actual codestream bit depth and DNG metadata:

File BitsPerSample codestream bits_per_sample WhiteLevel decoded white
GX85 16 12 63232 ~63232 (3952 × 16.004)
iPhone 17 10 16 65535 full-range

The key point: BitsPerSample is not the value-range indicator — WhiteLevel is. Apple declares 10-bit but stores full-range values (WhiteLevel 65535); Adobe declares 16-bit (WhiteLevel 63232). In both cases the encoder pre-scales the codestream so libjxl's [0,1]→[0,65535] mapping lands exactly on WhiteLevel. So the unconditional JXL_TYPE_UINT16 request is correct as-is — decoded values match WhiteLevel for both conventions, no decoder-side bit-depth handling needed.

(Conversely, keying the output depth off BitsPerSample would be wrong: scaling the iPhone file to its declared 10-bit would put white at ~1023 against a WhiteLevel of 65535, i.e. ~64× too dark. The current code avoids that by always producing full-range output.)

Re: the toucan dead-pixel question

For anyone following the dead-sensor-pixel thread: the GX85 DNG carries only OpcodeList3 = WarpRectilinear — no OpcodeList1, no FixBadPixels*. So the missing defect correction vs. the camera-native RW2 is not a decode issue here; the RW2→DNG converter simply didn't emit bad-pixel opcodes. (rawspeed applies OpcodeList1 Stage-1 opcodes in decodeRaw(); darktable's host side implements only OpcodeList2/3. Nothing to apply if the converter wrote none.)

Net: this branch decodes both real-world DNG 1.7 JPEG XL framings (container + bare codestream) and both channel layouts (1ch CFA + 3ch linear) correctly, with values matching the declared WhiteLevel.


Edited: corrected the bit-depth section. An earlier version of this comment suggested the decoder might need to "honor bits_per_sample" to scale output to the DNG's declared depth. That was wrong — I verified that doing so would regress the Apple files (BitsPerSample=10 but WhiteLevel=65535). The current unconditional full-range UINT16 output is correct for both Apple and Adobe DNGs.

@dholth

dholth commented Jun 30, 2026

Copy link
Copy Markdown

FYI digikam has a RW2 converter that does an excellent job copying metadata but doesn't do what I want re jxl. For me a perfect converter would either store the CFA data as lossless JXL; or interpolate out the dead pixels, arrange planes into a 2x2 layout, before doing (lossy, distance=0.5) compression.
rawspeed's RW4 record bad pixel code is here.
Where is the handling for Exif.Image.RowInterleaveFactor and Exif.Image.ColumnInterleaveFactor which are [2, 2] if each of [B G, G R] are stored in quadrants of the image?
It is also possible to use threads when decoding a single JXL tile.

@dholth

dholth commented Jun 30, 2026

Copy link
Copy Markdown

FYI Adobe's DNG SDK includes sample images, mirror here.

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.

2 participants