2026/8/30 12:49:49

Presse:用Rust打造本地优先的PDF压缩与合并工具

Presse:用Rust打造本地优先的PDF压缩与合并工具 PDF 处理是我工作中经常绕不开的一环。几年前我还在用在线工具压缩和合并 PDF后来遇到一份涉及敏感信息的合同扫描件需要压缩发出去我在上传前犹豫了很久文件去了哪里服务器上留多久我完全不知道。从那时起我开始认真考虑把 PDF 压缩和合并这类操作放到本地工具里来完成。最近看到一个名为 Presse 的项目标题写着“Show HN: Presse – a Rust CLI tool to compress and merge PDFs”也就是用 Rust 写的一个命令行工具专门做 PDF 压缩和合并。这类工具其实不少见但 Presse 让我想聊的并不是“又多了一个 CLI”而是它背后代表的一个趋势用 Rust 把“小工具”做成本地优先、单文件、可脚本化的基础设施。本文我会从实际使用场景出发聊一聊 PDF 压缩和合并的真正难点以及这类 Rust CLI 工具在落地时值得关注的细节。1. 为什么我不太愿意再把 PDF 处理交给在线工具1.1 在线工具真正让人担心的不是功能而是文件流向PDF 压缩和合并本身不是一个高难度操作但放到在线平台执行问题就多了一层。你需要先把文件上传到别人的服务器压缩或合并完成后再下载回来。整个过程中你看到的是进度条和结果看不到的是文件到底经历了哪些节点、有没有被复制、有没有被用于其他用途。如果你只是处理一份公开的教程 PDF这个风险也许能接受。但如果文件是合同、简历、身份证扫描件、财务报表或者内部研发文档上传第三方平台就需要慎重。作为经常跟技术文档和客户资料打交道的人我的底线是能让文件停留在本地的操作就不要让文件进入网络链路。这不是说所有在线工具都不靠谱而是你没办法在拿到结果的同时验证文件被如何处理。对于合规性要求比较高的团队这甚至是一个需要写进采购评估的风险点。1.2 “装个软件”听起来简单维护起来不简单不用在线工具那用桌面软件行不行。很多 PDF 编辑器功能很全但问题也很具体体积大、安装流程复杂、经常弹更新提示而且大多数完整功能都在付费墙后面。如果你只是“一个月偶尔压缩几次 PDF”专门装一个重型 PDF 套件性价比很低。另一种常见做法是用 Python 脚本配合 PyPDF2、pypdf 或 reportlab 这类库处理 PDF。Python 方案的好处是灵活坏处是环境依赖。换台机器Python 版本不一致、虚拟环境没激活、某个底层依赖编译失败都会影响结果。如果还要在 CI 流程里处理 PDFPython 环境的维护成本就更明显了。这时候Rust CLI 工具的优势就体现出来了编译后的产物是单个可执行文件不需要目标机器上预装运行环境跨平台能力也成熟启动快处理 PDF 这种 IO 密集型任务时性能也足够。Presse 这一类工具的目的就是用一个小体积的原生二进制替代“浏览器上传 网络服务 下载”或者“安装一个好几 GB 的 PDF 套件”的工作流。1.3 CLI 工具最大的价值不是省时间而是可重复从表面看CLI 工具和在线工具都能把 PDF 压缩变小。但两者有一个根本差别CLI 命令是可重复、可脚本化、可进入自动化流程的。在线工具适合“一次性处理”CLI 工具适合“每天都要处理”。比如每周生成一份项目报告 PDF自动压缩后归档。CI 构建产物里的 PDF 文档需要统一合并成一份交付件。批量处理目录下的几十个 PDF每个都做压缩和重命名。这些场景不适合打开网页手动操作。CLI 工具可以把处理过程写成 shell 脚本留存下来下次直接复用。Rust 写的工具在这一点上做得很好因为它没有 Python 那种解释器版本依赖也不像 Node 工具那样会牵扯运行时。你在 CI 容器里放一个二进制直接跑干净利落。这里可以先下一个判断Presse 这类 Rust CLI 工具真正解决的不是“压缩算法多高级”而是把 PDF 处理从一次性的、依赖人力介入的临时操作变成可控、可复用、可追溯的工程流程。这个判断会贯穿全文。2. PDF 压缩这件事到底在压什么2.1 不是“压一遍”就行要理解 PDF 体积从哪里来很多人对 PDF 压缩的理解是“文件大跑一下压缩文件变小”。实际上PDF 压缩并不像 ZIP 压缩那样对整体数据做通用压缩而是需要针对 PDF 内部的不同对象做差异化处理。一份 PDF 的体积通常来自四个方面图片资源扫描件、截图、插入的高分辨率照片是体积大头。字体内嵌字体尤其是中文字体体积可达几 MB 到几十 MB。对象流与结构数据PDF 内部的对象、交叉引用表、增量更新记录。冗余元数据文档属性、缩略图、重复的资源引用。如果一份 PDF 是纯文本没有大图本身已经做过压缩那“压”的空间就很小。如果一份 PDF 是几百页的扫描图像体积大头基本就是图片编码这时候关键是图片重编码参数。2.2 压缩的基本思路分层处理而不是一把梭Rust 生态里处理 PDF普遍会用到类似lopdf、pdfium-render、pdf-writer或mupdf封装库。Presse 这类工具具体用了什么库标题里没说明但从常见实现来看PDF 压缩的完整流程大致是解析 PDF遍历页面和对象。定位图片对象提取像素数据。按目标 DPI 或质量参数重新编码图片。对字体做子集化只保留实际用到的字符。重构页面内容流压缩对象流。重写 PDF 结构生成新的文件。关键在第三步和第四步。图片重编码需要考虑分辨率如果原图是 300 DPI 的扫描件而用途只是屏幕阅读降到 150 DPI 就能明显减重。如果原图包含文字截图降到过低会糊所以质量参数要留给用户控制。字体子集化是另一个常见优化点一份 PDF 内嵌的完整字体文件可能包含几千个字符但文档真正用到只有几十个。子集化之后只保留用到的字形体积能下降很多。但注意子集化如果处理不当会导致某些生僻字或特殊符号显示异常。所以质量优先的场景下子集化不能激进。2.3 有损 vs 无损为什么有些压缩“压不动”压缩 PDF 时一定会碰到“无损”和“有损”的取舍。无损压缩适合处理文字文档、矢量图形和需要保持精确排版的内容。无损方式会保留原始图像和字体数据只是优化内部结构、压缩对象流。它的优点是视觉上没有变化缺点是压缩率有限尤其对图片多的扫描件几乎压不动。有损压缩适合处理扫描件、照片、阅读类文档。常用做法是把图片重新编码为 JPEG 或更高效的格式降低 DPI或者把图片从无损格式转换成有损格式。这样压缩率通常很可观但画质会有损失。所以一个合格的 PDF 压缩 CLI 至少应该提供质量分级选择。比如预设几个档位fast无损为主只做结构优化速度快压缩率低。balanced图片适度重编码保留可接受的清晰度。high高压缩降低 DPI 和质量适合屏幕阅读和存档。Presse 这类工具如果不提供分级选项用户就只能在“压完太大”和“压完太糊”之间反复试体验会差很多。从我个人的使用偏好看一个 CLI 工具如果能提供--quality或--dpi这类显式参数比提供含糊的“压缩级别”更直观。2.4 从 Presse 看压缩参数该怎么设计虽然原项目标题没有列出具体参数但如果你要实际使用或评估一个 Rust PDF 压缩 CLI建议关注这几个点是否支持设置输出 DPI。是否支持控制图片质量百分比。是否跳过已经很小的图片。是否默认删除元数据。是否保留批量压缩的进度输出。是否支持覆盖源文件和输出新文件两种模式。“跳过已经很小的图片”这个点容易被忽略。如果不做尺寸判断把所有图片都重新编码一遍既消耗 CPU还可能让原本清晰的小图变糊。比较好的做法是只对超过阈值的图片重编码小图保持原样。另一个实用细节是输出文件的管理。比如presse input.pdf -o output.pdf明确指定输出路径比直接覆盖输入文件更安全。批量操作时如果输出路径和输入路径相同一旦压缩中途出错源文件可能被破坏。稳妥的做法是先生成临时文件全部成功后再原子替换原文件。注意压缩前先备份原文件永远是好习惯。CLI 工具做得再好也无法完全避免异常断电、磁盘满或输入文件损坏等情况。3. 合并 PDF 也不是把文件拼在一起那么简单3.1 页面合并 vs 文件拼接完全是两回事“合并 PDF”听起来像“把几个文件按顺序拼成一个文件”实现起来也确实可以这么做。但拼完之后你可能会遇到一堆问题每个文件有自己的页面尺寸合并后尺寸不一致打印时页面大小混乱。每个文件有自己的元数据合并后文档标题、作者、书签该以谁为准。内部链接、注释、表单字段在合并后可能失效或错乱。文件里如果有密码保护和加密合并前必须正确解锁否则会失败。页面级的合并不是简单地把字节串起来而是要把每个 PDF 的页面对象、资源对象、内容流都导入新的文档并重新映射对象引用关系。这就是为什么很多 PDF 合并工具在遇到复杂文件时会失败或输出异常。3.2 合并后书签、元数据和页面尺寸怎么处理如果你只需要把几份 PDF 按顺序拼成一个文件然后发给别人看那最简单的合并就够了。但如果你需要合并后的文件保留目录结构、可以在侧边栏跳转就必须关注书签处理。很多 CLI 工具会提供选项保留原有书签。重建顶层书签把每个输入文件作为一个章节。丢弃所有书签。我在实际使用中通常选择“重建顶层书签”也就是把输入文件名作为一级标题合并后生成新的目录结构。这样保留了一部分跳转能力又不会被几十个子书签淹没。元数据的处理也要考虑。合并后的文档标题、作者、创建时间到底采用第一个文件的值、最后一个文件的值还是由用户通过参数指定很多用户不会注意这个细节直到合并后的 PDF 在阅读器里显示一个乱七八糟的标题才发现问题。页面尺寸不一致的问题也很常见。比如 A4 文档和 A3 横版文档合并后阅读器默认按第一页的尺寸显示其他页面可能缩放异常。更好的做法是支持“保持原尺寸”和“统一缩放到指定尺寸”两种模式。默认建议保持原尺寸因为统一缩放可能让原本排版好的内容变形。3.3 输入顺序、异常文件和重复页面合并操作里最容易忽视的是输入顺序的控制。命令行工具一般支持把文件列表传给参数比如presse merge 01-intro.pdf 02-spec.pdf 03-appendix.pdf -o complete.pdf但如果文件名来自通配符展开比如*.pdf排序规则在不同 shell 下可能不一致甚至会混入一些临时文件。稳妥做法是在脚本里显式列出文件顺序或者用ls -1v这类命令先确认排序再传给合并工具。异常文件也需要单独处理。合并过程中如果某个输入文件损坏、加密或不是标准 PDF最好马上停止并给出明确的错误提示而不是默默跳过。因为“跳过了一个文件”这个行为用户如果不仔细看日志很难发现。我曾经遇到过一次批量合并某个文件被 Word 导出的过程中没有正常结束导致 PDF 结构不完整但外壳看起来正常。合并工具跑完没有报错但最终 PDF 的中间有几十页是空白的。后来排查发现是工具在解析失败时选择了继续跳过。从那以后我评估一个合并工具时会特别看重它对异常文件的处理策略是 fail fast还是 skip and continue必须能在日志里明确看到。3.4 合并和压缩组合使用时顺序有讲究Presse 的标题同时提到 compress 和 merge实际操作时会遇到一个顺序问题先压缩再合并还是先合并再压缩如果输入文件多、体积大通常建议先压缩再合并。原因很简单合并操作会重新组织 PDF 结构如果先合并再压缩工具需要重新分析整个大文件中所有对象内存和 CPU 占用都会更高如果先压缩每个文件再合并压缩和合并的边界更清晰出问题也更容易定位。不过先压缩每个文件也有一个代价如果文件之间有重复内容比如多个文档包含同一份附件或同一组图片先压缩再合并不会对这些重复对象做去重。先合并再压缩则可以做到整体优化。所以更优的流程可能是先用无损方式合并。再对合并结果做有损压缩。但如果单个输入文件本身非常大先合并再压缩可能让中间文件峰值体积更大。实际工程上我一般这样决策文件多且各自体积已经很大时先逐份压缩文件少但内容高度重复时先合并再压缩。4. 从最小用例到批处理CLI 工具的工程化思路4.1 先跑通单文件压缩不要急着批量很多人第一次拿到一个 PDF 处理 CLI 工具喜欢立刻找一个几十个 PDF 的目录跑一遍。结果遇到报错因为文件太多根本不知道是哪一个文件触发的错误。更稳的做法永远是先用一个文件跑通最小用例。比如先看看命令长什么样presse compress sample.pdf -o sample-compressed.pdf跑完之后用 PDF 阅读器打开输出文件确认页数、内容和清晰度没有异常。再检查输出文件大小如果压缩率符合预期再考虑跑第二个文件。单文件跑通的意义在于它把问题收敛到一个最小的闭环里输入可预期输出可检查。一旦这一步稳定再扩展文件数量问题面就会窄很多。4.2 验证合并结果不是只看文件存在合并操作的验证比压缩更复杂因为合并后的 PDF 如果页数不对你可以通过文档属性看出来但如果某几页内容错乱单看文件大小是发现不了的。我的验证顺序一般是用pdftk或 PDF 阅读器确认总页数等于各输入文件页数之和。抽查首页、中间页和最后一页的内容是否正确。检查书签是否存在跳转是否正常。检查文件大小是否符合预期。如果输出 PDF 需要进入交付流程我还会用命令行方式再提取一次文本确认关键页面的文字内容没有丢失。这一步可以写成简单的脚本避免每次手动打开阅读器检查。4.3 把命令串成流水线脚本化是 CLI 工具的灵魂CLI 工具真正的威力在于把多个命令串联起来。比如# 逐个压缩目录中的输入文件 for f in drafts/*.pdf; do presse compress $f -o out/$(basename $f) done # 合并成最终交付件 presse merge out/*.pdf -o final-report.pdf这段脚本可以放进每天的定时任务里也可以进入 CI 流程。比起手动打开在线工具上传下载用脚本处理的好处不仅是快更重要的是它把操作固化成了一套可审计的流程。别人拿到这台机器看到脚本和日志就知道 PDF 是从哪些文件生成的用的是哪个版本的工具输出被放在哪里。这里也要提醒一句脚本化的前提是工具提供清晰的退出码和稳定的输出。如果压缩失败时退出码也是 0脚本里的逻辑就会误导你。所以实际使用前建议先测一下错误路径的退出码。命令行工具普遍约定 0 表示成功非 0 表示失败但并不是所有工具都严格遵守需要确认。4.4 日志、输出目录和失败重试是长期使用的关键如果只是偶尔用一次日志可有可无。但如果每周都要处理一批 PDF没有日志就等于没有“记忆”。使用 CLI 工具处理 PDF 时我建议至少做到把命令和参数记录在脚本里。把每次处理的时间、输入文件、输出文件、执行结果写入日志文件。失败时保留现场比如原始文件名和错误码。临时文件和最终输出分开目录存放。输出目录的管理也很重要。如果直接用输入目录作为输出目录要小心覆盖源文件。我一般习惯用out/或processed/子目录放结果保留一个干净的输入区。以后想重跑也能区分哪些文件已经处理过、哪些是新的。失败重试逻辑要看场景。如果处理的是同一批文件偶尔有一个文件因为权限问题失败重跑一次基本能解决。但如果是工具本身对某个边角特性不支持重跑多少次都没用。这时应该把这类文件单独挑出来判断是输入文件的问题还是工具能力边界的问题。4.5 如何验证“压缩后还能用”压缩工具的终极考验是压缩后的 PDF 在别人的机器上、不同的 PDF 阅读器里还能正常打开、正常打印。我自己会做三层验证加载验证用至少两个不同的 PDF 阅读器打开输出文件。内容验证检查关键图片是否清晰得不影响阅读文字是否能选中和复制。打印验证如果文档需要打印使用打印预览功能抽看几页确认 DPI 降低后没有明显发虚。对于图片较多的文档我还会特意找包含小字号文字的那一页检查。因为小字号文字在图像压缩后最容易出现明显模糊如果这一页都能接受其他页面一般没问题。5. 新手最容易踩的坑和排查链路5.1 先给现象分类再动手修使用 Rust CLI 工具处理 PDF报错大多是以下几个类型。排查问题时第一步不是改代码而是先判断问题属于哪一类命令本身报错参数写错、文件路径不对、格式不识别。运行中途报错某个 PDF 文件损坏、权限不足、加密、内存不足。运行成功但输出异常文件大小没变、输出的大小比预想大、页数不对、内容错乱。运行失败但没有明确错误没有日志或者退出码异常。环境相关报错Rust 编译环境、Cargo 依赖下载失败、Windows 下 MSVC 工具链问题。不同的现象对应不同的排查路径。直接去搜一个具体的报错信息往往只能解决眼前问题不能帮你建立系统的判断能力。5.2 按“输入、环境、依赖、参数、工具边界”逐层排查我在处理这类问题时习惯按下面的顺序排查先看输入。文件是不是标准 PDF有没有加密文件路径是否包含特殊字符文件名里的空格、中文、括号都可能导致命令行解析出问题。把文件路径用引号包起来是最基本的防护。再看环境。Rust CLI 工具跨平台能力不错但不同平台对文件路径分隔符、环境变量、权限模型的处理有差异。Windows 上使用 MSVC 工具链编译的程序和 GNU 工具链编译的程序行为也可能不同。如果是在 Windows 上使用先确认工具是哪个工具链编译的遇到过有人在 Windows 下编译时依赖了只在 Unix 上存在的特性导致程序行为异常。再看依赖。工具本身如果依赖外部动态库比如某些 PDF 处理库需要系统安装 C 库那目标机器上就需要补齐这些依赖。使用 Rust 静态链接的好处是能减少这类问题但并不是所有库都支持纯静态链接。再看参数。压缩质量设置得太高或太低出来的结果都会让你觉得“不对劲”。合并时如果排序参数没控制好输出页序自然就是乱的。这类问题通常不是 bug而是参数和预期不匹配。最后看工具边界。工具声称支持 PDF 压缩不代表它能处理所有 PDF 特性。比如包含 JavaScript 的 PDF、包含复杂表单的 PDF、带数字签名的 PDF处理过程中都可能出现异常。如果输入文件触发了工具的能力边界最合理的做法不是硬扛而是换工具或先对文件做预处理。5.3 Rust 使用环境本身的几个常见坑既然聊的是 Rust CLI 工具这里也提一下使用和编译 Rust 工具时经常遇到的环境问题。Rust 安装与工具链问题在“最新网络热词”里也反复出现说明这是很多人实际面临的困扰。常见的包括从源码构建时 Cargo 下载依赖慢尤其在国内网络环境下访问 crates.io 不稳定。Windows 下安装 Rust 时选择 MSVC 工具链要求电脑装有 Visual Studio Build Tools选择 GNU 工具链则要求配置相关依赖不同环境容易踩坑。工具链更新到新版本后某些旧项目可能因为依赖版本不兼容而编译失败。如果你的目标是“使用一个 Rust CLI 工具”而不是“开发一个 Rust 工具”最优路径是直接下载作者发布的预编译二进制而不是从源码编译。如果没有预编译产物再考虑安装 Rust 环境后执行cargo install。安装过程中遇到依赖下载慢可以配置国内镜像源但镜像源的稳定性和安全性需要有取舍和判断。5.4 资源占用与大批量任务的处理处理大型 PDF 时资源占用是另一个容易忽略的问题。PDF 合并不像单纯的文件拼接它需要在内存中维护多个文档的对象映射。几十个大型 PDF 同时合并时内存峰值可能远高于你的预期。如果任务比较大建议分批处理。比如先把 100 个文件分成 4 组每组 25 个先合并组内文件再把 4 个中间文件合并成最终结果。这样能有效降低单次处理的内存压力。磁盘空间也要预留。压缩和合并过程中工具可能生成临时文件输出文件在生成过程中也可能占用比最终文件大得多的空间。如果磁盘满了工具可能报一个莫名其妙的错误。所以大批量处理前检查一下输出目录所在磁盘的剩余空间是值得的。6. 我的判断Presse 这类 CLI 工具适合谁不适合谁6.1 适合开发者和自动化场景Presse 这类 Rust CLI 工具最适合的人群是有命令行使用基础、需要把 PDF 处理融入自动化流程的开发者。典型场景包括文档系统每天自动生成 PDF需要压缩后归档。CI 中把多份测试报告合并成一份交付文件。批量处理导出的大量数据报表。本地运行不需要把文件上传到任何服务器。在这些场景里CLI 工具的结构化输入输出、退出码和脚本兼容性远比图形界面或在线工具的点击体验重要。6.2 和普通用户的距离感说实话这类 CLI 工具不太适合完全不懂命令行的普通用户。普通用户需要的不是一个命令而是一个“界面”拖拽文件到窗口设置压缩档位点击导出。CLI 工具对它们的使用门槛仍然太高。所以如果你是团队里的“技术接口人”可以考虑把 Presse 这类 CLI 包装成一个简单的脚本或一个小的图形界面。比如提供一个双击运行的.bat或.sh脚本用户只需要把 PDF 放进特定文件夹脚本自动处理并把结果输出到另一个文件夹。这样既保留了 CLI 工具的效率又降低了普通用户的使用门槛。6.3 要不要自己写一个 Rust PDF 工具看到 Presse 这样的项目很多 Rust 学习者会冒出“我自己也写一个”的念头。我的建议是如果目的是练习 Rust这个题目很适合如果目的是解决日常问题先直接用现成工具不要把“造轮子”作为首要目标。自己写 PDF 压缩合并工具真正困难的部分不在 CLI 参数解析而在 PDF 格式的兼容性。PDF 是一个已经存在了几十年的格式包含大量历史特性、边缘情况和兼容性要求。你写的工具能处理 90% 的普通文件但不代表它能处理剩下 10% 的复杂文件。这 10% 的兼容性工作可能需要非常长的时间投入。如果决定自己写我建议先定一个很小的范围只处理本地产的、非加密的 PDF。先实现合并再考虑压缩。压缩先只做图片重编码不做完整字体子集化。为每个阶段准备测试样例记录失败案例。范围越小越能保证完成度。一开始就追求“像 Presse 一样支持所有 PDF 功能”很容易陷入 PDF 格式的沼泽。6.4 从“试用”到“长期使用”要补的几块拼图如果你决定把某个 Rust PDF CLI 工具放进日常工作流在完全依赖它之前建议补上这些检查版本锁定记录工具版本避免升级后行为变化导致结果不一致。回归样例准备一份标准测试 PDF 集每次升级后跑一遍确认关键功能没有退化。输出检查脚本里增加对输出文件大小、页数的自动检查。失败告警批量任务失败时能否通过日志、邮件或企业聊天机器人通知到人。备份策略压缩和合并的过程尽量不改原始文件保留一份输入文件的备份。只有补齐这些工程化能力一个“小工具”才能变成“生产工具”。否则它可能只是你下载列表里的一个二进制偶尔用一次遇到问题就换下一个。回到最初的话题。Presse 作为“Show HN”上的一个开源项目它的价值不一定在于功能有多惊艳。真正的价值在于它用 Rust 证明了 PDF 压缩和合并这类“无聊但高频”的任务可以做到本地优先、单文件分发、可脚本化。这种能力对于保护文档隐私、简化自动化流程、降低工具维护成本都有直接帮助。如果你正考虑把这类的 CLI 工具引入自己的流程我的建议很简单先拿一个不重要的文件跑通压缩和合并验证输出效果再写一个最小脚本覆盖日常操作最后再逐步补上日志、备份和检查步骤。PDF 处理不是一朝一夕的需求值得用工程化的方式对待。