PDFium 组件的文本整形经过一个可安装对象。ConfigureTextShaper 安装每个整形入口都经由的整形器,替换并释放原有者;ActiveTextShaper 返回已安装者,首次使用时创建平台默认;ActiveTextShaperName 报告当前活跃的后端;ClearTextShaper 撤销安装,让默认值得以再次创建。Windows 上默认是 TPdfUniscribeTextShaper。Free Pascal 下有 TPdfHarfBuzzTextShaper,运行时绑定 libharfbuzz,库缺失是一种被报告的状况而不是加载失败
一个接口,两个以完全不同方式划分工作的后端。理解这种不对称,才能防止可移植路径产出整形正确而定位错误的文本
为什么 Windows 后端是一个类,可移植后端是三件套?
因为 Uniscribe 是假装成一个的四个 API。ScriptItemize 按文字系统切分字符串并解析双向级别;ScriptShape 把字符映射到字形;ScriptPlace 计算步进与偏移;ScriptLayout 把得到的运行段排成视觉顺序。建立在它之上的后端无事可做,所以 Windows 整形器是带单个方法的单个类
HarfBuzz 覆盖中间两步。它对方向与文字系统已由调用方决定的运行段做整形与定位,对段落如何切分成运行段、这些运行段以什么顺序出现毫无主见。因此可移植后端补齐其余:双向算法解析嵌入级别,HarfBuzz 的 Unicode 函数按文字系统切分文本,运行段按 UAX #9 规则 L2 产出的视觉顺序排布。双向这一半体量足以独立成单元,见UAX #9 嵌入级别一文
整形器不解析字体,这是刻意的
Uniscribe 从 GDI 设备上下文读取字体二进制。它没有可移植的等价物,在整形单元里发明一个,等于替每个应用决定字体来自 fontconfig、CoreText、应用字体文件夹还是数据库。所以 HarfBuzz 后端接收一个解析器:把字体名映射到 TrueType 或 OpenType 字节的回调。返回 False 会让整形请求失败,与 Windows 上不可读的 GDI 字体同等待遇
uses
FPdfTextShaping
{$IFDEF FPC}
, FPdfTextShapingHb
{$ENDIF}
;
function TFontCatalogue.Resolve(const FontName: WideString;
out FontData: TBytes): Boolean;
var
Path: string;
begin
// 你的策略:fontconfig、CoreText、应用字体文件夹、数据库
Result := FLookup.TryGetValue(LowerCase(FontName), Path);
if Result then
FontData := TFile.ReadAllBytes(Path);
end;
procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
// 所有权移交给单元;启动期间调用一次,
// 在任何文本被整形之前
ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
// Delphi 上平台默认(Uniscribe)按需创建,
// 所以完全无需安装
LogInfo('shaping backend: ' + ActiveTextShaperName);
end;
把字体发现留在整形器之外,还有一个在服务器上显现的好处:同一进程可以用一组与机器上安装内容毫无关系的嵌入字体整形——当输出必须跨主机逐字节可重现时,这正是你要的。组件也提供宿主系统字体提供者,用于确实想要已安装字体的场景,见系统字体提供者一文
结果记录与后端无关,簇是原因
两个后端填同一个 TPdfShapedText:源文本、字体名、字号、字体字节、运行段数组、总宽度、字形数与逻辑字符数。每个 TPdfShapedRun 携带自己在源文本中的跨度、视觉 X 位置、宽度、双向级别与从右到左标志,外加自己的字形。每个 TPdfShapedGlyph 携带字形标识符、步进、X 与 Y 偏移,以及所属簇——以源文本中的起点与长度表示
正是这些簇字段让记录可用而不只是有信息量。整形不是一对一映射:一个天城文音节由四个字符变成一个字形,一个阿拉伯连字合并两个,一个字符也能产出多个附加符。没有簇跨度,你无法放置插入符、做点击命中测试或高亮选区,因为你说不出一个字形属于哪些字符。有了它们,算术是局部的,同一段代码对两个后端都成立
var
Shaped: TPdfShapedText;
R, G: Integer;
begin
if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
for R := 0 to High(Shaped.Runs) do
begin
// 运行段到达时已是视觉顺序,VisualX 已填好
X := Shaped.Runs[R].VisualX;
for G := 0 to High(Shaped.Runs[R].Glyphs) do
begin
EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
X + Shaped.Runs[R].Glyphs[G].OffsetX,
Shaped.Runs[R].Glyphs[G].OffsetY);
X := X + Shaped.Runs[R].Glyphs[G].Advance;
end;
end;
end;
预算放在选项记录里
TPdfTextShapingOptions 携带一个方向加三项上限:最大字符数、最大字形数与最大运行段数,并有填入合理值的 Default 类函数。这些上限不是对畸形输入的偏执;它们是算术。整形会扩张:一个上下文替换激进的字体可以发出比输入字符更多的字形,每隔几个字符就切换文字系统的段落每次切换产出一个运行段。一份为同时放大两者而构造的文档,能把一段朴实的字符串变成一次大分配;从不可信 PDF 整形文本的服务,需要一个自己选定的限制,而不是机器强加的限制
凡是已知方向,就值得显式设置而不是留在自动。自动按段落方向规则从第一个强字符猜测,这对自由文本是对的,对表单字段是错的——字段的方向是字段本身的属性,不是某人在里面输入的值的属性
运行时绑定,而不是构建期依赖
HarfBuzz 后端动态加载库。这是一个有实际后果的部署决策:同一个二进制既能在有 HarfBuzz 的机器上运行,也能在没有的机器上运行,第二种情况报告能力降级而不是无法启动。对交付给其他开发者的库,这是唯一可行的安排——你不能要求 PDF 组件的每个使用者都去获取并匹配版本一个他们可能根本不需要的整形库
调用方的相应规则是检查。平台没有默认值又未配置任何东西时,ActiveTextShaper 返回 nil,整形入口把它报告为整形器不可用,而不是整形失败。这是两种不同的问题,值得不同的消息:一个是部署缺口,另一个是字体或文本问题
安装一次,在任何整形发生之前
安装会替换并释放前一个整形器,所以反复调用安全但无意义,而在另一线程整形期间调用则完全不安全。在启动期间做。之后若需回退平台默认,传入 nil——这也是测试结束时撤掉测试替身的方式
后端一旦安装,测量与换行在两个平台上行为一致,因为它们消费的是运行段与字形度量,而不是直接调用平台;换行模型见文本测量与自动换行一文。组件支持的平台与工具链列在 PDFium Delphi component 产品页