Ofdrw.Net.Layout 的 FlowDocument 用段落和其他块生成多页 OFD,调用方不需要手动计算每个 TextObject 的页面坐标。它支持文本、局部样式、表格、固定尺寸图片和 Canvas 块、折行及分页。
所有页面尺寸、边距、缩进、字间距和字号都以毫米表示。需要固定列网格、合并单元格和表格分页时,继续阅读流式表格;需要显式页面坐标时使用 OfdDocumentBuilder。
生成多页文档
安装 Ofdrw.Net.Layout 和 Ofdrw.Net.Packaging:
using Ofdrw.Net.Core.Models;
using Ofdrw.Net.Layout;
using Ofdrw.Net.Packaging;
var document = new FlowDocument();
var paragraph = new Paragraph { Alignment = ParagraphAlignment.Center };
paragraph.Spans.Add(new Span("中文 English "));
paragraph.Spans.Add(new Span("重点")
{
Bold = true,
Color = new OfdColor(192, 0, 0)
});
document.Blocks.Add(paragraph);
for (var i = 0; i < 100; i++)
document.Blocks.Add(new Paragraph($"第 {i + 1} 段内容。"));
var package = document.Render();
await using var output = File.Create("report.ofd");
await new OfdPackageWriter().WriteAsync(package, output);默认页面为 A4、四边距 25.4 mm、10.5 pt 等值字号(3.704 mm)、SimSun 字体名。两端对齐和浮动盒尚未实现。
Span 与局部样式
每个 Span 的粗体、斜体和颜色相互独立,不会传播到相邻 Span。
FontWeight可取 100–900,步长 100;与Bold=true同时设置时只接受 700。LetterSpacingMillimeters在同一 Span 的相邻字素之间增加非负间距,自动折行末尾不增加尾间距。Underline可配置偏移和线宽,沿最终文字游程绘制原生 OFD 路径。- 段落和表格单元格共用这些样式规则。
- 相邻 Span 会合并为逻辑文本,
CRLF/CR归一为LF;显式\n换行,\f换页。一个字素跨 Span 时采用起始字符的样式。
这些样式仍使用下述 Flow 字宽估算,不代表真实字体预排版或复杂文字 shaping。
图片与 Canvas 块
图片块和 Canvas 块占据明确宽高,是不可拆分的分页单元。宽度超过正文区或高度超过新页正文高度会失败,不会自动缩小。
using Ofdrw.Net.Layout.Graphics;
var pngBytes = File.ReadAllBytes("chart.png");
document.Blocks.Add(new ImageBlock(pngBytes, 60, 40)
{
Alignment = ParagraphAlignment.Center,
SpaceAfterMillimeters = 4
});
document.Blocks.Add(new CanvasBlock(100, 28, (graphics, box) =>
{
graphics.FillRectangle(
new OfdBrush(new OfdColor(225, 240, 225)),
0, 0, box.WidthMillimeters, box.HeightMillimeters);
})
{
Alignment = ParagraphAlignment.Center
});图片接受原生绘图入口支持的静态 PNG 或基线 JPEG,构造时复制输入字节。Canvas 回调在排版完成后运行,接收以最终块左上角为原点、初始裁剪到块矩形的 OfdGraphics,可绘制路径、文字或图片。
页面与元素回调
document.OnPage = (graphics, page) =>
{
// 使用整页毫米坐标绘制页背景。
};
document.OnElement = (graphics, placement) =>
{
// placement.Block 是最终块片段,placement.Boundary 是页内毫米坐标。
};OnPage按页序各调用一次,绘制对象位于正文下方。OnElement按文档顺序对每个最终片段调用一次,绘制对象紧随该片段。- 段落按行、表格按行、固定块与命名区域按整体提供片段。
FlowBoundary.PageIndex从零开始,X/Y/Width/Height 是物理页上的毫米坐标。
测量失败时不运行回调;回调抛错或取消会使 Render 失败且不返回包。回调运行时不要修改 Blocks 或其内容,调用方仍需正确使用绘图上下文的变换和裁剪。
字宽、折行与空格
Flow 使用确定性的兼容度量,而不是运行机器的实际字体测量:
- Basic Latin 与 Latin-1 使用固定的 Arial 兼容比例字宽表。
- CJK(含 Hangul 和增补汉字)使用 1 em。
- 其他脚本暂按固定 0.6 em 近似。
FontFamily决定 OFD 声明字体;默认只声明名称,不嵌入字体字节。实际字体宽度差异较大时,需要单独检查视觉结果。- 写出的 OFD
DeltaX与分页测量一致。字体子集和嵌入复用见字体参考。
过长英文单词按 Unicode 文本元素拆分。Basic Latin/Latin-1 字母和数字支持按字素基字符预测折行,包含 café 和分解形式的 café。预组合与分解形式可组成受支持 Latin-1 字母时使用组合字母字宽,OFD 仍保留原始字符序列。
普通分隔空格在自动折行末尾保留字符、推进量归零,不影响居中或右对齐;显式换行和末行保留空格推进量。
NBSP(U+00A0)、窄 NBSP(U+202F)和数字空格(U+2007)按 Unicode GL 空格处理,相连文字作为不可拆分单元:
- 普通 NBSP 等于普通空格宽。
- 窄 NBSP 固定为 0.2 em。
- 数字空格等于对应粗斜体样式的
0字宽。 - 不可拆分单元超出可用行宽时明确失败,不删除缩进或额外生成空首行。
这不是完整 Unicode 行断算法。DOCX Native 保留其自己的超宽内容溢出策略和字体度量入口。
空行与分页
- 段末
\n的空行在后续内容出现时计入高度,不单独创建文末空白页。 - 连续段末换行保留全部空行高度;显式空行使用换行 Span/run 的字号和段落最小行高,并逐行分页。
PageBreakBefore在已有正文或延期显式空行时开启新页并清除尾间距;首块直接设置不会产生前置空页。- DOCX 的非末节结束时,用所属节页面尺寸提交延期空行;末节到文末仍不生成空白尾页。
- Native 正文
PAGE在首个正文行页号确定后重新测量;同段后续自动分页或正文后的显式换页沿用该上下文。页眉页脚的逐页计数遵循独立契约。
PDF 导出绘制单个受支持 Latin-1 字素时使用对应组合字形,复制文本为规范等价形式,避免分解字符的基字符点和重音叠绘。
渲染、预算与并发
Render() 每次生成新包,不修改之前返回的包。调用方可以复用描述对象,但不要在其他线程同时修改 Blocks、Spans 或 Options。
超高行、无可用宽度、页数、字符数和文本元素数量超限会明确失败,不会静默裁切。预算由 MaxPageCount、MaxCharacters、MaxTextElements 控制,取消通过 CancellationToken 传入。
运行仓库样例
dotnet run --project e2e/Ofdrw.Net.Layout.E2E -c Release -- artifacts/flow-layout样例生成公开 Flow、显式 Native DOCX 和默认 DOCX 的 OFD/PDF。视觉检查应打开本次 OFD 导出的 PDF;自动测试和 PNG 渲染不代替目标阅读器验收。实际检查范围见验收记录。
只运行图片/Canvas/Span 样例:
dotnet run --project e2e/Ofdrw.Net.Layout.E2E -c Release -- \
artifacts/flow-fixed \
e2e/Ofdrw.Net.Converter.Docx.E2E/testdata/generated-layout.docx \
--flow-fixed-only