Build Pixlr

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 layered editor whose hard problems are keeping pixel data out of the canvas so repeated edits do not degrade it, holding large images in bounded memory, and exporting files that still carry the orientation and colour information they arrived with.

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

ShellTauri 2 with the file dialogs and decode on the Rust sideInterfaceReact and TypeScript, canvas used only as a display surfacePixel storeFixed-size tiles of unpremultiplied RGBA in typed arraysHistoryAn operation log with inverse operations, not bitmap snapshotsCodecsDecode and encode in Rust, carrying orientation and any colour profile

Before step 1

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

  1. installfree

    Why Builds the interface half of the application.

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

    Verify node --version prints v22 or higher

  2. installfree

    Why Decoding and encoding happen here, which is the only place orientation and colour profiles are visible.

    Get it rustup.rs, then the platform packages listed in the Tauri prerequisites. open ↗

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

  3. have readyfree

    Why Orientation handling is the bug that ships, and it is only caught by a fixture per value rather than by the one photograph on your desk.

    Get it Generate them from one image with an exif tool, or use a published orientation test set.

    Verify All eight fixtures import upright in the suite

  4. have readyfree

    Why Memory behaviour is invisible until the document is bigger than the ceiling, and a sample image from a tutorial never is.

    Get it A full resolution photograph from a recent camera or phone, at least twenty megapixels.

    Verify It opens, edits and exports inside the stated memory ceiling

  5. decidefree

    Why Both are asserted by tests, and picking them after watching the app fall over means choosing whatever number happens to pass.

    Get it Write down the tile memory ceiling, the history byte cap and the autosave interval before Phase 2.

    Verify All three are in the configuration and named in tests

Data model

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

- documents(id, name, width, height, colour_space, format_version, created_at, updated_at)
- layers(id, document_id, ordinal, name, kind, opacity, blend_mode, visible, offset_x, offset_y)
- tiles(layer_id, tx, ty, bytes) · fixed size unpremultiplied RGBA, the only place pixel data lives
- operations(id, document_id, seq, kind, params_json, layer_id, at) · the undo history is this table
- originals(document_id, path, sha256, orientation, icc_profile, exif_json) · the imported file, kept byte for byte
- exports(id, document_id, at, format, settings_json, path, flattened, profile_written)
- Nothing here is a canvas element. The canvas is redrawn from tiles and is never a source of truth.

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
DOCUMENTS_DIRrequired~/.local/share/pixeleditor/documentsProject files. Versioned from the first release.
ORIGINALS_DIRrequired~/.local/share/pixeleditor/originalsImported files, kept byte for byte and never written to.
SCRATCH_DIRrequired~/.cache/pixeleditor/tilesEvicted tiles. Safe to delete when the app is not running.
TILE_MEMORY_MBrequired512The ceiling above which tiles are evicted to scratch.
HISTORY_MAX_MBrequired256Undo history cap in bytes, not in steps.
AUTOSAVE_SECONDSrequired30The most work a crash can cost. Written to a temporary file first.

The build, in order

  1. Pixels in buffers the browser cannot round

    A document is tiles of unpremultiplied RGBA and the canvas only ever receives them.

    1. This single decision is what keeps repeated edits from degrading the image.

      Files src/pixels/tiles.tssrc/pixels/tiles.test.ts

    2. Tiles go to the screen. Nothing comes back.

      Files src/render/compositor.ts

    3. A grep test is crude and it will catch the regression a year from now.

      Files tests/no-canvas-readback.test.ts

    done when · tick each as it passes
    watch out
    • Holding each layer in its own canvas and calling getImageData before every operation loses accuracy on every pixel that is not fully opaque, because the output bitmap is premultiplied and the round trip divides and multiplies in finite precision, so soft brush edges and feathered masks drift grey after a few dozen edits. Keep the pixels in your own buffers and treat the canvas as write only.
  2. Undo that does not cost a copy of the image

    Every edit is a record with an inverse, and history has a memory ceiling.

    1. The cost of an undo step should scale with what changed, not with the canvas size.

      Files src/history/operations.tssrc/history/patch.ts

    2. A count based cap means a hundred tiny steps and one enormous one look the same.

      Files src/history/limits.ts

    3. Reorder, delete and resize are where inverse operations are usually wrong.

      Files src/history/history.test.ts

    done when · tick each as it passes
    watch out
    • Snapshotting the whole document before each stroke looks fine on a small test image and then eats a hundred and ninety two megabytes per undo step on a six by four thousand pixel photograph, so the app dies on the fifth brush stroke of real work. Store only the tiles an operation touched, and bound the history in bytes.
  3. Selections, masks and a small set of tools that are correct

    Fewer tools than the original, each one composited properly.

    1. A path is a description; a mask is what the compositor can use.

      Files src/tools/selection.ts

    2. One place to get premultiplication right, not one per tool.

      Files src/render/compositor.tssrc/tools/brush.ts

    3. Dark fringes on cut-outs are invisible on white and obvious everywhere else.

      Files src/render/compositor.test.ts

    done when · tick each as it passes
    watch out
    • Compositing a feathered mask by multiplying colour channels that are already premultiplied darkens every soft edge towards black, and the error only shows on transparent backgrounds, which is where cut-outs are always used. Decide once that everything in the store is unpremultiplied, and do the multiply inside the compositor rather than in each tool.
  4. Files that arrive and leave intact

    Import keeps orientation and profile, and export says what it is about to lose.

    1. Orientation and profile are lost the moment the browser hands you a drawn image.

      Files src-tauri/src/decode.rssrc/import/import.ts

    2. Hash on import, hash again after export, assert they match.

      Files tests/original-untouched.test.ts

    3. Before the write, not in the README.

      Files src/export/warnings.ts

    done when · tick each as it passes
    watch out
    • Loading a phone photograph through an image element and drawing it to a canvas gives you whatever the browser decided about rotation, and exporting from the canvas writes a file with no profile and no orientation tag, so the result looks right in your app and sideways or washed out everywhere else. Decode where you can see the metadata, normalise once, and record what you applied.
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.