Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion orca-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -2120,12 +2120,56 @@
"ref": "main",
"path": "plugins/image-markup"
},
"description": "Mark up images with pen, arrow, box and text tools. Copy the result, or save it into the image with the original kept inside and the markup still editable.",
"description": "Mark up images in Orca: pen, highlighter, arrow, box, ellipse, text, numbered steps and redaction. Save the markup into the image with the original kept inside and still editable, or export a copy.",
"categories": [
"editors",
"official"
],
"releases": [
{
"version": "2.0.0",
"commit": "d5c95b67c9befdccd78bfeffa2434b1a1ec60a5f",
"contentHash": "e588afe0ecaf7fc3da63ccdc5b4d3ae9b6e10ad969c834fd76a5a46e1fd91f41",
"execution": "sandboxed-ui",
"minimumOrca": "1.4.248",
"changelog": "Saving runs on Orca's document session: Orca owns the unsaved state and the save, and markup Orca had not received when the file changed outside (say, an agent replacing the screenshot) is moved onto the new image instead of dropped. New Highlighter, Step number and Redact tools; redactions are removed from the saved file, original included. Export saves a copy as a new PNG. Needs Orca 1.4.248.",
"name": "Image Markup",
"contributes": [
{
"kind": "customEditors",
"name": "Markup",
"detail": ".png, .jpg, .jpeg, .webp, .gif, .bmp, .avif"
}
],
"fileTypes": [
"png",
"jpg",
"jpeg",
"webp",
"gif",
"bmp",
"avif"
],
"editorFileTypes": [
"png",
"jpg",
"jpeg",
"webp",
"gif",
"bmp",
"avif"
],
"images": {
"cover": {
"path": "plugins/image-markup/images/cover.webp",
"alt": "A sunlit reading nook in Image Markup, with a red ellipse around the chair and an arrow pointing to it from a Cozy corner label",
"sha256": "45a43a6c45b04e0382050cb9c78f90a18cf3146cc79163d049c2fa6d082d7137",
"width": 1600,
"height": 1000
}
},
"license": "MIT"
},
{
"version": "1.0.2",
"commit": "1fe9ab8f3133f3712766c974aded306d648f483f",
Expand Down
86 changes: 65 additions & 21 deletions plugins/image-markup/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,99 @@ Orca's own viewer stays one click away in the editor's view toggle. Draw with:
| --- | --- | --- |
| Select | `V` | Move shapes, drag arrow ends and box or ellipse corners, double-click text to edit it |
| Pen | `P` | Freehand ink; a click leaves a dot |
| Highlighter | `H` | A wide translucent stroke; hold Shift for a straight one along a line of text |
| Arrow | `A` | Hold Shift to snap to 45° |
| Box | `R` | Hold Shift for a square |
| Ellipse | `O` | Hold Shift for a circle |
| Text | `T` | Click to type; Esc or a click outside finishes |
| Step number | `N` | Each click places the next numbered disc; deleting one renumbers the rest |
| Redact | `X` | Drag over anything secret to cover it; see below |

`1`–`8` pick a colour and the dots pick a stroke size; with a shape selected, both restyle it.
Delete removes the selection, arrow keys nudge it, `⌘Z` / `⇧⌘Z` undo and redo. Pinch or
Ctrl/⌘-scroll zooms, scrolling or dragging empty space pans, Space-drag pans with any tool,
`⌘0` fits and `⌘1` shows 100%.

## Copying and saving
## Saving, exporting and copying

**`⌘S` is how you share markup today.** Orca doesn't yet let plugin frames put images on the
clipboard, and a stand-in (such as HTML with an embedded image) pastes nowhere an agent can read,
so **Copy** (`⌘C`) explains this instead of copying. Once Orca allows it, Copy puts a real PNG
of the image with its markup on the clipboard, with no plugin update needed.
**`⌘S`** saves the markup into the image file through Orca's normal save, and the tab shows
unsaved changes like any other file, from the first stroke on. The file then shows the markup everywhere: other viewers,
agents, uploads. It also keeps the original image and the shapes inside it, so reopening Markup
gives you the untouched original with every shape still editable, and clearing all markup and
saving restores the original file byte for byte. Close the tab without saving to throw the
markup away.

**`⌘S`** saves the markup into the image file through Orca's normal save. The file then shows
the markup everywhere: other viewers, agents, uploads. It also keeps the original image and the
shapes inside it, so reopening Markup gives you the untouched original with every shape still
editable, and clearing all markup and saving restores the original file byte for byte. The tab
is marked unsaved while there is markup to save; close it without saving to throw the markup away.
**Export** saves a copy of the image with its markup as a new PNG, wherever you choose, and
leaves the open file alone. It is in the desktop app only.

**Copy** (`⌘C`) is not available yet: Orca doesn't let plugins put images on the clipboard, and
a stand-in (such as HTML with an embedded image) pastes nowhere an agent can read, so `⌘C`
explains this instead. Once Orca allows it, Copy puts a real PNG on the clipboard with no
plugin update needed.

PNG files keep the extra data in a private `orCA` chunk and JPEG files in APP9 segments, so
every image reader ignores it. If another tool later changes the pixels but keeps that data,
Markup notices and opens the file as a plain image. A saved file grows by about the size of the
original. JPEG markup is saved as a new JPEG at quality 0.92; the original keeps its quality.

**Markup is not redaction.** The saved file keeps the original image inside it, so drawing over
a password or token hides it from viewers but not from anyone who opens the file in Markup or
reads its bytes. Crop or edit the pixels with another tool before sharing anything secret.
## Redaction

Redact covers an area with one flat colour (the area's average, hatched so it reads as
redacted). Nothing of what was under it can be read back. Saving removes it from the file for
good: the area is covered in the visible image **and** in the original kept inside. Everything
drawn before the last redaction (text, strokes, steps) is flattened into that original with
it, so no label or stroke under a redaction survives as text in the file; only markup drawn
after it stays editable. A JPEG's kept original is then re-encoded at quality 0.92. Until you
save you can still move, resize or undo a redaction; after saving, clearing all markup restores
the redacted image, not the secret.

Every other tool only draws over the image: a box or pen stroke over a password hides it from
viewers, but the original kept inside still has it. Use Redact for anything secret.

## When the file changes outside Markup

Orca owns the file: it marks the tab unsaved, saves it, and tells Markup when something else
writes it, such as an agent taking a new screenshot to the same path.

- With no unsaved markup, Markup shows the new image.
- Markup you drew but Orca hadn't received yet, or that was on its way when the file changed, is
moved onto the new image and stays unsaved; a toast says so. Shapes keep their place in image
pixels, and markup another editor saved into the file meanwhile is merged with yours.
- Once the tab is marked unsaved, Orca shows its banner instead: **Overwrite** keeps your
markup over the old image, **Reload from Disk** takes the new image and drops it.

## Limits

- PNG and JPEG can hold markup. WebP, GIF (first frame only), BMP and AVIF open for markup, but
are never written. Read-only tabs are never written either.
are never written; use Export. Read-only tabs are never written either.
- Images over 64 megapixels, or over 32,767 px on a side, open in Orca's own viewer only, or
open for markup without saving.
- One image keeps up to 5,000 shapes, 200,000 pen points and 20,000 characters of text.
- The saved image is limited to Orca's 64 MiB document cap, original included.
- Orca gives an editor 1 s to hand over its changes when you save. Markup is encoded as you
work, so this is only tight for a save pressed straight after a stroke on a very large image
(around 24 megapixels); if Orca reports that it couldn't save, press `⌘S` again.
- Markup is encoded as you work, and Orca waits for the latest encode when you save. If a save
straight after a stroke on a very large image is refused, press `⌘S` again.
- The flattened image is sRGB and keeps the original's pixel density; colour profiles and
animation stay only in the original inside.
- Two split panes showing Markup for the same file don't share unsent strokes: an edit in one
reloads the other.
- Orca plugins cannot create files, so there is no separate "export as" file yet.
- Export suggests the name `markup.png`: plugins never learn the file's own name.

Requires Orca 1.4.248 or later; Orca 1.4.245 to 1.4.247 keep installing Image Markup 1.0.2.
Build instructions and tests are in [build/README.md](build/README.md).

## Changelog

### 2.0.0

- Saving runs on Orca's document session: Orca owns the unsaved state and the save, waits for
the latest markup before saving, and a refused save keeps the markup unsaved.
- An outside write (say, an agent replacing the screenshot) no longer drops markup Orca hadn't
received yet: it is moved onto the new image.
- New tools: Highlighter, Step number and Redact. Redact really removes the pixels from the
saved file, original included.
- Export saves a copy with the markup as a new PNG through Orca's save dialog.
- Needs Orca 1.4.248.

### 1.0.2 and earlier

Requires Orca 1.4.245 or later. Build instructions and tests are in [build/README.md](build/README.md).
Pen, arrow, box, ellipse and text, saved into PNG and JPEG files with the original kept inside.

MIT licence. No third-party code is bundled.
58 changes: 47 additions & 11 deletions plugins/image-markup/build/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,25 @@ npm test

Browser tests use the Chromium headless shell expected by `playwright-core`; install it with
`npx playwright-core install chromium-headless-shell` if needed. The harness reproduces Orca's
frame, CSP, theme tokens and document API, including `applyEdit` version checks and
`editor.flush`, served from a secure origin as in Orca.
frame, CSP, theme tokens and envelope, and runs Orca's real `orca.document` (with `session()`),
copied into `test/orca-frame/document-script.mjs` from Orca's
`src/shared/plugins/frames/plugin-panel-shell-document.ts`. Its host answers edits the way Orca
does (a stale `baseVersion` is a conflict), sends disk changes with their `ancestor`, asks the
frame to flush, and answers `files.saveAs`. `test/document-sync.test.mjs` runs the sync module
against `mockEditorHost` from `orca/testing`, vendored in `test/orca-testing/` (copy
`orca-testing.mjs` and `orca-runtime.mjs` from Orca's `resources/plugins/declarative/` to
update it).

| File | Purpose |
| --- | --- |
| `src/editor.js`, `src/editor.css` | Frame wiring, tools, rendering, history, saving, Copy |
| `src/editor.js`, `src/editor.css` | Frame wiring, tools, rendering, history, encoding, Copy and Export |
| `src/document-sync.js` | Orca's document session: how a file decodes, encodes and merges with an outside write |
| `src/scene.js` | Shape model: bounds, hit tests, moves, handles, simplifying, (de)serialising |
| `src/render.js` | Drawing shapes, text metrics, the full-resolution flattened render |
| `src/render.js` | Drawing shapes, text metrics, redaction fills, the full-resolution flattened render |
| `src/png.js` | PNG encoder: RGB when opaque, per-row adaptive filters, native deflate |
| `src/container.js` | Markup stored in PNG `orCA` chunks and JPEG APP9 segments, with a pixel CRC |
| `src/view.js` | Fit, zoom and coordinate mapping |
| `test/*.test.mjs` | Container and scene unit tests; editor tests in the Orca-like frame |
| `test/*.test.mjs` | Container, scene and document-sync tests; editor tests in the Orca-like frame |

## Performance

Expand All @@ -46,11 +53,40 @@ frame, CSP, theme tokens and document API, including `applyEdit` version checks
The editor test suite drives a 24-megapixel image carrying 2,000 strokes and checks that
drawing, panning and zooming hold 60 fps in headless software rendering.

## Talking to Orca

- `orca.document.session({ decode, encode, merge })` (`src/document-sync.js`) does the document
sync: Orca keeps the buffer, the unsaved state and the save. A value is the whole file plus its
original and shapes. Encoding a scene is asynchronous (a full-resolution render and PNG
encode), so the editor encodes first and calls `change` with the finished file; `encode` only
hands it over. The editor also answers `editor.flush` itself: Orca waits for every handler, so a
save straight after a stroke waits for that stroke's encode, then for the session's flush.
- `merge(base, outside, mine)` keeps the outside file's image and shapes, adds the shapes this
editor drew since `base` and drops the ones it deleted (shapes match by their saved form). It
can't encode, so a merge that changed anything is marked `stale` and carries the outside file;
the editor loads it, encodes it and sends it as its next change. Strokes not yet handed to the
session merge the same way when an outside change arrives.
- Redactions, and every shape drawn before the last one, are rendered into the original before
it is stored and left out of the stored scene, so nothing they cover survives as pixels or as
text. With nothing else to keep, the file is just the flattened image.
- Orca flushes editors before a save, but not before closing, reloading or switching a tab's
view, and an encode can take seconds. So the first edit after each handover sends a stand-in at
once and the tab is unsaved. The stand-in is the last file with the latest shapes written into
its markup (scene marked `redraw`), so if it is saved anyway it reopens with every shape; the
next edit encodes it again, or opening does at once when it holds a redaction. With a
redaction it adds nothing the last file lacks: that file's own original, and no shape a later
redaction covers. Only past the size cap is it the last file with a note (a private
`orCp` PNG chunk, or an APP9 `ORCA-PENDING` segment the JPEG pixel CRC skips), which decoding
strips. A real file always follows: the encode, or the last one again after an undo back to it.
- `orca.files.saveAs` (desktop only, needs the click's activation) is Export. The render is kept,
so when a large image outlasts the click, the second click saves at once.

## Clipboard

Orca's editor iframe has no `allow="clipboard-write"`, so the async Clipboard API is blocked by
permissions policy, and the plugin host API has no clipboard method (Orca's own browser markup
copies through `window.api.ui.writeClipboardImage`, which frames cannot reach). Copy checks the
policy up front: when it is blocked, the button explains that ⌘S saves the markup instead. There
is deliberately no HTML or data-URL fallback; agents and terminals cannot paste it, and large
images made multi-megabyte clipboard entries. When the policy allows it, Copy writes `image/png`.
Orca's frames refuse the async Clipboard API (desktop isolated frames deny the permission;
browser-client iframes have no `allow="clipboard-write"`), and the frame API has no clipboard
method. `execCommand('copy')` in a click works, but carries text only. Copy checks the policy up
front: when it is blocked, `⌘C` explains that `⌘S` and Export keep the markup instead, and the
toolbar shows Export in its place where Orca offers it. There is deliberately no HTML or data-URL
fallback; agents and terminals cannot paste it, and large images made multi-megabyte clipboard
entries. When the policy allows it, Copy writes `image/png`.
Loading
Loading