把 PDF 压到指定大小,为什么比调 Quality 难得多?

0 阅读5分钟

“把这份 PDF 压到 500 KB 以内”听起来像一个质量滑块,其实不是。

对报名系统、邮件附件或企业门户来说,500 KB 不是建议值,而是一条硬约束:文件只要多出 1 个字节,上传依然会失败。于是一个真正可用的“指定大小压缩”需要满足的不是“尽量变小”,而是:

outputBytes <= targetBytes
同时尽量保住可读性,并且不给界面制造假成功

这篇文章复盘一个纯浏览器 PDF 压缩器的实现思路。重点不在于某个 JPEG 参数,而在于如何把“字节上限、页面清晰度、内存、耗时和失败语义”变成一个诚实的产品契约。

目标大小模式:500 KB 上限、精确输入与常用预设

先把问题说对:这是启发式约束搜索,不是一个滑块

普通压缩通常让人选“低 / 中 / 高”。它回答的是“我愿意牺牲多少视觉质量”;但用户常常问的是“这个文件能不能过 500 KB 的上传限制”。

两者差别很大。扫描件、照片型资料、纯文字 PDF 和带图表的报告,对同一组 JPEG 参数的响应完全不同。仅靠「quality = 0.6」无法预测最终 PDF 是 420 KB 还是 860 KB。

更合理的模型是:在渲染比例「scale」与 JPEG 质量「quality」组成的候选空间中,找出已经实际编码完成、体积不超过上限的候选结果。它不是全局最优算法,更不是无损压缩;它只是用有限次尝试,在已采样的候选里尽量靠近上限。

原 PDF
  -> 页面渲染(有分辨率与内存上限)
  -> 可复用的源页图像
  -> 候选 { scale, quality }
  -> 组装 PDF 并测量真实字节数
  -> 最佳达标结果 / 明确失败

最差的实现:每次尝试都重新渲染整份文档

最直觉的循环是:渲染所有页,按某个质量导出 JPEG,重新组 PDF,测字节数;如果超标,再从头来一遍。

它能工作,但会把最贵的工作重复很多次。页数一多,Canvas 的分配、PDF.js 的渲染和 JPEG 编码就会反复占用 CPU 与内存,用户看到的往往只是越来越久的等待。

当前实现先把每页预渲染成受上限约束的高质量源图,再让候选构建复用这些源页。单页源图会限制在 400 万像素,整批缓存受 128 MiB 预算保护。兼容浏览器可使用 Worker、OffscreenCanvas 和 createImageBitmap 将反复缩放与编码移出交互路径;Worker 不可用或缓存不安全时,则退回兼容路径,而不是直接失败。

处理进度:先准备一次源页,再构建候选结果

只相信最终文件的字节数

候选的起点是较高的「scale」与 JPEG 质量。若结果超标,就同时下调两者;若已经找到一个达标候选,就在它与此前超标候选之间做对数空间细化。

这里的“对数”不是为了显得高级,而是因为文件体积对分辨率与质量并不线性。一次尝试从 0.94 降到 0.80,未必只少一点点;对图片型文档,差异可能很大。

best = null

最多尝试 6 次:
  candidate = build(scale, jpegQuality)

  如果 candidate.bytes <= target:
    保留更接近上限的达标候选
    在“达标”和“此前超标”之间细化
  否则:
    同时降低 scale 与 jpegQuality

有 best:允许下载
没有 best:显示未达标状态,不伪装为成功

实现中的最低边界是「scale = 0.35」「quality = 0.25」。一旦已达到目标的 94%,就停止继续压榨细节。这个数字不是“最佳视觉质量”的科学证明,而是有意识的停止条件:少做无收益的重编码,避免用大量时间换来几 KB。

需要提前讲清的代价:视觉保留,不等于语义保留

浏览器端的这条路径是“渲染页面 -> JPEG -> jsPDF 重组”。它尽量维持页面的几何尺寸与肉眼看到的内容,但输出已经是图像型 PDF。

因此,无论是普通压缩还是指定大小模式,只要确实生成了重建后的文件,都可能丢失:

  • 可选择与可搜索的文本;
  • 链接与表单字段;
  • 书签;
  • 无障碍语义;
  • 原生矢量数据。

这不是一个可以藏在“高压缩”标签后面的细节。扫描件、照片型资料也许很适合;需要检索、填写或无障碍访问的合同与表单,则可能应该保留原件。把这句话放在启动按钮旁边,比下载之后再解释更尊重用户。

语义取舍说明:视觉外观保留,不等于可检索文本与链接仍在

失败状态本身也是功能

一个 500 KB 的限制,501 KB 就是失败。更糟的是,工具生成了比原文件还大的“压缩结果”,却依然显示绿色成功。

所以流程还有两个简单但重要的规则:

  1. 原文件本来就在上限内时,保留原件,不做无意义重编码;
  2. 最低安全设置仍然达不到目标时,不创建可下载的 Blob,而是说明目标未达成。

下面这次本地测试使用一份 3 页、图片较多的 PDF:输入 1.09 MB,目标 500 KB,实际完成 473.0 KB,减少 58%。这个数字不是对所有文档的承诺;真正的承诺只有一个:下载按钮出现前,最终编码后的字节数已经被核验。

核验后的结果:1.09 MB 压至 473 KB,低于 500 KB 上限

“指定大小”真正该保证什么

我不认为一个压缩器应该承诺“永远压到你想要的数字”。有些文档在可读性下限以内就是做不到。

它应该承诺的是:

  • 文件内容在浏览器内读取、渲染、编码和下载,不上传到 PDF 处理服务器;
  • 只依据最终 PDF 的真实字节数判断是否达标;
  • 不把超标结果包装成成功;
  • 在无法满足约束时,给人可以据此行动的失败信息。

这比一个漂亮的进度条难很多,但也更接近用户真正想解决的问题。

在线体验可以直接试:把 PDF 压缩到指定大小(浏览器本地处理)