/** * Text → PNG renderer. Blits atlas glyphs into a grayscale framebuffer, then PNG-encodes. * Iterates by codepoint so East Asian Wide chars (2-cell advance) and surrogate pairs handled correctly. * Pages capped at 1568×728 px (1.14 MP): under both Anthropic tiers' limits, so every * page bills at its raw 28-px patch count with no server-side downscale (WYSIWYG). */ export type RenderFont = 'spleen-5x8' | 'jetbrains-mono-10' | 'jetbrains-mono-12' | 'jetbrains-mono-14'; export declare const DEFAULT_RENDER_FONT: RenderFont; /** Page-height ceiling. Measured (2026-07-01, count_tokens sweep, claude-sonnet-4-5 — see * /tmp/pxexp/LEVER1-findings.md): the API downscales any image to fit BOTH long-edge ≤1568 * AND ~1.15 MP (≈1,143,750 px), then bills the exact 28-px patch count ⌈w/28⌉×⌈h/28⌉ * (1568×728 = 56×26 = 1456 tokens/image; the old ≈px/750 slope was the 28²=784 approximation). * The old 1932×1932 page was billed at cap but resampled 0.555× → 5×8 glyphs reached the * encoder at ~2.8×4.4 px. New page shape 1568×728 = 1,141,504 px fits both bounds → * WYSIWYG for the vision encoder (also satisfies ≤2000 px/side for >20-image requests). */ export declare const MAX_HEIGHT_PX = 728; /** Char budget for the static slab (system + tools + CLAUDE.md). Matches physical page * capacity at 312 cols × 90 rows so image-count estimates track real pagination. */ export declare const READABLE_CHARS_PER_IMAGE = 28080; /** Char budget for dense content (tool output, collapsed history). 312 cols × 90 rows = 28080 * chars fills the 1568×728 page. NOTE: verbatim recall of imaged text is unreliable at any size. */ export declare const DENSE_CONTENT_CHARS_PER_IMAGE = 28080; export declare const DENSE_CONTENT_COLS = 312; /** Bare 5×8 cell (no padding). A/B showed 5×8 beats 7×10 on dense JSON (4/5 vs 3/5 reads, 42% fewer tokens). * Revert to {cellWBonus:2, cellHBonus:2} if misread rates rise. */ export declare const DENSE_RENDER_STYLE: RenderStyle; /** Anthropic static slab uses the same measured 312-column no-resize geometry. */ export declare const ANTHROPIC_SLAB_COLS = 312; /** Horizontal padding (left + right each), px. Exported for transform.ts token-cost math. */ export declare const PAD_X = 4; /** Vertical padding (top + bottom each), px. Exported for transform.ts token-cost math. */ export declare const PAD_Y = 4; /** Production ships bare 5×8 atlas cell (reflow+grayscale+inimage instruction band * brought 5×8 to 98.95% OCR accuracy on Opus 4.7, matching or beating padded cells). * RenderStyle.cellWBonus/cellHBonus override per-eval only. */ export declare const DEFAULT_CELL_W_BONUS = 0; export declare const DEFAULT_CELL_H_BONUS = 0; /** Effective cell pixel dimensions. transform.ts derives image-budget math from these. */ export declare const CELL_W: number; export declare const CELL_H: number; /** Visual rows per image: `floor((MAX_HEIGHT_PX − 2·PAD_Y) / CELL_H)`. Derived * from the cell geometry above so break-even math auto-tracks it. */ export declare const LINES_PER_IMAGE: number; /** Real char capacity of one page AT A GIVEN COLUMN WIDTH. * * Lives here, next to the geometry it is derived from, because every caller that * budgets images must price pages at the width it actually renders at. The * DENSE_CONTENT_CHARS_PER_IMAGE constant is only correct for DENSE_CONTENT_COLS * (312×90); using it while rendering at, say, COLS=100 overstates capacity 3.1×, * so an image budget clears a plan that then emits 3× the images — the request * blows the API's per-request limit and comes back 500. Always pass the cols the * renderer will actually use. */ export declare function maxCharsPerImage(cols: number): number; export interface RenderedImage { png: Uint8Array; width: number; height: number; /** Input codepoints rendered (wide chars count as 1, not 2). */ charsRendered: number; /** Codepoints absent from atlas, rendered as blank cells. Surface as telemetry. */ droppedChars: number; /** Per-codepoint drop histogram. Empty when droppedChars === 0; never undefined. */ droppedCodepoints: Map; } /** Optional render-time styling. All fields unset = production default 5×8 cell. * Eval harness overrides per variant to A/B cell sizes and structure aids. */ export interface RenderStyle { /** Rasterized font atlas. Alternate atlases fall back to Spleen/Unifont for missing Unicode. */ font?: RenderFont; /** Draw faint grey grid rules onto background pixels (zero pixel cost). */ grid?: boolean; /** Draw a vertical grid rule every N columns. 0/unset = row rules only. */ gridCols?: number; /** Horizontal size multiplier for the ↵ newline marker. 1 = off. */ markerScale?: number; /** Render the ↵ marker in red (switches PNG to RGB truecolor). */ markerRed?: boolean; /** Extra blank rows above the 8px glyph (cell height = 8 + this). Unset = DEFAULT_CELL_H_BONUS. */ cellHBonus?: number; /** Extra blank columns beside the 5px glyph (cell width = 5 + this). Negative overlaps glyphs. Unset = DEFAULT_CELL_W_BONUS. */ cellWBonus?: number; /** Use the AA grayscale companion atlas. */ aa?: boolean; /** Cycle palette colors per glyph for per-character boundary cues. Forces RGB output. Composes with aa. */ colorCycle?: boolean; /** Tint only the structural / boundary tags (body stays black) * so speakers are scannable without recoloring content. Forces RGB. Composes with aa. */ colorByRole?: boolean; /** Morphological ink dilate radius in pixels (pre-invert). Thickens glyphs without * changing cell pitch — pure-image OCR aid at fixed 5×8 density. 0/unset = off. */ inkDilate?: number; /** Dilate axis: 'both' (default), 'x', or 'y'. Prefer 'y' at 5×8 so neighbors do not merge. */ inkDilateAxis?: 'both' | 'x' | 'y'; /** Post-blit polarity. Default true = black ink on white (production). false keeps * white ink on black (pre-invert canvas). Fixed cell pitch. */ invert?: boolean; /** * Post-invert paper gray (0–255). Default 255 = pure white. Mid-light values * (e.g. 230–240) reduce glare and lift faint grid rules without changing cell pitch. * Applied after invert; ink stays near-black via linear remap onto the paper. */ paperGray?: number; } export declare function renderCellWidth(style?: RenderStyle): number; export declare function renderCellHeight(style?: RenderStyle): number; /** Strip trailing whitespace per line and collapse 4+ consecutive \n to 3. * Does NOT touch mid-line spaces or leading indent — structure is preserved. */ export declare function minifyForRender(text: string): string; /** U+21B5 ↵ sentinel for original hard newlines in reflowed text. In full-bmp atlas via Unifont. */ export declare const NL_SENTINEL = "\u21B5"; /** Look-alike (U+23CE ⏎) for a ↵ that was ALREADY in the source content — distinct from * the U+21B5 ↵ we insert for newlines. reflow() bails when its input already contains the * sentinel; that's vanishingly rare for normal content but common when the content is about * pxpipe itself (rendered dumps, OCR, this very transcript). {@link neutralizeSentinel} * swaps pre-existing sentinels for this glyph in RENDER-PREP only, so reflow can pack * newlines instead of bailing to a raw, unpacked render. Originals are preserved verbatim * elsewhere (recordRecoverable / cache-stable history), and reflow()'s own round-trip * contract — and its tests — are left untouched. */ export declare const NL_SENTINEL_LITERAL = "\u23CE"; export declare function neutralizeSentinel(text: string): string; /** colorByRole palette, indexed by slot-1. Only the boundary TAGS are tinted; * body content stays black. [ tags, tags]. */ export declare const ROLE_PALETTE: [number, number, number][]; /** * Slot markers for the parallel "slot string" — the structure-through mechanism * that replaces the old parse-back. A slot string is a width-preserving copy of * the rendered text: every structural role-tag character is swapped for one of * these control codes, and every other codepoint is copied verbatim. Because the * markers are width-1 (exactly like the ASCII tag chars they stand in for), the * existing reflow / wrapLines / paging transforms mutate the slot string in lock- * step with the text — only whitespace/newlines move, and those are slot 0 in * both. The renderer then reads role attribution BY POSITION instead of trying to * re-find "" in flattened text (which miscolors a body that literally quotes * a tag). The structure is known at serialize time and carried, never guessed. */ export declare const SLOT_MARK_USER: string; export declare const SLOT_MARK_ASSISTANT: string; /** Width-preserving slot-0 copy of body text: identical codepoints (so wrap math is * unchanged) but with any literal slot-marker control char neutralized. These are rare in * real content but DO occur (e.g. binary-ish tool output that gets collapsed into history), * so the replacement is width- and strip-equivalent to the marker — never a space, which * the minifier would strip and misalign. Guarantees body can't forge a role hue. */ export declare function slotCopyBody(body: string): string; /** Build the slot-string segment for one role-wrapped turn, mirroring the text * form `<${tag}>\n${body}\n`: marker chars over the open/close tags, * a verbatim slot-0 copy of the body. `mark` is the role's slot marker. */ export declare function roleSlotSegment(tag: string, body: string, mark: string, attr?: string): string; /** Minify + tab-expand + join lines with ↵ sentinel. Returns null if text already * contains ↵ (caller falls back to non-reflow path; vanishingly rare in practice). */ export declare function reflow(text: string): string | null; /** Inverse of reflow: ↵ → '\n'. dereflow(reflow(text)) === minifyForRender(text) with tabs expanded. */ export declare function dereflow(reflowed: string): string; /** Pure-ASCII delimiters: `🔥` → `[U+1F525]`. ASCII is the one range guaranteed * present in BOTH atlases (mathematical white brackets U+27E6/7 were tried first * and are absent from each — the "full-bmp" note on NL_SENTINEL overstates real * coverage). ASCII output also makes idempotency structural: an escaped line * contains no atlas misses, so a second pass is a no-op. `[U+…]` can collide * with literal source text discussing codepoints; the escape is for model * legibility, not machine round-trip, so the ambiguity is acceptable. */ export declare const GLYPH_ESCAPE_OPEN = "[U+"; export declare const GLYPH_ESCAPE_CLOSE = "]"; /** Replace atlas-missing codepoints with `[U+HEX]` (uppercase hex — e.g. * 🔥 → `[U+1F525]`). Lossless for non-exempt misses (hex → codepoint) and * idempotent: the escape spells only atlas-present chars, so a second pass is * a no-op. Fast path allocates nothing when every codepoint is in the atlas. */ export declare function escapeMissingGlyphs(line: string): string; /** Expand \t to U+2192 → + padding to the next TAB_WIDTH stop. Visible marker lets the * model distinguish indent-spaces from intentional-spaces. Wide CJK chars count as 2 cols. * U+0009 is absent from the atlas (control codepoint), so without this every tab was a drop. */ export declare function expandTabsInLine(line: string): string; /** Visual width of a line in cells. Wide CJK = 2; enlarged ↵ = markerScale. */ export declare function measureLineCols(line: string, markerScale?: number, font?: RenderFont): number; /** Always renders at full canvas width. Signature kept for transform.ts compatibility; returns cols unchanged. */ export declare function shrinkColsToContent(text: string, cols: number, markerScale?: number, font?: RenderFont): number; /** * Real content-width measurement (the capability `shrinkColsToContent` historically * stubbed out): the display width, in cols, of the widest line in `text`, capped at * `maxCols`. Lets a renderer size a narrow canvas to short-line content (e.g. code) * instead of padding every page to full width. Pure function of (text, maxCols) → * deterministic width → cache-prefix-safe. Tabs are expanded so the measured width * matches what the renderer actually lays out. */ export declare function measureContentCols(text: string, maxCols: number, markerScale?: number, font?: RenderFont): number; export declare function wrapLines(text: string, cols: number, markerScale?: number, font?: RenderFont): string[]; /** Render text to a single PNG (≤ MAX_HEIGHT_PX tall). Wide glyphs occupy 2 consecutive cells. */ export declare function renderChunkToPng(text: string, cols?: number, style?: RenderStyle, maxHeightPx?: number, slotText?: string): Promise; /** Reflow-aware variant of renderTextToPngs. Falls back to non-reflow on sentinel collision. */ export declare function renderTextToPngsReflow(text: string, cols?: number, style?: RenderStyle): Promise; /** Observability for the dashboard/tests. `bytes` counts retained PNG payloads plus * the fixed-width keys. * * `evictions` and `oversized` answer different questions and a single "the cache is * not helping" number would conflate them: evictions mean the working set outgrew the * budget (raise it, or accept the churn), while `oversized` means a single render was * bigger than the entire budget and was therefore never stored at all — the failure a * too-small edge budget produces, where hit rate stays at zero no matter how stable * the input is. */ export declare function renderCacheStats(): { entries: number; bytes: number; hits: number; misses: number; evictions: number; oversized: number; }; /** Current byte budget. Exposed so the dashboard can report utilisation rather than a * bare byte count the operator has no denominator for. */ export declare function renderCacheMaxBytes(): number; /** * Set the byte budget at runtime and evict down to it immediately. * * Exists for the Worker entrypoint, which cannot use the module-init `process.env` * read: bindings are handed to `fetch(req, env, ctx)`, long after this module * evaluated. Node keeps using the env read. * * `0` disables the cache and drops everything already held. Negative or non-finite * input is ignored rather than obeyed — a broken value must not read as "unbounded". */ export declare function setRenderCacheMaxBytes(maxBytes: number): void; /** Drop every entry. Tests use this to isolate hit/miss accounting. */ export declare function clearRenderCache(): void; export declare function renderTextToPngsWithCharLimit(text: string, cols?: number, maxCharsPerImage?: number, style?: RenderStyle, maxHeightPx?: number, slotText?: string): Promise; export declare function renderTextToPngs(text: string, cols?: number, style?: RenderStyle, maxHeightPx?: number, slotText?: string): Promise; export interface RenderDensePagesOptions { /** Wrap-width cap in cols. Default DENSE_CONTENT_COLS (384). */ readonly cols?: number; /** Shrink the canvas to the widest actual line (default true). `false` keeps the full * `cols` width — the proxy's eval-backed full-canvas / slab behavior. */ readonly shrink?: boolean; /** Reflow (minify + join hard newlines with ↵) before rendering. Default false. Callers * that pre-reflow (the proxy's maybeReflow / history lockstep) pass false; `pxpipe export` * passes true so short lines pack into full-width rows. */ readonly reflow?: boolean; /** Max source chars per page. Default DENSE_CONTENT_CHARS_PER_IMAGE. */ readonly maxCharsPerImage?: number; /** Render style. Default DENSE_RENDER_STYLE. */ readonly style?: RenderStyle; /** Max page height in px. Default MAX_HEIGHT_PX. */ readonly maxHeightPx?: number; } /** * The single dense-page rendering decision shared by the public SDK primitive * `renderTextToImages` (library.ts → `pxpipe export`) AND the proxy's `textToImageBlocks` * (transform.ts): optionally reflow, measure the content width, then render. Both callers * route through HERE so * export PNGs and proxy image blocks are produced by the exact same code and cannot drift — * `shrinkColsToContent` is `measureContentCols`, so the proxy's old inline path was already * identical at the default 384 cols; this makes it identical at every cols. Returns the raw * rendered pages; each caller packages them (PNG files vs. base64 Anthropic ImageBlocks). * * History (history.ts) deliberately does NOT use this: it reflows a parallel role-slot string * in lockstep with the text and renders with `colorByRole`, which code export has no concept * of — wiring slots through this public surface would bloat it for one internal caller. It * still shares the underlying `reflow()` + `renderTextToPngsWithCharLimit` primitives. */ export declare function renderDensePages(text: string, opts?: RenderDensePagesOptions): Promise; //# sourceMappingURL=render.d.ts.map