@ingcreators/annot-annotator 0.2.0 → 0.3.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,34 @@
1
1
  # @ingcreators/annot-annotator
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 86d0853: `Annotator.toEncoded(input, encodeOptions?)` — new async method that combines rasterise + smart-encode in one step. Achieves feature parity with Chrome extension's "Save size" + "Format smart" capture options for the manual-creation / agent use cases.
8
+
9
+ **EncodeOptions** (re-exported from `@ingcreators/annot-core/encode/options`):
10
+ - `format: "smart" | "png" | "jpeg"` — Smart picks PNG-8 for UI-heavy content (via libimagequant) and JPEG / PNG-32 for photo-heavy content per `smartFallback`. PNG / JPEG paths skip the smart heuristic.
11
+ - `saveSizePreset: "light" | "standard" | "highQuality" | "original"` — Max-width caps at 1280 / 1920 / 2560 / no-resize. Aspect-preserving; never upscales.
12
+ - `smartFallback: "png" | "jpeg"` — Photo-heavy fallback format.
13
+ - `smartColorThreshold: number` — Unique-colour count threshold (default 15000). Above this the image is treated as photo-heavy.
14
+ - `jpegPercent: number` — JPEG quality 60–100 (default 92).
15
+
16
+ Returns `{ bytes, chosen, reason?, width, height }` so callers can log which format was actually chosen (`"png-8"` / `"photo-fallback-jpeg"` / `"imagequant-missing"` etc.).
17
+
18
+ Standalone `encodeRgba(rgba, width, height, options)` also exported for callers who already have raw RGBA bytes (e.g. from a Playwright screenshot fed through their own canvas).
19
+
20
+ **New runtime dependencies:**
21
+ - `@napi-rs/canvas` (regular `dependencies`) — native canvas binding for PNG / JPEG encoding + resize. ~20 MB platform-matched binary at install time.
22
+ - `@ingcreators/annot-imagequant` (regular `dependencies`) — GPL-3.0 WASM wrapper around libimagequant for PNG-8 quantization. Loaded via dynamic import; consumers who explicitly uninstall the package to avoid the GPL inclusion get a graceful fallback to PNG-32 (`reason: "imagequant-missing"`).
23
+ - `pako` (regular `dependencies`) — pulled in transitively via the PNG-8 encoder; declared explicitly so consumer installs are deterministic.
24
+
25
+ The existing `toPng()` and `toSvg()` methods are unchanged — `toEncoded()` is purely additive.
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies [adae49d]
30
+ - @ingcreators/annot-imagequant@0.1.0
31
+
3
32
  ## 0.2.0
4
33
 
5
34
  ### Minor Changes
@@ -1,3 +1,5 @@
1
+ import { EncodeOptions } from '@ingcreators/annot-core/encode/options';
2
+ import { EncodeResult } from './encode/options.js';
1
3
  /**
2
4
  * Structural shape the annotator accepts. A real `ImageRecord`
3
5
  * from `@ingcreators/annot-core/storage` satisfies this — the
@@ -47,16 +49,23 @@ export interface AnnotatorOptions {
47
49
  defaultFontFamily?: string;
48
50
  }
49
51
  /**
50
- * Annotator instance returned by {@link createAnnotator}. Use
51
- * `toPng` to rasterise; use `toSvg` to get the merged SVG without
52
- * rasterisation (useful for piping into another tool, or for
53
- * inspecting what the annotator would render).
52
+ * Annotator instance returned by {@link createAnnotator}.
53
+ *
54
+ * - {@link toPng} synchronous rasterise PNG-32 bytes.
55
+ * - {@link toSvg} build the rasterise-ready SVG string (no
56
+ * rasterisation).
57
+ * - {@link toEncoded} (since 0.3.0) async rasterise + smart
58
+ * encode pipeline — `saveSizePreset` resize,
59
+ * `format: "smart" | "png" | "jpeg"` decision
60
+ * tree, PNG-8 quantization via the optional
61
+ * `@ingcreators/annot-imagequant`.
54
62
  */
55
63
  export interface Annotator {
56
64
  /**
57
- * Rasterise the input to a PNG byte array. JPEG output is not
58
- * yet supported Phase 1.5 brings `sharp` as an optional peer
59
- * dep.
65
+ * Rasterise the input to a PNG-32 byte array. Synchronous, no
66
+ * resize, no smart format selection the most direct path.
67
+ * Use {@link toEncoded} when you need `saveSize` / format
68
+ * decisions.
60
69
  */
61
70
  toPng(input: AnnotatorInput): Uint8Array;
62
71
  /**
@@ -65,6 +74,21 @@ export interface Annotator {
65
74
  * the caller can feed to any other SVG tool.
66
75
  */
67
76
  toSvg(input: AnnotatorInput): string;
77
+ /**
78
+ * Rasterise + encode in one step. Honours `saveSizePreset`
79
+ * (max-width resize), `format` (`"smart"` / `"png"` /
80
+ * `"jpeg"`), and `jpegPercent`. Returns the encoded bytes plus
81
+ * a metadata record so the caller can log which format was
82
+ * actually picked.
83
+ *
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"`.
90
+ */
91
+ toEncoded(input: AnnotatorInput, encodeOptions?: EncodeOptions): Promise<EncodeResult>;
68
92
  }
69
93
  /**
70
94
  * Construct a headless annotator. Options are forwarded to the
@@ -0,0 +1,37 @@
1
+ import { EncodeOptions } from '@ingcreators/annot-core/encode/options';
2
+ import { EncodeResult } from './options.js';
3
+ /**
4
+ * Encode raw RGBA pixels per the requested {@link EncodeOptions}.
5
+ *
6
+ * Algorithm (mirrors the browser-side pipeline):
7
+ *
8
+ * 1. Resize the source via `saveSizePreset` (aspect-preserving;
9
+ * never upscale).
10
+ * 2. Branch on `format`:
11
+ * - `"png"` → PNG-32 encode (lossless).
12
+ * - `"jpeg"` → JPEG encode at `jpegPercent`.
13
+ * - `"smart"`:
14
+ * - If pixel count > `MAX_SMART_PIXELS`, return PNG-32
15
+ * with `reason: "too-large-for-png8"`.
16
+ * - Sample unique-colour count via {@link isPhotoHeavy}.
17
+ * If photo-heavy, emit `smartFallback` (PNG-32 or
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"`.
23
+ */
24
+ export declare function encodeRgba(rgba: Uint8Array, width: number, height: number, options?: EncodeOptions): Promise<EncodeResult>;
25
+ /**
26
+ * Decode a PNG / JPEG byte stream into RGBA and re-encode it
27
+ * through {@link encodeRgba}. Useful for downstream pipelines
28
+ * that emit a PNG first (e.g. the redact-burn output or a
29
+ * Playwright `page.screenshot()` capture) and want to apply
30
+ * `saveSize` / `format` after the fact.
31
+ *
32
+ * The decode goes through `@napi-rs/canvas`'s `loadImage`, so
33
+ * any format the underlying Skia decoder understands (PNG /
34
+ * JPEG / WebP / AVIF on the canvas's supported list) flows
35
+ * through transparently.
36
+ */
37
+ export declare function decodeAndEncodeImage(imageBytes: Uint8Array, options?: EncodeOptions): Promise<EncodeResult>;
@@ -0,0 +1,19 @@
1
+ export type { EncodeFormat, EncodeOptions, EncodeResult as BrowserEncodeResult, SaveSizePreset, } from '@ingcreators/annot-core/encode/options';
2
+ export { computeResizeTarget, DEFAULT_ENCODE_OPTIONS, SAVE_SIZE_LABEL, SAVE_SIZE_MAX_WIDTH, } from '@ingcreators/annot-core/encode/options';
3
+ /**
4
+ * Node-side encode result. Returns raw bytes (vs the browser-side
5
+ * counterpart returning a `data:` URL string) — agents and test
6
+ * runtimes typically want bytes to attach to GitHub issues / test
7
+ * reports / write to disk.
8
+ */
9
+ export interface EncodeResult {
10
+ /** Encoded image bytes (PNG-8 / PNG-32 / JPEG depending on `chosen`). */
11
+ bytes: Uint8Array;
12
+ /** Actual format chosen (may differ from requested in smart mode). */
13
+ chosen: "png" | "jpeg";
14
+ /** Human-readable note (`"png-8"` / `"photo-fallback-jpeg"` / …). */
15
+ reason?: string;
16
+ /** Final pixel dimensions after `saveSizePreset` resize. */
17
+ width: number;
18
+ height: number;
19
+ }
@@ -0,0 +1,23 @@
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.
5
+ *
6
+ * The caller is responsible for falling back to a non-PNG-8 path
7
+ * (PNG-32 or JPEG) when this returns `null`.
8
+ */
9
+ export declare function quantizeRgbaToPng8(rgba: Uint8Array, width: number, height: number): Promise<Uint8Array | null>;
10
+ /**
11
+ * Heuristic: does this image look photo-heavy?
12
+ *
13
+ * Samples ~50,000 pixels from the RGBA buffer (stride-walks rather
14
+ * than reading every pixel — sufficient for the bimodal "UI vs
15
+ * photo" decision). Returns `true` if the unique-color count
16
+ * exceeds `threshold`. Typical UI screenshots return <5,000 unique
17
+ * colours; pages with rich photography return >20,000.
18
+ */
19
+ 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,4 +1,7 @@
1
1
  export { type Annotator, type AnnotatorInput, type AnnotatorOptions, createAnnotator, } from './annotator.js';
2
+ export { decodeAndEncodeImage, encodeRgba } from './encode/encode.js';
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';
2
5
  export { BBOX_ANNOTATION_SCHEMA, BBOX_REDACT_REGION_SCHEMA, SHARED_DEFS, } from './dsl/schema.js';
3
6
  export { type ArrowOptions, arrowBetween, type BoundingBox, type RectOptions, rectForBoundingBox, type TextOptions, textAt, } from './dsl/svg-primitives.js';
4
7
  export { bboxAnnotationsToSvg } from './dsl/to-svg.js';
package/dist/index.js CHANGED
@@ -1,10 +1,199 @@
1
- import { Resvg as b } from "@resvg/resvg-js";
2
- import { DOMParser as w, XMLSerializer as B } from "@xmldom/xmldom";
3
- const S = "1", N = "data-annot-version";
4
- function T(e) {
1
+ import { Resvg as P } from "@resvg/resvg-js";
2
+ import { loadImage as R, createCanvas as m } from "@napi-rs/canvas";
3
+ import { deflate as L } from "pako";
4
+ import { DOMParser as j, XMLSerializer as q } from "@xmldom/xmldom";
5
+ const W = "1", M = "data-annot-version", X = {
6
+ light: 1280,
7
+ standard: 1920,
8
+ highQuality: 2560,
9
+ original: null
10
+ }, Se = {
11
+ light: "Light (1280px)",
12
+ standard: "Standard (1920px)",
13
+ highQuality: "High Quality (2560px)",
14
+ original: "Original"
15
+ }, C = {
16
+ format: "smart",
17
+ smartFallback: "png",
18
+ smartColorThreshold: 15e3,
19
+ jpegPercent: 92,
20
+ saveSizePreset: "standard"
21
+ };
22
+ function V(e, t, r) {
23
+ const n = X[r];
24
+ if (n === null || e <= n)
25
+ return { width: e, height: t, scaled: !1 };
26
+ const o = n / e;
27
+ return {
28
+ width: n,
29
+ height: Math.max(1, Math.round(t * o)),
30
+ scaled: !0
31
+ };
32
+ }
33
+ const G = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]), Q = (() => {
34
+ const e = new Uint32Array(256);
35
+ for (let t = 0; t < 256; t++) {
36
+ let r = t;
37
+ for (let n = 0; n < 8; n++)
38
+ r = r & 1 ? 3988292384 ^ r >>> 1 : r >>> 1;
39
+ e[t] = r >>> 0;
40
+ }
41
+ return e;
42
+ })();
43
+ function H(e) {
44
+ let t = 4294967295;
45
+ for (let r = 0; r < e.length; r++)
46
+ t = Q[(t ^ e[r]) & 255] ^ t >>> 8;
47
+ return (t ^ 4294967295) >>> 0;
48
+ }
49
+ function h(e, t) {
50
+ const r = new Uint8Array(12 + t.length), n = new DataView(r.buffer);
51
+ n.setUint32(0, t.length, !1);
52
+ for (let i = 0; i < 4; i++) r[4 + i] = e.charCodeAt(i);
53
+ r.set(t, 8);
54
+ const o = r.subarray(4, 8 + t.length);
55
+ return n.setUint32(8 + t.length, H(o), !1), r;
56
+ }
57
+ function Z(e, t, r, n, o = 9) {
58
+ if (r <= 0 || n <= 0) throw new Error("encodePng8: invalid dimensions");
59
+ if (e.length % 4 !== 0)
60
+ throw new Error("encodePng8: palette must be RGBA bytes (multiple of 4)");
61
+ const i = e.length >>> 2;
62
+ if (i < 1 || i > 256)
63
+ throw new Error(`encodePng8: palette must have 1–256 colors, got ${i}`);
64
+ if (t.length !== r * n)
65
+ throw new Error(
66
+ `encodePng8: indices length ${t.length} != width*height ${r * n}`
67
+ );
68
+ const s = new Uint8Array(13), a = new DataView(s.buffer);
69
+ a.setUint32(0, r, !1), a.setUint32(4, n, !1), s[8] = 8, s[9] = 3, s[10] = 0, s[11] = 0, s[12] = 0;
70
+ const c = new Uint8Array(i * 3);
71
+ let u = !1, f = -1;
72
+ for (let l = 0; l < i; l++)
73
+ c[l * 3] = e[l * 4], c[l * 3 + 1] = e[l * 4 + 1], c[l * 3 + 2] = e[l * 4 + 2], e[l * 4 + 3] < 255 && (u = !0, f = l);
74
+ let g = null;
75
+ if (u) {
76
+ g = new Uint8Array(f + 1);
77
+ for (let l = 0; l <= f; l++) g[l] = e[l * 4 + 3];
78
+ }
79
+ const A = r + 1, x = new Uint8Array(A * n);
80
+ for (let l = 0; l < n; l++) {
81
+ const T = l * A;
82
+ x[T] = 0, x.set(t.subarray(l * r, l * r + r), T + 1);
83
+ }
84
+ const U = L(x, { level: o, memLevel: 9, windowBits: 15 }), d = [G, h("IHDR", s), h("PLTE", c)];
85
+ g && d.push(h("tRNS", g)), d.push(h("IDAT", U)), d.push(h("IEND", new Uint8Array(0)));
86
+ let I = 0;
87
+ for (const l of d) I += l.length;
88
+ const S = new Uint8Array(I);
89
+ let v = 0;
90
+ for (const l of d)
91
+ S.set(l, v), v += l.length;
92
+ return S;
93
+ }
94
+ let w = null;
95
+ async function N() {
96
+ return w || (w = (async () => {
97
+ try {
98
+ const e = await import("@ingcreators/annot-imagequant");
99
+ return await e.default(), { quantize_image: e.quantize_image };
100
+ } catch {
101
+ return null;
102
+ }
103
+ })(), w);
104
+ }
105
+ async function J(e, t, r) {
106
+ const n = await N();
107
+ if (!n) return null;
108
+ const o = n.quantize_image(e, t, r, 256);
109
+ if (!(o?.palette instanceof Uint8Array) || !(o?.indices instanceof Uint8Array))
110
+ throw new Error("quantize_image returned an unexpected shape");
111
+ return Z(o.palette, o.indices, t, r, 9);
112
+ }
113
+ function K(e, t) {
114
+ const r = new Uint32Array(e.buffer, e.byteOffset, e.byteLength >>> 2), n = r.length;
115
+ if (n === 0) return !1;
116
+ const i = Math.max(1, Math.floor(n / 5e4)), s = /* @__PURE__ */ new Set();
117
+ for (let a = 0; a < n; a += i)
118
+ if (s.add(r[a]), s.size > t) return !0;
119
+ return !1;
120
+ }
121
+ async function ve() {
122
+ return await N() !== null;
123
+ }
124
+ const Y = 1e7;
125
+ async function O(e, t, r, n = C) {
126
+ const o = n.saveSizePreset ?? "original", i = V(t, r, o), s = i.scaled ? ee(e, t, r, i.width, i.height) : e, a = i.width, c = i.height;
127
+ if (n.format === "png")
128
+ return { bytes: b(s, a, c), chosen: "png", width: a, height: c };
129
+ if (n.format === "jpeg")
130
+ return {
131
+ bytes: B(s, a, c, n.jpegPercent / 100),
132
+ chosen: "jpeg",
133
+ width: a,
134
+ height: c
135
+ };
136
+ if (a * c > Y)
137
+ return {
138
+ bytes: b(s, a, c),
139
+ chosen: "png",
140
+ reason: "too-large-for-png8",
141
+ width: a,
142
+ height: c
143
+ };
144
+ if (K(s, n.smartColorThreshold))
145
+ return n.smartFallback === "jpeg" ? {
146
+ bytes: B(s, a, c, n.jpegPercent / 100),
147
+ chosen: "jpeg",
148
+ reason: "photo-fallback-jpeg",
149
+ width: a,
150
+ height: c
151
+ } : {
152
+ bytes: b(s, a, c),
153
+ chosen: "png",
154
+ reason: "photo-fallback-png",
155
+ width: a,
156
+ height: c
157
+ };
158
+ const f = await J(s, a, c);
159
+ return f ? { bytes: f, chosen: "png", reason: "png-8", width: a, height: c } : {
160
+ bytes: b(s, a, c),
161
+ chosen: "png",
162
+ reason: "imagequant-missing",
163
+ width: a,
164
+ height: c
165
+ };
166
+ }
167
+ async function Te(e, t = C) {
168
+ const r = await R(Buffer.from(e)), o = m(r.width, r.height).getContext("2d");
169
+ o.drawImage(r, 0, 0);
170
+ const i = o.getImageData(0, 0, r.width, r.height).data, s = new Uint8Array(i.buffer, i.byteOffset, i.byteLength);
171
+ return O(s, r.width, r.height, t);
172
+ }
173
+ function b(e, t, r) {
174
+ const n = m(t, r), o = n.getContext("2d"), i = o.createImageData(t, r);
175
+ i.data.set(e), o.putImageData(i, 0, 0);
176
+ const s = n.toBuffer("image/png");
177
+ return new Uint8Array(s.buffer, s.byteOffset, s.byteLength);
178
+ }
179
+ function B(e, t, r, n) {
180
+ const o = m(t, r), i = o.getContext("2d"), s = i.createImageData(t, r);
181
+ s.data.set(e), i.putImageData(s, 0, 0);
182
+ const a = o.toBuffer("image/jpeg", n);
183
+ return new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
184
+ }
185
+ function ee(e, t, r, n, o) {
186
+ const i = m(t, r), s = i.getContext("2d"), a = s.createImageData(t, r);
187
+ a.data.set(e), s.putImageData(a, 0, 0);
188
+ const u = m(n, o).getContext("2d");
189
+ u.imageSmoothingEnabled = !0, u.imageSmoothingQuality = "high", u.drawImage(i, 0, 0, t, r, 0, 0, n, o);
190
+ const g = u.getImageData(0, 0, n, o).data;
191
+ return new Uint8Array(g.buffer, g.byteOffset, g.byteLength);
192
+ }
193
+ function te(e) {
5
194
  if (!e || e.trim().length === 0)
6
195
  return "";
7
- const t = new w();
196
+ const t = new j();
8
197
  let r;
9
198
  try {
10
199
  r = t.parseFromString(e, "image/svg+xml");
@@ -13,31 +202,31 @@ function T(e) {
13
202
  }
14
203
  const n = r.documentElement;
15
204
  if (!n) return "";
16
- const o = new B();
205
+ const o = new q();
17
206
  let i = "";
18
- for (let d = 0; d < n.childNodes.length; d++) {
19
- const f = n.childNodes.item(d);
20
- if (!f || f.nodeType !== 1) continue;
21
- const s = f, p = s.localName ?? s.tagName ?? "";
22
- if (p === "defs") {
23
- const c = A(s);
24
- c !== null && (i += o.serializeToString(c));
207
+ for (let s = 0; s < n.childNodes.length; s++) {
208
+ const a = n.childNodes.item(s);
209
+ if (!a || a.nodeType !== 1) continue;
210
+ const c = a, u = c.localName ?? c.tagName ?? "";
211
+ if (u === "defs") {
212
+ const f = re(c);
213
+ f !== null && (i += o.serializeToString(f));
25
214
  continue;
26
215
  }
27
- if (!(p === "image" && !s.getAttribute("data-redact-style")) && s.getAttribute("id") !== "ui-overlay") {
28
- if (s.getAttribute("id") === "annotations") {
29
- for (let c = 0; c < s.childNodes.length; c++) {
30
- const u = s.childNodes.item(c);
31
- u && u.nodeType === 1 && (i += o.serializeToString(u));
216
+ if (!(u === "image" && !c.getAttribute("data-redact-style")) && c.getAttribute("id") !== "ui-overlay") {
217
+ if (c.getAttribute("id") === "annotations") {
218
+ for (let f = 0; f < c.childNodes.length; f++) {
219
+ const g = c.childNodes.item(f);
220
+ g && g.nodeType === 1 && (i += o.serializeToString(g));
32
221
  }
33
222
  continue;
34
223
  }
35
- i += o.serializeToString(s);
224
+ i += o.serializeToString(c);
36
225
  }
37
226
  }
38
227
  return i;
39
228
  }
40
- function A(e) {
229
+ function re(e) {
41
230
  const t = e.cloneNode(!0);
42
231
  for (let r = t.childNodes.length - 1; r >= 0; r--) {
43
232
  const n = t.childNodes.item(r);
@@ -47,32 +236,48 @@ function A(e) {
47
236
  }
48
237
  return t.childNodes.length === 0 ? null : t;
49
238
  }
50
- function H(e = {}) {
239
+ function Pe(e = {}) {
51
240
  const t = {
52
241
  loadSystemFonts: e.loadSystemFonts ?? !1,
53
242
  ...e.fontFiles ? { fontFiles: e.fontFiles } : {},
54
243
  ...e.fontDirs ? { fontDirs: e.fontDirs } : {},
55
244
  ...e.defaultFontFamily ? { defaultFontFamily: e.defaultFontFamily } : {}
56
245
  };
246
+ function r(n) {
247
+ const o = $(n), s = new P(o, {
248
+ fitTo: { mode: "width", value: n.width },
249
+ background: "rgba(0, 0, 0, 0)",
250
+ font: t
251
+ }).render();
252
+ return {
253
+ pixels: s.pixels,
254
+ width: s.width,
255
+ height: s.height
256
+ };
257
+ }
57
258
  return {
58
- toSvg(r) {
59
- return m(r);
259
+ toSvg(n) {
260
+ return $(n);
60
261
  },
61
- toPng(r) {
62
- const n = m(r);
63
- return new b(n, {
64
- fitTo: { mode: "width", value: r.width },
262
+ toPng(n) {
263
+ const o = $(n);
264
+ return new P(o, {
265
+ fitTo: { mode: "width", value: n.width },
65
266
  background: "rgba(0, 0, 0, 0)",
66
267
  font: t
67
268
  }).render().asPng();
269
+ },
270
+ async toEncoded(n, o) {
271
+ const { pixels: i, width: s, height: a } = r(n);
272
+ return O(i, s, a, o);
68
273
  }
69
274
  };
70
275
  }
71
- function m(e) {
72
- const t = "http://www.w3.org/2000/svg", r = "http://www.w3.org/1999/xlink", n = T(e.annotationsSvg);
73
- return `<svg xmlns="${t}" xmlns:xlink="${r}" ${N}="${S}" width="${e.width}" height="${e.height}" viewBox="0 0 ${e.width} ${e.height}"><image href="${e.originalDataUrl}" width="${e.width}" height="${e.height}"/>` + n + "</svg>";
276
+ function $(e) {
277
+ const t = "http://www.w3.org/2000/svg", r = "http://www.w3.org/1999/xlink", n = te(e.annotationsSvg);
278
+ return `<svg xmlns="${t}" xmlns:xlink="${r}" ${M}="${W}" width="${e.width}" height="${e.height}" viewBox="0 0 ${e.width} ${e.height}"><image href="${e.originalDataUrl}" width="${e.width}" height="${e.height}"/>` + n + "</svg>";
74
279
  }
75
- const K = {
280
+ const Be = {
76
281
  BBox: {
77
282
  type: "object",
78
283
  required: ["x", "y", "width", "height"],
@@ -108,7 +313,7 @@ const K = {
108
313
  color: { type: "string" }
109
314
  }
110
315
  }
111
- }, F = {
316
+ }, ne = {
112
317
  type: "object",
113
318
  required: ["type", "bbox"],
114
319
  additionalProperties: !1,
@@ -121,7 +326,7 @@ const K = {
121
326
  fill: { type: "string" },
122
327
  color: { type: "string" }
123
328
  }
124
- }, v = {
329
+ }, oe = {
125
330
  type: "object",
126
331
  required: ["type", "center", "radius"],
127
332
  additionalProperties: !1,
@@ -135,7 +340,7 @@ const K = {
135
340
  fill: { type: "string" },
136
341
  color: { type: "string" }
137
342
  }
138
- }, O = {
343
+ }, ie = {
139
344
  type: "object",
140
345
  required: ["type", "from", "to"],
141
346
  additionalProperties: !1,
@@ -148,7 +353,7 @@ const K = {
148
353
  strokeWidth: { type: "number", minimum: 0 },
149
354
  color: { type: "string" }
150
355
  }
151
- }, W = {
356
+ }, se = {
152
357
  type: "object",
153
358
  required: ["type", "at", "content"],
154
359
  additionalProperties: !1,
@@ -161,7 +366,7 @@ const K = {
161
366
  intent: { $ref: "#/$defs/Intent" },
162
367
  color: { type: "string" }
163
368
  }
164
- }, P = {
369
+ }, ae = {
165
370
  type: "object",
166
371
  required: ["type", "at", "targetBbox", "content"],
167
372
  additionalProperties: !1,
@@ -174,7 +379,7 @@ const K = {
174
379
  stroke: { type: "string" },
175
380
  color: { type: "string" }
176
381
  }
177
- }, I = {
382
+ }, ce = {
178
383
  type: "object",
179
384
  required: ["type", "svgFragment"],
180
385
  additionalProperties: !1,
@@ -182,9 +387,9 @@ const K = {
182
387
  type: { const: "raw" },
183
388
  svgFragment: { type: "string" }
184
389
  }
185
- }, Y = {
186
- oneOf: [F, v, O, W, P, I]
187
- }, J = {
390
+ }, Ee = {
391
+ oneOf: [ne, oe, ie, se, ae, ce]
392
+ }, _e = {
188
393
  type: "object",
189
394
  required: ["bbox"],
190
395
  additionalProperties: !1,
@@ -194,124 +399,132 @@ const K = {
194
399
  color: { type: "string" }
195
400
  }
196
401
  };
197
- function $(e, t = {}) {
402
+ function F(e, t = {}) {
198
403
  const r = t.stroke ?? "red", n = t.strokeWidth ?? 2, o = t.fill ?? "none";
199
- return `<rect x="${e.x}" y="${e.y}" width="${e.width}" height="${e.height}" fill="${l(o)}" stroke="${l(r)}" stroke-width="${n}"/>`;
404
+ return `<rect x="${e.x}" y="${e.y}" width="${e.width}" height="${e.height}" fill="${p(o)}" stroke="${p(r)}" stroke-width="${n}"/>`;
200
405
  }
201
- function k(e, t, r = {}) {
202
- const n = r.color ?? "red", o = r.strokeWidth ?? 2, i = `annot-arrow-${_()}`;
203
- return `<defs><marker id="${i}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 5 L 0 10 z" fill="${l(n)}"/></marker></defs><line x1="${e.x}" y1="${e.y}" x2="${t.x}" y2="${t.y}" stroke="${l(n)}" stroke-width="${o}" marker-end="url(#${i})"/>`;
406
+ function z(e, t, r = {}) {
407
+ const n = r.color ?? "red", o = r.strokeWidth ?? 2, i = `annot-arrow-${le()}`;
408
+ return `<defs><marker id="${i}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto" markerUnits="strokeWidth"><path d="M 0 0 L 10 5 L 0 10 z" fill="${p(n)}"/></marker></defs><line x1="${e.x}" y1="${e.y}" x2="${t.x}" y2="${t.y}" stroke="${p(n)}" stroke-width="${o}" marker-end="url(#${i})"/>`;
204
409
  }
205
- function x(e, t, r = {}) {
410
+ function D(e, t, r = {}) {
206
411
  const n = r.color ?? "red", o = r.fontSize ?? 14, i = r.anchor ?? "start";
207
- return `<text x="${e.x}" y="${e.y}" fill="${l(n)}" font-size="${o}" text-anchor="${i}">` + E(t) + "</text>";
412
+ return `<text x="${e.x}" y="${e.y}" fill="${p(n)}" font-size="${o}" text-anchor="${i}">` + fe(t) + "</text>";
208
413
  }
209
- let g = 0;
210
- function _() {
211
- return g = g + 1 | 0, g;
414
+ let k = 0;
415
+ function le() {
416
+ return k = k + 1 | 0, k;
212
417
  }
213
- function l(e) {
418
+ function p(e) {
214
419
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
215
420
  }
216
- function E(e) {
421
+ function fe(e) {
217
422
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
218
423
  }
219
- const R = {
424
+ const ge = {
220
425
  info: { stroke: "#3b82f6", fill: "rgba(59, 130, 246, 0.12)", text: "#1e40af" },
221
426
  warning: { stroke: "#f59e0b", fill: "rgba(245, 158, 11, 0.12)", text: "#92400e" },
222
427
  error: { stroke: "#ef4444", fill: "rgba(239, 68, 68, 0.12)", text: "#991b1b" },
223
428
  success: { stroke: "#10b981", fill: "rgba(16, 185, 129, 0.12)", text: "#065f46" },
224
429
  neutral: { stroke: "#6b7280", fill: "rgba(107, 114, 128, 0.12)", text: "#374151" }
225
- }, z = "error";
226
- function a(e) {
227
- const t = e.intent ?? z, r = R[t];
430
+ }, ue = "error";
431
+ function y(e) {
432
+ const t = e.intent ?? ue, r = ge[t];
228
433
  return {
229
434
  stroke: e.stroke ?? r.stroke,
230
435
  fill: e.fill ?? "none",
231
436
  text: e.color ?? r.text
232
437
  };
233
438
  }
234
- function Q(e) {
235
- return e.map(j).join("");
439
+ function Ce(e) {
440
+ return e.map(de).join("");
236
441
  }
237
- function j(e) {
442
+ function de(e) {
238
443
  switch (e.type) {
239
444
  case "rect":
240
- return q(e);
445
+ return he(e);
241
446
  case "circle":
242
- return C(e);
447
+ return me(e);
243
448
  case "arrow":
244
- return X(e);
449
+ return pe(e);
245
450
  case "text":
246
- return D(e);
451
+ return ye(e);
247
452
  case "callout":
248
- return L(e);
453
+ return we(e);
249
454
  case "raw":
250
- return M(e);
455
+ return be(e);
251
456
  }
252
457
  }
253
- function q(e) {
254
- const t = a(e);
255
- return $(e.bbox, {
458
+ function he(e) {
459
+ const t = y(e);
460
+ return F(e.bbox, {
256
461
  stroke: t.stroke,
257
462
  strokeWidth: e.strokeWidth ?? 2,
258
463
  fill: t.fill
259
464
  });
260
465
  }
261
- function C(e) {
262
- const t = a(e), r = e.strokeWidth ?? 2;
263
- return `<circle cx="${e.center.x}" cy="${e.center.y}" r="${e.radius}" fill="${y(t.fill)}" stroke="${y(t.stroke)}" stroke-width="${r}"/>`;
466
+ function me(e) {
467
+ const t = y(e), r = e.strokeWidth ?? 2;
468
+ return `<circle cx="${e.center.x}" cy="${e.center.y}" r="${e.radius}" fill="${_(t.fill)}" stroke="${_(t.stroke)}" stroke-width="${r}"/>`;
264
469
  }
265
- function X(e) {
266
- const t = a(e);
267
- return k(e.from, e.to, {
470
+ function pe(e) {
471
+ const t = y(e);
472
+ return z(e.from, e.to, {
268
473
  color: t.stroke,
269
474
  strokeWidth: e.strokeWidth ?? 2
270
475
  });
271
476
  }
272
- function D(e) {
273
- const t = a(e);
274
- return x(e.at, e.content, {
477
+ function ye(e) {
478
+ const t = y(e);
479
+ return D(e.at, e.content, {
275
480
  color: t.text,
276
481
  fontSize: e.fontSize ?? 14,
277
482
  anchor: e.anchor ?? "start"
278
483
  });
279
484
  }
280
- function L(e) {
281
- const t = a(e), r = e.targetBbox, n = U(r, e.at);
282
- return $(r, {
485
+ function we(e) {
486
+ const t = y(e), r = e.targetBbox, n = xe(r, e.at);
487
+ return F(r, {
283
488
  stroke: t.stroke,
284
489
  strokeWidth: e.strokeWidth ?? 2,
285
490
  fill: t.fill
286
- }) + k(e.at, n, {
491
+ }) + z(e.at, n, {
287
492
  color: t.stroke,
288
493
  strokeWidth: 2
289
- }) + x(e.at, e.content, {
494
+ }) + D(e.at, e.content, {
290
495
  color: t.text,
291
496
  fontSize: 14,
292
497
  anchor: "start"
293
498
  });
294
499
  }
295
- function M(e) {
500
+ function be(e) {
296
501
  return e.svgFragment;
297
502
  }
298
- function U(e, t) {
299
- const r = h(t.x, e.x, e.x + e.width), n = h(t.y, e.y, e.y + e.height);
503
+ function xe(e, t) {
504
+ const r = E(t.x, e.x, e.x + e.width), n = E(t.y, e.y, e.y + e.height);
300
505
  return { x: r, y: n };
301
506
  }
302
- function h(e, t, r) {
507
+ function E(e, t, r) {
303
508
  return Math.min(Math.max(e, t), r);
304
509
  }
305
- function y(e) {
510
+ function _(e) {
306
511
  return e.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
307
512
  }
308
513
  export {
309
- Y as BBOX_ANNOTATION_SCHEMA,
310
- J as BBOX_REDACT_REGION_SCHEMA,
311
- K as SHARED_DEFS,
312
- k as arrowBetween,
313
- Q as bboxAnnotationsToSvg,
314
- H as createAnnotator,
315
- $ as rectForBoundingBox,
316
- x as textAt
514
+ Ee as BBOX_ANNOTATION_SCHEMA,
515
+ _e as BBOX_REDACT_REGION_SCHEMA,
516
+ C as DEFAULT_ENCODE_OPTIONS,
517
+ Se as SAVE_SIZE_LABEL,
518
+ X as SAVE_SIZE_MAX_WIDTH,
519
+ Be as SHARED_DEFS,
520
+ z as arrowBetween,
521
+ Ce as bboxAnnotationsToSvg,
522
+ V as computeResizeTarget,
523
+ Pe as createAnnotator,
524
+ Te as decodeAndEncodeImage,
525
+ O as encodeRgba,
526
+ ve as isImagequantAvailable,
527
+ K as isPhotoHeavy,
528
+ F as rectForBoundingBox,
529
+ D as textAt
317
530
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ingcreators/annot-annotator",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Headless annotator — render annotated screenshots from Node without a browser. Built on @resvg/resvg-js. Pairs with @ingcreators/annot-playwright for fail-on-test annotated reports.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "ingcreators",
@@ -49,8 +49,11 @@
49
49
  "@ingcreators/annot-core": "0.1.0"
50
50
  },
51
51
  "dependencies": {
52
+ "@napi-rs/canvas": "^0.1.0",
52
53
  "@resvg/resvg-js": "^2.6.2",
53
- "@xmldom/xmldom": "^0.9.10"
54
+ "@xmldom/xmldom": "^0.9.10",
55
+ "pako": "^2.1.0",
56
+ "@ingcreators/annot-imagequant": "0.1.0"
54
57
  },
55
58
  "scripts": {
56
59
  "build": "vite build",