@ingcreators/annot-annotator 0.3.0 → 0.5.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,85 @@
1
1
  # @ingcreators/annot-annotator
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 806badc: Retire the `@ingcreators/annot-imagequant` (GPL-3.0) dynamic-import
8
+ boundary that gated PNG-8 output in the headless annotator and the
9
+ MCP server. `Annotator.toEncoded()`'s smart mode now routes PNG-8
10
+ through the pure-TS Median Cut + Floyd–Steinberg dither at
11
+ `@ingcreators/annot-core/encode/quantize-median-cut` directly.
12
+
13
+ ### Removed public API
14
+ - `isImagequantAvailable()` is no longer exported from
15
+ `@ingcreators/annot-annotator`. PNG-8 is now unconditionally
16
+ available — callers that previously gated `format: "smart"` on
17
+ this can drop the check.
18
+ - `Annotator.toEncoded()` no longer emits
19
+ `EncodeResult.reason === "imagequant-missing"`. The
20
+ graceful-PNG-32-fallback path that produced this reason is
21
+ unreachable.
22
+
23
+ ### Removed dependency
24
+ - `@ingcreators/annot-imagequant` is dropped from
25
+ `@ingcreators/annot-annotator`'s `dependencies`. Consumers
26
+ that previously installed it as a side-effect of installing
27
+ `annot-annotator` will save the WASM payload from their
28
+ `node_modules`. `annot-mcp` inherits the removal transitively.
29
+
30
+ Phase 3 of `docs/plans/replace-libimagequant-with-median-cut.md`.
31
+ Phase 4 deletes the `@ingcreators/annot-imagequant` workspace
32
+ package and deprecates the published 0.1.0 on npm.
33
+
34
+ - df1a429: **`@ingcreators/annot-annotator` — new `Annotator.toEditablePng()`
35
+ method** that returns a re-editable PNG. The bytes carry the same
36
+ visible pixels as `toPng()` plus the original un-annotated capture +
37
+ the annotations SVG embedded in the PNG's XMP / custom `svGo` chunk.
38
+ Re-opening the file in the Annot editor (or `annot.work/app/`)
39
+ restores the annotations as selectable / movable / restylable
40
+ objects rather than a flat bitmap.
41
+
42
+ ```ts
43
+ const annotator = createAnnotator();
44
+ const editablePng = annotator.toEditablePng({
45
+ originalDataUrl,
46
+ annotationsSvg,
47
+ width,
48
+ height,
49
+ tags: {
50
+ source: "playwright-fixture",
51
+ capturedAt: new Date().toISOString(),
52
+ },
53
+ });
54
+ await writeFile("shot.png", editablePng);
55
+ ```
56
+
57
+ Image viewers that don't know about the custom chunks display the
58
+ rasterised pixels verbatim — no compatibility loss vs `toPng()`.
59
+
60
+ The existing `toPng()` / `toSvg()` / `toEncoded()` methods are
61
+ unchanged — `toEditablePng()` is purely additive.
62
+
63
+ **`@ingcreators/annot-core` — new `/xmp-bytes` Tier-A subpath**
64
+ exposing the pure-bytes XMP encode / decode primitives that used to
65
+ live (Blob-wrapped) inside `/xmp`:
66
+ - `createEditablePngBytes(opts) -> Uint8Array` — write a re-editable
67
+ PNG. Takes raw PNG bytes for both the rasterised image and the
68
+ original capture; no `Blob` / `FileReader` dependency. The
69
+ function the new `Annotator.toEditablePng()` is built on.
70
+ - `readEditablePngBytes(data) -> AnnotMetadata | null` — PNG-only
71
+ reader.
72
+ - `readEditableImage(data) -> AnnotMetadata | null` — dual PNG /
73
+ JPEG reader (moved here from `/xmp`, also re-exported from `/xmp`
74
+ for source-compat).
75
+ - `WELL_KNOWN_TAG_KEYS` — soft-convention key names for the
76
+ optional `tags` field (`source` / `screen` / `capturedAt` /
77
+ `commit`).
78
+
79
+ Existing `@ingcreators/annot-core/xmp` consumers stay working
80
+ without source changes — `xmp-browser.ts` re-exports the Tier-A
81
+ surface alongside its Blob-wrapped `createEditableImage`.
82
+
3
83
  ## 0.3.0
4
84
 
5
85
  ### 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
  }
@@ -52,11 +52,53 @@ 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
+ export type BboxAnnotation = BboxRectAnnotation | BboxCircleAnnotation | BboxArrowAnnotation | BboxTextAnnotation | BboxCalloutAnnotation | BboxNumberedBadgeAnnotation | RawAnnotation;
60
102
  export type RedactStyle = "solid" | "mosaic" | "blur";
61
103
  export interface BboxRedactRegion {
62
104
  bbox: BBox;
@@ -16,10 +16,10 @@ import { EncodeResult } from './options.js';
16
16
  * - Sample unique-colour count via {@link isPhotoHeavy}.
17
17
  * If photo-heavy, emit `smartFallback` (PNG-32 or
18
18
  * JPEG) with reason `"photo-fallback-*"`.
19
- * - Otherwise quantize to ≤256 colours via libimagequant
20
- * (if installed) and emit PNG-8 with reason `"png-8"`.
21
- * When the optional imagequant WASM isn't installed,
22
- * fall back to PNG-32 with `reason: "imagequant-missing"`.
19
+ * - Otherwise quantize to ≤256 colours via the
20
+ * in-tree Median Cut + FS dither and emit PNG-8 with
21
+ * reason `"png-8"`. PNG-8 is unconditionally
22
+ * available post-Phase 3 (no optional WASM gate).
23
23
  */
24
24
  export declare function encodeRgba(rgba: Uint8Array, width: number, height: number, options?: EncodeOptions): Promise<EncodeResult>;
25
25
  /**
@@ -1,12 +1,10 @@
1
1
  /**
2
- * Quantize RGBA pixels to ≤256 colours via libimagequant and emit
3
- * a PNG-8 file. Returns the encoded bytes, OR `null` if the
4
- * optional imagequant module isn't installed.
2
+ * Quantize RGBA pixels to ≤256 colours via Median Cut + Floyd–
3
+ * Steinberg dither and emit a PNG-8 file.
5
4
  *
6
- * The caller is responsible for falling back to a non-PNG-8 path
7
- * (PNG-32 or JPEG) when this returns `null`.
5
+ * Synchronous, deterministic, GPL-free.
8
6
  */
9
- export declare function quantizeRgbaToPng8(rgba: Uint8Array, width: number, height: number): Promise<Uint8Array | null>;
7
+ export declare function quantizeRgbaToPng8(rgba: Uint8Array, width: number, height: number): Uint8Array;
10
8
  /**
11
9
  * Heuristic: does this image look photo-heavy?
12
10
  *
@@ -17,7 +15,3 @@ export declare function quantizeRgbaToPng8(rgba: Uint8Array, width: number, heig
17
15
  * colours; pages with rich photography return >20,000.
18
16
  */
19
17
  export declare function isPhotoHeavy(rgba: Uint8Array, threshold: number): boolean;
20
- /** Whether the optional imagequant WASM is available at runtime.
21
- * Useful for callers that want to decide ahead of time whether to
22
- * request `format: "smart"`. */
23
- export declare function isImagequantAvailable(): Promise<boolean>;
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { type Annotator, type AnnotatorInput, type AnnotatorOptions, createAnnotator, } from './annotator.js';
1
+ export { type Annotator, type AnnotatorInput, type AnnotatorOptions, createAnnotator, type EditableInput, } from './annotator.js';
2
2
  export { decodeAndEncodeImage, encodeRgba } from './encode/encode.js';
3
3
  export { type BrowserEncodeResult, computeResizeTarget, DEFAULT_ENCODE_OPTIONS, type EncodeFormat, type EncodeOptions, type EncodeResult, SAVE_SIZE_LABEL, SAVE_SIZE_MAX_WIDTH, type SaveSizePreset, } from './encode/options.js';
4
- export { isImagequantAvailable, isPhotoHeavy } from './encode/quantize.js';
4
+ export { isPhotoHeavy } from './encode/quantize.js';
5
5
  export { BBOX_ANNOTATION_SCHEMA, BBOX_REDACT_REGION_SCHEMA, SHARED_DEFS, } from './dsl/schema.js';
6
6
  export { type ArrowOptions, arrowBetween, type BoundingBox, type RectOptions, rectForBoundingBox, type TextOptions, textAt, } from './dsl/svg-primitives.js';
7
7
  export { bboxAnnotationsToSvg } from './dsl/to-svg.js';
8
- export type { AnnotationStyle, BBox, BboxAnnotation, BboxArrowAnnotation, BboxCalloutAnnotation, BboxCircleAnnotation, BboxRectAnnotation, BboxRedactRegion, BboxTextAnnotation, Intent, Point, RawAnnotation, RedactStyle, } from './dsl/types.js';
8
+ export type { AnnotationStyle, BadgePlacement, BBox, BboxAnnotation, BboxArrowAnnotation, BboxCalloutAnnotation, BboxCircleAnnotation, BboxNumberedBadgeAnnotation, BboxRectAnnotation, BboxRedactRegion, BboxTextAnnotation, Intent, Point, RawAnnotation, RedactStyle, } from './dsl/types.js';