ConTeXt MkIV 中文标点间距压缩问题的解决方案

有关在 ConTeXt MkIV 中解决中文标点符号微排版的话题,在背景一文中已做过介绍。为这个问题已经潜水很久,现在才开始准备浮出水面。这篇文章主要总结一下这段时间所做过的一些有关中文标点间距压缩方面的尝试,这样即便我受到一些外因的限制无法完成此事,也可对其他有志于此者有所帮助。

Posted by LiYanrui Nov 28, 2009 07:49:20 PM


用构造 hlist 的方式实现中文标点符号的“升降”

本来认为无解的事情,结果还比较有灵感,居然想到要去查看“\TeX“所对应的结点类型,结果发现是它是"glyph + kern + hlist + kern + glyph" 的形式,正好与“\TeX“的宏展开式“T\kern-.1667em\lower.5ex\hbox{E}\kern-.125em X”相对应,可见在 LuaTeX 中那个下沉的 "E" 是通过 hlist 封装起来的。

于是自己写函数去造一些中文标点符号的 hlist 结点,然后在遍历文档结点时,用经过 hlist 封装的结点去替换原有结点,这样就可以实现对中文标点符号上下方向的位置调整了。

例如对于文鼎宋体,是台湾公司免费发布的,所以其中的中文标点大都是按照台湾的习惯来设计的,标点符号都是处于中间的,如下图的中文句号:

如果将这个句号字符采用 hlist 的方式封装,例如:

local function full_stop (g)
    local n = node.copy (hlist)
    n.attr = nil
    n.width = fontdata[g.font].size
    n.depth = 0
    n.shift = 0.3 * n.width
    n.glue_order, n.glue_set, n.glue_sign = 0, 0, 0
    n.list = nodes.glyph (g.font, g.char)
    n.dir = 'TLT'
    return n
end

用上述 full_stop 函数所生成的结点替换掉文档中出现的句号字符结点,效果如下图所示:

现在,在 ConTeXt MkIV 对中文标点符号的微排版方面,感觉基本上没有什么太困难的问题了。这段时间,先把以前的各个环节的尝试总结一下,先制作一份文档出来。

Posted by LiYanrui Nov 26, 2009 04:50:57 AM


又可以接着做 ConTeXt MkIV 的中文标点处理了

今天在翻看 LuaTeX 文档时,无意中看到已经看了好几遍的一句话:

Callback assignments are always global. You can use the special value nil instead of a function for clearing the callback.

这让我想起四个多月前败了一次就放弃的 ConTeXt MkIV 中文标点处理的事情。当时只是怀疑 Hans 在做字体的 Fallback 时已经利用了那个pre_linebreak_filter 回调函数,以致于只要我使用了自定义的 pre_linebreak_filter 回调函数就会把 Hans 的覆盖掉,所以导致 ConTeXt 的字体机制不正常。所以就简单地放弃了。

刚才进入 /path-to/texmf-context/tex/context/base 目录 grep 了一下,发现 Hans 的确是用了 pre_linebreak_filter:

$ grep pre_linebreak_filter *
node-ini.mkiv:%       callbacks.push('pre_linebreak_filter')
node-ini.mkiv:%       callbacks.pop('pre_linebreak_filter')
node-pro.lua:function nodes.processors.pre_linebreak_filter(head,groupcode)
node-pro.lua:callback.register('pre_linebreak_filter', nodes.processors.pre_linebreak_filter)

仔细想想,其实是有办法来解决这个回调函数冲撞的问题的,只是我对动态语言太缺乏实战经验,所以一直逃避至今。

大致可以这样解决:

local old_pre_linebreak_filter = callback.find ('pre_linebreak_filter')

function new_pre_line_break (head, groupcode)
    old_pre_linebreak_filter (head, groupcode)
    
    -- 下面是我自己的代码
    ... ...
    
    return true
end

callback.register ('pre_linebreak_filter', new_pre_line_break)

这个问题一旦解决了,那么基于以前所做的那些有关 LuaTeX 的尝试,实现中文标点横排压缩和边界对齐只是个体力活。

Posted by LiYanrui Nov 24, 2009 05:54:52 AM


罚点

在 TeX 中,罚点(penalty)可以用于控制在某个字符后是否必然会发生断行,我们可以利用它来实现标点禁则。在 LuaTeX 中,函数 nodes.penalty () 可以制作一个罚点,只需要使用 node.insert_before () 或 node.insert_after () 函数将罚点插入不希望断行的字符结点之前或之后即可。

Posted by LiYanrui Jul 03, 2009 03:52:30 PM


标点在左右边界处的伸出

先把要解决的问题交代一下。所谓“标点在左右边界处的伸出 (protruding)”,可以通过下面的示例来理解。

第一个示例是标点在版面左侧文本边界处未有伸出的示例。

第二个示例是标点在版面的左侧文本边界处伸出的示例,伸出的目的是让文字在左边界处垂向对齐,让版面更齐整。

同理,为了排版的美观,对于标点出现在文本右侧边界处也需要伸出处理。本文要讲述的是怎样控制 LuaTeX 来处理这类问题。

Posted by LiYanrui Jun 30, 2009 04:56:35 PM


如何创建 LuaTeX 结点

LuaTeX 的诸多结点(Node)类型中,目前我仅关心 glyph、glue、kern、penalty 和 rule。LuaTeX Reference 在第 8 章中粗略介绍了这些结点的数据结构,但是要真正理解它们,需要从 Knuth 的 The TeXbook 中获得一些 TeX 常识。我目前的目标仅仅是学会使用上述的那 5 种结点来解决几个事关中文处理的问题,因此我也许可以在不了解 LuaTeX 的情况下完成我的目的。本文以 glyph 结点的创建为例,来说明如何在自己写的外部扩展中创建那些结点并插入到文档中。

Posted by LiYanrui Jun 25, 2009 06:48:19 AM


背景

TeX 相关软件中文字体嵌入一个存在已久的问题水落石出,兼谈不拘小节的中文字体设计中 Wang Yue 谈到了这样的问题:

中文字体设计不拘小节也让我也想到了另一个问题,用先前,中文用户使用 XeTeX,需要频繁地切换中英文字体,后来 XeTeX 开发者不得不提供了一个机制来让字体切换变得不那么折腾。而我和 ConTeXt 开发者交流中文排版问题,还要煞费苦心地讲怎么切换,需要编程实现复杂的虚拟字体机制来实现。这个都归罪于中文字体普遍地缺乏高质量的英文部分,仔细看看 simsun 或者 simhei 的英文部分,就可以看出有多么夸张了。

如果说这个问题的原因是中国的字体公司,向来没有很好的英文字体设计基础,同时对这个问题也不加以重视,那么中文标点的设计,就没有丝毫的可以开罪的地方了,这个问题直接导致用户和开发者都非常为难。我们知道,高质量的中文排版,标点并不是占据一个中文字符的位置,而要比中文字符略小。 同时,标点之间需要存在压缩,比如逗号后紧紧跟随的关门引号,需要使用类似 kerning 的特性把两个 glyph 的距离减小。另外,类似破折号和省略号, 其实应该放在一个 glyph 中而不应该分开。而现在所有的中文字体的糟糕程度,竟然到所有的标点符号都占用一个中文字符距离的程度。本来这个问题如果中文 字体设计得当,使用默认的排版算法,就基本上能够解决一般的中文的排版问题,而现在糟糕的设计就使得排版软件的设计难上加难。首先我们需要重新定义一系列 的新算法和新规则,然后需要手工赋值去确定标点的大小和两个标点连在一起时候的压缩程度。更麻烦的是,不同字体中的相同的 glyph,比如逗号或者句号, 往往会在这个 box 的不同的位置,大小也会千差万别。调好了中易宋体的冒号和开门引号,把相同的数值使用到中易的隶书中,顿时两个符号就会挤在一起,这就 使得如果不针对每一个字体仔细调整,高质量的中文排版就几乎不可能。

我是受了 Wang Yue 几篇文章忽悠才开始使用 ConTeXt MkIV(下面简称 MkIV)的,迄今为止一年又半载。期间,看见了 MkIV 的效率以及 CJK 文字支持的诸多进步。比如,现在利用 MkIV 的 fallback 字体机制,已经很好地解决了中西文字体混合的问题,也就是说用设计比较专业的英文字体去替换中文字体所包含的英文字符部分。此外 Wolfgang Schuster 实现了 simplefonts 模块,提供了类似 XeLaTeX 的 fontspec 宏包那样的功能,简化了 ConTeXt 字体配置过程。但是,对于中文标点符号的处理,迄今也未有解决。虽然 Hans 多次许诺将来会解决这个问题,但是我们不知道他说的将来是什么时候了。

为了让 ConTeXt 对中文支持的更好一些,我想自己动手解决这个问题。假如将来 Hans 真的兑现了他所说的,提供了很好的中文支持,大不了我就扔掉这块工作。

一开始,我不知道该从哪里入手,在 ConTeXt 的 base 目录里 grep 了好长时间,发现有关中文断行以及标点处理的代码集中在 font-otf.lua 文件里,当时我对这个文件进行了一些 hack,只得到到了轻微的改善,不过我却受到了鼓舞,打消了对 ConTeXt 的畏惧。后来 Hans 对 CJK 文字的处理进行了调整,原先在 font-otf.lua 文件里的 CJK 文字处理部分的代码被重新改写了,并且放在了 scrp-ini.lua 和 scrp-cjk.lua 文件中,我对这两个文件继续 hack,工作成果见 http://bbs.ctex.org/viewthread.php?tid=48562。在此期间,把学习 MkIV 过程中胡乱写的一些笔记整理了一下,挂在了 http://bbs.ctex.org/viewthread.php?tid=45237。我不是一个有耐心的人,做什么事情都是三分钟热度,再加上 MkIV 层出不穷的 bug,所以有一段时间对 MkIV 的喜欢的热度也退却了很多。

在 hack MkIV 的时候,我感觉 MkIV 对中文标点的处理方式很脏,太依赖具体的字体。像 scrp-cjk.lua 文件中的许多参数,对 AdobeSongStd-Light.otf 字体是适合的,但是换成 simsun.ttc 就不行了,具体见 http://bbs.ctex.org/viewthread.php?tid=47559。仅仅是因为这个看似很小的问题,我还几次都想回到 XeTeX + xeCJK 的环境里。如果 OpenOffice.org 在排版方面不是那么废柴,我甚至都可以放弃 TeX。

再后来,看到 Wang Yue 频率较高的宣扬利用 bbox 来解决中文标点间距压缩问题,说这样就可以做到不依赖具体的字体。一开始,我对此是不以为然的,而且我也没法以之为然,因为那时我只是比较浅薄地知道 scrp-cjk.lua,其它的我都不知道。直到后来,孙文昌(CTeX 论坛的 mytex)老师给出了一个示例(见 http://bbs.ctex.org/viewthread.php?tid=49757)之时,我才大致明白所谓 bbox 的解决方式是怎么一回事。另外,Wang Yue 精简 ConTeXt Minimals,作了一个 mini luatex 包,我从他那里学会了如何生成 luatex 格式文件,如何在 tex 文件里加载自己写的 lua 程序。

我对 LuaTeX 和 MkIV 的认识就是这样私有似无地累积起来了,直到有一天我感觉可以从 LuaTeX 的层面上来实现对中文的单独处理,这样我就可以摆脱 Hans 的那套目前有些残废的中文支持方式,不必再担心他每一次升级 beta 版本而让我没法再用中文。虽然事实上还是有隐患,因为 Hans 为 LuaTeX 分离出来的字体处理部分,也就是 luatex-fonts-merged.lua 文件,它也是不稳定的,但是至少要比 MkIV 稳定。这段时间,我一直在折腾这件事情,随着最后一个有关标点边界对齐问题得到了解决,现在整套方案终于有了一个大概的眉目。

现在,打算正式开始解决这个问题。在此过程中,我会再重新整理一下思路,以连载的形式记录整个过程,希望能够对喜欢 LuaTeX与 MkIV 的同学有所帮助。

Posted by LiYanrui Jun 19, 2009 08:03:39 PM


走弯路了!

在“寻找标点符号及其包围盒”中,发现在 luatex-fonts-merged.lua 中需要将 define.read () 函数里的 "fontdata.cache" 的值被设置为 "yes" 才可以访问到字符包围盒信息,便发信给 Hans 询问为啥默认要设置为 "no"。结果 Hans 的回答让我很沮丧,原来通过我说的那种途径获得包围盒信息是绕了一个大圈子。

事实上,每个字体的信息都被存放在一个全局的表 fonts.ids 里,而且这个表在“断行”中我已经用过了,只是当时没理解这个表的作用。等后来稍微明白了 LuaTeX 处理字体的机制后,又把那个表给忘记了。

正确的做法是这样的:

local fontdata = fonts.ids

function f4zhcn.pre_linebreak_filter (head, groupcode)
   for t in node.traverse(head) do
      if is_cjk_ideo (t) then
         texio.write_nl ('*** CJK Ideo ***')
      elseif is_cjk_puncts (t) then
         texio.write_nl ('*** CJK Punct ***')
         for k in pairs(fontdata[t.font]) do
            texio.write_nl (k)
         end
      end
   end
   return true
end

Posted by LiYanrui Jun 18, 2009 04:17:39 PM