TypeScript · 0 Dependencies · ESM
Generate PDFs directly.
No Chromium, no detours.
PDFs direkt erzeugen.
Ohne Chromium, ohne Umwege.
直接生成 PDF。
无需 Chromium,不绕弯路。
fast-pdf writes PDF syntax itself — no HTML rendering, no headless browser, no native binaries. Runs unchanged on Node ≥ 18, Bun, Deno, in the browser and on edge runtimes. This page explains every feature with code that works exactly as shown.
fast-pdf schreibt PDF-Syntax selbst — kein HTML-Rendering, kein Headless-Browser, keine nativen Binaries. Läuft unverändert auf Node ≥ 18, Bun, Deno, im Browser und auf Edge-Runtimes. Diese Seite erklärt jedes Feature mit Code, der genau so funktioniert.
fast-pdf 直接书写 PDF 语法——没有 HTML 渲染、没有无头浏览器、没有原生二进制依赖。可在 Node ≥ 18、Bun、Deno、浏览器以及边缘运行时 中原样运行。本页用可直接运行的代码讲解每一个功能。
- 0runtime dependencies
- ~1.6 ms3-page document
- 245tests · 94 % coverage
- ~187 KBESM, tree-shakeable
- 0Runtime-Dependencies
- ~1,6 ms3-Seiten-Dokument
- 245Tests · 94 % Coverage
- ~187 KBESM, tree-shakeable
- 0运行时依赖
- ~1.6 ms3 页文档
- 245项测试 · 94 % 覆盖率
- ~187 KBESM,支持 Tree-Shaking
Compared to the usual approaches:
Im Vergleich zu den üblichen Ansätzen:
与常见方案对比:
| fast-pdf | Component-framework renderers | HTML → headless browser | |
|---|---|---|---|
| Runtime deps | 0 | 10+, incl. a UI framework as peer dependency | a ~300 MB browser binary |
| Runs on | Node, Bun, Deno, browser, edge | Node + browser; edge runtimes are tricky | servers that can run a browser |
| 3-page document | ~1.6 ms | element tree → flexbox layout → PDF lib | ~1000 ms+ (browser startup + render) |
| Tables | built-in: repeating headers, spans, JSON → table | hand-built from flexbox views | HTML/CSS |
| Signature form fields | ✓ AcroForm | — | ✗ printed output has no form fields |
| TOC, outlines, watermark | ✓ built-in | partly | ✗ |
| Bundle | ~187 KB ESM, tree-shakeable | several 100 KB + the framework | n/a |
| fast-pdf | Komponenten-Framework-Renderer | HTML → Headless-Browser | |
|---|---|---|---|
| Runtime-Dependencies | 0 | 10+, inkl. UI-Framework als Peer-Dependency | ~300-MB-Browser-Binary |
| Läuft auf | Node, Bun, Deno, Browser, Edge | Node + Browser; Edge-Runtimes schwierig | nur Servern mit Browser |
| 3-Seiten-Dokument | ~1,6 ms | Elementbaum → Flexbox-Layout → PDF-Lib | ~1000 ms+ (Browser-Start + Rendering) |
| Tabellen | eingebaut: wiederholende Header, Spans, JSON → Tabelle | selbst bauen aus Flexbox-Views | HTML/CSS |
| Signatur-Formularfelder | ✓ AcroForm | — | ✗ Druckausgabe hat keine Formularfelder |
| TOC, Lesezeichen, Wasserzeichen | ✓ eingebaut | teilweise | ✗ |
| Bundle | ~187 KB ESM, tree-shakeable | mehrere 100 KB + Framework | n/a |
| fast-pdf | 组件框架渲染方案 | HTML → 无头浏览器 | |
|---|---|---|---|
| 运行时依赖 | 0 | 10 余个,且需 UI 框架作为 peer 依赖 | 约 300 MB 的浏览器二进制 |
| 运行环境 | Node、Bun、Deno、浏览器、边缘运行时 | Node + 浏览器;边缘运行时困难 | 仅限能运行浏览器的服务器 |
| 3 页文档 | ~1.6 ms | 元素树 → Flexbox 布局 → PDF 库 | ~1000 ms+(浏览器启动 + 渲染) |
| 表格 | 内置:重复表头、跨行列、JSON → 表格 | 需用 Flexbox 视图手工搭建 | HTML/CSS |
| 签名表单字段 | ✓ AcroForm | — | ✗ 打印输出没有表单字段 |
| 目录、书签、水印 | ✓ 内置 | 部分支持 | ✗ |
| 包体积 | ~187 KB ESM,可 Tree-Shaking | 数百 KB + 框架本身 | n/a |
01Quick start
01Schnellstart
01快速开始
Create a PDFDocument, add content with calls like text(),
table() and image(), then save — that’s the whole workflow.
Content flows down the page like in a word processor, and a new page starts automatically
when space runs out. Positions are measured
from the top-left corner, in points (1 pt = 1/72″). The one-pager below is the
complete program — vector graphics, type, an image, a watermark and a table, no other
library involved:
Du erstellst ein PDFDocument, fügst Inhalte mit Aufrufen wie
text(), table() und image() hinzu und speicherst —
das ist der ganze Workflow. Inhalte laufen wie in einer Textverarbeitung von oben nach unten
über die Seite; ist kein Platz mehr, beginnt automatisch eine neue Seite. Positioniert wird
ab der linken oberen Ecke, in Punkt (1 pt = 1/72″). Der One-Pager unten ist das
komplette Programm — Vektorgrafik, Typo, Bild, Wasserzeichen und Tabelle, ohne weitere
Bibliothek:
创建一个 PDFDocument,用 text()、table()、image()
等调用添加内容,最后保存——整个流程就是这么简单。内容像在文字处理器中一样自上而下排布,
空间不够时会自动开始新的一页。定位从页面左上角算起,单位为点(1 pt = 1/72
英寸)。
下面这份单页文档就是完整程序——矢量图形、文字排版、图片、水印和表格,不依赖任何其他库:
npm install fast-pdf // Node ≥ 18
bun add fast-pdf // Bun
deno add npm:fast-pdf // Deno ≥ 2 — or import directly from "npm:fast-pdf"
npm install fast-pdf // Node ≥ 18
bun add fast-pdf // Bun
deno add npm:fast-pdf // Deno ≥ 2 — oder direkt aus "npm:fast-pdf" importieren
npm install fast-pdf // Node ≥ 18
bun add fast-pdf // Bun
deno add npm:fast-pdf // Deno ≥ 2 —— 也可直接从 "npm:fast-pdf" 导入
brief.ts — the complete program, 83 linesimport { readFile } from "node:fs/promises"; import { PDFDocument } from "fast-pdf"; const pdf = new PDFDocument({ format: "A4", margins: 64 });
import { readFile } from "node:fs/promises";
import { PDFDocument } from "fast-pdf";
const INK = "#101828", MUTED = "#667085", FAINT = "#e4e7ec", ACCENT = "#4f46e5";
const pdf = new PDFDocument({
format: "A4", // A3 | A4 | A5 | Letter | Legal | { width, height }
margins: 64,
font: "helvetica", // standard 14: helvetica | times | courier
metadata: { title: "Product Brief", author: "Example Software" },
});
const { width, height } = pdf.pageSize;
// Vector graphics — points, measured from the page's top-left corner. A page
// is transparent until painted, so lay down a white ground first.
pdf.rect(0, 0, width, height, { fill: "#ffffff" });
pdf.rect(0, 0, width, 5, { fill: ACCENT }); // full-bleed accent bar
pdf.watermark("DRAFT", { color: ACCENT, opacity: 0.05 }); // on every page
// Image, clipped to a circle — real vector clipping, no Canvas needed
pdf.image(await readFile("logo.png"), { x: width - 112, y: 62, width: 48, shape: "circle" });
// Title block: eyebrow label, display type, meta line, hairline
pdf.text("PRODUCT BRIEF", { x: 64, y: 66, size: 9.5, bold: true, color: ACCENT, letterSpacing: 3 });
pdf.text("Nimbus Workstation", { x: 62, y: 88, size: 33, bold: true, color: INK });
pdf.text("Q3 2026 · Example Software", { x: 64, y: 132, size: 10, color: MUTED });
pdf.line(64, 168, width - 64, 168, { color: FAINT, width: 0.5 });
// From here content flows down the page and breaks pages on its own
pdf.y = 196;
pdf.text("A workstation configured for build farms: sixteen cores, 64 GB of " +
"memory and a chassis that stays quiet under sustained load.",
{ width: 360, size: 11, color: "#334155", lineHeight: 1.6, spacingAfter: 34 });
// Three stats — an accent tick, a label, a number. No charting library.
pdf.grid([
["CORES", "16"], ["MEMORY", "64 GB"], ["WARRANTY", "36 mo"],
].map(([label, value]) => (d) => {
d.rect(d.x, d.y, 26, 2, { fill: ACCENT });
d.y += 16;
d.text(label, { size: 8.5, bold: true, color: MUTED, letterSpacing: 1.6 });
d.text(value, { size: 21, bold: true, color: INK });
}), { columns: 3, gap: 18 });
pdf.moveDown(1.5);
pdf.table([
["Item", "Qty", "Unit", "Amount"],
["Nimbus Workstation", "1", "999 €", "999 €"],
["Docking station", "2", "149 €", "298 €"],
["USB-C cable, 2 m", "3", "19 €", "57 €"],
], {
widths: [227, 50, 90, 100], aligns: ["left", "right", "right", "right"],
headerFill: "#f8f9fc", headerColor: MUTED, borderColor: FAINT, borderWidth: 0.5,
padding: 9,
});
pdf.table([["Total, net", "1.354 €"]], { // totals block: a borderless table
header: false, widths: [367, 100], aligns: ["right", "right"],
borderWidth: 0, fontSize: 12,
});
// A short list — bullets centred from real font metrics, not a magic constant
pdf.moveDown(1.4);
pdf.text("IN THE BOX", { size: 8.5, bold: true, color: MUTED, letterSpacing: 1.6, spacingAfter: 9 });
for (const item of ["Workstation, assembled and burned in for 48 h",
"Two docking stations, firmware pre-flashed",
"Three USB-C cables, 2 m, e-marked",
"On-site swap within one business day, 36 months"]) {
const m = pdf.fontMetrics({ size: 10 });
pdf.circle(pdf.x + 2, pdf.y + m.baseline - m.capHeight / 2, 1.8, { fill: ACCENT });
pdf.text(item, { x: 13, size: 10, color: "#334155", spacingAfter: 6 });
}
// Full-bleed call-to-action strip and a footer, both absolute
pdf.rect(0, height - 188, width, 68, { fill: INK });
pdf.text("Ready to order?", { x: 64, y: height - 168, size: 13.5, bold: true, color: "#ffffff" });
pdf.text("Reply to this brief by 30 Sep 2026 · sales@example-software.com",
{ x: 64, y: height - 146, size: 9.5, color: "#98a2b3" });
pdf.line(64, height - 84, width - 64, height - 84, { color: FAINT, width: 0.5 });
pdf.text("Prices excluding VAT · valid for 30 days · example-software.com",
{ x: 64, y: height - 74, width: width - 128, size: 8.5, color: MUTED, align: "center" });
await pdf.save("brief.pdf");
brief.ts — das komplette Programm, 83 Zeilenimport { readFile } from "node:fs/promises"; import { PDFDocument } from "fast-pdf"; const pdf = new PDFDocument({ format: "A4", margins: 64 });
import { readFile } from "node:fs/promises";
import { PDFDocument } from "fast-pdf";
const INK = "#101828", MUTED = "#667085", FAINT = "#e4e7ec", ACCENT = "#4f46e5";
const pdf = new PDFDocument({
format: "A4", // A3 | A4 | A5 | Letter | Legal | { width, height }
margins: 64,
font: "helvetica", // Standard-14: helvetica | times | courier
metadata: { title: "Product Brief", author: "Example Software" },
});
const { width, height } = pdf.pageSize;
// Vektorgrafik — Punkt, gemessen ab der linken oberen Ecke. Eine Seite ist
// transparent, solange sie niemand füllt: also zuerst der weiße Grund.
pdf.rect(0, 0, width, height, { fill: "#ffffff" });
pdf.rect(0, 0, width, 5, { fill: ACCENT }); // randlose Akzentleiste
pdf.watermark("DRAFT", { color: ACCENT, opacity: 0.05 }); // auf jeder Seite
// Bild, auf einen Kreis geclippt — echtes Vektor-Clipping, ohne Canvas
pdf.image(await readFile("logo.png"), { x: width - 112, y: 62, width: 48, shape: "circle" });
// Titelblock: Eyebrow-Label, Display-Typo, Meta-Zeile, Haarlinie
pdf.text("PRODUCT BRIEF", { x: 64, y: 66, size: 9.5, bold: true, color: ACCENT, letterSpacing: 3 });
pdf.text("Nimbus Workstation", { x: 62, y: 88, size: 33, bold: true, color: INK });
pdf.text("Q3 2026 · Example Software", { x: 64, y: 132, size: 10, color: MUTED });
pdf.line(64, 168, width - 64, 168, { color: FAINT, width: 0.5 });
// Ab hier fließt der Inhalt die Seite hinunter und bricht Seiten selbst um
pdf.y = 196;
pdf.text("A workstation configured for build farms: sixteen cores, 64 GB of " +
"memory and a chassis that stays quiet under sustained load.",
{ width: 360, size: 11, color: "#334155", lineHeight: 1.6, spacingAfter: 34 });
// Drei Kennzahlen — Akzentstrich, Label, Zahl. Ohne Chart-Bibliothek.
pdf.grid([
["CORES", "16"], ["MEMORY", "64 GB"], ["WARRANTY", "36 mo"],
].map(([label, value]) => (d) => {
d.rect(d.x, d.y, 26, 2, { fill: ACCENT });
d.y += 16;
d.text(label, { size: 8.5, bold: true, color: MUTED, letterSpacing: 1.6 });
d.text(value, { size: 21, bold: true, color: INK });
}), { columns: 3, gap: 18 });
pdf.moveDown(1.5);
pdf.table([
["Item", "Qty", "Unit", "Amount"],
["Nimbus Workstation", "1", "999 €", "999 €"],
["Docking station", "2", "149 €", "298 €"],
["USB-C cable, 2 m", "3", "19 €", "57 €"],
], {
widths: [227, 50, 90, 100], aligns: ["left", "right", "right", "right"],
headerFill: "#f8f9fc", headerColor: MUTED, borderColor: FAINT, borderWidth: 0.5,
padding: 9,
});
pdf.table([["Total, net", "1.354 €"]], { // Summenblock: rahmenlose Tabelle
header: false, widths: [367, 100], aligns: ["right", "right"],
borderWidth: 0, fontSize: 12,
});
// Kurze Liste — Bullets aus echten Font-Metriken zentriert, keine Konstante
pdf.moveDown(1.4);
pdf.text("IN THE BOX", { size: 8.5, bold: true, color: MUTED, letterSpacing: 1.6, spacingAfter: 9 });
for (const item of ["Workstation, assembled and burned in for 48 h",
"Two docking stations, firmware pre-flashed",
"Three USB-C cables, 2 m, e-marked",
"On-site swap within one business day, 36 months"]) {
const m = pdf.fontMetrics({ size: 10 });
pdf.circle(pdf.x + 2, pdf.y + m.baseline - m.capHeight / 2, 1.8, { fill: ACCENT });
pdf.text(item, { x: 13, size: 10, color: "#334155", spacingAfter: 6 });
}
// Randloser Call-to-Action-Streifen und eine Fußzeile, beide absolut
pdf.rect(0, height - 188, width, 68, { fill: INK });
pdf.text("Ready to order?", { x: 64, y: height - 168, size: 13.5, bold: true, color: "#ffffff" });
pdf.text("Reply to this brief by 30 Sep 2026 · sales@example-software.com",
{ x: 64, y: height - 146, size: 9.5, color: "#98a2b3" });
pdf.line(64, height - 84, width - 64, height - 84, { color: FAINT, width: 0.5 });
pdf.text("Prices excluding VAT · valid for 30 days · example-software.com",
{ x: 64, y: height - 74, width: width - 128, size: 8.5, color: MUTED, align: "center" });
await pdf.save("brief.pdf");
brief.ts — 完整程序,83 行import { readFile } from "node:fs/promises"; import { PDFDocument } from "fast-pdf"; const pdf = new PDFDocument({ format: "A4", margins: 64 });
import { readFile } from "node:fs/promises";
import { PDFDocument } from "fast-pdf";
const INK = "#101828", MUTED = "#667085", FAINT = "#e4e7ec", ACCENT = "#4f46e5";
const pdf = new PDFDocument({
format: "A4", // A3 | A4 | A5 | Letter | Legal | { width, height }
margins: 64,
font: "helvetica", // 标准 14 字体:helvetica | times | courier
metadata: { title: "Product Brief", author: "Example Software" },
});
const { width, height } = pdf.pageSize;
// 矢量图形——单位为点,从页面左上角算起。PDF 页面在被绘制之前是透明的,
// 所以先铺一层白色底。
pdf.rect(0, 0, width, height, { fill: "#ffffff" });
pdf.rect(0, 0, width, 5, { fill: ACCENT }); // 满版强调色横条
pdf.watermark("DRAFT", { color: ACCENT, opacity: 0.05 }); // 每一页都会绘制
// 图片裁切成圆形——真正的矢量裁切,无需 Canvas
pdf.image(await readFile("logo.png"), { x: width - 112, y: 62, width: 48, shape: "circle" });
// 标题区:小标签、大字标题、元信息行、细分隔线
pdf.text("PRODUCT BRIEF", { x: 64, y: 66, size: 9.5, bold: true, color: ACCENT, letterSpacing: 3 });
pdf.text("Nimbus Workstation", { x: 62, y: 88, size: 33, bold: true, color: INK });
pdf.text("Q3 2026 · Example Software", { x: 64, y: 132, size: 10, color: MUTED });
pdf.line(64, 168, width - 64, 168, { color: FAINT, width: 0.5 });
// 从这里开始内容自上而下排布,并自动分页
pdf.y = 196;
pdf.text("A workstation configured for build farms: sixteen cores, 64 GB of " +
"memory and a chassis that stays quiet under sustained load.",
{ width: 360, size: 11, color: "#334155", lineHeight: 1.6, spacingAfter: 34 });
// 三个指标——强调色短线、标签、数字。无需任何图表库。
pdf.grid([
["CORES", "16"], ["MEMORY", "64 GB"], ["WARRANTY", "36 mo"],
].map(([label, value]) => (d) => {
d.rect(d.x, d.y, 26, 2, { fill: ACCENT });
d.y += 16;
d.text(label, { size: 8.5, bold: true, color: MUTED, letterSpacing: 1.6 });
d.text(value, { size: 21, bold: true, color: INK });
}), { columns: 3, gap: 18 });
pdf.moveDown(1.5);
pdf.table([
["Item", "Qty", "Unit", "Amount"],
["Nimbus Workstation", "1", "999 €", "999 €"],
["Docking station", "2", "149 €", "298 €"],
["USB-C cable, 2 m", "3", "19 €", "57 €"],
], {
widths: [227, 50, 90, 100], aligns: ["left", "right", "right", "right"],
headerFill: "#f8f9fc", headerColor: MUTED, borderColor: FAINT, borderWidth: 0.5,
padding: 9,
});
pdf.table([["Total, net", "1.354 €"]], { // 合计区:一张无边框表格
header: false, widths: [367, 100], aligns: ["right", "right"],
borderWidth: 0, fontSize: 12,
});
// 短列表——圆点位置由真实字体度量算出,而不是写死的常数
pdf.moveDown(1.4);
pdf.text("IN THE BOX", { size: 8.5, bold: true, color: MUTED, letterSpacing: 1.6, spacingAfter: 9 });
for (const item of ["Workstation, assembled and burned in for 48 h",
"Two docking stations, firmware pre-flashed",
"Three USB-C cables, 2 m, e-marked",
"On-site swap within one business day, 36 months"]) {
const m = pdf.fontMetrics({ size: 10 });
pdf.circle(pdf.x + 2, pdf.y + m.baseline - m.capHeight / 2, 1.8, { fill: ACCENT });
pdf.text(item, { x: 13, size: 10, color: "#334155", spacingAfter: 6 });
}
// 满版行动号召条和页脚,均为绝对定位
pdf.rect(0, height - 188, width, 68, { fill: INK });
pdf.text("Ready to order?", { x: 64, y: height - 168, size: 13.5, bold: true, color: "#ffffff" });
pdf.text("Reply to this brief by 30 Sep 2026 · sales@example-software.com",
{ x: 64, y: height - 146, size: 9.5, color: "#98a2b3" });
pdf.line(64, height - 84, width - 64, height - 84, { color: FAINT, width: 0.5 });
pdf.text("Prices excluding VAT · valid for 30 days · example-software.com",
{ x: 64, y: height - 74, width: width - 128, size: 8.5, color: MUTED, align: "center" });
await pdf.save("brief.pdf");
brief.pdf, page 1, rendered with macOS
Quartz.
brief.pdf, Seite 1, gerendert mit macOS
Quartz.
brief.pdf 第 1 页,由 macOS Quartz 渲染。
02Output formats
02Ausgabeformate
02输出格式
The right output for every platform — the core stays identical everywhere:
Für jede Plattform die passende Ausgabe — der Kern bleibt überall identisch:
为每个平台提供合适的输出方式——核心在任何环境都完全一致:
const bytes = await pdf.render(); // Uint8Array — works everywhere
await pdf.save("invoice.pdf"); // Node/Bun/Deno: file · browser: download
const buffer = await pdf.toBuffer(); // Node Buffer (Uint8Array elsewhere)
const blob = await pdf.toBlob(); // Blob for FormData, object URLs, …
const stream = pdf.toStream(); // ReadableStream, 64 KiB chunks
// API route (Next.js, Hono, Workers, …):
return new Response(pdf.toStream(), {
headers: { "Content-Type": "application/pdf" },
});
const bytes = await pdf.render(); // Uint8Array — funktioniert überall
await pdf.save("rechnung.pdf"); // Node/Bun/Deno: Datei · Browser: Download
const buffer = await pdf.toBuffer(); // Node-Buffer (sonst Uint8Array)
const blob = await pdf.toBlob(); // Blob für FormData, Object-URLs, …
const stream = pdf.toStream(); // ReadableStream, 64-KiB-Chunks
// API-Route (Next.js, Hono, Workers, …):
return new Response(pdf.toStream(), {
headers: { "Content-Type": "application/pdf" },
});
const bytes = await pdf.render(); // Uint8Array —— 任何环境都可用
await pdf.save("invoice.pdf"); // Node/Bun/Deno:写文件 · 浏览器:下载
const buffer = await pdf.toBuffer(); // Node Buffer(其他环境为 Uint8Array)
const blob = await pdf.toBlob(); // Blob:用于 FormData、Object URL 等
const stream = pdf.toStream(); // ReadableStream,64 KiB 分块
// API 路由(Next.js、Hono、Workers 等):
return new Response(pdf.toStream(), {
headers: { "Content-Type": "application/pdf" },
});
03Pages & breaks
03Seiten & Umbrüche
03页面与分页
Text and tables break pages automatically. What most libraries are missing:
deciding yourself where the next page starts. That’s exactly what
pageBreak() is for — an explicit break at precisely the spot you choose,
optionally with a custom start position and per-page setup:
Text und Tabellen brechen Seiten automatisch. Was bei vielen Bibliotheken fehlt:
selbst bestimmen, wo die nächste Seite anfängt. Genau dafür gibt es
pageBreak() — expliziter Umbruch an exakt der Stelle, an der du ihn willst,
optional mit eigener Startposition und eigenem Seiten-Setup:
文本和表格会自动分页。而大多数库缺少的是:由你自己决定下一页从哪里开始。
pageBreak()
正是为此而生——在你指定的位置显式分页,还可以自定义起始位置和页面设置:
pdf.text("Chapter 1 …");
pdf.pageBreak(); // new page, cursor at the top margin
pdf.pageBreak({ y: 200 }); // new page, content starts at y = 200 pt
pdf.pageBreak({ landscape: true }); // new page in landscape
pdf.pageBreak({ format: "A5", margins: 30, y: 120 }); // all combinable
pdf.text("Kapitel 1 …");
pdf.pageBreak(); // neue Seite, Cursor am oberen Rand
pdf.pageBreak({ y: 200 }); // neue Seite, Inhalt startet bei y = 200 pt
pdf.pageBreak({ landscape: true }); // neue Seite im Querformat
pdf.pageBreak({ format: "A5", margins: 30, y: 120 }); // alles kombinierbar
pdf.text("第一章 …");
pdf.pageBreak(); // 新页面,光标位于上边距
pdf.pageBreak({ y: 200 }); // 新页面,内容从 y = 200 pt 开始
pdf.pageBreak({ landscape: true }); // 新页面,横向
pdf.pageBreak({ format: "A5", margins: 30, y: 120 }); // 可任意组合
There’s manual cursor control as well:
Daneben gibt es die manuelle Cursor-Steuerung:
此外还有手动光标控制:
pdf.addPage(); // like pageBreak() without options
pdf.moveDown(2); // move down two line heights
pdf.y = 300; // set the cursor directly
console.log(pdf.y); // … or read it
pdf.addPage(); // wie pageBreak() ohne Optionen
pdf.moveDown(2); // zwei Zeilenhöhen nach unten
pdf.y = 300; // Cursor direkt setzen
console.log(pdf.y); // … oder auslesen
pdf.addPage(); // 等同于不带参数的 pageBreak()
pdf.moveDown(2); // 向下移动两个行高
pdf.y = 300; // 直接设置光标
console.log(pdf.y); // …… 或读取光标位置
container(),
columns() and grid(), pageBreak() is deliberately
forbidden — those blocks guarantee their content stays on one page. The call throws a
FastPDFError there instead of tearing the layout apart.
container(),
columns() und grid() ist pageBreak() bewusst verboten
— diese Blöcke garantieren, dass ihr Inhalt zusammen auf einer Seite bleibt. Der Aufruf
wirft dort einen FastPDFError statt das Layout zu zerreißen.
container()、columns()
和 grid() 内部,pageBreak()
被有意禁用——这些块保证其内容保持在同一页上。 在其中调用会抛出
FastPDFError,而不是撕裂布局。
04Text & typography
04Text & Typografie
04文本与排版
Line wrapping against real font metrics, justified text, decorations and letter spacing:
Zeilenumbruch mit echten Font-Metriken, Blocksatz, Dekorationen und Laufweite:
基于真实字体度量的自动换行、两端对齐、文字装饰与字距:
pdf.text("Wrapped automatically — äöüß € “curly quotes” – dashes.", {
size: 12, bold: true,
color: "#334155", // "#rgb" | "#rrggbb" | { r, g, b }
align: "justify", // left | center | right | justify
underline: true, // also: strikethrough
letterSpacing: 0.5, // pt between characters
lineHeight: 1.4,
width: 300, // wrap width (default: content width)
spacingAfter: 8,
});
pdf.text("Header", { y: 20, align: "right" }); // absolute: no flow, no break
pdf.widthOfText("How wide am I?", { size: 12 }); // measure without drawing
pdf.text("Automatisch umbrochen — äöüß € „Anführungszeichen“ – Gedankenstriche.", {
size: 12, bold: true,
color: "#334155", // "#rgb" | "#rrggbb" | { r, g, b }
align: "justify", // left | center | right | justify
underline: true, // auch: strikethrough
letterSpacing: 0.5, // pt zwischen Zeichen
lineHeight: 1.4,
width: 300, // Umbruchbreite (Default: Inhaltsbreite)
spacingAfter: 8,
});
pdf.text("Kopfzeile", { y: 20, align: "right" }); // absolut: kein Flow, kein Umbruch
pdf.widthOfText("Wie breit bin ich?", { size: 12 }); // Messen ohne Zeichnen
pdf.text("自动换行 — äöüß € “弯引号” – 破折号。", {
size: 12, bold: true,
color: "#334155", // "#rgb" | "#rrggbb" | { r, g, b }
align: "justify", // left | center | right | justify
underline: true, // 还有 strikethrough(删除线)
letterSpacing: 0.5, // 字符间距(pt)
lineHeight: 1.4,
width: 300, // 换行宽度(默认:内容区宽度)
spacingAfter: 8,
});
pdf.text("页眉", { y: 20, align: "right" }); // 绝对定位:不参与排版流,不分页
pdf.widthOfText("我有多宽?", { size: 12 }); // 只测量,不绘制
Hyphenation
Silbentrennung
断词连字
Mark preferred break points with a soft hyphen (U+00AD). Words break only where needed — with a visible hyphen at the break:
Weiche Trennstellen markierst du mit dem Soft Hyphen (U+00AD). Gebrochen wird nur dort, wo es nötig ist — dann mit sichtbarem Trennstrich:
用软连字符(U+00AD)标记可断词的位置。只有在必要时才会断开——断开处会显示连字符:
pdf.text("Donaudampfschifffahrtsgesellschaft", { width: 100 });
// → "Donaudampf-" ⏎ "schifffahrts-" ⏎ "gesellschaft"
pdf.text("Donaudampfschifffahrtsgesellschaft", { width: 100 });
// → "Donaudampf-" ⏎ "schifffahrts-" ⏎ "gesellschaft"
pdf.text("Donaudampfschifffahrtsgesellschaft", { width: 100 });
// → "Donaudampf-" ⏎ "schifffahrts-" ⏎ "gesellschaft"
05Custom fonts
05Eigene Schriften
05自定义字体
TrueType fonts are embedded as Type0/Identity-H — with subsetting (only the glyphs you use end up in the PDF) and a ToUnicode CMap (copy & search keep working). That makes the font’s full Unicode range available:
TrueType-Fonts werden als Type0/Identity-H eingebettet — mit Subsetting (nur benutzte Glyphen landen im PDF) und ToUnicode-CMap (Kopieren & Suchen funktioniert). Damit steht der volle Unicode-Umfang der Schrift zur Verfügung:
TrueType 字体以 Type0/Identity-H 形式嵌入——支持子集化(只有用到的字形会进入 PDF) 和 ToUnicode CMap(复制和搜索照常可用)。这样即可使用字体的完整 Unicode 范围:
const inter = await fetch("/fonts/Inter.ttf").then(r => r.arrayBuffer());
pdf.registerFont(inter, { family: "inter" });
pdf.registerFont(interBold, { family: "inter", bold: true });
pdf.text("Full Unicode — Ελληνικά, кириллица, 中文", { font: "inter" });
pdf.text("Set in bold", { font: "inter", bold: true });
const inter = await fetch("/fonts/Inter.ttf").then(r => r.arrayBuffer());
pdf.registerFont(inter, { family: "inter" });
pdf.registerFont(interBold, { family: "inter", bold: true });
pdf.text("Volles Unicode — Ελληνικά, кириллица, 中文", { font: "inter" });
pdf.text("Fett gesetzt", { font: "inter", bold: true });
const inter = await fetch("/fonts/NotoSansSC.ttf").then(r => r.arrayBuffer());
pdf.registerFont(inter, { family: "noto" });
pdf.registerFont(interBold, { family: "noto", bold: true });
pdf.text("完整 Unicode — Ελληνικά, кириллица, 中文", { font: "noto" });
pdf.text("粗体文字", { font: "noto", bold: true });
06Layout engine
06Layout-Engine
06布局引擎
Boxes, columns and grids with padding, borders and relative widths — height grows with the content:
Boxen, Spalten und Raster mit Padding, Rahmen und relativen Breiten — die Höhe wächst mit dem Inhalt:
盒子、分栏和网格,支持内边距、边框和相对宽度——高度随内容增长:
pdf.container(
{
width: "80%", align: "center", // points or "%" of the available width
padding: 12, margin: { top: 8 },
background: "#eef4ff",
border: { color: "#4a7dff", width: 1 },
radius: 8, minHeight: 60,
},
(d) => d.text("A box that grows with its content."),
);
pdf.columns(
[(d) => d.text("Left column"), (d) => d.text("Right column")],
{ widths: ["35%", "65%"], gap: 16 },
);
pdf.grid(
cards.map((c) => (d) => d.text(c.title)),
{ columns: 3, gap: 10 }, // breaks between rows, never inside cells
);
pdf.container(
{
width: "80%", align: "center", // Punkt oder "%" der verfügbaren Breite
padding: 12, margin: { top: 8 },
background: "#eef4ff",
border: { color: "#4a7dff", width: 1 },
radius: 8, minHeight: 60,
},
(d) => d.text("Eine Box, die mit ihrem Inhalt wächst."),
);
pdf.columns(
[(d) => d.text("Linke Spalte"), (d) => d.text("Rechte Spalte")],
{ widths: ["35%", "65%"], gap: 16 },
);
pdf.grid(
karten.map((k) => (d) => d.text(k.titel)),
{ columns: 3, gap: 10 }, // Umbrüche zwischen Zeilen, nie in Zellen
);
pdf.container(
{
width: "80%", align: "center", // 点数或可用宽度的百分比
padding: 12, margin: { top: 8 },
background: "#eef4ff",
border: { color: "#4a7dff", width: 1 },
radius: 8, minHeight: 60,
},
(d) => d.text("随内容增长的盒子。"),
);
pdf.columns(
[(d) => d.text("左栏"), (d) => d.text("右栏")],
{ widths: ["35%", "65%"], gap: 16 },
);
pdf.grid(
cards.map((c) => (d) => d.text(c.title)),
{ columns: 3, gap: 10 }, // 只在行之间分页,绝不拆开单元格
);
After columns() the cursor sits below the tallest column. Containers and
columns nest freely.
Nach columns() steht der Cursor unter der höchsten Spalte. Container und
Spalten sind beliebig schachtelbar.
columns() 之后,光标位于最高一栏的下方。容器与分栏可以任意嵌套。
07Tables
07Tabellen
07表格
The header repeats on every page, the footer is drawn once at the end. Cells can span columns and rows — span groups never straddle page breaks:
Header wiederholt sich auf jeder Seite, der Footer wird einmal am Ende gezeichnet. Zellen können Spalten und Zeilen überspannen — Span-Gruppen brechen nie über Seitengrenzen:
表头在每页重复,表尾只在末尾绘制一次。单元格可以跨列和跨行——跨行组绝不会被分页拆开:
pdf.table(
[
[{ text: "Invoice Q3", colSpan: 3, align: "center" }],
["Item", "Qty", "Price"],
[{ text: "Consulting", rowSpan: 2 }, "8 h", "960.00 €"],
["4 h", "480.00 €"],
[{ text: "Total", colSpan: 2, bold: true }, "1,440.00 €"],
],
{
widths: [220, 90, 110],
aligns: ["left", "right", "right"],
header: true, // first row: repeats on every page
footer: true, // last row: styled, never repeated
zebraFill: "#f8fafc",
headerFill: "#0f172a", headerColor: "#ffffff",
padding: 6, borderWidth: 0.5,
},
);
pdf.table(
[
[{ text: "Rechnung Q3", colSpan: 3, align: "center" }],
["Position", "Menge", "Preis"],
[{ text: "Beratung", rowSpan: 2 }, "8 h", "960,00 €"],
["4 h", "480,00 €"],
[{ text: "Summe", colSpan: 2, bold: true }, "1.440,00 €"],
],
{
widths: [220, 90, 110],
aligns: ["left", "right", "right"],
header: true, // erste Zeile: wiederholt sich je Seite
footer: true, // letzte Zeile: gestylt, nie wiederholt
zebraFill: "#f8fafc",
headerFill: "#0f172a", headerColor: "#ffffff",
padding: 6, borderWidth: 0.5,
},
);
pdf.table(
[
[{ text: "第三季度账单", colSpan: 3, align: "center" }],
["项目", "数量", "金额"],
[{ text: "咨询服务", rowSpan: 2 }, "8 小时", "960.00 €"],
["4 小时", "480.00 €"],
[{ text: "合计", colSpan: 2, bold: true }, "1,440.00 €"],
],
{
widths: [220, 90, 110],
aligns: ["left", "right", "right"],
header: true, // 首行:每页重复
footer: true, // 末行:带样式,绝不重复
zebraFill: "#f8fafc",
headerFill: "#0f172a", headerColor: "#ffffff",
padding: 6, borderWidth: 0.5,
},
);
Style individual cells inline:
{ text, bold, italic, color, fill, align, colSpan, rowSpan }.
Einzelne Zellen stylst du inline:
{ text, bold, italic, color, fill, align, colSpan, rowSpan }.
单元格可以内联设置样式:
{ text, bold, italic, color, fill, align, colSpan, rowSpan }。
Straight from a REST/JSON response
Direkt aus einer REST-/JSON-Antwort
直接来自 REST/JSON 响应
Got an array of records from your API? objectTable() turns it into a table with
no manual row mapping — columns default to the keys of the first record, or you pick order,
headers, widths and formatting:
Ein Array von Objekten aus deiner API? objectTable() macht daraus eine Tabelle,
ganz ohne manuelles Row-Mapping — Spalten sind standardmäßig die Keys des ersten Objekts,
oder du bestimmst Reihenfolge, Überschriften, Breiten und Formatierung:
从 API 拿到一个对象数组?objectTable() 会直接把它变成表格,无需手动映射每一行——
列默认取第一条记录的键,你也可以指定顺序、表头、宽度和格式化:
const orders = await fetch("/api/orders").then((r) => r.json());
pdf.objectTable(orders); // columns = keys of the first record
pdf.objectTable(orders, { // …or take full control
columns: [
{ key: "id", header: "No.", align: "right", width: 60 },
{ key: "customer", header: "Customer" },
{ key: "total", header: "Amount", align: "right",
format: (v) => `${v} €` }, // format(value, record) — computed cells too
],
zebraFill: "#f8fafc", // every table() option works here as well
});
const orders = await fetch("/api/orders").then((r) => r.json());
pdf.objectTable(orders); // Spalten = Keys des ersten Objekts
pdf.objectTable(orders, { // …oder volle Kontrolle
columns: [
{ key: "id", header: "Nr.", align: "right", width: 60 },
{ key: "customer", header: "Kunde" },
{ key: "total", header: "Betrag", align: "right",
format: (v) => `${v} €` }, // format(value, record) — auch berechnete Zellen
],
zebraFill: "#f8fafc", // jede table()-Option funktioniert auch hier
});
const orders = await fetch("/api/orders").then((r) => r.json());
pdf.objectTable(orders); // 列 = 第一条记录的键
pdf.objectTable(orders, { // …… 或完全自定义
columns: [
{ key: "id", header: "编号", align: "right", width: 60 },
{ key: "customer", header: "客户" },
{ key: "total", header: "金额", align: "right",
format: (v) => `${v} €` }, // format(value, record) —— 也支持计算列
],
zebraFill: "#f8fafc", // 每个 table() 选项在这里同样可用
});
save() triggers a browser download; on the
server it writes a file (same call).
async function onClick() {
const rows = await fetch("/api/orders").then((r) => r.json());
const pdf = new PDFDocument();
pdf.objectTable(rows);
await pdf.save("orders.pdf"); // → browser download
}
save() löst im Browser
einen Download aus, auf dem Server schreibt es eine Datei (gleicher Aufruf).
async function onClick() {
const rows = await fetch("/api/orders").then((r) => r.json());
const pdf = new PDFDocument();
pdf.objectTable(rows);
await pdf.save("orders.pdf"); // → Browser-Download
}
save() 在浏览器中触发下载,在服务器上写入文件(同一个调用)。
async function onClick() {
const rows = await fetch("/api/orders").then((r) => r.json());
const pdf = new PDFDocument();
pdf.objectTable(rows);
await pdf.save("orders.pdf"); // → 浏览器下载
}
08HTML table → PDFnew
08HTML-Tabelle → PDFneu
08HTML 表格 → PDF新
A table is already on the page, and somebody wants it as a PDF. That usually ends in a
screenshot library, a headless browser on the server, or a second data pipeline that
rebuilds the same rows by hand. tableToPDF() takes the
<table> itself — structure and computed CSS — and renders real
PDF text: selectable, searchable, a few kilobytes, no server involved.
Eine Tabelle steht bereits auf der Seite, und jemand möchte sie als PDF. Üblicherweise endet
das in einer Screenshot-Bibliothek, einem Headless-Browser auf dem Server oder einer zweiten
Datenstrecke, die dieselben Zeilen noch einmal von Hand aufbaut.
tableToPDF() nimmt die <table> selbst — Struktur
und berechnetes CSS — und erzeugt echten PDF-Text: markierbar, durchsuchbar, wenige
Kilobyte, ohne Server.
表格已经在页面上,而有人想要一份
PDF。通常的结局是引入截图库、在服务器上跑无头浏览器,或者再搭一条数据管线把同样的行重新拼一遍。tableToPDF()
直接读取 <table> 本身——结构和计算后的 CSS——生成真正的 PDF
文本:可选中、可搜索、只有几 KB,且无需服务器。
Try it
Ausprobieren
试一试
The button below is not a picture. It runs fast-pdf in your browser, reads the table next to it and hands you the file:
Der Knopf darunter ist kein Bild. Er führt fast-pdf in deinem Browser aus, liest die Tabelle daneben und gibt dir die Datei:
下面的按钮不是图片。它在你的浏览器里运行 fast-pdf,读取旁边的表格并交给你文件:
| Position | Qty | Unit | Total |
|---|---|---|---|
| Coffee beans Arabica, 1 kg |
3 | 8.00 € | 24.00 € |
| Filter papers | 12 | 0.40 € | 4.80 € |
| Grinder service | 1 | 45.00 € | 45.00 € |
| Internal note — never leaves the screen | 0 | 0.00 € | 0.00 € |
| Total | 73.80 € | ||
The button is not in this page's markup — fast-pdf creates it and places it where the
switches above say. The row marked "internal note" is dropped via
skip: ".no-print".
Der Knopf steht nicht im Markup dieser Seite — fast-pdf erzeugt ihn und setzt ihn
dorthin, wo die Schalter oben es sagen. Die als interne Notiz markierte Zeile fällt über
skip: ".no-print" raus.
按钮并不在本页的标记里——它由 fast-pdf 创建,并放在上方开关所指定的位置。
标记为内部备注的那一行通过 skip: ".no-print" 被剔除。
In your framework
In deinem Framework
在你的框架里
import { useRef } from "react";
import { attachTablePdfButton, downloadTablePDF } from "fast-pdf";
export function RevenueTable({ rows }) {
const table = useRef(null);
return (
<>
<table ref={table} className="report">
<thead>
<tr><th>Position</th><th>Qty</th><th>Total</th></tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id}>
<td>{r.label}</td>
<td className="num">{r.qty}</td>
<td className="num">{r.total}</td>
</tr>
))}
</tbody>
</table>
<button
onClick={() =>
downloadTablePDF(table.current, {
title: "Revenue 2026",
header: "Acme GmbH",
footer: "Confidential",
pageNumbers: true,
filename: "revenue.pdf",
})
}
>
Download PDF
</button>
</>
);
}
<script setup>
import { ref } from "vue";
import { downloadTablePDF } from "fast-pdf";
const table = ref(null);
function download() {
return downloadTablePDF(table.value, {
title: "Revenue 2026",
header: "Acme GmbH",
footer: "Confidential",
pageNumbers: true,
filename: "revenue.pdf",
});
}
</script>
<template>
<table ref="table" class="report">
<thead>
<tr><th>Position</th><th>Qty</th><th>Total</th></tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td>{{ row.label }}</td>
<td class="num">{{ row.qty }}</td>
<td class="num">{{ row.total }}</td>
</tr>
</tbody>
</table>
<button @click="download">Download PDF</button>
</template>
<table id="revenue" class="report">
<thead>
<tr><th>Position</th><th>Qty</th><th>Total</th></tr>
</thead>
<tbody>
<tr><td>Coffee beans</td><td class="num">3</td><td class="num">24.00</td></tr>
</tbody>
</table>
<script type="module">
import { attachTablePdfButton } from "fast-pdf";
// Creates the button, puts it after the table and wires the click.
attachTablePdfButton("#revenue", {
label: "Download PDF",
filename: "revenue.pdf",
title: "Revenue 2026",
header: "Acme GmbH",
footer: "Confidential",
pageNumbers: true,
});
</script>
import { useRef } from "react";
import { attachTablePdfButton, downloadTablePDF } from "fast-pdf";
export function RevenueTable({ rows }) {
const table = useRef(null);
return (
<>
<table ref={table} className="report">
<thead>
<tr><th>Position</th><th>Qty</th><th>Total</th></tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id}>
<td>{r.label}</td>
<td className="num">{r.qty}</td>
<td className="num">{r.total}</td>
</tr>
))}
</tbody>
</table>
<button
onClick={() =>
downloadTablePDF(table.current, {
title: "Revenue 2026",
header: "Acme GmbH",
footer: "Confidential",
pageNumbers: true,
filename: "revenue.pdf",
})
}
>
Download PDF
</button>
</>
);
}
<script setup>
import { ref } from "vue";
import { downloadTablePDF } from "fast-pdf";
const table = ref(null);
function download() {
return downloadTablePDF(table.value, {
title: "Revenue 2026",
header: "Acme GmbH",
footer: "Confidential",
pageNumbers: true,
filename: "revenue.pdf",
});
}
</script>
<template>
<table ref="table" class="report">
<thead>
<tr><th>Position</th><th>Qty</th><th>Total</th></tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td>{{ row.label }}</td>
<td class="num">{{ row.qty }}</td>
<td class="num">{{ row.total }}</td>
</tr>
</tbody>
</table>
<button @click="download">Download PDF</button>
</template>
<table id="revenue" class="report">
<thead>
<tr><th>Position</th><th>Qty</th><th>Total</th></tr>
</thead>
<tbody>
<tr><td>Coffee beans</td><td class="num">3</td><td class="num">24.00</td></tr>
</tbody>
</table>
<script type="module">
import { attachTablePdfButton } from "fast-pdf";
// Creates the button, puts it after the table and wires the click.
attachTablePdfButton("#revenue", {
label: "Download PDF",
filename: "revenue.pdf",
title: "Revenue 2026",
header: "Acme GmbH",
footer: "Confidential",
pageNumbers: true,
});
</script>
import { useRef } from "react";
import { attachTablePdfButton, downloadTablePDF } from "fast-pdf";
export function RevenueTable({ rows }) {
const table = useRef(null);
return (
<>
<table ref={table} className="report">
<thead>
<tr><th>Position</th><th>Qty</th><th>Total</th></tr>
</thead>
<tbody>
{rows.map((r) => (
<tr key={r.id}>
<td>{r.label}</td>
<td className="num">{r.qty}</td>
<td className="num">{r.total}</td>
</tr>
))}
</tbody>
</table>
<button
onClick={() =>
downloadTablePDF(table.current, {
title: "Revenue 2026",
header: "Acme GmbH",
footer: "Confidential",
pageNumbers: true,
filename: "revenue.pdf",
})
}
>
Download PDF
</button>
</>
);
}
<script setup>
import { ref } from "vue";
import { downloadTablePDF } from "fast-pdf";
const table = ref(null);
function download() {
return downloadTablePDF(table.value, {
title: "Revenue 2026",
header: "Acme GmbH",
footer: "Confidential",
pageNumbers: true,
filename: "revenue.pdf",
});
}
</script>
<template>
<table ref="table" class="report">
<thead>
<tr><th>Position</th><th>Qty</th><th>Total</th></tr>
</thead>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td>{{ row.label }}</td>
<td class="num">{{ row.qty }}</td>
<td class="num">{{ row.total }}</td>
</tr>
</tbody>
</table>
<button @click="download">Download PDF</button>
</template>
<table id="revenue" class="report">
<thead>
<tr><th>Position</th><th>Qty</th><th>Total</th></tr>
</thead>
<tbody>
<tr><td>Coffee beans</td><td class="num">3</td><td class="num">24.00</td></tr>
</tbody>
</table>
<script type="module">
import { attachTablePdfButton } from "fast-pdf";
// Creates the button, puts it after the table and wires the click.
attachTablePdfButton("#revenue", {
label: "Download PDF",
filename: "revenue.pdf",
title: "Revenue 2026",
header: "Acme GmbH",
footer: "Confidential",
pageNumbers: true,
});
</script>
Header, footer and page numbers
Header, Footer und Seitenzahlen
页眉、页脚与页码
header and footer take a string or a draw callback and run on
every page; pageNumbers adds the count. A table that outgrows one page repeats
its <thead> at the top of the next one, and a wide table turns the paper
to landscape on its own — only when the columns genuinely do not fit, not because the layout
happens to be wide:
header und footer nehmen einen String oder eine Zeichenfunktion
und laufen auf jeder Seite; pageNumbers ergänzt die Zählung. Eine Tabelle, die
über eine Seite hinauswächst, wiederholt ihren <thead> oben auf der
nächsten, und eine breite Tabelle dreht das Blatt selbst ins Querformat — nur wenn die
Spalten wirklich nicht passen, nicht weil das Layout zufällig breit ist:
header 与 footer 接受字符串或绘制回调,并在每一页生效;pageNumbers
负责页码。超过一页的表格会在下一页顶部重复
<thead>;较宽的表格会自行转为横向——仅当列确实放不下时,而不是因为布局恰好很宽:
import { tableToPDF } from "fast-pdf";
const pdf = tableToPDF("#revenue", {
title: "Revenue 2026",
header: "Acme GmbH · Q1 report",
footer: (doc, page) => doc.text(`Printed ${new Date().toLocaleDateString()}`),
pageNumbers: { position: "bottom", align: "right" },
skip: ".no-print",
});
await pdf.save("revenue.pdf");
What comes from the CSS
Was aus dem CSS kommt
哪些内容来自 CSS
Cell backgrounds, text colors, per-side borders, font sizes, weights, italics, padding,
alignment and the column proportions all come from getComputedStyle() —
including modern color spaces like oklch() and
color(display-p3 …). Rows hidden by CSS stay out, and a selector (skip: ".no-print") drops whatever else does not belong in the file. The surface counts too: a row without
its own background is transparent in the browser and shows the card or page behind it, so
the PDF page is painted in that colour. Without it, a table from a dark interface would
arrive as light text on white paper. background takes a colour of your own, or
false to keep the paper white.
Zellhintergründe, Textfarben, Rahmen pro Seite, Schriftgrößen, -schnitte, Kursivstellung,
Padding, Ausrichtung und die Spaltenverhältnisse kommen alle aus
getComputedStyle() — auch moderne Farbräume wie oklch() und
color(display-p3 …). Per CSS versteckte Zeilen bleiben draußen, und ein
Selektor (skip: ".no-print") wirft alles Weitere raus, was nicht in die Datei
gehört. Auch der Untergrund zählt: Eine Zeile ohne eigenen Hintergrund ist im Browser
durchsichtig und zeigt die Karte oder Seite dahinter — deshalb wird die PDF-Seite in genau
dieser Farbe eingefärbt. Ohne das käme eine Tabelle aus einem dunklen Interface als heller
Text auf weißem Papier an. background nimmt eine eigene Farbe, oder
false für weißes Papier.
单元格背景、文字颜色、逐边框线、字号、字重、斜体、内边距、对齐方式以及列宽比例,全部来自
getComputedStyle()——包括 oklch() 和
color(display-p3 …) 这类现代色彩空间。被 CSS
隐藏的行不会进入文件,再用一个选择器(skip: ".no-print")剔除其余不该出现的内容。背景同样重要:没有自身背景色的行在浏览器中是透明的,
显示的是其后的卡片或页面,因此 PDF 页面会被填充为该颜色。否则,
来自深色界面的表格就会变成白纸上的浅色文字。background 可指定自定义颜色,false
则保持白色纸张。
One thing genuinely cannot travel: web font files. CSS names a font, it never hands
over its bytes, so a family the document does not carry is mapped onto the closest built-in
one. Register the real file with registerFont() and pass it as
table.font when the typeface has to match exactly.
Eines kann wirklich nicht mit: Webfont-Dateien. CSS nennt eine Schrift, liefert
aber nie ihre Bytes — eine Familie, die das Dokument nicht mitbringt, wird auf die
nächstliegende eingebaute abgebildet. Wenn die Schrift exakt stimmen muss, registriere die
Datei mit registerFont() und übergib sie als table.font.
确实有一样东西带不走:Web 字体文件。CSS
只给出字体名称,从不交出字节,因此文档未携带的字族会被映射到最接近的内置字体。若字体必须完全一致,请用
registerFont() 注册真实文件,并作为 table.font 传入。
The four entry points
Die vier Einstiege
四个入口
Three entry points, depending on how much you want to hand over:
Drei Einstiege, je nachdem wie viel du abgeben willst:
三个入口,取决于你想交出多少控制权:
| attachTablePdfButton(table, options) |
Creates the button, puts it next to the table, wires the click. Pass
button to use one you built yourself.
|
| downloadTablePDF(table, options) | Renders and hands the browser the file — for a button you wired yourself. |
| tableToPDF(table, options) |
Returns the PDFDocument. Keep drawing on it, append pages, encrypt or
sign it before saving.
|
| tableToRows(table, options) |
Just the rows plus matching table() options — to drop the table into a
document you are already building.
|
| attachTablePdfButton(table, options) |
Erzeugt den Knopf, setzt ihn neben die Tabelle, verdrahtet den Klick. Mit
button nimmt er einen, den du selbst gebaut hast.
|
| downloadTablePDF(table, options) | Rendert und gibt dem Browser die Datei — für einen Knopf, den du selbst verdrahtest. |
| tableToPDF(table, options) |
Gibt das PDFDocument zurück. Weiterzeichnen, Seiten anhängen,
verschlüsseln oder signieren, bevor du speicherst.
|
| tableToRows(table, options) |
Nur die Zeilen samt passender table()-Optionen — um die Tabelle in ein
Dokument zu übernehmen, das du ohnehin baust.
|
| attachTablePdfButton(table, options) | 创建按钮、放到表格旁边并绑定点击。传入 button 可使用你自己的按钮。 |
| downloadTablePDF(table, options) | 渲染并把文件交给浏览器——适合你自己接管按钮的场景。 |
| tableToPDF(table, options) | 返回 PDFDocument。保存前可继续绘制、追加页面、加密或签名。 |
| tableToRows(table, options) | 仅返回行与配套的 table() 选项——便于并入你正在构建的文档。 |
Options
Optionen
选项
Everything the two functions accept:
Alles, was die beiden Funktionen entgegennehmen:
两个函数可接受的全部选项:
| title | Heading above the table, and the PDF's title |
| header / footer | String or draw callback, repeated on every page |
| pageNumbers | true, or the usual page-number options |
| orientation |
"auto" (default), "portrait", "landscape"
|
| format / margins | Page format and margins — "A4" and 40 pt by default |
| skip | CSS selector for rows and cells to leave out |
| includeHidden | Take rows hidden by CSS along. Default: false |
| styles | false ignores the CSS and uses fast-pdf's own table style |
| background |
The page's surface: "auto" (default) takes the colour behind the table, a
colour forces one, false keeps the paper white
|
| table | Passed straight to table() — wins over everything read from CSS |
| filename / label | File name and button caption (button helper only) |
| mount / position |
Where the created button goes. position: one of the four corners
("top-left", "top-right", "bottom-left",
"bottom-right"), or "after"/"before" as a
sibling, or "append"/"prepend" inside.
mount points all of it at any element or selector
|
| icon / ariaLabel |
true for the built-in glyph or your own markup; without
label the button is icon-only and takes its name from
ariaLabel
|
| title | Überschrift über der Tabelle und Titel des PDFs |
| header / footer | String oder Zeichenfunktion, auf jeder Seite wiederholt |
| pageNumbers | true oder die gewohnten Seitenzahl-Optionen |
| orientation |
"auto" (Standard), "portrait", "landscape"
|
| format / margins | Seitenformat und Ränder — Standard "A4" und 40 pt |
| skip | CSS-Selektor für Zeilen und Zellen, die draußen bleiben |
| includeHidden | Per CSS versteckte Zeilen mitnehmen. Standard: false |
| styles | false ignoriert das CSS und nutzt fast-pdfs eigenen Tabellenstil |
| background |
Der Untergrund der Seite: "auto" (Standard) nimmt die Farbe hinter der
Tabelle, eine Farbe erzwingt sie, false lässt das Papier weiß
|
| table | Direkt an table() — gewinnt gegen alles aus dem CSS Gelesene |
| filename / label | Dateiname und Beschriftung des Knopfes (nur Button-Helfer) |
| mount / position |
Wohin der erzeugte Knopf kommt. position: eine der vier Ecken
("top-left", "top-right", "bottom-left",
"bottom-right"), oder "after"/"before" daneben,
oder "append"/"prepend" hinein. mount richtet
das alles auf ein beliebiges Element oder einen Selektor
|
| icon / ariaLabel |
true für das eingebaute Glyph oder eigenes Markup; ohne
label ist der Knopf reines Icon und bekommt seinen Namen aus
ariaLabel
|
| title | 表格上方的标题,同时作为 PDF 标题 |
| header / footer | 字符串或绘制回调,在每页重复 |
| pageNumbers | true,或常规的页码选项 |
| orientation | "auto"(默认)、"portrait"、"landscape" |
| format / margins | 页面格式与页边距——默认 "A4" 和 40 pt |
| skip | 用于排除行与单元格的 CSS 选择器 |
| includeHidden | 将被 CSS 隐藏的行一并纳入。默认:false |
| styles | false 忽略 CSS,改用 fast-pdf 自带的表格样式 |
| background |
页面底色:"auto"(默认)取表格背后的颜色,指定颜色则强制使用,
false 保持白色纸张
|
| table | 直接传给 table()——优先于从 CSS 读取的一切 |
| filename / label | 文件名与按钮文字(仅按钮辅助函数) |
| mount / position |
创建的按钮放在哪里。position:四个角之一("top-left"、
"top-right"、"bottom-left"、"bottom-right"),
或 "after"/"before" 置于其旁,或 "append"/"prepend"
置于其内。mount
可将上述位置指向任意元素或选择器
|
| icon / ariaLabel |
true 使用内置图标,或传入自定义标记;未提供
label 时按钮为纯图标,其无障碍名称取自 ariaLabel
|
09ImagesNew: GIF & WebP
09BilderNeu: GIF & WebP
09图片新增:GIF 与 WebP
pdf.image(bytes, { width: 200 }); // flows, keeps aspect ratio
pdf.image(logo, { x: 400, y: 30, width: 120 }); // absolute position
pdf.image(photo, { width: 200, height: 200, fit: "cover" }); // fill | contain | cover
pdf.image(photo, { width: 100, crop: { x: 50, y: 50, width: 400, height: 400 } });
pdf.image(stamp, { width: 80, rotate: -15, align: "center" });
pdf.image(bytes, { width: 200 }); // Flow, Seitenverhältnis bleibt
pdf.image(logo, { x: 400, y: 30, width: 120 }); // absolute Position
pdf.image(foto, { width: 200, height: 200, fit: "cover" }); // fill | contain | cover
pdf.image(foto, { width: 100, crop: { x: 50, y: 50, width: 400, height: 400 } });
pdf.image(stempel, { width: 80, rotate: -15, align: "center" });
pdf.image(bytes, { width: 200 }); // 排版流内,保持宽高比
pdf.image(logo, { x: 400, y: 30, width: 120 }); // 绝对定位
pdf.image(photo, { width: 200, height: 200, fit: "cover" }); // fill | contain | cover
pdf.image(photo, { width: 100, crop: { x: 50, y: 50, width: 400, height: 400 } });
pdf.image(stamp, { width: 80, rotate: -15, align: "center" });
| Format | Behavior |
|---|---|
| JPEG | Embedded as-is (DCTDecode), no re-encoding — grayscale, RGB, CMYK |
| PNG | Gray/RGB/indexed without re-encoding; the alpha channel becomes a real SMask (transparency) |
| GIF | First frame; palette decoded to RGB, a transparent index becomes an SMask (dependency-free LZW) |
| WebP | Lossless (VP8L) decoded to RGB + SMask — full transform/colour-cache/LZ77 set; lossy (VP8) is rejected |
| Cache | Identical image bytes are embedded once, no matter how many pages use them |
| Format | Verhalten |
|---|---|
| JPEG | 1:1 eingebettet (DCTDecode), kein Re-Encoding — Graustufen, RGB, CMYK |
| PNG | Grau/RGB/indiziert ohne Re-Encoding; Alphakanal wird zur echten SMask (Transparenz) |
| GIF | Erster Frame; Palette zu RGB dekodiert, transparenter Index wird zur SMask (dependency-freies LZW) |
| WebP | Verlustfrei (VP8L) zu RGB + SMask — voller Transform-/Color-Cache-/LZ77-Satz; verlustbehaftet (VP8) wird abgewiesen |
| Cache | Identische Bild-Bytes werden nur einmal eingebettet, egal auf wie vielen Seiten |
| 格式 | 行为 |
|---|---|
| JPEG | 原样嵌入(DCTDecode),不重新编码——支持灰度、RGB、CMYK |
| PNG | 灰度/RGB/索引色不重新编码;Alpha 通道转换为真正的 SMask(透明度) |
| GIF | 首帧;调色板解码为 RGB,透明索引转换为 SMask(无依赖 LZW) |
| WebP | 无损 (VP8L) 解码为 RGB + SMask——完整的变换/颜色缓存/LZ77;有损 (VP8) 会被拒绝 |
| 缓存 | 相同的图片字节只嵌入一次,无论多少页引用它 |
10Shapes
10Formen
10图形
pdf.line(50, 100, 545, 100, { color: "#e2e8f0", width: 0.5 });
pdf.rect(50, 120, 100, 40, { fill: "#3b82f6", radius: 8 }); // rounded
pdf.circle(100, 300, 40, { fill: "#ffd166", stroke: "#c79000" });
pdf.ellipse(300, 300, 80, 40, { stroke: "#0f172a", lineWidth: 2 });
pdf.line(50, 100, 545, 100, { color: "#e2e8f0", width: 0.5 });
pdf.rect(50, 120, 100, 40, { fill: "#3b82f6", radius: 8 }); // abgerundet
pdf.circle(100, 300, 40, { fill: "#ffd166", stroke: "#c79000" });
pdf.ellipse(300, 300, 80, 40, { stroke: "#0f172a", lineWidth: 2 });
pdf.line(50, 100, 545, 100, { color: "#e2e8f0", width: 0.5 });
pdf.rect(50, 120, 100, 40, { fill: "#3b82f6", radius: 8 }); // 圆角
pdf.circle(100, 300, 40, { fill: "#ffd166", stroke: "#c79000" });
pdf.ellipse(300, 300, 80, 40, { stroke: "#0f172a", lineWidth: 2 });
Circles and ellipses are exact Bézier approximations; fill and
stroke combine freely.
Kreise und Ellipsen sind exakte Bézier-Approximationen; fill und
stroke lassen sich frei kombinieren.
圆和椭圆采用精确的贝塞尔曲线近似;fill 与 stroke 可自由组合。
11SVG graphicsNew: svg()
11SVG-GrafikenNeu: svg()
11SVG 图形新增:svg()
svg() renders a practical SVG subset as native PDF vector graphics — no
rasterization, crisp at every zoom level. Ideal for logos and icons. Supported:
rect, circle, ellipse, line,
polyline, polygon, path, text and groups
(g) with fills, strokes, opacity and the full transform list
(translate, rotate, scale, skewX/Y,
matrix):
svg() rendert ein praxistaugliches SVG-Subset als
native PDF-Vektorgrafik — keine Rasterung, gestochen scharf bei jedem Zoom. Ideal für
Logos und Icons. Unterstützt: rect, circle, ellipse,
line, polyline, polygon, path,
text und Gruppen (g) mit Füllungen, Konturen, Opazität und der
vollen Transform-Liste (translate, rotate, scale,
skewX/Y, matrix):
svg() 将一个实用的 SVG 子集渲染为原生 PDF 矢量图形——
不做栅格化,任何缩放级别都清晰锐利。非常适合徽标和图标。支持:
rect、circle、ellipse、line、polyline、
polygon、path、text
以及带填充、描边、不透明度和完整变换列表
(translate、rotate、scale、skewX/Y、matrix)的组(g):
const logo = await fetch("/logo.svg").then((r) => r.text());
pdf.svg(logo, { width: 160, align: "center" }); // flows, keeps aspect ratio
pdf.svg(logo, { x: 430, y: 30, width: 110 }); // absolute: letterhead logo
pdf.svg(logo, { width: 200, height: 100, fit: "cover" }); // contain | fill | cover
// currentColor is substituted — one icon set, any theme color:
pdf.svg(icon, { width: 14, color: "#2e5ce6" });
const logo = await fetch("/logo.svg").then((r) => r.text());
pdf.svg(logo, { width: 160, align: "center" }); // Flow, Seitenverhältnis bleibt
pdf.svg(logo, { x: 430, y: 30, width: 110 }); // absolut: Briefkopf-Logo
pdf.svg(logo, { width: 200, height: 100, fit: "cover" }); // contain | fill | cover
// currentColor wird ersetzt — ein Icon-Set, jede Theme-Farbe:
pdf.svg(icon, { width: 14, color: "#2e5ce6" });
const logo = await fetch("/logo.svg").then((r) => r.text());
pdf.svg(logo, { width: 160, align: "center" }); // 排版流内,保持宽高比
pdf.svg(logo, { x: 430, y: 30, width: 110 }); // 绝对定位:信头徽标
pdf.svg(logo, { width: 200, height: 100, fit: "cover" }); // contain | fill | cover
// currentColor 会被替换——一套图标,任意主题色:
pdf.svg(icon, { width: 14, color: "#2e5ce6" });
Uint8Array; a zero-sized viewBox throws
INVALID_ARGUMENT.
Uint8Array; eine leere viewBox wirft INVALID_ARGUMENT.
Uint8Array;viewBox
尺寸为零会抛出 INVALID_ARGUMENT。
12MarkdownNew: markdown()
12MarkdownNeu: markdown()
12Markdown新增:markdown()
markdown() renders a CommonMark/GFM subset straight into the flow — headings,
paragraphs, nested ordered/unordered lists, pipe tables, fenced code blocks, blockquotes,
horizontal rules, plus inline bold, italic, code, links and
images. Perfect for turning LLM output, release notes or CMS content into a document without
writing layout code:
markdown() rendert ein CommonMark-/GFM-Subset direkt in den Flow —
Überschriften, Absätze, verschachtelte Listen, Pipe-Tabellen, Code-Blöcke, Blockquotes,
Trennlinien sowie inline fett, kursiv, code, Links und Bilder.
Perfekt, um LLM-Output, Release Notes oder CMS-Inhalte ohne Layout-Code in ein Dokument zu
verwandeln:
markdown() 将 CommonMark/GFM 子集直接渲染到排版流中——
标题、段落、嵌套的有序/无序列表、管道表格、围栏代码块、引用块、分割线,
以及行内加粗、斜体、code、链接和图片。 无需编写布局代码,即可把
LLM 输出、发布说明或 CMS 内容变成文档:
pdf.markdown(`
# Quarterly Report
Revenue grew **12 %** — details in the [appendix](#appendix).
| Region | Revenue |
| ------ | ------: |
| EMEA | 4.2 M€ |
| APAC | 3.1 M€ |
- automatic page breaks
- nested lists
1. work too
`);
pdf.markdown(`
# Quartalsbericht
Der Umsatz wuchs um **12 %** — Details im [Anhang](#anhang).
| Region | Umsatz |
| ------ | -----: |
| EMEA | 4,2 M€ |
| APAC | 3,1 M€ |
- automatische Seitenumbrüche
- verschachtelte Listen
1. gehen auch
`);
pdf.markdown(`
# 季度报告
营收增长 **12 %**——详情见[附录](#appendix)。
| 区域 | 营收 |
| ---- | -----: |
| EMEA | 4.2 M€ |
| APAC | 3.1 M€ |
- 自动分页
- 嵌套列表
1. 同样支持
`);
Tables go through table() — repeating headers included. Since the core does no
I/O, you resolve image references yourself:
Tabellen laufen über table() — inklusive wiederholender Header. Weil der Kern
kein I/O macht, löst du Bild-Referenzen selbst auf:
表格由 table() 渲染——包括每页重复的表头。由于核心不做任何 I/O,
图片引用由你自己解析:
pdf.markdown(readme, {
resolveImage: (src) => imageBytes.get(src), //  → raw bytes
});
// without a resolver, images render as their alt text
pdf.markdown(readme, {
resolveImage: (src) => imageBytes.get(src), //  → rohe Bytes
});
// ohne Resolver werden Bilder als Alt-Text gerendert
pdf.markdown(readme, {
resolveImage: (src) => imageBytes.get(src), //  → 原始字节
});
// 不提供 resolver 时,图片渲染为其 alt 文本
pdf.markdown(completion), done. Headings scale from your base font
size, code blocks are set in Courier on a tinted background, and everything breaks pages
like regular content.
pdf.markdown(completion), fertig. Überschriften skalieren aus
deiner Basis-Schriftgröße, Code-Blöcke stehen in Courier auf getöntem Hintergrund, und alles
bricht Seiten wie regulärer Inhalt.
pdf.markdown(completion),就这么简单。标题按基础字号缩放,代码块用 Courier
配浅色背景排版,所有内容都像常规内容一样自动分页。
13Document features
13Dokumentfeatures
13文档功能
Recurring elements are drawn onto every page at render time — so they know the final page number and count:
Wiederkehrende Elemente werden zur Renderzeit auf jede Seite gezeichnet — sie kennen also die endgültige Seitenzahl und -anzahl:
重复元素在渲染阶段绘制到每一页上——因此它们知道最终的页码和总页数:
pdf.header("Annual Report 2026", { align: "right" });
pdf.footer("© 2026 Example Software");
pdf.pageNumbers({ format: (n, t) => `Page ${n} of ${t}`, startAt: 2 });
pdf.watermark("DRAFT", { opacity: 0.1 }); // diagonal, transparent
pdf.header("Geschäftsbericht 2026", { align: "right" });
pdf.footer("© 2026 Example Software");
pdf.pageNumbers({ format: (n, t) => `Seite ${n} von ${t}`, startAt: 2 });
pdf.watermark("ENTWURF", { opacity: 0.1 }); // diagonal, transparent
pdf.header("2026 年度报告", { align: "right" });
pdf.footer("© 2026 Example Software");
pdf.pageNumbers({ format: (n, t) => `第 ${n} 页,共 ${t} 页`, startAt: 2 });
pdf.watermark("草稿", { opacity: 0.1 }); // 对角线方向,半透明
Bookmarks, links & table of contents
Lesezeichen, Links & Inhaltsverzeichnis
书签、链接与目录
pdf.outline("Chapter 1"); // PDF bookmark (viewer sidebar)
pdf.outline("Section 1.1", { level: 1 }); // nested
pdf.anchor("details"); // named jump target
pdf.text("Read more …", { link: "#details" }); // internal link
pdf.text("Website", { link: "https://example.com" }); // external link
pdf.toc({ title: "Contents" }); // call last: builds linked TOC pages
// and inserts them at the front —
// page numbers shift correctly
pdf.outline("Kapitel 1"); // PDF-Lesezeichen (Sidebar im Viewer)
pdf.outline("Abschnitt 1.1", { level: 1 }); // verschachtelt
pdf.anchor("details"); // benanntes Sprungziel
pdf.text("Mehr dazu …", { link: "#details" }); // interner Link
pdf.text("Website", { link: "https://example.com" }); // externer Link
pdf.toc({ title: "Inhalt" }); // zum Schluss aufrufen: baut verlinkte
// TOC-Seiten und fügt sie vorne ein —
// Seitenzahlen werden korrekt verschoben
pdf.outline("第一章"); // PDF 书签(阅读器侧边栏)
pdf.outline("1.1 节", { level: 1 }); // 支持嵌套
pdf.anchor("details"); // 命名跳转目标
pdf.text("查看详情 …", { link: "#details" }); // 内部链接
pdf.text("官网", { link: "https://example.com" }); // 外部链接
pdf.toc({ title: "目录" }); // 最后调用:生成带链接的目录页
// 并插入到文档最前面——
// 页码会自动正确偏移
Clickable buttons0.7.0
Anklickbare Buttons0.7.0
可点击按钮0.7.0
A link does not have to be underlined text. button() draws a filled, optionally
bordered box with an optically centred label and puts the link annotation on top. It flows
like text and breaks the page when it no longer fits; passing y places it
absolutely. Without width the box sizes itself to its label:
Ein Link muss kein unterstrichener Text sein. button() zeichnet einen
gefüllten, optional gerahmten Kasten mit optisch zentrierter Beschriftung und legt die
Link-Annotation darüber. Er fließt wie Text und bricht die Seite um, wenn er nicht mehr
passt; mit y wird er absolut gesetzt. Ohne width passt sich der
Kasten seiner Beschriftung an:
链接不一定要是带下划线的文字。button() 会绘制一个带填充、可选边框的方框,
标签在视觉上居中,并在其上方放置链接注释。它像文本一样参与排版流,放不下时自动分页; 传入
y 则改为绝对定位。不指定 width 时,方框会自动适配标签宽度:
pdf.button("Open the demo", {
link: "https://kevinci.github.io/fast-pdf/", // URL or "#anchor"
fill: "#4f46e5", // button colour
borderColor: "#3730a3", // borderWidth defaults to 1 once set
color: "#ffffff", // label colour
width: 200, // points or "60%" — omit to fit the label
radius: 6,
align: "center", // placement in the flow area
});
pdf.button("Ghost", { // outline style: light box, dark label
link: "#details", fill: "#ffffff", borderColor: "#e4e7ec", color: "#101828",
});
pdf.button("Sidebar CTA", { link: "…", x: 40, y: 700, width: 120 }); // absolute
pdf.link(50, 50, 200, 20, "#details"); // bare clickable area, draws nothing
pdf.button("Zur Demo", {
link: "https://kevinci.github.io/fast-pdf/", // URL oder "#anker"
fill: "#4f46e5", // Buttonfarbe
borderColor: "#3730a3", // borderWidth ist dann automatisch 1
color: "#ffffff", // Textfarbe
width: 200, // Punkte oder "60%" — ohne Angabe: passt sich dem Text an
radius: 6,
align: "center", // Platzierung im Flussbereich
});
pdf.button("Ghost", { // Umriss-Variante: heller Kasten, dunkle Schrift
link: "#details", fill: "#ffffff", borderColor: "#e4e7ec", color: "#101828",
});
pdf.button("Sidebar CTA", { link: "…", x: 40, y: 700, width: 120 }); // absolut
pdf.link(50, 50, 200, 20, "#details"); // reine Klickfläche, zeichnet nichts
pdf.button("打开演示", {
link: "https://kevinci.github.io/fast-pdf/", // URL 或 "#锚点"
fill: "#4f46e5", // 按钮底色
borderColor: "#3730a3", // 设置后 borderWidth 默认为 1
color: "#ffffff", // 标签颜色
width: 200, // 点值或 "60%" —— 省略则自动适配标签
radius: 6,
align: "center", // 在排版区域中的位置
});
pdf.button("Ghost", { // 描边样式:浅色方框,深色标签
link: "#details", fill: "#ffffff", borderColor: "#e4e7ec", color: "#101828",
});
pdf.button("Sidebar CTA", { link: "…", x: 40, y: 700, width: 120 }); // 绝对定位
pdf.link(50, 50, 200, 20, "#details"); // 纯点击区域,不绘制任何内容
/Btn widget. It needs no form support,
renders in every viewer and can do nothing but follow its target. A label wider than the box
is truncated with an ellipsis instead of spilling out, and unsafe schemes
(javascript:, vbscript:, data:, file:)
are rejected with UNSAFE_LINK — the same check link() and
text({ link }) use.
/Btn). Sie braucht keine
Formular-Unterstützung, wird in jedem Viewer dargestellt und kann nichts anderes als ihr
Ziel öffnen. Eine zu breite Beschriftung wird gekürzt statt überzulaufen, und gefährliche
Schemata (javascript:, vbscript:, data:,
file:) werden mit UNSAFE_LINK abgewiesen — dieselbe Prüfung wie
bei link() und text({ link }).
/Btn 控件。它无需阅读器支持表单,在任何阅读器中都能显示,
并且只能打开自己的目标。标签超出方框时会以省略号截断而不会溢出;不安全的协议
(javascript:、vbscript:、data:、file:)会以
UNSAFE_LINK 拒绝——与 link() 和
text({ link }) 使用同一套检查。
Availability: button() ships with
version 0.7.0 at the end of August 2026. link(),
anchor() and text({ link }) work today in 0.7.0.
Verfügbarkeit: button() kommt mit
Version 0.7.0 Ende August 2026. link(), anchor() und
text({ link }) funktionieren schon heute in 0.7.0.
可用性: button() 将随 0.7.0 版本于 2026 年
8 月底发布。 link()、anchor() 和 text({ link }) 在
0.7.0 中已经可用。
Signature fields
Signaturfelder
签名字段
signature() places an empty AcroForm /Sig field — the clickable area
recipients use to digitally sign the document in their PDF viewer (e.g. Adobe Acrobat) and
send it back. A signature line and an optional label are drawn automatically; in flow mode
the field moves the cursor and breaks pages like any other content:
signature() platziert ein leeres AcroForm-/Sig-Feld — die klickbare
Fläche, über die Empfänger das Dokument im PDF-Viewer (z. B. Adobe Acrobat) digital
signieren und zurückschicken. Signaturlinie und optionales Label werden automatisch
gezeichnet; im Flow-Modus bewegt das Feld den Cursor und bricht Seiten wie jeder andere
Inhalt:
signature() 会放置一个空的 AcroForm /Sig 字段——接收方在 PDF 阅读器(如
Adobe Acrobat)中点击该区域即可对文档进行数字签名并回传。签名线和可选的标签会自动绘制;
在排版流模式下,该字段会像其他内容一样移动光标并自动分页:
pdf.signature({ label: "Client · place, date" }); // flow mode, auto-named "Signature1"
// Absolute positioning + custom names: two fields side by side
pdf.signature({ name: "client", x: 60, y: 600, width: 210, height: 56, label: "Client" });
pdf.signature({ name: "contractor", x: 325, y: 600, width: 210, height: 56, label: "Contractor" });
pdf.signature({ name: "initials-p1", width: 86, height: 32, line: false }); // initials, no line
pdf.signature({ label: "Auftraggeber · Ort, Datum" }); // Flow-Modus, Auto-Name "Signature1"
// Absolute Positionierung + eigene Namen: zwei Felder nebeneinander
pdf.signature({ name: "client", x: 60, y: 600, width: 210, height: 56, label: "Auftraggeber" });
pdf.signature({ name: "contractor", x: 325, y: 600, width: 210, height: 56, label: "Auftragnehmer" });
pdf.signature({ name: "initials-p1", width: 86, height: 32, line: false }); // Paraphe ohne Linie
pdf.signature({ label: "甲方 · 地点、日期" }); // 排版流模式,自动命名 "Signature1"
// 绝对定位 + 自定义名称:两个字段并排
pdf.signature({ name: "client", x: 60, y: 600, width: 210, height: 56, label: "甲方" });
pdf.signature({ name: "contractor", x: 325, y: 600, width: 210, height: 56, label: "乙方" });
pdf.signature({ name: "initials-p1", width: 86, height: 32, line: false }); // 缩写签名,无签名线
Signature1, Signature2, … if omitted). fast-pdf
creates the fields to be signed by the recipient — it does not cryptographically
sign the document itself. A complete, ready-to-sign contract lives in
examples/signature.ts (npx tsx examples/signature.ts, then open
the PDF in Acrobat and click a field).
Signature1, Signature2, …). fast-pdf
erzeugt die vom Empfänger zu signierenden Felder — das Dokument selbst wird nicht
kryptografisch signiert. Ein kompletter, unterschriftsreifer Vertrag liegt in
examples/signature.ts (npx tsx examples/signature.ts, dann das PDF
in Acrobat öffnen und ein Feld anklicken).
Signature1、Signature2……)。fast-pdf 创建的是
供接收方签署的字段——它不会对文档本身进行加密签名。
一份完整的、可直接签署的合同示例见 examples/signature.ts (运行
npx tsx examples/signature.ts,然后在 Acrobat 中打开 PDF 并点击签名字段)。
14Appending PDFsNew: append()
14PDFs anhängenNeu: append()
14追加 PDF新增:append()
append() attaches the pages of an existing PDF to the document you
just generated — a certificate, a reference letter, a scan somebody uploaded. The classic
case: a CV builder that lets applicants add their references to the résumé it produces.
Until now that was the one job that needed a second library:
append() hängt die Seiten eines bestehenden PDFs an das gerade
erzeugte Dokument — ein Zeugnis, ein Empfehlungsschreiben, einen hochgeladenen Scan. Der
klassische Fall: ein CV-Builder, in dem Bewerber ihre Nachweise an den generierten
Lebenslauf anhängen. Bisher war das die eine Aufgabe, für die eine zweite Library nötig war:
append() 把一个已有 PDF 的页面追加到你刚生成的文档中——
一份证书、一封推荐信、用户上传的扫描件。典型场景:简历生成器让申请人把证明材料
附在生成的简历后面。在此之前,这是唯一需要引入第二个库才能完成的任务:
const pdf = new PDFDocument();
pdf.text("Curriculum Vitae", { size: 24 });
await pdf.append(certificateBytes); // original size, 1:1
await pdf.append(letterBytes, { fit: "page" }); // scaled onto A4
await pdf.append(scanBytes, { pages: [1, 3] }); // a selection
await pdf.save("application.pdf");
const pdf = new PDFDocument();
pdf.text("Lebenslauf", { size: 24 });
await pdf.append(zeugnisBytes); // Originalgröße, 1:1
await pdf.append(schreibenBytes, { fit: "page" }); // auf A4 eingepasst
await pdf.append(scanBytes, { pages: [1, 3] }); // Seitenauswahl
await pdf.save("bewerbung.pdf");
const pdf = new PDFDocument();
pdf.text("个人简历", { size: 24 });
await pdf.append(certificateBytes); // 原始尺寸,1:1
await pdf.append(letterBytes, { fit: "page" }); // 缩放到 A4
await pdf.append(scanBytes, { pages: [1, 3] }); // 选取部分页面
await pdf.save("application.pdf");
Pages are copied, not re-rendered: content streams, fonts and images move over
byte-for-byte with their filters intact. An appended page therefore looks exactly like the
original and stays as small as it was, and objects shared by several pages of one file are
written once. append() is async — the only page-producing call
that is, because the source has to be parsed before its page count is known.
Seiten werden kopiert, nicht neu gerendert: Content-Streams, Schriften und Bilder
wandern byteweise samt Filtern hinüber. Eine angehängte Seite sieht deshalb exakt aus wie
das Original und bleibt so klein wie sie war; Objekte, die sich mehrere Seiten einer Datei
teilen, werden nur einmal geschrieben. append() ist async — der
einzige seitenerzeugende Aufruf, der das ist, weil die Quelle erst geparst werden muss.
页面是复制而非重新渲染:内容流、字体和图片连同其过滤器逐字节搬运过来。
因此追加的页面与原件完全一致,体积也保持原样;同一文件中被多个页面共用的对象只写入一次。
append() 是 async 的——它是唯一如此的产生页面的调用,
因为必须先解析来源才能知道其页数。
| Option | Meaning |
|---|---|
| pages |
1-based page numbers to take, in the order given (2 or
[3, 1]). Default: all
|
| fit |
"keep" (default) keeps the original page size; "page" scales
onto this document's format
|
| overlay |
Allow header()/footer()/pageNumbers()/watermark()
and your own drawing on the appended pages. Default: off for "keep",
always on for "page"
|
| padding | fit: "page" only — inset from the page edge, in points |
| autoRotate |
fit: "page" only — give the target page the source's orientation.
Default: true
|
| Option | Bedeutung |
|---|---|
| pages |
1-basierte Seitennummern in der angegebenen Reihenfolge (2 oder
[3, 1]). Default: alle
|
| fit |
"keep" (Default) behält die Originalgröße; "page" skaliert
auf das Format dieses Dokuments
|
| overlay |
Erlaubt header()/footer()/pageNumbers()/watermark()
und eigenes Zeichnen auf den angehängten Seiten. Default: aus bei "keep",
immer an bei "page"
|
| padding | Nur bei fit: "page" — Abstand zum Seitenrand in Punkt |
| autoRotate |
Nur bei fit: "page" — Zielseite bekommt die Ausrichtung der Quelle.
Default: true
|
| 选项 | 含义 |
|---|---|
| pages |
要取的页码(从 1 开始),按给定顺序(2 或
[3, 1])。默认:全部
|
| fit |
"keep"(默认)保留原始页面尺寸;"page"
缩放到本文档的页面格式
|
| overlay |
允许 header()/footer()/pageNumbers()/watermark()
以及你自己的绘制作用于追加的页面。默认:"keep" 时关闭,"page"
时始终开启
|
| padding | 仅 fit: "page" — 距页边的内缩量,单位为点 |
| autoRotate | 仅 fit: "page" — 目标页采用来源页的方向。默认:true |
Check an upload before you take it
Upload prüfen, bevor du ihn übernimmst
在接收上传前先检查
pdfInfo() reads page count, page sizes and whether a file is encrypted —
without importing anything. Exactly what an upload form needs to reject a 500-page file or
tell the user their PDF is password-protected:
pdfInfo() liest Seitenzahl, Seitengrößen und ob eine Datei verschlüsselt ist —
ohne irgendetwas zu importieren. Genau das, was ein Upload-Formular braucht, um eine
500-Seiten-Datei abzulehnen oder dem Nutzer zu sagen, dass sein PDF passwortgeschützt ist:
pdfInfo() 读取页数、页面尺寸以及文件是否加密——不导入任何内容。
这正是上传表单需要的:拒收 500 页的文件,或告知用户其 PDF 受密码保护:
import { pdfInfo } from "fast-pdf";
const info = await pdfInfo(uploadBytes);
// { version: "1.7", pageCount: 3, encrypted: false, pageSizes: [ … ] }
if (info.encrypted) return "Please remove the password protection first.";
if (info.pageCount > 20) return "At most 20 pages, please.";
await pdf.append(uploadBytes);
import { pdfInfo } from "fast-pdf";
const info = await pdfInfo(uploadBytes);
// { version: "1.7", pageCount: 3, encrypted: false, pageSizes: [ … ] }
if (info.encrypted) return "Bitte zuerst den Passwortschutz entfernen.";
if (info.pageCount > 20) return "Bitte höchstens 20 Seiten.";
await pdf.append(uploadBytes);
import { pdfInfo } from "fast-pdf";
const info = await pdfInfo(uploadBytes);
// { version: "1.7", pageCount: 3, encrypted: false, pageSizes: [ … ] }
if (info.encrypted) return "请先移除密码保护。";
if (info.pageCount > 20) return "最多 20 页。";
await pdf.append(uploadBytes);
Drawing on appended pages
Auf angehängte Seiten zeichnen
在追加的页面上绘制
By default an appended page is left alone — no page number gets stamped over a document
somebody else signed. With overlay: true the imported content stays underneath
and everything you draw lands on top, upright even on a page that was scanned sideways.
Without it, the flow cursor continues on a fresh page, so the next
text() never ends up on the attachment:
Standardmäßig bleibt eine angehängte Seite unberührt — es wird keine Seitenzahl über ein
Dokument gestempelt, das jemand anderes unterschrieben hat. Mit
overlay: true bleibt der importierte Inhalt darunter und alles, was du
zeichnest, landet darüber — aufrecht auch auf einer quer eingescannten Seite. Ohne die
Option läuft der Fluss-Cursor auf einer neuen Seite weiter, damit das nächste
text() nicht auf der Anlage landet:
默认情况下追加的页面不会被改动——不会在别人签署过的文档上盖页码。 使用
overlay: true 时,导入的内容留在下层,你绘制的一切叠加在上层——
即使页面是横向扫描的,叠加内容也保持正向。不使用该选项时,排版光标会在新的
一页继续,因此下一个 text() 绝不会落到附件上:
pdf.pageNumbers({ format: (n, total) => `Page ${n} of ${total}` });
await pdf.append(letterBytes, { overlay: true }); // numbered too
pdf.text("Attachment 1", { y: 30, size: 9 }); // stamped on top
await pdf.append(contractBytes); // untouched
pdf.text("Cover note"); // → on a new page
pdf.pageNumbers({ format: (n, total) => `Seite ${n} von ${total}` });
await pdf.append(schreibenBytes, { overlay: true }); // wird mitnummeriert
pdf.text("Anlage 1", { y: 30, size: 9 }); // liegt oben drauf
await pdf.append(vertragBytes); // bleibt unberührt
pdf.text("Anschreiben"); // → auf neuer Seite
pdf.pageNumbers({ format: (n, total) => `第 ${n} / ${total} 页` });
await pdf.append(letterBytes, { overlay: true }); // 一并编号
pdf.text("附件 1", { y: 30, size: 9 }); // 叠加在上层
await pdf.append(contractBytes); // 保持不变
pdf.text("附信"); // → 落在新的一页
Drawing on an appended page without overlay is a typed error rather
than silently dropped content. Reading covers classic cross-reference tables,
cross-reference streams and object streams (PDF 1.5+), page rotation and crop boxes; a file
whose cross-reference table is damaged or stale is recovered by scanning it. Encrypted
sources are rejected with ENCRYPTED_PDF.
Auf eine angehängte Seite zu zeichnen ohne overlay ist ein typisierter
Fehler statt still verworfener Inhalt. Gelesen werden klassische xref-Tabellen, xref-Streams
und Objekt-Streams (PDF 1.5+), Seitendrehung und CropBox; eine Datei mit kaputter oder
veralteter xref-Tabelle wird durch Scannen gerettet. Verschlüsselte Quellen werden mit
ENCRYPTED_PDF abgelehnt.
在未启用 overlay 的追加页面上绘制会抛出带错误码的异常,
而不是静默丢弃内容。读取支持传统 xref 表、xref 流与对象流(PDF 1.5+)、页面旋转和裁剪框;
xref 表损坏或过期的文件会通过扫描文件来恢复。加密的来源文件会以
ENCRYPTED_PDF 被拒绝。
/JavaScript action cannot
travel out of an upload and into your output. Try it yourself: the repository ships a
browser test page at docs/append-playground.html — run
npm run build, then python3 -m http.server 8080 and open it; drop
PDFs in and watch the result. The runnable Node example is
examples/append.ts (npm run append).
/JavaScript-Aktion kommt nicht aus dem Upload in deine Ausgabe. Selbst
ausprobieren: im Repository liegt eine Browser-Testseite unter
docs/append-playground.html — mit npm run build bauen, dann
python3 -m http.server 8080 starten und öffnen; PDFs hineinziehen und das
Ergebnis ansehen. Das lauffähige Node-Beispiel ist examples/append.ts (npm run append).
/JavaScript 动作无法从上传文件进入你的输出。自己试试:
仓库内附带一个浏览器测试页 docs/append-playground.html——先执行
npm run build,然后运行 python3 -m http.server 8080 并打开它; 把
PDF 拖进去即可查看结果。可运行的 Node 示例为 examples/append.ts (npm run append)。
15Error handling
15Fehlerbehandlung
15错误处理
All user-facing failures throw FastPDFError with a stable, machine-readable
code — messages may be reworded, codes never change:
Alle nutzerseitigen Fehler werfen FastPDFError mit einem stabilen,
maschinenlesbaren code — Meldungstexte können sich ändern, Codes nicht:
所有面向使用者的错误都会抛出带有稳定、机器可读 code 的
FastPDFError——错误文案可能调整,错误码永不改变:
import { FastPDFError } from "fast-pdf";
try {
pdf.text("Hello", { font: "doesNotExist" });
} catch (e) {
if (e instanceof FastPDFError && e.code === "UNKNOWN_FONT") {
// "UNKNOWN_FONT" | "INVALID_COLOR" | "UNSUPPORTED_IMAGE"
// "UNKNOWN_PAGE_FORMAT" | "INVALID_ARGUMENT" | …
}
}
import { FastPDFError } from "fast-pdf";
try {
pdf.text("Hallo", { font: "gibtEsNicht" });
} catch (e) {
if (e instanceof FastPDFError && e.code === "UNKNOWN_FONT") {
// "UNKNOWN_FONT" | "INVALID_COLOR" | "UNSUPPORTED_IMAGE"
// "UNKNOWN_PAGE_FORMAT" | "INVALID_ARGUMENT" | …
}
}
import { FastPDFError } from "fast-pdf";
try {
pdf.text("你好", { font: "不存在的字体" });
} catch (e) {
if (e instanceof FastPDFError && e.code === "UNKNOWN_FONT") {
// "UNKNOWN_FONT" | "INVALID_COLOR" | "UNSUPPORTED_IMAGE"
// "UNKNOWN_PAGE_FORMAT" | "INVALID_ARGUMENT" | …
}
}
16SecurityNew: encryption & signatures
16SicherheitNeu: Verschlüsselung & Signaturen
16安全新增:加密与签名
Security is a first-class design goal, not an afterthought. The long-term aim is a build that regulated industries — banks and insurers — can rely on. That bar isn’t fully cleared yet, so here is the honest split: what is hardened today, and what is still on the roadmap.
Sicherheit ist von Anfang an ein zentrales Entwurfsziel, kein nachträglicher Aufsatz. Das Fernziel ist eine Version, der regulierte Branchen — Banken und Versicherungen — vertrauen können. Diese Messlatte ist noch nicht vollständig erreicht, deshalb hier die ehrliche Aufteilung: was heute gehärtet ist und was noch auf der Roadmap steht.
安全是从一开始就确立的核心设计目标,而非事后补丁。长期目标是打造一个受监管行业——银行与保险公司—— 可以信赖的版本。这个标准目前尚未完全达到,因此在此如实说明:当前已加固的部分,以及仍在路线图上的部分。
Hardened today
Heute schon gehärtet
当前已加固
| Area | Protection |
|---|---|
| String & name injection |
Every literal and name is byte-escaped (escapeString, name
#XX) — content can’t break out of the object stream
|
| Link schemes |
javascript:, vbscript:, data: and
file: targets are rejected → UNSAFE_LINK
|
| Null & control bytes | Escaped octally, never written raw |
| Numeric input |
NaN, ±Infinity and out-of-range magnitudes are rejected at
the call site → INVALID_NUMBER
|
| Memory exhaustion |
Decompression is size-bounded (zlib-bomb guard); oversized images are refused →
IMAGE_TOO_LARGE
|
| Failure surface |
Every user-facing error is a typed FastPDFError with a stable
code
|
| Document encryption |
AES-256 (R6 / AESV3) with user/owner passwords and granular permissions via
encrypt — strings and streams are both encrypted
|
| Reproducible output |
deterministic mode emits byte-identical PDFs (no wall-clock timestamp)
with a content-derived file /ID
|
| Digital signatures |
Detached PAdES-B (CAdES) signing via signature({ sign }) — RSA + SHA-256,
embedded certificate, verifiable in Acrobat and with openssl
|
| Bereich | Schutz |
|---|---|
| String- & Name-Injection |
Jedes Literal und jeder Name wird byteweise escaped (escapeString, Name
#XX) — Inhalte können nicht aus dem Objektstrom ausbrechen
|
| Link-Schemata |
javascript:, vbscript:, data: und
file: werden abgewiesen → UNSAFE_LINK
|
| Null- & Steuerbytes | Oktal escaped, nie roh geschrieben |
| Numerische Eingaben |
NaN, ±Infinity und zu große Beträge werden am Aufrufort
abgewiesen → INVALID_NUMBER
|
| Speichererschöpfung |
Dekompression ist größenbegrenzt (zlib-Bomben-Schutz); zu große Bilder werden
abgelehnt → IMAGE_TOO_LARGE
|
| Fehleroberfläche |
Jeder nutzerseitige Fehler ist ein typisierter FastPDFError mit stabilem
code
|
| Dokumentverschlüsselung |
AES-256 (R6 / AESV3) mit User-/Owner-Passwörtern und granularen Berechtigungen über
encrypt — Strings und Streams werden verschlüsselt
|
| Reproduzierbare Ausgabe |
deterministic erzeugt byte-identische PDFs (kein Wall-Clock-Zeitstempel)
mit inhaltsabgeleitetem /ID
|
| Digitale Signaturen |
Detached PAdES-B (CAdES) über signature({ sign }) — RSA + SHA-256,
eingebettetes Zertifikat, in Acrobat und mit openssl prüfbar
|
| 领域 | 防护 |
|---|---|
| 字符串与名称注入 |
每个字面量和名称都按字节转义(escapeString、名称
#XX)——内容无法逃逸出对象流
|
| 链接协议 |
拒绝 javascript:、vbscript:、data: 和
file: 目标 → UNSAFE_LINK
|
| 空字节与控制字节 | 以八进制转义,绝不原样写入 |
| 数值输入 |
在调用处即拒绝 NaN、±Infinity 及超范围数值 →
INVALID_NUMBER
|
| 内存耗尽 | 解压受大小限制(zlib 炸弹防护);过大图片被拒绝 → IMAGE_TOO_LARGE |
| 错误界面 | 每个面向使用者的错误都是带稳定 code 的 FastPDFError |
| 文档加密 |
AES-256(R6 / AESV3),通过
encrypt 提供用户/所有者密码与细粒度权限——字符串与流均加密
|
| 可复现输出 |
deterministic 模式生成字节一致的
PDF(无墙钟时间戳),并带有内容派生的文件 /ID
|
| 数字签名 |
通过 signature({ sign }) 进行分离式 PAdES-B(CAdES)签名——RSA +
SHA-256,内嵌证书,可在 Acrobat 与 openssl 中验证
|
On the roadmap
Auf der Roadmap
路线图上
17Performance
17Performance
17性能
Direct PDF synthesis instead of a browser detour. Compression uses the native
CompressionStream API (zlib) — zero bundle bytes for it. Measured with
npm run bench (Apple Silicon, Node 22):
Direkte PDF-Synthese statt Browser-Umweg. Kompression läuft über die native
CompressionStream-API (zlib) — null Bundle-Bytes dafür. Gemessen mit
npm run bench (Apple Silicon, Node 22):
直接合成 PDF,无需绕道浏览器。压缩使用原生
CompressionStream API(zlib)——不占任何打包体积。 以下数据由
npm run bench 测得(Apple Silicon,Node 22):
| Document | Time | Throughput | Size |
|---|---|---|---|
| Text, ~3 pages | 1.6 ms | 621 docs/s | 1.9 KB |
| Text, ~30 pages | 12.6 ms | 79 docs/s | 13.0 KB |
| Table, 500 rows (~17 p.) | 26.4 ms | 38 docs/s | 55.4 KB |
| Table, 5000 rows (~170 p.) | 232 ms | 4 docs/s | 549 KB |
| Mixed (text/tables/vector) | 0.9 ms | 1071 docs/s | 2.8 KB |
| Dokument | Zeit | Durchsatz | Größe |
|---|---|---|---|
| Text, ~3 Seiten | 1,6 ms | 621 Docs/s | 1,9 KB |
| Text, ~30 Seiten | 12,6 ms | 79 Docs/s | 13,0 KB |
| Tabelle, 500 Zeilen (~17 S.) | 26,4 ms | 38 Docs/s | 55,4 KB |
| Tabelle, 5000 Zeilen (~170 S.) | 232 ms | 4 Docs/s | 549 KB |
| Gemischt (Text/Tabellen/Vektor) | 0,9 ms | 1071 Docs/s | 2,8 KB |
| 文档 | 耗时 | 吞吐 | 大小 |
|---|---|---|---|
| 文本,约 3 页 | 1.6 ms | 621 份/秒 | 1.9 KB |
| 文本,约 30 页 | 12.6 ms | 79 份/秒 | 13.0 KB |
| 表格,500 行(约 17 页) | 26.4 ms | 38 份/秒 | 55.4 KB |
| 表格,5000 行(约 170 页) | 232 ms | 4 份/秒 | 549 KB |
| 混合(文本/表格/矢量) | 0.9 ms | 1071 份/秒 | 2.8 KB |
18See it rendered
18So sieht’s aus
18效果展示
A design-forward report from examples/report.ts (npx tsx examples/report.ts), rendered with macOS Quartz — full-bleed color, a vector bar chart and big type, all from
the same primitives shown above. Proof that "fast" doesn't mean "plain":
Ein designstarker Report aus examples/report.ts (npx tsx examples/report.ts), gerendert mit macOS Quartz — vollflächige Farbe, ein Vektor-Balkendiagramm und große
Typo, alles aus denselben Primitiven von oben. Der Beweis, dass „schnell" nicht „schlicht"
heißt:
一份注重设计的报告,来自 examples/report.ts(npx tsx examples/report.ts), 由 macOS Quartz
渲染——满版配色、矢量柱状图和大字号,全部使用上文展示的同一批基础绘图能力。
证明「快」并不等于「简陋」:
rect() and circle().
rect() und circle().
rect() 和
circle() 绘制的装饰圆环。
rect()s and a
progress ring, no charting library.
rect()s und ein
Fortschrittsring, ganz ohne Chart-Bibliothek.
rect() 构成的柱状图和进度环,无需任何图表库。
19Templates & AI skill
19Templates & AI-Skill
19模板与 AI 技能
The fastest way to a good-looking document is copying a finished one. Five complete,
designed templates ship with the npm package (node_modules/fast-pdf/examples/)
— copy one into your project and change the import from "../src/index" to
"fast-pdf":
Der schnellste Weg zu einem gut aussehenden Dokument ist, ein fertiges zu kopieren. Fünf
komplette, designte Templates liegen dem npm-Package bei
(node_modules/fast-pdf/examples/) — eins ins Projekt kopieren und den Import
von "../src/index" auf "fast-pdf" ändern:
做出一份好看文档最快的方式,是复制一份现成的。npm 包内附带五个完整的精心设计的模板
(node_modules/fast-pdf/examples/)——复制一个到你的项目,并把导入从
"../src/index" 改为 "fast-pdf":
| Template | What you get |
|---|---|
| invoice.ts | Invoice with letterhead, item table, totals block, footer |
| report.ts | Design-forward report: full-bleed cover, KPI cards, vector bar chart |
| signature.ts | Contract with clause sections and clickable AcroForm signature fields |
| showcase.ts | Feature tour: TOC, outlines, watermark, cell spans, columns, links |
| basic.ts | Minimal text + table starting point |
| Template | Was du bekommst |
|---|---|
| invoice.ts | Rechnung mit Briefkopf, Positionstabelle, Summenblock, Footer |
| report.ts | Designstarker Report: vollflächiges Cover, KPI-Karten, Vektor-Balkendiagramm |
| signature.ts | Vertrag mit Paragraphen und klickbaren AcroForm-Signaturfeldern |
| showcase.ts | Feature-Tour: TOC, Lesezeichen, Wasserzeichen, Zell-Spans, Spalten, Links |
| basic.ts | Minimaler Startpunkt mit Text + Tabelle |
| 模板 | 包含内容 |
|---|---|
| invoice.ts | 发票:信头、明细表、合计区、页脚 |
| report.ts | 注重设计的报告:满版封面、KPI 卡片、矢量柱状图 |
| signature.ts | 合同:条款章节与可点击的 AcroForm 签名字段 |
| showcase.ts | 功能巡览:目录、书签、水印、单元格合并、分栏、链接 |
| basic.ts | 最小起点:文本 + 表格 |
Working with Claude Code or another coding agent? fast-pdf ships a design skill — curated palettes, layout recipes (letterhead, totals block, signature area), typography rules and a render-preview-iterate loop. One command installs it into your project:
Du arbeitest mit Claude Code oder einem anderen Coding-Agenten? fast-pdf bringt eine Design-Skill mit — kuratierte Paletten, Layout-Rezepte (Briefkopf, Summenblock, Signaturbereich), Typografie-Regeln und eine Render-Preview-Iterationsschleife. Ein Befehl installiert sie ins Projekt:
在使用 Claude Code 或其他编码智能体?fast-pdf 附带一个设计技能——精选调色板、 版式配方(信头、合计区、签名区)、排版规则,以及“渲染—预览—迭代”工作流。一条命令即可安装到你的项目:
npm install fast-pdf
npx fast-pdf-skill // copies the skill to ./.claude/skills/fast-pdf-designer/
npm install fast-pdf
npx fast-pdf-skill // kopiert die Skill nach ./.claude/skills/fast-pdf-designer/
npm install fast-pdf
npx fast-pdf-skill // 将技能复制到 ./.claude/skills/fast-pdf-designer/
20Changelog
Release notes, newest first — kept in English so they match CHANGELOG.md in the
repository one to one. The format follows
Keep a Changelog, versioning follows
SemVer.
0.8.02026-09-23An HTML table on the page becomes a PDF — structure and computed CSS — plus CSS colors everywhere and per-cell styling in the table engine.
Added
-
tableToPDF(table, options) — a table that is already on the page becomes a PDF.
The output is real PDF text, selectable and searchable and a few kilobytes in size,
and it looks like what the reader is looking at: cell backgrounds, text colors,
per-side borders, font sizes, weights, italics, padding, alignment and the column
proportions are read from
getComputedStyle()rather than guessed.<thead>repeats on every page,<tfoot>is drawn once,colspan/rowspancarry over, CSS-hidden rows stay out,skipdrops whatever else does not belong in the file, and a table whose columns genuinely do not fit turns the page to landscape by itself. -
attachTablePdfButton(), downloadTablePDF() and tableToRows() —
the button next to the table, the one-liner for a button you built yourself, and the
rows plus matching
table()options for a document you are already building.header,footerandpageNumbersapply to all of them. -
CSS colors everywhere. Every
color,fillandborderColoroption now understands what a browser hands back: the 148 named colors,rgb()/rgba()old and new,hsl(),hwb(),oklab(),oklch(),lab(),lch()andcolor(srgb …)/color(display-p3 …), converted to sRGB and clipped to gamut. -
Per-cell styling in table().
TableCellgainedfontSize,padding(one value or separate{ x, y }insets) andborders— per side, each with its own width and color, a side left out simply not painted.
Changed
-
parseColor()accepts more than it did: strings that previously threwINVALID_COLOR—"red","rgb(0 0 0)"— are now valid colors. Anything that is genuinely not a color still throws.
0.7.12026-08-05Clarity fix around appended annotations, plus a technical report and a dependency-free audit tool for verifying the filter.
Changed
-
A
/Linkleft without a destination by the annotation filter is now dropped instead of copied as an inert rectangle. Its behaviour had sat in/AA(mouse-enter JavaScript), which is never copied — so the action was already gone in 0.7.0 and clicking did nothing. But the rectangle survived, and viewers still show a hand cursor over it, which reads as “the filter did not work”. Purely a clarity fix: no security-relevant behaviour changed between 0.7.0 and 0.7.1.
Added
-
docs/APPEND-SECURITY.md — technical report on what
append()reads, copies and discards, why it is a whitelist rather than a blocklist, and how to verify it independently. Covers the three observations that regularly look like a broken filter and are not: visible page text spelling out payload names (page content is copied verbatim by design), a deliberately harmless control link, and an in-document/GoTojump that is retargeted rather than removed. - scripts/audit-pdf.mjs — lists the security-relevant structures of any PDF. Dependency-free and independent of fast-pdf, so it works as a second opinion. Strips stream payloads before scanning, so visible page text cannot produce false hits.
0.7.02026-08-05Appending the pages of an existing PDF, and clickable buttons: a filled, optionally bordered box with a centred label.
Added
-
append(pdfBytes, options)— attach the pages of an existing PDF. The most common reason to run fast-pdf next to a second library: a CV builder whose applicants upload a reference letter or a certificate and want it attached to the generated résumé. Pages are copied, not re-rendered — content streams, fonts and images move over byte-for-byte with their filters intact, so an appended page looks exactly like the original, stays as small as it was, and no filter beyond/FlateDecodehas to be understood. Objects shared by several pages of one file are written once.fit: "keep"(default) copies the page dictionary, so the page keeps its size, rotation and annotations;fit: "page"scales it onto this document's format (padding,autoRotate).overlayopens imported pages toheader(),footer(),pageNumbers(),watermark()and direct drawing — via a form XObject whose/Matrixundoes the source page's rotation and box offset, so a stamp lands upright even on a page scanned sideways. Withoutoverlaythe flow continues on a fresh page, and drawing on an appended page is a typed error rather than silently dropped content. Reading covers classic cross-reference tables, cross-reference streams and object streams (PDF 1.5+) with PNG/TIFF predictors, inherited page attributes,/Rotateand/CropBox; a damaged or stale cross-reference table is recovered by scanning the file. Encrypted sources are rejected withENCRYPTED_PDF; source form fields, bookmarks and tagged structure are not carried over, and neither are annotations holding an action other than a plain web link or a jump inside the imported pages, so a/Launchor/JavaScriptaction cannot travel out of an upload and into the output. Decompression is bounded at 64 MB per stream. -
pdfInfo(pdfBytes)→{ version, pageCount, pageSizes, encrypted }. Inspect an upload — reject a 500-page file, show “3 pages”, detect a password-protected file — without importing anything. -
New error codes:
INVALID_PDF_FILE,ENCRYPTED_PDF,UNSUPPORTED_PDF,DECOMPRESSION_UNSUPPORTED. -
button(label, options)— a clickable button: a filled (optionally bordered) rounded box with an optically centred label, covered by a link annotation.link,fill,borderColor,borderWidth,color,width(points or"60%"),height,radius,paddingX/paddingY,font,size,bold,letterSpacing,textAlign,align,opacity,x/y,spacingBefore/spacingAfter. Flows by default and breaks the page when it no longer fits;yswitches to absolute placement. Withoutwidththe box sizes itself to the label; a label wider than the box is truncated with an ellipsis rather than allowed to spill out. Deliberately a link annotation and not an AcroForm/Btnwidget: no form support needed, renders in every viewer, and it can do nothing but follow its target. Targets go through the sameUNSAFE_LINKcheck aslink().
0.6.02026-08-01Driven almost entirely by a field report from a production application that builds six CV designs, a skill matrix, invoices and CLI documents with fast-pdf — in the browser, on a Node server and in scripts. Every item removes something that project had to build or work around by hand.
Driven almost entirely by a field report from a production application that builds six CV designs, a skill matrix, invoices and CLI documents with fast-pdf — in the browser, on a Node server and in scripts. Every item removes something that project had to build or work around by hand. No breaking changes: existing documents render as before.
Added
-
Measurement —
measureText(),measureBlock(),lastBlockHeight. The single largest gap: absolutely positioned designs had to predict their own line counts, which meant reimplementing the line breaker.measureText()wraps through the same enginetext()draws with;measureBlock(fn, { width })lays arbitrary flow content out on a throwaway page and reports its height. -
fontMetrics()→{ baseline, ascent, descent, capHeight, lineGap, lineHeight }in points, so optical alignment no longer needs a reverse-engineered constant. -
Browser build behind the
browserexport condition.dist/index.browser.jscontains nofsreference at all and is selected automatically — no more resolve aliases and stub modules in Vite, webpack or Turbopack. Also reachable asfast-pdf/browser. -
Clipping in the public API —
clip(),image({ radius, shape }). A round avatar used to require punching alpha through a<canvas>; it is now a real vector path in every runtime. -
opacityon every primitive —line,rect,circle,ellipse,text,image,svgandcontainerbackgrounds. -
Flow control for absolute layouts —
ensureSpace(),remainingHeight,keepTogether(), plusspacingBeforeandkeepWithNextontext(). -
region({ x, y, width, height, clip }, fn)— flow content with its own cursor inside any rectangle, including insideonPage()decorators. Returns{ usedHeight, remaining, overflow }, so a sidebar that no longer fits says so. -
flowColumns(items, options)— newspaper-style multi-column flow that runs from column 1 into column 2 and onto the next page, withbalance: trueand adroppedreport instead of silent overflow. -
pdf.xandpdf.width— the active flow area's left edge and width; the documented bridge between flow offsets and absolute page coordinates. -
Rotated text —
text({ rotate })for marginalia, turned column heads and spine labels. -
Table
valignand self-drawing cells. Cells takevalign: "top" | "middle" | "bottom"and arender: (doc, box) => …callback for progress bars, badges or logos. -
languagedocument option → the catalog's/Lang, plusViewerPreferences /DisplayDocTitlewhen a title is set. - Synthetic italic. A family registered without an italic cut is slanted by the standard 12° oblique shear instead of rendering silently upright.
-
Permissions-only encryption —
encrypt: { permissions: {…} }without inventing a dummy owner password, andencrypt.onUnsupported: "throw" | "skip"for runtimes without Web Crypto.
Fixed
-
widthOfText()ignoredletterSpacing— every letterspaced heading had to be corrected by hand at the call site. It now shares one measurement function with the renderer. -
SVG arc flags were mis-parsed. Minifiers run
large-arc-flagandsweep-flaginto the following number (a5 5 0 0150 0means0, 1, 50, 0), which corrupted practically every icon set usinga/A— Lucide, Feather, Heroicons. -
Zero-length SVG arcs emitted
NaNand poisoned the rest of the path; they are now dropped, per SVG 1.1 F.6.2. -
Line breaking only considered spaces and soft hyphens. Real hyphens, dashes and
slashes are break opportunities now (UAX #14 classes HY/BA); digit groups such as
2026-08-01and3/4stay whole. - Table row heights are derived from measured content height rather than line count, so rows containing rendered cells size correctly.
Changed
-
wrapLines()is documented and enforced as the single line-breaking implementation: drawing and measuring cannot diverge by construction. -
Fontimplementations exposecapHeightandlineGap, read fromOS/2/hheafor embedded fonts. -
Shapes emit colour and line-width operators before the path is constructed,
matching PDF's graphics object model. Output is visually identical, but if you hash
deterministic: trueoutput, expect new digests for documents containingrect(),circle()orellipse(). -
npm run buildclearsdist/itself — tsup runs the two build configs concurrently, so its owncleanwould race them.
Documentation
- README: measurement, regions, multi-column flow, clipping, opacity, self-drawing table cells, the browser condition, and a new Limitations table stating plainly what fast-pdf does not do (reading/merging PDFs, tagged PDF, non-signature form fields, WOFF2, shaping, gradients).
- Encryption is documented for the first time, including the advisory nature of PDF permissions.
-
The font section names
.ttfas the required format and gives a one-line WOFF2 conversion command.
0.5.02026-07-23
Added
-
WebP images (lossless).
image()accepts the VP8L profile, decoded in-house to DeviceRGB plus an 8-bit/SMaskwhen the picture has transparency. The decoder implements the complete VP8L feature set — the four inverse transforms, the colour cache, meta-Huffman code groups and LZ77 with 2-D distance mapping — with no dependency, validated bit-exact against libwebp. Lossy WebP (VP8) is rejected withUNSUPPORTED_IMAGE. -
GIF images. First frame, composited onto the logical screen; the palette is
decoded to DeviceRGB and a transparent colour index becomes an
/SMask. Dependency-free LZW decoder. -
SVG rendering.
svg()renders a practical subset as native PDF vector graphics —rect,circle,ellipse,line,polyline,polygon,path,text, groups with fills, strokes and opacity, and the full transform list. -
Markdown rendering.
markdown()renders a CommonMark subset — headings, paragraphs, lists, tables, code blocks, blockquotes, rules, and inline emphasis, links and images. -
Digital signatures.
signature({ sign })signs with a detached PAdES-B (CAdES) signature — RSA + SHA-256, ESS signing-certificate-v2, CMS SignedData built in-house and signed via Web Crypto. Validated againstopenssl cms -verifyand macOS Quartz. - Document encryption. AES-256 standard security handler (revision 6 / AESV3, ISO 32000-2) with user password, owner password and granular permissions. No RC4/MD5, no dependency; both strings and streams are encrypted.
-
Deterministic output. The
deterministicoption produces byte-identical PDFs for identical input — no wall-clock timestamp unlessmetadata.creationDateis set. -
Every document carries a file
/IDderived from a 128-bit digest of the file body;ModDateis written alongsideCreationDate.
Security
-
Numeric input hardening:
line(),rect(),circle(),ellipse()andtext()rejectNaN,±Infinityand magnitudes ≥ 1e21 at the call site with the stableINVALID_NUMBERcode. -
fmtNumber()throws a typedFastPDFErroras a last-line-of-defence guard, so no code path can emit a corrupt PDF number.
0.4.02026-07-19
Added
-
The
fast-pdf-designerClaude Code skill ships with the package: palettes, layout recipes and a visual validation loop. Install withnpx fast-pdf-skill. -
The example templates (
examples/*.ts) are part of the npm package, linked from the README as copy-and-adapt starting points. -
signature()— empty AcroForm signature fields (/FT /Sig) for contracts: draws a signature line and optional label, participates in the flow layout, auto-names fields with uniqueness enforced.
Security
-
Link targets reject
javascript:,vbscript:,data:andfile:schemes — including variants disguised with control characters — withUNSAFE_LINK. - The PNG alpha decode path is hardened against decompression bombs: IDAT size is capped at the size implied by the declared dimensions, pixel count at 2²⁷ (~134 MP).
-
Truncated or malformed PNG/JPEG files fail with typed
FastPDFErrors (INVALID_IMAGE_FILE,IMAGE_TOO_LARGE) instead ofRangeErrors deep in the parser; same for corrupt fonts (INVALID_FONT_FILE). - Numbers ≥ 1e21 are rejected instead of silently serializing in exponent notation; PDF names beyond U+00FF are escaped as UTF-8 byte sequences per ISO 32000-1.
-
Added
SECURITY.md(threat model, reporting) and a README security section.
0.3.02026-07-18
Added
-
objectTable(records, { columns })— render an array of records (e.g. a JSON REST response) straight into a table. Columns default to the keys of the first record, or you pick order, headers, widths, alignment and a per-columnformat(). -
examples/report.ts— a design-forward two-page report showing fast-pdf beyond invoices. -
pageBreak({ y?, format?, landscape?, margins? })— explicit page break with a controllable start position and per-page setup. - Trilingual demo page (English/German/Chinese) with a language selector.
- The package builds itself on install from GitHub (
preparescript).
Fixed
- Stroke-only shapes were always drawn in black — the requested stroke colour was reset right before stroking.
0.2.02026-07-17
Added
-
Shapes —
circle(),ellipse(), rounded rectangles, plus Bézier/clip/transform operators in the content stream. -
Layout engine —
container()(padding, margin, background, border, radius, minHeight),columns(),grid(), relative sizes ("50%"), block alignment. - Typography — underline, strikethrough, letter spacing, justified text and soft-hyphen (U+00AD) hyphenation.
-
Tables — footer rows,
colSpan/rowSpan(span groups never straddle page breaks). -
Images —
fit: contain | cover,crop,rotate,align. -
Document features —
header(),footer(),pageNumbers(),watermark(),outline()bookmarks, link annotations and a linked table of contents viatoc(). -
toStream()output, andFastPDFErrorwith stable codes.
0.1.02026-07-14
Added
-
Initial engine:
PDFDocument/Page, multi-page documents, page formats (A3–A5, Letter, Legal), landscape, margins. - Standard-14 fonts with real AFM metrics, WinAnsi encoding, TrueType embedding with subsetting.
- Text with word wrap, alignment and colours; automatic page breaks; tables with header repetition and zebra rows; JPEG/PNG images; vector primitives.
-
Output as
Uint8Array,toBuffer(),toBlob(),save()across Node/Bun/Deno/browser.