@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 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).
@@ -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 optional
61
- * `@ingcreators/annot-imagequant`.
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 requires `@ingcreators/annot-imagequant` to be
85
- * available at runtime for PNG-8 output; the WASM ships as a
86
- * regular `dependencies` entry but is dynamic-imported, so a
87
- * consumer who explicitly uninstalls the package (to avoid
88
- * its GPL-3.0 license) gets a graceful fallback to PNG-32
89
- * with `reason: "imagequant-missing"`.
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
  }
@@ -0,0 +1,6 @@
1
+ import { BBox } from './dsl/types.js';
2
+ /**
3
+ * Walk a pixelmatch diff mask and return one bbox per contiguous
4
+ * changed region. 4-connected adjacency.
5
+ */
6
+ export declare function aggregateDiffRegions(diffRgba: Uint8Array, width: number, height: number): BBox[];
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>;
@@ -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[];
@@ -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
- export type BboxAnnotation = BboxRectAnnotation | BboxCircleAnnotation | BboxArrowAnnotation | BboxTextAnnotation | BboxCalloutAnnotation | RawAnnotation;
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;