# How drawio-desktop PDF Export Works Using @cantoo/pdf-lib

> Discover how drawio-desktop PDF export merges rasterized buffers using webContents printToPDF and @cantoo/pdf-lib to create PDFs with embedded metadata and original diagram XML.

- Repository: [draw.io/drawio-desktop](https://github.com/jgraph/drawio-desktop)
- Tags: internals
- Published: 2026-03-05

---

**Draw.io Desktop generates PDFs through a two-stage pipeline where Electron first rasterizes each diagram page into individual buffers using `webContents.printToPDF()`, then leverages `@cantoo/pdf-lib` to merge these buffers, embed metadata, and optionally attach the original diagram XML as a file or encoded Subject field.**

The **drawio-desktop** application (also known as diagrams.net desktop) implements its PDF export functionality using `@cantoo/pdf-lib` to manipulate PDF documents without native dependencies. This implementation combines Electron's printing APIs with pure-JavaScript PDF assembly to support both single-page exports and complex multi-page diagrams while preserving editable XML content.

## The Two-Stage PDF Export Architecture

The export process operates in distinct rasterization and assembly phases coordinated between the Electron main process and an off-screen renderer.

### Stage 1: Page-by-Page Rasterization

When a user initiates a PDF export, the main process in [`src/main/electron.js`](https://github.com/jgraph/drawio-desktop/blob/main/src/main/electron.js) spawns a hidden **BrowserWindow** loading [`export3.html`](https://github.com/jgraph/drawio-desktop/blob/main/export3.html). The renderer draws each diagram page onto a hidden canvas and signals completion via IPC. The main process then captures the output:

- For physical printing (`args.print === true`), the window calls `webContents.print()` and returns immediately.
- For digital PDF export, the window calls `webContents.printToPDF(pdfOptions)`, which returns a **Buffer** containing a single-page PDF for the current diagram page.

These buffers accumulate in an array (`pdfs`) until all pages are rendered.

### Stage 2: PDF Assembly with @cantoo/pdf-lib

Once the last page is collected, the main process invokes `mergePdfs(pdfs, xml)`, the sole location where `@cantoo/pdf-lib` is utilized. This function handles metadata injection, XML embedding, and page consolidation according to the source code in [`src/main/electron.js`](https://github.com/jgraph/drawio-desktop/blob/main/src/main/electron.js).

## Inside the mergePdfs Function

The `mergePdfs` function, located in [`src/main/electron.js`](https://github.com/jgraph/drawio-desktop/blob/main/src/main/electron.js), differentiates between single-page and multi-page exports to optimize metadata storage.

### Single-Page Export Flow

When `pdfFiles.length === 1`, the function loads the buffer using `PDFDocument.load()` and sets the **Creator** metadata to `diagrams.net`. If XML embedding is requested, the diagram data is encoded and stored in the **Subject** field rather than as a file attachment, ensuring compatibility with PDF viewers that poorly support attachments:

```javascript
const doc = await PDFDocument.load(pdfBuffers[0]);
doc.setCreator('diagrams.net');
if (xml) {
    doc.setSubject(
        encodeURIComponent(xml).replace(/\(/g, '\\(').replace(/\)/g, '\\)')
    );
}
return Buffer.from(await doc.save());

```

### Multi-Page Export Flow

For documents with multiple pages, the function creates a fresh PDF using `PDFDocument.create()` and copies pages from each rendered buffer:

```javascript
const doc = await PDFDocument.create();
doc.setCreator('diagrams.net');

if (xml) {
    await doc.attach(
        Buffer.from(xml).toString('base64'), 
        'diagram.xml', 
        {
            mimeType: 'application/vnd.jgraph.mxfile',
            description: 'Diagram Content',
        }
    );
}

for (const buf of pdfBuffers) {
    const part = await PDFDocument.load(buf);
    const pages = await doc.copyPages(part, part.getPageIndices());
    pages.forEach(p => doc.addPage(p));
}

return Buffer.from(await doc.save());

```

The **multi-page** approach uses `pdfDoc.attach()` to embed the diagram XML as a base64-encoded file attachment, which is more reliable for multi-page documents than the Subject field approach.

## Key Files in the Export Pipeline

- **[`src/main/electron.js`](https://github.com/jgraph/drawio-desktop/blob/main/src/main/electron.js)** – Contains the main process logic, IPC handlers, and the `mergePdfs` function that imports `PDFDocument` from `@cantoo/pdf-lib` at line 13.
- **[`src/main/electron-preload.js`](https://github.com/jgraph/drawio-desktop/blob/main/src/main/electron-preload.js)** – Provides the secure preload bridge exposing `exportDiagram` to the renderer process.
- **[`export3.html`](https://github.com/jgraph/drawio-desktop/blob/main/export3.html)** – The hidden renderer page that receives `render` messages, draws diagrams on a hidden canvas, and emits `render-finished` signals to trigger the PDF capture.

## Why @cantoo/pdf-lib?

The library selection provides three critical capabilities for drawio-desktop:

- **Pure JavaScript implementation** – Operates without native system dependencies, ensuring cross-platform compatibility across Windows, macOS, and Linux.
- **Page copying** – The `PDFDocument.copyPages()` method enables efficient merging of multiple Electron-generated PDF buffers into a single document.
- **Arbitrary data attachment** – The `pdfDoc.attach()` API allows embedding the original diagram XML directly into the PDF container, preserving editability when files are reopened in draw.io.

## Summary

- Drawio-desktop uses **Electron's `webContents.printToPDF()`** to render diagram pages as individual PDF buffers in an off-screen BrowserWindow.
- The **`mergePdfs`** function in [`src/main/electron.js`](https://github.com/jgraph/drawio-desktop/blob/main/src/main/electron.js) uses `@cantoo/pdf-lib` to consolidate these buffers, with distinct logic for single-page (Subject field embedding) and multi-page (file attachment) exports.
- **Metadata injection** includes setting the Creator field to `diagrams.net` and optionally embedding the original diagram XML for round-trip editing.
- The process relies on standard **PDFDocument** methods including `load()`, `create()`, `copyPages()`, `addPage()`, and `attach()`.

## Frequently Asked Questions

### Why does drawio-desktop embed XML into exported PDFs?

The application embeds diagram XML to enable **round-trip editing**. When a user reopens an exported PDF in draw.io, the application extracts the embedded XML from either the Subject field (single-page) or the file attachment (multi-page) to restore the editable diagram state.

### How does @cantoo/pdf-lib differ from pdf-lib?

**@cantoo/pdf-lib** is a fork or specific distribution of the pdf-lib library that provides a pure-JavaScript API for PDF manipulation. It allows drawio-desktop to create, load, and modify PDF documents without requiring native compilation steps, making it ideal for Electron applications targeting multiple platforms.

### What triggers the multi-page versus single-page export path?

The export path depends on the page count of the diagram. If the user exports a diagram containing only one page (or selects a single page in the export dialog), the system uses the **single-page** logic where XML is stored in the Subject field. Multi-page diagrams automatically trigger the **merge logic** that uses `copyPages` and file attachments.

### Where is the PDF export logic implemented in the source code?

All PDF export orchestration resides in **[`src/main/electron.js`](https://github.com/jgraph/drawio-desktop/blob/main/src/main/electron.js)**, specifically within the IPC handlers that manage the `export-diagram` event and the `mergePdfs` helper function (lines 78-130 in the current source). The actual diagram rendering occurs in [`export3.html`](https://github.com/jgraph/drawio-desktop/blob/main/export3.html), which communicates back to the main process via `ipcRenderer`.