Build ON1 Photo RAW

KINDAback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A local non-destructive RAW editor whose hard problems are a colour pipeline you can explain stage by stage, edits stored as parameters rather than pixels, and rendering fast enough to drag a slider on a forty megapixel file.

estimated effort multi-daythe files for this build are in the project pack

ShellTauri 2 with React and TypeScript, decoding and rendering in RustDecodingA maintained RAW decoding library, not your own parserColourICC profiles as files on disk, applied in a named stage orderCatalogueSQLite, treated as a rebuildable indexSidecarsOne versioned file per photo, written atomically beside the originalCacheRendered proxies keyed by pipeline version and adjustment hash

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree

    Why Builds the interface half of the desktop app.

    Get it nodejs.org, or nvm install 22. open ↗

    Verify node --version prints v22 or higher

  2. installfree

    Why Decoding and rendering happen in Rust, and Tauri needs platform libraries installed before the first build will succeed.

    Get it rustup, then follow the Tauri prerequisites page for your operating system. open ↗

    Verify cargo --version prints a version and a bare Tauri app builds

  3. installfree

    Why Writing a RAW parser means reimplementing a decade of per-camera quirks, and the result will be wrong for the first camera you did not own.

    Get it A well-maintained decoding crate or a binding to an established C library.

    Verify It decodes a file from each camera you own and reports the metadata

  4. have readyfree

    Why A pipeline tested on one sensor is a pipeline tuned to one sensor, and the colour matrix fallback only shows itself on a camera you do not have a profile for.

    Get it Your own files, including one portrait frame and one very large file for the performance work.

    Verify Both cameras import, and one file is large enough to make the slow path visible

  5. have readyfree

    Why Colour management without profiles on disk is a set of numbers nobody can check, and the export test compares an embedded profile against a file.

    Get it The profiles shipped with your operating system, plus any camera profiles your decoder provides.

    Verify Each profile loads and its name is shown in the export dialogue

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- photos(id, content_hash, path, filename, camera_make, camera_model, captured_at, orientation, width, height, missing_since) · the hash is the identity, the path is a hint
- sidecars(photo_id, format_version, pipeline_version, adjustments_json, written_at) · written beside the original, never inside it
- adjustments · an ordered list inside the sidecar payload, each entry a kind and its parameters, because order changes the result
- history(photo_id, ordinal, adjustments_json, at) · every state the photo has been in, so undo survives a restart
- renders(photo_id, pipeline_version, adjustments_hash, scale, path, rendered_at) · the proxy cache, keyed so a pipeline change invalidates it
- profiles(id, name, kind, icc_path) · camera, working and output profiles, each a file you can point at and inspect
- collections(id, name) and collection_photos(collection_id, photo_id, ordinal) · grouping without moving a single file on disk
- Nothing here stores a rendered pixel as an edit, which is the whole design: the image you see is recomputed from the original every time.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
LIBRARY_PATHrequired~/Pictures/rawThe photo folder. Opened read-only apart from sidecars written beside each file.
CATALOGUE_PATHrequired~/.local/share/rawedit/catalogue.dbThe index. Deletable, because rebuild reconstructs it from the folder.
CACHE_DIRrequired~/.cache/rawedit/rendersProxy renders, keyed by pipeline version and adjustment hash.
CACHE_MAX_GBrequired20Render caches grow faster than photo libraries do. Evict by last use.
WORKING_PROFILErequired/usr/share/color/icc/AdobeRGB1998.iccThe space adjustments happen in, recorded with every render.
OUTPUT_PROFILErequired/usr/share/color/icc/sRGB.iccEmbedded in every export and verified by reading it back.
PIPELINE_VERSIONrequired3Written into every sidecar. Bumping it invalidates the render cache.

The build, in order

  1. Reading a RAW file without touching it

    Photos are catalogued and identified while the source folder stays byte for byte identical.

    1. A checksum test over the source folder is the cheapest guarantee in this build.

      Files src-tauri/src/import.rssrc-tauri/tests/originals_untouched.rs

    2. Paths change, files do not, and the hash is what survives a reorganised library.

      Files src-tauri/src/catalogue.rs

    3. The embedded preview and the sensor data disagree about which way is up.

      Files src-tauri/src/orientation.rssrc/components/Grid.tsx

    done when · tick each as it passes
    watch out
    • Keying the catalogue on file path means the first time someone reorganises a folder every edit is orphaned and the app cheerfully offers to import the photos again as new, which loses the work without ever saying so. Identify a photo by a hash of its content, keep the path as a hint, and relink by hash when a file turns up somewhere else.
  2. A colour pipeline you can explain

    Every render runs named stages in a fixed order and every export carries a profile.

    1. A colour cast is only debuggable if you can say which stage introduced it.

      Files src-tauri/src/pipeline.rsdocs/pipeline.md

    2. Read the profile back off the written file; encoders drop it more often than you expect.

      Files src-tauri/src/export.rssrc-tauri/tests/icc_embedded.rs

    3. Otherwise a render improvement shows up as half the library looking different.

      Files src-tauri/src/cache.rs

    done when · tick each as it passes
    watch out
    • Exporting wide-gamut pixels without embedding a profile produces a file that looks right in your editor and muted everywhere else, because CSS Color 4 says untagged images must be treated as sRGB and an invalid profile is treated the same as none. Embed the output profile on every export, then read it back off the written file in a test rather than trusting the encoder.
  3. Edits that are parameters, not pixels

    Adjustments are an ordered list that can be reordered, undone and copied to another photo.

    1. The order is part of the meaning and the bitmap is only ever a cache.

      Files src-tauri/src/adjustments.rs

    2. Undo that dies at quit is not undo, it is a session.

      Files src-tauri/src/sidecar.rs

    3. One representation for one edit, applied to one photo or two hundred.

      Files src/components/Presets.tsx

    done when · tick each as it passes
    watch out
    • Applying each slider to the cached bitmap and saving the result is destructive editing wearing a disguise: the third time someone moves exposure they are brightening an already brightened image and the highlights are gone for good. Keep adjustments as parameters, re-render from the original every time, and cache only the output.
  4. Fast enough to drag a slider

    Interaction runs on a proxy and the full resolution render happens only when it must.

    1. The proxy is what makes the sliders feel like sliders.

      Files src-tauri/src/render_queue.rs

    2. A queue that finishes work nobody wants is a queue that never catches up.

      Files src-tauri/src/render_queue.rssrc-tauri/tests/cancel.rs

    3. Judging noise reduction on a downscaled preview is judging nothing.

      Files src/components/DetailView.tsx

    done when · tick each as it passes
    watch out
    • Re-rendering forty megapixels on every mouse move gives about two frames a second and the app feels broken, while rendering only a thumbnail hides exactly the adjustments people bought a RAW editor for, since sharpening and noise reduction are invisible below full size. Render a proxy for interaction and a one to one crop for the detail work.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more design.