Build BeFunky

KINDAback to the verdict

0%0 of 17 items done

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

A non-destructive local photo editor whose hard problems are keeping the original file untouched, keeping colour honest through a canvas that is sRGB by default, and making a screen-sized preview agree with a full-resolution export.

estimated effort weekendthe files for this build are in the project pack

ShellTauri 2 with a React and TypeScript front endRenderingCanvas 2D for preview, the same operation list replayed at full size for exportDocumentA JSON edit stack stored beside the image, never inside itColourOne declared working space, recorded per project, conversions loggedStorageSQLite for the project index, files on disk for everything large

Before step 1

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

  1. installfree

    Why Builds the front end and runs the tests.

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

    Verify node --version prints v22 or higher

  2. installfree

    Why Tauri 2 needs it, and the file handling that keeps the original untouched lives on that side.

    Get it rustup.rs, then the platform build dependencies Tauri lists for your operating system. open ↗

    Verify cargo --version prints a version and a Tauri dev build starts

  3. have readyfree

    Why One wide-gamut, one with location metadata, one very large. Each exists to make a specific test fail before it passes.

    Get it Take them, or export them from your own library. Do not use stock images with unclear terms.

    Verify One has an embedded non-sRGB profile, one has GPS tags, one exceeds your pixel ceiling

  4. have readyfree

    Why Text rendering references a file, not a family name, so you need a face whose licence permits shipping it beside the project.

    Get it An open licensed family from a font repository, with the licence file kept next to it.

    Verify The licence permits redistribution and the file loads in the renderer

  5. decidefree

    Why Whether you convert wide-gamut imports to sRGB or refuse them is a decision that changes what users see, and deciding it after the first complaint means choosing whatever is easiest.

    Get it Write one paragraph: the working space, what happens to a non-matching embedded profile, and what the user is told.

    Verify The paragraph is in the README and a test asserts the behaviour it describes

Data model

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

- projects(id, name, source_path, source_sha256, source_width, source_height, created_at, opened_at)
- source_profile(project_id, declared_space, embedded_profile_name, converted_to, conversion_note) · what the file claimed and what you did about it
- ops(project_id, ordinal, kind, params_json, enabled, created_at) · the edit stack, which IS the document
- layers(project_id, id, kind, z, transform_json, asset_id, text_json) · for collages and text
- assets(id, path, sha256, mime, width, height, bytes) · imported images, copied, never moved
- exports(id, project_id, path, format, quality, long_edge, ops_ordinal_max, metadata_policy, written_at)
- Nothing in this model stores pixels that an operation could recompute, which is why undo is free and a project file is a few kilobytes.

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
DATA_DIRrequired~/.local/share/photoeditProjects, the index and copied assets. Never the user's originals.
EXPORT_DIRrequired~/Pictures/exportsWhere new files are written. Exports never land beside the source.
WORKING_COLOUR_SPACErequiredsrgbRecorded in every project so an old project still renders the way it did.
MAX_IMAGE_PIXELSrequired60000000Above this the import is refused with the real dimensions in the message.
PREVIEW_MAX_EDGErequired2048The preview downscale. The render scale derived from it must reach every operation.
STRIP_METADATA_ON_EXPORTrequiredtrueDefault true. Turning it off lists exactly what will be kept.

The build, in order

  1. Open a photograph without changing it

    An imported file is read, hashed and left alone, and the colour decision is made explicitly rather than by accident.

    1. The one test that would have saved every photograph an editor has ever destroyed.

      Files src-tauri/src/import.rstests/source-untouched.test.ts

    2. Not to implement colour management, but so the app can say what it did.

      Files src/colour.tssrc/types/project.ts

    3. A forty megapixel file on an eight gigabyte machine is a decision, not a crash.

      Files src/import.ts

    done when · tick each as it passes
    watch out
    • Loading a photograph shot in a wide-gamut space into a default canvas converts it to sRGB on the way in, and the user sees a flatter, duller version of their own picture with no message anywhere explaining why. Read the embedded profile, declare the working space in the project, show the user which one they are editing in, and record the conversion so the export can say what happened.
  2. An edit stack, not a pixel buffer

    Every adjustment is an entry in an ordered list, so undo, reorder and disable are free.

    1. Input surface plus parameters out, no shared mutable bitmap.

      Files src/ops/index.tssrc/ops/exposure.ts

    2. Re-rendering ten operations on every slider drag is the reason people say this is slow.

      Files src/render/cache.ts

    3. Undo that is nearly right is undo that is wrong.

      Files tests/undo-roundtrip.test.ts

    done when · tick each as it passes
    watch out
    • Applying each adjustment destructively to a working bitmap is faster to write and traps you within a day, because undo then means keeping a full copy of the image for every step and a twelve step history on a large photograph exhausts memory. Keep the operation list and re-render from the source, cached at the last unchanged step.
  3. A preview that predicts the export

    What you see at screen size is what the full-resolution file contains.

    1. The parameter that is forgotten in exactly one operation, every time.

      Files src/render/scale.tssrc/ops/blur.ts

    2. The only check that catches the forgotten one.

      Files tests/scale-agreement.test.ts

    3. A full-resolution replay takes seconds and silence reads as a hang.

      Files src/export/progress.ts

    done when · tick each as it passes
    watch out
    • A blur radius of eight pixels on a preview downscaled to a quarter is a blur radius of thirty two pixels in the export, and so is every feather, stroke width, corner radius and vignette falloff in the stack, which is why the exported file looks nothing like the screen. Carry a render scale through every operation and multiply pixel-denominated parameters by it.
  4. Collage and text, where fonts break things

    A layout of several images plus text renders the same on export as on screen.

    1. A substituted face is a different layout.

      Files src/text/fonts.ts

    2. Line breaking is the thing that moves between preview and export.

      Files src/text/measure.tstests/text-scale.test.ts

    3. Same inputs, same layout, or the export is a surprise.

      Files src/layout/grid.ts

    done when · tick each as it passes
    watch out
    • Setting a font by family name means the export silently substitutes whatever the system has when the named face is missing, and the line breaks move, so a caption that fitted on screen overruns the frame in the file. Reference a font file the project owns and refuse to render with a substitute, saying which face is missing.
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.