Two RAW decoders, side by side
Last time I checked which cameras each app opens. This time, what happens inside. LightPhotos decodes every RAW with the rawler library, except on the Mac. LightCraft decodes every RAW with its own Rust crate, everywhere. Same files, two very different designs.
rawler · what LightPhotos uses off the Mac
LightCraft · its own crate
rawler: one call, one fixed pipeline, a 738-body settings table, LGPL-2.1, used off the Mac. LightCraft: five callable stages, colour from the file, MIT or Apache-2.0, everywhere.
One pipeline, or five stages
rawler hands you an image and one way to finish it.
decode() reads the file; then RawDevelop runs
the same eight steps in the same order every time — rescale,
demosaic, Fuji rotate, crop the active area, white balance, calibrate the
colour, crop the default frame, sRGB. There is no header-only read and no
hook between the steps.
LightCraft's crate breaks that into stages you can call on their own.
probe recognises the container. probe_info
reads the geometry, orientation, colour filter array, colour data and
metadata without decompressing a single pixel — a compressed Nikon
NEF probes in milliseconds from its first 128 rows. Then
normalized subtracts black, scales white and crops while
applying the DNG opcode lists; demosaic turns the mosaic
into camera RGB; develop finishes it.
That difference has a cost on our side. rawler's last step is an sRGB
gamma applied one pixel at a time on rawler's own thread pool, which is
most of a 24-megapixel develop. LightPhotos skips that step entirely and
runs the same curve in parallel across the machine, then denoises the
finished image, because RawDevelop has nowhere to insert a
filter mid-pipeline. When the stages are separate, you do not need any of
that.
Where the colour comes from
rawler carries a database: 738 TOML files, one per camera body, next to 48 lens files. Each body file holds its colour matrices, black and white levels, active and crop areas and filter array. The calibrate step picks a matrix by illuminant and prefers D65. DNG files instead use the matrices recorded in the file itself.
LightCraft implements the DNG colour model directly — dual-illuminant interpolation, forward matrices, camera calibration, analog balance — and turns camera RGB into linear Rec.2020. A file with no matrix falls back, in order, to 52 spectral matrices fitted to measured sensor sensitivities, a per-file fit against the camera's own JPEG, and finally a documented neutral model, with eight bundled camera profiles for the most common bodies.
One is a table of camera bodies. The other is derived from the file.
Demosaic and develop
For Bayer sensors rawler uses PPG (patterned pixel grouping). For X-Trans its develop path uses a bilinear pass; a better Markesteijn module ships in the crate but the pipeline does not use it. It also has a quick 2×2 superpixel mode that LightPhotos leans on for the first preview.
LightCraft defaults to AHD (adaptive homogeneity-directed), with PPG and bilinear for drafts and a thumbnails path, and its own edge-weighted directional demosaic for X-Trans. It also develops straight from the mosaic at a fraction of the size, so a preview never has to touch every pixel.
rawler's demosaic dispatch is written to panic on a filter array it does
not know. Ordinary Bayer and X-Trans files never reach those arms, but it
is why LightPhotos wraps rawler in guards and never lets it run a decode
without a memory cap. LightCraft's crate is
#![forbid(unsafe_code)] and denies the clippy lints for
panics, unwraps and todo!, so the same class of file is an
error it returns, not a crash.
What each one opens
| Format | rawler | LightCraft |
|---|---|---|
| DNG | Yes, including JPEG XL and linear | Yes, plus floating point, ProRAW gain maps and semantic masks; writes DNG |
| Canon CR2 / CR3 | Yes | Yes; some CR3 CRX variants refused |
| Sony ARW | Yes | Yes |
| Nikon NEF | Yes | Yes; not HE / HE★ |
| Fujifilm RAF | Yes | Yes; pre-X bodies refused |
| Panasonic RW2, Pentax PEF | Yes | Yes |
| Olympus ORF | Yes, including compressed | Uncompressed only |
| Medium format, older bodies | IIQ, X3F, MRW, KDC, MOS, DCR, ERF, MEF | ERF and MEF only; the rest show their preview |
So rawler is the wider one on paper, and the compressed ORF it reads is the single biggest hole in LightCraft's list. LightCraft is the deeper one inside DNG, where phones and drone cameras live.
Licence, patches and panics
rawler is licensed under LGPL-2.1. That is why the Mac
build of LightPhotos never links it: the App Store app must not carry a
copyleft library, so on macOS the decode goes through Apple's ImageIO and
Core Image instead. Off the Mac, LightPhotos pins rawler 0.8.0 and applies
four local patches — a clock source for the browser, lossless-JPEG
restart markers, a guard against a panic on an unusual filter array, and a
Canon sRAW black-level fix. On top of the library it adds a 500-million
sample cap, a Linear-DNG level normaliser, a Fujifilm dynamic-range
reader, and a catch_unwind around metadata and preview
reads.
LightCraft's own crate is MIT or Apache-2.0 and needs none of that. It is
pure Rust, forbids unsafe, and treats a malformed file as an
error it returns. Licensing is a design input here, not an afterthought.
What this means for LightPhotos
Neither decoder is simply better. rawler buys breadth across older and less common systems, at the price of an LGPL dependency we have to keep off the Mac, a patch set we maintain against an upstream crate, and defensive guards around code that can panic. LightCraft trades some of that breadth for depth inside DNG and a shape that is easy to trust: no unsafe, no panics, colour from the file.
If LightPhotos ever replaces rawler off the Mac, LightCraft's crate is the closest thing to a drop-in in spirit — and its permissive licence would remove the LGPL constraint altogether. It would just have to catch up on compressed ORF and the older containers first. That is a trade for another day.
The rawler camera database is at
dnglab/rawler.
LightCraft's format coverage is measured in
docs/raw-parity.md.
LightPhotos' rawler wrapper and its guards live in
src/decode/rawler/.