A darkroom in the browser. Lumière develops camera RAW files through a physically modelled film pipeline: real characteristic curves, grain derived from silver-halide counting statistics, development chemistry, and printing onto paper with toning. It also develops scanned negatives by inverting them through the curves of the stock they were actually shot on.
Everything runs locally. There is no account, no cloud, and no network call anywhere in the application — your photographs and your edits never leave the machine.
Rust and WebAssembly decode; WebGL2 renders. Every control re-runs the whole chain at interactive speed.
Try it in the browser — bring your own RAW file; nothing is uploaded.
Film emulation usually means a look-up table and a noise overlay. That reads as wrong immediately to anyone who has shot film, because film is not a filter: it is an emulsion with a characteristic curve, a crystal structure, a development process and a print. Lumière models those, and measures itself against the manufacturers' own published numbers.
| Claim | How it is verified |
|---|---|
| Grain matches the real stock | Rendered RMS granularity at D=1.0 through a 48 µm aperture is within 4 % of the datasheet value for every tested stock, with no per-stock fudge factor |
| Grain is a counting process, not noise | The density distribution's skewness flips sign between low and high coverage — filtered noise cannot do this, and the old noise path is kept as a negative control that fails the test |
| Paper grades are real grades | Print gamma spans a factor 3.39 across grades 00–5, against 3.40 predicted independently from the papers' published ISO range |
| Scan inversion follows the stock | A synthetic negative round-trips with 0.0004 stops of crossover; developing it as the wrong stock is 445× worse |
| Culling is genuinely fast | 8.3 ms median photo-to-photo, straight off the embedded preview, with zero full decodes |
Numbers come from tools/bench.mjs and tools/verify.mjs, which drive the real
application in a real browser against real camera files. Claims without a
measurement do not ship.
Every tile is a real export through the develop path. Top row: one portrait clean, then through Portra 400, Velvia 50 and Tri-X 400. Below: CineStill 800T's halation on a night street, Ektar on a landscape, Gold 200 expired ten years warm, Tri-X pushed two stops in Rodinal 1+50, Ektachrome cross-processed in C-41, Superia with bleach bypass, T-Max P3200's grain, and HP5 Plus on a selenium-toned print. The portraits are used with the photographer's permission, the other frames are CC0; see credits.
The film browser renders your actual photograph through every stock. Hovering a tile opens a 1:1 inset so grain and structure can be judged before committing.
Develops RAW files. Canon CR2 and CR3 (including lossy CRAW), Nikon NEF and Adobe DNG including Apple ProRAW are verified against real camera files. Sony ARW and Fujifilm RAF decode through the same library but are not yet covered by the test suite. Demosaicing is Malvar-He-Cutler; colour goes through the camera's own matrix into linear Rec.2020 and stays floating point to the end.
Simulates film, physically. Seventeen stocks — Portra, Ektar, Gold, Kodachrome, Ektachrome, Tri-X, T-Max, Velvia, Provia, Superia, Acros, HP5+, FP4+, Pan F+, Delta, CineStill 800T — each with density-versus-log-exposure curves from the manufacturer's data. Grain is a boolean point process: a virtual crystal lattice, log-normal grain radii, Bernoulli development, and aperture-integrated counting statistics, with the lattice solved from each stock's published granularity. Halation adapts to the frame's own specular content.
Simulates development. Eight developers (D-76, XTOL, Rodinal dilutions, HC-110 B, DD-X, stand), push and pull through real development kinetics rather than a contrast slider, temperature, agitation-driven compensating development and Mackie-line edge effects, expired film by age and storage temperature, E-6/C-41 cross-processing with genuine crossover, and bleach bypass computed from retained silver density.
Prints. Multigrade paper grades, fibre and resin-coated stocks, print exposure in stops, selenium, sepia and gold toning as density-dependent hue shifts on the print curve — selenium acts on the silver-rich shadows and barely touches the highlights, which is exactly why it looks the way it does — split toning, and dry-down.
Dodges and burns. Local adjustments over radial, linear, brush and luminosity-range masks, with exposure measured in stops, because that is how a dodge under an enlarger is timed. Plus spotting for dust, stored as vectors so it survives re-cropping and re-exporting.
Develops scanned negatives. Pick the stock the film was shot on and Lumière inverts through its actual characteristic curves, removing the orange mask as a measured per-channel density offset rather than a white-balance guess. The same physics engine that emulates film for digital captures develops real film that was scanned.
Organises a shoot. Multiple catalogs, ratings and pick/reject flags, colour
labels, keywords, variants, snapshots, batch export, contact sheets, and a
full-screen keyboard culling mode that never decodes a file. Catalogs export as
.lumiere bundles that carry edits, thumbnails and styles — with or without
the originals — so a catalog moves between machines and relinks to the files
wherever they live.
Exports JPEG, PNG-8, PNG-16 and TIFF-16 with cropping, rotation, resizing and framing.
Tri-X 400 pushed two stops in Rodinal 1+50. The panel shows the computed development time, because push is modelled as development, not as contrast.
Requirements: Rust with the wasm32-unknown-unknown target, wasm-bindgen-cli
0.2.122, and Node 20+.
bash crates/rawcore/build.sh # build the decoder into web/src/wasm
cd web && npm install && npm run devThen open the printed URL and drop a RAW file onto the window.
cargo test -p rawcore --release # decoder, colour, encoders
node web/src/film/selftest.ts # film and chemistry model
node tools/verify.mjs # end-to-end, headless, real RAW files
node tools/bench.mjs # performance budgetstools/verify.mjs builds the production bundle, serves it, drives headless
Chrome over the DevTools protocol and asserts on rendered pixels. Put a few RAW
files in samples/ first; the harness names the ones it expects.
G library · D develop · C crop · A auto tone · 1–5 rate · 0 clear
rating · P pick · X reject · U unflag · \ hold for before/after · 0
fit · 1 100 % · arrows move between photographs.
| Path | What lives there |
|---|---|
crates/rawcore |
RAW decode, demosaic, colour transform, encoders (Rust, compiled to wasm) |
third_party/rawler |
vendored RAW decoder, patched for wasm (see its LUMIERE-PATCHES.md) |
web/src/gpu |
the WebGL2 develop pipeline |
web/src/film |
film stocks, chemistry, paper, LUT construction |
web/src/scan |
scanned-negative inversion |
web/src/masks, web/src/spot |
dodging and burning, spotting |
web/src/library, web/src/bundle |
catalogs, persistence, .lumiere archives |
web/src/ui |
the interface |
docs/ |
the design and physics specifications the code is written against |
The docs/ directory is worth reading if you care how any of this works.
film-physics.md and chemistry.md describe the models and cite their
sources; product.md records the research the feature set is based on and the
decisions it forced, including where a measurement disagreed with an
expectation and the expectation lost.
GPL-3.0-or-later. Lumière vendors rawler
(LGPL-2.1) with small patches for WebAssembly, documented in
third_party/rawler/LUMIERE-PATCHES.md; that directory remains under its own
licence.
Working and measured, but young. The develop pipeline, film and chemistry
models, scan inversion, catalog and printing tools are implemented and covered
by the test suites above. Known limits are recorded honestly in CHANGELOG.md
and in the docs/ specifications rather than glossed over — including the
performance budget that was revised when a measurement proved the original
number unreachable.




