ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

华为云码道从 0 到 1 开发跨端 PDF 保功能压缩工具

2026/10/2 7:16:43 拓冰建站 浏览量
华为云码道从 0 到 1 开发跨端 PDF 保功能压缩工具 用华为云码道从 0 到 1 开发跨端 PDF 保功能压缩工具7 个压缩原语、4 档预设与 6 个真实踩坑项目pdfintact保功能 PDF 压缩工具环境Windows 11、Python 3.12.10、Node.js 22后端包管理uv前端包管理pnpm开源仓库AtomGithttps://atomgit.com/CYXue/pdfintact文中压缩数据由本机脚本实测复现方法见文末。0. 先看结果我用华为云码道 CodeArts从 0 到 1 开发了一个 PDF 工具pdfintact。它以本地服务为核心提供浏览器前端和命令行两种入口PDF 在本机处理不需要上传到云端。它的目标不只是压得更小而是尽量做到压缩后仍然可用无损、均衡、极限三档保留文本、书签和搜索能力超极限档通过整页栅格化换取更高压缩率同时明确告知用户功能损失。压缩完成后工具还会生成一份功能变更清单逐项检查文字可选中复制、书签可跳转、文字显示正常、全文可搜索、图像质量共 5 个维度。一份 70 页的中文教材从21.57 MB 压到 5.41 MB压缩率 74.90%。不过最激进的档位并不总是最有效文本和矢量内容为主的 PDF整页栅格化后反而可能大幅膨胀。这也是为什么工具既要报告压缩率也要说明压缩前后的功能变化。1. 为什么需要保功能压缩项目的起点是一批体积较大的 PDF 资料。在线压缩虽然方便但敏感文件需要上传一些离线工具压完后又可能出现书签失效、文本变成图片、无法复制搜索或者字体显示异常等问题。PDF 体积大并不总是因为页面内容本身太多。常见原因还包括嵌入了完整字体、存在重复对象流、保存了冗余元数据或包含高分辨率图像。其中一部分可以通过无损清理减小体积另一些操作例如整页栅格化、移除嵌入字体或降低图像质量则会带来功能或画质上的取舍。因此pdfintact的产品目标被定义为尽量保留文本可选、可搜索和书签可跳转等能力全程在本机处理 PDF避免把文件上传到云端对有损或可能破坏功能的操作明确提示压缩完成后提供功能检查结果而不只展示一个压缩率。竞品调研重点对比了在线工具、桌面软件、Ghostscript 和商业 PDF 软件。基于本轮调研材料压缩后逐项报告功能变化是pdfintact希望突出的差异点具体产品能力仍应以各软件当前版本和实际测试为准。能力维度在线压缩工具Ghostscript商业 PDF 软件pdfintact本地离线处理通常需要上传支持通常支持支持压缩后功能检查清单调研中未见普遍提供未见统一报告视产品而定提供5 项原语级自定义少见部分支持视产品而定支持CLI / HTTP / 浏览器前端通常以网页为主CLI通常以 GUI 为主三种入口共用同一核心逻辑目标体积压缩少见可通过参数实现视产品而定支持质量参数二分搜索逼近表 1能力维度概览依据项目调研材料整理不代表对所有产品版本的穷尽测试。2. 先定边界再搭工程这次开发没有从界面或某个压缩算法开始而是先逐步确定技术方案、编程语言、框架与库、架构模式。最终方案如下形态本地 Python 服务 浏览器前端 CLI不上传待处理 PDF后端Python 3.12PDF 与图像处理pikepdf、fontTools、Pillow、pypdfium2、pypdf服务与命令行FastAPI UvicornTyper前端Vue 3.5 Vite 8 TypeScript Naive UI 2.45 Pinia 4 vue-router 4架构分层与端口适配器核心业务不依赖 HTTP 或 CLI 协议。项目把压缩、页面操作和历史记录放在src/pdfintact/核心包中src/app/是适配层其中app/http.py创建 FastAPI 实例并注册路由app/cli.py提供命令行入口app/__main__.py是服务启动器。这样核心逻辑可以独立测试新增入口时也不必复制压缩实现。浏览器 ──HTTP──▶ app/http.pyFastAPI 适配层 ─┐ ├──▶ pdfintact/核心逻辑零协议依赖 命令行 ────────▶ app/cli.pyTyper 适配层 ────┘核心层有一条明确边界pdfintact/内禁止 import fastapi / typer / uvicorn / pydantic。PDF 领域逻辑留在 core 中协议和入口细节由适配层处理。后端依赖以 Python 包为主前后端分别使用uv和pnpm管理依赖并提交锁文件以便复现。下方为项目依赖配置节选# pyproject.toml节选 dependencies [ pikepdf8.0, fonttools4.40, Pillow10.0, pypdfium24.0, pypdf4.0, fastapi0.141.1, uvicorn0.53.0, typer0.27.2, python-multipart0.0.32, pydantic2.0, ]2.1 码道带来的开发体感这个项目横跨 Python 后端、Vue 前端、CLI、打包和 PDF 领域知识冷启动时需要同时搭出多个模块还要准备设计文档和测试用例。开发过程中的时间估算如下环节纯手写估时码道辅助后体感提效架构骨架与多模块脚手架4 小时0.5 小时约 8 倍竞品调研报告4 篇2 天0.5 天起草后校订约 4 倍测试脚手架10 个文件3 小时0.5 小时约 6 倍核心算法实现1 天1 天人工主导约 1 倍这些是项目过程中的估算不是严格的对照实验。它反映出的体感是AI 对脚手架、资料初稿和边界用例起草帮助较大压缩策略是否正确、哪些功能必须保留仍需要开发者定义并通过真实 PDF 验证。3. 压缩核心七个原语一条带护栏的管线pdfintact将压缩拆成7 个可以独立启用的原语。预设档位是这些原语及其参数的组合而不是四套互不相干的实现。原语风险主要作用适用场景与注意事项结构重压structureL重压对象流、优化内部结构通用现代 PDF 的收益可能较小重复流去重dedup_streamsL相同对象流复用引用含重复图像或对象的文件元数据清理clean_metadataL清理冗余元数据和缩略图通用通常只减少少量体积字体子集化font_subsetM仅保留文档实际使用的字形嵌入完整中文字体时可能收益明显图像重编码imageM降 DPI JPEG quality 重编码q≤30 时进入极端降质模式扫描件、教材等图像较多的 PDF整页栅格化rasterizeH将整页渲染成图像适合图像占比较高的文件文本层会丢失文本型 PDF 可能膨胀字体去嵌入deembed_fontsH移除嵌入字体依赖阅读器字体回退仅在确认目标设备有合适字体时考虑表 2七个压缩原语及风险提示。L / M / H 表示项目中的低、中、高风险等级。原语显示顺序按风险 L→M→H 排列UI 层执行顺序按语义依赖排列管线层两者故意独立。3.1 四档预设是参数组合预设把不同原语和参数组合起来便于用户快速选择也保留了自定义原语勾选能力。# src/pdfintact/compress/presets.py节选LOSSLESSPreset(name无损,structureTrue,font_subsetFalse,imageImageParams(enabledFalse),clean_metadataTrue,)BALANCEDPreset(name均衡,structureTrue,font_subsetTrue,imageImageParams(enabledTrue,target_dpi150,jpeg_quality80),clean_metadataTrue,)EXTREMEPreset(name极限,structureTrue,font_subsetTrue,imageImageParams(enabledTrue,target_dpi72,jpeg_quality55),clean_metadataTrue,)HYPERPreset(name超极限,structureTrue,font_subsetFalse,imageImageParams(enabledFalse),# 栅格化会重新渲染整页rasterizeTrue,raster_dpi126,raster_quality55,clean_metadataTrue,)档位结构重压重复流去重元数据清理字体子集化图像处理整页栅格化无损开开开关关关均衡开开开开150 DPI / q80关极限开开开开72 DPI / q55关超极限开开开关关126 DPI / q55表 3四档预设的原语开关。三个 L 级原语始终开启。具体功能保留情况以压缩后的检查结果为准。3.2 管线护栏与互斥规则管线统一编排原语并在执行前后检查文件体积。若普通原语没有减小体积就回退到上一步如果某个原语失败则将原因写入警告避免单步失败悄悄变成压缩成功。栅格化会覆盖文本、字体或图像处理的中间结果因此管线会跳过互相冲突的步骤并报告原因。# src/pdfintact/compress/pipeline.py节选_H_PRIMITIVES{rasterize,deembed_fonts}_RASTERIZE_CONFLICTS{font_subset,image,deembed_fonts}_DEEMBED_CONFLICTS{font_subset}forpinself.primitives:ifnotp.enabled(ctx.preset):continueifrasterize_onandp.namein_RASTERIZE_CONFLICTS:ctx.report.warnings.append(f{p.name}: 因 rasterize 已启用成果会被丢弃跳过)continueifdeembed_onandp.namein_DEEMBED_CONFLICTS:ctx.report.warnings.append(f{p.name}: 因 deembed_fonts 已启用成果会被丢弃跳过)continuebefore_pathctx.current_path before_sizebefore_path.stat().st_sizeifbefore_path.exists()else0try:p.run(ctx)exceptExceptionaserror:ctx.report.warnings.append(f{p.name}:{error})continueafter_pathctx.current_path after_sizeafter_path.stat().st_sizeifafter_path.exists()else0skip_guardp.namein_H_PRIMITIVESifp.nameimage:img_paramsgetattr(ctx.preset,image,None)ifimg_paramsandimg_params.jpeg_quality30:skip_guardTrue# 极端降质模式跳过体积护栏ifafter_path!before_pathandafter_sizebefore_sizeandnotskip_guard:ctx.current_pathbefore_path ctx.report.warnings.append(f{p.name}: 处理后体积未减小跳过该步结果)continuectx.report.primitives_run.append(p.name)管线中有两层互斥规则栅格化与内容原语互斥rasterize开启时font_subset、image、deembed_fonts成果会被整页位图覆盖直接跳过并告警。字体去嵌入与字体子集化互斥deembed_fonts移除嵌入字体后font_subset无字体可操作反之亦然。体积护栏也很重要已经高度压缩的 JPEG 再编码一次结果可能更大对这类普通压缩步骤保留上一步通常更合理。3.3 压缩后的功能自检与目标体积压缩结束后verify.py会比较原 PDF 与输出文件检查 5 个维度文字可选中复制是否仍可提取文本pypdf 提取检测书签可跳转/Outlines 书签树节点数是否保留pikepdf 递归计数文字显示正常是否去嵌入字体去嵌入则标 at_risk全文可搜索文本层是否存在图像质量是否启用了图像重编码启用则标 at_risk。检查结果汇总为功能变更清单随压缩报告返回。对于整页栅格化等有意牺牲文本层的操作清单会明确标示相关功能已放弃abandoned而不是将其包装成无损压缩。此外工具支持指定目标体积。target_solver.py通过二分搜索调整 JPEG quality多次运行图像重编码以逼近目标字节数目标是否可达取决于 PDF 内容和其他约束因此这是逼近而非绝对保证。4. 三种入口共用一套核心前端使用 Vue 3.5、Naive UI 2.45 和 Pinia 4主要面板覆盖压缩、页面操作和历史记录。开发时用pnpm dev热更新生产构建输出到frontend/dist/由 FastAPI 静态文件中间件提供。后端的app/http.py是 FastAPI 薄层路由按域拆为app/routes/compress.py、app/routes/pages.py、app/routes/history.py共16 个 API 端点压缩域 4 个GET /api/presets、POST /api/compress、POST /api/compress/batch、GET /api/download/{output_id}页面操作域 10 个合并、拆分、旋转、提取、删除、N-up 拼版、小册子、水印、水印删除、页码历史域 2 个GET /api/history、DELETE /api/history。上传会限制文件大小并检查%PDF-文件头产物采用 TTL 清理策略避免临时输出长期堆积。CLI 由 Typer 提供覆盖压缩、目标体积、批量任务、页面操作、历史记录和预设查询。CLI 与 HTTP 调用同一套 core 逻辑包括预设和原语选择的解析不维护两份压缩实现。defparse_selection_json(raw:str|None)-PrimitivesSelection|None:解析 CLI / HTTP 共用的原语勾选清单。importjsonifnotraw:returnNonereturnPrimitivesSelection(**json.loads(raw))具体使用时HTTP、CLI 和浏览器前端分别承担适合自己的交互方式PDF 处理规则仍收敛在核心层。5. 实测压缩率之外也看功能是否保留项目使用tests/fixtures/中的 5 份 PDF 测试四个档位记录压缩体积、耗时和功能变化。下面先看 70 页中文教材的结果档位原体积压缩后压缩率耗时文本可选书签可跳全文搜索无损21.57 MB21.42 MB0.70%2.73 秒保留保留保留均衡21.57 MB20.96 MB2.82%5.91 秒保留保留保留极限21.57 MB17.26 MB19.99%6.05 秒保留保留保留超极限21.57 MB5.41 MB74.90%3.78 秒放弃放弃放弃表 470 页中文教材四档实测数据记录于 2026-09-28。超极限档通过栅格化换取体积下降因此文本、书签和搜索不再保留。将测试扩展到 5 份语料后结果如下语料页数无损均衡极限超极限中文教材图像型700.70%2.82%19.99%74.90%纯文本pdf80.77%16.19%28.69%23.52%上海交大生存手册pdf1247.05%7.05%7.05%-804.30%海康威视校招 QA44.17%8.42%40.93%5.09%七上数学76.52%6.52%6.52%-341.04%表 55 份语料各档位实测压缩率。负值表示输出文件比原文件更大。负值最能说明问题《上海交通大学生存手册》从 2.28 MB 增至 20.64 MB膨胀 804.30%《七上数学》从 0.17 MB 增至 0.75 MB膨胀 341.04%。这两份内容以文本或矢量页面为主整页栅格化把紧凑的文字和矢量对象转换为整页位图页面信息反而需要更多空间。所以档位更激进不代表结果一定更小。在图像占比较高的文档上栅格化可能非常有效在文本型 PDF 上则可能适得其反。预设、警告和压缩后自检需要配合使用不能只看档位名称判断结果。6. 六个真实踩坑坑 1DCTDecode 图像被静默跳过现象某些 PDF 的压缩率长期停在个位数没有明显报错。原因PDF 中常见的 JPEG 图像使用DCTDecode。直接调用pikepdf的read_bytes()读取这类不可过滤流会失败如果异常被宽泛捕获后静默跳过大批图像就完全没有进入重编码流程。处理按/Filter分流。JPEG 流通过read_raw_bytes()读取原始数据再交给 Pillow 解码其他可解码流走read_bytes()路径。两个图像原语共用解码逻辑并为失败路径补充日志。修复后图像处理才实际覆盖了原先遗漏的 JPEG 场景。坑 2CCITT G4 参数不完整导致花屏现象输出 PDF 打开后出现花屏或整页发黑。原因CCITT G4 图像流的/DecodeParms缺少/Columns、/Rows等参数黑白极性也可能配置错误此外Pillow 的 TIFF strip 数据不能直接当作 PDF 图像流使用。处理当前实现对二值图像采用 FlateDecodezlib 压缩位图路径避免不匹配的 G4 参数造成阅读器解码异常。坑 3Windows 临时字体文件被占用现象清理临时.ttf时出现WinError 32更隐蔽的是清理阶段的异常可能覆盖前面已经成功的子集化结果。原因fontTools的TTFont默认懒加载可能继续持有临时文件句柄如果finally中的清理异常没有处理就会影响函数最终结果。处理使用lazyFalse及时读入字体完成后显式关闭句柄临时文件删除失败时记录日志不让清理异常覆盖主要处理结果。fontsubset.load_font(str(in_path),options,lazyFalse)try:subsubset.Subsetter(options)sub.populate(unicodesunicodes)sub.subset(font)subset.save_font(font,str(out_path),options)resultout_path.read_bytes()finally:try:font.close()exceptException:log.debug(字体句柄关闭失败,exc_infoTrue)fortemporary_pathin(in_path,out_path):try:temporary_path.unlink(missing_okTrue)exceptOSError:log.debug(临时字体文件清理失败: %s,temporary_path)坑 4PyInstaller 下__file__指向临时解包目录现象开发环境运行正常打包后日志和持久化资源路径混乱。原因PyInstaller 运行时会将部分资源解压到_MEIxxxxxx临时目录模块的__file__可能指向该目录。写入其中的日志可能在程序退出后消失。处理区分打包与开发模式。打包时用sys.executable所在目录定位应用持久化文件开发时再使用项目目录。路径规则集中放在配置模块中管理。坑 5静默打包后浏览器没有启动现象设置consoleFalse后服务已经启动但浏览器没有弹出也没有控制台可供排查。原因静默模式下webbrowser.open或os.startfile可能无法按预期打开浏览器如果没有日志启动失败就很难定位。处理为浏览器启动增加多策略回退并将启动状态和错误写入日志。这样即使自动打开失败也能确认服务是否启动以及失败发生在哪一步。坑 6整页栅格化让文本型 PDF 体积暴涨现象小体积的文本或矢量 PDF 经超极限压缩后输出反而变大数倍。原因栅格化将每一页渲染成位图。对于原本由紧凑文字和矢量对象构成的页面位图表示可能远大于原始内容。处理与结论栅格化保持为高风险的可选操作压缩后检查实际文件大小并显示功能变化。后续还应在服务端增加压缩前风险探测提醒用户文本型 PDF 可能不适合该档位。其他工程问题开发期间还处理了几类常见问题图像处理中的静默except补充诊断日志清理 Vue 脚手架残留下载产物由模糊 glob 匹配改为精确后缀匹配通过元数据保存上传文件原名版本号从硬编码改为读取包元数据统一开发与打包模式下的日志路径。这些问题背后的共同教训是捕获异常不等于处理异常。失败若没有日志、返回状态或测试用户看到的往往只是压缩效果不好真正原因却被藏起来。7. 从能跑到能交付7.1 测试与覆盖项目包含 10 个测试文件、225 个测试用例覆盖压缩预设、字体子集化、图像重编码、PDF 边界、页面操作、批量任务、历史记录和安全边界。项目记录的测试结果为225 passed in 15.44s TOTAL 2399 1714 29%其中字体和图像处理用例较密集因为这两个领域涉及空字体、无效字节、异常图像流和写回失败等多种边界情况。测试数字描述的是项目当时的运行结果发布前建议在最终代码版本上重新执行并更新。7.2 输入校验、产物清理与打包HTTP 上传接口会限制文件大小并检查%PDF-文件头减少非 PDF 文件进入处理流程的情况。临时产物按 TTL 策略清理下载时使用保存的原始文件名元数据恢复可读名称。项目还准备了 Docker 配置、CI 工作流和 PyInstaller 打包配置。PyInstaller 可生成pdfintact.exe目标是让没有 Python 环境的用户也能启动本地服务。开发环境启动方式如下uvsynccdfrontendpnpminstallpnpmbuildcd..uv run python-mappCLI 示例# 均衡档压缩uv run python-mapp.cli compress in.pdf-oout.pdf-p均衡# 指定目标体积字节uv run python-mapp.cli compress in.pdf-oout.pdf-t1048576# 批量压缩uv run python-mapp.cli compress a.pdf b.pdf c.pdf-ooutdir/-p极限--concurrency47.3 文档与调研项目开发过程中沉淀了竞品调研、压缩策略、原语设计和工程实现等专题文档。调研帮助梳理了现有工具的能力边界最终产品的差异化重点则落在压缩结果之外再检查并报告功能变化这一工作流上。8. 复盘与后续计划回看整个开发过程AI 的帮助并不平均架构骨架、脚手架、调研初稿和测试边界用例适合由 AI 加速起步核心算法、互斥关系和功能保留标准则需要人来定义并通过真实 PDF、日志和测试验证。真正从能运行走到可靠仍离不开对业务正确性的持续检查。目前仍有一个明确的改进项服务端尚未针对超极限 文本型 PDF建立硬性风险拦截。下一步可以在压缩前估算页面的文本与图像占比对反增风险给出警告或建议用户改用其他档位。后续计划包括增加 JBIG2、MRC 等图像压缩方案增加文本 / 图像占比探测与反增风险提示支持 PDF/A 归档格式完善前端拖拽排序和批量进度展示探索 ArkTS ArkWeb 形态的鸿蒙端。鸿蒙端目前仍是计划尚未实现。对这个项目来说最重要的实测结果不只有 74.90%也包括 -804.30%。前者说明特定图像型 PDF 可以显著减小后者提醒我们压缩策略必须结合文档内容并且要把用户失去的功能如实说明。复现方式uvsynccdfrontendpnpminstallpnpmbuildcd..uv run python-mappAtomGit 仓库https://atomgit.com/CYXue/pdfintact如果你也在做 PDF 工具或关注 AI 辅助全栈开发中的工程化实践欢迎在 AtomGit 上交流。