How it works, and where the code is
All of LumiRaw's code is public at github.com/margual56/lumiraw, under the AGPL-3.0. This page follows a photo from the moment you open it to the moment you download it, and says where each part lives, so you can read it instead of taking my word for it.
The short version
LumiRaw is a static site: HTML, JavaScript, CSS, a WebAssembly module and a few data files. There's no server of its own, no database and no accounts. The server only hands out those files, the same ones to everyone. Everything that happens to a photo happens in your browser.
What your browser downloads
- The page, built with SvelteKit into static files (
web/). - The engine,
web/src/lib/wasm/darkroom.wasm: about 6 MB, 1.7 MB compressed. It's the Rust code incore/compiled to WebAssembly. - lensfun's list of cameras, then only the lenses for your camera's mount
(
web/src/lib/wasm/lenses/).
No third-party scripts, no external fonts, no analytics. web/package.json only lists build tools: no outside library runs on the page.
Opening a file
The browser reads the file into memory (web/src/lib/api.js) and hands it to a
Web Worker (web/src/lib/wasm/worker.js), which copies it into the WebAssembly
module's memory. From there on it's all Rust:
- Decoding. Each camera's format is read by rawler, dnglab's raw decoder,
vendored in
core/vendor/rawler/. The rest of the code only talks to it throughcore/src/raw.rs. - Developing the sensor data.
core/src/decode.rssubtracts the black level, applies the camera's white balance, demosaics (Malvar, He and Cutler for the usual Bayer pattern; colour difference for X-Trans and others), rebuilds blown highlights and converts the camera's colours to linear sRGB. - Reading the metadata.
core/src/exif.rsreads the EXIF, andcore/src/profile.rsworks out what matters for developing: how much noise to expect at that ISO, how much diffraction softens that aperture, whether camera shake is likely. - Finding the lens.
core/src/lensdb.rslooks it up in the lensfun database.
Developing
Every preview and every export goes through these steps, in this order. Most of them are in core/src/grade.rs, and core/src/develop.rs ties them
together:
- Lens corrections: vignetting, distortion and chromatic aberration
(
core/src/lensdb.rs). - Framing: tilt detection (
core/src/straighten.rs), rotation and crop (core/src/geometry.rs). - Dust spot removal (
core/src/local.rs), before anything is measured. - White balance, applied as a Bradford chromatic adaptation.
- Exposure. The measurements the automatic corrections work from are in
core/src/analyze.rs. - Local tone mapping, which compresses the scene's range and is also where clarity comes in.
- Graduated and radial filters (
core/src/local.rs). - A filmic shoulder, which brings scene light into what a screen can show.
- In Oklab: black and white point, contrast, shadows/midtones/highlights, colour and luminance noise, vibrance, focus recovery and sharpening.
- Grade: the colour mixer (
core/src/mixer.rs), black and white and effects (core/src/effects.rs), the colour wheels (core/src/wheels.rs), your curves (core/src/curve.rs), your .cube (core/src/lut.rs), vignette and grain. A look is only a setting of those controls (web/src/lib/looks.json, fitted fromcore/src/looks.rs).
Each automatic step writes down what it decided in a report
(core/kit/src/report.rs). That's what the interface shows next to each
control and in the list of corrections on the Before and after step.
Previews are developed small. The 100 % view develops only the piece you're looking at, using the decisions made on the whole photo, so it matches the export.
Exporting
- PNG, JPEG and TIFF are encoded in Rust (
core/src/output.rs), with the camera's metadata copied over (core/src/exif.rs). - WebP is encoded by the browser itself from a canvas, and
web/src/lib/wasm/webp.jsadds the EXIF to it. - The zip for several photos is written by
web/src/lib/zip.js, about a hundred lines with no libraries. - The download is a link to a blob made by the page itself: the file goes from the tab's memory to your disk.
Merging brackets
core/src/merge.rs groups photos into brackets by camera settings and the time
between shots, checks each one's exposure against its own pixels, aligns them (with median
threshold bitmaps), blends them so each area comes from the exposure that recorded it best,
and takes out things that moved. The API for it is in core/darkroom/src/bracket.rs.
Picking photos
core/critic/src/net.rs is MobileNet written by hand, with no libraries, built
into a wasm module of its own that only the culling page loads. idealo's two NIMA networks
(one aesthetic, one technical) are converted by tools/bake-nima.mjs and
downloaded the first time they're used. web/src/lib/critic.js shares the photos
out between several workers, and web/src/lib/cull.js groups bursts and picks.
Networks in the developer
The developer runs three small networks through core/critic/src/graph.rs, each
downloaded the first time it's needed: MediaPipe's face detector, to read skin tones
(core/src/skin.rs); MediaPipe's DeepLab v3, for "Subject" filters
(core/src/masks.rs, which also finds the sky without a network); and Megvii's
PMRID, which cleans the raw mosaic before anything else when deep noise reduction is on
(core/src/denoise.rs, baked by tools/bake-denoiser.mjs).
What's stored
- In your browser's
localStorage: the settings for each file, identified by name, size and modification date, up to 300 files (web/src/lib/memory.js); the language; whether to straighten automatically; and what you type in the calculators, with your saved spots. - In the service worker's cache (
web/src/service-worker.js): the app's own files, so it works offline, and up to 500 forecast, terrain and atlas files from the calculators, so they aren't asked for again. The desktop app keeps those files in its cache folder (desktop/src-tauri/src/cache.rs). - Not a single pixel of your photos, anywhere.
Checking it yourself
- Offline. Load the page, switch on airplane mode and open a raw file. It works the same.
- Network tab. In the developer tools, develop and export a photo: no request carries its contents.
- Security policy. The site sends a Content-Security-Policy header
(
web/static/_headers) that forbids the page from connecting to any address but its own, save Terrain Tiles (on AWS) for the terrain's heights, David Lorenz's light pollution atlas and Open-Meteo for the cloud forecast, for the golden hour and dark skies calculators. Your browser enforces it, not me, and you can see it in the response headers. It also says who may show the app inside their pages: only the blog, so its guides can let you try each step on a sample photo, which the blog itself hands over. - Run it yourself. If you'd rather not trust my server, you don't have
to: clone the repository, run
npm run buildand thennpm run preview, and it's onlocalhost:8790.tools/build.shalso rebuilds the WebAssembly module if you have Rust.
Map of the repository
| Folder | What's in it |
|---|---|
core/src/ | The developing pipeline, in Rust. |
core/kit/ | Images, colour, matrices, filters and maths. |
core/darkroom/ | The API over the pipeline, and the WebAssembly module built from it. |
core/vendor/rawler/ | The raw decoder, vendored with one change to its manifest. |
web/src/ | The page: steps, components, translations and the worker. |
web/static/ | Icons, headers, redirects and the sample photos. |
tools/ | Build, the lens database, the looks, and tests. |
.github/workflows/ | The checks that run on every change. |
Licence
LumiRaw is AGPL-3.0 (LICENSE): you can read it, change it and publish it, and
if you offer a modified version to others you have to share its code. rawler is LGPL-2.1,
the lensfun database CC BY-SA 3.0, the looks CC BY-SA 4.0, d3-celestial's sky catalogues
BSD-3-Clause and Open-Meteo's forecast CC BY 4.0; more on the About page.