Use export for a downloadable picture of your live diagram, a vector PDF for printing, or an SVG/PNG that also carries the editable document. Exports render the scene graph, not a screenshot: the default includes content outside the camera.
1. Add download controls after paint
Use DiagramInstance for browser exports. export('svg') returns raw SVG source; PNG, JPEG, WebP and PDF return data URLs. PDF keeps paths as paths and labels as selectable text.
The following samples draw Author → Review, with three download buttons. Click a button to download that format; the status beside the buttons identifies the requested format while export runs. Look for diagram.png in your browser's downloads to confirm the PNG download. The PNG uses 2× scale. SVG and PNG include the editable model.
Create these two shared files in your browser project. The data uses the library's NodeSpec and EdgeSpec types. The download helper accepts the instance method's own type and passes ExportOptions through unchanged.
tsimport type { NodeSpec, EdgeSpec } from '@grafloria/renderer';
export const nodes: NodeSpec[] = [
{ id: 'author', label: 'Author', position: { x: 40, y: 80 },
size: { width: 150, height: 66 }, style: { fill: '#dbeafe', stroke: '#2563eb' } },
{ id: 'review', label: 'Review', position: { x: 300, y: 80 },
size: { width: 150, height: 66 }, style: { fill: '#dcfce7', stroke: '#16a34a' } },
];
export const edges: EdgeSpec[] = [
{ id: 'review-link', source: 'author', target: 'review' },
];
tsimport type { DiagramInstance, ExportFormat, ExportOptions } from '@grafloria/renderer';
export async function downloadDiagram(
exportImage: DiagramInstance['export'],
format: ExportFormat,
): Promise<string> {
const options: ExportOptions = {
scale: format === 'png' ? 2 : 1,
embedModel: format === 'svg' || format === 'png',
embedModelCreatedAt: '2026-01-01T00:00:00Z',
onWarnings: (warnings) => {
if (warnings.length) console.warn('Export fidelity:', warnings);
},
};
const artifact = await exportImage(format, options);
const a = document.createElement('a');
a.href = format === 'svg'
? 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(artifact)
: artifact;
a.download = `diagram.${format}`;
a.click();
return `Download requested: diagram.${format}`;
}
ExportFormat also accepts 'jpeg' and 'webp'. Run the helper in the browser, where the anchor and default raster backend are available.
JavaScript
Install the packages, then use render in your browser entry. Export from the buttons after the mounted diagram appears.
bashnpm install @grafloria/element @grafloria/renderer @grafloria/engine
tsimport { render } from '@grafloria/element';
import type { ExportFormat } from '@grafloria/renderer';
import { nodes, edges } from './diagram-data';
import { downloadDiagram } from './download';
const toolbar = document.createElement('div');
const status = document.createElement('span');
const host = document.createElement('div');
host.style.height = '400px';
document.body.append(toolbar, host);
const instance = render({ nodes, edges }, host);
const formats: ExportFormat[] = ['svg', 'png', 'pdf'];
for (const format of formats) {
const button = document.createElement('button');
button.textContent = `Download ${format.toUpperCase()}`;
button.onclick = async () => {
status.textContent = `Exporting diagram.${format}`;
try {
status.textContent = await downloadDiagram(instance.export, format);
} catch (error) {
status.textContent = error instanceof Error ? error.message : String(error);
}
};
toolbar.append(button);
}
toolbar.append(status);
When your application removes this view, call instance.dispose() from its teardown handler; keep the instance alive while the view remains mounted.
React
Render GrafloriaFlow and hold the ready instance in a ref, following the download demo.
bashnpm install @grafloria/react @grafloria/renderer @grafloria/engine react react-dom
tsximport { useRef, useState } from 'react';
import { GrafloriaFlow } from '@grafloria/react';
import type { DiagramInstance, ExportFormat } from '@grafloria/renderer';
import { nodes, edges } from './diagram-data';
import { downloadDiagram } from './download';
export default function ExportDiagram() {
const instance = useRef<DiagramInstance | null>(null);
const [ready, setReady] = useState(false);
const [status, setStatus] = useState('');
async function save(format: ExportFormat) {
if (!instance.current) return;
setStatus(`Exporting diagram.${format}`);
try {
setStatus(await downloadDiagram(instance.current.export, format));
} catch (error) {
setStatus(error instanceof Error ? error.message : String(error));
}
}
return <div>
<button disabled={!ready} onClick={() => save('svg')}>Download SVG</button>
<button disabled={!ready} onClick={() => save('png')}>Download PNG</button>
<button disabled={!ready} onClick={() => save('pdf')}>Download PDF</button>
<span>{status}</span>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
style={{ height: '400px' }} onInit={(api) => {
instance.current = api;
setReady(true);
}} />
</div>;
}
The React view shows Author connected to Review and three download buttons.
Vue
Use GrafloriaFlow's init event and keep the instance in a shallow ref. The buttons become available after the synchronous paint.
bashnpm install @grafloria/vue @grafloria/renderer @grafloria/engine vue
vue<script setup lang="ts"> import { ref, shallowRef } from 'vue'; import { GrafloriaFlow } from '@grafloria/vue'; import type { DiagramInstance, ExportFormat } from '@grafloria/renderer'; import { nodes, edges } from './diagram-data'; import { downloadDiagram } from './download'; const instance = shallowRef<DiagramInstance | null>(null); const status = ref(''); function onInit(api: DiagramInstance) { api.renderNow(); instance.value = api; } async function save(format: ExportFormat) { if (!instance.value) return; status.value = `Exporting diagram.${format}`; try { status.value = await downloadDiagram(instance.value.export, format); } catch (error) { status.value = error instanceof Error ? error.message : String(error); } } </script> <template> <button :disabled="!instance" @click="save('svg')">Download SVG</button> <button :disabled="!instance" @click="save('png')">Download PNG</button> <button :disabled="!instance" @click="save('pdf')">Download PDF</button> <span>{{ status }}</span> <GrafloriaFlow :default-nodes="nodes" :default-edges="edges" style="height:400px" @init="onInit" /> </template>
The Vue view shows the blue Author node, green Review node and three download buttons.
Angular
Use DiagramCanvasComponent's exportDiagram() method. This component exposes the same async export pipeline without requiring a renderer instance.
bashnpm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer rxjs @grafloria/element
tsimport { Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import type { NodeSpec, EdgeSpec, ExportFormat } from '@grafloria/renderer';
import { nodes, edges } from './diagram-data';
import { downloadDiagram } from './download';
@Component({
selector: 'app-export-diagram',
standalone: true,
imports: [DiagramCanvasComponent],
template: `
<button (click)="save('svg')">Download SVG</button>
<button (click)="save('png')">Download PNG</button>
<button (click)="save('pdf')">Download PDF</button>
<span>{{ status }}</span>
<grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
style="display:block;height:400px" />
`,
})
export class ExportDiagramComponent {
readonly canvas = viewChild.required(DiagramCanvasComponent);
nodes: NodeSpec[] = nodes;
edges: EdgeSpec[] = edges;
status = '';
async save(format: ExportFormat) {
this.status = `Exporting diagram.${format}`;
try {
this.status = await downloadDiagram(
(f, options) => this.canvas().exportDiagram(f, options), format,
);
} catch (error) {
this.status = error instanceof Error ? error.message : String(error);
}
}
}
The Angular canvas shows Author connected to Review below three download buttons.
Export from the button after the canvas appears, not during component construction. An unavailable renderer throws “the canvas has not rendered yet”.
Qwik
Render GrafloriaFlow, and store the ready instance with noSerialize(). The plain specs remain serializable; the live renderer does not enter resumable state.
bashnpm install @grafloria/qwik @grafloria/renderer @grafloria/engine @builder.io/qwik
tsximport { component$, $, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow } from '@grafloria/qwik';
import type { DiagramInstance, ExportFormat } from '@grafloria/renderer';
import { nodes, edges } from './diagram-data';
import { downloadDiagram } from './download';
export default component$(() => {
const instance = useSignal<NoSerialize<DiagramInstance>>();
const status = useSignal('');
const save = $(async (format: ExportFormat) => {
if (!instance.value) return;
status.value = `Exporting diagram.${format}`;
try {
status.value = await downloadDiagram(instance.value.export, format);
} catch (error) {
status.value = error instanceof Error ? error.message : String(error);
}
});
return <div>
<button disabled={!instance.value} onClick$={() => save('svg')}>Download SVG</button>
<button disabled={!instance.value} onClick$={() => save('png')}>Download PNG</button>
<button disabled={!instance.value} onClick$={() => save('pdf')}>Download PDF</button>
<span>{status.value}</span>
<GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
style={{ height: '400px' }} onInit$={$((api: DiagramInstance) => {
api.renderNow();
instance.value = noSerialize(api);
})} />
</div>;
});
The Qwik view shows Author connected to Review with SVG, PNG and PDF download controls.
See the live download image demo and vector PDF demo.
2. Reopen an embedded-model artifact
Pass embedModel: true when exporting SVG or PNG. SVG stores the document in <metadata>; PNG stores it in an iTXt chunk. importDiagram returns a live model, or null for an image with no embedded model. It uses the engine's deserializer, including checksum verification and migrations. extractModel returns the embedded envelope without mounting it.
This browser sample exports a painted diagram and restores its nodes, links and groups into a second mounted canvas. Drag a node in the second canvas to edit the reopened data. It passes those entities as live models rather than projecting them into specs. This mounting recipe does not transfer whiteboard strokes, comments, diagram-level metadata or the saved viewport, although the imported model retains them. Use the full-document restoration path in Save and restore documents for documents containing those fields.
tsimport { render } from '@grafloria/element';
import { importDiagram, extractModel } from '@grafloria/renderer';
import { nodes, edges } from './diagram-data';
async function reopen() {
const originalHost = document.createElement('div');
const reopenedHost = document.createElement('div');
originalHost.style.height = '250px';
reopenedHost.style.height = '250px';
document.body.append(originalHost, reopenedHost);
const original = render({ nodes, edges }, originalHost);
const svg = await original.export('svg', {
embedModel: true, embedModelCreatedAt: '2026-01-01T00:00:00Z',
});
const envelope = extractModel(svg);
const model = importDiagram(svg);
if (!envelope || !model) throw new Error('No embedded model');
const reopened = render({}, reopenedHost);
reopened.setNodes(model.getNodes());
reopened.setEdges(model.getLinks());
reopened.setGroups(model.getGroups());
}
void reopen();
The import functions also accept PNG bytes (Uint8Array) and exported data URLs. Handle an import exception as a damaged artifact, not as an empty document. Embedding does not make JPEG, WebP or PDF editable through this import path. For complete document persistence, including the saved viewport, see Save and restore documents.
See the live editable round-trip demo.
3. Scope a dashboard to its active view
Dashboard tabs park inactive views off-camera, but the default export frames the whole model. On your mounted dashboard, pass includeIds: handle.exportIds() to instance.export('pdf', options); in Angular, pass the same options to canvas.exportDiagram('pdf', options).
exportIds() supplies the active view's group and its widgets, including nested containers and their descendants. Read it at export time, after any view switch. This avoids exporting the large empty gap between parked views. Use exportIds(viewId) to name a particular view instead. See Build a dashboard for obtaining the dashboard handle.
For a camera slice rather than a view's entities, pass scope: 'viewport' and an explicit world-space viewport rectangle. For the live selection, pass scope: 'selection'; it overrides includeIds.
4. Produce server SVG and PNG
Use renderStatic for the headless path. It delegates to renderToStaticSVG and returns SVG, CSS, layer markup and a hydration snapshot. Specify width and height rather than relying on the 800×600 defaults. Identical specs and options produce deterministic SVG and CSS.
The static SVG needs the returned CSS as well as standalone: true. Fold that stylesheet into the file, as the server export demo does. Custom HTML-layer components are not server-rendered by this path.
For Node raster output, use the shipped createSharpBackend. This step goes below the browser instance because the server has no mounted canvas or browser SVG rasterizer. Sharp accepts SVG bytes; its adapter returns a data URL. Install the native package only in your server project.
bashnpm install @grafloria/element @grafloria/renderer @grafloria/engine sharp npm install --save-dev tsx typescript @types/node
tsimport { writeFile } from 'node:fs/promises';
import { Buffer } from 'node:buffer';
import sharp from 'sharp';
import { renderStatic } from '@grafloria/element';
import { createSharpBackend } from '@grafloria/renderer';
import { nodes, edges } from './diagram-data';
async function exportFiles() {
const width = 520;
const height = 300;
const result = renderStatic({ nodes, edges, width, height, standalone: true });
const svg = result.svg.replace(/^(<svg[^>]*>)/, `$1<style>${result.css}</style>`);
await writeFile('diagram.svg', svg, 'utf8');
const png = await createSharpBackend(sharp).rasterize({
svg, width, height, mimeType: 'image/png',
});
const payload = png.slice(png.indexOf(',') + 1);
await writeFile('diagram.png', Buffer.from(payload, 'base64'));
}
void exportFiles();
bashnpx tsx server-export.ts
Open diagram.svg or diagram.png: both show Author connected to Review in a 520×300 image. Keep specs, options, fonts and rasterizer versions fixed when you need reproducible raster bytes; deterministic SVG alone does not pin an external encoder's environment.
The shipped createResvgBackend accepts the @resvg/resvg-js module and encodes PNG only; it rejects JPEG and WebP. Sharp supports all three raster formats. In the browser, exports default to createDomRasterBackend, so you do not need either native package there.
See the live server-side export demo.
Options and fidelity
| Option | Type | Default | What it does |
|---|---|---|---|
scale | number | 1 | Multiplies intrinsic output size without changing the world-space picture. |
quality | number | 0.92 | Sets JPEG/WebP encoder quality, from 0 to 1. |
backgroundColor | string | Transparent; JPEG uses white | Paints the export backdrop. |
padding | number | 20 | Adds world-space margin around content; ignored with explicit viewport. |
scope | 'content' | 'viewport' | 'selection' | 'content' | Chooses whole content, an explicit slice, or selected entities. |
includeIds | Iterable<string> | Not set | Prunes output to the named entities. |
maxSize | number | 4000 for raster output | Reduces scale to fit the cap, rather than cropping. SVG has no automatic cap. |
embedModel | boolean | Not set | Carries editable data in SVG/PNG. |
embedModelCreatedAt | string | Wall-clock timestamp when embedding | Pins the envelope timestamp for repeatable embedded exports. |
customNodeTimeout | number | 5000 ms | Bounds waiting for tracked async painters; reports unfinished paint. |
Prefer async export when custom nodes or external images matter. It waits for tracked painters and fetches external images; onWarnings reports fidelity gaps. exportSvgString() and exportPdf() capture current custom-node content synchronously and do not fetch assets. Angular names the synchronous SVG method exportSvg().
If an image server blocks CORS, use assetFetcher to route through your own allowlisted proxy, or supply resolvedAssets with bytes already encoded as data URLs. Never expose an unrestricted image proxy: arbitrary upstream URLs create an SSRF risk. An unresolved image remains an SVG reference but is missing from PDF, with a warning.
Browser file export does not preserve CSS animations. Fonts are not embedded automatically; use embedFonts or embedFontCss when the SVG must carry its own glyph resources. Inspect warnings before distributing files with custom HTML content.
Related
- Instance and lifecycle — keep the renderer alive until unmount.
- Save and restore documents — persist the full document rather than a framework projection.
- Theme a canvas — size the mounted view and choose the theme its exports use.
Was this page helpful?