Commands
Every operation in Photo Slop is a JSON command — the same bus drives the UI, Python callbacks and AI agents.
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.
{ "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:
{ "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
idis executed once. Re-sending anidis 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.
batchruns several commands as one undo step and stops at the first failure.- Every committed change updates the
document,layers,selection,rev,canUndoandcanRedoprops.
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. 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) — see 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
Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs:
- /guide/commands/llms.txt — LLM-friendly documentation
- /sitemap.xml
- /robots.txt