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#pdf-lib#源码分析#浏览器端#架构