“把这份 PDF 压到 500 KB 以内”听起来像一个质量滑块,其实不是。
对报名系统、邮件附件或企业门户来说,500 KB 不是建议值,而是一条硬约束:文件只要多出 1 个字节,上传依然会失败。于是一个真正可用的“指定大小压缩”需要满足的不是“尽量变小”,而是:
outputBytes <= targetBytes
同时尽量保住可读性,并且不给界面制造假成功
这篇文章复盘一个纯浏览器 PDF 压缩器的实现思路。重点不在于某个 JPEG 参数,而在于如何把“字节上限、页面清晰度、内存、耗时和失败语义”变成一个诚实的产品契约。
先把问题说对:这是启发式约束搜索,不是一个滑块
普通压缩通常让人选“低 / 中 / 高”。它回答的是“我愿意牺牲多少视觉质量”;但用户常常问的是“这个文件能不能过 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 就是失败。更糟的是,工具生成了比原文件还大的“压缩结果”,却依然显示绿色成功。
所以流程还有两个简单但重要的规则:
- 原文件本来就在上限内时,保留原件,不做无意义重编码;
- 最低安全设置仍然达不到目标时,不创建可下载的 Blob,而是说明目标未达成。
下面这次本地测试使用一份 3 页、图片较多的 PDF:输入 1.09 MB,目标 500 KB,实际完成 473.0 KB,减少 58%。这个数字不是对所有文档的承诺;真正的承诺只有一个:下载按钮出现前,最终编码后的字节数已经被核验。
“指定大小”真正该保证什么
我不认为一个压缩器应该承诺“永远压到你想要的数字”。有些文档在可读性下限以内就是做不到。
它应该承诺的是:
- 文件内容在浏览器内读取、渲染、编码和下载,不上传到 PDF 处理服务器;
- 只依据最终 PDF 的真实字节数判断是否达标;
- 不把超标结果包装成成功;
- 在无法满足约束时,给人可以据此行动的失败信息。
这比一个漂亮的进度条难很多,但也更接近用户真正想解决的问题。
在线体验可以直接试:把 PDF 压缩到指定大小(浏览器本地处理)。