@ingcreators/annot-annotator 0.3.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +375 -0
- package/README.md +1 -1
- package/dist/annotator.d.ts +44 -8
- package/dist/diff-aggregate.d.ts +6 -0
- package/dist/diff.d.ts +30 -0
- package/dist/dsl/schema.d.ts +68 -0
- package/dist/dsl/types.d.ts +83 -1
- package/dist/encode/encode.d.ts +4 -4
- package/dist/encode/quantize.d.ts +4 -10
- package/dist/flatten-editable-png.d.ts +37 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +1193 -522
- package/dist/redact-burn.d.ts +63 -0
- package/package.json +10 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,380 @@
|
|
|
1
1
|
# @ingcreators/annot-annotator
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 6124d59: **`BboxAnnotation` palette extensions: `freehand` + `focusMask`** —
|
|
8
|
+
Phase 3b of `docs/plans/living-spec-authoring-roadmap.md`.
|
|
9
|
+
|
|
10
|
+
Two new variants on the `BboxAnnotation` union expose the rest of
|
|
11
|
+
the Annot visual palette to `bboxAnnotationsToSvg()` and through
|
|
12
|
+
it to `Annotator.toPng()` / `.toSvg()` / `.toEditablePng()`:
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
// Free-form path stroke — `path` is the SVG <path> `d` attribute.
|
|
16
|
+
{
|
|
17
|
+
type: "freehand",
|
|
18
|
+
path: "M100,200 L150,250 L200,210",
|
|
19
|
+
intent: "info", // optional — defaults to "error"
|
|
20
|
+
strokeWidth: 4, // optional — defaults to 2
|
|
21
|
+
fill: "#ffeecc", // optional — defaults to "none"
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Dim everything except the cutout region. One <path> with
|
|
25
|
+
// fill-rule="evenodd" combines a full-image rect with the
|
|
26
|
+
// cutout — the even-odd rule cancels overlap.
|
|
27
|
+
{
|
|
28
|
+
type: "focusMask",
|
|
29
|
+
cutout: { x: 200, y: 100, width: 80, height: 40 },
|
|
30
|
+
imageWidth: 1280,
|
|
31
|
+
imageHeight: 800,
|
|
32
|
+
dimColor: "rgba(0,0,0,0.5)", // optional — default same value
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`@ingcreators/annot-product-docs`'s `<Screen annotations>`
|
|
37
|
+
Image Service composition (Phase 3c) will map yaml
|
|
38
|
+
`AnnotationSpec` entries to these two primitives plus the
|
|
39
|
+
existing `rect` / `circle` / `arrow` / `text` / `callout` /
|
|
40
|
+
`numberedBadge` shapes. Useful standalone for any annotator
|
|
41
|
+
caller (Playwright fixtures, MCP server, custom test reporters)
|
|
42
|
+
without involving yaml or MDX.
|
|
43
|
+
|
|
44
|
+
**JSON schemas** — `BBOX_ANNOTATION_SCHEMA.oneOf` gains
|
|
45
|
+
`BBOX_FREEHAND` + `BBOX_FOCUS_MASK` entries so MCP callers can
|
|
46
|
+
validate either kind at the boundary.
|
|
47
|
+
|
|
48
|
+
**Out of scope** — redact (mosaic / blur) needs raster pixel
|
|
49
|
+
access and is not implementable as an SVG fragment; it stays
|
|
50
|
+
on the existing destructive `burnRedactions` path in
|
|
51
|
+
`@ingcreators/annot-mcp`. Phase 3a's annotation yaml rejects
|
|
52
|
+
`style: "mosaic" | "blur"` accordingly.
|
|
53
|
+
|
|
54
|
+
**Compatibility** — additive. Existing `BboxAnnotation` callers
|
|
55
|
+
keep working; the new variants are opt-in by setting
|
|
56
|
+
`type: "freehand" | "focusMask"`.
|
|
57
|
+
|
|
58
|
+
- 0c7ac26: **Relocate `burnRedactions` from `@ingcreators/annot-mcp` to
|
|
59
|
+
`@ingcreators/annot-annotator`** — Phase 3e of
|
|
60
|
+
`docs/plans/living-spec-authoring-roadmap.md` (Phase 3 follow-up).
|
|
61
|
+
|
|
62
|
+
The destructive raster burn primitive (solid / mosaic / blur over
|
|
63
|
+
a PNG buffer, built on `@napi-rs/canvas`) historically lived in
|
|
64
|
+
`@ingcreators/annot-mcp` because the MCP server's
|
|
65
|
+
`annot_redact_screenshot` tool was the first caller. The function
|
|
66
|
+
itself has no MCP-specific surface — it's pure
|
|
67
|
+
(`pngBytes + regions → pngBytes`). To let non-MCP callers consume
|
|
68
|
+
it without dragging the MCP server's dep footprint (Playwright,
|
|
69
|
+
`@modelcontextprotocol/sdk`, etc.), the primitive moves to
|
|
70
|
+
`@ingcreators/annot-annotator` — the canonical Node-side raster
|
|
71
|
+
home, which already depends on `@napi-rs/canvas` for its encode
|
|
72
|
+
pipeline (so the move adds **zero** transitive deps).
|
|
73
|
+
|
|
74
|
+
### `@ingcreators/annot-annotator` — new public surface
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import {
|
|
78
|
+
burnRedactions,
|
|
79
|
+
type RedactRegion,
|
|
80
|
+
} from "@ingcreators/annot-annotator";
|
|
81
|
+
|
|
82
|
+
const out = await burnRedactions(pngBytes, [
|
|
83
|
+
{
|
|
84
|
+
bbox: { x: 10, y: 20, width: 100, height: 30 },
|
|
85
|
+
style: "solid",
|
|
86
|
+
color: "#000000",
|
|
87
|
+
},
|
|
88
|
+
{ bbox: { x: 200, y: 100, width: 80, height: 40 }, style: "mosaic" },
|
|
89
|
+
{ bbox: { x: 0, y: 0, width: 64, height: 64 }, style: "blur" },
|
|
90
|
+
]);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`RedactRegion` is exposed as an alias of `BboxRedactRegion`
|
|
94
|
+
(structurally identical, already declared in the DSL types) so
|
|
95
|
+
existing MCP-side consumers see no shape change.
|
|
96
|
+
|
|
97
|
+
### `@ingcreators/annot-mcp` — no public API change
|
|
98
|
+
|
|
99
|
+
The existing `burnRedactions` + `RedactRegion` re-exports from
|
|
100
|
+
the package root keep working byte-identical, sourced from the
|
|
101
|
+
annotator instead of the old MCP-local file. MCP's
|
|
102
|
+
`annot_redact_screenshot` / `annot_redact_url` tools continue
|
|
103
|
+
to import from `../redact/burn.js`, which is now a one-line
|
|
104
|
+
re-export from annotator.
|
|
105
|
+
|
|
106
|
+
### Compatibility
|
|
107
|
+
|
|
108
|
+
Additive on annotator's side; zero behaviour change on MCP's
|
|
109
|
+
side. Tests move with the code (annotator 64 → 71 passed; MCP
|
|
110
|
+
91 → 84 passed — same scenarios at the new home).
|
|
111
|
+
|
|
112
|
+
### Out of scope
|
|
113
|
+
|
|
114
|
+
`@napi-rs/canvas` stays as an MCP direct dep — `compare/diff.ts`
|
|
115
|
+
and several other MCP tool tests still use it directly, so
|
|
116
|
+
collapsing it onto a transitive-via-annotator import is a
|
|
117
|
+
separate cleanup.
|
|
118
|
+
|
|
119
|
+
- 64dc6e8: **Relocate `diffScreenshots` from `@ingcreators/annot-mcp` to
|
|
120
|
+
`@ingcreators/annot-annotator`** — Phase 3i of
|
|
121
|
+
`docs/plans/living-spec-authoring-roadmap.md` (Phase 3
|
|
122
|
+
follow-up #2). Same pattern as 3e's `burnRedactions` relocate.
|
|
123
|
+
|
|
124
|
+
The pixelmatch-driven PNG comparison + contiguous-region bbox
|
|
125
|
+
aggregation lived in `@ingcreators/annot-mcp/compare/` for
|
|
126
|
+
historical reasons (the MCP server's
|
|
127
|
+
`annot_compare_screenshots` tool was the first caller). The
|
|
128
|
+
function itself has no MCP-specific surface — it's pure
|
|
129
|
+
(`pngBytes + pngBytes → DiffResult`). Relocating it to
|
|
130
|
+
`@ingcreators/annot-annotator` lets non-MCP callers
|
|
131
|
+
(Playwright visual regression fixtures, Astro pixel drift CI,
|
|
132
|
+
custom test reporters, editor before/after preview) consume
|
|
133
|
+
it without dragging the MCP server's dep footprint.
|
|
134
|
+
|
|
135
|
+
### `@ingcreators/annot-annotator` — new public surface
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import {
|
|
139
|
+
diffScreenshots,
|
|
140
|
+
aggregateDiffRegions,
|
|
141
|
+
DimensionMismatchError,
|
|
142
|
+
type DiffResult,
|
|
143
|
+
type DiffOptions,
|
|
144
|
+
} from "@ingcreators/annot-annotator";
|
|
145
|
+
|
|
146
|
+
const result = await diffScreenshots(beforePng, afterPng, { threshold: 0.1 });
|
|
147
|
+
// → { mismatchedPixels: number, regions: BBox[], width, height }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
annotator gains `pixelmatch` (~4 KB, no transitive deps) as a
|
|
151
|
+
runtime dep.
|
|
152
|
+
|
|
153
|
+
### `@ingcreators/annot-mcp` — no public API change
|
|
154
|
+
|
|
155
|
+
The existing `compare/diff.ts` + `compare/aggregate.ts` modules
|
|
156
|
+
become one-line re-export shims forwarding from annotator. MCP's
|
|
157
|
+
internal callers (`tools/compare-screenshots.ts`) and any
|
|
158
|
+
external consumer importing from `@ingcreators/annot-mcp` keep
|
|
159
|
+
working byte-identical.
|
|
160
|
+
|
|
161
|
+
### Compatibility
|
|
162
|
+
|
|
163
|
+
Additive on annotator's side; zero behaviour change on MCP's
|
|
164
|
+
side. Tests move with the code (annotator 71 → 81 passed; MCP
|
|
165
|
+
84 → 78 passed — same scenarios at the new home, plus a new
|
|
166
|
+
`diffScreenshots` smoke test that the MCP-side aggregate-only
|
|
167
|
+
test didn't cover).
|
|
168
|
+
|
|
169
|
+
### Out of scope
|
|
170
|
+
|
|
171
|
+
`pixelmatch` stays as a direct MCP dep — even though MCP no
|
|
172
|
+
longer imports it from the moved code, it's a tiny package
|
|
173
|
+
and removing the explicit dep would force consumers to rely
|
|
174
|
+
on a transitive resolution through annotator, which is more
|
|
175
|
+
fragile than declaring the intent directly.
|
|
176
|
+
|
|
177
|
+
- 691bec5: **Add `flattenEditablePng(pngBytes) → pngBytes`** — Phase 3j of
|
|
178
|
+
`docs/plans/living-spec-authoring-roadmap.md` (Phase 3
|
|
179
|
+
follow-up #2). The editor's editable-PNG format embeds the
|
|
180
|
+
original un-annotated capture + the annotations SVG in PNG
|
|
181
|
+
ancillary chunks for re-edit; "flatten" drops those chunks and
|
|
182
|
+
keeps just the visible (already-annotated) bytes.
|
|
183
|
+
|
|
184
|
+
### `@ingcreators/annot-annotator` — new public surface
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
import { flattenEditablePng } from "@ingcreators/annot-annotator";
|
|
188
|
+
|
|
189
|
+
const flat = flattenEditablePng(editablePngBytes);
|
|
190
|
+
// → flat PNG: same visible pixels, no Adobe XMP iTXt chunk,
|
|
191
|
+
// no custom svGo chunk. `readEditablePngBytes(flat)` returns
|
|
192
|
+
// null. File size drops significantly (the editable layer
|
|
193
|
+
// roughly doubled the bytes).
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### `@ingcreators/annot-core/xmp-bytes` — new public surface
|
|
197
|
+
|
|
198
|
+
The implementation lives in `@ingcreators/annot-core` as
|
|
199
|
+
`stripPngEditableLayer` — the same chunk-walking helper that
|
|
200
|
+
`writePngWithMetadata` / `writePngWithTagsOnly` already used
|
|
201
|
+
internally to clean stale metadata before re-injecting. Now
|
|
202
|
+
exported so other Tier A consumers (not just annotator) can
|
|
203
|
+
use it directly.
|
|
204
|
+
|
|
205
|
+
annotator's `flattenEditablePng` is a one-line wrapper that
|
|
206
|
+
calls `stripPngEditableLayer` under a more user-facing name.
|
|
207
|
+
|
|
208
|
+
### Why this is metadata removal, not re-rasterization
|
|
209
|
+
|
|
210
|
+
`toEditablePng` rasterizes the SVG fragment onto the base image
|
|
211
|
+
FIRST and embeds the editable layer as ancillary PNG chunks
|
|
212
|
+
(`iTXt` carrying Adobe XMP + custom `svGo` chunk). The visible
|
|
213
|
+
bytes are already the annotated bitmap. Flattening strips the
|
|
214
|
+
ancillary chunks; the IDAT pixel data stays byte-identical.
|
|
215
|
+
No decode, no re-encode, no `@napi-rs/canvas` round-trip.
|
|
216
|
+
|
|
217
|
+
### Use cases
|
|
218
|
+
- **Publish-flat** — editor session → distribution-ready PNG;
|
|
219
|
+
the editable layer is dead weight for downstream consumers
|
|
220
|
+
(Slack drop, third-party viewers).
|
|
221
|
+
- **File size** — editable PNG roughly doubles in bytes
|
|
222
|
+
(original + SVG embedded); flattening drops the overhead.
|
|
223
|
+
- **Privacy hardening** — `burnRedactions` is the strong
|
|
224
|
+
version for _redact_ regions; flattening drops the
|
|
225
|
+
recoverable original entirely for _all_ annotations,
|
|
226
|
+
including non-redact ones whose annotated visual the
|
|
227
|
+
publisher wants to keep but whose original capture they
|
|
228
|
+
don't want shippable.
|
|
229
|
+
|
|
230
|
+
### Internal rename in annot-core
|
|
231
|
+
|
|
232
|
+
The private `removePngMetadata` helper in
|
|
233
|
+
`@ingcreators/annot-core/xmp-bytes` is renamed to
|
|
234
|
+
`stripPngEditableLayer` (clearer name describing what it does
|
|
235
|
+
rather than how it's used). Internal callers in the same
|
|
236
|
+
module updated. No external API change for the rename itself;
|
|
237
|
+
`writePngWithMetadata` + `writePngWithTagsOnly` keep their
|
|
238
|
+
existing signatures + behaviour.
|
|
239
|
+
|
|
240
|
+
### Compatibility
|
|
241
|
+
|
|
242
|
+
Additive on annotator + core. No behaviour change for existing
|
|
243
|
+
callers (the only internal rename is a private helper).
|
|
244
|
+
|
|
245
|
+
- 9697f27: **Export `burnRegions` as an operation-aligned alias for
|
|
246
|
+
`burnRedactions`** — Phase 3k of
|
|
247
|
+
`docs/plans/living-spec-authoring-roadmap.md`
|
|
248
|
+
(Phase 3 follow-up #2). Closes the follow-up.
|
|
249
|
+
|
|
250
|
+
`burnRedactions` is named for its first caller's intent (MCP's
|
|
251
|
+
`annot_redact_screenshot`), but the underlying primitive is a
|
|
252
|
+
`pngBytes + region[] → pngBytes` raster transform — generic
|
|
253
|
+
over the caller's purpose. The new export surfaces the
|
|
254
|
+
operation-aligned name alongside the intent-named original.
|
|
255
|
+
|
|
256
|
+
### `@ingcreators/annot-annotator` — new public export
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { burnRegions } from "@ingcreators/annot-annotator";
|
|
260
|
+
|
|
261
|
+
// Identical signature + behaviour to burnRedactions.
|
|
262
|
+
const out = await burnRegions(pngBytes, [
|
|
263
|
+
{ bbox: { x: 10, y: 20, width: 100, height: 30 }, style: "mosaic" },
|
|
264
|
+
]);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Identity-equal to `burnRedactions` (`burnRegions === burnRedactions`
|
|
268
|
+
at the export level) — picking one name over the other is purely
|
|
269
|
+
a docs-readability choice.
|
|
270
|
+
|
|
271
|
+
### Use cases that motivated the alias
|
|
272
|
+
|
|
273
|
+
The function isn't redact-specific — the JSDoc on `burnRedactions`
|
|
274
|
+
now enumerates:
|
|
275
|
+
- Editor-side "highlight this region with a translucent colour
|
|
276
|
+
and ship it baked" workflow.
|
|
277
|
+
- Visual-regression pre-processing — burn dynamic content
|
|
278
|
+
(timestamps, login state badges) into the screenshot so pixel
|
|
279
|
+
diffs stay deterministic.
|
|
280
|
+
- Watermark / overlay burn for downstream distribution.
|
|
281
|
+
- Privacy hardening at non-redact regions (e.g. blur a logo in
|
|
282
|
+
a publicly-shared screenshot).
|
|
283
|
+
|
|
284
|
+
For any of these, `burnRegions` reads as the natural name.
|
|
285
|
+
Redact callers stay on `burnRedactions` (still the recommended
|
|
286
|
+
name when the intent IS redaction); no migration forced.
|
|
287
|
+
|
|
288
|
+
### `@ingcreators/annot-mcp` — no public API change
|
|
289
|
+
|
|
290
|
+
MCP's `compare/burn.ts` re-export shim + `index.ts` forward both
|
|
291
|
+
names. Existing `burnRedactions` callers see no change.
|
|
292
|
+
|
|
293
|
+
### Compatibility
|
|
294
|
+
|
|
295
|
+
Additive. `burnRedactions` keeps its public API + JSDoc; the
|
|
296
|
+
alias is purely additive.
|
|
297
|
+
|
|
298
|
+
## 0.5.0
|
|
299
|
+
|
|
300
|
+
### Minor Changes
|
|
301
|
+
|
|
302
|
+
- 806badc: Retire the `@ingcreators/annot-imagequant` (GPL-3.0) dynamic-import
|
|
303
|
+
boundary that gated PNG-8 output in the headless annotator and the
|
|
304
|
+
MCP server. `Annotator.toEncoded()`'s smart mode now routes PNG-8
|
|
305
|
+
through the pure-TS Median Cut + Floyd–Steinberg dither at
|
|
306
|
+
`@ingcreators/annot-core/encode/quantize-median-cut` directly.
|
|
307
|
+
|
|
308
|
+
### Removed public API
|
|
309
|
+
- `isImagequantAvailable()` is no longer exported from
|
|
310
|
+
`@ingcreators/annot-annotator`. PNG-8 is now unconditionally
|
|
311
|
+
available — callers that previously gated `format: "smart"` on
|
|
312
|
+
this can drop the check.
|
|
313
|
+
- `Annotator.toEncoded()` no longer emits
|
|
314
|
+
`EncodeResult.reason === "imagequant-missing"`. The
|
|
315
|
+
graceful-PNG-32-fallback path that produced this reason is
|
|
316
|
+
unreachable.
|
|
317
|
+
|
|
318
|
+
### Removed dependency
|
|
319
|
+
- `@ingcreators/annot-imagequant` is dropped from
|
|
320
|
+
`@ingcreators/annot-annotator`'s `dependencies`. Consumers
|
|
321
|
+
that previously installed it as a side-effect of installing
|
|
322
|
+
`annot-annotator` will save the WASM payload from their
|
|
323
|
+
`node_modules`. `annot-mcp` inherits the removal transitively.
|
|
324
|
+
|
|
325
|
+
Phase 3 of `docs/plans/replace-libimagequant-with-median-cut.md`.
|
|
326
|
+
Phase 4 deletes the `@ingcreators/annot-imagequant` workspace
|
|
327
|
+
package and deprecates the published 0.1.0 on npm.
|
|
328
|
+
|
|
329
|
+
- df1a429: **`@ingcreators/annot-annotator` — new `Annotator.toEditablePng()`
|
|
330
|
+
method** that returns a re-editable PNG. The bytes carry the same
|
|
331
|
+
visible pixels as `toPng()` plus the original un-annotated capture +
|
|
332
|
+
the annotations SVG embedded in the PNG's XMP / custom `svGo` chunk.
|
|
333
|
+
Re-opening the file in the Annot editor (or `annot.work/app/`)
|
|
334
|
+
restores the annotations as selectable / movable / restylable
|
|
335
|
+
objects rather than a flat bitmap.
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const annotator = createAnnotator();
|
|
339
|
+
const editablePng = annotator.toEditablePng({
|
|
340
|
+
originalDataUrl,
|
|
341
|
+
annotationsSvg,
|
|
342
|
+
width,
|
|
343
|
+
height,
|
|
344
|
+
tags: {
|
|
345
|
+
source: "playwright-fixture",
|
|
346
|
+
capturedAt: new Date().toISOString(),
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
await writeFile("shot.png", editablePng);
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Image viewers that don't know about the custom chunks display the
|
|
353
|
+
rasterised pixels verbatim — no compatibility loss vs `toPng()`.
|
|
354
|
+
|
|
355
|
+
The existing `toPng()` / `toSvg()` / `toEncoded()` methods are
|
|
356
|
+
unchanged — `toEditablePng()` is purely additive.
|
|
357
|
+
|
|
358
|
+
**`@ingcreators/annot-core` — new `/xmp-bytes` Tier-A subpath**
|
|
359
|
+
exposing the pure-bytes XMP encode / decode primitives that used to
|
|
360
|
+
live (Blob-wrapped) inside `/xmp`:
|
|
361
|
+
- `createEditablePngBytes(opts) -> Uint8Array` — write a re-editable
|
|
362
|
+
PNG. Takes raw PNG bytes for both the rasterised image and the
|
|
363
|
+
original capture; no `Blob` / `FileReader` dependency. The
|
|
364
|
+
function the new `Annotator.toEditablePng()` is built on.
|
|
365
|
+
- `readEditablePngBytes(data) -> AnnotMetadata | null` — PNG-only
|
|
366
|
+
reader.
|
|
367
|
+
- `readEditableImage(data) -> AnnotMetadata | null` — dual PNG /
|
|
368
|
+
JPEG reader (moved here from `/xmp`, also re-exported from `/xmp`
|
|
369
|
+
for source-compat).
|
|
370
|
+
- `WELL_KNOWN_TAG_KEYS` — soft-convention key names for the
|
|
371
|
+
optional `tags` field (`source` / `screen` / `capturedAt` /
|
|
372
|
+
`commit`).
|
|
373
|
+
|
|
374
|
+
Existing `@ingcreators/annot-core/xmp` consumers stay working
|
|
375
|
+
without source changes — `xmp-browser.ts` re-exports the Tier-A
|
|
376
|
+
surface alongside its Blob-wrapped `createEditableImage`.
|
|
377
|
+
|
|
3
378
|
## 0.3.0
|
|
4
379
|
|
|
5
380
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -129,7 +129,7 @@ See also:
|
|
|
129
129
|
- [`docs/plans/_done/headless-annotator-spike.md`](../../docs/plans/_done/headless-annotator-spike.md)
|
|
130
130
|
— Phase 0 feasibility report (`@resvg/resvg-js` vs
|
|
131
131
|
`@napi-rs/canvas` trade-offs; documented gaps).
|
|
132
|
-
- [`docs/plans/annot-annotator-package.md`](../../docs/plans/annot-annotator-package.md)
|
|
132
|
+
- [`docs/plans/_done/annot-annotator-package.md`](../../docs/plans/_done/annot-annotator-package.md)
|
|
133
133
|
— Phase 1 design + rationale for the public API shape.
|
|
134
134
|
- [`PRODUCT_DIRECTION.md`](../../PRODUCT_DIRECTION.md) — strategic
|
|
135
135
|
context (Playwright + GitHub vectors).
|
package/dist/annotator.d.ts
CHANGED
|
@@ -20,6 +20,22 @@ export interface AnnotatorInput {
|
|
|
20
20
|
/** Output PNG height in pixels — should match the source bitmap. */
|
|
21
21
|
height: number;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Input for {@link Annotator.toEditablePng}. Same shape as
|
|
25
|
+
* {@link AnnotatorInput} plus optional opaque kv `tags` written into
|
|
26
|
+
* the embedded XMP for provenance / debugging.
|
|
27
|
+
*
|
|
28
|
+
* Soft-convention key names (not validated by the writer; documented
|
|
29
|
+
* in `@ingcreators/annot-core/xmp-bytes`'s `WELL_KNOWN_TAG_KEYS`):
|
|
30
|
+
* - `source` — what produced the PNG (`"docs-tour"`, `"playwright-fixture"`, `"annot-mcp"`).
|
|
31
|
+
* - `screen` — for living-product-docs, the `<Screen id>` value.
|
|
32
|
+
* - `capturedAt` — ISO timestamp.
|
|
33
|
+
* - `commit` — git SHA when applicable.
|
|
34
|
+
*/
|
|
35
|
+
export interface EditableInput extends AnnotatorInput {
|
|
36
|
+
/** Optional opaque kv tags. */
|
|
37
|
+
tags?: Record<string, string>;
|
|
38
|
+
}
|
|
23
39
|
/** Annotator construction options. All optional. */
|
|
24
40
|
export interface AnnotatorOptions {
|
|
25
41
|
/**
|
|
@@ -57,8 +73,12 @@ export interface AnnotatorOptions {
|
|
|
57
73
|
* - {@link toEncoded} (since 0.3.0) async rasterise + smart
|
|
58
74
|
* encode pipeline — `saveSizePreset` resize,
|
|
59
75
|
* `format: "smart" | "png" | "jpeg"` decision
|
|
60
|
-
* tree, PNG-8 quantization via the
|
|
61
|
-
*
|
|
76
|
+
* tree, PNG-8 quantization via the in-tree
|
|
77
|
+
* pure-TS Median Cut + Floyd–Steinberg dither
|
|
78
|
+
* (post-Phase 3 of
|
|
79
|
+
* `docs/plans/_done/replace-libimagequant-with-median-cut.md`;
|
|
80
|
+
* prior versions used the GPL-3.0
|
|
81
|
+
* `@ingcreators/annot-imagequant` WASM).
|
|
62
82
|
*/
|
|
63
83
|
export interface Annotator {
|
|
64
84
|
/**
|
|
@@ -74,6 +94,20 @@ export interface Annotator {
|
|
|
74
94
|
* the caller can feed to any other SVG tool.
|
|
75
95
|
*/
|
|
76
96
|
toSvg(input: AnnotatorInput): string;
|
|
97
|
+
/**
|
|
98
|
+
* Rasterise the input to a re-editable PNG. Same visible pixels as
|
|
99
|
+
* {@link toPng}, plus the original un-annotated capture + the
|
|
100
|
+
* annotations SVG are embedded in the PNG's XMP metadata + custom
|
|
101
|
+
* `svGo` chunk. Re-opening the resulting file in the Annot editor
|
|
102
|
+
* (or `annot.work/app/`) restores the annotations as selectable,
|
|
103
|
+
* movable, restylable objects rather than a flat bitmap.
|
|
104
|
+
*
|
|
105
|
+
* Image viewers that don't know about the custom chunks display the
|
|
106
|
+
* rasterised pixels verbatim — no compatibility loss vs `toPng`.
|
|
107
|
+
*
|
|
108
|
+
* Synchronous. Returns PNG bytes as a `Uint8Array`.
|
|
109
|
+
*/
|
|
110
|
+
toEditablePng(input: EditableInput): Uint8Array;
|
|
77
111
|
/**
|
|
78
112
|
* Rasterise + encode in one step. Honours `saveSizePreset`
|
|
79
113
|
* (max-width resize), `format` (`"smart"` / `"png"` /
|
|
@@ -81,12 +115,14 @@ export interface Annotator {
|
|
|
81
115
|
* a metadata record so the caller can log which format was
|
|
82
116
|
* actually picked.
|
|
83
117
|
*
|
|
84
|
-
* Smart mode
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
118
|
+
* Smart mode emits PNG-8 (via the in-tree Median Cut +
|
|
119
|
+
* Floyd–Steinberg dither at
|
|
120
|
+
* `@ingcreators/annot-core/encode/quantize-median-cut`) for
|
|
121
|
+
* UI-heavy content, falling back to PNG-32 / JPEG for
|
|
122
|
+
* photo-heavy content per `smartFallback`. PNG-8 is
|
|
123
|
+
* unconditionally available since Phase 3 of
|
|
124
|
+
* `docs/plans/_done/replace-libimagequant-with-median-cut.md`
|
|
125
|
+
* retired the optional GPL-3.0 imagequant WASM dependency.
|
|
90
126
|
*/
|
|
91
127
|
toEncoded(input: AnnotatorInput, encodeOptions?: EncodeOptions): Promise<EncodeResult>;
|
|
92
128
|
}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BBox } from './dsl/types.js';
|
|
2
|
+
export interface DiffResult {
|
|
3
|
+
/** Number of mismatched pixels. */
|
|
4
|
+
mismatchedPixels: number;
|
|
5
|
+
/** Bounding boxes of contiguous changed regions. */
|
|
6
|
+
regions: BBox[];
|
|
7
|
+
/** Dimensions of the input pair. */
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
}
|
|
11
|
+
export interface DiffOptions {
|
|
12
|
+
/** Matching threshold (0 to 1); smaller is more sensitive. */
|
|
13
|
+
threshold?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare class DimensionMismatchError extends Error {
|
|
16
|
+
constructor(a: {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
}, b: {
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Compare two PNGs pixel-by-pixel and return the changed-region
|
|
26
|
+
* bboxes. Throws `DimensionMismatchError` when the inputs have
|
|
27
|
+
* different sizes — the caller has to capture both at the same
|
|
28
|
+
* viewport for the comparison to make sense.
|
|
29
|
+
*/
|
|
30
|
+
export declare function diffScreenshots(before: Uint8Array, after: Uint8Array, options?: DiffOptions): Promise<DiffResult>;
|
package/dist/dsl/schema.d.ts
CHANGED
|
@@ -206,6 +206,74 @@ export declare const BBOX_ANNOTATION_SCHEMA: {
|
|
|
206
206
|
type: string;
|
|
207
207
|
};
|
|
208
208
|
};
|
|
209
|
+
} | {
|
|
210
|
+
type: string;
|
|
211
|
+
required: string[];
|
|
212
|
+
additionalProperties: boolean;
|
|
213
|
+
properties: {
|
|
214
|
+
type: {
|
|
215
|
+
const: string;
|
|
216
|
+
};
|
|
217
|
+
path: {
|
|
218
|
+
type: string;
|
|
219
|
+
minLength: number;
|
|
220
|
+
};
|
|
221
|
+
intent: {
|
|
222
|
+
$ref: string;
|
|
223
|
+
};
|
|
224
|
+
stroke: {
|
|
225
|
+
type: string;
|
|
226
|
+
};
|
|
227
|
+
strokeWidth: {
|
|
228
|
+
type: string;
|
|
229
|
+
minimum: number;
|
|
230
|
+
};
|
|
231
|
+
fill: {
|
|
232
|
+
type: string;
|
|
233
|
+
};
|
|
234
|
+
color: {
|
|
235
|
+
type: string;
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
} | {
|
|
239
|
+
type: string;
|
|
240
|
+
required: string[];
|
|
241
|
+
additionalProperties: boolean;
|
|
242
|
+
properties: {
|
|
243
|
+
type: {
|
|
244
|
+
const: string;
|
|
245
|
+
};
|
|
246
|
+
cutout: {
|
|
247
|
+
$ref: string;
|
|
248
|
+
};
|
|
249
|
+
imageWidth: {
|
|
250
|
+
type: string;
|
|
251
|
+
minimum: number;
|
|
252
|
+
};
|
|
253
|
+
imageHeight: {
|
|
254
|
+
type: string;
|
|
255
|
+
minimum: number;
|
|
256
|
+
};
|
|
257
|
+
dimColor: {
|
|
258
|
+
type: string;
|
|
259
|
+
};
|
|
260
|
+
intent: {
|
|
261
|
+
$ref: string;
|
|
262
|
+
};
|
|
263
|
+
stroke: {
|
|
264
|
+
type: string;
|
|
265
|
+
};
|
|
266
|
+
strokeWidth: {
|
|
267
|
+
type: string;
|
|
268
|
+
minimum: number;
|
|
269
|
+
};
|
|
270
|
+
fill: {
|
|
271
|
+
type: string;
|
|
272
|
+
};
|
|
273
|
+
color: {
|
|
274
|
+
type: string;
|
|
275
|
+
};
|
|
276
|
+
};
|
|
209
277
|
} | {
|
|
210
278
|
type: string;
|
|
211
279
|
required: string[];
|
package/dist/dsl/types.d.ts
CHANGED
|
@@ -52,11 +52,93 @@ export type BboxCalloutAnnotation = AnnotationStyle & {
|
|
|
52
52
|
targetBbox: BBox;
|
|
53
53
|
content: string;
|
|
54
54
|
};
|
|
55
|
+
/**
|
|
56
|
+
* Which corner of the target bbox the badge sits at. The badge
|
|
57
|
+
* is centred on the corner — half inside the rect, half outside —
|
|
58
|
+
* so the legend number remains readable against either light or
|
|
59
|
+
* dark target content. `"auto"` (the default) picks the corner
|
|
60
|
+
* that's furthest from the supplied image edge so the badge
|
|
61
|
+
* never clips off the screenshot.
|
|
62
|
+
*/
|
|
63
|
+
export type BadgePlacement = "auto" | "topLeft" | "topRight" | "bottomLeft" | "bottomRight";
|
|
64
|
+
/**
|
|
65
|
+
* Numbered legend badge — target rect outline plus a filled
|
|
66
|
+
* intent-coloured circle at one corner with a bold white number
|
|
67
|
+
* inside. The visual idiom for "this is item N in a step-by-step
|
|
68
|
+
* legend over a screenshot."
|
|
69
|
+
*
|
|
70
|
+
* Differs from `callout` in two ways:
|
|
71
|
+
* 1. No caption arrow — the badge sits ON the target, not next to it.
|
|
72
|
+
* 2. The number renders inside a sized circle, not as bare `<text>`,
|
|
73
|
+
* so it stays readable when the screenshot is scaled down in
|
|
74
|
+
* docs / slides.
|
|
75
|
+
*
|
|
76
|
+
* When `imageWidth` / `imageHeight` are supplied alongside
|
|
77
|
+
* `placement: "auto"`, the renderer picks the corner furthest
|
|
78
|
+
* from the image edge so the badge never clips. Without those,
|
|
79
|
+
* `"auto"` falls back to `"topRight"`.
|
|
80
|
+
*/
|
|
81
|
+
export type BboxNumberedBadgeAnnotation = AnnotationStyle & {
|
|
82
|
+
type: "numberedBadge";
|
|
83
|
+
bbox: BBox;
|
|
84
|
+
number: number;
|
|
85
|
+
/** Override the corner. Default `"auto"`. */
|
|
86
|
+
placement?: BadgePlacement;
|
|
87
|
+
/** Badge diameter in image pixels. Default `40`. */
|
|
88
|
+
badgeSize?: number;
|
|
89
|
+
/**
|
|
90
|
+
* Image dimensions in page pixels — used by `placement: "auto"`
|
|
91
|
+
* to pick the corner furthest from the image edge. When omitted,
|
|
92
|
+
* `"auto"` resolves to `"topRight"`.
|
|
93
|
+
*/
|
|
94
|
+
imageWidth?: number;
|
|
95
|
+
imageHeight?: number;
|
|
96
|
+
};
|
|
55
97
|
export interface RawAnnotation {
|
|
56
98
|
type: "raw";
|
|
57
99
|
svgFragment: string;
|
|
58
100
|
}
|
|
59
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Free-form path. The `path` field carries the SVG `<path>` d
|
|
103
|
+
* attribute verbatim — `M` / `L` / `C` / `Q` / `Z` commands.
|
|
104
|
+
*
|
|
105
|
+
* Style fields are interpreted as stroke-only by default
|
|
106
|
+
* (`fill: "none"` unless overridden) so a freehand stroke
|
|
107
|
+
* doesn't accidentally fill its own enclosed area.
|
|
108
|
+
*
|
|
109
|
+
* Phase 3b of
|
|
110
|
+
* [`docs/plans/living-spec-authoring-roadmap.md`](../../../../docs/plans/living-spec-authoring-roadmap.md).
|
|
111
|
+
*/
|
|
112
|
+
export type BboxFreehandAnnotation = AnnotationStyle & {
|
|
113
|
+
type: "freehand";
|
|
114
|
+
path: string;
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Focus mask — dim the entire image area EXCEPT the cutout
|
|
118
|
+
* region. The cutout draws crisp; everything outside is filled
|
|
119
|
+
* with `dimColor` (default `rgba(0,0,0,0.5)`).
|
|
120
|
+
*
|
|
121
|
+
* Implemented as a single `<path>` element with `fill-rule="evenodd"`
|
|
122
|
+
* combining a full-image rect with the cutout rect: the
|
|
123
|
+
* even-odd rule cancels overlap, leaving the cutout area
|
|
124
|
+
* transparent on top of the dimmed background.
|
|
125
|
+
*
|
|
126
|
+
* `imageWidth` / `imageHeight` are required so the outer rect
|
|
127
|
+
* covers the full screenshot; the renderer rejects a partial
|
|
128
|
+
* focus mask. Phase 3b of
|
|
129
|
+
* [`docs/plans/living-spec-authoring-roadmap.md`](../../../../docs/plans/living-spec-authoring-roadmap.md).
|
|
130
|
+
*/
|
|
131
|
+
export type BboxFocusMaskAnnotation = AnnotationStyle & {
|
|
132
|
+
type: "focusMask";
|
|
133
|
+
/** The crisp cutout region. */
|
|
134
|
+
cutout: BBox;
|
|
135
|
+
/** Full-image dimensions — required so the outer rect covers everything. */
|
|
136
|
+
imageWidth: number;
|
|
137
|
+
imageHeight: number;
|
|
138
|
+
/** CSS colour for the dim layer. Default `rgba(0,0,0,0.5)`. */
|
|
139
|
+
dimColor?: string;
|
|
140
|
+
};
|
|
141
|
+
export type BboxAnnotation = BboxRectAnnotation | BboxCircleAnnotation | BboxArrowAnnotation | BboxTextAnnotation | BboxCalloutAnnotation | BboxNumberedBadgeAnnotation | BboxFreehandAnnotation | BboxFocusMaskAnnotation | RawAnnotation;
|
|
60
142
|
export type RedactStyle = "solid" | "mosaic" | "blur";
|
|
61
143
|
export interface BboxRedactRegion {
|
|
62
144
|
bbox: BBox;
|