Import these from @grafloria/renderer.
Functions
base64ToBytes
Pure base64 → bytes. (No atob: it does not exist in Node, and this must stay DOM-free.)
tsfunction 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.
tsfunction buildPrintDocument(svgPages: string[], options: PrintOptions = {}): string
bytesToBase64
Pure bytes → base64.
tsfunction bytesToBase64(bytes: Uint8Array): string
bytesToDataUrl
bytes → data:<mime>;base64,…
tsfunction bytesToDataUrl(bytes: Uint8Array, mimeType: string): string
canRasterizeInThisEnvironment
Is there a canvas we can draw into? (browser main thread or worker)
tsfunction 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.
tsfunction 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.
tsfunction 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.
tsasync 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.
tsfunction 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.
tsfunction 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.
tsfunction computeBreaks(
start: number,
end: number,
pageSize: number,
boxes: Box[],
options: { snap: boolean; tolerance: number; overlap: number; warnings: string[]; axis: 'x' | 'y' }
): number[]
crc32
tsfunction 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.
tsfunction createClassStyleResolver(theme: Theme, warnings: string[] = []): ClassStyleResolver
Parameters
theme: the theme whose--grafloria-*values get baked inwarnings: collector — a declaration whose variable cannot be resolved is dropped and reported here rather than silently emittingvar(--…).
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.
tsfunction 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>.
tsfunction createDomRasterBackend(): RasterBackend
createExportPipeline
Bind the whole pipeline to one renderer and one canvas's custom-node hosts.
tsfunction 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.
tsfunction 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.
tsfunction 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.
tsfunction 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.
tsfunction 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.
tsfunction 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).
tsfunction 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.
tsfunction embedModelInSvg(svg: string, envelope: DiagramDocumentEnvelope): string
escapeAttr
XML-escape an attribute value.
tsfunction escapeAttr(value: string): string
escapeText
XML-escape text content.
tsfunction 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.
tsasync 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.
tsfunction exportScopeFilter(exportOptions?: ExportOptions): (node: NodeModel) => boolean
exportSvg
Serialize a rendered VNode tree to a standalone SVG string.
tsfunction exportSvg(root: VNode, options: SvgExportOptions = {}): SvgExportResult
Parameters
root: the root<svg>VNode fromSVGRenderer.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).
tsfunction 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).
tsfunction 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.
tsfunction 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.
tsasync function fetchAssetsTiered(
urls: readonly string[],
options: TieredFetchOptions = {}
): Promise<TieredFetchResult>
fetchFont
Fetch a font and turn it into a {@link FontSource}, ready for {@link fontFaceCss}.
tsasync 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.
tsfunction 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).
tsfunction 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.
tsfunction fontFaceCss(fonts: FontSource[]): string
fontFormatFromUrl
woff2 from a .woff2 URL, etc. Defaults to woff2 — by far the most common on the web.
tsfunction 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.
tsfunction 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.
tsfunction inlineAssets(root: VNode, byUrl: ReadonlyMap<string, string>): VNode
isEditableArtifact
Does this artifact carry a model we could re-open?
tsfunction isEditableArtifact(artifact: Artifact): boolean
isExternalUrl
Is this a reference that has to leave the document to resolve?
tsfunction 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).
tsasync 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.
tsfunction mimeTypeForFormat(format: string): string
n
Deterministic number formatting — no -0, no float noise, no locale.
tsfunction 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.
tsfunction nodeBoxes(root: VNode): Array<{ x: Box; y: Box }>
padRect
Grow a rectangle by padding on every side.
tsfunction padRect(rect: Rectangle, padding: number): Rectangle
paginate
Lay a diagram out across a grid of pages.
tsfunction 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.
tsfunction 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.
tsasync 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.
tsfunction 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.
tsfunction 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.
tsfunction 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.
tsfunction 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.
tsfunction 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.
tsfunction 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.
tsfunction 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.
tsfunction svgToDataUri(svg: string): string
Was this page helpful?