内容
# pretext-pdf
> **PDF 库 AI 智能体原生支持 — 人类喜欢写作。**
>
> `PdfDocument` 是纯 JSON 对象。大型语言模型(LLM)可以直接输出它,无需代码生成、无需无头浏览器,也无需 `eval`。
> 人类可以获得用于发票、报告、简历和模板的严格类型声明式 API。
[](https://www.npmjs.com/package/pretext-pdf)
[](https://www.npmjs.com/package/pretext-pdf)
[](https://github.com/Himaan1998Y/pretext-pdf/actions)
[](https://www.typescriptlang.org/)
[](#tests)
[](LICENSE)
[](#runtime-footprint)
**[在线演示](https://himaan1998y.github.io/pretext-pdf/)** · **[`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp)** (MCP 服务器) · **[从 pdfmake 迁移?](#migrating-from-pdfmake)**
*布局由 [`@chenglou/pretext`](https://github.com/chenglou/pretext) 驱动 — 由 Cheng Lou(React 核心团队,Midjourney)开发的精确文本布局引擎。*
---
## 目录
- [为什么选择 pretext-pdf](#why-pretext-pdf)
- [安装](#install)
- [快速入门](#quick-start)
- [库 API](#library-api)
- [CLI](#cli)
- [Markdown](#markdown)
- [模板](#templates)
- [从 pdfmake 迁移](#migrating-from-pdfmake)
- [MCP 服务器(Claude / Cursor / Windsurf)](#mcp-server-claude--cursor--windsurf)
- [为 AI 智能体构建](#built-for-ai-agents)
- [元素目录](#element-catalog)
- [文档功能](#document-level-features)
- [API 参考](#api-reference)
- [严格验证](#strict-validation)
- [印度 / GST 发票](#india--gst-invoicing)
- [自定义字体](#custom-fonts)
- [富文本](#rich-text)
- [脚注](#footnotes)
- [自定义元素类型(插件)](#custom-element-types-plugins)
- [示例](#examples)
- [错误处理](#error-handling)
- [故障排除](#troubleshooting)
- [非目标](#non-goals)
- [运行时占用](#runtime-footprint)
- [兼容性矩阵](#compatibility-matrix)
- [性能](#performance)
- [测试](#tests)
- [安全](#security)
- [路线图](#roadmap)
- [贡献](#contributing)
- [更新日志](CHANGELOG.md)
- [致谢](#credits)
---
## 为什么选择 pretext-pdf
JavaScript PDF 生成领域的三大阵营和一个空白点。pretext-pdf 填补了这个空白。
| | pdfmake / jsPDF / pdfkit | Puppeteer / Playwright | LaTeX / WeasyPrint | **pretext-pdf** |
|---|---|---|---|---|
| 轻量级(无 Chromium) | ✅ | ❌ ~300 MB | ❌ 原生二进制文件 | ✅ |
| 纯 ESM,在无服务器环境中运行 | ✅ | ⚠️ 在 Lambda 中很痛苦 | ❌ | ✅ |
| 专业排版(字间距、连字符、RTL/CJK) | ❌ | ✅ | ✅ | ✅ |
| 声明式 — 描述文档,而不是绘制它 | ⚠️ 部分支持 | ❌ | ❌ | ✅ |
| **LLM 直接输出工作文档** | ❌ 需要代码生成循环 | ❌ 需要 HTML+CSS 知识 | ❌ 需要 LaTeX 知识 | ✅ 纯 JSON |
| 开箱即用的 MCP 服务器 | ❌ | ❌ | ❌ | ✅ |
| 用于 shell 管道的 CLI | ❌ | ⚠️ 用代码包装 | ⚠️ 单独的二进制文件 | ✅ `pretext-pdf in.json out.pdf` |
| pdfmake 迁移垫片 | — | ❌ | ❌ | ✅ `fromPdfmake()` |
**头条新闻:** 其他所有 JavaScript PDF 库都要求 LLM(或您)*编写代码*。pretext-pdf 只需要一个 JSON 对象。这种差异使智能体生成的 PDF 可靠 — 而同样的形状恰好对人类来说也是干净的声明式 API。
---
## 安装
```bash
npm install pretext-pdf
```
> **仅支持 ESM** — 使用 `import`,而不是 `require`。需要 Node.js ≥ 18。
> **CommonJS 项目:** 使用 `const { render } = await import('pretext-pdf')` — 静态 `require()` 不起作用。
可选的同伴依赖项 — 仅安装您需要的依赖项:
| 同伴依赖项 | 何时需要 |
|---|---|
| `@napi-rs/canvas` | 仅限 SVG 和图表元素(Node;浏览器使用 `OffscreenCanvas`)。`qr-code` 和 `barcode` 是纯 JavaScript,无需 canvas。 |
| `qrcode` | `qr-code` 元素 |
| `bwip-js` | `barcode` 元素(100+ 种符号) |
| `vega` + `vega-lite` | `chart` 元素 |
| `marked` | `pretext-pdf/markdown` 入口点和 `--markdown` CLI 标志 |
| `@signpdf/signpdf` | PKCS#7 加密签名 |
| `highlight.js` | `code` 元素语法高亮(需要 `code` 元素上的 `language` 字段) |
> **内置加密** 从 v0.4.0 开始 — 无需额外安装。
---
## 快速入门
### 库 API
```typescript
import { render } from 'pretext-pdf'
import { writeFileSync } from 'fs'
const pdf = await render({
pageSize: 'A4',
margins: { top: 40, bottom: 40, left: 50, right: 50 },
metadata: { title: '发票 #001', author: 'Acme Corp' },
content: [
{ type: 'heading', level: 1, text: '发票 #12345' },
{ type: 'paragraph', text: '感谢您的惠顾。', fontSize: 12 },
{
type: 'table',
columns: [
{ width: 200 },
{ width: 50, align: 'right' },
{ width: 100, align: 'right' },
],
rows: [
{ isHeader: true, cells: [{ text: '项目', fontWeight: 700 }, { text: '数量', fontWeight: 700 }, { text: '价格', fontWeight: 700 }] },
{ cells: [{ text: '专业服务' }, { text: '10' }, { text: '$1,000' }] },
{ cells: [{ text: '托管(年度)' }, { text: '1' }, { text: '$500' }] },
],
},
{ type: 'paragraph', text: '总计:$1,500', align: 'right', fontWeight: 700 },
],
})
writeFileSync('invoice.pdf', pdf)
```
> **验证 — 当文档来自外部源时**
>
> 如果您的文档 JSON 来自用户输入、LLM 智能体、API 响应或任何外部源,请先调用 `validate()` 或 `validateDocument()`:
>
> ```typescript
> import { validate, render } from 'pretext-pdf'
> // 来自 LLM 智能体或用户输入的文档:验证后再渲染
> validate(untrustedDoc) // 如果无效则抛出异常
> const pdf = await render(untrustedDoc)
> // 或使用 validateDocument() 进行非抛出验证:
> const result = validateDocument(untrustedDoc)
> if (!result.valid) {
> console.error('无效文档:', result.errors)
> return
> }
> ```
>
> 验证可防止注入攻击,尽早捕获形状错误,并提供比单独的 render() 更好的错误消息。
> **⚠️ 安全 — 文件路径访问(生产部署前请阅读)**
>
> **`allowedFileDirs` 是可选的,默认不启用。** 如果您不设置它,
> `render()` 将读取 `image.src`、`svg.src`、字体 `src`、水印图像或 P12 证书路径中提供的任何绝对文件路径 — 包括
> 像 `/etc/passwd`、`~/.ssh/id_rsa`、`/proc/self/environ` 或 AWS 凭证文件等敏感系统路径。
>
> 这种默认开放行为是为受信任的进程内调用者(您的后端根据内部数据构造文档)而设计的。对于文档 JSON 跨越信任边界的任何部署:API 请求、Webhook、用户上传、LLM 生成的文档或任何 MCP 样式的工具调用。
>
> **对于不受信任的输入,您必须设置 `allowedFileDirs`:**
>
> ```typescript
> await render(doc, { allowedFileDirs: ['/srv/safe/assets/'] })
> ```
>
> 列出的目录之外的路径将抛出 `PATH_TRAVERSAL`。HTTPS 图片 URL 始终会针对 SSRF 阻止列表(undici 固定的 DNS、私有范围阻止)进行验证,而与此设置无关。
>
> **使用不受信任的输入的参考部署还必须在 `render(doc)` 之前调用 `validateDocument(doc)`**(参见下面的“验证”部分)— `allowedFileDirs` 是三个层次之一;验证和 SSRF 防御是另外两个。
### CLI
`pretext-pdf` 附带一个二进制文件,可以将 JSON 或 Markdown 文件转换为 PDF — 无需 Node.js 代码。
```bash
# JSON 输入,PDF 输出
pretext-pdf doc.json invoice.pdf
# 标准输入 → 标准输出(适合管道)
echo '{"content":[{"type":"heading","level":1,"text":"Hi"}]}' | pretext-pdf > out.pdf
# Markdown 直接转换为 PDF
pretext-pdf --markdown --code-font 'Courier New' README.md docs.pdf
# 帮助 / 版本
pretext-pdf --help
pretext-pdf --version
```
| 标志 | 含义 |
|---|---|
| `-i, --input <path>` | 从文件读取输入(默认为第一个位置参数或标准输入) |
| `-o, --output <path>` | 将 PDF 写入文件(默认为第二个位置参数或标准输出) |
| `--markdown` | 将输入视为 Markdown — 通过 `pretext-pdf/markdown` 转换 |
| `--code-font <name>` | 使用 `--markdown` 时,代码块的字体系列 |
| `-v, --version` | 打印版本 |
| `-h, --help` | 打印帮助 |
退出代码:`0` 成功,`1` 用户错误(参数错误、无效 JSON),`2` 渲染错误。
### Markdown
将任何 Markdown 字符串转换为 `ContentElement[]`,只需一次调用。需要 `marked` 同伴依赖项。
```typescript
import { markdownToContent } from 'pretext-pdf/markdown'
import { render } from 'pretext-pdf'
const md = `
# 2026 年第一季度报告
收入同比增长 **18%**。
| 指标 | 2025 年第四季度 | 2026 年第一季度 | 变化 |
|--------|--------:|--------:|:------:|
| 收入 | $45M | $60M | +33% |
| 利润率 | 62% | 68% | +6pp |
- [x] 云扩展启动
- [x] 企业管道翻倍
- [ ] 亚太地区于第二季度开业
> 所有数字均为百万美元。
`
const content = await markdownToContent(md, { codeFontFamily: 'Courier New' })
const pdf = await render({ content })
```
支持:h1-h4 标题、粗体、斜体、删除线、内联代码、链接、有序/无序列表(递归嵌套)、**GFM 表格(带列对齐)**、**GFM 任务列表**(☑/☐)、代码块、块引用、水平规则。
### 模板
预建的零依赖模板函数:
```typescript
import { createInvoice, createGstInvoice, createReport } from 'pretext-pdf/templates'
import { render } from 'pretext-pdf'
const content = createInvoice({
from: { name: 'Acme Corp', address: '123 Main St', email: 'billing@acme.com' },
to: { name: 'Client Ltd', address: '456 Oak Ave' },
invoiceNumber: 'INV-2026-001',
date: '2026-04-20',
items: [{ description: '咨询', quantity: 10, unitPrice: 150 }],
currency: '$', taxRate: 10, taxLabel: 'GST',
qrData: 'upi://pay?pa=acme@bank&am=1650',
})
const pdf = await render({ content })
```
可用:`createInvoice`(任意货币),`createGstInvoice`(印度 GST/IGST/CGST+SGST + UPI QR + 词汇金额),`createReport`(可选 TOC)。
### 从 pdfmake 迁移
`pretext-pdf/compat` 将 pdfmake 文档描述符转换为 `PdfDocument` — 大多数常见模式无需代码更改即可工作。
```typescript
import { fromPdfmake } from 'pretext-pdf/compat'
import { render } from 'pretext-pdf'
// 现有的 pdfmake 文档,不变
const pdfmakeDoc = {
pageSize: 'LETTER',
pageMargins: [40, 60, 40, 60],
defaultStyle: { fontSize: 11 },
styles: {
header: { fontSize: 22, bold: true },
subheader: { fontSize: 16 },
},
content: [
{ text: '发票 #001', style: 'header' },
{ text: 'Acme Corp', style: 'subheader' },
'感谢您的惠顾。',
{
table: {
widths: ['*', 'auto', 80],
headerRows: 1,
body: [
['项目', '数量', '价格'],
['小部件', '3', '$30'],
['齿轮', '5', '$50'],
],
},
},
{ ul: ['30 天期限', '延迟费:1.5%/月'] },
],
}
const pdf = await render(fromPdfmake(pdfmakeDoc))
```
| pdfmake 功能 | 兼容支持 |
|---|---|
| `string` 内容 | ✅ → 段落 |
| `{ text, bold, italics, color, fontSize, alignment, font }` | ✅ → 段落或富文本段落 |
| `{ text, style: 'header' }`(样式查找) | ✅ — `header`/`h1`/`title` 映射到标题 1,`subheader`/`h2` 到 2,等等 |
| `{ ul }` / `{ ol }`(递归) | ✅ → 列表 |
| `{ table: { body, widths, headerRows } }` | ✅ → 表格 |
| `{ image, width, height }` | ✅ → 图片 |
| `{ qr, fit }` | ✅ → 二维码 |
| `{ pageBreak: 'before' \| 'after' }` | ✅ → 分页符 |
| `{ stack }` | ✅ → 扁平化内联 |
| `{ link }` 在内联文本上 | ✅ → span.href |
| `pageSize`, `pageOrientation`, `pageMargins` | ✅ |
| `info`(标题/作者/主题/关键词) | ✅ → 元数据 |
| `header`, `footer`(字符串形式) | ✅ |
| `{ columns }` | ⚠️ 带有警告的扁平化 |
| `{ canvas }` | ❌ 不支持(绘图基元) |
| 函数样式 `header`/`footer` | ❌ 传递字符串 |
覆盖默认的标题名称映射:`fromPdfmake(doc, { headingMap: { ... } })`。
### MCP 服务器(Claude / Cursor / Windsurf)
进入任意 MCP 感知 AI 智能体的 60 秒:
```json
{
"mcpServers": {
"pretext-pdf": {
"command": "npx",
"args": ["-y", "pretext-pdf-mcp"]
}
}
}
```
公开:`generate_pdf`, `generate_invoice`, `generate_report`, `generate_from_markdown`, `list_element_types`, `validate_document`. 版本与本库相同 — 请参阅 [`pretext-pdf-mcp`](https://www.npmjs.com/package/pretext-pdf-mcp)。
---
## 为 AI 智能体构建
`PdfDocument` 是纯 JSON 对象。无需函数。除了 `type` 和少数元素特定的必需字段外,所有字段都是可选的。这种结构正是 LLM 可以可靠地生成的结构,无需工具使用循环。
```typescript
import { render } from 'pretext-pdf'
// 无论是 Claude、GPT、工作流节点还是表单提交,生成的 JSON 都一样
const pdf = await render({
metadata: { title: 'AI 生成的季度报告' },
content: [
{ type: 'heading', level: 1, text: '2026 年第一季度总结' },
{ type: 'paragraph', text: '收入同比增长 18%。' },
{ type: 'table', columns: [/* ... */], rows: [/* ... */] },
],
})
```
### 为什么 JSON-first 对智能体很重要
- **无需代码执行循环。** 模型返回 JSON;您调用 `render()`。无需沙盒、无需 `vm`、也无需 Vercel Sandbox 回程。
- **模式可验证。** 严格的 TypeScript 类型兼作契约。与 Anthropic 工具使用或 Vercel AI SDK 结构化输出配对。
- **自我纠正错误。** 每个失败都抛出 `PretextPdfError`,带有类型化的 `code`。将其反馈给模型,它就会自我修复。
- **渐进式披露。** 可选的同伴依赖项意味着智能体仅在需要时请求二维码、图表或 Markdown — 提示高效。
---
## 元素目录
```
paragraph heading(1-4) spacer hr page-break
table image svg list code
blockquote rich-paragraph callout comment form-field
toc qr-code barcode chart footnote-def
float-group
```
| 元素 | 功能 |
| --- | --- |
| `paragraph` | 文本块 — 字体、大小、颜色、对齐、背景、字母间距、小写字母、表格数字、多列 (`columns` + `columnGap`)、RTL (`dir`) |
| `heading` | 带有书签、URL 链接、内部锚点、表格数字、RTL 的 H1-H4 |
| `table` | 固定/比例/自动列、跨列、跨行、在分页符上重复表头 |
| `image` | PNG/JPG/WebP,带尺寸、对其、浮动左侧/右侧,带有 `floatText` 或富裕 `floatSpans` |
| `list` | 有序/无序,递归嵌套,`nestedNumberingStyle: 'restart' \| 'continue'` |
| `code` | 等宽代码块,带背景、填充、可选的 `highlight.js` 语法高亮(需要 `language` 字段),RTL 代码的 `dir` |
| `float-group` | 图片浮动,带有环绕文本 — 图片锚定在左侧或右侧,带有 `floatText` 或 `floatSpans` 流式排版 |
| `blockquote` | 左侧边框 + 背景 |
| `rich-paragraph` | 混合粗体/斜体/颜色/大小/上标/下标跨度,带有内联超链接 |
| `svg` | 嵌入式 SVG 图形,从 viewBox 自动调整大小 |
| `toc` | 自动生成的目录,带有准确的页码(两次传递) |
| `qr-code` | 可扫描的二维码 — UPI、URL、vCard。需要 `qrcode` 同伴依赖项。 |
| `barcode` | 100+ 种符号 — EAN-13、Code128、PDF417、DataMatrix 等。需要 `bwip-js`。 |
| `chart` | Vega-Lite 矢量 SVG 数据可视化。需要 `vega` + `vega-lite`。 |
| `comment` | PDF 贴纸注释(在 Acrobat/Preview 边栏中可见) |
| `form-field` | 交互式文本/复选框/单选/下拉/按钮(使用 `flattenForms` 进行烘焙) |
| `callout` | 信息/警告/提示/注意提示框 |
| `footnote-def` | 与 `span.footnoteRef` 配对,实现正确的脚注编号 + 区域预留 |
| `hr` / `spacer` / `page-break` | 布局基元 |
### 文档级功能
| 功能 | 配置键 | 备注 |
| --- | --- | --- |
| 水印 | `doc.watermark` | 文本或图像,不透明度,旋转 |
| 加密 | `doc.encryption` | 密码 + 粒度权限,内置 |
| 加密签名 | `doc.signature: { p12, passphrase, ... }` | PKCS#7,可选 `@signpdf/signpdf` |
| PDF 书签 | `doc.bookmarks` | 从标题自动生成 |
| 连字符 | `doc.hyphenation` | Liang 算法,例如 `language: 'en-us'` |
| 页眉/页脚 | `doc.header` / `doc.footer` | `{{pageNumber}}`, `{{totalPages}}`, `{{date}}` 标记 |
| 每节覆盖 | `doc.sections` | 不同页范围的页眉/页脚 |
| 元数据 | `doc.metadata` | 标题、作者、主题、关键词、语言、生成器 |
| 超链接 | `paragraph.url`, `heading.url`, `heading.anchor`, `span.href` | 外部、mailto、内部锚点 |
| 文档组装 | `merge(pdfs)`, `assemble(parts)` | 组合预渲染 + 新渲染 |
| 路径穿越锁定 | `doc.allowedFileDirs` | 将文件源读取限制到列出的目录 |
---
## API 参考
## 严格验证
## 印度 / GST 发票
## 自定义字体
## 富文本
## 脚注
## 自定义元素类型(插件)
## 示例
## 错误处理
## 故障排除
## 非目标
## 运行时占用
## 兼容性矩阵
## 性能
## 测试
## 安全
## 路线图
## 贡献
## 更新日志
## 致谢
### `render(doc): Promise<Uint8Array>`
```typescript
import { render } from 'pretext-pdf'
const pdf = await render({
pageSize: 'A4', // 'A4' | 'A3' | 'A5' | 'Letter' | 'Legal' | 'Tabloid' | [w, h]
margins: { top: 72, bottom: 72, left: 72, right: 72 },
defaultFont: 'Inter', // Inter 400/700 bundled
defaultFontSize: 12,
metadata: { title: '...', author: '...', keywords: ['pdf'] },
watermark: { text: 'DRAFT', opacity: 0.15, rotation: -45 },
encryption: { userPassword: 'open', ownerPassword: 'admin', permissions: { printing: true, copying: false } },
bookmarks: { minLevel: 1, maxLevel: 3 },
hyphenation: { language: 'zh-CN', minWordLength: 6 },
header: { text: '{{pageNumber}} of {{totalPages}}', align: 'right' },
footer: { text: 'Confidential', align: 'center', color: '#999' },
content: [ /* ContentElement[] */ ],
})
```
### `merge(pdfs): Promise<Uint8Array>`
Combine pre-rendered PDFs:
```typescript
import { merge } from 'pretext-pdf'
const combined = await merge([coverPdf, bodyPdf, appendixPdf])
```
### `assemble(parts): Promise<Uint8Array>`
Mix new docs with existing PDFs:
```typescript
import { assemble } from 'pretext-pdf'
const report = await assemble([
{ pdf: existingCoverPdf },
{ doc: { content: [/* fresh */] } },
{ pdf: standardTermsPdf },
])
```
### `createPdf(opts): PdfBuilder` (fluent builder)
```typescript
import { createPdf } from 'pretext-pdf'
const pdf = await createPdf({ pageSize: 'A4' })
.addHeading('My Report', 1)
.addText('Fluent chainable API.')
.addTable({ columns: [{ name: 'Col A' }, { name: 'Col B' }], rows: [{ 'Col A': 'x', 'Col B': 'y' }] })
.build()
```
### `markdownToContent(md, opts?)` *(from `pretext-pdf/markdown`)*
### `createInvoice / createGstInvoice / createReport` *(from `pretext-pdf/templates`)*
### `fromPdfmake(doc, opts?)` *(from `pretext-pdf/compat`)*
### `validateDocument(doc, opts?)` — non-throwing validation
```typescript
import { validateDocument } from 'pretext-pdf'
const result = validateDocument(doc, { strict: true })
// result: { valid, errors[], errorCount, warningCount }
if (!result.valid) {
for (const err of result.errors) {
console.log(`${err.severity} at ${err.path}: ${err.message}`)
if (err.suggestion) console.log(` → did you mean '${err.suggestion}'?`)
}
}
```
Unlike `validate()` which throws, `validateDocument()` always returns. Useful for MCP tools and agent preflight checks.
### `pdfDocumentSchema` *(from `pretext-pdf/schema`)*
Machine-readable JSON Schema for the `PdfDocument` type. Intended for editor tooling, MCP clients, and LLM context injection.
```typescript
import { pdfDocumentSchema } from 'pretext-pdf/schema'
// Use with ajv, json-schema-to-typescript, Smithery UI, or inject into LLM context:
const schemaString = JSON.stringify(pdfDocumentSchema, null, 2)
```
---
## Validation
When `document` comes from external sources (API requests, user input, MCP tools, LLM output), **ALWAYS** run `validateDocument(doc)` (or `validate(doc)`) before `render(doc)`:
```typescript
import { validateDocument, render } from 'pretext-pdf'
const validation = validateDocument(untrustedDoc)
if (!validation.valid) {
return { error: validation.errors }
}
const bytes = await render(untrustedDoc)
```
Skipping validation on untrusted input may cause:
- **Stack overflow on deeply nested malicious input** — Without the depth and
cycle guards in `validate()`, cyclic or pathologically nested documents can
exhaust the call stack inside the layout engine.
- **Prototype pollution** — Properties like `__proto__` smuggled through
`JSON.parse` can leak into the rendering pipeline if not filtered by the
validator's strict checks.
- **Unexpected runtime errors that surface as 500s** — Renderer assumes
well-typed input; passing malformed shapes through `render()` directly will
surface as opaque stack traces rather than structured `VALIDATION_ERROR`s.
The validator enforces:
- A nesting depth cap (`MAX_VALIDATION_DEPTH = 32`) at every container entry.
- Cycle detection on `ListItem.items`, `FloatGroup.content`, `RichParagraph.spans`,
and `TableElement.rows`.
- URL scheme allow-listing (no `javascript:`, `data:`, `vbscript:`).
- File-path safety for fonts and images (no UNC, no remote URLs).
---
## Strict validation
By default, `render()` uses permissive validation — unknown properties are silently ignored. Enable strict mode to catch typos and ensure property names match the schema exactly:
```typescript
import { render } from 'pretext-pdf'
const pdf = await render(doc, { strict: true })
```
In strict mode:
- **Unknown properties are rejected** with a `VALIDATION_ERROR` that includes:
- Property name and location (JSONPath-like: `doc.content[3].table.rows[0].cells[1].align`)
- Typo suggestions via Levenshtein distance (edit distance ≤2)
- All violations collected before throwing, with a 20-error cap + overflow indicator
Example error:
```
VALIDATION_ERROR:
unknown property 'fontSizee' at doc.content[0].fontSizee; did you mean "fontSize"?
unknown property 'colorr' at doc.content[1].inline.colorr; did you mean "color"?
```
Strict validation is useful for:
- **AI agent self-correction**: LLMs can parse error messages and fix typos
- **Template development**: catch copy-paste errors in large documents
- **Type safety**: ensure your generator is emitting well-formed documents
You can also call `validate()` standalone for testing:
```typescript
import { validate } from 'pretext-pdf'
// Throws PretextPdfError('VALIDATION_ERROR', ...) if strict check fails
validate(doc, { strict: true })
```
---
## India / GST invoicing
Built-in support for Indian invoice requirements:
- **₹ symbol** renders correctly (bundled Inter includes the Rupee glyph)
- **Indian number formatting** (`1,00,000` not `100,000`)
- **GST structure** — CGST/SGST (intra-state) and IGST (inter-state) layouts (auto-detected from state fields)
- **Amount in words** — Indian numbering system (Lakh/Crore), with correct sub-rupee handling
- **SAC/HSN codes** — column support in line-item tables
```typescript
import { createGstInvoice } from 'pretext-pdf/templates'
import { render } from 'pretext-pdf'
const content = createGstInvoice({
supplier: { name: 'Antigravity Systems', address: 'Gurugram, HR', gstin: '06AAACA1234A1ZV', state: 'Haryana' },
buyer: { name: 'TechStartup Ltd', address: 'Mumbai, MH', gstin: '27AABCB5678B1ZP', state: 'Maharashtra' },
invoiceNumber: 'INV/2026-27/001',
invoiceDate: '20 Apr 2026',
placeOfSupply: 'Maharashtra (27)',
items: [
{ description: 'Software Development', hsnSac: '998314', quantity: 80, unit: 'Hrs', rate: 3000, taxRate: 18 },
],
qrUpiData: 'upi://pay?pa=merchant@hdfc&pn=Antigravity&am=283200',
bankName: 'HDFC Bank', accountNumber: '501001234567', ifscCode: 'HDFC0001234',
})
const pdf = await render({ content })
```
See [`examples/gst-invoice-india.ts`](examples/gst-invoice-india.ts) for a fully wired example.
---
## Custom fonts
```typescript
const pdf = await render({
fonts: [
{ family: 'Roboto', weight: 400, src: '/path/to/Roboto-Regular.ttf' },
{ family: 'Roboto', weight: 700, src: '/path/to/Roboto-Bold.ttf' },
{ family: 'Roboto', style: 'italic', src: '/path/to/Roboto-Italic.ttf' },
],
defaultFont: 'Roboto',
content: [
{ type: 'paragraph', text: 'Uses Roboto' },
{ type: 'paragraph', text: 'Bold', fontWeight: 700 },
],
})
```
> **Avoid `system-ui`** — known Pretext layout-measurement inaccuracy on macOS. Always name fonts explicitly.
---
## Rich text
```typescript
{
type: 'rich-paragraph',
fontSize: 13,
spans: [
{ text: 'Normal ' },
{ text: 'bold', fontWeight: 700 },
{ text: ' and ', fontStyle: 'italic' },
{ text: 'colored', color: '#e63946' },
{ text: ' and ' },
{ text: 'linked', href: 'https://example.com', underline: true, color: '#0070f3' },
{ text: '. Also: E=mc' },
{ text: '2', verticalAlign: 'superscript' },
{ text: ' and H' },
{ text: '2', verticalAlign: 'subscript' },
{ text: 'O.' },
],
}
```
---
## Footnotes
`createFootnoteSet()` produces matched reference/definition pairs with guaranteed unique IDs:
```typescript
import { render, createFootnoteSet } from 'pretext-pdf'
const notes = createFootnoteSet([
{ text: 'Smith, J. (2022). Typography in PDFs.' },
{ text: 'Ibid., p. 42.' },
])
await render({
content: [
{
type: 'rich-paragraph',
spans: [
{ text: 'See the original research' },
{ text: '¹', verticalAlign: 'superscript', footnoteRef: notes[0]!.id },
{ text: ' for details.' },
],
},
...notes.map(n => n.def), // footnote-def elements go at end of document
],
})
```
---
## Custom element types (plugins)
The plugin API lets you register new element types without forking the library.
Each plugin definition handles one `type` string and participates in the standard
validate → measure → render pipeline.
```typescript
import { render } from 'pretext-pdf'
import type: { PluginDefinition } from 'pretext-pdf'
import { rgb } from '@cantoo/pdf-lib'
const highlightBoxPlugin: PluginDefinition = {
type: 'highlight-box',
// Optional: reject bad elements early
validate(element) {
if (typeof element['label'] !== 'string') return '"label" must be a string'
},
// Required: return block height for layout/pagination
async measure(element) {
return { height: 48, spaceBefore: 8, spaceAfter: 8 }
},
// Required: draw onto the pdf-lib page
render({ element, pdfPage, x, y, width, height }) {
pdfPage.drawRectangle({ x, y: y - height, width, height, color: rgb(1, 0.93, 0.73) })
pdfPage.drawText(element['label'] as string, { x: x + 16, y: y - 30, size: 13 })
},
}
// Pass plugins via render() options or createPdf() options
const pdf = await render(doc, { plugins: [highlightBoxPlugin] })
```
**How it works:**
| Hook | Stage | Required | Purpose |
| ---- | ----- | -------- | ------- |
| `validate` | 1 | No | Reject malformed custom elements; return error string or void |
| `loadAsset` | 2b | No | Embed a `PDFImage` (passed back as `context.pdfImage` in render) |
| `measure` | 3 | **Yes** | Return `height`, optional `spaceBefore`/`spaceAfter`, optional `pluginData` |
| `render` | 5 | **Yes** | Draw onto `context.pdfPage` using pdf-lib's drawing API |
**Y-coordinate note:** pdf-lib uses a bottom-left origin. `context.y` is the **top** edge of your block.
To fill the block: `drawRectangle({ x, y: y - height, width, height })`.
To draw the first line of text: `drawText(line, { x, y: y - fontSize })`.
**Constraints:** Plugin elements can only appear at the top level of `doc.content`.
They cannot be nested inside callout, blockquote, or float-group children (those
have hardcoded child type whitelists). Use top-level layout with spacers for positioning.
See `examples/plugin-custom-element.ts` for a full runnable example:
```bash
npm run example:plugin
```
---
## Examples
```bash
npm run example # Basic invoice
npm run example:gst # India GST invoice
npm run example:watermark # Text/image watermarks
npm run example:bookmarks # PDF outline/bookmarks
npm run example:toc # Auto table of contents
npm run example:rtl # Arabic/Hebrew RTL text
npm run example:encryption # Password-protected PDF
npm run example:hyperlinks # External + email + internal anchors
npm run example:annotations # Sticky notes
npm run example:assembly # Merge + assemble multiple PDFs
npm run example:inline # Super/subscript, letterSpacing, smallCaps
npm run example:forms # Interactive form fields
npm run example:callout # Callout boxes
npm run example:plugin # Custom element types (plugin API)
```
All write to `output/*.pdf`.
---
## Error handling
Every error throws `PretextPdfError` with a typed `code`:
```typescript
import { render, PretextPdfError } from 'pretext-pdf'
try {
const pdf = await render(config)
} catch (err) {
if (err instanceof PretextPdfError) {
| err.code === 'VALIDATION_ERROR': // Invalid config
case 'FONT_LOAD_FAILED': // Font file not found
case 'IMAGE_TOO_TALL': // Image doesn't fit on page
case 'IMAGE_LOAD_FAILED': // URL fetch / safety check failed
case 'ASSEMBLY_EMPTY': // merge / assemble called with empty array
// ... see CHANGELOG.md for the full list
}
}
}
```
This shape is also designed for AI self-correction loops — the typed `code` is enough context for an LLM to fix its own output.
---
## Troubleshooting
### Hyphenation language not found
Use **lowercase** language codes that match the npm package name:
```typescript
hyphenation: { language: 'zh-CN' } // ✅
hyphenation: { language: 'zh-CN' } // ❌ fails on Linux (case-sensitive FS)
```
### SVG / chart / qr-code / barcode rendering
Install `@napi-rs/canvas` (Node only — browsers use native `OffscreenCanvas`):
```bash
npm install @napi-rs/canvas
```
### PDF is blank or too small
Check margins. If `left + right` exceeds page width, content width becomes negative:
```typescript
margins: { top: 36, bottom: 36, left: 36, right: 36 }
```
### Form fields not interactive
`flattenForms: true` bakes fields into static content — by design. Remove the flag to keep them interactive.
### Common schema mistakes
#### `doc.fonts must be an array`
```typescript
// ❌ WRONG
{ pageSize: 'A4', fonts: {} }
// ✅ CORRECT
{ pageSize: 'A4', fonts: [] }
```
#### Table schema is not like other PDF libraries
```typescript
// ❌ WRONG — assumes headers + simple rows
{
type: 'table',
headers: ['Col1', 'Col2'],
rows: [['val1', 'val2']]
}
// ✅ CORRECT — columns + rows with cell objects
{
type: 'table',
columns: [{ width: '*' }, { width: 200 }], // '*' is flex; number is fixed pt
rows: [
{ isHeader: true, cells: [{ text: 'Col1', fontWeight: 700 }, { text: 'Col2' }] },
{ cells: [{ text: 'val1' }, { text: 'val2' }] }
]
}
```
> **Why the schema differs:** pretext-pdf's table design supports colspan, rowspan, repeating headers, and fine-grained per-cell styling that other PDF libraries can't express. The column-first design lets you define layout once and reuse it across all rows.
#### Callout element uses `content` not `text`
```typescript
// ❌ WRONG
{ type: 'callout', variant: 'info', text: 'Note here' }
// ✅ CORRECT
{ type: 'callout', style: 'info', content: 'Note here' }
```
Valid `style` values: `'info'`, `'warning'`, `'tip'`, `'error'`.
#### List items must be objects
```typescript
// ❌ WRONG
{ type: 'list', items: ['Item 1', 'Item 2'] }
// ✅ CORRECT
{ type: 'list', items: [{ text: 'Item 1' }, { text: 'Item 2' }] }
```
### Browser usage
Supply font bytes via `doc.fonts: [{ family: 'Inter', weight: 400, src: <Uint8Array> }]` — the bundled Inter loader is Node-only. Also register the same font with `document.fonts.add(new FontFace(...))` so pretext's measurement matches pdf-lib's drawing.
---
## Non-goals
What pretext-pdf is **not** trying to be — pick a different tool for these:
- **Editing or parsing existing PDFs** → [`pdf-lib`](https://github.com/Hopding/pdf-lib), [`pdf-parse`](https://www.npmjs.com/package/pdf-parse)
- **Filling existing PDF form templates** → [`pdf-lib`](https://github.com/Hopding/pdf-lib), [`pdftk`](https://www.pdflabs.com/tools/pdftk-server/)
- **Heavily art-directed pages** with CSS grids, SVG illustrations, floats, background images → headless Chrome (Puppeteer)
- **PDF/A archival, PDF/UA accessibility tagging** → not yet
- **Print-shop kerning pairs, OpenType ligatures, variable-font axes beyond weight** → upstream Pretext doesn't model these
---
## Runtime footprint
Mandatory runtime dependencies:
- `@cantoo/pdf-lib` — PDF assembly
- `@chenglou/pretext` — text-layout engine
- `@fontsource/inter` + `@fontsource-variable/inter` — bundled Inter (static + variable)
- `@pdf-lib/fontkit` — font subsetting
- `bidi-js` — bidirectional text resolution
- `hypher` + `hyphenation.zh-CN` — hyphenation
All other capabilities (SVG, charts, QR, barcodes, markdown, signing) are optional peer deps — install only what you use.
**Browser:** the library imports cleanly from any non-`file://` URL (esm.sh, Vite dev server, browser bundles) since v0.8.1. Bring your own Inter font via `doc.fonts` and register it with `document.fonts.add(...)` for accurate measurement.
---
## Compatibility matrix
| Environment | Status | Notes |
| ----------- | ------ | ----- |
| **Node.js 18 / 20 / 22** | ✅ Confirmed | CI tests all three. Requires `@napi-rs/canvas` peer dep for SVG / chart / QR elements. |
| **Browser (Vite, webpack, esm.sh)** | ✅ Confirmed | Uses native `OffscreenCanvas`. No canvas peer dep needed. Bring your own font bytes via `doc.fonts` — the bundled Inter loader is Node-only. |
| **Bun** | ⚠️ Untested | Bun has Node.js compat mode. `@napi-rs/canvas` provides Bun builds but is untested end-to-end. |
| **Deno** | ⚠️ Untested | Deno's Node compat layer may work. `@napi-rs/canvas` native bindings are the unknown variable. |
| **AWS Lambda / serverless (Node runtime)** | ⚠️ Likely works | Node.js runtime, ESM supported. Cold-start impact from `@napi-rs/canvas` native addon if used. Elements that don't need canvas (paragraph, heading, table, list) have no native dep. |
| **Cloudflare Workers** | ❌ Not supported | No Node.js runtime, no native addons, no `OffscreenCanvas`. Neither the Node polyfill nor the browser path can run. |
| **Next.js (server components / API routes)** | ✅ Confirmed (Node path) | Runs on Node.js server side. Client-side rendering follows the browser path above. |
**Legend:** ✅ Confirmed in CI or end-to-end testing · ⚠️ Untested / likely works · ❌ Known not supported
## 性能
在 Windows 11 / Node 22 / Intel i7-12 代处理器上进行基准测试。平均运行时间超过 10 次,不包括首次冷启动。
| 文档 | 渲染时间 | PDF 大小 |
| --- | --- | --- |
| 1 页(标题 + 段落 + 列表) | 约 220 毫秒 | 约 45 KB |
| 混合(标题 + 段落 + 20 行表格 + 列表 + 分隔线) | 约 290 毫秒 | 约 60 KB |
| 10 页(40 个章节,混合元素) | 约 1,100 毫秒 | 约 180 KB |
**字体子集** 自动适用于 TTF/OTF 字体。仅嵌入使用的字形 — 通常比完整字体嵌入小 40–60%。单字体发票渲染小于 65 KB。
对于包含 10,000+ 个元素的文档,请设置 `NODE_OPTIONS=--max-old-space-size=4096`。
---
## 测试
691 个测试,100% 通过率:
```bash
npm test # 完整测试套件(合约 + 单元测试 + 端到端测试 + 阶段测试 + 2f 压力测试)
npm run test:unit # 验证、构建器、富文本
npm run test:e2e # 端到端渲染
npm run test:phases # 所有阶段测试,包括 v0.8/v0.9 功能
npm run test:rich # 富文本段落合成器(包括 v0.8.2 空格回归)
npm run test:contract # 公共 API 接口合约
npm run test:visual # 像素差异视觉回归
```
**覆盖率**:类型安全、路径验证、SSRF、错误处理、边界情况、加密签名、文档组装、每个内容元素、依赖错误代码、MCP 工具验证、浏览器导入模拟。
---
## 安全
2026 年 4 月的一次全面审计修复了 41 个问题,包括路径遍历保护、异步 I/O、错误净化、类型安全和显式故障模式。后续修复:
- **v0.8.3** — 修复 IPv4 映射的 IPv6 SSRF 绕过漏洞;`fetch` 重定向现在按跳跃重新验证。
- **v0.8.1** — 修复浏览器模块初始化崩溃(Node-only APIs 受 `IS_NODE` 检查限制)。
当前安全状况的亮点:
- 可选的 `allowedFileDirs` 锁定,用于用户控制的文件输入
- 所有错误消息均已净化(无文件系统路径或机密泄露)
- 异步文件 I/O(非阻塞)
- 严格的 TypeScript 类型检查,文档记录的 `any` 类型转换仅在 pdf-lib 内部边界处
- HTTPS-only fetch,带有私有 IP / SSRF 防护,包括 IPv6
- HTTP 重定向链针对相同的 SSRF 防护进行重新验证
参见 [SECURITY.md](SECURITY.md) 以了解披露政策。
---
## 路线图
| 阶段 | 功能 | 状态 |
|-------|---------|--------|
| 1–6 | 核心引擎、分页、排版、富文本、构建器、列 | ✅ |
| 7A–G | 书签、水印、连字符、目录、SVG、RTL、加密 | ✅ |
| 8A–H | 注释、表单、组装、标注、签名、元数据、超链接、内联格式化 | ✅ |
| 9A–C | 加密签名(PKCS#7)、图像浮动、字体子集 | ✅ |
| 10A–D | 二维码、条形码、Vega-Lite 图表、Markdown、模板 | ✅ |
| 11+ | 性能增强、安全加固 | ✅ |
| **0.9.0** | **CLI、pdfmake 兼容性补丁、GFM 表格 + 任务列表** | ✅ |
| **1.0.0** | **插件 API(自定义元素类型)、严格验证、`PdfBuilder` 流畅 API** | ✅ |
| **1.0.2–1.0.6** | **`validateDocument()`、JSON Schema 导出、完整模式覆盖、审计修复** | ✅ |
| **1.1.0** | **预置布局引擎、移除 `@chenglou/pretext` npm 依赖** | ✅ |
| **1.2.x** | **鉴别联合类型、安全加固(SSRF、isError)、基准测试数据集** | ✅ |
| **1.3.0–1.3.4** | **DNS 去重、并行光栅、字宽缓存(约 1.66 倍加速);漂移防护;目录项验证** | ✅ |
| 未来 | 可变字体、OpenType 功能、PDF/A、PDF/UA 无障碍访问 | 🔜 |
参见 [docs/ROADMAP.md](docs/ROADMAP.md)。
---
## 贡献
参见 [CONTRIBUTING.md](CONTRIBUTING.md)。TDD 方法 — 先编写测试。
有用命令:
```bash
npm install # 一次性设置
npm run build # tsc → dist/
npm run typecheck # tsc --noEmit
npm test # 完整测试套件
npm run example # 运行示例渲染
```
---
## 许可证
[MIT](LICENSE)
---
## 鸣谢
由 [Himanshu Jain](https://github.com/Himaan1998Y) 在 [pretext](https://github.com/chenglou/pretext)、[pdf-lib](https://github.com/Hopding/pdf-lib) 和 [@napi-rs/canvas](https://github.com/napi-rs/canvas) 的基础上构建。
有疑问? [开启一个问题](https://github.com/Himaan1998Y/pretext-pdf/issues) — 或在 [演示](https://himaan1998y.github.io/pretext-pdf/) 中试用。
MCP 配置
以下是此 MCP Server 的配置信息,可直接复制到 Cursor 或其他 MCP 客户端中使用。
mcp.json
连接信息
猜你喜欢
暂无推荐服务器