PDFlibPas 用 DrawTextFlowColumns 把一段保留的文本流排入 1 到 64 个等宽分栏,一旦调用了 SetTextFlowLanguage 和 SetTextFlowHyphenation,就会用有边界的、具备语言感知能力的连字符断行来断句。支持九种语言,而且语言设置可以从文档 Catalog 的 /Lang 值继承,不必逐个文本流单独设置
这两项功能存在的理由是同一个:在一个窄栏里,朴素的断行方式会从看起来像排版,变成看起来像一份 bug 报告
为什么两端对齐的文本在窄栏里会散架?
因为两端对齐是把多余的空间分配到一行内的词间距里,而多余多少则取决于能放下什么内容。在一个宽的排版尺度下,多余的空间很小,眼睛根本不会注意到。把宽度减半,一个放不下的长单词就会被挤到下一行,把它前面的词全部留下来吸收这些空出来的空间。连续三行出现这种情况,就会产生排版师称为"河流"的竖直白色通道,读者感受到的则是一段读起来莫名其妙难以跟随的文字,却说不清是为什么
连字符断行允许在单词内部断开,从根源而不是从症状上解决了这个问题。德语和荷兰语的复合词让这一点没有商量的余地:一个 60 毫米宽的栏里放一个 24 个字符的名词,没有断点根本不会有好结果。英语对没有断点的容忍度更高,这也是为什么以英语为先的产品,排版代码往往在第一次遇到德语客户时就翻车了
支持哪些语言,语言设置又从哪里来?
连字符断行支持英语、德语、荷兰语、法语、西班牙语、意大利语、葡萄牙语、俄语和土耳其语。可以用 SetTextFlowLanguage 为每个文本流单独显式设置,也可以让它从文档 Catalog 的 /Lang 条目继承——这正是一份带标签的无障碍文档本来就已经携带的值
这种继承机制值得直接使用,而不是去覆盖它。一份在 Catalog 中声明了语言的文档,是在用同一个地方,把同一个事实同时告诉屏幕阅读器、搜索索引器和连字符断行逻辑,而一个事实本来就该只存在于一个地方。如果你已经在按面向无障碍 PDF 的自动标签一文中描述的方式生成带标签的输出,语言条目已经设置好了,文本流直接跟随它就行
var
Lib: TPDFlib;
Flow, Drawn: Integer;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.AddTrueTypeFont('Georgia', 1);
Lib.SetTextSize(10.5);
Flow := Lib.NewTextFlow(ArticleBody);
try
Lib.SetTextFlowLanguage(Flow, 'de');
// 启用,断点前至少 3 个字符,断点后至少 3 个字符
Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
Lib.SetTextFlowMinLines(Flow, 2); // 永远不要留下孤零零的一行
repeat
// 在 480 pt 宽的区域内分三栏,18 pt 栏间距,启用均衡
Drawn := Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);
if (Drawn = 0) or (Lib.TextFlowFinished(Flow) = 1) then
Break;
Lib.NewPage;
until False;
finally
Lib.ReleaseTextFlow(Flow);
end;
Lib.SaveToFile('newsletter.pdf');
finally
Lib.Free;
end;
end;
MinPrefix 和 MinSuffix 是排版参数,不是校验规则
启用标志后面的两个整数,设定的是断点前后必须保留的最少字符数。3 和 3 是一个大多数公司排版规范都能接受的保守默认值。2 和 2 会产生更多的断点机会,但结果明显更难看,因为一行末尾挂着一个两个字母的残片,读起来就像一个拼写错误
当字号较大、每个残片在视觉上都很显眼时,应该调高这两个最小值;只有在栏确实很窄、并且你已经决定紧凑的排版尺度比干净的断行效果更重要时,才应该调低它们。这是一个公司排版规范层面的决策,而不是技术决策,这也正是为什么它是一个参数,而不是一个写死的常量
这里的"均衡"到底是什么意思?
Balance 参数只在一段文字的末尾改变行为。启用均衡后,当剩余的全部内容都能装进这个区域时,各栏会被缩短到行数完全相等,这正是能防止最后一页出现两栏满满当当、第三栏却只孤零零一行文字的手段。当这段文字装不下时,每一栏都会保持完整高度,让这一页尽可能多地承载文字,剩下的内容继续排到下一页
对于连续性的文档来说,这种不对称正是正确的默认行为。如果在一篇连续排版的文章中间也做均衡处理,会在每一页上浪费垂直空间去追求一个没人会注意到的美观效果,因为反正各栏本来就是满的。真正会被读者注意到的是末尾的均衡效果,而这恰好也是它生效的地方
断行按完整单词度量
断行算法按完整单词来度量,而不是逐字符累加宽度,并为那种一整行都放不下的超大 token(比如一个 URL 或一个编号)保留一次有边界的搜索。这让常见情况保持快速,让极端情况保持有边界,而不是反过来
可选软连字符和自动连字符,只有在它们标记的断点恰好就是最终被选中的断点时,才会被渲染出来。这听起来理所当然,却是一个经典缺陷:一个朴素的实现会在度量阶段就写入连字符字符,一旦断点位置发生变化,这个连字符就会遗留在一行的中间。没有什么比一个单词中间冒出一个孤零零的连字符,更能让文本引擎看起来是坏的了
var
Lib: TPDFlib;
Flow, Needed: Integer;
begin
// 先确定版面布局,再开始绘制任何内容
Flow := Lib.NewTextFlow(ArticleBody);
try
Lib.SetTextFlowLanguage(Flow, 'fr');
Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
// 剩余文字在单栏宽度下需要的行数
Needed := Lib.MeasureTextFlow(Flow, 148);
if Needed > 3 * LinesPerColumn then
UseTwoPageSpread
else
UseSinglePage;
Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);
if Lib.TextFlowFinished(Flow) <> 1 then
CarryOver(Lib.GetTextFlowRemaining(Flow));
finally
Lib.ReleaseTextFlow(Flow);
end;
end;
让各个文本框的字体设置保持一致
有一条规则支配着所有基于文本流的排版,值得直白地说清楚:DrawTextFlow、DrawTextFlowColumns 和 MeasureTextFlow 断行时,用的都是它们被调用那一刻所选中的字体。如果在同一个文本流的两个文本框之间改变了字体或字号,或者新开了一页却没有重新选择字体,第二个文本框的断行结果就会和第一个文本框度量出来的结果不一致
这种症状让人抓狂,恰恰是因为它看起来时有时无:第一页装得下的文字,到第二页就溢出了,或者度量出的行数和实际画出来的对不上。在循环开始之前先选好字体,每次 NewPage 之后重新选一次,文本流就会正常工作。当同一段文字里出现混合文字系统时,CJK 与 emoji 文本的自动字体回退一文中描述的解决方案,同时适用于度量和绘制,因此即便跨越了回退片段,宽度也能保持一致
对于文本流只是报表版面中众多元素之一(还有页眉、页脚和数据驱动的区块)的报表布局,数据集报表引擎一文中的组合模式能和分栏文本流干净地结合起来:先度量,再放置固定的版面元素,最后把剩下的区域全部交给文本流
PDFlibPas 是一个面向 Delphi、C++Builder 和 Lazarus 的 PDF 库,完整的 TextFlow 生命周期——创建、绘制、度量、检查、回退和释放——同样通过 DLL 和 ActiveX 接口对外暴露。完整文档见 PDFlibPas Delphi PDF 库页面