# Export images and documents

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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-nodespec) and [`EdgeSpec`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-instance-edgespec) types. The download helper accepts the instance method's own type and passes [`ExportOptions`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-types-exportoptions) through unchanged.

```ts title="diagram-data.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 title="download.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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-types-types) 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core) in your browser entry. Export from the buttons after the mounted diagram appears.

```bash
npm install @grafloria/element @grafloria/renderer @grafloria/engine
```

```ts title="main.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.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/a7d9cc7e573617152fc3bffabd574843.png)

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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-react) 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 title="ExportDiagram.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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-vue)'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 title="ExportDiagram.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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-angular-components-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 title="export-diagram.component.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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-qwik), 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 title="export-diagram.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](https://grafloria.com/demos/misc/download-image.html) and [vector PDF demo](https://grafloria.com/demos/misc/pdf-export.html).

## 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-export-functions-b-s) 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-export-functions-b-s) 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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents) for documents containing those fields.

```ts title="reopen.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.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/6e73b320d46ae1ef142c22c340d491ae.png)

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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents).

See the live [editable round-trip demo](https://grafloria.com/demos/misc/editable-round-trip.html).

## 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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-element-core) for the headless path. It delegates to [`renderToStaticSVG`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-core) 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-export-functions-b-s). 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 title="server-export.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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-export-functions-b-s) 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`](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/grafloria-renderer-export-functions-b-s), so you do not need either native package there.

See the live [server-side export demo](https://grafloria.com/demos/misc/server-side-export.html).

## 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](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/instance-and-lifecycle) — keep the renderer alive until unmount.
- [Save and restore documents](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/save-and-restore-documents) — persist the full document rather than a framework projection.
- [Theme a canvas](https://bench-grafloria-61d.atloria.app/p/bench-grafloria-61d-mQCmWpDrxv/developer/theme-a-canvas) — size the mounted view and choose the theme its exports use.
