Vibecode ON1 Photo RAW
track this build4 phases, 12 steps, beginner friendly0%The visible photo editing suite loop is buildable, but a credible replacement needs more than the first screen. ON1 Photo RAW earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of ON1 Photo RAW. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # ON1 Photo RAW · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, decoding and rendering in Rust | the pixel work needs a real language and the interface does not | | Decoding | A maintained RAW decoding library, not your own parser | camera formats are a decade of undocumented variation each | | Colour | ICC profiles as files on disk, applied in a named stage order | the pipeline has to be inspectable or you cannot debug a colour cast | | Catalogue | SQLite, treated as a rebuildable index | the sidecars beside the photos are the record | | Sidecars | One versioned file per photo, written atomically beside the original | an edit that only exists in a database is an edit waiting to be lost | | Cache | Rendered proxies keyed by pipeline version and adjustment hash | a pipeline change must invalidate, not serve stale pixels | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface half of the desktop app. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Rust toolchain and the Tauri prerequisites** · free - 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. - Verify: cargo --version prints a version and a bare Tauri app builds - [ ] **A maintained RAW decoding library** · free - 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 - [ ] **RAW files from at least two different cameras** · free - 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 - [ ] **ICC profiles for your working and output spaces** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Generative editing of any kind. - Support for cameras you do not own. - Cloud sync and mobile apps. - Printing and soft proofing. - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases If one of those is essential to you, that is the reason to keep paying for ON1 Photo RAW, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · ON1 Photo RAW The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local RAW editor that keeps every edit in a sidecar and never writes to your originals to replace ON1 Photo RAW. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is read-only, for the whole life of the program. Open it read-only, never write to it, never rename it, never move it. Every adjustment lives in a sidecar beside the photo and in the catalogue, and a render is a function of the original plus the sidecar plus a recorded pipeline version. A test asserts that a checksum of the source folder is identical before and after a full editing session, and that test is not allowed to be deleted. The second half of the same rule: the catalogue is an index, not the record. If the database is the only place an edit exists, a corrupted file loses years of work, which is how people actually lose photo libraries. Write the sidecar on every save and prove the catalogue can be rebuilt from the photo folder alone, with no database, in the test suite. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, decoding and rendering in Rust · the pixel work needs a real language and the interface does not - Decoding: A maintained RAW decoding library, not your own parser · camera formats are a decade of undocumented variation each - Colour: ICC profiles as files on disk, applied in a named stage order · the pipeline has to be inspectable or you cannot debug a colour cast - Catalogue: SQLite, treated as a rebuildable index · the sidecars beside the photos are the record - Sidecars: One versioned file per photo, written atomically beside the original · an edit that only exists in a database is an edit waiting to be lost - Cache: Rendered proxies keyed by pipeline version and adjustment hash · a pipeline change must invalidate, not serve stale pixels ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading a RAW file without touching it Build: Import a folder through a maintained decoder. Read metadata, compute a content hash, and show the embedded camera preview for a first paint while the real render is queued. Open every source file read-only and write nothing into the photo folder except sidecars. Trap: 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. Done when: A checksum of every file in the source folder is identical before and after an import and a full editing session, asserted in the suite. Moving a photo to a different folder relinks it to its existing edits by hash instead of creating a second entry. Orientation is applied exactly once, since the embedded preview is already rotated and the sensor data is not, tested with a portrait frame. A file the decoder cannot read is listed as unsupported by name rather than silently skipped. Do not build yet: No adjustments, no render pipeline, no export. ### Phase 2 · A colour pipeline you can explain Build: Demosaic, white balance, camera matrix into a working space, tone and colour adjustments, then convert to the output profile. Name each stage, record a pipeline version, and embed the output ICC profile in every file you write. Trap: 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. Done when: Every exported file carries an embedded ICC profile, read back off disk and compared in the suite. Rendering the same sidecar twice at the same pipeline version produces identical bytes. Changing the pipeline version invalidates cached renders instead of serving stale ones. A camera with no matrix available falls back to a documented generic matrix and the interface says which one was used. Do not build yet: No adjustment editing, caching strategy or export options yet. ### Phase 3 · Edits that are parameters, not pixels Build: Exposure, contrast, curves, white balance, crop, and one local adjustment through a simple mask. Store them as an ordered list in the sidecar, keep full history, and allow a set of adjustments to be copied to a selection or saved as a preset. Trap: 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. Done when: Reordering two adjustments changes the render, proving the order is real rather than decorative. Undo crosses a restart because history lives in the sidecar, tested by relaunching. Copying adjustments to twenty photos writes twenty sidecars and leaves twenty originals untouched. Resetting a photo restores the unedited render exactly, compared byte for byte. Do not build yet: No render performance work or export options yet. ### Phase 4 · Fast enough to drag a slider Build: Render at the displayed size for interaction, cancel a render when its input changes, and run the queue off the interface thread. Offer a one to one view of a crop region for the adjustments that only exist at full resolution, and render full size on export. Trap: 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. Done when: Dragging a slider on a forty megapixel file updates at an interactive rate on the proxy, measured and recorded in the README. A render whose input changed is cancelled rather than completed and thrown away, asserted by counting completions. Sharpening and noise reduction are inspected at one to one on a crop, not on a downscaled proxy. Export renders at full resolution regardless of what the screen was showing. Do not build yet: No export options, catalogue rebuild or backup yet. ### Phase 5 · A library you can walk away with Build: Write the sidecar beside each photo atomically on every save. Provide a rebuild command that reconstructs the whole catalogue from a folder with no database present. Export with the chosen profile, the applied orientation and a stated metadata policy, and never offer to delete an original. Trap: A delete button that removes the file from disk instead of from the catalogue is one misread dialogue away from losing a photograph that has no other copy, and photo software has been doing this to people for thirty years. Remove from the catalogue only, name the button accordingly, and leave deleting files to the file manager. Done when: The catalogue rebuilds from the photo folder alone with no database present and every edit returns, exercised in the suite. A crash during a sidecar write leaves the previous sidecar intact, tested by killing the process mid-write. Export states which profile, which metadata and which pipeline version produced the file. Removing a photo removes a catalogue row and never a file, asserted by a checksum of the folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Generative fill, sky replacement and subject removal, which are a model pipeline and a licensing question rather than an afternoon. - Supporting every camera ever made, since each new sensor is its own colour matrix and its own decoder quirk; support the cameras you own and say so. - Cloud sync and mobile apps, which turn a local tool into a service with other people's photographs in it. - Printing with soft proofing, which needs printer profiles, rendering intents and a calibrated display to mean anything. ### README must contain - That originals are opened read-only and a test asserts the source folder is unchanged after a session - That edits live in sidecars beside each photo and the catalogue can be rebuilt from the folder alone - Which cameras have a real colour matrix and what the generic fallback does to the result - The measured interaction rate on a named file size, so a slow machine is a known quantity rather than a surprise - That removing a photo removes a catalogue row and never deletes a file ===== AGENTS.md ===== # Agent instructions · ON1 Photo RAW indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, decoding and rendering in Rust, A maintained RAW decoding library, not your own parser, ICC profiles as files on disk, applied in a named stage order, SQLite, treated as a rebuildable index, One versioned file per photo, written atomically beside the original, Rendered proxies keyed by pipeline version and adjustment hash. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · ON1 Photo RAW 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading a RAW file without touching it Photos are catalogued and identified while the source folder stays byte for byte identical. ### Steps 1. Open originals read-only and assert it A checksum test over the source folder is the cheapest guarantee in this build. Files: `src-tauri/src/import.rs`, `src-tauri/tests/originals_untouched.rs` 2. Identify photos by content hash Paths change, files do not, and the hash is what survives a reorganised library. Files: `src-tauri/src/catalogue.rs` 3. Apply orientation once, from the metadata The embedded preview and the sensor data disagree about which way is up. Files: `src-tauri/src/orientation.rs`, `src/components/Grid.tsx` ### Done when - [ ] The source folder checksum is unchanged after a session - [ ] A moved photo relinks by hash - [ ] Portrait frames are rotated exactly once - [ ] Unsupported files are listed, not skipped ### 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. ## Phase 2 · A colour pipeline you can explain Every render runs named stages in a fixed order and every export carries a profile. ### Steps 1. Fix the stage order and record it A colour cast is only debuggable if you can say which stage introduced it. Files: `src-tauri/src/pipeline.rs`, `docs/pipeline.md` 2. Embed the output profile, then verify it Read the profile back off the written file; encoders drop it more often than you expect. Files: `src-tauri/src/export.rs`, `src-tauri/tests/icc_embedded.rs` 3. Version the pipeline and key the cache on it Otherwise a render improvement shows up as half the library looking different. Files: `src-tauri/src/cache.rs` ### Done when - [ ] Exports carry an embedded profile, verified from the file - [ ] The same sidecar renders to identical bytes - [ ] A pipeline version change invalidates the cache - [ ] A missing camera matrix falls back visibly ### 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. ## Phase 3 · Edits that are parameters, not pixels Adjustments are an ordered list that can be reordered, undone and copied to another photo. ### Steps 1. Store an ordered list, never a bitmap The order is part of the meaning and the bitmap is only ever a cache. Files: `src-tauri/src/adjustments.rs` 2. Keep history in the sidecar Undo that dies at quit is not undo, it is a session. Files: `src-tauri/src/sidecar.rs` 3. Copy and preset from the same structure One representation for one edit, applied to one photo or two hundred. Files: `src/components/Presets.tsx` ### Done when - [ ] Reordering adjustments changes the render - [ ] Undo survives a restart - [ ] A copy to many photos leaves originals untouched - [ ] Reset restores the unedited render exactly ### 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. ## Phase 4 · Fast enough to drag a slider Interaction runs on a proxy and the full resolution render happens only when it must. ### Steps 1. Render at display size for interaction The proxy is what makes the sliders feel like sliders. Files: `src-tauri/src/render_queue.rs` 2. Cancel superseded renders A queue that finishes work nobody wants is a queue that never catches up. Files: `src-tauri/src/render_queue.rs`, `src-tauri/tests/cancel.rs` 3. Give detail adjustments a one to one view Judging noise reduction on a downscaled preview is judging nothing. Files: `src/components/DetailView.tsx` ### Done when - [ ] Slider drags stay interactive on a large file - [ ] Superseded renders are cancelled - [ ] Detail work happens at one to one - [ ] Export always renders at full resolution ### 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. ## Not in this build - Generative editing of any kind. - Support for cameras you do not own. - Cloud sync and mobile apps. - Printing and soft proofing. ## After v1, if you want it - Lens correction from a published profile database, once the pipeline order is settled - A culling mode with flags and compare, which is where most editing sessions actually start - Batch export with per-target profiles, reusing the render queue without changing it ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The photo folder. Opened read-only apart from sidecars written beside each file. LIBRARY_PATH=~/Pictures/raw # Required. The index. Deletable, because rebuild reconstructs it from the folder. CATALOGUE_PATH=~/.local/share/rawedit/catalogue.db # Required. Proxy renders, keyed by pipeline version and adjustment hash. CACHE_DIR=~/.cache/rawedit/renders # Required. Render caches grow faster than photo libraries do. Evict by last use. CACHE_MAX_GB=20 # Required. The space adjustments happen in, recorded with every render. WORKING_PROFILE=/usr/share/color/icc/AdobeRGB1998.icc # Required. Embedded in every export and verified by reading it back. OUTPUT_PROFILE=/usr/share/color/icc/sRGB.icc # Required. Written into every sidecar. Bumping it invalidates the render cache. PIPELINE_VERSION=3
You are building a lean indie version of ON1 Photo RAW. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # ON1 Photo RAW · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, decoding and rendering in Rust | the pixel work needs a real language and the interface does not | | Decoding | A maintained RAW decoding library, not your own parser | camera formats are a decade of undocumented variation each | | Colour | ICC profiles as files on disk, applied in a named stage order | the pipeline has to be inspectable or you cannot debug a colour cast | | Catalogue | SQLite, treated as a rebuildable index | the sidecars beside the photos are the record | | Sidecars | One versioned file per photo, written atomically beside the original | an edit that only exists in a database is an edit waiting to be lost | | Cache | Rendered proxies keyed by pipeline version and adjustment hash | a pipeline change must invalidate, not serve stale pixels | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface half of the desktop app. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Rust toolchain and the Tauri prerequisites** · free - 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. - Verify: cargo --version prints a version and a bare Tauri app builds - [ ] **A maintained RAW decoding library** · free - 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 - [ ] **RAW files from at least two different cameras** · free - 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 - [ ] **ICC profiles for your working and output spaces** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Generative editing of any kind. - Support for cameras you do not own. - Cloud sync and mobile apps. - Printing and soft proofing. - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases If one of those is essential to you, that is the reason to keep paying for ON1 Photo RAW, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · ON1 Photo RAW The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local RAW editor that keeps every edit in a sidecar and never writes to your originals to replace ON1 Photo RAW. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is read-only, for the whole life of the program. Open it read-only, never write to it, never rename it, never move it. Every adjustment lives in a sidecar beside the photo and in the catalogue, and a render is a function of the original plus the sidecar plus a recorded pipeline version. A test asserts that a checksum of the source folder is identical before and after a full editing session, and that test is not allowed to be deleted. The second half of the same rule: the catalogue is an index, not the record. If the database is the only place an edit exists, a corrupted file loses years of work, which is how people actually lose photo libraries. Write the sidecar on every save and prove the catalogue can be rebuilt from the photo folder alone, with no database, in the test suite. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, decoding and rendering in Rust · the pixel work needs a real language and the interface does not - Decoding: A maintained RAW decoding library, not your own parser · camera formats are a decade of undocumented variation each - Colour: ICC profiles as files on disk, applied in a named stage order · the pipeline has to be inspectable or you cannot debug a colour cast - Catalogue: SQLite, treated as a rebuildable index · the sidecars beside the photos are the record - Sidecars: One versioned file per photo, written atomically beside the original · an edit that only exists in a database is an edit waiting to be lost - Cache: Rendered proxies keyed by pipeline version and adjustment hash · a pipeline change must invalidate, not serve stale pixels ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading a RAW file without touching it Build: Import a folder through a maintained decoder. Read metadata, compute a content hash, and show the embedded camera preview for a first paint while the real render is queued. Open every source file read-only and write nothing into the photo folder except sidecars. Trap: 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. Done when: A checksum of every file in the source folder is identical before and after an import and a full editing session, asserted in the suite. Moving a photo to a different folder relinks it to its existing edits by hash instead of creating a second entry. Orientation is applied exactly once, since the embedded preview is already rotated and the sensor data is not, tested with a portrait frame. A file the decoder cannot read is listed as unsupported by name rather than silently skipped. Do not build yet: No adjustments, no render pipeline, no export. ### Phase 2 · A colour pipeline you can explain Build: Demosaic, white balance, camera matrix into a working space, tone and colour adjustments, then convert to the output profile. Name each stage, record a pipeline version, and embed the output ICC profile in every file you write. Trap: 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. Done when: Every exported file carries an embedded ICC profile, read back off disk and compared in the suite. Rendering the same sidecar twice at the same pipeline version produces identical bytes. Changing the pipeline version invalidates cached renders instead of serving stale ones. A camera with no matrix available falls back to a documented generic matrix and the interface says which one was used. Do not build yet: No adjustment editing, caching strategy or export options yet. ### Phase 3 · Edits that are parameters, not pixels Build: Exposure, contrast, curves, white balance, crop, and one local adjustment through a simple mask. Store them as an ordered list in the sidecar, keep full history, and allow a set of adjustments to be copied to a selection or saved as a preset. Trap: 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. Done when: Reordering two adjustments changes the render, proving the order is real rather than decorative. Undo crosses a restart because history lives in the sidecar, tested by relaunching. Copying adjustments to twenty photos writes twenty sidecars and leaves twenty originals untouched. Resetting a photo restores the unedited render exactly, compared byte for byte. Do not build yet: No render performance work or export options yet. ### Phase 4 · Fast enough to drag a slider Build: Render at the displayed size for interaction, cancel a render when its input changes, and run the queue off the interface thread. Offer a one to one view of a crop region for the adjustments that only exist at full resolution, and render full size on export. Trap: 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. Done when: Dragging a slider on a forty megapixel file updates at an interactive rate on the proxy, measured and recorded in the README. A render whose input changed is cancelled rather than completed and thrown away, asserted by counting completions. Sharpening and noise reduction are inspected at one to one on a crop, not on a downscaled proxy. Export renders at full resolution regardless of what the screen was showing. Do not build yet: No export options, catalogue rebuild or backup yet. ### Phase 5 · A library you can walk away with Build: Write the sidecar beside each photo atomically on every save. Provide a rebuild command that reconstructs the whole catalogue from a folder with no database present. Export with the chosen profile, the applied orientation and a stated metadata policy, and never offer to delete an original. Trap: A delete button that removes the file from disk instead of from the catalogue is one misread dialogue away from losing a photograph that has no other copy, and photo software has been doing this to people for thirty years. Remove from the catalogue only, name the button accordingly, and leave deleting files to the file manager. Done when: The catalogue rebuilds from the photo folder alone with no database present and every edit returns, exercised in the suite. A crash during a sidecar write leaves the previous sidecar intact, tested by killing the process mid-write. Export states which profile, which metadata and which pipeline version produced the file. Removing a photo removes a catalogue row and never a file, asserted by a checksum of the folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Generative fill, sky replacement and subject removal, which are a model pipeline and a licensing question rather than an afternoon. - Supporting every camera ever made, since each new sensor is its own colour matrix and its own decoder quirk; support the cameras you own and say so. - Cloud sync and mobile apps, which turn a local tool into a service with other people's photographs in it. - Printing with soft proofing, which needs printer profiles, rendering intents and a calibrated display to mean anything. ### README must contain - That originals are opened read-only and a test asserts the source folder is unchanged after a session - That edits live in sidecars beside each photo and the catalogue can be rebuilt from the folder alone - Which cameras have a real colour matrix and what the generic fallback does to the result - The measured interaction rate on a named file size, so a slow machine is a known quantity rather than a surprise - That removing a photo removes a catalogue row and never deletes a file ===== AGENTS.md ===== # Agent instructions · ON1 Photo RAW indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, decoding and rendering in Rust, A maintained RAW decoding library, not your own parser, ICC profiles as files on disk, applied in a named stage order, SQLite, treated as a rebuildable index, One versioned file per photo, written atomically beside the original, Rendered proxies keyed by pipeline version and adjustment hash. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · ON1 Photo RAW 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading a RAW file without touching it Photos are catalogued and identified while the source folder stays byte for byte identical. ### Steps 1. Open originals read-only and assert it A checksum test over the source folder is the cheapest guarantee in this build. Files: `src-tauri/src/import.rs`, `src-tauri/tests/originals_untouched.rs` 2. Identify photos by content hash Paths change, files do not, and the hash is what survives a reorganised library. Files: `src-tauri/src/catalogue.rs` 3. Apply orientation once, from the metadata The embedded preview and the sensor data disagree about which way is up. Files: `src-tauri/src/orientation.rs`, `src/components/Grid.tsx` ### Done when - [ ] The source folder checksum is unchanged after a session - [ ] A moved photo relinks by hash - [ ] Portrait frames are rotated exactly once - [ ] Unsupported files are listed, not skipped ### 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. ## Phase 2 · A colour pipeline you can explain Every render runs named stages in a fixed order and every export carries a profile. ### Steps 1. Fix the stage order and record it A colour cast is only debuggable if you can say which stage introduced it. Files: `src-tauri/src/pipeline.rs`, `docs/pipeline.md` 2. Embed the output profile, then verify it Read the profile back off the written file; encoders drop it more often than you expect. Files: `src-tauri/src/export.rs`, `src-tauri/tests/icc_embedded.rs` 3. Version the pipeline and key the cache on it Otherwise a render improvement shows up as half the library looking different. Files: `src-tauri/src/cache.rs` ### Done when - [ ] Exports carry an embedded profile, verified from the file - [ ] The same sidecar renders to identical bytes - [ ] A pipeline version change invalidates the cache - [ ] A missing camera matrix falls back visibly ### 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. ## Phase 3 · Edits that are parameters, not pixels Adjustments are an ordered list that can be reordered, undone and copied to another photo. ### Steps 1. Store an ordered list, never a bitmap The order is part of the meaning and the bitmap is only ever a cache. Files: `src-tauri/src/adjustments.rs` 2. Keep history in the sidecar Undo that dies at quit is not undo, it is a session. Files: `src-tauri/src/sidecar.rs` 3. Copy and preset from the same structure One representation for one edit, applied to one photo or two hundred. Files: `src/components/Presets.tsx` ### Done when - [ ] Reordering adjustments changes the render - [ ] Undo survives a restart - [ ] A copy to many photos leaves originals untouched - [ ] Reset restores the unedited render exactly ### 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. ## Phase 4 · Fast enough to drag a slider Interaction runs on a proxy and the full resolution render happens only when it must. ### Steps 1. Render at display size for interaction The proxy is what makes the sliders feel like sliders. Files: `src-tauri/src/render_queue.rs` 2. Cancel superseded renders A queue that finishes work nobody wants is a queue that never catches up. Files: `src-tauri/src/render_queue.rs`, `src-tauri/tests/cancel.rs` 3. Give detail adjustments a one to one view Judging noise reduction on a downscaled preview is judging nothing. Files: `src/components/DetailView.tsx` ### Done when - [ ] Slider drags stay interactive on a large file - [ ] Superseded renders are cancelled - [ ] Detail work happens at one to one - [ ] Export always renders at full resolution ### 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. ## Not in this build - Generative editing of any kind. - Support for cameras you do not own. - Cloud sync and mobile apps. - Printing and soft proofing. ## After v1, if you want it - Lens correction from a published profile database, once the pipeline order is settled - A culling mode with flags and compare, which is where most editing sessions actually start - Batch export with per-target profiles, reusing the render queue without changing it ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The photo folder. Opened read-only apart from sidecars written beside each file. LIBRARY_PATH=~/Pictures/raw # Required. The index. Deletable, because rebuild reconstructs it from the folder. CATALOGUE_PATH=~/.local/share/rawedit/catalogue.db # Required. Proxy renders, keyed by pipeline version and adjustment hash. CACHE_DIR=~/.cache/rawedit/renders # Required. Render caches grow faster than photo libraries do. Evict by last use. CACHE_MAX_GB=20 # Required. The space adjustments happen in, recorded with every render. WORKING_PROFILE=/usr/share/color/icc/AdobeRGB1998.icc # Required. Embedded in every export and verified by reading it back. OUTPUT_PROFILE=/usr/share/color/icc/sRGB.icc # Required. Written into every sidecar. Bumping it invalidates the render cache. PIPELINE_VERSION=3
You are building a production product version of ON1 Photo RAW. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # ON1 Photo RAW · product brief ## Problem The visible photo editing suite loop is buildable, but a credible replacement needs more than the first screen. ON1 Photo RAW earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A RAW editor whose originals are provably untouched, whose colour pipeline can be explained stage by stage, whose edits survive the database being deleted, and which stays interactive on the largest file you own. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - sample user-owned assets ## Explicit non-goals for v1 - Generative editing of any kind. - Support for cameras you do not own. - Cloud sync and mobile apps. - Printing and soft proofing. - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases ## Success criteria - A source folder checksum is confirmed unchanged after a full session - A catalogue rebuild from the folder alone is confirmed to restore edits - An embedded ICC profile is confirmed by reading the exported file - Slider interaction on the largest test file is confirmed and the rate recorded - Removing a photo is confirmed never to delete a file ===== BRIEF.md ===== # Build brief · ON1 Photo RAW The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local RAW editor that keeps every edit in a sidecar and never writes to your originals to replace ON1 Photo RAW. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is read-only, for the whole life of the program. Open it read-only, never write to it, never rename it, never move it. Every adjustment lives in a sidecar beside the photo and in the catalogue, and a render is a function of the original plus the sidecar plus a recorded pipeline version. A test asserts that a checksum of the source folder is identical before and after a full editing session, and that test is not allowed to be deleted. The second half of the same rule: the catalogue is an index, not the record. If the database is the only place an edit exists, a corrupted file loses years of work, which is how people actually lose photo libraries. Write the sidecar on every save and prove the catalogue can be rebuilt from the photo folder alone, with no database, in the test suite. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, decoding and rendering in Rust · the pixel work needs a real language and the interface does not - Decoding: A maintained RAW decoding library, not your own parser · camera formats are a decade of undocumented variation each - Colour: ICC profiles as files on disk, applied in a named stage order · the pipeline has to be inspectable or you cannot debug a colour cast - Catalogue: SQLite, treated as a rebuildable index · the sidecars beside the photos are the record - Sidecars: One versioned file per photo, written atomically beside the original · an edit that only exists in a database is an edit waiting to be lost - Cache: Rendered proxies keyed by pipeline version and adjustment hash · a pipeline change must invalidate, not serve stale pixels ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading a RAW file without touching it Build: Import a folder through a maintained decoder. Read metadata, compute a content hash, and show the embedded camera preview for a first paint while the real render is queued. Open every source file read-only and write nothing into the photo folder except sidecars. Trap: 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. Done when: A checksum of every file in the source folder is identical before and after an import and a full editing session, asserted in the suite. Moving a photo to a different folder relinks it to its existing edits by hash instead of creating a second entry. Orientation is applied exactly once, since the embedded preview is already rotated and the sensor data is not, tested with a portrait frame. A file the decoder cannot read is listed as unsupported by name rather than silently skipped. Do not build yet: No adjustments, no render pipeline, no export. ### Phase 2 · A colour pipeline you can explain Build: Demosaic, white balance, camera matrix into a working space, tone and colour adjustments, then convert to the output profile. Name each stage, record a pipeline version, and embed the output ICC profile in every file you write. Trap: 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. Done when: Every exported file carries an embedded ICC profile, read back off disk and compared in the suite. Rendering the same sidecar twice at the same pipeline version produces identical bytes. Changing the pipeline version invalidates cached renders instead of serving stale ones. A camera with no matrix available falls back to a documented generic matrix and the interface says which one was used. Do not build yet: No adjustment editing, caching strategy or export options yet. ### Phase 3 · Edits that are parameters, not pixels Build: Exposure, contrast, curves, white balance, crop, and one local adjustment through a simple mask. Store them as an ordered list in the sidecar, keep full history, and allow a set of adjustments to be copied to a selection or saved as a preset. Trap: 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. Done when: Reordering two adjustments changes the render, proving the order is real rather than decorative. Undo crosses a restart because history lives in the sidecar, tested by relaunching. Copying adjustments to twenty photos writes twenty sidecars and leaves twenty originals untouched. Resetting a photo restores the unedited render exactly, compared byte for byte. Do not build yet: No render performance work or export options yet. ### Phase 4 · Fast enough to drag a slider Build: Render at the displayed size for interaction, cancel a render when its input changes, and run the queue off the interface thread. Offer a one to one view of a crop region for the adjustments that only exist at full resolution, and render full size on export. Trap: 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. Done when: Dragging a slider on a forty megapixel file updates at an interactive rate on the proxy, measured and recorded in the README. A render whose input changed is cancelled rather than completed and thrown away, asserted by counting completions. Sharpening and noise reduction are inspected at one to one on a crop, not on a downscaled proxy. Export renders at full resolution regardless of what the screen was showing. Do not build yet: No export options, catalogue rebuild or backup yet. ### Phase 5 · A library you can walk away with Build: Write the sidecar beside each photo atomically on every save. Provide a rebuild command that reconstructs the whole catalogue from a folder with no database present. Export with the chosen profile, the applied orientation and a stated metadata policy, and never offer to delete an original. Trap: A delete button that removes the file from disk instead of from the catalogue is one misread dialogue away from losing a photograph that has no other copy, and photo software has been doing this to people for thirty years. Remove from the catalogue only, name the button accordingly, and leave deleting files to the file manager. Done when: The catalogue rebuilds from the photo folder alone with no database present and every edit returns, exercised in the suite. A crash during a sidecar write leaves the previous sidecar intact, tested by killing the process mid-write. Export states which profile, which metadata and which pipeline version produced the file. Removing a photo removes a catalogue row and never a file, asserted by a checksum of the folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Generative fill, sky replacement and subject removal, which are a model pipeline and a licensing question rather than an afternoon. - Supporting every camera ever made, since each new sensor is its own colour matrix and its own decoder quirk; support the cameras you own and say so. - Cloud sync and mobile apps, which turn a local tool into a service with other people's photographs in it. - Printing with soft proofing, which needs printer profiles, rendering intents and a calibrated display to mean anything. ### README must contain - That originals are opened read-only and a test asserts the source folder is unchanged after a session - That edits live in sidecars beside each photo and the catalogue can be rebuilt from the folder alone - Which cameras have a real colour matrix and what the generic fallback does to the result - The measured interaction rate on a named file size, so a slow machine is a known quantity rather than a surprise - That removing a photo removes a catalogue row and never deletes a file ===== ARCHITECTURE.md ===== # Architecture · ON1 Photo RAW ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, decoding and rendering in Rust | the pixel work needs a real language and the interface does not | | Decoding | A maintained RAW decoding library, not your own parser | camera formats are a decade of undocumented variation each | | Colour | ICC profiles as files on disk, applied in a named stage order | the pipeline has to be inspectable or you cannot debug a colour cast | | Catalogue | SQLite, treated as a rebuildable index | the sidecars beside the photos are the record | | Sidecars | One versioned file per photo, written atomically beside the original | an edit that only exists in a database is an edit waiting to be lost | | Cache | Rendered proxies keyed by pipeline version and adjustment hash | a pipeline change must invalidate, not serve stale pixels | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Decoding, hashing and orientation. | Any decoder, provided originals stay read-only. | | Pipeline | Named stages in a fixed order with a version. | Any implementation, provided the stage order stays inspectable and versioned. | | Sidecars | Atomic writes beside each photo. | Any format, provided it is versioned and human-readable. | | Render queue | Proxy rendering and cancellation. | Any scheduler, provided superseded work is cancelled. | | Export | Full resolution output with an embedded profile. | Any encoder, provided the profile is verified on the written file. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_PATH` · required · The photo folder. Opened read-only apart from sidecars written beside each file. - `CATALOGUE_PATH` · required · The index. Deletable, because rebuild reconstructs it from the folder. - `CACHE_DIR` · required · Proxy renders, keyed by pipeline version and adjustment hash. - `CACHE_MAX_GB` · required · Render caches grow faster than photo libraries do. Evict by last use. - `WORKING_PROFILE` · required · The space adjustments happen in, recorded with every render. - `OUTPUT_PROFILE` · required · Embedded in every export and verified by reading it back. - `PIPELINE_VERSION` · required · Written into every sidecar. Bumping it invalidates the render cache. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · ON1 Photo RAW product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, decoding and rendering in Rust, A maintained RAW decoding library, not your own parser, ICC profiles as files on disk, applied in a named stage order, SQLite, treated as a rebuildable index, One versioned file per photo, written atomically beside the original, Rendered proxies keyed by pipeline version and adjustment hash. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - A delete button that removes the file from disk instead of from the catalogue is one misread dialogue away from losing a photograph that has no other copy, and photo software has been doing this to people for thirty years. Remove from the catalogue only, name the button accordingly, and leave deleting files to the file manager. ===== MILESTONES.md ===== # Delivery milestones · ON1 Photo RAW Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading a RAW file without touching it Photos are catalogued and identified while the source folder stays byte for byte identical. ### Steps 1. Open originals read-only and assert it A checksum test over the source folder is the cheapest guarantee in this build. Files: `src-tauri/src/import.rs`, `src-tauri/tests/originals_untouched.rs` 2. Identify photos by content hash Paths change, files do not, and the hash is what survives a reorganised library. Files: `src-tauri/src/catalogue.rs` 3. Apply orientation once, from the metadata The embedded preview and the sensor data disagree about which way is up. Files: `src-tauri/src/orientation.rs`, `src/components/Grid.tsx` ### Done when - [ ] The source folder checksum is unchanged after a session - [ ] A moved photo relinks by hash - [ ] Portrait frames are rotated exactly once - [ ] Unsupported files are listed, not skipped ### 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. ## M2 · A colour pipeline you can explain Every render runs named stages in a fixed order and every export carries a profile. ### Steps 1. Fix the stage order and record it A colour cast is only debuggable if you can say which stage introduced it. Files: `src-tauri/src/pipeline.rs`, `docs/pipeline.md` 2. Embed the output profile, then verify it Read the profile back off the written file; encoders drop it more often than you expect. Files: `src-tauri/src/export.rs`, `src-tauri/tests/icc_embedded.rs` 3. Version the pipeline and key the cache on it Otherwise a render improvement shows up as half the library looking different. Files: `src-tauri/src/cache.rs` ### Done when - [ ] Exports carry an embedded profile, verified from the file - [ ] The same sidecar renders to identical bytes - [ ] A pipeline version change invalidates the cache - [ ] A missing camera matrix falls back visibly ### 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. ## M3 · Edits that are parameters, not pixels Adjustments are an ordered list that can be reordered, undone and copied to another photo. ### Steps 1. Store an ordered list, never a bitmap The order is part of the meaning and the bitmap is only ever a cache. Files: `src-tauri/src/adjustments.rs` 2. Keep history in the sidecar Undo that dies at quit is not undo, it is a session. Files: `src-tauri/src/sidecar.rs` 3. Copy and preset from the same structure One representation for one edit, applied to one photo or two hundred. Files: `src/components/Presets.tsx` ### Done when - [ ] Reordering adjustments changes the render - [ ] Undo survives a restart - [ ] A copy to many photos leaves originals untouched - [ ] Reset restores the unedited render exactly ### 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. ## M4 · Fast enough to drag a slider Interaction runs on a proxy and the full resolution render happens only when it must. ### Steps 1. Render at display size for interaction The proxy is what makes the sliders feel like sliders. Files: `src-tauri/src/render_queue.rs` 2. Cancel superseded renders A queue that finishes work nobody wants is a queue that never catches up. Files: `src-tauri/src/render_queue.rs`, `src-tauri/tests/cancel.rs` 3. Give detail adjustments a one to one view Judging noise reduction on a downscaled preview is judging nothing. Files: `src/components/DetailView.tsx` ### Done when - [ ] Slider drags stay interactive on a large file - [ ] Superseded renders are cancelled - [ ] Detail work happens at one to one - [ ] Export always renders at full resolution ### 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. ## M5 · A library you can walk away with (production only) The photo folder alone is enough to reconstruct everything, and export is honest about what it wrote. ### Steps 1. Rebuild the catalogue from the folder The test that proves the database was only ever an index. Files: `src-tauri/src/rebuild.rs`, `src-tauri/tests/rebuild.rs` 2. Write sidecars atomically Temporary file, flush, replace. A truncated sidecar is a lost edit. Files: `src-tauri/src/sidecar.rs` 3. Never delete an original Remove from the catalogue, say so on the button, and let the file manager do the rest. Files: `src/components/RemoveDialog.tsx` ### Done when - [ ] A catalogue rebuild from the folder restores every edit - [ ] A crash mid-write leaves the previous sidecar intact - [ ] Export records profile, metadata and pipeline version - [ ] Removing a photo never deletes a file ### Watch out - A delete button that removes the file from disk instead of from the catalogue is one misread dialogue away from losing a photograph that has no other copy, and photo software has been doing this to people for thirty years. Remove from the catalogue only, name the button accordingly, and leave deleting files to the file manager. ===== OPERATIONS.md ===== # Operations · ON1 Photo RAW ## Backup Back up the photo folder, sidecars included, and treat the catalogue and render cache as disposable. If your backup covers the database but not the sidecars, you have backed up the index and not the work. ## Restore Restore the folder, delete the catalogue, run the rebuild command, and confirm a sample of photos return with their adjustments. That is the drill, and it is the same procedure as recovering from a corrupted database. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Cache size against its ceiling, the count of photos marked missing, and the count of files the decoder rejected. A library slowly filling with missing rows means something is moving files behind your back. ## Incident checklist A colour cast is a pipeline stage, and the recorded stage order plus the pipeline version narrow it to one. An unopenable catalogue is not an incident here, it is a rebuild, which is the reason the sidecars exist. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A source folder checksum is confirmed unchanged after a full session - [ ] A catalogue rebuild from the folder alone is confirmed to restore edits - [ ] An embedded ICC profile is confirmed by reading the exported file - [ ] Slider interaction on the largest test file is confirmed and the rate recorded - [ ] Removing a photo is confirmed never to delete a file ## Launch constraint Do not market omitted ON1 Photo RAW capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. The photo folder. Opened read-only apart from sidecars written beside each file. LIBRARY_PATH=~/Pictures/raw # Required. The index. Deletable, because rebuild reconstructs it from the folder. CATALOGUE_PATH=~/.local/share/rawedit/catalogue.db # Required. Proxy renders, keyed by pipeline version and adjustment hash. CACHE_DIR=~/.cache/rawedit/renders # Required. Render caches grow faster than photo libraries do. Evict by last use. CACHE_MAX_GB=20 # Required. The space adjustments happen in, recorded with every render. WORKING_PROFILE=/usr/share/color/icc/AdobeRGB1998.icc # Required. Embedded in every export and verified by reading it back. OUTPUT_PROFILE=/usr/share/color/icc/sRGB.icc # Required. Written into every sidecar. Bumping it invalidates the render cache. PIPELINE_VERSION=3
# ON1 Photo RAW · indie build 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-day**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, decoding and rendering in Rust | the pixel work needs a real language and the interface does not | | Decoding | A maintained RAW decoding library, not your own parser | camera formats are a decade of undocumented variation each | | Colour | ICC profiles as files on disk, applied in a named stage order | the pipeline has to be inspectable or you cannot debug a colour cast | | Catalogue | SQLite, treated as a rebuildable index | the sidecars beside the photos are the record | | Sidecars | One versioned file per photo, written atomically beside the original | an edit that only exists in a database is an edit waiting to be lost | | Cache | Rendered proxies keyed by pipeline version and adjustment hash | a pipeline change must invalidate, not serve stale pixels | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Builds the interface half of the desktop app. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Rust toolchain and the Tauri prerequisites** · free - 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. - Verify: cargo --version prints a version and a bare Tauri app builds - [ ] **A maintained RAW decoding library** · free - 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 - [ ] **RAW files from at least two different cameras** · free - 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 - [ ] **ICC profiles for your working and output spaces** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Generative editing of any kind. - Support for cameras you do not own. - Cloud sync and mobile apps. - Printing and soft proofing. - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases If one of those is essential to you, that is the reason to keep paying for ON1 Photo RAW, and the README should say so rather than pretend.
# Build brief · ON1 Photo RAW The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local RAW editor that keeps every edit in a sidecar and never writes to your originals to replace ON1 Photo RAW. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is read-only, for the whole life of the program. Open it read-only, never write to it, never rename it, never move it. Every adjustment lives in a sidecar beside the photo and in the catalogue, and a render is a function of the original plus the sidecar plus a recorded pipeline version. A test asserts that a checksum of the source folder is identical before and after a full editing session, and that test is not allowed to be deleted. The second half of the same rule: the catalogue is an index, not the record. If the database is the only place an edit exists, a corrupted file loses years of work, which is how people actually lose photo libraries. Write the sidecar on every save and prove the catalogue can be rebuilt from the photo folder alone, with no database, in the test suite. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, decoding and rendering in Rust · the pixel work needs a real language and the interface does not - Decoding: A maintained RAW decoding library, not your own parser · camera formats are a decade of undocumented variation each - Colour: ICC profiles as files on disk, applied in a named stage order · the pipeline has to be inspectable or you cannot debug a colour cast - Catalogue: SQLite, treated as a rebuildable index · the sidecars beside the photos are the record - Sidecars: One versioned file per photo, written atomically beside the original · an edit that only exists in a database is an edit waiting to be lost - Cache: Rendered proxies keyed by pipeline version and adjustment hash · a pipeline change must invalidate, not serve stale pixels ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading a RAW file without touching it Build: Import a folder through a maintained decoder. Read metadata, compute a content hash, and show the embedded camera preview for a first paint while the real render is queued. Open every source file read-only and write nothing into the photo folder except sidecars. Trap: 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. Done when: A checksum of every file in the source folder is identical before and after an import and a full editing session, asserted in the suite. Moving a photo to a different folder relinks it to its existing edits by hash instead of creating a second entry. Orientation is applied exactly once, since the embedded preview is already rotated and the sensor data is not, tested with a portrait frame. A file the decoder cannot read is listed as unsupported by name rather than silently skipped. Do not build yet: No adjustments, no render pipeline, no export. ### Phase 2 · A colour pipeline you can explain Build: Demosaic, white balance, camera matrix into a working space, tone and colour adjustments, then convert to the output profile. Name each stage, record a pipeline version, and embed the output ICC profile in every file you write. Trap: 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. Done when: Every exported file carries an embedded ICC profile, read back off disk and compared in the suite. Rendering the same sidecar twice at the same pipeline version produces identical bytes. Changing the pipeline version invalidates cached renders instead of serving stale ones. A camera with no matrix available falls back to a documented generic matrix and the interface says which one was used. Do not build yet: No adjustment editing, caching strategy or export options yet. ### Phase 3 · Edits that are parameters, not pixels Build: Exposure, contrast, curves, white balance, crop, and one local adjustment through a simple mask. Store them as an ordered list in the sidecar, keep full history, and allow a set of adjustments to be copied to a selection or saved as a preset. Trap: 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. Done when: Reordering two adjustments changes the render, proving the order is real rather than decorative. Undo crosses a restart because history lives in the sidecar, tested by relaunching. Copying adjustments to twenty photos writes twenty sidecars and leaves twenty originals untouched. Resetting a photo restores the unedited render exactly, compared byte for byte. Do not build yet: No render performance work or export options yet. ### Phase 4 · Fast enough to drag a slider Build: Render at the displayed size for interaction, cancel a render when its input changes, and run the queue off the interface thread. Offer a one to one view of a crop region for the adjustments that only exist at full resolution, and render full size on export. Trap: 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. Done when: Dragging a slider on a forty megapixel file updates at an interactive rate on the proxy, measured and recorded in the README. A render whose input changed is cancelled rather than completed and thrown away, asserted by counting completions. Sharpening and noise reduction are inspected at one to one on a crop, not on a downscaled proxy. Export renders at full resolution regardless of what the screen was showing. Do not build yet: No export options, catalogue rebuild or backup yet. ### Phase 5 · A library you can walk away with Build: Write the sidecar beside each photo atomically on every save. Provide a rebuild command that reconstructs the whole catalogue from a folder with no database present. Export with the chosen profile, the applied orientation and a stated metadata policy, and never offer to delete an original. Trap: A delete button that removes the file from disk instead of from the catalogue is one misread dialogue away from losing a photograph that has no other copy, and photo software has been doing this to people for thirty years. Remove from the catalogue only, name the button accordingly, and leave deleting files to the file manager. Done when: The catalogue rebuilds from the photo folder alone with no database present and every edit returns, exercised in the suite. A crash during a sidecar write leaves the previous sidecar intact, tested by killing the process mid-write. Export states which profile, which metadata and which pipeline version produced the file. Removing a photo removes a catalogue row and never a file, asserted by a checksum of the folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Generative fill, sky replacement and subject removal, which are a model pipeline and a licensing question rather than an afternoon. - Supporting every camera ever made, since each new sensor is its own colour matrix and its own decoder quirk; support the cameras you own and say so. - Cloud sync and mobile apps, which turn a local tool into a service with other people's photographs in it. - Printing with soft proofing, which needs printer profiles, rendering intents and a calibrated display to mean anything. ### README must contain - That originals are opened read-only and a test asserts the source folder is unchanged after a session - That edits live in sidecars beside each photo and the catalogue can be rebuilt from the folder alone - Which cameras have a real colour matrix and what the generic fallback does to the result - The measured interaction rate on a named file size, so a slow machine is a known quantity rather than a surprise - That removing a photo removes a catalogue row and never deletes a file
# Agent instructions · ON1 Photo RAW indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Tauri 2 with React and TypeScript, decoding and rendering in Rust, A maintained RAW decoding library, not your own parser, ICC profiles as files on disk, applied in a named stage order, SQLite, treated as a rebuildable index, One versioned file per photo, written atomically beside the original, Rendered proxies keyed by pipeline version and adjustment hash. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · ON1 Photo RAW 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Reading a RAW file without touching it Photos are catalogued and identified while the source folder stays byte for byte identical. ### Steps 1. Open originals read-only and assert it A checksum test over the source folder is the cheapest guarantee in this build. Files: `src-tauri/src/import.rs`, `src-tauri/tests/originals_untouched.rs` 2. Identify photos by content hash Paths change, files do not, and the hash is what survives a reorganised library. Files: `src-tauri/src/catalogue.rs` 3. Apply orientation once, from the metadata The embedded preview and the sensor data disagree about which way is up. Files: `src-tauri/src/orientation.rs`, `src/components/Grid.tsx` ### Done when - [ ] The source folder checksum is unchanged after a session - [ ] A moved photo relinks by hash - [ ] Portrait frames are rotated exactly once - [ ] Unsupported files are listed, not skipped ### 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. ## Phase 2 · A colour pipeline you can explain Every render runs named stages in a fixed order and every export carries a profile. ### Steps 1. Fix the stage order and record it A colour cast is only debuggable if you can say which stage introduced it. Files: `src-tauri/src/pipeline.rs`, `docs/pipeline.md` 2. Embed the output profile, then verify it Read the profile back off the written file; encoders drop it more often than you expect. Files: `src-tauri/src/export.rs`, `src-tauri/tests/icc_embedded.rs` 3. Version the pipeline and key the cache on it Otherwise a render improvement shows up as half the library looking different. Files: `src-tauri/src/cache.rs` ### Done when - [ ] Exports carry an embedded profile, verified from the file - [ ] The same sidecar renders to identical bytes - [ ] A pipeline version change invalidates the cache - [ ] A missing camera matrix falls back visibly ### 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. ## Phase 3 · Edits that are parameters, not pixels Adjustments are an ordered list that can be reordered, undone and copied to another photo. ### Steps 1. Store an ordered list, never a bitmap The order is part of the meaning and the bitmap is only ever a cache. Files: `src-tauri/src/adjustments.rs` 2. Keep history in the sidecar Undo that dies at quit is not undo, it is a session. Files: `src-tauri/src/sidecar.rs` 3. Copy and preset from the same structure One representation for one edit, applied to one photo or two hundred. Files: `src/components/Presets.tsx` ### Done when - [ ] Reordering adjustments changes the render - [ ] Undo survives a restart - [ ] A copy to many photos leaves originals untouched - [ ] Reset restores the unedited render exactly ### 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. ## Phase 4 · Fast enough to drag a slider Interaction runs on a proxy and the full resolution render happens only when it must. ### Steps 1. Render at display size for interaction The proxy is what makes the sliders feel like sliders. Files: `src-tauri/src/render_queue.rs` 2. Cancel superseded renders A queue that finishes work nobody wants is a queue that never catches up. Files: `src-tauri/src/render_queue.rs`, `src-tauri/tests/cancel.rs` 3. Give detail adjustments a one to one view Judging noise reduction on a downscaled preview is judging nothing. Files: `src/components/DetailView.tsx` ### Done when - [ ] Slider drags stay interactive on a large file - [ ] Superseded renders are cancelled - [ ] Detail work happens at one to one - [ ] Export always renders at full resolution ### 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. ## Not in this build - Generative editing of any kind. - Support for cameras you do not own. - Cloud sync and mobile apps. - Printing and soft proofing. ## After v1, if you want it - Lens correction from a published profile database, once the pipeline order is settled - A culling mode with flags and compare, which is where most editing sessions actually start - Batch export with per-target profiles, reusing the render queue without changing it
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The photo folder. Opened read-only apart from sidecars written beside each file. LIBRARY_PATH=~/Pictures/raw # Required. The index. Deletable, because rebuild reconstructs it from the folder. CATALOGUE_PATH=~/.local/share/rawedit/catalogue.db # Required. Proxy renders, keyed by pipeline version and adjustment hash. CACHE_DIR=~/.cache/rawedit/renders # Required. Render caches grow faster than photo libraries do. Evict by last use. CACHE_MAX_GB=20 # Required. The space adjustments happen in, recorded with every render. WORKING_PROFILE=/usr/share/color/icc/AdobeRGB1998.icc # Required. Embedded in every export and verified by reading it back. OUTPUT_PROFILE=/usr/share/color/icc/sRGB.icc # Required. Written into every sidecar. Bumping it invalidates the render cache. PIPELINE_VERSION=3
# ON1 Photo RAW · product brief ## Problem The visible photo editing suite loop is buildable, but a credible replacement needs more than the first screen. ON1 Photo RAW earns its keep through editor polish, assets, algorithms, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A RAW editor whose originals are provably untouched, whose colour pipeline can be explained stage by stage, whose edits survive the database being deleted, and which stays interactive on the largest file you own. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - desktop development machine - Node.js 22 and Rust toolchain - sample user-owned assets ## Explicit non-goals for v1 - Generative editing of any kind. - Support for cameras you do not own. - Cloud sync and mobile apps. - Printing and soft proofing. - cloud collaboration and mobile apps - high-fidelity color, format, and export handling - pixel-perfect professional tooling - large template and asset libraries - color-management edge cases ## Success criteria - A source folder checksum is confirmed unchanged after a full session - A catalogue rebuild from the folder alone is confirmed to restore edits - An embedded ICC profile is confirmed by reading the exported file - Slider interaction on the largest test file is confirmed and the rate recorded - Removing a photo is confirmed never to delete a file
# Build brief · ON1 Photo RAW The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local RAW editor that keeps every edit in a sidecar and never writes to your originals to replace ON1 Photo RAW. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero The original file is read-only, for the whole life of the program. Open it read-only, never write to it, never rename it, never move it. Every adjustment lives in a sidecar beside the photo and in the catalogue, and a render is a function of the original plus the sidecar plus a recorded pipeline version. A test asserts that a checksum of the source folder is identical before and after a full editing session, and that test is not allowed to be deleted. The second half of the same rule: the catalogue is an index, not the record. If the database is the only place an edit exists, a corrupted file loses years of work, which is how people actually lose photo libraries. Write the sidecar on every save and prove the catalogue can be rebuilt from the photo folder alone, with no database, in the test suite. ### Stack (fixed, do not substitute) - Shell: Tauri 2 with React and TypeScript, decoding and rendering in Rust · the pixel work needs a real language and the interface does not - Decoding: A maintained RAW decoding library, not your own parser · camera formats are a decade of undocumented variation each - Colour: ICC profiles as files on disk, applied in a named stage order · the pipeline has to be inspectable or you cannot debug a colour cast - Catalogue: SQLite, treated as a rebuildable index · the sidecars beside the photos are the record - Sidecars: One versioned file per photo, written atomically beside the original · an edit that only exists in a database is an edit waiting to be lost - Cache: Rendered proxies keyed by pipeline version and adjustment hash · a pipeline change must invalidate, not serve stale pixels ### Data model (create this before Phase 1) - 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. ### Phase 1 · Reading a RAW file without touching it Build: Import a folder through a maintained decoder. Read metadata, compute a content hash, and show the embedded camera preview for a first paint while the real render is queued. Open every source file read-only and write nothing into the photo folder except sidecars. Trap: 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. Done when: A checksum of every file in the source folder is identical before and after an import and a full editing session, asserted in the suite. Moving a photo to a different folder relinks it to its existing edits by hash instead of creating a second entry. Orientation is applied exactly once, since the embedded preview is already rotated and the sensor data is not, tested with a portrait frame. A file the decoder cannot read is listed as unsupported by name rather than silently skipped. Do not build yet: No adjustments, no render pipeline, no export. ### Phase 2 · A colour pipeline you can explain Build: Demosaic, white balance, camera matrix into a working space, tone and colour adjustments, then convert to the output profile. Name each stage, record a pipeline version, and embed the output ICC profile in every file you write. Trap: 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. Done when: Every exported file carries an embedded ICC profile, read back off disk and compared in the suite. Rendering the same sidecar twice at the same pipeline version produces identical bytes. Changing the pipeline version invalidates cached renders instead of serving stale ones. A camera with no matrix available falls back to a documented generic matrix and the interface says which one was used. Do not build yet: No adjustment editing, caching strategy or export options yet. ### Phase 3 · Edits that are parameters, not pixels Build: Exposure, contrast, curves, white balance, crop, and one local adjustment through a simple mask. Store them as an ordered list in the sidecar, keep full history, and allow a set of adjustments to be copied to a selection or saved as a preset. Trap: 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. Done when: Reordering two adjustments changes the render, proving the order is real rather than decorative. Undo crosses a restart because history lives in the sidecar, tested by relaunching. Copying adjustments to twenty photos writes twenty sidecars and leaves twenty originals untouched. Resetting a photo restores the unedited render exactly, compared byte for byte. Do not build yet: No render performance work or export options yet. ### Phase 4 · Fast enough to drag a slider Build: Render at the displayed size for interaction, cancel a render when its input changes, and run the queue off the interface thread. Offer a one to one view of a crop region for the adjustments that only exist at full resolution, and render full size on export. Trap: 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. Done when: Dragging a slider on a forty megapixel file updates at an interactive rate on the proxy, measured and recorded in the README. A render whose input changed is cancelled rather than completed and thrown away, asserted by counting completions. Sharpening and noise reduction are inspected at one to one on a crop, not on a downscaled proxy. Export renders at full resolution regardless of what the screen was showing. Do not build yet: No export options, catalogue rebuild or backup yet. ### Phase 5 · A library you can walk away with Build: Write the sidecar beside each photo atomically on every save. Provide a rebuild command that reconstructs the whole catalogue from a folder with no database present. Export with the chosen profile, the applied orientation and a stated metadata policy, and never offer to delete an original. Trap: A delete button that removes the file from disk instead of from the catalogue is one misread dialogue away from losing a photograph that has no other copy, and photo software has been doing this to people for thirty years. Remove from the catalogue only, name the button accordingly, and leave deleting files to the file manager. Done when: The catalogue rebuilds from the photo folder alone with no database present and every edit returns, exercised in the suite. A crash during a sidecar write leaves the previous sidecar intact, tested by killing the process mid-write. Export states which profile, which metadata and which pipeline version produced the file. Removing a photo removes a catalogue row and never a file, asserted by a checksum of the folder. Do not build yet: Nothing after this. ### Out of scope (and why) - Generative fill, sky replacement and subject removal, which are a model pipeline and a licensing question rather than an afternoon. - Supporting every camera ever made, since each new sensor is its own colour matrix and its own decoder quirk; support the cameras you own and say so. - Cloud sync and mobile apps, which turn a local tool into a service with other people's photographs in it. - Printing with soft proofing, which needs printer profiles, rendering intents and a calibrated display to mean anything. ### README must contain - That originals are opened read-only and a test asserts the source folder is unchanged after a session - That edits live in sidecars beside each photo and the catalogue can be rebuilt from the folder alone - Which cameras have a real colour matrix and what the generic fallback does to the result - The measured interaction rate on a named file size, so a slow machine is a known quantity rather than a surprise - That removing a photo removes a catalogue row and never deletes a file
# Architecture · ON1 Photo RAW ## Stack | Part | Choice | Why | | --- | --- | --- | | Shell | Tauri 2 with React and TypeScript, decoding and rendering in Rust | the pixel work needs a real language and the interface does not | | Decoding | A maintained RAW decoding library, not your own parser | camera formats are a decade of undocumented variation each | | Colour | ICC profiles as files on disk, applied in a named stage order | the pipeline has to be inspectable or you cannot debug a colour cast | | Catalogue | SQLite, treated as a rebuildable index | the sidecars beside the photos are the record | | Sidecars | One versioned file per photo, written atomically beside the original | an edit that only exists in a database is an edit waiting to be lost | | Cache | Rendered proxies keyed by pipeline version and adjustment hash | a pipeline change must invalidate, not serve stale pixels | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Import | Decoding, hashing and orientation. | Any decoder, provided originals stay read-only. | | Pipeline | Named stages in a fixed order with a version. | Any implementation, provided the stage order stays inspectable and versioned. | | Sidecars | Atomic writes beside each photo. | Any format, provided it is versioned and human-readable. | | Render queue | Proxy rendering and cancellation. | Any scheduler, provided superseded work is cancelled. | | Export | Full resolution output with an embedded profile. | Any encoder, provided the profile is verified on the written file. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `LIBRARY_PATH` · required · The photo folder. Opened read-only apart from sidecars written beside each file. - `CATALOGUE_PATH` · required · The index. Deletable, because rebuild reconstructs it from the folder. - `CACHE_DIR` · required · Proxy renders, keyed by pipeline version and adjustment hash. - `CACHE_MAX_GB` · required · Render caches grow faster than photo libraries do. Evict by last use. - `WORKING_PROFILE` · required · The space adjustments happen in, recorded with every render. - `OUTPUT_PROFILE` · required · Embedded in every export and verified by reading it back. - `PIPELINE_VERSION` · required · Written into every sidecar. Bumping it invalidates the render cache. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · ON1 Photo RAW product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Tauri 2 with React and TypeScript, decoding and rendering in Rust, A maintained RAW decoding library, not your own parser, ICC profiles as files on disk, applied in a named stage order, SQLite, treated as a rebuildable index, One versioned file per photo, written atomically beside the original, Rendered proxies keyed by pipeline version and adjustment hash. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - A delete button that removes the file from disk instead of from the catalogue is one misread dialogue away from losing a photograph that has no other copy, and photo software has been doing this to people for thirty years. Remove from the catalogue only, name the button accordingly, and leave deleting files to the file manager.
# Delivery milestones · ON1 Photo RAW Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Reading a RAW file without touching it Photos are catalogued and identified while the source folder stays byte for byte identical. ### Steps 1. Open originals read-only and assert it A checksum test over the source folder is the cheapest guarantee in this build. Files: `src-tauri/src/import.rs`, `src-tauri/tests/originals_untouched.rs` 2. Identify photos by content hash Paths change, files do not, and the hash is what survives a reorganised library. Files: `src-tauri/src/catalogue.rs` 3. Apply orientation once, from the metadata The embedded preview and the sensor data disagree about which way is up. Files: `src-tauri/src/orientation.rs`, `src/components/Grid.tsx` ### Done when - [ ] The source folder checksum is unchanged after a session - [ ] A moved photo relinks by hash - [ ] Portrait frames are rotated exactly once - [ ] Unsupported files are listed, not skipped ### 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. ## M2 · A colour pipeline you can explain Every render runs named stages in a fixed order and every export carries a profile. ### Steps 1. Fix the stage order and record it A colour cast is only debuggable if you can say which stage introduced it. Files: `src-tauri/src/pipeline.rs`, `docs/pipeline.md` 2. Embed the output profile, then verify it Read the profile back off the written file; encoders drop it more often than you expect. Files: `src-tauri/src/export.rs`, `src-tauri/tests/icc_embedded.rs` 3. Version the pipeline and key the cache on it Otherwise a render improvement shows up as half the library looking different. Files: `src-tauri/src/cache.rs` ### Done when - [ ] Exports carry an embedded profile, verified from the file - [ ] The same sidecar renders to identical bytes - [ ] A pipeline version change invalidates the cache - [ ] A missing camera matrix falls back visibly ### 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. ## M3 · Edits that are parameters, not pixels Adjustments are an ordered list that can be reordered, undone and copied to another photo. ### Steps 1. Store an ordered list, never a bitmap The order is part of the meaning and the bitmap is only ever a cache. Files: `src-tauri/src/adjustments.rs` 2. Keep history in the sidecar Undo that dies at quit is not undo, it is a session. Files: `src-tauri/src/sidecar.rs` 3. Copy and preset from the same structure One representation for one edit, applied to one photo or two hundred. Files: `src/components/Presets.tsx` ### Done when - [ ] Reordering adjustments changes the render - [ ] Undo survives a restart - [ ] A copy to many photos leaves originals untouched - [ ] Reset restores the unedited render exactly ### 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. ## M4 · Fast enough to drag a slider Interaction runs on a proxy and the full resolution render happens only when it must. ### Steps 1. Render at display size for interaction The proxy is what makes the sliders feel like sliders. Files: `src-tauri/src/render_queue.rs` 2. Cancel superseded renders A queue that finishes work nobody wants is a queue that never catches up. Files: `src-tauri/src/render_queue.rs`, `src-tauri/tests/cancel.rs` 3. Give detail adjustments a one to one view Judging noise reduction on a downscaled preview is judging nothing. Files: `src/components/DetailView.tsx` ### Done when - [ ] Slider drags stay interactive on a large file - [ ] Superseded renders are cancelled - [ ] Detail work happens at one to one - [ ] Export always renders at full resolution ### 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. ## M5 · A library you can walk away with (production only) The photo folder alone is enough to reconstruct everything, and export is honest about what it wrote. ### Steps 1. Rebuild the catalogue from the folder The test that proves the database was only ever an index. Files: `src-tauri/src/rebuild.rs`, `src-tauri/tests/rebuild.rs` 2. Write sidecars atomically Temporary file, flush, replace. A truncated sidecar is a lost edit. Files: `src-tauri/src/sidecar.rs` 3. Never delete an original Remove from the catalogue, say so on the button, and let the file manager do the rest. Files: `src/components/RemoveDialog.tsx` ### Done when - [ ] A catalogue rebuild from the folder restores every edit - [ ] A crash mid-write leaves the previous sidecar intact - [ ] Export records profile, metadata and pipeline version - [ ] Removing a photo never deletes a file ### Watch out - A delete button that removes the file from disk instead of from the catalogue is one misread dialogue away from losing a photograph that has no other copy, and photo software has been doing this to people for thirty years. Remove from the catalogue only, name the button accordingly, and leave deleting files to the file manager.
# Operations · ON1 Photo RAW ## Backup Back up the photo folder, sidecars included, and treat the catalogue and render cache as disposable. If your backup covers the database but not the sidecars, you have backed up the index and not the work. ## Restore Restore the folder, delete the catalogue, run the rebuild command, and confirm a sample of photos return with their adjustments. That is the drill, and it is the same procedure as recovering from a corrupted database. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Cache size against its ceiling, the count of photos marked missing, and the count of files the decoder rejected. A library slowly filling with missing rows means something is moving files behind your back. ## Incident checklist A colour cast is a pipeline stage, and the recorded stage order plus the pipeline version narrow it to one. An unopenable catalogue is not an incident here, it is a rebuild, which is the reason the sidecars exist. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] A source folder checksum is confirmed unchanged after a full session - [ ] A catalogue rebuild from the folder alone is confirmed to restore edits - [ ] An embedded ICC profile is confirmed by reading the exported file - [ ] Slider interaction on the largest test file is confirmed and the rate recorded - [ ] Removing a photo is confirmed never to delete a file ## Launch constraint Do not market omitted ON1 Photo RAW capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. The photo folder. Opened read-only apart from sidecars written beside each file. LIBRARY_PATH=~/Pictures/raw # Required. The index. Deletable, because rebuild reconstructs it from the folder. CATALOGUE_PATH=~/.local/share/rawedit/catalogue.db # Required. Proxy renders, keyed by pipeline version and adjustment hash. CACHE_DIR=~/.cache/rawedit/renders # Required. Render caches grow faster than photo libraries do. Evict by last use. CACHE_MAX_GB=20 # Required. The space adjustments happen in, recorded with every render. WORKING_PROFILE=/usr/share/color/icc/AdobeRGB1998.icc # Required. Embedded in every export and verified by reading it back. OUTPUT_PROFILE=/usr/share/color/icc/sRGB.icc # Required. Written into every sidecar. Bumping it invalidates the render cache. PIPELINE_VERSION=3
$ choose a build depth, inspect the files, then open the complete pack in your agent
ON1 Photo RAW: Creative users pay for speed, output fidelity, nondestructive editing, and years of edge-case work hidden behind direct manipulation.
xcloud collaboration and mobile apps
xhigh-fidelity color, format, and export handling
xpixel-perfect professional tooling
xlarge template and asset libraries
xcolor-management edge cases
Don't feel like building it? These folks already made it free.
all 3 free alternatives to ON1 Photo RAW →· no votes, no pay-to-list · just what's real
ON1 Photo RAW pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| photo raw 2026.5 | custom | — | Perpetual licence for 2 computers; updates within the 2026 release line. |
| photo raw max 2026.5, perpetual | custom | — | Perpetual MAX licence for 3 computers; exact current public USD checkout price was not exposed. |
| photo raw max, subscription | custom | — | 200 GB cloud storage, future versions and up to 5 installs; exact public USD price was not exposed. |
| photo studio, subscription | custom | — | 1 TB cloud storage, all ON1 titles/plugins, future releases and up to 5 installs; exact public USD price was not exposed. |
free tierno free tier; a trial is offered
billingone-time perpetual licences and recurring subscriptions are both sold
hidden costsThe one-time licence covers the current major-year release, not future major versions. Future upgrades, auto-upgrade programmes and cloud storage beyond the perpetual product are additional costs.
verified 2026-08-14 · source ↗
Vibecode ON1 Photo RAW
Kinda. The core of ON1 Photo RAW is buildable in a weekend with the prompt on this page, but there are real gaps: cloud collaboration and mobile apps, high-fidelity color, format, and export handling. Read the honest list above before committing.
How much does ON1 Photo RAW cost?
ON1 Photo RAW's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing ON1 Photo RAW?
Honestly: cloud collaboration and mobile apps; high-fidelity color, format, and export handling; pixel-perfect professional tooling; large template and asset libraries; color-management edge cases. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to ON1 Photo RAW?
Yes: darktable (Catalog, RAW development, masks, effects, lens profiles, and batch export in one mature desktop app; expect knobs, many knobs.) RapidRAW (A friendlier new RAW editor with a library, ratings, culling, masks, presets, and denoise; not yet as deep or proven.) digiKam (A formidable local catalog with RAW import, ratings, tags, duplicates, batch tools, and an editor; prettier workflows were not invited.) All 3 curated free alternatives are at vibecodeit.com/on1-photo-raw/alternatives. The prompt is for when you want it exactly your way.