pdf-lib源码架构解析:纯JavaScript如何实现PDF的创建、修改与合并

源码分析(更新于 2026年5月16日)

为什么选择 pdf-lib?

在浏览器端处理 PDF,选择库的标准很严格:

大小 创建 修改 合并 字体 维护
pdf-lib ~350KB 活跃
pdfjs-dist ~2MB Mozilla
jsPDF ~300KB 部分 活跃
PDFKit ~1MB Node优先

pdf-lib 是唯一能在浏览器端同时创建和修改 PDF 的纯 JS 库。


PDF 文件格式基础

PDF 的内部结构

%PDF-1.7                          ← 版本头
1 0 obj                           ← 对象 1
  << /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj                           ← 对象 2
  << /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj                           ← 对象 3
  << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
xref                               ← 交叉引用表
0 4
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
trailer
  << /Size 4 /Root 1 0 R >>
startxref
190
%%EOF

核心概念

概念 说明
间接对象 1 0 obj ... endobj,通过编号引用
字典 << /Key /Value >>,类似 JSON 对象
stream ... endstream,二进制数据(通常 FlateDecode 压缩)
交叉引用表 (xref) 记录每个对象的字节偏移,实现 O(1) 随机访问
页面树 嵌套的 /Pages 节点,形成树结构

pdf-lib 架构解析

模块层次

PDFDocument (顶层 API)
  ├── PDFPage (页面操作)
  ├── PDFFont (字体管理)
  ├── PDFImage (图片嵌入)
  └── PDFCatalog (文档结构)
       └── PDFContext (底层对象模型)
            ├── PDFObject (基础对象)
            ├── PDFDict (字典对象)
            ├── PDFStream (流对象)
            ├── PDFRef (间接引用)
            └── PDFCrossRefSection (交叉引用)

对象模型

pdf-lib 的核心是 PDFContext,它维护整个文档的对象图:

class PDFContext {
  // 所有间接对象的注册表
  objects: Map<PDFRef, PDFObject>;

  // 分配新的对象编号
  assign(ref: PDFRef, object: PDFObject): void;

  // 查找对象
  lookup(ref: PDFRef): PDFObject;

  // 删除对象
  delete(ref: PDFRef): void;
}

PDF 合并的实现原理

// PDFDocument.copyPages 的核心逻辑
copyPages(srcDoc: PDFDocument, indices: number[]): PDFPage[] {
  const pages: PDFPage[] = [];

  for (const index of indices) {
    // 1. 从源文档获取页面对象
    const srcPage = srcDoc.getPage(index);

    // 2. 深拷贝页面对象及其所有引用的对象
    const copiedPage = this.context.copy(srcPage.node);

    // 3. 在目标文档注册所有拷贝的对象
    //    包括:页面字典、内容流、资源字典、字体等
    //    关键:更新所有 PDFRef 指向新的对象编号

    pages.push(PDFPage.of(copiedPage));
  }

  return pages;
}

核心难点:深拷贝时必须重映射所有间接引用。对象 A 引用了对象 B(通过 2 0 R),在目标文档中 B 可能分配到不同编号,需要维护一个映射表。


流压缩

PDF 中的内容流通常使用 FlateDecode(即 zlib/deflate)压缩:

class PDFStream {
  dictionary: PDFDict;
  contents: Uint8Array;

  // 编码方法
  getContentsString(): string;
  getContentsSize(): number;
}

// 压缩写入
const compressed = pako.deflate(rawBytes);
stream.dictionary.set(PDFName.of('Filter'), PDFName.of('FlateDecode'));
stream.contents = compressed;

pdf-lib 使用 pako 库(纯 JS 的 zlib 实现)进行压缩/解压。


字体嵌入

标准字体 vs 自定义字体

PDF 定义了 14 种标准字体(如 Helvetica、Times-Roman),无需嵌入即可显示。但中文字体必须嵌入。

工具库的字体策略

// 工具库预置的中文字体
const fonts = {
  sourceHanSans: await pdfDoc.embedFont(
    await fetch('/fonts/CN/SourceHanSansCN-Regular.otf')
  ),
  sourceHanSansBold: await pdfDoc.embedFont(
    await fetch('/fonts/CN/SourceHanSansCN-Bold.otf')
  ),
};

字体子集化:pdf-lib 支持字体子集化,只嵌入文档中实际使用的字符,大幅减小文件体积。

情况 全量字体 子集化字体 减少
10 个中文字 ~7MB ~15KB 99.8%
100 个中文字 ~7MB ~80KB 98.9%

工具库 PDF 工具链的实现

20+ PDF 工具的实现映射

工具 pdf-lib API 补充库
合并 copyPages() + addPage() -
拆分 新建 doc + copyPages() -
旋转 page.setRotation() -
水印 page.drawText() 透明度 -
页码 page.drawText() 遍历 -
加密 - @pdfsmaller/pdf-encrypt-lite
提取文本 - pdfjs-dist
PDF转图片 - pdfjs-dist + canvas
压缩 移除元数据 + 优化流 -

加密:pdf-lib 之外

pdf-lib 不支持 PDF 加密,工具库使用 @pdfsmaller/pdf-encrypt-lite

import { encrypt } from '@pdfsmaller/pdf-encrypt-lite';

const encryptedPdf = await encrypt(pdfBytes, {
  userPassword: 'user123',
  ownerPassword: 'owner456',
  permissions: {
    printing: true,
    copying: false,
    modifying: false,
  },
});

性能优化实践

大文件处理

// 对于 100+ 页的 PDF,逐页处理避免内存峰值
async function processLargePdf(file: File) {
  const pdfDoc = await PDFDocument.load(await file.arrayBuffer());
  const totalPages = pdfDoc.getPageCount();

  // 显示进度
  for (let i = 0; i < totalPages; i++) {
    const page = pdfDoc.getPage(i);
    // 逐页操作...
    updateProgress(i / totalPages);
  }
}

流式加载

pdf-lib 目前不支持流式加载——必须将整个 PDF 读入内存。对于超大文件(100MB+),这可能导致内存压力。

应对:工具库在处理大文件时显示进度提示,并建议用户在桌面端处理超大文件。


总结

pdf-lib 以 ~350KB 的体积实现了浏览器端 PDF 的创建和修改,这是非常了不起的工程。其核心设计——基于 PDFContext 的对象图模型、间接引用重映射、流压缩——体现了对 PDF 规范的深刻理解。

工具库基于 pdf-lib 构建了 PDF合并拆分旋转水印页码 等 20+ 种工具,全部浏览器本地处理。结合 pdfjs-dist(渲染/提取文本)和 pdf-encrypt-lite(加密),形成了完整的 PDF 工具链。

📝 开发手记:选择 pdf-lib 而不是 PDFKit 是我们做过最正确的技术决策之一。PDFKit 虽然功能更全,但它依赖 Node.js 的 stream API,在浏览器端需要大量 polyfill(增加约 1MB 体积)。pdf-lib 从设计之初就面向浏览器,Tree-shaking 友好,最终我们只需要 ~180KB 的核心模块就能实现合并+拆分+旋转。第一次看到 200 页的 PDF 在 3 秒内完成合并时,我们整个团队都很兴奋。

本站提供浏览器本地工具,免注册即可试用 →

#PDF#pdf-lib#源码分析#浏览器端#架构