ADR-0003:字体与基础文本排版
2026-10-14 更新:本文保留初始决策背景。富文本、字体组缺字策略、分页控制、分节、合并表格与中文表单的扩展边界见 ADR-0007。
默认复杂文本路径、静态 CFF 与字体集合的后续扩展见 ADR-0009。显式
SimpleTextShaper保留本文中的严格契约。
背景
阶段一的标准字体路径无法可靠输出中文,也缺少可供布局使用的字体度量。下一阶段需要先建立字体、文本测量与输出的一致性,供后续语义布局和分页复用。
字体解析应复用已有基础库,同时避免字体类型、PDF 对象和打开的资源进入公共契约。
决策
- 新增字体与文本两个实际模块。字体模块内部使用 FontBox / pdfbox-io 3.0.8;PDFBox 文档和绘制 API 仍只进入 PDF 实现模块。这是对 ADR-0001 第 4 条的细化。
- 显式加载静态独立 TrueType。先复制输入字节,再提取 Unicode cmap、水平度量及嵌入权限,返回不可变字体快照。加载完成前关闭解析资源,不扫描系统字体。
- 字体字节以 SHA-256 标识。水平 advance 量化到 PDF 的 1,000 单位 em 网格,使基础测量与 PDF 字宽一致。
- 提供可替换的
TextShaper。当前SimpleTextShaper使用一个码点对应一个字形,无 kerning、连字或复杂塑形;固定回退顺序,并为每次回退提供原文范围和字体标识。 - 段落换行使用 ROOT 区域设置的 JDK
BreakIterator;没有可用断点时按完整 Unicode 码点紧急断行。单字超宽明确失败,设置码点与行数预算。 - 字体栈最大上升高度和最大下降高度定义统一行框;
lineHeight为固定基线间距,小于行框则失败。显式换行保留空行,来源范围使用原文 UTF-16 偏移。 ParagraphLayout只包含段落内部行位置。TextLayoutPainter平移为GlyphText命令,不测量、不分页。PDF 后端嵌入同一份原始字体,并校验字形映射、使用显式 advance。- 字体缓存仅存在于一次 PDF 输出。不同字体的原始数据总量默认限制为 64 MiB;字体的受限与仅位图嵌入标志触发失败,禁止子集化则完整嵌入。
- 示例与测试使用仓库内固定、许可明确的字体。核心库不打包示例字体。
考虑的替代方案
| 方案 | 取舍 |
|---|---|
| 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 文本提取。
结果及范围见阶段二验收记录。