技术文章

使用 Delphi 和 HotPDF 在 PDF 中排版中日韩垂直文本

在页面上排版一本日本小说,你首先会注意到文本是沿着列向下延伸的,而不是横向排列的,而且列是从纸张的右边缘向左推进。习惯了这种排版的读者会觉得横向文本略显枯燥。工程上的问题在于,PDF 和几乎所有数字文本系统一样,都是围绕横向基线构建的,从左到右延伸,内容流里根本没有“把这段文字向下写”的概念。因此,当一个 Delphi 应用程序需要为台湾、日本或韩国读者生成证书、诗歌、标牌或传统格式的法律文档时,排版必须手工完成:一个字符在下一个字符下方,一列在上一列的左侧。

HotPDF 提供了一个开关,可为你处理每个字符的记账工作。你设置的字体带有一个 IsVertical 标志,一旦开启,单个 TextOut 调用就会将整个字符串堆叠成一个垂直列,而不是沿着基线延伸。列的放置、从右到左的顺序以及一个悄然重要的字形替换,正是本页剩余部分要探讨的内容。

由 HotPDF 生成的 A4 PDF 页面,展示了堆叠成垂直列的中日韩文本,从右向左阅读
一个单页 A4 文档,其中包含成垂直列的中日韩文本,由一个 Delphi 过程生成。

开关在 SetFont 上

垂直排版不是页面或文档的属性。它是你用来绘制的字体的属性,你可以通过 SetFont 的第五个参数将其打开:

// SetFont(FontName, FontStyle, Size, FontCharset, IsVertical)
// 第 5 个参数将当前字体切换为垂直模式。
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, False); // 横向
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, True);  // 垂直

因为标志附着在字体上,你可以只需通过带不同最后一个参数的另一次 SetFont 调用即可在横向和垂直书写之间切换。一个页面可以在顶部放置一个横向标题,在其下方放置垂直正文,HotPDF 通过按字体对象而不是按页面将这两种模式分开。这就是混合排版成为可能而无需在你这边进行任何特殊模式处理的原因:在垂直的 SetFont 之后的每次 TextOut 都会堆叠,在横向的 SetFont 之后的每次 TextOut 都会沿着基线延伸,以最后一次 SetFont 为准。

第四个参数是 Windows 字符集,与横向调用所采用的相同。传递 DEFAULT_CHARSET 让系统按字符串解析字形,这在这里很重要,因为垂直文档经常混合使用脚本。关于字体的其他一切仍然适用:它必须安装在构建机器上,而且你几乎总是需要 FontEmbedding := True,以便在从未拥有 Arial Unicode MS 的阅读器上文件能呈现相同的中日韩字形。

一次 TextOut 调用就是一列

激活垂直字体后,TextOut 调用不再从给定点横向展开其字符串。它将第一个字符放在顶部,然后将其余字符笔直向下排布,每个字形按字体的行高推进。你传递的 X 固定了列;你传递的 Y 固定了列顶部开始的位置。因此,为了排版一个真正的段落,你每列发出一次 TextOut,并在调用之间向左步进 X,因为中日韩列是从右向左阅读的。

var
  Pdf: THotPDF;
const
  ColTop = 760;   // 每列第一个字形的 Y 坐标(点数)
  ColGap = 28;    // 列之间的水平距离
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'VerticalText.pdf';
    Pdf.FontEmbedding := True;       // 嵌入中日韩字体以实现可移植渲染
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := psA4;

    // 首先是一个横向标题,使用普通的书写模式。
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 16, DEFAULT_CHARSET, False);
    Pdf.CurrentPage.TextOut(60, 800, 0, '唐诗,垂直排版');

    // 将字体切换到垂直模式;下面的每个 TextOut 现在都会堆叠。
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 18, DEFAULT_CHARSET, True);

    // 列从右向左推进,因此 X 在每次调用中都会减小。
    Pdf.CurrentPage.TextOut(520, ColTop, 0, '床前明月光');
    Pdf.CurrentPage.TextOut(520 - ColGap,     ColTop, 0, '疑是地上霜');
    Pdf.CurrentPage.TextOut(520 - ColGap * 2, ColTop, 0, '舉頭望明月');
    Pdf.CurrentPage.TextOut(520 - ColGap * 3, ColTop, 0, '低頭思故鄉');

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

诗歌按照它应有的方式阅读:最右边的一列优先,从上到下,然后视线向左跳到下一列。字符串本身没有任何东西编码该顺序。你在 X 坐标中对其进行编码,在一次次调用中减小它们。如果方向弄反了,诗句就会变成反的,这是将横向排版移植到垂直排版时最常见的错误。

坐标:沿页面向下,从右到左

有两个轴在起作用并且它们向相反的方向拉伸,因此值得精确说明。HotPDF 从页面的左下角开始测量,Y 向上增加,单位为点。因此,一垂直列从高 Y 值(靠近纸张顶部)开始,并且字符从那里下降,因为 HotPDF 为每个字符减去一个行高。你每列设置一次该起始 Y 值,组件将处理下降过程。

水平轴是你手动驱动的。每一列位于其自己的 X,并且后续的列步入更小的 X 值,因为阅读顺序是从右到左。一个合理的节奏是选择最右边列的 X,然后为每次后续调用减去一个固定的列间距,正如示例中使用 ColGap 所做的那样。间距由你选择;太紧相邻列会碰到,太松文字块看起来稀疏。对于 12 到 18 点的正文,比字体大小稍大一点的间距阅读起来很舒适。

必须改变形状的字形

有一些字符并不只是其横向形式的旋转副本;在文本变成垂直时它们必须被替换。HotPDF 为你处理的一个是日文长音符号 (U+30FC),即出现在诸如 コーヒー (咖啡) 等片假名单词中的长元音横条。横向绘制时它是基线上的一个短破折号。如果你把同一个字形堆叠到一列中,它将平躺穿过这列,这是错误的:在垂直日文中该符号变成一个垂直的笔画,连接它所在的两个字符。HotPDF 在垂直路径中检测到 U+30FC,并将其渲染为垂直条 (U+007C),因此长元音符号指向正确的方向,无需你做任何工作。

这单一替换涵盖了打破大多数天真实现的案例,但值得了解普遍问题的更深层次所在。完整的垂直排印还会将拉丁字母和西方标点符号旋转九十度,移动小假名,并将括号和逗号重新定位为它们的垂直形式,而对此的完整实现存在于字体的 OpenType 垂直特性中,而不是固定的规则中。当字体携带它们时,HotPDF 可以利用这些特性:垂直替换 (vertvrt2 GSUB 特性) 以及垂直字距调整 (vkrnvpal GPOS 查找) 都是可选择的,仅当活动字体实际定义它们时才会沿着垂直路径应用。对于像 Arial Unicode MS 这样单一种类的广覆盖面字体中混合的中日韩文本,内置的 U+30FC 处理加上均匀的行高步长足以为你生成正确、可读的列;当你转移到为精细垂直排版而设计的字体,并希望其本来的假名定位和字符间距时,OpenType 特性就显得重要了。

在一页上混合脚本和方向

现实的文档很少是纯粹的。一个垂直的日文页面可能会带有一个横向的英文标题、沿着底部的页码、或者在日文旁边垂直排版的一块韩文。因为垂直标志是一个字体级别的开关,所以你可以通过交替进行 SetFont 调用来组合这些,而不是管理任何页面范围的状态。设置一个横向字体,写下页眉和页脚的页码,设置一个垂直字体,排版列,再次为页脚设置横向字体。每个区域都会拾取最近一次 SetFont 的模式,因此唯一需要的准则是每当你改变方向时都调用它。

当脚本混合时有一个细节需要计划:中日文和韩文的表意文字接近方形并以均匀步长堆叠,但是嵌入垂直列的拉丁文字运行段落并没有那么均匀的推进。如果你在否则是垂直的文本内需要几个拉丁词汇,仔细决定它们是应该被旋转以沿着列向下排布,还是直立放置作为一个短的水平嵌入片段,并通过其自己的 TextOut 定位该片段,而不是让其乘坐为表意文字准备的垂直步长。把混合运行部分当作它自己的布局问题处理可以保持列的节奏完好。

从示例到生产

组件很小而且它们的组合是可预测的。在 SetFont 中打开垂直标志,从固定的顶部 Y 每个列发出一次 TextOut,并跨调用减小 X 以便列从右向左阅读。嵌入字体以便中日韩字形能够顺利传输到阅读器的机器上,并让组件处理 U+30FC 长元音标记,以及(如果字体支持的话)OpenType 垂直特性。从此,使排版投入生产主要是算术工作:从测量的文本块宽度推导列 X 的位置,将长段落分成适合页面高度的列,并为横向页眉和页脚保留空间。

关于这建立在更广泛的文本和字体表面,包括横向 TextOut 约定和 Unicode 字体注册,请参阅 多语言的 Hello World 示例TextOut 示例。这里展示的 SetFont 垂直开关和 TextOut 调用都是 Delphi 和 C++Builder 的 HotPDF 组件 的一部分。