Build Polarr

KINDAback to the verdict

0%0 of 16 items done

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

A local photo editor whose hard problems are keeping the original file untouched while edits live as a replayable recipe, choosing a colour pipeline deliberately instead of inheriting the defaults, and making the preview and the export come out of the same code.

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

ShellTauri 2 with a React and TypeScript front endPixelsCanvas 2D with colour space and colour type set explicitly at context creationCatalogueSQLite holding references and recipes, never image bytesRecipesA versioned JSON document per image, written atomically and mirrored to a sidecarRenderingOne operation list replayed at preview size and at full size by the same code

Before step 1

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

  1. installfree

    Why Tauri needs both, and discovering that halfway through the first render is a bad afternoon.

    Get it nodejs.org and rustup.rs, then the Tauri prerequisites for your platform. open ↗

    Verify A fresh Tauri template builds and opens a window

  2. have readyfree

    Why Colour handling is untestable without at least one wide gamut file and one plain sRGB file whose profiles you already know.

    Get it Export the same photograph twice from any camera application, once tagged Display P3 and once sRGB, and note which is which.

    Verify An image inspector reports a different profile for each file

  3. have readyfree

    Why Banding from an eight bit working chain is invisible on a photograph of leaves and obvious on a gradient, which is the point of having one.

    Get it Generate a wide, very gradual grey or sky gradient at full resolution and keep it in the test fixtures.

    Verify The gradient renders smoothly before any adjustment is applied

  4. decidefree

    Why Deciding what you convert after seeing which images look wrong means writing a policy that matches your bugs.

    Get it Write one paragraph: the working colour space, the colour type, what happens to a wide gamut import, and what the export writes.

    Verify The paragraph is in the README and the values it names are the ones in the code

Data model

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

- images(id, path, sha256, bytes, width, height, source_profile, orientation_tag, imported_at, missing_since)
- recipes(image_id, format_version, ops_json, updated_at) · the ordered operation list, which is the whole of the edit
- ops inside a recipe(ordinal, kind, params_json, mask_id, enabled) · order matters, so order is stored rather than implied
- masks(id, image_id, kind, geometry_json, feather, inverted) · geometry and parameters, never a saved pixel buffer
- history(image_id, ordinal, ops_json, at) · undo that survives closing the application
- exports(id, image_id, recipe_version, path, format, colour_space, bytes, at) · what was rendered, from which recipe, in which space
- Nothing here can be destroyed by editing a photograph, because nothing here is the photograph.

Environment variables

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

VariableNeededExampleWhere the value comes from
CATALOGUE_PATHrequired~/.local/share/photoedit/catalogue.dbReferences and recipes. Rebuildable from sidecars, which is the whole idea.
SIDECAR_SUFFIXrequired.recipe.jsonWritten beside each image. This is the durable copy of the edit.
WORKING_COLOUR_SPACErequireddisplay-p3Passed to the context at creation. Named in the interface and the README.
WORKING_COLOUR_TYPErequiredfloat16The other half of the same decision. Check what your runtime actually supports.
PREVIEW_MAX_PIXELSrequired4000000Preview render budget. The export ignores it and uses the same code.
HISTORY_KEEPrequired200Undo steps retained per image across restarts.

The build, in order

  1. A catalogue that only points at your photographs

    Importing records a reference and a checksum and writes nothing to the original.

    1. The checksum is how you tell moved from edited from gone.

      Files src/import.tstests/import-readonly.test.ts

    2. A thumbnail cache you can delete without losing anything is the right kind of cache.

      Files src/thumbnails.ts

    3. Run the suite against a read only library directory so a stray write fails loudly.

      Files src/files.tstests/import-readonly.test.ts

    done when · tick each as it passes
    watch out
    • Importing by copying every file into an application folder doubles the disk use of a photo library and leaves two copies that drift apart, while importing by path with no checksum turns a moved or re-saved file into a blank frame with no explanation. Store the path and the checksum, and show a missing file as missing with the path it had.
  2. Colour you can name

    The pipeline states its colour space instead of inheriting one.

    1. Colour space and colour type are arguments, not defaults you inherit.

      Files src/render/context.ts

    2. You cannot convert honestly from a profile you did not record.

      Files src/import.tssrc/render/colour.ts

    3. Eight bits per channel through six operations is where banding appears.

      Files tests/gradient-banding.test.ts

    done when · tick each as it passes
    watch out
    • Leaving the context settings alone converts every wide gamut photograph into sRGB before the first adjustment, so the saturated reds somebody bought the camera for are clipped silently on a display that could have shown them. Set the colour space and colour type at creation, keep the source profile on the image row, and name the working space in the interface.
  3. An edit that is a list, not a bitmap

    Adjustments are ordered parameters replayed identically at any resolution.

    1. The export path is the preview path with a different size argument.

      Files src/render/pipeline.tstests/preview-matches-export.test.ts

    2. Undo that dies at quit is a worse undo than none, because people rely on it.

      Files src/history.ts

    3. Turning an adjustment off is not the same as throwing it away.

      Files src/recipe.ts

    done when · tick each as it passes
    watch out
    • Rendering the preview with a fast approximation and the export with a separate proper path produces a file that does not match what the person approved, and they find out after it is published. Keep one implementation and two resolutions, with a test that renders one recipe at two sizes and compares the downscaled results within a stated tolerance.
  4. Masks that are geometry

    A local adjustment survives a crop, a re-export and a format change.

    1. So a crop is a coordinate change rather than a rebuild.

      Files src/masks.ts

    2. Whatever resolution this render needs, and nothing persisted.

      Files src/render/masks.tstests/mask-edges.test.ts

    3. Two operations sharing one mask should share one mask.

      Files src/recipe.ts

    done when · tick each as it passes
    watch out
    • Saving a mask as a rasterised alpha buffer at preview resolution gives an export with visibly blocky edges and quietly invalidates every mask the first time the image is cropped or straightened. Keep the geometry in original image coordinates and rasterise at whatever resolution the current render needs.
what this build does not replace
after v1, if you want it

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