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"); // → 浏览器下载
}
08ImagesNew: GIF & WebP
08BilderNeu: GIF & WebP
08图片新增: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) 会被拒绝 |
| 缓存 | 相同的图片字节只嵌入一次,无论多少页引用它 |
09Shapes
09Formen
09图形
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 可自由组合。
10SVG graphicsNew: svg()
10SVG-GrafikenNeu: svg()
10SVG 图形新增: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。
11MarkdownNew: markdown()
11MarkdownNeu: markdown()
11Markdown新增: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
配浅色背景排版,所有内容都像常规内容一样自动分页。
12Document features
12Dokumentfeatures
12文档功能
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 并点击签名字段)。
13Appending PDFsNew: append()
13PDFs anhängenNeu: append()
13追加 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)。
14Error handling
14Fehlerbehandlung
14错误处理
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" | …
}
}
15SecurityNew: encryption & signatures
15SicherheitNeu: Verschlüsselung & Signaturen
15安全新增:加密与签名
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
路线图上
16Performance
16Performance
16性能
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 |
17See it rendered
17So sieht’s aus
17效果展示
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() 构成的柱状图和进度环,无需任何图表库。
18Templates & AI skill
18Templates & AI-Skill
18模板与 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/
19Changelog
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.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.