繁體中文
企業版

從零手寫 PDF(01):Hello, World——構建一個最小可用 PDF

Doclingo TeamJanuary 30, 2026

從零手寫 PDF(01):Hello, World——構建一個最小可用 PDF

系列目標:把 PDF 當作一種可讀的文件格式來理解——先從"能跑"的最小例子開始,再逐步擴展到圖形、多頁、壓縮與資源復用。

系列目錄

  • 第 01 篇(本文):手寫一個最小 PDF(1 頁 + 1 行文字),並用工具補齊為可打開的標準 PDF
  • 第 02 篇:在內容流裡畫線/畫矩形(理解路徑、描邊、填充)
  • 第 03 篇:多頁 PDF(Pages 樹怎麼長出來)
  • 第 04 篇:更接近真實世界(壓縮流、資源復用、可選結構等)

為什麼要了解 PDF 的底層結構?

PDF(Portable Document Format,可攜帶文檔格式)是當今最流行的頁面描述語言之一。它和 HTML/CSS 那種"內容與呈現分離、可回流(Reflowable)"的思路不同,PDF 更強調版面固定、所見即所得——無論在什麼設備上打開,排版都一致。

了解 PDF 底層結構有幾個實際好處:

  • 調試 PDF 生成問題:當你用代碼庫生成 PDF 出錯時,能看懂底層結構才能快速定位問題
  • 自動化處理:批量提取文本、合併文檔、添加水印等操作,理解結構後才能精準操作
  • 安全審計:了解 PDF 可以嵌入哪些內容(JavaScript、附件、表單等),有助於安全分析
  • 學習文件格式設計:PDF 的"對象圖 + 隨機訪問"設計是經典範例,值得學習

準備工作

本文會先寫一個"結構不完整但邏輯正確"的 hello-broken.pdf,再用 pdftk 自動補齊關鍵結構並輸出 hello.pdf

  • 需要工具pdftk(免費命令行工具,支持 Windows/macOS/Linux)
  • 輸出文件hello-broken.pdf(手寫)、hello.pdf(修復後可打開)

核心概念:PDF 的三層結構

理解 PDF 最重要的是建立一個三層心智模型

PDF 三層心智模型

1. 對象層(Document Content)

PDF 文檔由很多對象組成,對象之間用間接引用(如 2 0 R)連接成一張圖。常見對象類型:

類型示例說明
Name/Page/ 開頭的名稱
整數/實數5036.0數值
字符串(Hello, World!)用圓括號包裹
陣列[0 0 612 792]有序集合
字典<< /Type /Page >>鍵值對集合
間接引用2 0 R引用對象 2(生成號 0)
stream...endstream二進制數據(如繪製指令、圖片)

2. 內容層(Page Content)

真正"把文字/圖形畫到頁面上"的指令序列,通常寫在 stream ... endstream 裡。格式是:操作數在前,操作符在後

/F0 36 Tf          ← 操作數: /F0, 36  操作符: Tf(設置字體)
(Hello, World!) Tj ← 操作數: 字符串   操作符: Tj(繪製文本)

3. 文件結構層(File Structure)

讓閱讀器能快速隨機訪問任意對象,而不必從頭讀到尾:

元素作用
%PDF-1.x文件頭,標識 PDF 版本
xref交叉引用表:對象編號 → 字節偏移
trailer尾部字典:指向根對象 /Root
startxref指出 xref 表的起始位置
%%EOF文件結束標記

最小 PDF 需要哪些對象?

一個"最小但能顯示文字"的 PDF,對象之間的引用關係如下:

PDF 最小對象關係圖

最小對象清單

對象作用關鍵字段
Catalog根對象,文檔入口/Type /Catalog, /Pages
Pages頁面樹/Type /Pages, /Kids, /Count
Page單個頁面/Type /Page, /MediaBox, /Resources, /Contents, /Parent
Resources資源容器/Font(字體字典)
Font字體定義/Type /Font, /BaseFont, /Subtype
Contents內容流繪製指令的 stream

實戰:手寫 hello-broken.pdf

新建文件 hello-broken.pdf,把下面內容完整粘貼進去:

%PDF-1.0
1 0 obj
<< /Type /Pages
   /Count 1
   /Kids [2 0 R]
>>
endobj

2 0 obj
<< /Type /Page
   /MediaBox [0 0 612 792]
   /Resources 3 0 R
   /Parent 1 0 R
   /Contents [4 0 R]
>>
endobj

3 0 obj
<< /Font
     << /F0
          << /Type /Font
             /BaseFont /Times-Italic
             /Subtype /Type1 >>
     >>
>>
endobj

4 0 obj
<< >>
stream
1. 0. 0. 1. 50. 700. cm
BT
 /F0 36. Tf
 (Hello, World!) Tj
ET
endstream
endobj

5 0 obj
<< /Type /Catalog
   /Pages 1 0 R
>>
endobj

xref
0 6
trailer
<< /Size 6
   /Root 5 0 R
>>
startxref
0
%%EOF

為什麼這個文件是"壞的"?

我們故意省略或填錯了以下內容:

缺失/錯誤項說明
xref 偏移沒有填寫每個對象的真實字節偏移
startxref填了 0,不是 xref 的真實位置
/Length內容流沒有聲明長度
二進制標記缺少頭部的二進制標識行

這些都是閱讀器需要的關鍵信息,缺失會導致無法打開或只能容錯打開。


關鍵內容流指令詳解

內容流在對象 4 0 objstream ... endstream 之間,逐行解釋:

1. 0. 0. 1. 50. 700. cm   ← 設置變換矩陣(注意 1. 代表浮點數 1.0)
BT                         ← 開始文本對象
 /F0 36. Tf                ← 選擇字體 F0,字號 36pt
 (Hello, World!) Tj        ← 繪製字符串
ET                         ← 結束文本對象

變換矩陣 cm 操作符

1 0 0 1 50 700 cm 是一個 6 元素的變換矩陣 [a b c d e f],對應:

| a  b  0 |     | 1  0  0 |
| c  d  0 |  =  | 0  1  0 |
| e  f  1 |     | 50 700 1 |

a=1, b=0, c=0, d=1 時,這是一個純平移矩陣,把坐標系原點(也就是後面繪製操作的 (0,0) 點)移動到 (50, 700)。如果不移動,默認原點在頁面左下角。

文本操作符

操作符含義示例
BTBegin Text,開始文本對象BT
ETEnd Text,結束文本對象ET
Tf設置字體和字號/F0 36 Tf
Tj繪製字符串(Hello!) Tj

用 pdftk 修復為可打開的 PDF

hello-broken.pdf 所在目錄執行:

pdftk hello-broken.pdf output hello.pdf

用任意 PDF 閱讀器打開 hello.pdf,你應該能看到頁面上出現 "Hello, World!"(Times-Italic 字體,36pt,位於頁面左上方)。

pdftk 幫你補齊了什麼?

補齊項說明
二進制標記行%PDF-1.0 後加一行不可打印字符,確保被識別為二進制文件
/Length為內容流計算並添加字節長度
xref計算每個對象的字節偏移並填入
startxref填入 xref 表的真實起始位置

為什麼需要 xref / trailer / startxref?

核心目的:隨機訪問

想像一個 500 頁的 PDF,如果沒有 xref,閱讀器想顯示第 450 頁就必須從頭解析到第 449 頁——這太慢了。

有了 xref,閱讀器可以:

  1. 先讀 startxref → 找到 xref 位置
  2. trailer → 找到根對象 /Root
  3. 從根對象順藤摸瓜 → 直接跳到第 450 頁對象
  4. 通過 xref 查該對象的字節偏移 → 直接 seek 過去讀取

時間複雜度從 O(n) 降到 O(1)


本篇練習

建議你真的動手改 hello-broken.pdf,然後重新用 pdftk 修復,觀察效果:

練習修改內容觀察點
A(Hello, World!) 改成其他英文短句文字變化
B36 改成 1272字號變化
C50 700 改成 50 100位置下移(PDF 坐標系原點在左下角)
D/Times-Italic 改成 /Helvetica/Courier字體變化
E/MediaBox [0 0 612 792] 改成 [0 0 595 842]紙張從 US Letter 變成 A4

提示:PDF 坐標系原點在頁面左下角,Y 軸向上。(50, 700) 表示距左邊 50pt、距底部 700pt。


常見問題

Q: 為什麼用 Type1 內置字體而不是 TrueType?

A: Type1 的 14 種標準字體(Times、Helvetica、Courier 等)是 PDF 閱讀器必須內置的,不需要嵌入字體文件,最簡單。真實場景中通常需要嵌入字體以保證跨平台一致性。

Q: /MediaBox [0 0 612 792] 這些數字是什麼?

A: 單位是 point(1 point = 1/72 英寸)。612 × 792 點 = 8.5 × 11 英寸 = US Letter 紙張。A4 是 595 × 842 點。

Q: 生成號(如 2 0 R 中的 0)是什麼?

A: 用於增量更新。當對象被修改時,生成號加 1。新建的 PDF 裡所有對象生成號通常都是 0。


下一篇預告

第 02 篇我們繼續沿用"手寫內容流"的方式,加入最基礎的圖形路徑操作

  • m(moveto)、l(lineto):定義路徑
  • S(stroke):描邊
  • re(rectangle)、f(fill):畫矩形並填充

讓你在同一頁上同時畫出:標題文字 + 一條水平分隔線 + 一個矩形框,從"會寫字"進階到"會畫圖形"。

Copyright © 2026 Doclingo. All Rights Reserved.
產品
文檔翻譯
更多工具
API
企業版
資源
方案
App
關於
說明中心
服務條款
隱私政策
版本更新
部落格
聯繫信息
郵箱:support@doclingo.ai
繁體中文
Copyright © 2026 Doclingo. All Rights Reserved.