夜雨聆风学习资料网

ARTICLE · 1051897

PDF 这么复杂,Go 是怎么把它搞定的?

PDF 这么复杂,Go 是怎么把它搞定的?
平时我们处理 PDF,可能就是:打开、阅读、打印、合并。

但对于程序员来说,PDF 其实是一个挺麻烦的东西。

里面有:

文字     图片      字体      页面     对象      压缩数据     加密      签名     附件  

更麻烦的是,不同软件生成的 PDF,还可能存在各种奇奇怪怪的问题。

所以我最近在 GitHub 上发现一个挺有意思的 Go 项目:pdfcpu。

它干的事情非常简单:用 Go 处理 PDF。

但真正有意思的是,它不是简单调用一个现成的 PDF 程序,而是自己实现了一套 PDF 处理能力。

01 一个 Go 项目,能处理多少 PDF 操作?

先看看它能干什么。

安装:

go install github.com/pdfcpu/pdfcpu/cmd/pdfcpu@latest      

然后:

查看版本 pdfcpu version验证     pdfcpu validate test.pdf    合并     pdfcpu merge merged.pdf a.pdf b.pdf  压缩     pdfcpu optimize input.pdf output.pdf加密     pdfcpu encrypt input.pdf output.pdf 

除此之外,还支持:

拆分      提取图片      提取字体      添加水印      数字签名      附件管理      PDF 信息查看    

也就是说,它不是一个简单的“PDF 合并工具”,而是一套完整的 PDF 工具链。

02 PDF 为什么难?

如果你以为 PDF 就是:

第一页      第二页      第三页 

那就低估它了,PDF 内部更像这样:

PDF      ├── Catalog      ├── Pages      │   ├── Page      │   ├── Page      │   └── Page      ├── Fonts      ├── Images     ├── Streams      ├── Metadata      ├── Encryption      └── Cross Reference      

页面可能引用对象,对象可能引用另外一个对象,文字可能放在 Stream 里,图片可能经过压缩,最后还有 xref 来记录对象的位置。

所以 PDF 解析,本质上是在做:

PDF文件         ↓      解析         ↓      理解对象         ↓      建立关系         ↓      修改         ↓      重新生成PDF      

这已经不是简单的:os.ReadFile(”test.pdf”)了。

03 Go 是怎么处理这种复杂格式的?

Go 有一个非常适合做这件事情的特点:类型系统简单,而且特别适合把复杂的数据结构抽象出来。

比如一个简化版的 PDF 对象模型,可以想象成:

type Object interface {          Type() string      }       type Integer struct {          Value int64      }       type String struct {          Value string      }       type Array []Object             type Dict map[string]Object    

PDF 里面各种对象:

Integer      String     Array      Dictionary      Stream      

就可以映射成 Go 的类型,这样程序处理 PDF 时,就不是在操作一堆:[]byte

而是在操作一个真正的 PDF 对象模型。

这也是写解析器时非常重要的一步:先把复杂的文件格式变成程序能够理解的数据结构。

04 真正麻烦的,是“坏 PDF”

正常 PDF 并不可怕,最麻烦的是:

损坏的 PDF      超大的 PDF      结构异常的 PDF      恶意构造的 PDF      

假设你的服务器提供一个:POST /upload

用户上传 PDF,你直接:

data, _ := io.ReadAll(r.Body)      

然后开始解析。

如果对方上传一个超大文件怎么办?

如果 PDF 解压之后突然膨胀几十倍怎么办?

如果里面有非常深的对象引用怎么办?

如果图片像素数量巨大怎么办?

这时候 PDF 解析器就不只是“功能代码”了。

它还必须考虑:

内存      CPU      递归深度      对象数量      解压数据大小      图片尺寸      

这也是 pdfcpu 近几个版本比较值得关注的地方。

它逐渐增加了各种 parser/resource limits,用来限制:

Stream 大小      解码后的数据大小      图片像素      对象数量      xref 数量      递归深度      

目的就一个:别让一个 PDF 把你的服务搞死。

05 这其实很像我们平时做安全产品

看到这里,是不是有点熟悉?一个文件进来:

用户输入         ↓      解析         ↓      识别         ↓      处理      

安全产品也是一样。

漏洞扫描器面对:

HTTP      HTML      JSON      XML      JavaScript      证书      文件      协议      

同样需要处理大量:

异常输入      畸形数据      超大数据      恶意数据      

所以一个成熟的解析器,最重要的能力往往不是:“正常情况下能不能解析。”

而是:“面对一个故意搞你的输入,还能不能安全退出。”

这一点,pdfcpu 很值得研究。

06 为什么我觉得这个项目值得 Go 程序员看看?

因为很多人提到 Go,想到的还是:

Gin      MySQL     Redis      CRUD      REST API      

但 Go 真正有意思的地方远不止这些,你看 GitHub 上一些经典 Go 项目:

Caddy       Web Server     Prometheus  监控      rclone      文件同步      Hugo        静态网站      PocketBase  后端      pdfcpu      PDF      

它们有一个共同点:都不是简单写几个 API,而是在解决一个明确的问题。

而 pdfcpu 给我的最大感受就是:PDF 这么复杂,Go 一样可以啃下来。

07 一个很有意思的启发

我们平时写项目,很容易把架构想复杂。

一个需求来了:PDF处理

第一反应可能是:

Go服务      +      Python服务      +      第三方PDF服务      +      消息队列      +      对象存储      

但其实也可以先看看:Go + pdfcpu能不能解决?

如果能,那为什么不能先这么做?

这其实也是很多优秀开源项目给我们的启发:

不是所有问题都需要分布式。

不是所有功能都需要一个独立服务。

更不是所有项目都需要一堆基础设施。

最后

我越来越喜欢研究这种 GitHub 上的 Go 项目。

不是为了记住:pdfcpu 有哪些 API

而是想看看:别人是怎么用 Go,把一个看起来很复杂的问题解决掉的。

PDF 只是一个例子。

下一次遇到一个复杂问题,也许可以先问自己一句:

“如果用 Go 自己做,会不会其实没有想象中那么难?”

这可能才是这些开源项目最值得我们学习的地方。

相关学习资料