Skip to content
D
Documentation

Export images and documents

how-to
6 min readUpdated

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.

ts
import 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' },
];
ts
import 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.

bash
npm install @grafloria/element @grafloria/renderer @grafloria/engine
ts
import { 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);
Author connects to Review below the SVG, PNG and PDF download buttons.

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.

bash
npm install @grafloria/react @grafloria/renderer @grafloria/engine react react-dom
tsx
import { 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.

bash
npm 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.

bash
npm install @grafloria/angular @angular/common @angular/core @angular/forms @angular/platform-browser @grafloria/engine @grafloria/renderer rxjs @grafloria/element
ts
import { 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.

bash
npm install @grafloria/qwik @grafloria/renderer @grafloria/engine @builder.io/qwik
tsx
import { 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.

ts
import { 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 original and reopened canvases each show Author connected to Review, stacked vertically.

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.

bash
npm install @grafloria/element @grafloria/renderer @grafloria/engine sharp
npm install --save-dev tsx typescript @types/node
ts
import { 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();
bash
npx 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

OptionTypeDefaultWhat it does
scalenumber1Multiplies intrinsic output size without changing the world-space picture.
qualitynumber0.92Sets JPEG/WebP encoder quality, from 0 to 1.
backgroundColorstringTransparent; JPEG uses whitePaints the export backdrop.
paddingnumber20Adds world-space margin around content; ignored with explicit viewport.
scope'content' | 'viewport' | 'selection''content'Chooses whole content, an explicit slice, or selected entities.
includeIdsIterable<string>Not setPrunes output to the named entities.
maxSizenumber4000 for raster outputReduces scale to fit the cap, rather than cropping. SVG has no automatic cap.
embedModelbooleanNot setCarries editable data in SVG/PNG.
embedModelCreatedAtstringWall-clock timestamp when embeddingPins the envelope timestamp for repeatable embedded exports.
customNodeTimeoutnumber5000 msBounds 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.

Was this page helpful?