简体中文
企业版

从零手写 PDF(01):Hello, World——构建一个最小可用 PDF

Doclingo Team2026年1月30日

从零手写 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.