Skip to content

createAnnotator

import { createAnnotator } from "@ingcreators/annot-annotator";
const annotator = createAnnotator(options?);

Factory that returns a headless annotator instance. The returned object is stateless — you can create one per call or hoist it to a module-level singleton; both are equivalent.

function createAnnotator(options?: AnnotatorOptions): Annotator;
interface AnnotatorOptions {
/**
* Override the default font stack used for text annotations.
* Defaults to the logical `Annot Sans` token with the
* system-font cascade.
*/
fontFamily?: string;
}
interface Annotator {
toPng(input: AnnotateInput): Promise<Buffer>;
toSvg(input: AnnotateInput): Promise<string>;
toEditablePng(input: EditableInput): Promise<Buffer>;
}
interface AnnotateInput {
/**
* Base image bytes. Accepts a Buffer, a Uint8Array, or a
* `data:image/png;base64,...` URL.
*/
baseImage: Buffer | Uint8Array | string;
/**
* SVG fragment to overlay on the base image. Coordinate space
* is the natural pixel dimensions of `baseImage` (read from
* the PNG IHDR chunk).
*/
annotationsSvg: string;
}
interface EditableInput extends AnnotateInput {
/**
* Optional opaque kv tags written into the embedded XMP.
* Soft-convention key names (`source`, `screen`, `capturedAt`,
* `commit`) are documented but not validated.
*/
tags?: Record<string, string>;
}
const annotator = createAnnotator();
const annotated = await annotator.toPng({
baseImage: await readFile("./screenshot.png"),
annotationsSvg: `
<rect x="100" y="100" width="200" height="40"
fill="none" stroke="#ff5252" stroke-width="3" />
`,
});

When you want the SVG itself — for embedding in a webpage, for diffing in tests, for piping into another converter — use toSvg:

const svg = await annotator.toSvg({ baseImage, annotationsSvg });
await writeFile("./annotated.svg", svg);

toEditablePng returns the same visible image as toPng, plus the original un-annotated capture + the annotations SVG embedded in the PNG’s XMP metadata + custom svGo chunk. Re-opening the file in the Annot editor — annot.work/app/, the desktop / VSCode hosts, or the Chrome extension — restores the annotations as selectable / movable / restylable objects rather than a flat bitmap.

const annotator = createAnnotator();
const editable = await annotator.toEditablePng({
baseImage: await readFile("./screenshot.png"),
annotationsSvg,
tags: {
source: "docs-tour",
screen: "app-overview",
capturedAt: new Date().toISOString(),
},
});
await writeFile("./editable.png", editable);

Image viewers that don’t know about the custom chunks display the rasterised pixels verbatim — no compatibility loss versus toPng. The bytes are still a valid PNG.

The optional tags are written verbatim into the XMP — no validation, no transformation. Soft-convention key names that built-in producers use (so downstream readers can key off the same names when present):

KeyMeaning
sourceWhat produced the PNG — e.g. "docs-tour", "playwright-fixture", "annot-mcp".
screenLiving-product-docs <Screen id> value.
capturedAtISO timestamp.
commitGit SHA when applicable.

For Playwright specs producing screenshots of a documented screen, the @ingcreators/annot-product-docs package ships an extended test fixture that bundles the whole flow (refresh MDX snapshot → take raw screenshot → bake editable PNG → write to disk) into one page.screenshot() call:

import { test } from "@ingcreators/annot-product-docs";
test("app overview", async ({ page }) => {
await page.goto("https://annot.work/app/");
await page.screenshot({
path: "public/app/shots/app-overview.png",
annot: {
mdx: { id: "app-overview", path: "src/content/docs/app/index.mdx" },
tags: { source: "docs-tour", capturedAt: new Date().toISOString() },
},
});
});

If you don’t need MDX-linked screenshots, swap the import to @ingcreators/annot-playwright — same annot: { overlays, tags, editable } surface, no Astro / MDX dependency.

The annot option is compositional — mdx / overlays / tags / editable are independent fields, each contributing to a separate slice of the embedded XMP record. See product-docs/playwright-fixture for the full vocabulary, locator screenshot semantics, and the codegen→hand-edit workflow.

The annotator strips editor-internal artefacts from the input SVG fragment before composition:

  • <style data-annot-fonts> blocks
  • legacy base-image-in-wrapper structures
  • #ui-overlay and <g id="annotations"> wrappers

This means it’s safe to pass output from the web app’s Save as SVG straight in — the artefacts that exist only for the live editor session are removed before rendering.