【翻译】重磅推出 LibPDF:TypeScript 值得拥有的 PDF 库

0 阅读6分钟

原文链接Introducing LibPDF: The PDF Library TypeScript Deserves

作者:Lucas Smith

打造我们所需的 PDF 库,让你无需再重复造轮子。

在 Documenso,过去几年我们一直在打造一个开源文档签名平台。这意味着我们花了大量时间与 PDF 打交道。

在此我们坦诚一件事:长久以来,我们都是靠 “胶带” 勉强维系着整个技术生态的运转。

PDF 库的现存问题

当我们启动 Documenso 项目时,曾全面调研了 JavaScript 生态中的 PDF 库,结果发现:

  • pdf.js:Mozilla 开发的成熟库,渲染功能极为出色。它能处理你能想到的最怪异、最畸形的 PDF 文件,甚至支持注释编辑和带增量保存的表单填充 —— 但这些功能依赖浏览器环境。其编辑器是 DOM 元素,而非可在 Node.js 或无服务器函数中使用的无头 API(headless API)。
  • pdf-lib:拥有设计优美的 TypeScript API,确实堪称佳作,多年来一直是我们的首选工具。但当每天要处理来自各种 PDF 生成工具的数千份文档时,你就会不断遭遇边缘案例:这里一个古怪的页眉、那里一个畸形的交叉引用表(xref)。每个问题单独来看都很罕见,但在海量数据规模下,罕见就变成了常态。
  • pdfkit:仅支持 PDF 生成,完全不具备解析功能。

这些库中没有任何一个能完整覆盖文档签名工作流的全生命周期:解析用户上传的任意 PDF、填充表单字段、添加签名,并且在保存时保留所有现有签名。

我们的变通之路

为了解决这些问题,我们编写了自己的方法来处理边缘案例,用更健壮的解析程序替代了 pdf-lib 的内置版本;在处理前添加了预处理步骤,“清理” 有问题的 PDF;对于数字签名,我们开发了带有 N-API 绑定的 Rust 库,以便能从 Node.js 中原生调用。

这套方案大体上是可行的。但每隔几周,就会有客户上传一份 PDF 打破现有的逻辑,我们不得不再次修复。

此外,还有一些功能需求我们根本无法满足:增量保存(将更改追加到 PDF 而非重写文件,以保持现有签名有效)?无法实现。某些加密格式?不支持。

我们发现自己不得不频繁对客户说 “这做不到”,而我们心里清楚,唯一真正的解决方案是彻底重新设计整个架构。

于是,我们决定打造一个真正符合自身需求的库。

重磅推出 LibPDF

今天,我们正式发布 LibPDF—— 一个面向 TypeScript 的现代 PDF 库,专为处理真实世界的文档而设计。

import { PDF, P12Signer } from '@libpdf/core';

// 加载加密 PDF
const pdf = await PDF.load(bytes, { password: 'secret' });

// 填充表单字段
const form = pdf.getForm();
form.getTextField('name').setText('简·多伊');
form.getCheckBox('agree').check();

// 使用证书签名
const signer = await P12Signer.create(p12Bytes, 'password');
const { bytes: signed } = await pdf.sign({ signer });

LibPDF 有何不同之处?

  • 宽松解析:借鉴了 pdf.js 和 PDFBox 的设计理念 —— 当标准解析失败时,自动切换到暴力恢复模式:扫描整个文件,从头重建文档结构。用户无需了解或关心他们的 PDF 是否有古怪的交叉引用表,库会自行处理。
  • 增量保存:修改已签名的文档、添加新签名后保存,所有先前的签名依然有效。这对于多方签名工作流至关重要。
  • 原生数字签名:支持 PAdES B-B 至 B-LTA 全级别签名,具备完整的长期验证支持。可嵌入时间戳、在线证书状态协议(OCSP)响应和证书吊销列表(CRLs)。无需再调用外部签名服务或维护 Rust 绑定。
  • 功能完整:支持加密(RC4、AES-128、AES-256)、表单填充与扁平化、PDF 合并与拆分、文本提取、带自动子集化的字体嵌入、图片嵌入(支持带透明通道的 JPEG 和 PNG)、图层扁平化。
  • TypeScript 优先:并非 “兼容 TypeScript”,而是从设计之初就专为 TypeScript 打造。支持完整的类型推断,无 any 类型的 “逃生舱”,采用规范的异步模式。

原生签名的畅快体验

告诉你,终于能删除我们的 Rust 签名库时,那种感觉有多棒。

多年来,我们为了 PDF 签名维护着一个独立的代码库:带有 N-API 绑定的 Rust 代码,通过 npm 发布平台特定的二进制文件。每个新的 Node.js 版本发布都是一次赌博,每个 CI 流水线都需要为原生编译做特殊处理。

而现在,签名功能只需 TypeScript 代码即可实现:

const signer = await P12Signer.create(certificateBytes, 'password');
const signed = await pdf.sign({
  signer,
  level: 'B-LTA', // 长期归档级别
  timestampServer: 'http://timestamp.example.com',
});

一个依赖、一种语言、无需编译、无平台特定二进制文件。同一套代码可在 Node.js、Bun 和浏览器中运行。

站在巨人的肩膀上

如果没有前人的卓越成果,LibPDF 就不可能存在。

  • Hopding/pdf-lib:Andrew Dillon 开发的这个库为 TypeScript PDF API 树立了标杆。LibPDF 的高层 API 直接借鉴了 pdf-lib 直观的设计理念。如果你用过 pdf-lib,会立刻感到熟悉。
  • Mozilla/pdf.js:目前最成熟的 PDF 解析器,能处理其他任何库都会崩溃的文档。我们深入研究了它的恢复策略,我们的暴力解析器也采用了类似的模式。
  • Apache PDFBox:“如何实际实现这个 PDF 功能” 的参考实现。PDFBox 全面的测试套件和边缘案例处理能力,让我们明白了生产级 PDF 支持应有的样子。我们的整个字体子系统(src/fontbox/)是 PDFBox fontbox 的 TypeScript 移植版本,基于 Apache 2.0 许可证维护。

在此,向 Andrew、Mozilla 团队以及 Apache PDFBox 的贡献者们表示衷心的感谢。

未来规划

LibPDF 目前处于 Beta 阶段。我们已在 Documenso 的生产环境中使用它,但随着从真实场景中不断学习,我们预计 API 会持续演进。

以下是我们正在开发的功能:

  • 签名验证:目前我们能创建签名,但尚未支持验证功能。这是我们的下一个主要里程碑。
  • 注释支持:包括评论、高亮、图章和其他标记工具。
  • HTML 转 PDF:我们相信存在一种更现代的 HTML 转 PDF 方案,无需依赖浏览器基础设施。
  • PDF 渲染:最终,我们希望 LibPDF 成为一个完整的 PDF 解决方案,包括渲染到画布或图像格式的功能。

快速开始

安装

npm install @libpdf/core

资源链接

试用邀请

如果你曾在 JavaScript 中为 PDF 解析而苦恼,我们诚挚邀请你试用 LibPDF。上传你最怪异的 PDF、填充最复杂的表单,告诉我们哪里出了问题。

如果你正在构建文档相关工作流(签名、表单填充、模板生成),我们希望 LibPDF 能成为你一直在寻找的技术基石。

LibPDF 由开源文档签名平台 Documenso 开发维护。