跳到正文

ADR-0003:字体与基础文本排版 ​

2026-10-14 更新:本文保留初始决策背景。富文本、字体组缺字策略、分页控制、分节、合并表格与中文表单的扩展边界见 ADR-0007。

默认复杂文本路径、静态 CFF 与字体集合的后续扩展见 ADR-0009。显式 SimpleTextShaper 保留本文中的严格契约。

  • 日期:2026-10-14
  • 状态:已采纳,基础范围已实现。
  • 关联:ADR-0001、ADR-0002

背景 ​

阶段一的标准字体路径无法可靠输出中文,也缺少可供布局使用的字体度量。下一阶段需要先建立字体、文本测量与输出的一致性,供后续语义布局和分页复用。

字体解析应复用已有基础库,同时避免字体类型、PDF 对象和打开的资源进入公共契约。

决策 ​

  1. 新增字体与文本两个实际模块。字体模块内部使用 FontBox / pdfbox-io 3.0.8;PDFBox 文档和绘制 API 仍只进入 PDF 实现模块。这是对 ADR-0001 第 4 条的细化。
  2. 显式加载静态独立 TrueType。先复制输入字节,再提取 Unicode cmap、水平度量及嵌入权限,返回不可变字体快照。加载完成前关闭解析资源,不扫描系统字体。
  3. 字体字节以 SHA-256 标识。水平 advance 量化到 PDF 的 1,000 单位 em 网格,使基础测量与 PDF 字宽一致。
  4. 提供可替换的 TextShaper。当前 SimpleTextShaper 使用一个码点对应一个字形,无 kerning、连字或复杂塑形;固定回退顺序,并为每次回退提供原文范围和字体标识。
  5. 段落换行使用 ROOT 区域设置的 JDK BreakIterator;没有可用断点时按完整 Unicode 码点紧急断行。单字超宽明确失败,设置码点与行数预算。
  6. 字体栈最大上升高度和最大下降高度定义统一行框;lineHeight 为固定基线间距,小于行框则失败。显式换行保留空行,来源范围使用原文 UTF-16 偏移。
  7. ParagraphLayout 只包含段落内部行位置。TextLayoutPainter 平移为 GlyphText 命令,不测量、不分页。PDF 后端嵌入同一份原始字体,并校验字形映射、使用显式 advance。
  8. 字体缓存仅存在于一次 PDF 输出。不同字体的原始数据总量默认限制为 64 MiB;字体的受限与仅位图嵌入标志触发失败,禁止子集化则完整嵌入。
  9. 示例与测试使用仓库内固定、许可明确的字体。核心库不打包示例字体。

考虑的替代方案 ​

方案取舍
AWT 系统字体测量易受系统字体、替换和平台差异影响,难以保证测量与嵌入字节一致
布局时直接操作 PDType0Font能直接测量,但会把文本层绑定到 PDF 文档及其生命周期
立即引入完整塑形引擎能扩大文字支持,但会增加依赖、原生分发或语言桥接工作;在确有需求时通过 SPI 引入
中文使用阅读器字体替换文件较小,但字形和度量无法固定,难以形成可复现的布局

影响与兼容性 ​

export4j-graphics-api 新增对文本模块的 API 依赖,公共对象只包含项目自有类型。FontBox / pdfbox-io 使用内部实现依赖,不进入公共签名。

DrawingSurface 新增抽象方法 drawGlyphRun,外部实现需补齐。DrawCommand 的封闭集合增加 GlyphText,外部穷尽 switch 需更新。PdfLimits record 增加第七个组件;保留六参数构造器只维持该构造调用的源码兼容,不保证 record 形状及二进制兼容。项目尚未发布,调用方应重新编译。

当前字形定位不包含字形簇、X/Y offset、双向顺序或 GSUB/GPOS。复杂脚本、组合字符、Hangul Jamo、连接符和多码点 emoji 塑形需求明确拒绝。完整中文禁则、富文本样式、分页及 DOCX 不属于本次实现。

换行结果依赖固定字体、样式和 JDK 断行规则版本,不承诺跨任意 JDK 的逐行一致性,也不承诺 PDF 文件字节一致。

验证 ​

通过字体加载与资源所有权测试、段落几何与 Unicode 边界测试、PDF 嵌入及 ToUnicode 检查、逐字宽度和位置比较、禁止子集化的完整嵌入测试,以及独立 Poppler 渲染和 pypdf 文本提取。

结果及范围见阶段二验收记录。