Files
Elvis33LE b75267899d DNG: lossy compression 34892 (baseline JPEG tiles) (#2021)
* DNG: lossy compression 34892 (baseline JPEG tiles)

Adobe's lossy DNG (compression 34892) decodes: each tile is a complete
baseline JPEG (8-bit luma at tile size), decoded by zune-jpeg into the
tile geometry (a mismatch is an error), widened 0xFF -> 0xFFFF like
Photoshop's read of 8-bit CFA data. Predictors and the rest of the
pipeline are unchanged.

testgen grows LossyJpegTiles (image-rs encodes the Q60 fixtures behind
the testgen feature), so CI runs a decode oracle: a smooth ramp stays
within a few quantisation steps of the 8-bit source, and a tile cut
mid-stream fails cleanly.

* Lossy DNG: check the tile JPEG header before decoding; zune deps go workspace

decode_headers() runs and the (width, height) == (s.w, rows) check fires
before decode(), so a hostile SOF cannot allocate the decoder's buffers
(up to 16384x16364) before the size mismatch is caught. The mismatch has
a regression test that swaps a 32x32 tile JPEG for a 16x16 one. zune-core
and zune-jpeg move to [workspace.dependencies] next to image and flate2.

* chore(ui): format prefs_ui (the workspace fmt gate is red on main until #2417 lands)

* Lossy DNG: 3-sample LinearRaw (YCbCr) decodes to RGB; tiles size-check

Real lossy DNGs are 3-sample YCbCr LinearRaw, not single-channel CFA:
the tile now decodes through zune-jpeg's YCbCr-to-RGB output when
SamplesPerPixel is 3 (gray stays for CFA), the decoded length is checked
against the tile's sample count, and other sample counts say unsupported.
The generator writes RGB tiles for 3-sample specs. A LinearizationTable
maps the decoded samples in the develop stage as before - nothing to add
in the sensor. Test: a 3-sample ramp with 64-step channel separation
(surviving Q60 chroma quantisation) decodes within the quality band.
2026-10-10 13:39:57 -04:00
..

photocraft-raw

A clean-room, pure-Rust camera raw decoder and developer. The crate is standalone (no workspace dependencies; its decoding dependencies are flate2 for Deflate DNG and jxl-oxide, a pure-Rust JPEG XL decoder, for DNG 1.7, native builds only), has no unsafe, does no I/O (&[u8] in), builds for wasm32-unknown-unknown (sequential there, rayon-parallel on native) and never panics on hostile input: every offset is bounds-checked and sizes are checked against Limits before allocating.

use photocraft_raw::{develop, DevelopOptions, Demosaic};

let dev = develop(&bytes, &DevelopOptions { demosaic: Demosaic::Ahd, ..Default::default() })?;
// dev.rgb: interleaved 16-bit RGB in ProPhoto RGB (ROMM primaries, D50, gamma 1.8)
// dev.warnings: anything approximated or not applied

photocraft-io uses it so opening a raw file yields a normal 16-bit RGB document tagged with the built-in ProPhoto-compatible profile.

Sources (clean-room)

Implemented only from public specifications, papers and observation of files:

  • TIFF 6.0, TIFF/EP (ISO 12234-2) and the Adobe DNG Specification 1.7.
  • ITU-T T.81 (ISO 10918-1) Annex H: lossless JPEG, process 14 ("LJ92").
  • The published description of Canon's CR2 container (header, raw IFD, slice tag 0xC640).
  • Publicly documented maker-note / private tags (ExifTool's tag tables): Canon ModelID (0x0010), SensorInfo (0x00E0) and ColorData (0x4001), Nikon WB_RBLevels (0x000C) and BlackLevel (0x003D), Sony BlackLevel (0x7310), WB_RGGBLevels (0x7313), SonyRawFileType (0x7000) and SonyToneCurve (0x7010), the PanasonicRaw IFD0 tags, Olympus ImageProcessing (0x2040) and CameraSettings (0x2020) preview tags, the Fujifilm RAF header and metadata records (RawImageCropTopLeft / RawImageCroppedSize, FujiLayout, XTransLayout, WB_GRGBLevels, RawExposureBias) and FujiIFD (RawImageFullWidth / Height, BitsPerSample, StripOffsets / ByteCounts, BlackLevel, WB_GRBLevels).
  • Nikon compressed NEF: Laurent Clévy's NEF structure notes and Bill Claff's "NEF Compression" (prose: the 0x0096 table layout, a lossy curve followed by a fixed-table Huffman stage), ExifTool's Nikon tag-name documentation and ITU-T T.81 Annex F / H. The Huffman tables are not stored in the files; they were recovered by black-box analysis of CC0 raw.pixls.us samples (the same scenes shot compressed and uncompressed) for LightCraft (storytold/lightcraft#86) and verified there on ~45 files from ~35 bodies.
  • Sony cRAW (ARW 2): H. Dietz, "Sony ARW2 Compression: Artifacts And Credible Repair" (Electronic Imaging 2016) and the RawDigger / diglloyd write-ups of the 11 + 7-bit scheme; the exact bit layout and tone-curve scale were established by observation of sample files.
  • Panasonic RW2 RawFormat 5, uncompressed Olympus ORF and uncompressed Fujifilm RAF: established by observation of sample files (bit packing, page layout, sample justification; for RAF the three sample storages and the reversed order of the X-Trans / Bayer layout records, each verified by developing with every candidate phase against the camera's preview).
  • Demosaicing: Malvar, He & Cutler (ICASSP 2004); Hirakawa & Parks, "Adaptive homogeneity-directed demosaicing" (IEEE TIP 2005); for X-Trans and other non-Bayer CFAs, gradient-weighted green interpolation (Lu & Tan, "Color filter array demosaicking: new method and performance measures", IEEE TIP 2003) with edge-aware colour differences.
  • McCamy's CCT approximation (1992); the Bradford chromatic adaptation transform.

No code from dcraw, LibRaw, rawspeed, rawler, rawloader or darktable was read or used, and no camera colour tables were copied.

Support matrix

Format Status
DNG Uncompressed (8–16 bit, packed or not), Deflate (compression 8, predictors 1 and 2), lossless JPEG and JPEG XL (DNG 1.7, e.g. Samsung Expert RAW; decoded by jxl-oxide; not in wasm32 builds, where it is Unsupported); strips and tiles; CFA (Bayer) and LinearRaw; LinearizationTable, BlackLevel (+ repeat, DeltaH/V), WhiteLevel, ActiveArea, DefaultCrop, ColorMatrix1/2, CameraCalibration, ForwardMatrix, AnalogBalance, AsShotNeutral / AsShotWhiteXY, BaselineExposure, Orientation, OpcodeList2 GainMap (lens shading)
DNG (lossy JPEG, floating point; opcodes other than GainMap; ProfileGainTableMap) Unsupported / not applied (reported)
CR2 Lossless JPEG with slices, borders and as-shot white balance from the maker note, black measured on the masked border. CR2 has no CFA tag and the row phase varies by model, so it is measured from the data (the green diagonal), with a Canon model-ID table as the fallback (see src/cr2.rs)
CR2 sRAW / mRAW Unsupported
NEF / NRW, ARW, PEF and other TIFF/EP raws Uncompressed and lossless-JPEG (incl. Sony lossless ARW) CFA data
Nikon compressed NEF (compression 34713): lossless and lossy type 1 / 2, 12 and 14 bit Decoded: fixed Huffman tables + maker-note 0x0096 seeds and curve (see src/nefc.rs). BlackLevel 0x003d is read in 14-bit units. "Lossy after split" files are unsupported (preview fallback)
Sony compressed ARW ("cRAW", SonyRawFileType 2) Decoded: 11-bit min/max + 7-bit delta blocks, SonyToneCurve to 14 bits, the curve's 512 black level when no BlackLevel tag is written (the first-generation bodies, e.g. the ILCE-7)
Panasonic / Leica RW2, RawFormat 5 (12- and 14-bit packed) Decoded, with PanasonicRaw black / white / WB / sensor borders
Olympus ORF, uncompressed 16-bit (E-1, E-400…) Decoded, with ImageProcessing black / WB / ValidBits / crop
Nikon "lossy after split" NEF, Sony "Compressed RAW 2", Pentax compressed PEF, RW2 RawFormat 4 and older, Olympus compressed ORF Unsupported: no public description of these codes was found apart from GPL decoder source, which this crate may not use (clean-room). photocraft-io opens the embedded JPEG preview instead
Fujifilm RAF, uncompressed (X-Trans I–V, GFX and Bayer bodies since about 2010) Decoded: 16-bit containers (either byte order), 12-bit LSB-first packing, 14-bit packing in 32-bit words; X-Trans 6×6 or Bayer 2×2 layout, black, as-shot WB, crop, RawExposureBias, orientation from the preview's EXIF
Fujifilm compressed RAF, early FinePix / SuperCCD RAF (no CFA TIFF) Unsupported (no public description of the compression); preview fallback
CR3 Recognised, unsupported (preview fallback where a preview is found)
X-Trans and other periodic non-Bayer CFAs (up to 16×16) Demosaiced by one pattern-agnostic edge-directed method, whatever Demosaic is chosen

Development pipeline

  1. Linearization table, per-position black level, scale to the white level.
  2. White balance (as shot; or grey-world when the file has none; or explicit multipliers), normalized so the smallest multiplier is 1, then clip to 1 so blown highlights stay white.
  3. Demosaic: Bilinear, Mhc (Malvar–He–Cutler) or Ahd (default) for Bayer; non-Bayer CFAs (X-Trans) always use the edge-directed generic method (demosaic_cfa).
  4. Camera → XYZ (D50) per the DNG specification (ColorMatrix interpolated by the white's correlated colour temperature, or ForwardMatrix), → linear ProPhoto; exposure (BaselineExposure + user EV); gamma 1.8; 16 bits.
  5. Orientation.

Files without colour calibration (CR2, NEF, ARW, RW2, ORF, RAF…) use a documented neutral fallback: the white-balanced camera channels are treated as linear sRGB primaries (colours are plausible but less saturated than a calibrated profile; converting to DNG gives calibrated colour). No tone curve is applied: the result is a scene-referred rendering, flatter than a camera JPEG.

Tools

cargo run --release -p photocraft-raw --example rawinfo -- [--dump] [--demosaic ahd] [--png DIR] FILE... prints what was decoded, times decode and develop, and can write sRGB PNG previews and the embedded JPEG previews.