# Export diagrams

Export the scene graph from a painted diagram as SVG, PNG, PDF, or a deterministic server-rendered SVG. Set the output `width` and `height` explicitly for headless output; browser exports use the live instance.

## When to use each entry point

Use a [`DiagramInstance`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-instance-diagraminstance#diagraminstance) when the diagram is mounted. Its `export()` method returns a promise: SVG is a raw SVG string, while PNG, JPEG, WebP, and PDF are data URLs. The [`ExportFormat`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-types-types#exportformat) values are `'png'`, `'svg'`, `'jpeg'`, `'webp'`, and `'pdf'`.

Use `exportSvgString()` when you need a synchronous, DOM-free SVG result. It returns an [`SvgExportResult`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-export-interfaces#svgexportresult), including `warnings`. Use `exportPdf()` for synchronous PDF bytes; it returns a [`PdfExportResult`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-export-interfaces#pdfexportresult). PDF paths remain paths and text remains selectable text.

Use [`renderStatic`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-element-core#renderstatic) when nothing is mounted. It returns a [`StaticRenderResult`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-core#staticrenderresult) containing `svg`, `css`, `html`, and `snapshot`, and does not require a DOM.

## Export a mounted diagram

Give the host a resolved height so the live diagram paints before you export it. The following sample creates download buttons, then downloads a PNG at 2× scale or a raw SVG file:

:::code-group
```js title="JavaScript"
import { render } from '@grafloria/element';

document.body.innerHTML = '<button id="png">Download PNG</button><button id="svg">Download SVG</button><div id="diagram" style="height:400px"></div>';
const host = document.getElementById('diagram');
if (!host) throw new Error('Missing #diagram');

const instance = render({
  nodes: [
    { id: 'a', label: 'Extract', position: { x: 40, y: 60 }, size: { width: 150, height: 66 } },
    { id: 'b', label: 'Load', position: { x: 260, y: 60 }, size: { width: 150, height: 66 } },
  ],
  edges: [{ id: 'e1', source: 'a', target: 'b' }],
}, host);

instance.renderNow();

function download(href, filename) {
  const link = document.createElement('a');
  link.href = href;
  link.download = filename;
  link.click();
}

document.getElementById('png')?.addEventListener('click', async () => {
  download(await instance.export('png', { scale: 2 }), 'diagram.png');
});

document.getElementById('svg')?.addEventListener('click', async () => {
  const svg = await instance.export('svg');
  download('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg), 'diagram.svg');
});
```
```ts title="Angular"
import { Component, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <button (click)="downloadPng()">Download PNG</button>
    <button (click)="downloadSvg()">Download SVG</button>
    <grafloria-diagram-canvas [(nodes)]="nodes" [(edges)]="edges"
      style="display:block;height:400px" />
  `,
})
export class ExportComponent {
  canvas = viewChild.required(DiagramCanvasComponent);
  nodes = [
    { id: 'a', label: 'Extract', position: { x: 40, y: 60 }, size: { width: 150, height: 66 } },
    { id: 'b', label: 'Load', position: { x: 260, y: 60 }, size: { width: 150, height: 66 } },
  ];
  edges = [{ id: 'e1', source: 'a', target: 'b' }];

  private save(href: string, filename: string): void {
    const link = document.createElement('a');
    link.href = href;
    link.download = filename;
    link.click();
  }

  async downloadPng(): Promise<void> {
    this.save(await this.canvas().exportDiagram('png', { scale: 2 }), 'diagram.png');
  }

  async downloadSvg(): Promise<void> {
    const svg = await this.canvas().exportDiagram('svg');
    this.save('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg), 'diagram.svg');
  }

}
```
```tsx title="Qwik"
import { component$, $, noSerialize, useSignal, type NoSerialize } from '@builder.io/qwik';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/qwik';

const nodes = [
  { id: 'a', label: 'Extract', position: { x: 40, y: 60 }, size: { width: 150, height: 66 } },
  { id: 'b', label: 'Load', position: { x: 260, y: 60 }, size: { width: 150, height: 66 } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];

export default component$(() => {
  const instance = useSignal<NoSerialize<DiagramInstance>>();
  const save = $((href: string, filename: string) => {
    const link = document.createElement('a');
    link.href = href;
    link.download = filename;
    link.click();
  });
  return <div>
    <button onClick$={async () => save(await instance.value!.export('png', { scale: 2 }), 'diagram.png')}>Download PNG</button>
    <button onClick$={async () => {
      const svg = await instance.value!.export('svg');
      save('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg), 'diagram.svg');
    }}>Download SVG</button>
    <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
      style={{ display: 'block', height: '400px' }}
      onInit$={$((api: DiagramInstance) => { instance.value = noSerialize(api); })} />
  </div>;
});
```
```tsx title="React"
import { useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';

const nodes = [
  { id: 'a', label: 'Extract', position: { x: 40, y: 60 }, size: { width: 150, height: 66 } },
  { id: 'b', label: 'Load', position: { x: 260, y: 60 }, size: { width: 150, height: 66 } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];

export default function ExportDiagram() {
  const instance = useRef<DiagramInstance | null>(null);
  const save = (href: string, filename: string) => {
    const link = document.createElement('a');
    link.href = href;
    link.download = filename;
    link.click();
  };
  return <div>
    <button onClick={async () => save(await instance.current!.export('png', { scale: 2 }), 'diagram.png')}>Download PNG</button>
    <button onClick={async () => {
      const svg = await instance.current!.export('svg');
      save('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg), 'diagram.svg');
    }}>Download SVG</button>
    <GrafloriaFlow defaultNodes={nodes} defaultEdges={edges}
      style={{ display: 'block', height: 400 }}
      onInit={(api) => { instance.current = api; }} />
  </div>;
}
```
```vue title="Vue"
<script setup lang="ts">
import { ref } from 'vue';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/vue';

const instance = ref<DiagramInstance | null>(null);
const nodes = [
  { id: 'a', label: 'Extract', position: { x: 40, y: 60 }, size: { width: 150, height: 66 } },
  { id: 'b', label: 'Load', position: { x: 260, y: 60 }, size: { width: 150, height: 66 } },
];
const edges = [{ id: 'e1', source: 'a', target: 'b' }];

function save(href: string, filename: string): void {
  const link = document.createElement('a');
  link.href = href;
  link.download = filename;
  link.click();
}
async function downloadPng(): Promise<void> { save(await instance.value!.export('png', { scale: 2 }), 'diagram.png'); }
async function downloadSvg(): Promise<void> {
  const svg = await instance.value!.export('svg');
  save('data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg), 'diagram.svg');
}
</script>

<template>
  <button @click="downloadPng">Download PNG</button>
  <button @click="downloadSvg">Download SVG</button>
  <GrafloriaFlow :default-nodes="nodes" :default-edges="edges"
    style="display:block;height:400px" @init="instance = $event" />
</template>
```
:::

The mounted result is the same diagram in every tab. PNG downloads as a raster data URL at 2× scale; SVG downloads as vector markup containing the node labels and edges. Pass [`ExportOptions`](https://atloria.dev/p/grafloria-h7YM7amryF/developer/grafloria-renderer-types-exportoptions#exportoptions) such as `scale`, `backgroundColor`, `viewport`, or `padding` as the second argument.

![The mounted diagram shows two connected labeled nodes below the PNG and SVG download buttons.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/6b9f3062e2cfde001425eb3167c43d87.png)

## Export PDF

`export('pdf')` returns a `data:application/pdf;base64,…` URL. Download it like the PNG above. The resulting PDF keeps paths as paths and text as selectable text. For bytes without a data URL, call `instance.exportPdf()` and read its `pdf` `Uint8Array`.

```js
import { render } from '@grafloria/element';

document.body.innerHTML = '<button id="pdf">Download PDF</button><div id="pdf-diagram" style="height:400px"></div>';
const host = document.getElementById('pdf-diagram');
if (!host) throw new Error('Missing #pdf-diagram');

const instance = render({
  nodes: [
    { id: 'a', label: 'Requirements', position: { x: 40, y: 60 }, size: { width: 170, height: 66 } },
    { id: 'b', label: 'Design', position: { x: 260, y: 60 }, size: { width: 170, height: 66 } },
    { id: 'c', label: 'Ship', position: { x: 480, y: 60 }, size: { width: 170, height: 66 } },
  ],
  edges: [{ id: 'e1', source: 'a', target: 'b' }, { id: 'e2', source: 'b', target: 'c' }],
}, host);

instance.renderNow();
document.getElementById('pdf')?.addEventListener('click', async () => {
  const link = document.createElement('a');
  link.href = await instance.export('pdf');
  link.download = 'diagram.pdf';
  link.click();
});
```

## Render a deterministic SVG without mounting

Pass nodes, edges, and explicit dimensions to `renderStatic()`. `width` and `height` are CSS-pixel dimensions; `standalone: true` adds the SVG namespace so `svg` can be written as a file. The result is a styled artifact when you place `css` in a `<style>` element:

```ts
import { renderStatic } from '@grafloria/element';

const result = renderStatic({
  nodes: [
    { id: 'a', label: 'Extract', position: { x: 40, y: 40 }, size: { width: 150, height: 66 } },
    { id: 'b', label: 'Load', position: { x: 260, y: 40 }, size: { width: 150, height: 66 } },
  ],
  edges: [{ id: 'e1', source: 'a', target: 'b' }],
  width: 520,
  height: 300,
  standalone: true,
});

const fileContents = result.svg.replace(/^<svg([^>]*)>/, '<svg$1><style>' + result.css + '</style>');
document.body.innerHTML = '<div id="preview"></div><div id="fingerprint"></div>';
const preview = document.getElementById('preview');
if (!preview) throw new Error('Missing #preview');
preview.innerHTML = fileContents;

let hash = 0x811c9dc5;
for (let index = 0; index < fileContents.length; index += 1) {
  hash ^= fileContents.charCodeAt(index);
  hash = Math.imul(hash, 0x01000193);
}
const fingerprint = (hash >>> 0).toString(16);
const fingerprintElement = document.getElementById('fingerprint');
if (fingerprintElement) fingerprintElement.textContent = `artifact fingerprint: ${fingerprint}`;
```

Repeated calls with the same spec and dimensions produce byte-identical SVG and CSS. This path uses no DOM and does not mount an interactive diagram; custom and HTML-layer nodes are not server-rendered.

![The static export sample shows the rendered SVG preview and its deterministic artifact fingerprint.](https://atloriaassets.blob.core.windows.net/assets/a438221c-c1af-407c-b9e6-a56c986db846/6ac69771ac49754f04afed19215f0494.png)

## Options that matter

| Option | Type | Default | What it does |
| --- | --- | --- | --- |
| `scale` | `number` | `1` | Scales raster output. |
| `quality` | `number` | `0.92` | Sets JPEG/WebP quality from 0 to 1. |
| `backgroundColor` | `string` | transparent | Sets the raster background. |
| `viewport` | `Rectangle` | content bounds | Selects the world-space rectangle to export. |
| `padding` | `number` | `20` | Adds world-unit margin around content when `viewport` is not explicit. |
| `width` | `number` | `800` | Sets the static render width in CSS pixels. |
| `height` | `number` | `600` | Sets the static render height in CSS pixels. |
| `fitView` | `boolean` | `false` | Frames static content instead of using `viewport` and `zoom`. |

## Pitfalls

- Export after the first paint. A mounted export requires a painted canvas; call `renderNow()` when you need to force that synchronous paint.
- `export('svg')` returns a string, not a data URL. Prefix it with `data:image/svg+xml;charset=utf-8,` and encode it before downloading.
- Synchronous SVG and PDF exports cannot wait for unfinished asynchronous custom-node painters; inspect their `warnings`. The asynchronous `export()` call waits for those painters.
- Static output is SVG only. It does not mount a live instance, and framework HTML-layer nodes are not included.

## Live demos

- [Download PNG and SVG](https://grafloria.com/demos/misc/download-image.html)

- [Download a vector PDF](https://grafloria.com/demos/misc/pdf-export.html)

- [Render a deterministic SVG without a DOM](https://grafloria.com/demos/misc/server-side-export.html)

## Related

- [How Grafloria works](https://atloria.dev/p/grafloria-h7YM7amryF/developer/how-grafloria-works)
- [Style a diagram](https://atloria.dev/p/grafloria-h7YM7amryF/developer/style-a-diagram)
- [The DiagramInstance](https://atloria.dev/p/grafloria-h7YM7amryF/developer/instance-and-bindings)
