自 0.2.0 起可用。
Ofdrw.Net.Layout.Text.Paragraphs 将带样式的多段落文本排版为最终行和页面,ParagraphLayoutOfdWriter 则将该结果原样写为 OFD。这是一个新入口:FlowDocument、PlainTextOfdConverter、DOCX 转换和早期 R21/R22 查询均保持既有行为,不会隐式迁移。
var noto = new TextFontFace("Noto Sans", File.ReadAllBytes("NotoSans-Regular.ttf"));
var wenkai = new TextFontFace("LXGW WenKai", File.ReadAllBytes("LXGWWenKai-Regular.ttf"));
var dejavu = new TextFontFace("DejaVu Sans", File.ReadAllBytes("DejaVuSans.ttf"));
var style = new TextStyle(new[]
{
new TextFontFamily("Noto Sans", noto),
new TextFontFamily("LXGW WenKai", wenkai),
new TextFontFamily("DejaVu Sans", dejavu),
}, fontSizeMillimeters: 4.2);
var request = new ParagraphLayoutRequest("排版 layout שלום مرحبا\n第二段。", style,
new ParagraphLayoutOptions(widthMillimeters: 90, pageHeightMillimeters: 250,
alignment: TextAlignment.Justify, lineHeight: TextLineHeight.Multiple(1.2)));
var layout = HarfBuzzTextPrelayout.CreateParagraphLayout().Layout(request);
var package = ParagraphLayoutOfdWriter.CreateDocument(layout, 110, 280, 10, 15);
await new OfdPackageWriter().WriteAsync(package, output);处理流程
- 准入检查。 不成对的代理项、制表符、除行/段分隔符外的 C0/C1 控制符、显式双向格式字符(LRE…PDI、LRM/RLM/ALM)、其他格式控制符(ZWJ、ZWNJ、软连字符、单词连接符、BOM)以及变体选择符,都会在任何塑形之前连同源区间一起被拒绝。样式区间必须完整拼接覆盖文本,且不得拆分字素簇。
- 段落划分。 按此处准入的 UAX #9 段落分隔符拆分:LF、CR、CRLF、NEL、U+2029。VT、FF 和 U+2028 是段落内的硬换行。终止符属于行的源区间(
TerminatorLength),但没有字形。 - Unicode 分析。 使用冻结的 Unicode 17.0.0 数据表:UAX #9 rev 51(P2–P3、X1–X10、W1–W7、N0–N2、I1–I2、L1–L2)、UAX #14 rev 55 默认断行和 UAX #29 rev 47 默认扩展字素簇。不进行定制、词典分词或断字。参见一致性验证。
- 项目划分。 按解析后的双向级别、文字体系(通用/继承字符采用相邻文字体系)、所选字面和样式划分。每个字素簇采用样式回退链中首个满足条件的字体家族:其字面在请求的粗体/斜体声明下覆盖该簇的所有标量。通用文字体系的簇在旧字面仍可覆盖时保持使用该字面。不发现宿主机字体,也不合成粗体/斜体:缺少样式对应字面会以
UnsupportedFontProfile失败,缺少覆盖则以MissingGlyph失败。 - 塑形。 使用
ITextShaper(默认 HarfBuzz)对每个项目进行测量塑形。 - 断行。 在同时属于字素簇边界的 UAX #14 断行机会上采用首次适配贪心算法。尾部空格悬挂。随后重建所选行:被行边缘或 L1 级别变化切开的游程会重新塑形并检查实际宽度;若溢出,则尝试前一个断行机会(最多 32 次,随后
BudgetExceeded)。由于估算将跨断点字距调整计入左侧,在信任估算结果之前,还会重建最多三个“估算超出版心不超过半个 em”的后续合法断点。没有断行机会的片段会以LayoutOverflow失败(默认TextOverflowPolicy.Reject),或在字素簇边界断开(BreakGraphemes);绝不拆分簇内部。候选切点都会重建并重新塑形。重建后的宽度不具单调性(阿拉伯文 lam-alef 连字可能比单独的 lam 更窄),因此二分查找后还会进行有界向上扫描,并在连续四个切点溢出后停止。所选切点是已找到的最大适配切点,不保证全局最大。 - 重排序。 每行先应用 L1,再对游程应用 L2。游程内部保留塑形器的视觉字形顺序。
- 度量与对齐。 所有游程共享一条基线。正常行高使用各字面在相应字号下的上升部、下降部和行间距;也可使用
Multiple和Exact。Start/End随段落方向变化;首行缩进位于起始侧。Justify在词间空格以及汉字、假名、谚文或注音符号相邻字素之间拉伸非末行(闭标点前不拉伸);额外步进按字形记录。 - 分页。 按
PageHeightMillimeters划分页面(或使用一个高度不限的页面)。分页处丢弃段间距;高于页面的行会以LayoutOverflow失败。
最终结果约定
ParagraphLayoutResult 不可变且完整;失败或取消时不返回部分结果。坐标为相对于各页面内容框的毫米值,Y 向下,只从设计单位转换一次。
| 层级 | 包含内容 |
|---|---|
| 结果 | 请求(不可变样式)、页面、UnicodeVersion = 17.0.0、塑形器 EngineVersion、塑形调用次数 |
| 页面 | 索引、行、已用高度 |
| 行 | 绝对 UTF-16 源区间(含尾部空格和终止符)、段落/页面索引、基础级别、硬换行标记、顶部、基线、高度、上升部、下降部、对齐后的内容左侧位置/宽度、墨迹边界、按视觉顺序排列的游程 |
| 游程 | 逻辑源文本和起点、精确的 TextShapedRun(字面字节身份、文字体系、语言、方向、特性)、解析后的双向级别、TextResolvedStyle、原点、基线、步进、墨迹 |
| 字形 | 原始 GID、绝对簇起点/长度(多对多)、原点 X/Y、含两端对齐的步进、两端对齐增量、墨迹 |
TextResolvedStyle 快照保存画刷(DeviceRGB 填充/描边颜色及 alpha、描边宽度)、字号、语言、请求与字面各自的粗体/斜体状态、字体家族及其回退索引,以及 Synthetic = None。写出器不再查询请求样式,不重新选择字体,也不重新塑形。
OFD 输出
ParagraphLayoutOfdWriter.CreateDocument(result, pageWidth, pageHeight, left, top) 为每个排版页面创建一个 OFD 页面,并为每个游程创建一个 TextObject:
TextCode保存游程未经修改的逻辑文本;- 每个源簇对应一个按逻辑顺序排列的
CGTransform(CodePosition/CodeCount以 BMP 字符计数),列出原始 GID; - 字形槽位遵循该顺序,每个槽位都有显式原点(完整的
DeltaX/DeltaY),因此 RTL 游程使用负增量,而不是重排文本; - 每个不同字面对应一个字体资源,以保留 GID 的子集嵌入(或通过
embedFullFonts: true完整嵌入);遵守 OS/2 嵌入限制; - 写入填充颜色和 alpha;粗体/斜体由游程字面提供。
以下情况会在创建包之前以 UnsupportedOutput 拒绝:描边文字或无填充、增补(非 BMP)字符、文本框或墨迹超出页面,以及源文本/GID 身份丢失。
模型将此表示为 OfdPositionedGlyphMapping.ClusterMap。写出器在 https://ofdrw.net/layout-hints 命名空间中,用写出器自有的提示属性 ClusterMapV1="true" 标记此类 TextObject(其他阅读器会忽略它)。读取时,历史有界 CGTransform 配置仍优先;只有被这些配置拒绝的已标记 TextObject,才会按完整簇映射接纳:一个 TextCode、每个代码单元恰好属于一个递增 CGTransform、每个字形槽位都有原点。未标记的外来 CGTransform 继续保持历史失败关闭规则。PDF 和 SVG 根据接纳映射中每个字形的原始轮廓进行绘制,并将逻辑文本保留为不可见 PDF 文本或 SVG title/aria-label。
Unicode 一致性验证
数据表由 scripts/unicode/generate_unicode_tables.py 根据 UCD 17.0.0 文件生成,这些文件的 SHA-256 固定在 scripts/unicode/ucd-17.0.0-manifest.json 中(Unicode License v3)。完整官方测试在 UnicodeConformanceTests 中运行:
| 文件 | 用例数 | 失败数 |
|---|---|---|
| BidiTest.txt | 770,241 | 0 |
| BidiCharacterTest.txt | 91,707 | 0 |
| LineBreakTest.txt | 19,338 | 0 |
| GraphemeBreakTest.txt | 766 | 0 |
UnicodeTextAnalysis 公开相同算法(字素边界、断行机会、双向级别和视觉顺序)。双向 API 不应用 L3/L4;镜像由塑形器处理。
限制与不承诺的范围
- 预算:
MaximumTextLength(262,144 个 UTF-16 代码单元)、MaximumLines(10,000)、MaximumShapeCalls(100,000)、每个塑形游程 4,096 个 UTF-16 代码单元、坐标范围 ±1,000,000 mm。 - 已验证的输出文字体系以验证报告为准:使用所列字体的拉丁文、汉字、希伯来文、阿拉伯文和泰米尔文。其他文字体系经过相同流程排版,但不作通过承诺。
- 此配置不包括:制表符、ZWJ/ZWNJ/变体选择符、垂直文本、kashida 两端对齐、断字、语言定制、合成样式、描边文本输出、非 BMP 输出。
- 0.2.0 候选的三个固定段落输出页面已通过 macOS Preview 检查。PDF 选择几何、Copy 和 Find 仍未验证。详见发布说明。