Encode pipeline
Smart / save-size / JPEG encoding that mirrors the Chrome extension’s “Save size” + “Format smart” capture settings. Available since:
@ingcreators/annot-annotator@0.3.0—toEncoded()method + standaloneencodeRgba()/decodeAndEncodeImage()@ingcreators/annot-playwright@0.3.0—encodeoption onannotateScreenshot()@ingcreators/annot-mcp@0.2.0—encodeinput on all five tools
The encode pipeline is separate from the annotation DSL. The DSL describes what to draw; encode options control how the output bytes are encoded. They’re independent — you can use either, both, or neither.
Why encode?
Section titled “Why encode?”Default annotator output is PNG-32 (truecolor + alpha). For manual-creation workflows where 10–100 screenshots may go into a single document, this gets heavy fast:
| Encoding | 1280×800 UI screenshot |
|---|---|
| PNG-32 (default) | 200–500 KB |
| PNG-8 (smart) | 80–200 KB (~60% reduction) |
| JPEG (smart fallback) | 30–80 KB (lossy, photo-heavy only) |
For a 50-screenshot manual:
- PNG-32 baseline: ~15 MB total
- Smart + standard resize: ~5 MB total
Quick example
Section titled “Quick example”import { createAnnotator, type EncodeOptions,} from "@ingcreators/annot-annotator";
const annotator = createAnnotator();const result = await annotator.toEncoded(input, { format: "smart", // "smart" | "png" | "jpeg" saveSizePreset: "standard", // "light" | "standard" | "highQuality" | "original" smartFallback: "png", smartColorThreshold: 15000, jpegPercent: 92,});
console.log(result.chosen); // "png" | "jpeg" — what was actually pickedconsole.log(result.reason); // "png-8" | "photo-fallback-jpeg" | "imagequant-missing" | …console.log(result.bytes); // Uint8Array — the encoded imageconsole.log(result.width, result.height); // post-resize dimensionsFrom a Playwright test:
import { test, type BboxAnnotation } from "@ingcreators/annot-playwright";
test("login form", async ({ page, annotator }) => { const annotated = await annotator.annotateScreenshot(page, { annotations: [...] satisfies BboxAnnotation[], encode: { format: "smart", saveSizePreset: "standard" }, });});From an MCP tool (agent-side):
{ "url": "https://...", "annotations": [...], "encode": { "format": "smart", "saveSizePreset": "light" }}format
Section titled “format”| Value | Behaviour |
|---|---|
"smart" (default) | Sample unique-colour count. If photo-heavy, emit smartFallback. Otherwise quantize to ≤256 colours via the bundled Median Cut quantizer and emit PNG-8. |
"png" | Force PNG-32 (lossless truecolor). Resize still applies. |
"jpeg" | Force JPEG at jpegPercent quality. |
The smart heuristic samples ~50,000 pixels and counts unique
RGBA quads. Above smartColorThreshold (default 15,000) the
image is treated as photo-heavy.
saveSizePreset
Section titled “saveSizePreset”Caps the output’s max-width. Aspect-preserving; never upscales when the source is already narrower than the cap.
| Preset | Max width | Use case |
|---|---|---|
"light" | 1280 px | Mobile-friendly docs, thumbnail-grade attachments |
"standard" (default) | 1920 px | General-purpose manual screenshots |
"highQuality" | 2560 px | Designer-grade reference manuals |
"original" | no resize | Pixel-perfect, e.g. visual diff comparison input |
const compact = await annotator.toEncoded(input, { ...DEFAULT_ENCODE_OPTIONS, saveSizePreset: "light", // 1280px cap});// compact.width === 1280 (or less, if source was narrower)smartFallback
Section titled “smartFallback”When the image is photo-heavy, smart mode picks this format instead of trying PNG-8:
"jpeg"— recommended for manuals with embedded photos (smaller files, lossy is acceptable)"png"— lossless PNG-32 fallback (for archival use cases)
smartColorThreshold
Section titled “smartColorThreshold”Unique-colour count above which the image is treated as photo-heavy. Defaults to 15,000. Lower → more aggressive JPEG fallback. Higher → more aggressive PNG-8 quantization.
UI screenshots typically return <5,000 unique colours; pages with rich photography return >20,000.
jpegPercent
Section titled “jpegPercent”JPEG quality 60–100. Default 92 (visually indistinguishable from PNG for most UI content, ~5× smaller file than the equivalent PNG-32).
EncodeResult
Section titled “EncodeResult”interface EncodeResult { bytes: Uint8Array; chosen: "png" | "jpeg"; reason?: | "png-8" | "photo-fallback-png" | "photo-fallback-jpeg" | "too-large-for-png8"; width: number; height: number;}Use chosen and reason to log what the smart heuristic
picked. reason is omitted when the request was explicit
(format: "png" or format: "jpeg" with no fallbacks).
Standalone helpers
Section titled “Standalone helpers”For cases where you already have RGBA / PNG bytes:
import { encodeRgba, decodeAndEncodeImage,} from "@ingcreators/annot-annotator";
// Raw RGBA (e.g. from `Resvg.render().pixels`):const result = await encodeRgba(rgba, width, height, options);
// Existing PNG / JPEG bytes (e.g. from `page.screenshot()`):const result = await decodeAndEncodeImage(pngBytes, options);License posture
Section titled “License posture”@ingcreators/annot-annotator is Apache-2.0 end to end. The
PNG-8 path uses a pure-TypeScript Median Cut quantizer that
ships inside the annotator package — no GPL inclusion, no
separate install step.
Defaults
Section titled “Defaults”DEFAULT_ENCODE_OPTIONS (also exported from
@ingcreators/annot-annotator):
{ format: "smart", saveSizePreset: "standard", smartFallback: "png", smartColorThreshold: 15000, jpegPercent: 92,}You typically only set the fields you want to override:
await annotator.toEncoded(input, { saveSizePreset: "light", // override just this});