news 2026/10/16 1:54:10

MooTool next-compose F24 PDF 验收记录:基于 Apache PDFBox 3.0.4 的 PDF 拆分/合并对齐实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MooTool next-compose F24 PDF 验收记录:基于 Apache PDFBox 3.0.4 的 PDF 拆分/合并对齐实现
  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载

本篇基于 MooTool 仓库next-compose产品线(Jetpack Compose Multiplatform 桌面版)F24 PDF 功能的本轮验收记录 results.md 展开,说明在 JDK 21 环境下执行./gradlew :composeApp:desktopTest全量 87/87 通过的验收口径、拆分/合并的页码语义与_split.pdf输出规则,并结合同仓库 Electron 参考实现(pdf-lib)与 Compose 侧PdfEngine源码,讲清 PDFBoximportPage的页级复制原理、加密 PDF 的失败语义、取消时的半成品清理机制,以及本轮仍未覆盖的测试边界(窗口截图、加密样本走查、安装镜像等)。

一、验收记录原文口径:F24 是什么、本轮做了什么

验收记录位于 next-compose/docs/evidence/2026-09-09-f24/results.md,完整要素如下:

记录项内容
阶段/条目F24 PDF(P4 媒体/加密组)
本产品工作树仅next-compose/目录(Compose 产品线独立开发,不改动 Electron 或 Java 产品线)
依赖Apache PDFBox3.0.4(版本固化在 next-compose/gradle/libs.versions.toml:pdfbox = "3.0.4")
参考基线Electron 版 pdfService.ts、pageRanges.ts、PdfTool.tsx
已执行JAVA_HOME指向 Zulu 21,./gradlew :composeApp:desktopTest87/87通过
语义覆盖拆分/合并 Tab、最多 20 项、奇偶/自定义页码、源顺序去重、_split.pdf后缀、合并保存对话框、历史与分离窗口、取消删除半成品
关键差异DIFF-012(PDFBoximportPage对齐pdf-libcopyPages,见 diff/012-pdfbox-import-page.md)
未测项窗口截图、加密样本(手工走查)、表单/书签/签名(安装镜像内)、安装镜像
下一轮P4 剩余 F23 图片助手

在 next-compose/docs/feature-parity.md 的功能清单中,F24 登记为pdf工具(pdf/PdfTool.tsx的 Compose 对齐项),其验收状态与差异引用(DIFF-012、DIFF-032、DIFF-048)均指向本记录所在的 PDF 拆分/合并能力。

二、参考基线:Electron 版用 pdf-lib 做了什么

Compose 产品线的目标不是重写功能,而是与 Electron 产品线做行为对齐(parity)。Electron 侧的实现是两条短链:

  1. 页码解析在 next/src/shared/utils/pageRanges.ts:
export function parsePageSelection(expression: string, maxPage: number): number[] { if (!Number.isSafeInteger(maxPage) || maxPage < 1) throw new Error('PDF has no pages') const value = expression.trim().replace(/[,,]/g, ';').replace(/\s+/g, '') if (!value || !/^\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*$/.test(value)) throw new Error('Use page ranges such as 1-5;8;10-12') const pages = new Set<number>() for (const token of value.split(';')) { const [startText, endText = startText] = token.split('-') const start = Number(startText) const end = Number(endText) if (start < 1 || end < start || end > maxPage) throw new Error(`Page range must stay between 1 and ${maxPage}`) for (let page = start; page <= end; page += 1) pages.add(page) } return [...pages] }

要点:逗号(含中文逗号,)统一转分号、剔除空白后用^\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*$校验;用Set按 token 出现顺序去重,返回 1 基页码列表。selectSplitPages再在其上叠加odd/even/custom(自定义规则与候选页求交集,且保留候选顺序)。

  1. 页面操作在 next/electron/main/pdfService.ts:mergePdfs要求至少 2 个源;splitPdfs对每个任务读取字节 →PDFDocument.load→selectSplitPages选页 →output.copyPages(source, pages.map((p) => p - 1))→addPage,输出路径固定为源目录下的`${parsed.name.toLowerCase()}_split.pdf`(小写主名 +_split.pdf),已存在则覆盖。加密 PDF 未支持。

三、Compose 侧实现:PdfEngine 的页级复制与错误语义

Compose 产品线把上述逻辑落在 next-compose/composeApp/src/desktopMain/kotlin/com/rememberber/mootool/next/compose/domain/PdfEngine.kt(对象PdfEngine,依赖org.apache.pdfbox:pdfbox:3.0.4,在 composeApp/build.gradle.kts 的 desktopMain 源集中以implementation(libs.pdfbox)引入)。

3.1 页码解析与奇偶/自定义规则(对齐 pageRanges.ts)

parsePageSelection(expression, maxPage)与 Electron 版逐条对齐:maxPage < 1抛no-pages;trim后把[,,]替换为;、剔除空白;同一套正则校验,非法抛invalid-range(错误信息同为Use page ranges such as 1-5;8;10-12);用LinkedHashSet展开区间并保持源顺序去重,越界(start < 1 || end < start || end > maxPage)抛invalid-range。

selectSplitPages(pageRange, rule, customRule, maxPage)对PdfSplitRule.Odd/Even做奇偶过滤,Custom则把自定义表达式解析成集合后取交集——与 pageRanges.ts 中selectSplitPages的语义完全一致。

3.2 拆分:_split.pdf命名与 20 项上限

split(tasks, cancelled)的约束与行为:

  • 空任务列表抛empty-selection;tasks.size > MAX_TASKS(常量MAX_TASKS = 20)抛too-many(错误信息At most 20 PDF tasks);
  • 每个任务内先open(sourcePath)(内部ensurePdf校验存在性与.pdf扩展名,再Loader.loadPDF),按selectSplitPages选页,空选择抛empty-pages;
  • 输出路径由splitOutputPath(source)生成:source.resolveSibling("${source.nameWithoutExtension.lowercase()}_split.pdf")——即源目录、小写主名 +_split.pdf,与 ElectronpdfService.ts第 44 行的join(dirname(task.path), `${parsed.name.toLowerCase()}_split.pdf`)一致;源文件叫Quarterly.PDF时输出为quarterly_split.pdf;
  • 每页写入前与写完后都会检查cancelled(),命中即抛cancelled。

页级复制的核心是writePages:新建空文档,subset.importPage(source.getPage(page - 1))逐页导入,PDFMergerUtility().appendDocument(destination, subset)合并后保存。importPage是 PDFBox 的页面对象复制 API,直接搬移 PDF 内部的页面对象(内容流、资源引用),而不是把页面渲染成位图再重新造一个 PDF——这正是 DIFF-012 中「保留页面内容流,满足真 PDF 页面操作」的实现依据,也对应 Electron 侧pdf-libcopyPages的等价语义。

3.3 合并:至少 2 个源、按源顺序追加

merge(sources, outputPath, cancelled):少于 2 个源抛need-two,超过 20 个抛too-many;对每个源解析各自页码表达式、importPage到临时子集文档、appendDocument追加到目标文档;最终pageCount == 0抛empty-pages;保存前Files.createDirectories补齐父目录,destination.save落盘。合并入口由 UI 弹出保存对话框指定输出(验收记录中的「合并保存对话框」,Electron 侧默认桌面merge.pdf)。

3.4 三个值得注意的纵深细节

  • 加密 PDF 的稳定错误码:open()捕获 PDFBox 的InvalidPasswordException并转成PdfException("encrypted", "Encrypted PDF files are not supported"),其他读取失败统一归一为invalid。这与 DIFF-012 声明的「加密 PDF 以InvalidPasswordException明确失败」一致,且错误码稳定,便于 UI 以 toast 形式提示而非栈溢出信息。
  • 取消删除半成品:split的 catch 块中,若异常是code == "cancelled",会Files.deleteIfExists删除本批已经写出的全部输出文件再向外抛出——即验收记录中的「取消删除半成品」;测试侧用AtomicBoolean(true)模拟取消,断言目标文件不存在(见 4.1 节第 6 条)。
  • 页级表单修复repairImportedInteractiveForms:importPage复制的页面携带 AcroForm widget 标注后,PDFBox 需要重新挂接孤儿 widget 与默认值。源码对每个子集文档与最终文档执行AcroFormOrphanWidgetsProcessor(document).process()+AcroFormDefaultFixup(document).apply(),保证拆分/合并后表单字段、签名字段计数可被inspect正确统计。同时ELECTRON_PARITY_OUTLINES_NOT_COPIED = true显式声明:文档大纲/书签不被复制,与 Electronpdf-libcopyPages行为一致——这是刻意为之的 parity 取舍,而非能力缺失。

inspect(path)还会返回结构统计:页数、表单字段数(acroForm.fields)、书签数(递归遍历documentOutline)、签名字段数(PDSignatureField计数),hasSpecialObjects供 UI 在含表单/书签/签名时给出提示。

四、测试证据:PdfEngineTest 如何印证验收语义

验收记录的「已执行./gradlew :composeApp:desktopTest87/87」中,PDF 引擎的验证集中在 next-compose/composeApp/src/desktopTest/kotlin/com/rememberber/mootool/next/compose/domain/PdfEngineTest.kt。DIFF-012 记录的证据条目与测试一一对应:

  1. 1-3;2;7;9-10→[1,2,3,7,9,10]:跨 token 去重保序;1-2, 4,6验证逗号/中文逗号/空白归一;3-1、1;;2、1-6(maxPage=5) 断言invalid-range失败。
  2. 奇偶/自定义:2-8+Odd→[3,5,7];Even→[2,4,6,8];Custom "1;3-4;8-10"→[3,4,8](交集且保持候选顺序)。
  3. 真实文件合并:4 页First.pdf(取2-3)+ 3 页Second.pdf(取1,3)合并 4 页,extractText验证第 1 页含Page 2、第 4 页含Page 3——用PDFTextStripper提取文本证明输出是结构完整、可再解析的真 PDF。
  4. 拆分命名:Quarterly.PDF6 页按Odd拆 3 页,输出恰为quarterly_split.pdf,且第 1/3 页文本为Page 1/Page 5。
  5. 上限:21 个拆分任务与 21 个合并源均断言PdfException.code == "too-many"。
  6. 边界与取消:非 PDF 文件断言not-pdf;0 个合并源断言need-two;cancelled = { true }的合并不产出cancelled.pdf(取消无输出文件)。
  7. 结构保留:构造含表单 + 签名字段 + 书签的 fixture,合并/拆分后inspect的表单与签名字段数保持不变,而书签数为 0,断言注释明确标注原因即「Page-only merge does not copy document outlines (same as Electron pdf-lib copyPages)」。
  8. 加密样本:StandardProtectionPolicy保护后的 PDF,inspect断言code == "encrypted"。

测试 fixture(writeNumberedPdf/writeStructuredPdf/writeEncryptedPdf)全部用 PDFBox 在临时目录现造,无外部资产依赖,这也是 87/87 可以在无头 CI 环境稳定跑通的原因。

五、如何在本仓库复跑该验收

环境与命令以 next-compose/README.md 与 next-compose/scripts/check-core.sh 为准:

export JAVA_HOME="$(/usr/libexec/java_home -v 21)" # Zulu 21(README 环境表:JDK 21 / Zulu 21.0.12.1) ./gradlew :composeApp:desktopTest # 或 ./scripts/check-core.sh 封装执行

适用前提与限制:

  • 本记录对应的验收轮只跑next-compose/工作树(:composeApp:desktopTest87/87);next-compose/docs/acceptance.md中后续轮次的数字(如 1342/1344)属于后续 DIFF 累积后的全量结果,不代表 2026-09-09 本轮口径;
  • PDFBox 版本以gradle/libs.versions.toml的pdfbox = "3.0.4"为准;
  • 未覆盖项如实登记:窗口截图、加密样本的手工走查、表单/书签/签名在安装镜像内的表现、安装镜像内的 PDFBox native 字体替代路径均未测——即 DIFF-012 的「受影响范围」条目,阅读代码结论时应以此为边界。

六、与 Electron 版的差异与边界(DIFF-012 摘要)

diff/012-pdfbox-import-page.md 对两版实现做了明确归档,可归纳为三点:

维度Electron(pdf-lib)next-compose(PDFBox 3.0.4)
页面复制PDFDocument.load字节后copyPages+addPageLoader.loadPDF+PDDocument.importPage+PDFMergerUtility,页面对象复制,不光栅化
拆分输出源目录{小写主名}_split.pdf相同(splitOutputPath),同样覆盖已存在的*_split.pdf
加密 PDF未支持(PDFDocument.load失败)以InvalidPasswordException明确失败,稳定错误码encrypted

差异文档同时声明边界:表单/书签/签名的保留范围未做专项实测,不能宣称与pdf-lib像素级一致;文档书签在拆分/合并不复制(ELECTRON_PARITY_OUTLINES_NOT_COPIED)。选择 PDFBox 而非 iText 的理由是架构指定,避免引入额外 PDF 库。

七、结论:这条验收记录留下的可验证链路

F24 本轮验收的价值在于形成了一条完整可复现的证据链:页码语义由 pageRanges.ts 定义、PdfEngine.kt 逐条复刻(含too-many/encrypted/cancelled等稳定错误码与半成品清理),PdfEngineTest.kt 用真实生成的 PDF 验证拆分/合并/去重/取消/结构保留,./gradlew :composeApp:desktopTest87/87 收口,差异与未测项由 DIFF-012 与验收记录本身登记在案。下一轮按记录推进至 P4 剩余的 F23 图片助手。

  • 桌面应用
  • 开发工具

【免费下载链接】MooTool

Handy tool set for developers

项目地址:https://gitcode.com/gh_mirrors/mo/MooTool
点击查看免费下载
上一篇:PandasAI农业农村农业农村水利区块链应用:区块链应用数据分析与应用技能
下一篇:Rome 规则详解:noInvalidConstructorSuper 如何校验 class 继承中的 super() 调用

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 1:53:48

ok-ww:鸣潮自动化脚本,3 步把日常、声骸和周常一键跑完

ok-ww&#xff1a;鸣潮自动化脚本&#xff0c;3 步把日常、声骸和周常一键跑完 【免费下载链接】ok-wuthering-waves 鸣潮 后台自动战斗 自动刷声骸 一键日常 Automation for Wuthering Waves 项目地址: https://gitcode.com/GitHub_Trending/ok/ok-wuthering-waves 周五…

作者头像 李华
网站建设 2026/10/16 1:52:20

考研线代线性方程组全攻略:解的判定、基础解系与参数讨论

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 1:52:12

RPCS3 PS3 模拟器完整操作手册:从安装到调优清单

RPCS3 PS3 模拟器完整操作手册&#xff1a;从安装到调优清单 【免费下载链接】rpcs3 PlayStation 3 emulator and debugger 项目地址: https://gitcode.com/GitHub_Trending/rp/rpcs3 RPCS3 是用 C 编写的 PS3 模拟器兼调试器&#xff0c;支持 Windows、Linux、macOS 与…

作者头像 李华
网站建设 2026/10/16 1:52:10

Rittal PDU 7955 智能配电单元监控模板详解

【免费下载链接】community-templates Zabbix Community Templates repository 项目地址&#xff1a; https://gitcode.com/gh_mirrors/co/community-templates 点击查看 免费下载 本文全面解析 community-templates 仓库中的 Rittal PDU 7955 监控模板&#xff08;Zabbix 6.0…

作者头像 李华