Skip to content

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.0toEncoded() method + standalone encodeRgba() / decodeAndEncodeImage()
  • @ingcreators/annot-playwright@0.3.0encode option on annotateScreenshot()
  • @ingcreators/annot-mcp@0.2.0encode input 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.

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:

Encoding1280×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
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 picked
console.log(result.reason); // "png-8" | "photo-fallback-jpeg" | "imagequant-missing" | …
console.log(result.bytes); // Uint8Array — the encoded image
console.log(result.width, result.height); // post-resize dimensions

From 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"
}
}
ValueBehaviour
"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.

Caps the output’s max-width. Aspect-preserving; never upscales when the source is already narrower than the cap.

PresetMax widthUse case
"light"1280 pxMobile-friendly docs, thumbnail-grade attachments
"standard" (default)1920 pxGeneral-purpose manual screenshots
"highQuality"2560 pxDesigner-grade reference manuals
"original"no resizePixel-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)

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)

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.

JPEG quality 60–100. Default 92 (visually indistinguishable from PNG for most UI content, ~5× smaller file than the equivalent PNG-32).

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).

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);

@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.

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
});