PDF Export
Since v1.4.0 a ReoGrid instance can render the active worksheet to a real, vector PDF — text, borders, fills, merges, and cell-anchored images — with no external library and no server round-trip. Output is generated in-browser from worksheet data. Since v1.7.0 it can also put several sheets, or the whole workbook, into one PDF.
Note: PDF Export is available in the Pro edition.
A font is REQUIRED. The PDF renderer embeds glyph outlines, so every export needs real font bytes. Name a
locale('ja','zh-CN','zh-TW','ko') and ReoGrid resolves the font for you, or pass your own bytes infont. Without one of the two the export throws. This is what makes Japanese, Chinese and Korean text render correctly.
Quick start
import { createReogrid, preloadPdfFont } from '@reogrid/pro'
const grid = createReogrid({ workspace: '#app' })
// Fetch the locale's font once, at an async point you control
await preloadPdfFont('ja')
// Render + download in one call
grid.saveAsPdf({ locale: 'ja', filename: 'report.pdf' })
saveAsPdf() renders and triggers a browser download. Use exportPdf() when you want the raw bytes (e.g. to upload or preview):
const bytes: Uint8Array = grid.exportPdf({ locale: 'ja' })
// e.g. wrap in a Blob for a preview URL
const url = URL.createObjectURL(new Blob([bytes], { type: 'application/pdf' }))
Why the separate
preloadPdfFontstep? Export is synchronous on purpose:saveAsPdfends in a click on a generated link, which loses its user-gesture context — and gets caught by popup blockers — if anawaitruns first. So the download happens at a point you choose, once, and every later export is instant. It also keeps a multi-MB font fetch from hiding inside an innocuous-lookingsaveAsPdf().
Locales
locale names a font ReoGrid knows how to fetch. Four are registered out of the box:
| Locale | Font | Use for |
|---|---|---|
'ja' | Noto Sans JP | Japanese |
'zh-CN' | Noto Sans SC | Simplified Chinese |
'zh-TW' | Noto Sans TC | Traditional Chinese |
'ko' | Noto Sans KR | Korean |
import { preloadPdfFont, registerPdfFont } from '@reogrid/pro'
await preloadPdfFont('zh-CN') // once, at app start
grid.saveAsPdf({ locale: 'zh-CN' }) // anywhere, thereafter
Picking the right locale matters: a Japanese font covers only about a third of common simplified-Chinese business vocabulary, so exporting Chinese text with 'ja' produces tofu (□□□) for the rest.
Register your own locale — with a loader, a URL, or bytes you already have:
registerPdfFont('th', () => loadFont('/fonts/NotoSansThai-Subset.ttf'))
await preloadPdfFont('th')
grid.saveAsPdf({ locale: 'th' })
Registering bytes marks the locale loaded immediately, so no preload is needed — that is the way to avoid the network entirely (e.g. a font shipped with your app or installed from a package).
Related helpers, all exported from @reogrid/pro: isPdfFontRegistered, isPdfFontLoaded, getPdfFont, getPdfFontFaces, getPdfFontWeights, getRegisteredPdfFontTags, removePdfFontWeight, clearPdfFontCache.
Font weights
A PDF embeds glyph outlines, so a weight is a separate font file — there is nothing to interpolate at draw time. Since v1.6.0 a tag therefore holds a face per weight: thin, normal and bold.
// One source registers as `normal`
registerPdfFont('brand', regularBytes)
// Or name the weights — this is the shape the @reogrid/font-* packages export
registerPdfFont('brand', { normal: regularBytes, bold: boldBytes })
// Add one weight to an existing tag
registerPdfFont('brand', { bold: '/fonts/Brand-Bold.ttf' })
preloadPdfFont(tag) fetches normal and bold by default, because a worksheet almost always has bold headers. Narrow it when you know the document has none:
await preloadPdfFont('ja', { weights: ['normal'] }) // halve the download
It still resolves with the regular face’s bytes, so calls written before v1.6.0 are unchanged.
Bold cells are drawn with the bold face when one is present. Without it they fall back to a synthetic bold — a 4% stroke of the regular outline — which is all a single-weight document can do.
This is what fixed the hairline-PDF bug. Before v1.6.0 the built-in tags pointed at Google’s variable fonts, which keep their default-position outlines in
glyfand the deltas ingvar. PDF export embedsglyfand does not applygvar, so every document came out at thefvardefault of wght=100 — Thin (/BaseFont /…+NotoSansJP-Thin), and synthetic bold had a hairline outline to thicken. The built-in URLs now point at static faces.
Registering a single source for a built-in tag leaves that tag’s other built-in weights in place. If that is not what you want, register { normal: … } and drop the rest with removePdfFontWeight.
Font packages (no CDN, works offline)
The built-in locales fetch the full upstream font from a public CDN — Noto Sans SC alone is about 17 MB, and that CDN is slow and intermittently unreachable from mainland China. For production, install the matching subset package instead and register it: the bytes come from npm, so there is no runtime network at all, which also works on the offline intranets common in enterprise deployments.
npm install @reogrid/font-sc # zh-CN — also font-tc, font-jp, font-kr
import { registerPdfFont, preloadPdfFont } from '@reogrid/pro'
import { notoSansSC } from '@reogrid/font-sc'
registerPdfFont('zh-CN', notoSansSC) // every weight the package has
await preloadPdfFont('zh-CN') // once, at app start — normal + bold
grid.saveAsPdf({ locale: 'zh-CN', filename: 'report.pdf' })
Font packages v2. Since ReoGrid v1.6.0 the packages ship Thin / Regular / Bold static faces, and their default export is an object of exactly the shape
registerPdfFontwants — so hand overnotoSansSCrather than a single loader.registerPdfFont('zh-CN', loadNotoSansSC)still works, but registers only the regular face, which leaves bold text to the synthetic fallback.
| Package | Locale | Export | Characters | Per face |
|---|---|---|---|---|
@reogrid/font-sc | 'zh-CN' | notoSansSC | 7,709 (GB 2312) | ~2,378 KB |
@reogrid/font-tc | 'zh-TW' | notoSansTC | 13,682 (Big5) | ~4,756 KB |
@reogrid/font-jp | 'ja' | notoSansJP | 6,974 (JIS X 0208) | ~2,557 KB |
@reogrid/font-kr | 'ko' | notoSansKR | 3,196 (KS X 1001) | ~567 KB |
A default preload fetches two of those faces (regular and bold). loadNotoSansSC(weight?) and friends are still exported for a single face.
Each subset carries its language’s classic national standard including level 2 — that is where personal and place names live, and a customer’s name rendering as tofu is worse than a slightly larger download. The bytes sit behind a dynamic import, so a bundler gives them their own chunk: an app that never exports a PDF never downloads them.
Supplying your own font bytes
Use font for a typeface ReoGrid does not know about. It takes raw TrueType glyf bytes (ArrayBuffer | Uint8Array), and wins when both font and locale are given. Three ways to get them:
import { loadDefaultJapaneseFont, loadFont, DEFAULT_JAPANESE_FONT_URL } from '@reogrid/pro'
// 1. Bundled default — Noto Sans JP (~9.6 MB, fetched from a public CDN, cached)
const a = await loadDefaultJapaneseFont()
// 2. Your own hosted subset (recommended for production — much smaller)
const b = await loadFont('https://cdn.example.com/fonts/NotoSansJP-Subset.ttf')
// 3. Bytes you already have (e.g. from an <input type="file"> or fetch)
const c = new Uint8Array(await file.arrayBuffer())
Both helpers cache the downloaded bytes in memory and in Cache Storage, so the ~9.6 MB default is fetched at most once per browser. For production, host a subset of the glyphs you actually need and pass its URL to loadFont — DEFAULT_JAPANESE_FONT_URL is exported if you want to build a subset from the same source.
fontis not a font name.font: 'Arial'does not work — the option takes the font file bytes, because the glyph outlines are embedded into the PDF itself (that is what makes it render the same everywhere, with no font installed on the reader’s machine). Load the file first, as above. Passing a name throws:exportWorksheetPdf: `font` must be TrueType (glyf) font bytes, not a font-family name (got "Arial"). Either name a locale — …A locale in the wrong field is recognised and redirected:
font: 'zh-CN'says to pass it as{ locale: 'zh-CN' }.The font must be TrueType with
glyfoutlines. OpenType/CFF files (OTTO— many.otffiles) are not supported yet and are rejected with their own message.
ExportPdfOptions
| Option | Type | Default | Description |
|---|---|---|---|
locale | 'ja' | 'zh-CN' | 'zh-TW' | 'ko' or a registered tag | — | Font to embed, resolved through the registry. Preload it first. Required unless font is given. |
font | ArrayBuffer | Uint8Array | — | Embeddable TrueType (glyf) font bytes. Wins over locale. Required unless locale is given. |
fonts | { normal?, bold?, thin? } | — | v1.6.0 — extra weights to embed alongside font, as file bytes. A locale supplies these on its own. |
pageSize | 'A3'|'A4'|'A5'|'B4'|'B5'|'Letter'|'Legal'|'Tabloid'|'Executive' or { widthMm, heightMm } | 'A4' | Paper size. |
orientation | 'portrait' | 'landscape' | 'portrait' | Page orientation. |
marginMm | number | { top, right, bottom, left } | 12 | Margins in mm (number = all sides). |
range | PdfExportRange | everything that prints | Cell range to export. By default, A1 to the last cell that leaves a mark on paper — values, borders, non-white fills, merged cells and images (see Page Layout). |
showGridLines | boolean | the worksheet’s own setting | Draw light grid lines. Defaults to whatever worksheet.setShowGridLines() / the on-screen grid shows, so the PDF matches the screen unless you override it. |
scale | number | 1 | Uniform content scale (0.1–4). Ignored when fitToWidth. |
fitToWidth | boolean | false | Scale so all columns fit one page width. |
usePageBreaks | boolean | false | Paginate exactly like the on-screen page-break preview (see below). |
images | WorksheetImageInfo[] | sheet images | Cell-anchored images to draw. |
showImages | boolean | true | Draw cell-anchored images. |
title | string | — | Document title (PDF metadata). |
sheets | 'active' | 'all' | (number | string)[] | 'active' | v1.7.0 — instance exportPdf / saveAsPdf only. Which sheets go into the document (see below). |
saveAsPdf additionally accepts filename?: string. When omitted it defaults to the loaded document’s base name (see getDocumentName() / setDocumentName()), or worksheet.pdf when no file has been loaded.
Multi-page output with usePageBreaks
By default exportPdf lays the used range onto pages using the pageSize / orientation / marginMm / scale options. Set usePageBreaks: true to instead paginate using the worksheet’s own paging model — the PDF then breaks pages in exactly the same places as the Page Layout page-break preview, including manual breaks, the printable range, and the fit scale:
// The paper, orientation, margins, scale, and page bands all come from
// worksheet.getPrintSettings() + the computed breaks.
grid.worksheet.setPrintSettings({ paperSize: 'A4', orientation: 'landscape' })
grid.worksheet.insertRowPageBreak(40) // start a new page at row 40
await preloadPdfFont('ja')
grid.saveAsPdf({ locale: 'ja', usePageBreaks: true, filename: 'ledger.pdf' })
When usePageBreaks is on, the option-driven pageSize / orientation / marginMm / scale / fitToWidth / range fields are ignored — configure them through Page Layout instead. If the sheet has nothing to paginate, it falls back to the option-driven fields.
That includes fitToPages (v1.6.0): set it on the worksheet and the exported PDF uses the same derived scale the on-screen preview shows.
Several sheets in one PDF
Since v1.7.0 exportPdf() and saveAsPdf() take a sheets option — Excel’s “Print Entire Workbook”:
await preloadPdfFont('ja')
// Every visible sheet, in tab order
grid.saveAsPdf({ locale: 'ja', usePageBreaks: true, sheets: 'all', filename: 'workbook.pdf' })
// Chosen sheets, in the order given — by name or by index
const bytes = grid.exportPdf({ locale: 'ja', usePageBreaks: true, sheets: ['Quote', 2] })
sheets | What goes into the PDF |
|---|---|
'active' (default) | The sheet on screen — the same as before v1.7.0. |
'all' | Every visible sheet in tab order. Hidden sheets are left out, as in Excel. |
[…] | The listed sheets — indexes and/or names — in that order, each once. A hidden sheet is included when you name it. An unknown name or index throws, listing the workbook’s sheet names. |
- Each sheet keeps its own page setup. With
usePageBreaks, every sheet paginates on its own terms — paper size, orientation, margins, fit-to-pages and page breaks — so one document can mix A4 portrait and A3 landscape pages. Without it, the option-driven fields (andrange) apply to every sheet alike. - Page numbers run on across sheets. In a header or footer,
&Pcounts from the first page of the document and&Nis the document’s total, the way Excel numbers a whole-workbook print.&Astill names each page’s own sheet. A single-sheet export numbers exactly as before. - Empty sheets are skipped, as Excel skips them. A sheet counts when it has an explicit
range, a print area (underusePageBreaks), or anything that prints. If every sheet is empty, the first still gives its one blank page — a PDF needs at least one.
Without an instance
exportWorksheetsPdf(worksheets, options) is the headless counterpart: it renders the worksheets you pass, in that order, into one document with the same rules. exportWorksheetPdf(worksheet, options) is its one-sheet case.
import { exportWorksheetsPdf } from '@reogrid/pro'
const sheets = grid.workbook.getWorksheets()
const bytes = exportWorksheetsPdf([sheets[0], sheets[2]], { locale: 'ja', usePageBreaks: true })
Number format colours
A number format’s bracket colour — #,##0;[Red]"▲"#,##0 — is honoured in PDF output since v1.6.0. Before that, export read only the cell style’s text colour, so a negative that was red on screen printed black, quietly erasing the red figures on an accounting report. The order now matches the screen: format colour → cell style colour → default.
Rich-text cells carry a colour per run and are string cells, so a format colour never applies to them — same as on screen. Browser print (via HTML) is not covered and still uses the cell style colour.
React example: an Export PDF button
import { useEffect, useRef } from 'react'
import { Reogrid } from '@reogrid/pro/react'
import { preloadPdfFont } from '@reogrid/pro'
import type { ReogridInstance } from '@reogrid/pro/react'
function App() {
const gridRef = useRef<ReogridInstance>(null)
// Pay the font download once, on mount — not inside the click handler,
// where an `await` would cost the user gesture the download link needs.
useEffect(() => { void preloadPdfFont('ja') }, [])
function handleExport() {
gridRef.current?.saveAsPdf({ locale: 'ja', usePageBreaks: true, filename: 'report.pdf' })
}
return (
<>
<button onClick={handleExport}>Export PDF</button>
<Reogrid ref={gridRef} style={{ flex: 1 }} />
</>
)
}
Vue example: an Export PDF button
The Vue component publishes the grid as instance on its template ref (gridRef.value?.instance), so the export call is the same one as everywhere else:
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { Reogrid, type ReogridInstance } from '@reogrid/pro/vue'
import { preloadPdfFont } from '@reogrid/pro'
const gridRef = ref<{ instance: ReogridInstance | null } | null>(null)
// Pay the font download once, on mount — not inside the click handler,
// where an `await` would cost the user gesture the download link needs.
onMounted(() => { void preloadPdfFont('ja') })
function exportPdf() {
gridRef.value?.instance?.saveAsPdf({ locale: 'ja', usePageBreaks: true, filename: 'report.pdf' })
}
</script>
<template>
<button @click="exportPdf">Export PDF</button>
<Reogrid ref="gridRef" style="width: 100%; height: 400px" />
</template>
Related
- Page Layout — page size, margins, and the page-break preview that
usePageBreaksfollows. - Multi-Sheet Workbook — the sheets that
sheets: 'all'puts into one document. - Print — overview of all printing routes and how to choose.
- Print to HTML — quick print via the browser’s native dialog.
- Images — the cell-anchored images that appear in exported PDFs.
Sorry to hear that. What could be improved?