Add JPEG XL (DNG 1.7 / Compression 52546) decompressor#971
Add JPEG XL (DNG 1.7 / Compression 52546) decompressor#971MaykThewessen wants to merge 1 commit into
Conversation
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.
|
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. 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. |
Follow-up verification: CFA variant + a second real fileThe 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, End-to-end decode (full
|
| 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.
|
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. |
|
FYI Adobe's DNG SDK includes sample images, mirror here. |
What
Adds a libjxl-backed
JpegXlDecompressorso rawspeed can decode DNG 1.7 tiles compressed with JPEG XL (TIFFCompressiontag 52546). It's wired intoDngDecoderchunk acceptance andAbstractDngDecompressordispatch, modelled on the existing lossy-JPEG path (JpegDecompressor). Gated behind a newWITH_JPEGXLCMake option (default ON, mirroringWITH_JPEG/WITH_ZLIB); libjxl is discovered viapkg-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 withNo RAW chunks found, because compression52546is 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,
whitePoint65535). Built with-DRAWSPEED_ENABLE_WERROR=ONand theWITH_JPEGXLdefault (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
JxlDecoderSetImageOutBufferbyte-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.