# Functions B–S

Import these from `@grafloria/renderer`.

## Functions

### `base64ToBytes`

Pure base64 → bytes. (No `atob`: it does not exist in Node, and this must stay DOM-free.)

```ts
function base64ToBytes(base64: string): Uint8Array
```

### `buildPrintDocument`

Build a printable HTML document from already-exported SVG pages.

PURE — it returns a string. That is deliberate: it keeps the page-building testable, and
leaves the one genuinely browser-side act (opening a print dialog) to `printDocument`.

The `page-break-after` on every sheet but the last is what makes the browser emit one
physical page per tile instead of reflowing them all into a single column.

```ts
function buildPrintDocument(svgPages: string[], options: PrintOptions = {}): string
```

### `bytesToBase64`

Pure bytes → base64.

```ts
function bytesToBase64(bytes: Uint8Array): string
```

### `bytesToDataUrl`

bytes → `data:<mime>;base64,…`

```ts
function bytesToDataUrl(bytes: Uint8Array, mimeType: string): string
```

### `canRasterizeInThisEnvironment`

Is there a canvas we can draw into? (browser main thread or worker)

```ts
function canRasterizeInThisEnvironment(): boolean
```

### `captureCustomNodeHost`

Capture ONE custom node's host into plain data.

Never throws: an export must not be taken down by a host in a state we did not
anticipate. A failed capture degrades to `fidelity: 'empty'`, which the pure layer
turns into a marked box and a warning — the whole point being that a blank is never
silent.

```ts
function captureCustomNodeHost(
  id: string,
  rect: Rectangle,
  host: unknown,
  options: CaptureHostOptions = {}
): CustomNodeCapture
```

### `captureCustomNodes`

THE SYNCHRONOUS CAPTURE — materialize → read → restore, with no suspension point.

The DOM read happens HERE, once, and produces plain data — `exportSvg` stays pure,
DOM-free and deterministic. A painter that defers its paint has not drawn anything by
the time this looks; `paintWarning` says so rather than exporting a silent blank.

```ts
function captureCustomNodes(
  source: CustomNodeHostSource,
  needed: (node: NodeModel) => boolean = () => true
): CustomNodeCapture[]
```

### `captureCustomNodesAsync`

THE ASYNC CAPTURE — the same boundary, allowed to wait for a painter that said it was
not finished.

THE SIGNAL IS THE PROMISE (`pendingPaint`), and nothing else — never a fixed sleep. `timeoutMs` is a safety net: on expiry the export takes the host as it stands and
`paintWarning(…, waited = true)` reports it. If nothing in scope is pending, this runs
materialize → read → restore with no suspension point at all, i.e. the identical
sequence {@link captureCustomNodes} performs, so an all-sync board exports the same
bytes through both paths.

```ts
async function captureCustomNodesAsync(
  source: CustomNodeHostSource,
  needed: (node: NodeModel) => boolean,
  timeoutMs: number
): Promise<CustomNodeCapture[]>
```

### `clampOutputSize`

Clamp an export's PIXEL size.

A 3x export of a big diagram is how you ask a browser for a 30000 × 20000 canvas
and get back a blank image — canvas has a hard area/side limit (~16k on most
engines, less on Safari/mobile), and it fails SILENTLY: `toDataURL` hands you a
blank or throws deep inside the driver. So we cap the output and REDUCE THE SCALE
to fit, rather than emitting a request we know will fail.

Reducing scale (not cropping) is the right lever: it keeps the whole picture and
only spends fewer pixels on it, which is exactly the trade a caller who asked for
"3x, and it must fit" wants.

`minSize` floors the result so a tiny diagram still yields a usable image rather
than a 12 × 8 sliver.

```ts
function clampOutputSize(
  width: number,
  height: number,
  requestedScale: number,
  maxSize: number = DEFAULT_MAX_OUTPUT_SIZE,
  minSize = 1
): ClampResult
```

### `collectAssetUrls`

Every external asset URL the tree references. PURE.

Deduplicated and returned in a STABLE order (first appearance), so a caller that fetches
them and re-inlines gets the same bytes for the same diagram every time — determinism runs
all the way through this module.

```ts
function collectAssetUrls(root: VNode): string[]
```

### `computeBreaks`

Choose the break positions along ONE axis.

Walks left to right. At each naive break (`origin + pageSize`) it looks for boxes the
break would slice; if there are any, it tries pulling the break back to the leftmost
slicee's leading edge. That move is only taken when the page it leaves is still at least
`1 - tolerance` full — otherwise we would trade one cut node for a page count explosion.

```ts
function computeBreaks(
  start: number,
  end: number,
  pageSize: number,
  boxes: Box[],
  options: { snap: boolean; tolerance: number; overlap: number; warnings: string[]; axis: 'x' | 'y' }
): number[]
```

### `crc32`

```ts
function crc32(bytes: Uint8Array): number
```

### `createClassStyleResolver`

Build the resolver for a theme. Compiles BASE_STYLE_RULES once (var refs
resolved against the theme's variable values), then matches class lists
against it.

```ts
function createClassStyleResolver(theme: Theme, warnings: string[] = []): ClassStyleResolver
```

**Parameters**

- `theme`: the theme whose `--grafloria-*` values get baked in
- `warnings`: collector — a declaration whose variable cannot be resolved is dropped and reported here rather than silently emitting `var(--…)`.

### `createCustomNodeCapturer`

Bind the capture to one canvas's hosts.

ONE async capture at a time per canvas: two exports in flight would otherwise
interleave their materialize/restore pairs — the first's restore tearing down a host
the second is still waiting to read.

```ts
function createCustomNodeCapturer(source: CustomNodeHostSource): CustomNodeCapturer
```

### `createDomRasterBackend`

The zero-dependency browser backend: draw the SVG into a canvas and read the
pixels back out.

Prefers `OffscreenCanvas` when present (so it works in a worker, off the main
thread) and falls back to a detached `<canvas>`.

```ts
function createDomRasterBackend(): RasterBackend
```

### `createExportPipeline`

Bind the whole pipeline to one renderer and one canvas's custom-node hosts.

```ts
function createExportPipeline(
  renderer: PipelineRenderer,
  source: CustomNodeHostSource
): ExportPipeline
```

### `createResvgBackend`

A PNG rasterizer backed by resvg. PNG only — resvg has no JPEG or WebP encoder, and
pretending otherwise would mean handing a caller PNG bytes under a `image/jpeg` mime
type, which is the kind of quiet lie this seam exists to prevent.

`fitTo: width` is what applies the export's scale: the SVG carries the picture, and
resvg renders it at the pixel width we ask for.

```ts
function createResvgBackend(resvg: ResvgModule): RasterBackend
```

### `createSharpBackend`

A rasterizer backed by sharp (libvips + librsvg). Produces all three raster formats.

The SVG goes in as BYTES, not as a path: sharp reads an SVG buffer through librsvg. `density` is how sharp scales vector input — 72 is the 1:1 baseline, so we scale the
DPI by the ratio of the target pixel width to the SVG's intrinsic width and let
librsvg rasterize at that resolution rather than upscaling a small bitmap.

```ts
function createSharpBackend(sharp: SharpModule): RasterBackend
```

### `customNodeBounds`

The union of the captures' world rects.

Needed because a lifted chart is a `<g transform>` full of nested geometry and a
`foreignObject`'s content is opaque — so `vnodeBounds` alone can under-measure a
board. On an all-custom-node dashboard it would find NOTHING and fit the file to a
40px square. The node rects are the truth about where the widgets are, so the box
is fitted to those as well.

```ts
function customNodeBounds(
  captures: readonly CustomNodeCapture[],
  includeIds?: ReadonlySet<string>
): Rectangle | null
```

### `customNodeVNodes`

Turn captures into VNodes ready to append to the exported tree.

Pure: same captures in, same VNodes and same warnings out. Order follows the input,
which the boundary builds from the model's node order — so an export is byte-stable.

```ts
function customNodeVNodes(
  captures: readonly CustomNodeCapture[],
  options: CustomNodeOptions = {}
): { nodes: VNode[]; warnings: string[] }
```

### `dataUrlToBytes`

`data:image/png;base64,…` → the bytes. Throws on a URL that is not base64 data.

```ts
function dataUrlToBytes(dataUrl: string): Uint8Array
```

### `embedModelInPng`

Insert the model into a PNG as an `iTXt` chunk, immediately before `IEND`.

The pixels are untouched: the result is the same image, and any decoder that does
not know the chunk simply skips it (text chunks are ancillary by definition).

```ts
function embedModelInPng(png: Uint8Array, envelope: DiagramDocumentEnvelope): Uint8Array
```

### `embedModelInSvg`

Insert the model into an SVG document, right after the root `<svg …>` tag.

The JSON is XML-escaped text, not base64 — it stays greppable and diffable, and the
envelope's checksum catches any tool that mangles it on the way through.

```ts
function embedModelInSvg(svg: string, envelope: DiagramDocumentEnvelope): string
```

### `escapeAttr`

XML-escape an attribute value.

```ts
function escapeAttr(value: string): string
```

### `escapeText`

XML-escape text content.

```ts
function escapeText(value: string): string
```

### `exportBatch`

Export many documents. Never throws for a job-level failure — a failed job comes back
with `error` set and the batch keeps going.

Results are returned IN INPUT ORDER regardless of the order they finish in, because a
caller zipping results back onto its own list should not have to think about the pool.

```ts
async function exportBatch(jobs: BatchJob[], options: BatchOptions = {}): Promise<BatchResult[]>
```

### `exportScopeFilter`

Which nodes this export will actually contain.

Materializing is the expensive half — it runs a painter — so it is bounded by the
export's own scope rather than mounting a 300-widget board to capture the three
widgets `includeIds` asked for. The predicates mirror what the renderer resolves
`ids` to (`SVGRenderer.selectedIds` reads exactly this `state.selected`), so what is
mounted and what survives `filterCaptures` are the same set.

```ts
function exportScopeFilter(exportOptions?: ExportOptions): (node: NodeModel) => boolean
```

### `exportSvg`

Serialize a rendered VNode tree to a standalone SVG string.

```ts
function exportSvg(root: VNode, options: SvgExportOptions = {}): SvgExportResult
```

**Parameters**

- `root`: the root `<svg>` VNode from `SVGRenderer.render(viewport, zoom)`

### `extractModel`

Find the embedded model in ANY exported artifact — SVG text, a `data:` URL of
either kind, or PNG bytes. `null` when the artifact carries no model (a plain image
is not an error; it is simply not editable).

```ts
function extractModel(artifact: Artifact): DiagramDocumentEnvelope | null
```

### `extractModelFromPng`

Pull the model back out of a PNG. `null` when the image carries none.

Reads BOTH `iTXt` (what we write) and `tEXt` (so a file produced by another tool, or
an older writer, still opens).

```ts
function extractModelFromPng(png: Uint8Array): DiagramDocumentEnvelope | null
```

### `extractModelFromSvg`

Pull the model back out of an SVG. `null` when there is none — a plain SVG is not
an error, it is just not editable.

THROWS if a model IS present but does not parse: a corrupted payload must not
silently degrade into "no model", which would look to the user like a successful
import of an empty diagram.

```ts
function extractModelFromSvg(svg: string): DiagramDocumentEnvelope | null
```

### `fetchAssetsTiered`

Fetch every URL through the tiers above. Deduplicated: a URL is fetched once no
matter how many elements reference it. Resolves when the last URL settles; never
rejects.

```ts
async function fetchAssetsTiered(
  urls: readonly string[],
  options: TieredFetchOptions = {}
): Promise<TieredFetchResult>
```

### `fetchFont`

Fetch a font and turn it into a {@link FontSource}, ready for {@link fontFaceCss}.

```ts
async function fetchFont(
  url: string,
  descriptor: Omit<FontSource, 'data' | 'format'> & { format?: FontFormat },
  options: ResolveAssetsOptions = {}
): Promise<FontSource>
```

### `filterCaptures`

Keep only the captures whose node id is in `ids` — the `includeIds` scoping rule.

```ts
function filterCaptures(
  captures: readonly CustomNodeCapture[],
  ids: Iterable<string> | undefined
): readonly CustomNodeCapture[]
```

### `filterTreeByIds`

Prune a rendered tree down to the given node/link ids.

Returns a new tree; the input is not mutated (the caller's tree is the live
render, and mutating it would corrupt the next frame).

```ts
function filterTreeByIds(root: VNode, ids: Iterable<string>): VNode
```

### `fontFaceCss`

Build the `@font-face` CSS that makes an export carry its own glyphs.

PURE — bytes in, CSS out. Hand the result to `SvgExportOptions.embedFontCss` and the file
renders identically on a machine that has never heard of the typeface.

The `format('…')` hint is not decoration: without it a renderer must sniff the bytes, and
some (notably older librsvg) simply decline and fall back.

```ts
function fontFaceCss(fonts: FontSource[]): string
```

### `fontFormatFromUrl`

`woff2` from a `.woff2` URL, etc. Defaults to woff2 — by far the most common on the web.

```ts
function fontFormatFromUrl(url: string): FontFormat
```

### `importDiagram`

Re-open an exported artifact as a live diagram.

The rehydration runs through the ENGINE's own `DiagramSerializer.deserialize`, which
unwraps the envelope, VERIFIES the checksum (throwing on a mismatch), and runs the
schema migrations. So an artifact exported by an older build opens in a newer one,
and a corrupted one refuses to open rather than opening subtly wrong.

Returns `null` for an artifact with no embedded model.

```ts
function importDiagram(artifact: Artifact, options?: DiagramLoadOptions): DiagramModel | null
```

### `inlineAssets`

Replace external asset URLs with the supplied `data:` URIs. PURE — returns a new tree.

A URL with no entry in the map is LEFT ALONE rather than blanked: a broken-but-present
reference is debuggable, and an element silently stripped of its href is not. The async
layer reports those as warnings.

```ts
function inlineAssets(root: VNode, byUrl: ReadonlyMap<string, string>): VNode
```

### `isEditableArtifact`

Does this artifact carry a model we could re-open?

```ts
function isEditableArtifact(artifact: Artifact): boolean
```

### `isExternalUrl`

Is this a reference that has to leave the document to resolve?

```ts
function isExternalUrl(value: unknown): value is string
```

### `loadNodeRasterBackend`

Find a rasterizer in this Node process.

The imports are DYNAMIC and the specifiers are built at runtime, so a bundler cannot
statically resolve them and will not try to pull a native module into a browser bundle
(which is how an optional native dep usually breaks a web build).

```ts
async function loadNodeRasterBackend(format: 'png' | 'jpeg' | 'webp' = 'png'): Promise<RasterBackend>
```

**Parameters**

- `format`: the format you intend to produce — only sharp can do the lossy ones, so asking for jpeg/webp will not hand you a resvg backend that would then throw.

### `mimeTypeForFormat`

`'png'` → `'image/png'`. Throws for a format that is not a raster format.

```ts
function mimeTypeForFormat(format: string): string
```

### `n`

Deterministic number formatting — no `-0`, no float noise, no locale.

```ts
function n(value: number): string
```

### `nodeBoxes`

Collect the world boxes of the diagram's NODES.

Nodes only. Links are lines: cutting one across a page boundary is normal and reads fine
(the line simply continues on the next tile). Cutting a NODE leaves half a box and half a
word, which is what makes a tiled print look broken.

```ts
function nodeBoxes(root: VNode): Array<{ x: Box; y: Box }>
```

### `padRect`

Grow a rectangle by `padding` on every side.

```ts
function padRect(rect: Rectangle, padding: number): Rectangle
```

### `paginate`

Lay a diagram out across a grid of pages.

```ts
function paginate(root: VNode, options: PaginationOptions): PaginationResult
```

### `printDocument`

Open the browser's print dialog for a document built by {@link buildPrintDocument}.

Prints through a hidden IFRAME rather than `window.open`: a popup is blocked by default in
every browser unless the call is inside a user gesture, and a blocked popup means the
print button silently does nothing. An iframe always works, and it does not disturb the
page the user is on.

Browser-only, and it says so rather than throwing something cryptic in Node.

```ts
function printDocument(html: string): Promise<void>
```

### `resolveAssets`

Fetch every external asset the tree references and inline it as a `data:` URI.

A FAILED ASSET IS A WARNING, NOT A THROW. One 404 avatar must not lose you the export of a
200-node diagram — the reference is left as-is (still broken, but visible and debuggable)
and the caller is told. That is the same rule the batch exporter follows.

```ts
async function resolveAssets(root: VNode, options: ResolveAssetsOptions = {}): Promise<ResolveAssetsResult>
```

### `resolveCssVars`

Replace every `var(--grafloria-*)` in a declaration with its literal theme value. Returns `undefined` when a referenced variable has no value and no fallback —
the declaration is then DROPPED rather than emitted as an unresolvable
reference, and the caller records a warning.

```ts
function resolveCssVars(
  value: string,
  vars: Record<string, string>
): string | undefined
```

### `resolveRasterBackend`

The backend an export will actually use: the caller's, else the browser one,
else a hard failure that tells you exactly what to do.

```ts
function resolveRasterBackend(explicit?: RasterBackend): RasterBackend
```

### `scopeKeysFor`

Every key shape the renderer can mint for a given diagram id.

`renderNode` has a second early-return path for HTML-layer nodes that keys them
`node-<id>-html-layer`, so an exact-match set built only from `node-<id>` would
silently drop those from a selection export.

```ts
function selectionKeys(ids: Iterable<string>): Set<string>
```

### `selectionKeys`

Every key shape the renderer can mint for a given diagram id.

`renderNode` has a second early-return path for HTML-layer nodes that keys them
`node-<id>-html-layer`, so an exact-match set built only from `node-<id>` would
silently drop those from a selection export.

```ts
function selectionKeys(ids: Iterable<string>): Set<string>
```

### `serializeVNode`

Serialize ONE VNode (and its subtree) to an XML string.

Pure: same VNode in, same string out — no ambient state, no DOM, no clock, no
randomness. Attribute order follows prop insertion order, which the renderer
builds deterministically, so two calls on the same tree are byte-identical.

```ts
function serializeVNode(vnode: VNode, options: SerializeOptions = {}): string
```

### `stripResolvedImageWarnings`

Remove the two external-image caveats from a capture's warning — called by the async
export AFTER it embedded every external image the capture held, at which point both
sentences assert a problem that no longer exists (the same defect, mirrored, as
staying silent about one that does). Returns undefined when nothing else remains.

```ts
function stripResolvedImageWarnings(warning: string | undefined): string | undefined
```

### `substituteCssVars`

Substitute every `var(--name[, fallback])` in a CSS value through `lookup`.

Unlike {@link resolveCssVars} (written for our own stylesheet, whose fallbacks are
plain literals) this parses BALANCED parentheses, because an element's inline paint
can carry `var(--grafloria-link-stroke, rgb(107, 114, 128))` — and a fallback that is
itself a `var()`. A name `lookup` cannot answer falls back to the var()'s fallback;
a name with neither is listed in `unresolved` and the value is not returned.

```ts
function substituteCssVars(
  value: string,
  lookup: (name: string) => string | undefined
): { value?: string; unresolved: string[] }
```

### `svgToDataUri`

SVG string → `data:image/svg+xml` URL.

`encodeURIComponent`, NOT base64: `btoa` throws on any non-Latin-1 character,
so a diagram with a non-ASCII label (or the renderer's own '…' ellipsis, or the
📌 lock indicator) would blow up the PNG path.

```ts
function svgToDataUri(svg: string): string
```
