# Commands

> Every operation in Photo Slop is a JSON command — the same bus drives the UI, Python callbacks and AI agents.

**Site index:** [https://photoslop.2plot.dev/llms.txt](https://photoslop.2plot.dev/llms.txt) — every page on this site, as Markdown.  
**Network index:** [https://2plot.dev/llms.txt](https://2plot.dev/llms.txt) — The 2plot network; start here to discover sibling sites.  
**Sibling sites:** 14 more in The 2plot network — listed in the site index above.  
**Sitemap:** https://photoslop.2plot.dev/sitemap.xml  


---



## The command bus

Nothing in the editor mutates the canvas directly. A toolbar button, a keyboard shortcut, a Python callback and an AI agent all do the same thing: submit a command.

```json
{ "id": "any-unique-string", "type": "layer.addShape",
  "payload": { "shape": "rect", "width": 400, "height": 240, "fill": "#2f9e44" } }
```

From Python, set the component's `command` prop. The component runs it, **clears `command`**, and always answers on `commandResult`:

```json
{ "id": "any-unique-string", "type": "layer.addShape", "ok": true, "data": { "layerId": "l_gpcgpdlm" } }
```

On failure `ok` is `false` and `error` explains why. Rules worth knowing:

- Each `id` is executed **once**. Re-sending an `id` is ignored, so a re-rendered callback cannot double-apply an edit.
- Commands run **serially**, in the order they arrive, even when one awaits an image load.
- `batch` runs several commands as **one undo step** and stops at the first failure.
- Every committed change updates the `document`, `layers`, `selection`, `rev`, `canUndo` and `canRedo` props.

```python
from dash import Input, Output, State, callback
from dash_photo_canvas import ops

@callback(Output("pc-canvas", "command"), Input("add-logo", "n_clicks"), prevent_initial_call=True)
def add_logo(_):
    return ops.cmd("layer.addImage", src="https://example.com/logo.png", name="Logo", x=200, y=120)
```

`ops.cmd(type, **payload)` fills in a unique `id`. `ops.validate(command)` is a cheap structural check (an `id`, and a type that is one of the published agent commands); the component itself validates payload values and reports problems on `commandResult`.

## Reference

### Document and canvas

| Command | Payload | Notes |
|---------|---------|-------|
| `document.get` | — | Returns `{document, rev}` |
| `document.new` | `width?`, `height?`, `background?` | Discards the current document |
| `document.load` | `document` | Replace the whole document |
| `canvas.resize` | `width`, `height` | Max 16384 px per side. Layers keep their positions; the Document tab's *Scale layers* option is a `batch` of `canvas.resize` plus one `layer.update` (`x`, `y`, `scaleX`, `scaleY`) per layer |
| `canvas.background` | `color` | A CSS colour, or `transparent` |

### Layers

| Command | Payload | Notes |
|---------|---------|-------|
| `layer.addImage` | `src`, `fit?` (`auto` · `contain` · `cover` · `none`), `name?`, `x?`, `y?`, `scale?`, `angle?`, `opacity?`, `blend?`, `effects?`, `index?` | Returns `{layerId, natural, scale}`. `auto` shrinks only images larger than 90 % of the artboard |
| `layer.addText` | `text`, `fontFamily?`, `fontSize?`, `fill?`, `textAlign?`, `width?`, … | Returns `{layerId}` |
| `layer.addShape` | `shape` (`rect` · `ellipse` · `triangle` · `line`), `width?`, `height?`, `fill?`, `stroke?`, `strokeWidth?`, `rx?` | Returns `{layerId}`. `fill` (here, on text and in `layer.update`) is a CSS colour **or a CSS gradient string** — `linear-gradient(<angle>deg, <colour> <pct>%, …)` with 0deg = to top, or `radial-gradient(<colour> <pct>%, …)` — painted across the layer's box; the document keeps the string |
| `layer.update` | `layerId` or `layerIds`, any creation property, `live?` | `live: true` skips the undo entry — use it while dragging a slider | Text style fields: `fontFamily`, `fontSize`, `fontWeight`, `fontStyle`, `underline`, `linethrough`, `fill`, `stroke`, `strokeWidth`, `textAlign`, `lineHeight`; shape: `fill`, `stroke`, `strokeWidth`, `rx`, `width`, `height`. The editor's style bar is this command.
| `layer.remove` | `layerId?` / `layerIds?` | Omit ids to delete the selection |
| `layer.duplicate` | `layerId?` | Returns `{layerId}` |
| `layer.reorder` | `layerId?`, `to` | `front` · `back` · `forward` · `backward` · integer index (0 = bottom) |
| `layer.select` | `layerIds` | `[]` clears the selection |
| `layer.selectAll` | — | Selects every visible, unlocked layer |
| `layer.copy` · `layer.cut` · `layer.paste` | `layerIds?` · `layerIds?` · `offset?` | The engine's internal clipboard (what ⌘C / ⌘X / ⌘V use). Paste lands 24 px further each time |
| `layer.toggle` | `layerId?` / `layerIds?`, `prop`: `visible` · `locked` | Flip a flag on the targets. A locked layer keeps its place (no move, scale or rotate) and is skipped by the pointer, so clicks reach the layers under it; `layer.select` still selects it |
| `project.save` · `project.open` | `filename?` · — | Download the document JSON · open a file picker for one (`open` returns at once; the file loads as a `document.load`) |
| `layer.fit` | `layerId?`, `mode?`, `margin?` | `contain` · `cover` · `width` · `height` |
| `layer.crop` | `layerId?`, `x`, `y`, `width`, `height` or `reset: true` | Window in the image's natural pixels |
| `layer.flip` | `layerId?`, `axis?` | `x` (default) or `y` |

### Effects

| Command | Payload |
|---------|---------|
| `effect.set` | `layerId?`, `effect: {type, value?}`, `live?` |
| `effect.remove` | `layerId?`, `type` |
| `effect.clear` | `layerId?` |

Effect types and ranges are listed in the [Document Model](/guide/document-model#effects). Where `layerId` is optional, the first selected layer is used.

### Interactive crop

`layer.crop` sets a window numerically. The **crop tool** is the interactive version: a window the user drags and resizes on the canvas, with the rest of the image dimmed and rule-of-thirds guides. It is opened through `tool.set` and driven by four commands:

| Command | Payload | Notes |
|---------|---------|-------|
| `tool.set` | `tool: "crop"`, `layerId?`, `aspect?` | Opens the window over an image layer (the selection when `layerId` is omitted). `aspect` locks a ratio: a number (`1.5`) or `"original"`. Returns `{tool: "crop", crop: {layerId, x, y, width, height}}` |
| `crop.set` | `x?`, `y?`, `width?`, `height?`, `aspect?` | Moves or resizes the window programmatically (natural pixels, clamped inside the image) |
| `crop.get` | — | The current window, or `null` when the tool is not active |
| `crop.apply` | — | Commits: the layer keeps its place on the artboard and only its visible pixels change. Returns `{layerId, crop, changed}` |
| `crop.cancel` | — | Closes the window without changing anything |

Keyboard: `C` opens the tool on the selected image layer, `Enter` applies, `Esc` cancels; a double-click on the window applies too. Any *other* editing command sent while the tool is open cancels it first, because the window is a proposal, not document state — `view.*`, `document.get` and `export.render` are allowed and leave it open.

### View, tools and history

| Command | Payload |
|---------|---------|
| `view.fit` | — |
| `view.zoom` | `level` (absolute, 1 = 100 %) or `delta` (multiplier) |
| `tool.set` | `tool`: `select` · `hand` · `draw` · `crop` (see above) · `marquee` · `lasso` · `wand` · `paint` · `erase` (the pixel tools: `layerId?` names the image, else the selected one; they enter *pixel mode*, see below) |
| `history.undo`, `history.redo` | — |

### Batch

`batch` runs `commands: [{type, payload}, …]` in order as **one undo step**. It stops at the first failing step; earlier steps are kept. On failure the acknowledgement is `ok: false` with `error: "batch stopped at step N: …"` **and** `data: {results, failedStep}` — the per-step results up to and including the failure — so a caller (the agent loop uses this) knows exactly which steps ran.

### Export

`export.render` renders the artboard and, depending on `dest`, downloads it or sends it to the server.

| Field | Meaning |
|-------|---------|
| `format` | `png` · `jpeg` · `webp` · `svg` (in the browser) or `tiff` · `pdf` · `avif` · `heic` · `gif` · `bmp` · `ico` (transcoded by the server) |
| `scale` | Pixel multiplier, default 1 |
| `quality` | 0–1 for lossy formats |
| `region` | `canvas` (default) or `selection` |
| `dest` | `download` (default) · `disk` · `r2` · `catalog` (the signed-in user's [catalogue](/guide/catalog)) — see [Storage & Export](/guide/storage-export) |
| `filename`, `key` | Output name and, for `disk`/`r2`, an optional storage key |
| `name`, `project` | For `dest: catalog`: the item's name, and `project: true` to keep the document JSON beside the PNG so the composition can be reopened |
| `preview: true` | Render a small JPEG and return it as `data.dataUrl`, so an agent can look at the result |

The result carries `width`, `height`, `bytes`, `format` and, for server destinations, a `server` object with the stored `key` (a signed `url` for R2; `id`, `kind` and `url` for the catalogue). `dest: catalog` fails with `401` when nobody is signed in.

## Pixel selections and pixel edits (image layers)

The pixel tools act on ONE image layer's pixels in its natural pixel space. Picking a pixel tool, or sending any
`select.*` / `pixels.*` command, enters **pixel mode**: the image loses its handles (a dashed accent frame marks
it), the other layers freeze, the options bar tints, and the keyboard scopes to the SELECTION — ⌘C / ⌘X copy or
cut the selected pixels, ⌘V pastes them as a new layer in the same place, ⌘J lifts them into a layer, ⌫ erases
them, ⌘A / ⌘D select all / none, ⌘⇧I inverts, the arrows move the outline, Esc deselects and then leaves the mode.
Picking another image in the Layers panel retargets the tools; picking anything else ends the mode. The
selection is never part of the document and never exported. Points are artboard pixels unless `space: "image"`.

| Command | Payload | Notes |
|---------|---------|-------|
| `select.wand` | `layerId?`, `x`, `y`, `tolerance?` (0–100, default 32), `contiguous?` (default true), `mode?` (`replace` · `add` · `subtract`), `space?` | Magic wand from the colour under the point; alpha counts, so a transparent region never bleeds into a painted one. Returns the summary: `pixels`, `coverage`, `bbox` (natural px), `bboxArtboard` |
| `select.lasso` | `layerId?`, `points: [[x, y], …]` (≥3), `mode?`, `space?` | Polygon selection (even-odd) |
| `select.rect` | `layerId?`, `x`, `y`, `width`, `height`, `mode?`, `space?` | Rectangle; `x, y` is a corner |
| `select.all` · `select.none` · `select.invert` · `select.get` | `layerId?` | Every pixel · drop · invert · the summary (or `null`) |
| `select.move` | `dx`, `dy`, `space?` | Shift the outline without touching pixels (the arrow keys) |
| `pixels.fill` | `layerId?`, `color?` | Fill the selection — the whole image when nothing is selected — with a colour (default: the brush colour). A pixel edit: one undo step, `edited: true`, `originalSrc` kept |
| `pixels.erase` · `pixels.keep` | `layerId?` | Erase the selection to transparent · keep the selection and drop everything else (the mask) |
| `pixels.paint` | `layerId?`, `points`, `color?`, `size?` (natural px, default 24), `erase?`, `space?` | A round-brush stroke, clipped to the selection when there is one |
| `pixels.brush` | `color?`, `size?` | The brush the interactive paint / erase tools and the defaults use; emits a `brush` event |
| `pixels.copy` · `pixels.cut` | `layerId?` | The selected pixels to the editor's clipboard as an image layer (and the OS clipboard where the browser allows); `layer.paste` then adds it in the same place. Cut also erases |
| `pixels.lift` | `layerId?`, `name?` | New layer from the selection, directly above the source, selected; the mode ends |
| `pixels.revert` | `layerId?` | Every pixel edit on the layer undone at once: `src` goes back to `originalSrc` |

A pixel edit replaces the layer's source. The new pixels ride under a `blob:` URL — never a data URL, which
would put megabytes into every history entry and the `document` prop — and are uploaded through the asset
endpoint in the background; the layer, the clipboard and history then move to the stable URL, so a saved
project names a URL every page can resolve.


---

*Source: /guide/commands*
