断行

LiYanrui posted @ Jun 14, 2009 05:02:18 PM in Dream of TeX with tags luatex , 6033 阅读

在那篇“使用 luatex mini 包处理中文” 的文章中,我制造了一个不会断行的中文排版示例。中文断行问题的解决方法,以我的智慧只能想到两种。第一种方法是自己提出一个断行算法并程序实现。第二种 方法就是利用 LuaTeX 提供的断行算法。前者适合勤劳而且又懂 TeX 的人,而且 LuaTeX 也提供了相应的支持。很不幸,我又懒,又不懂 TeX,所以只好尝试第二种方法。

比较弱智的原理

早在 1 年多以前,我曾经卧薪尝胆几日,潦草地看了几眼  The TeXbook,并且想到到一个很弱智的“断行”方法,详情见我的另一篇文章“TeX 的 glue 与中文断行”,大概的思路如下例所示:

\font\a=file:simsun at 24pt \a

\def\zhglue{\hskip0em plus .5em minus.5em}

山\zhglue 近\zhglue 月\zhglue 远\zhglue 觉\zhglue 月\zhglue 小\zhglue ,\zhglue
便\zhglue 道\zhglue 此\zhglue 山\zhglue 大\zhglue 于\zhglue 月\zhglue 。\zhglue
若\zhglue 人\zhglue 有\zhglue 眼\zhglue 大\zhglue 如\zhglue 天\zhglue ,\zhglue
当\zhglue 见\zhglue 山\zhglue 高\zhglue 月\zhglue 更\zhglue 阔\zhglue 。\zhglue

\end

原理就是在每个中文字符之间插入一个 0 宽度并且伸缩宽度分别为半个字宽的 glue,这样就可以直接用 LuaTeX 提供的断行算法来处理中文断行了,不信就亲自尝试一下。现在,我要做的就是用程序实现在中文字符之间自动插入这样的 glue。

LuaTeX 是这样来玩的

luatex 最大的好处就是让用户可以在 TeX 文档里使用 lua 来完成原本应该由 TeX 宏来完成的那些“编程”方面的工作,比如在中文字符之间自动插入 glue 就是一个编程问题。

要在 TeX 文档里执行一段 lua 代码是很简单的,只需要将 lua 代码塞到 \directlua 宏中即可,例如:

\directlua{%
    a = {'null-terminated', 'dollar-terminated', 'Pascal shortstring'}
    tex.sprint(a[math.random(1,3)])
}

\end

该示例实现了从一个 lua 字串表中随机选择一个字串并打印出来。

为了保持 tex 文档的干净,当需要执行较长的 lua 代码时,建议采用 lua 模块的方式将 lua 代码隔离出来。譬如,可以将上例中的代码组织成下面的 lua 模块:

mymodule = {}

function mymodule.random_str ()
    a = {'null-terminated', 'dollar-terminated', 'Pascal shortstring'}
    tex.sprint(a[math.random(1,3)])
end

return mymodule

然后在 \directlua 宏里装载 mymodule 模块:

\directlua{%
    dofile (kpse.find_file ("mymodule", "lua"))
    mymodule.rand_str ()
}

\end

在 \directlua 宏里使用了 kpse 库来查找文件,所以要么将 mymodule.lua 文件放在 tex 文件所在目录,要么就将它放到 tex 目录树的某个位置,然后再刷新一下目录树,总之是要让 kpsewhich 可以找到该文件。一旦加载了 mymodule 模块,就可以执行其中的函数了。

结点与回调函数

http://luatex.bluwiki.com/go/Traversing_TeX_nodes 上面提供了一个简单的示例,可以遍历一份 tex 文档所包含的所有“结点”并打印出它们的类型。

luatex 采用结点来表示 tex 的各种对象,目前大概有 50 多种结点类型,譬如 hlist, vlist, glyph, glue, kern 等等。现在我只关心 glue 和 glyph 这两种类型。至于其它结点类型,如果很熟悉 TeX,理解它们应该很简单。

在“弱智的原理”一节中所提到的那个 \zhglue 就表示了一种 glue 结点。所谓 glue,顾名思义就是“胶水”,它可以将一个又一个的字符粘连在一起,而且很容易折断,也很容易拉长一些或压缩一些。如果没有 glue,那么那些字符是直接靠在一起,TeX 的力量不足以将它们折成一行又一行的摆出来。

至于 glyph 结点就很好理解了,文档中可见的每个字符都是一个 glyph 结点。下面,我们来关心一下 glyph 结点。先写一个 lua 模块——f4zhfont.lua,内容如下:

f4zhcn = {}

local glyph = node.id('glyph')

function f4zhcn.travnode (head, groupcode)
   for t in node.traverse(head) do
      if t.id == glyph then
         texio.write_nl ("*")
      end
   end
   return true
end

return f4zhcn

然后在 tex 文档中加载该模块:

\directlua{%
dofile (kpse.find_file ("f4zhcn", "lua"))
callback.register("pre_linebreak_filter", f4zhcn.travnode)
}

\font\a=file:simsun at 24pt \a

山近月远觉月小,便道此山大于月。若人有眼大如天,当见山高月更阔。

\end

然后,使用 luatex 编译该 tex 文档,在终端以及 log 文件中都可以看到输出 32 个 "*" 字符,这个字数恰好等于王守仁这首诗的字数(包含标点)。这意味着,我们遍历了文档包含的所有 glyph 结点。

是怎么做到的呢?这要感谢 luatex 提供的 pre_linebreak_filter 回调接口。

luatex 在处理文本断行的过程中向用户提供了许多接口,其中我只看懂了 pre_linebreak_filter, linebreak_filter 和 post_linebreak_filter 这三个接口的用法。pre_linebreak_filter 用于断行之前进行用户自定义的准备工作。 linebreak_filter 是让用户自己实现断行算法来替代 luatex 默认的断行算法。post_linebreak_filter 用来对断行处理结果进行用户自定义的善后工作。

pre_linebreak_filter 接口的参数有两个,第一个参数是一个结点列表的首结点,第二个参数是结点列表的类型。结点列表的类型有很多种,不过在这里我只关心一种,那就是 main vertical list 类型,因为它表示一个段落,而我就是要处理段落中的 glyph 结点。

可以利用 luatex 的 callback.register 可以让 pre_linebreak_filter 这个函数指向我写的 f4zhcn.travnode 函数。这样 luatex 就会自动执行我想实现的。

经过这样潦草地叙述,或许能让你大致明白上述示例代码的功能。到此为止吧。因为再深入的东西,我也不甚了解,也许我也没有必要去了解。

断行

现在,终于到了实现断行的时刻了。重新改写 f4zhcn.lua 如下:

f4zhcn = {}

local glyph = node.id('glyph')

local insert_node_after = node.insert_after
local make_glue_node = nodes.glue
local fontdata = fonts.ids

local inter_glue_width     = 0
local inter_glue_stretch   = 0
local inter_glue_shrink    = 0

local inter_glue = {
   width_factor   = 0.0,
   shrink_factor  = 0.0,
   stretch_factor = 0.25,
}

local function set_inter_glue(font,data)
    local parameters = fontdata[font].parameters
    local quad = (parameters and parameters.quad or parameters[6]) or 0
    inter_glue_width     = data.width_factor   * quad
    inter_glue_shrink    = data.shrink_factor  * quad
    inter_glue_stretch   = data.stretch_factor * quad
end

function f4zhcn.travnode (head, groupcode)
   for t in node.traverse(head) do
      if t.id == glyph then
         local font = t.font
         set_inter_glue (font, inter_glue)
         insert_node_after(head, t,
                  make_glue_node (inter_glue_width, inter_glue_stretch, inter_glue_shrink))
      end
   end
   return true
end

return f4zhcn

代码看上去多了一些,实际上很简单。无非就是做了一个 inter_glue 表,用户可以设置其中的三个参数来分别调整 glue 的宽度以及收缩和伸展能力。然后做了一个局部函数 set_inter_glue,它可以根据字体的 quad 值以及 inter_glue 表所定义的因子来确定三个局部变量 inter_glue_width, inter_glue_stretch 与 inter_glue_shrink 的值。

在结点遍历过程中,利用 insert_node_after () 函数(实际上是 node.insert_after () 函数)在当前结点 t 之后插入一个 glue,而这个 glue 是由 make_glue_node () 函数(实际上是 nodes.glue () 函数)生成的。

一切就是这么简单。因为我不懂 TeX 宏编程,不知道这些东西用 TeX 宏来实现是否也是如此简单。下面欣赏一下自己的这次拙劣的工作。

还可以做的更好

上面的程序实际上是错误的,因为对所有的 glyph 结点之后都插入 glue 是不合理的,我需要处理的仅仅是对中文 glyph。不过,那已经不是很困难的问题了,只需要写一个判断当前 glyph 结点的 unicode 编码是否落入中文 glyph 编码范畴即可。我就不再多说什么了。

Yue Wang 说:
2009年6月17日 04:01

哎,歇歇吧,有这个功夫折腾,不如花点时间改掉luafflib.c,这样你就功高盖世了:)

Avatar_small
LiYanrui 说:
2009年6月17日 04:57

@Yue Wang:

现在,我还能忍受 fontforge 的速度和内存。所以先集中精神把字体的问题解决掉,毕竟现在我没有站在你那个高度上

现在,正在想辙从 glyph 结点获得字符的 boundingbox 信息,要是这几天搞不定,就得请教 taco 了。本来是想请教你的,不过看你那么庄重的告别 tex 社区,就没好意思 :-)

Yue Wang 说:
2009年6月17日 05:55

>正在想辙从 glyph 结点获得字符的 boundingbox 信息,

boundingbox在dump出来的tma中是有的。
你打印一下载入tma后的那个保存字体信息的table,看看['boundingbox']被存放在哪了吧。
有时侯写一个类似python的print的function,来调试当前情况的table,是很有必要的,呵呵.

>毕竟现在我没有站在你那个高度上

我觉得改luafflib的难度比折腾hans的中文支持代码简单多了。

Avatar_small
LiYanrui 说:
2009年6月17日 16:25

对 luatex-font-merged.lua 文件里的那几个处理 otf 字体的函数以及从 otf 到 tfm 的函数进行了跟踪,那个 boundingbox 的信息开始是有的,不过稀里糊涂的不知在哪个函数里就没了。

另外,就是看到了 Hans 在合并代码时弄进去了许多重复的语句。

Avatar_small
LiYanrui 说:
2009年6月17日 17:26

呃,总算是找到它了。

最后跟踪到 luatex-font-merged.lua 的那个 define_font () 回调函数的实现上,也就是 define.read () 函数。在这个函数里,只需要将 fontdata.cache = "no" 改成 fontdata.cache = "yes" 就可以了。

boundingbox 信息是在 fontdata.descriptions[glyph_node.char] 表里。


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter