OpenEnlarge is an open-source desktop darkroom for color film negatives. It inverts and develops scans of negatives into finished positives — the job a darkroom enlarger does for optical prints.
Most tools treat a negative scan as a generic image and fit per-channel tone curves to flip it. OpenEnlarge instead works in the density domain, using a Beer-Lambert model of how dye layers absorb light. Density is linear in dye concentration; transmittance is not — which is exactly why a naive invert-and-flip looks wrong. Working in density first, then applying creative finishing on top, yields cleaner, more faithful color.
Every image is developed with a density-domain matrix inversion grounded in the Beer-Lambert model — a physically-based engine, not a per-channel tone curve.
| Negative (scan) | Developed (OpenEnlarge) |
|---|---|
![]() |
![]() |
- Density-domain inversion — physically-based Beer-Lambert engine, not a flipped curve
- Decodes RAW, TIFF, JPEG & PNG — Fuji RAF, Panasonic RW2, Nikon NEF, Sony ARW, Canon CR3, Hasselblad 3FR and DNG, plus 16-bit TIFF, JPEG and PNG → linear RGB
- Tethered shooting — watch a folder and auto-import + develop new scans as they land, so finished positives appear as you shoot ("shoot & see")
- Per-roll base calibration — sample the orange film base once per roll and apply it
- Full develop controls — tonal curve, color grading, color wheels, exposure/black/gamma
- Crop, rotate, straighten, flip with a live viewport and histogram
- Batch export to 16-bit TIFF / PNG / JPEG — with an optional batch crop applied across the whole selection in one pass
- In-app updates — checks on launch or on demand from Settings and installs the new version in place
- Headless CLI (
film-cli) for scripting and batch inversion - Cross-platform — macOS, Windows, Linux, built on Tauri
| Component | Path | Responsibility |
|---|---|---|
film-core |
crates/film-core |
Pure Rust engine — decode, density-domain inversion, calibration, export. No UI deps. |
film-cli |
crates/film-cli |
Headless CLI over film-core for batch/scripted inversion. |
| App shell | app/ |
Tauri 2 + SvelteKit UI wrapping film-core. |
Grab the latest installer for your OS from the Releases page:
- macOS —
.dmg(Apple Silicon) - Windows —
.msior.exe - Linux —
.AppImageor.deb
Prerequisites: Rust (stable), Node.js ≥ 18, and the Tauri prerequisites for your OS (on Linux: libwebkit2gtk-4.1-dev, libgtk-3-dev, librsvg2-dev, libappindicator3-dev, patchelf).
# Run the desktop app in dev mode
cd app
npm install
npm run tauri dev
# Build a release installer for your OS
npm run tauri buildThe engine also runs headless. From the repo root:
# Invert a scan with the density-domain engine → 16-bit TIFF
cargo run -p film-cli -- input.tiff -o output.tiff
# Sample the film base from a rect (x,y,w,h) and pick a stock profile
cargo run -p film-cli -- input.tiff -o out.tiff --stock portra400 --base-rect 0,0,128,128Run cargo run -p film-cli -- --help for all options.
A developed color negative is three stacked dye layers (Cyan, Magenta, Yellow) over an orange base. A scan is the forward model:
I_i = ∫ L(λ) · S_i(λ) · 10^(−D(λ)) dλ (spectral integration)
D(λ) = D_min(λ) + Σ_j C_j · D_j(λ) (Beer-Lambert: density linear in dye conc.)
OpenEnlarge's default engine inverts this in the density domain:
Ĉ = M_post · log₁₀(M_pre · I₀ / I)
It recovers dye concentrations with a cross-channel matrix instead of flipping each channel independently — the difference that makes color come out right. The deep version lives in docs/superpowers/specs/2026-06-03-film-inversion-poc-design.md.
Issues and pull requests are welcome. Before opening a PR, run the same checks CI does:
cargo fmt --all --check
cargo clippy --workspace --all-targets
cargo test --workspace
cd app && npm run check && npm run test:unitMIT © 2026 mohaelder


