pdf-lib 原始碼架構解析:純 JavaScript 如何實現 PDF 的建立、修改與合併
為什麼選擇 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 工具鏈。
本站提供瀏覽器本地工具,免註冊即可試用 →