跳到正文

普通文档逐页导出验收 ​

范围 ​

本轮验收开发源码中的 DocumentEngine.exportPaged、普通块源的单遍/显式两遍导出、同步逐页布局与绘制会话,以及 PdfSpoolBackend 直接复制最终化活页对象的路径。它们不属于已发布的 0.1.0-rc.1。API 与内存边界见逐页导出指南,设计见 ADR-0111及 ADR-0112。

Windows、Java 21、Gradle 9.4.1 的本地构建执行 build apiCheck sbom publishLocalBundle,包含后续内存优化与块源入口共 2,320 项 Java 测试,0 失败、0 错误、0 跳过。API 检查包含 16 个模块、733 个可访问类型,未发现不兼容声明或第三方实现类型泄漏;API 检查器自身的 3 项测试通过。以上是本地证据,不表示远端 CI 或其他平台已运行。

逐页入口首次验收时,临时签名身份验证了 17 个组件、82 个签名制品、篡改拒绝和 16 个库的 BOM/SBOM 对齐;新的隔离 Maven 调用方核对了实际解析的 16 个 JAR 摘要。首次依赖刷新遭遇 Maven Central TLS 连接失败,后续使用已有第三方依赖缓存离线完成调用方验证;项目制品仍从该次签名的本地仓库解析。本次块源入口再次以独立临时签名身份和新的隔离调用方通过上述验证,临时私钥环已移除。没有重新签署正式版本、上传制品或进行远端消费验收。

行为与输出 ​

本轮新增 40 项 Java 回归,覆盖逐页与完整布局等价、跨页绘制缓存复用、单页合成、最终总页数和显示页码、跨节来源/呈现预算、后代 ID 冲突、未来引用、重放变化拒绝及字节限制。取消、输出流借用、写出器关闭、主/suppressed 异常和原子目标保护分别断言;评论及可编辑表单在打开写出器前拒绝。重放校验覆盖原始来源、几何与绘制,包含相同 hashCode 的不同文字,不能以哈希码相同绕过确定性要求。

后续内存优化增加 11 项回归,覆盖字形并发复用及原始来源/缺字诊断、精确 advance 与正负零、布局值的完整相等条件和有界淘汰,以及局部字段解析的页面/片段复用、最终链接及跨页生成预算。完整布局释放已消费块的递归测量缓存;普通字形布局在完整测量和预算计费后有界共享。逐页路径继续释放完成页,不维护该完整布局值池。

本次继续增加 13 项回归,覆盖段落片段的来源与诊断、绘制指令共享的完整相等条件和字体资源权重,以及最终化字体子集的映射身份、页资源关闭、输出异常、精确页字节预算和取消。磁盘后端的字体子集快照限制为 64 项及 16 MiB 计费权重,单字体最多收集 8,192 个 Unicode/字形 ID 对;超限回到原路径。它不保留跨页 PDFBox 对象,命中后仍独立重建字体图并完成普通保存。绘制会话的指令池限制为 1,024 项、65,536 个字形/路径计费单位,字体源按实例身份另限 32 MiB;这些权重不是整个进程的精确内存上限。

隔离 Maven 调用方以实际发布坐标及本地 JAR 运行 pagedDocument。同一完整模型分别通过完整入口与逐页入口生成 PDF,最终均为 27 页:26 页正文、1 页目录,0 诊断。样例包含:

  • 两种纸张尺寸、分节显示页码重启、页眉/页脚 PAGE/PAGES。
  • 目录、书签、未来行内引用、块引用、反向引用及外部图文链接。
  • 64 行续页脚注、42 段续排内表、24 行表格、合并单元格和重复表头。
  • 透明 PNG、JPEG、真实字体嵌入及图片资源。

独立 pypdf/PDFium 检查页数、全部标记的唯一性及顺序、文字、图片、字体、链接矩形/目标、书签及显示页码。两条入口的这些结果一致;XYZ 目标中的缩放值 0 和 null 仅按 PDF 规范归一为保持当前缩放,不放宽目标页和坐标断言。

两份 PDF 的全部页面分别经 Poppler 与 PDFium 渲染。相同渲染器下,新旧入口的逐页像素完全一致;跨渲染器差异通过既定阈值,27 页全部完成视觉复查。另通过既有生产样例、高级 PDF、行内图片及语义重放四组独立输出回归,没有刷新视觉基线。未进行本轮人工 PDF 阅读器操作。

普通块源验收 ​

新增 25 项回归覆盖按需窗口释放、跨页原始来源、全局 ID 与增量预算、keep 链有限预读、组件内部来源、原始 I/O 异常及取消;导出层覆盖单遍准确页数、两遍来源及组件绘制变化拒绝、编号/题注、关闭顺序、配置快照和原子目标保护。磁盘索引测试直接核对原始 xref 的位宽、偏移与对象头,并检查小数页高下的导航坐标,避免解析器自动修复掩盖格式错误。

新的隔离 Maven 调用方运行 streamingDocument:同一混合来源分别通过完整入口、单遍块源及两遍块源输出 9 页,包含 3 组标题、54 行跨页表格、重复表头、透明 PNG/JPEG、9 个图文外链及正文/页区最终页字段。来源打开/关闭次数分别为 1/1 和 2/2,诊断为 0。独立工具检查全部文字、行顺序、图片、嵌入字体和链接坐标;三个入口的全部页面分别经 Poppler 与 PDFium 渲染,相同渲染器下逐页像素完全一致,9 页全部完成视觉复查。该小样例验证输出等价;按需输入的容量另由独立 JVM 样例验证。

内存容量回归 ​

后续优化使用独立 JVM 串行运行复杂容量样例,采用 Java 21、G1、4 个可用处理器和 2 页预热;计时包含字体加载、完整输入模型构建、布局及输出关闭。每页包含中英文多样式文字、16 行四列表格、合并单元格、PNG/JPEG、图文外链及页码,轮换使用 24 组图片,嵌入 5 种字体。这些样例具有可重复的文字与布局值,不代表所有唯一内容的容量上限。

页数最大 Java 堆完整入口+普通 PDF 后端完整入口+磁盘 PDF 后端逐页入口+磁盘 PDF 后端
1,000128 MiB完成完成完成
3,000256 MiB完成完成完成
10,000512 MiB未测未测完成

优化前,同一 3,000 页样例的两个完整入口均在 256 MiB 堆下发生 OutOfMemoryError;优化后通过该场景。万页逐页样例首次因默认组件绘制预算被明确拒绝,没有发生 OOM;在容量调用方显式配置 80,000 个放置、10,000,000 条指令和 80,000 个交互后完成。布局、后端及输出仍分别受有限预算约束,生产默认限制没有扩大。

对新实现、旧实现对照及中间版本产生的 14 份成功 PDF,共 39,000 页,独立检查全部页面的文字、16 行表格内容、图片对象与尺寸、嵌入字体、链接及页码;退出后临时文件为空。最终实现的上述 7 个容量场景另抽查首/中/末页,经 Poppler 与 PDFium 双渲染并完成视觉复核;有同页数旧实现对照的 6 组抽样页面在相同渲染器下像素完全一致。容量样例没有逐页进行人工视觉检查,单次成功也不等于稳定吞吐或长期无泄漏。

按需块源的低堆容量 ​

在相同复杂内容条件下,块源按需构造每页至多 5 个根块,分别验证单遍准确总页数和两遍自动确定总页数。布局、组件及 PDF 预算由容量调用方按页数明确配置,生产默认预算保持不变。

页数最大 Java 堆单遍块源+磁盘 PDF 后端两遍块源+磁盘 PDF 后端
1,000128 MiB完成完成
3,000128 MiB完成完成
10,000128 MiB完成完成

这 6 个独立 JVM 场景均未发生 OOM,来源打开/关闭次数分别为单遍 1/1、两遍 2/2,进程退出后临时文件为空。Java 堆上限与进程 RSS、私有字节分别采样;不能将 128 MiB 堆上限解释为总内存。该结论只覆盖上述混合内容与配置预算,不表示一个任意巨大的不可分割块也能在相同内存下完成。

6 份输出共 28,000 页,独立检查全部页面的文字、16 行完整表格、图片对象及尺寸、嵌入字体、图文外链和页码。每份另抽查首/中/末页,经 Poppler 与 PDFium 双渲染并完成视觉复核;1,000 页样例的这些抽样页与完整入口在相同渲染器下像素完全一致。没有逐页人工查看全部容量页面。

唯一资源、大块与失败恢复 ​

最终冻结构建补充 14 个容量配置,共 309 次导出尝试:203 次成功、106 次按预期拒绝。所有场景使用相同生产 JAR;容量调用方的输入与计数修正单独保留,不将早期调用方失败计为生产验收通过。

场景最大 Java 堆验收结果
七类注入失败,各重复 2 次并逐次恢复128 MiB取消、来源读取/关闭、布局预算、执行输出预算、后端单页字节预算、输出流 I/O 均按具体异常断言
768 张及 3,000 张混合唯一图片128 MiBPNG、透明 PNG、JPEG;独立解码的像素摘要逐张唯一
各 300 张唯一 PNG、透明 PNG、JPEG128 MiB三种编码分别完成,透明图片的软蒙版保持
同一图片重复 1,000 页128 MiB完成并检查资源复用及全部正文标记
尝试输入 5,000 张唯一图片128 MiB第 4,070 张超出默认累计 50,000,000 像素预算时明确拒绝;编码累计仍未超出 64 MiB
单段落 10,000 行、69,999 码点256 MiB输出 200 页,两遍各仅产生 1 个根块,全部行顺序完整
单表格 10,000 行256 MiB输出 233 页,全部行及重复表头完整
100,000 块的 keep 链来源128 MiB至多预读 52 块即报告 CONTENT_TOO_TALL,保留已有目标
17 种显式字体轮换 170 页128 MiB每页一段、字号 10 pt、行距 64 pt,逐页正文与嵌入字体检查通过
50 组连续任务128 MiB每组 20 页成功、一次注入失败、2 页恢复,共 150 次尝试
4 线程、40 组并发任务256 MiB每组同样包含成功、失败和恢复,共 120 次尝试

唯一图片使用 128×96 小图;PNG 为常色背景与噪声块,透明 PNG 含渐变透明度,JPEG 为全图噪声。这不是多千张高清图片的验收。默认源图片累计预算仍为 64 MiB 编码及 50,000,000 像素,没有扩大生产上限或增加配置入口。万行单块仍整体准备,不能据此宣称任意单块恒定内存。

失败场景分别断言原目标保留、借用输出流不被关闭、来源及写出器关闭,以及 finish 的尝试/成功次数;输出流 I/O 通过 cause 链核对原异常身份。每轮或每波完成后检查后端临时目录,进程退出后再检查 JVM 临时目录,最终均为空。50 组连续任务约运行 10 秒,只证明有限循环的清理与恢复,不代表长期无泄漏。

扩展成功输出共 8,279 页,使用 pypdf 严格结构解析、PDFium 正文提取及 Pillow 像素解码,逐页检查标记、唯一图片、软蒙版、表头与链接;失败目标逐个检查哨兵。与最终构建的其他 50 份复杂样例共 72,400 页合计,完成 80,679 页自动内容检查。这两个统计只计本批输出,不与前面的历史容量数字相加。

本批选取 37 份 PDF 的 139 页进行 Poppler/PDFium 双渲染,包括首/中/末页、三种图片编码及全部 17 种字体,接触表逐张完成视觉复核。7 组同页数的新旧实现或完整/块源输出在同一渲染器下抽样像素完全一致。密集细线表格在 216 dpi 下的差异经原图核对后定位为抗锯齿差异,再以 432 dpi 复核;仍使用原有均值差小于 4、变化像素比例小于 0.06 的阈值,没有放宽内容或视觉断言。另完成 3 页段落流和 3 页跨页表格样例的全部文字、重复表头、双渲染及视觉检查。没有逐页人工查看全部容量输出,也未执行本轮人工阅读器操作。

复现 ​

从仓库根目录执行;Linux/macOS 使用 ./gradlew。Python 验证环境按 scripts/requirements-verification.txt 准备,Poppler 的 pdftoppm 需可从 PATH 调用。

powershell
.\gradlew.bat build apiCheck sbom publishLocalBundle :examples:production :examples:advancedPdf :examples:inlineImages --console=plain
.\gradlew.bat -p verification/consumer -PconsumerBuildDirectory=build/validation-paged clean build run pagedDocument streamingReplay streamingDocument ioConvenience resolvedLibraries --console=plain
python scripts/test_api_compatibility.py
python scripts/verify_paged_document.py --consumer-build-directory verification/consumer/build/validation-paged --output-directory build/verification/paged-document
python scripts/verify_streaming_document.py --consumer-build-directory verification/consumer/build/validation-paged --output-directory build/verification/streaming-document

调用方构建目录须来自本次成功执行;不能复用历史 PDF 代替新验证。样例、渲染图、资源采样及原始日志放在忽略的 build/ 中。

限制 ​

exportPaged 仍持有完整不可变输入,最大单块、长表/组件准备结果、keep 链、唯一 ID、锚点及导航索引可能增长;脚注计划保留已经测量的文字切片。规划、预检和输出需要重复布局与绘制,以换取完成页的中间对象释放。块源入口释放已完成的输入,但最大块、keep 链、全局 ID 及两遍的页摘要仍需内存;它不支持完整模型的目录、未来引用和脚注重排。普通 PdfBackend 仍累计文档级格式对象;只有选择 PdfSpoolBackend 才使用单页 PDFBox 工作集及磁盘索引。

独立容量样例只能证明所测输入和预算,不表示任意输入恒定内存或长期无泄漏。页面 SPI 的评论/表单、归档/加密/签名等支持边界均保持原有显式限制。