跳到正文

ADR-0102:有界语义长表、显式重放与逐页 PDF spool ​

  • 日期:2026-10-28
  • 状态:已采纳;验收证据单独记录

背景 ​

单向固定列长表已支持行内文字、图片、链接和有限原子纵向组,但块格/嵌套、纵向 owner 跨页、自动宽与未来信息仍没有完整入口。逐页释放 IR 不能解决 PDFBox 累计页树和 COS 对象的问题;调用两次数据库查询也不能证明两个输入一致。

决策 ​

保留已有 export 的默认单行预读与原子组行为,新增 exportSemantic 和 exportReplay。两者复用 SemanticStreamingTableLayouter,每次仅保留有限表头、当前页及一个受行/码点预算限制的传递连通组。组转换为普通固定宽语义表,由 TableCursor 复用既有递归测量与完整行续排。组中间的逻辑表头只参与游标测量,合并到实际页面时去掉,保证物理表头每页只出现一次。根行号映射回全局源行,子块序号、嵌套来源和 UTF-16 范围保持。

为校验跨组语义 ID 唯一性,还保留受 maxParagraphs 限制的已见 ID 集合;它不保存正文,但空间随已见独立块数增长。容量结论需区分普通行和含大量独立语义子块的来源,不能把每页 PDF 对象释放扩展成所有布局索引恒定大小。

exportSemantic 只开一次来源,支持语义块格与拆分 owner。PAGE 使用实际页,PAGES 需要准确声明;未来引用不在单次源上猜测。exportReplay 明确重开调用方提供的稳定快照:可先做一个有限前缀列宽探测,再完成首遍分页和第二遍写出。各遍来源分别拥有、关闭,不并行保持两个来源。无隐藏重新查询或完整正文收集器。

探测复用普通表格列宽算法,并将结果固定为 point 列宽;后续行只换行,不回头改变已确定宽度。探测行数和规范序列化字节均有上限,不能越过上限读取不完整纵向组。第二遍同时核对探测前缀和完整语义 SHA-256:长度、类型、格式、运行、跨度、子树及不可变图片内容标识都参与摘要,不依赖含分隔符歧义的 toString()。源变更使整次调用失败,不能完成 finish。

未来目标、编号、书签、目录和组脚注由有限 StreamingReplayPlan 明确声明。首遍只保留被声明的目标和有限链接几何;目录的预留字段宽度固定,插入目录页后统一偏移目标和物理总数。脚注锚定连通组首行并预留同页空间,仅在该组首次呈现页输出一次;内部其他源行的隐式脚注不移动到组首。声明、字段、生成文字、链接、输出行和诊断分别计预算。

graphics-api 增加 PageNavigation 和 PageWriter.configureNavigation。既有写出器默认明确拒绝非空导航。PdfSpoolBackend 提前在磁盘页索引中预留未来页对象,用相同最终目标生成内部链接和书签;不把导航注入绘制命令,也不让后端重新分页。

PdfSpoolBackend 每次通过普通 PDF 渲染器生成一页,自主重编号复制该页可达对象图,返回前释放该页 PDFBox 文档。2026-11-06 改为完成保存后直接复制活页对象,省去中间 PDF 文件及重新解析,具体决策见 ADR-0111。xref、页对象索引和页树叶索引存磁盘;内存保留最多 256 页的叶节点引用、一个受限页对象图、有限导航声明及有界资源缓存。最终页树和传统 xref 按磁盘索引顺序写出,无完整文档 PDDocument、全页数组或跨页累积的 PDF 字体/COS 对象。

2026-11-05 细化字体策略:每页继续独立生成字体资源,以包含全部字典属性、数组次序及流字节的规范 SHA-256 指纹识别等价字体图,并复用其已有输出对象号。编码、Unicode 映射、字宽或字体程序不同都不能共享,字典插入顺序及间接对象编号不参与内容身份。缓存按最近使用顺序最多保留 256 个指纹和对象号,关闭时清空;字体图过深、含循环或未知类型时跳过复用,走原复制路径。淘汰只影响复用机会,不影响输出语义。每页源对象预算仍计算完整字体图,输出对象与字节按实际写出计费。单页保存使用有界丢弃流,继续受原有页字节和临时工作预算约束;这些字节不再落盘。

图片复用沿用同一完整对象图指纹,另设最多 256 项的会话索引;尺寸、颜色空间、蒙版和原始编码流均参与比较。索引只保留指纹与输出对象号。共享命中仍遍历完整源图并执行每页对象预算与取消检查,输出预算按实际写出计费。

已完成像素验证且通过保守识别的 8 位标准三组件 JFIF JPEG,直接以 DCT 原编码流嵌入,不重复调用图像读取器建立文件缓存。识别仍核对真实尺寸、组件和标记结构;灰度、ICC、Adobe、未知颜色或标记继续走原像素路径,不扩展颜色支持范围,也不改变进程全局 ImageIO 设置。

三个磁盘索引分别使用至多两个 8 KiB 缓存块,按块换出并复用数组。保留项的逻辑长度完整计入原临时预算,不按缓存块填充收取额外空间,也不在磁盘预分配额外空间;未写保留项显式读零,最终写出仍拒绝缺失对象。索引数据被消费或会话失败后,关闭时丢弃缓存并删除自有文件。

重复无损图像的压缩结果另设 64 项/32 MiB 压缩流字节的会话缓存,只保存自有描述值与字节,不保留 PDFBox/COS 对象。每页重建独立的图像和蒙版流;仅缓存可完整描述的 DeviceRGB/DeviceGray、8 位 Flate 输出,未知属性、未知结构或超限项继续走原生成路径。淘汰只影响压缩复用,关闭会话时清空;页源对象、单页序列化与输出字节仍按实际路径执行预算。

FontBox TrueType 解析程序在单个会话内按最近使用顺序复用,限制为 8 项及 32 MiB 源字节权重;不共享文档级可变 PDF 字体。解析表可能占用更多堆,源权重不是精确内存上限。当前页借用项固定到该页 PDF 文档关闭,只有未固定项可被淘汰并关闭;程序超限或本页占满缓存时回到页自有解析与关闭。会话关闭释放全部缓存程序并保留主异常及清理异常。

对允许子集嵌入的横向 TrueType 字形,会话进一步有界复用已最终化的子集图快照。身份包含字体源摘要、子集许可及本页完整的 Unicode/原字形 ID 对;不能用相同文字、字体名称或不同映射的字形集合互换。快照只保存自有值和压缩字节,每页重建独立 PDF 字体图,不保留跨页 PDFBox 对象。未知结构、超限项、CFF 或禁止子集嵌入的字体回到原路径。命中仍执行单页保存、精确字节测量、完整源对象计费和取消检查;缓存只省去重复字体子集生成,不能改变预算。

子集快照同时保存最终 PDF 字宽,元数据计入同一缓存权重。命中后的写入使用原 Unicode/CID 编码与最终字宽,避免为写入重新解析字体程序;需要完整字体读取功能时才创建页内读取器,其解码字体由当前页文档负责关闭。读取器和任何 PDF 字体对象均不跨页保留,普通保存和精确页字节计费保持。

失败与边界 ​

写出器使用唯一自有临时子目录,所有路径、取消、输出异常及失败关闭都清理索引;临时父目录与目标流继续由调用方拥有。路径输出复用同目录原子替换;输入一致性、来源关闭或写出器关闭失败均保留旧目标。借用流失败可能已有部分字节。

该后端输出普通 PDF,不自行承诺 PDF/A、PDF/UA、加密或签名。此类配置继续走明确的已有接口。StreamingTable 仍不冒充普通 Block,因此没有隐式 XML/DOCX/恢复序列化。重复表头不支持纵跨,自动宽是明确前缀探测而非全源无界扫描,脚注是显式组锚点语义。

验证要求 ​

行为验证覆盖单遍/多遍源所有权、全局来源、嵌套/owner 续排、所有预算、前缀和全源不一致、关闭失败、原子目标、导航及 256 页叶边界。独立工具核对完整行顺序、最终字段/目标、几何和双引擎渲染。容量必须包含独立有限堆 JVM 的布局与真实 PDF、重放、重复失败/取消恢复和并发,分别报告观察堆、工作集、私有字节、句柄和临时盘;有限结果不能证明永久无泄漏或任意输入恒定进程内存。

API 见有界语义与重放长表。