如何创建 LuaTeX 结点

LiYanrui posted @ Jun 25, 2009 06:48:19 AM in Dream of TeX with tags luatex tex , 4582 阅读

LuaTeX 的诸多结点(Node)类型中,目前我仅关心 glyph、glue、kern、penalty 和 rule。LuaTeX Reference 在第 8 章中粗略介绍了这些结点的数据结构,但是要真正理解它们,需要从 Knuth 的 The TeXbook 中获得一些 TeX 常识。

说两句废话。虽然我很想认认真真地将 The TeXBook 读上三遍,但理想与现实的差距决定了我迄今只是按照个人兴趣琐碎地看了一部分内容。并且,有些幸运地是那部分让我感兴趣的内容现在恰好覆盖了 LuaTeX 中我同样感兴趣的部分。不过,我现在已经非常清楚了,要想理解 LuaTeX,必须要理解 The TeXbook,要理解 TTF & OTF 字体细节,要熟悉 Lua。好在,我目前的目标仅仅是学会使用上述的那 5 种结点来解决几个事关中文处理的问题,因此我也许可以在不了解 LuaTeX 的情况下完成我的目的。

下面以 glyph 结点的创建为例,来说明如何在自己写的外部扩展中创建那些结点并插入到文档中。

在 LuaTeX Reference 的 8.1.2.12 节描述了 glyph 结点的组成,结构比较复杂,而且我也不愿意深入 LuaTeX 底层去探究,因此我就在 luatex-fonts-merged.lua 文件中发现了 Hans 写的几个简单的函数,其中一个函数叫做 nodes.glyph,它的定义是这样的:

function nodes.glyph(fnt,chr)
    local n = copy_node(glyph)
    if fnt then n.font = fnt end
    if chr then n.char = chr end
    return n
end

看到这个函数,我着实有些高兴,因为它足够简单。在这个函数的第一行中,调用了 copy_node () 函数,而它实际是 node.copy (),这是 LuaTeX 的结点复制函数(LuaTeX 的结点处理函数的详细说明,可以在 LuaTeX Reference 的 4.3 节中找到)。之所以这般肯定,是因为在 luatex-fonts-merged.lua 文件的开始处中有着这样的定义:

local copy_node         = node.copy

在 nodes.glyph () 函数中,copy_node () 函数的作用是基于一个已经定义的 glyph 结点去创建一新的 glyph 结点。那个已经定义的 glyph 结点是怎么来得呢? luatex-fonts-merged.lua 文件如是说:

local glyph      = nodes.register(new_node("glyph",0))

这个 new_node () 函数所玩的把戏通那个 copy_node () 差不多,只不过它对应的是 LuaTeX 的新结点构造函数 node.new ()。这个函数可以接受两个参数,在这里第一个参数表示新建结点的类型是 'glyph',其 subtype 是 0。根据 LuaTeX Reference 的 8.1.2.12 节的描述,可以这确定该 'glyph' 结点是一个字符(character)结点。

好了,经过这番探寻,可以大致知道 nodes.glyph () 函数是怎么来的了,现在来考察一下它所接受的那两个参数 fnt 和 chr。根据我多日以来折腾 LuaTeX 的经验,初步判定 fnt 表示某款字体的“代号”,而 chr 表示该款字体中某个字符的“代号”。这个,可以实证一下的,前提是要知道 LuaTeX 在编译文档时加载了哪些字体。

下面是一份完整的 tex 文档,使用 luatex 编译它,可以在终端里输出 luatex 在编译该文档时所加载的所有字体的代号。 

$ cat test.tex
\directlua{
    for i, _ in font.each () do
        texio.write_nl (i)
    end
}

Hello World!

\bye


$ luatex test
This is LuaTeX, Version beta-0.41.0-2009062400 (Web2C 2009)
 \write18 enabled.
(test.tex (/home/garfileo/dlf/tex/micro-luatex/texmf-luatex/tex/generic/luatex/luatex-basics.tex) (/home/garfileo/dlf/tex/micro-luatex/texmf-luatex/tex/generic/luatex/luatex-fonts.tex <luatex-fonts-merged.lua> <luatex-fonts.lua loaded in 0.032 seconds>)
(/home/garfileo/dlf/tex/micro-luatex/texmf-luatex/tex/generic/luatex/luatex-mplib.tex)
1
2
... (此处省略了一部分)
50
[1{/home/garfileo/dlf/tex/micro-luatex/texmf/fonts/map/pdftex/plain/pdftex.map}] ){/home/garfileo/dlf/tex/micro-luatex/texmf/fonts/enc/dvips/lm/lm-rep-cmrm.enc}</home/garfileo/dlf/tex/micro-luatex/texmf/fonts/type1/public/lm/lmr10.pfb>
Output written on test.pdf (1 page, 24312 bytes).
Transcript written on test.log.

我从该文档的编译输出信息中看到了 LuaTeX 一共加载了 50 种字体。这样的话,我可以试探一下“代号”为 30 的字体,也就是说,我可以将 nodes.glyph () 函数第一个参数 fnt 取值为 30;对于第二个参数,假定前面的猜测是正确的话,这里可以在 26 个英文字符中随便取一个 "E",它对应 Unicode 的十进制编码为 69。下面我们就将这个字体代号为 30 的第 69 个字符插入到文档中。

与其说将这个字体代号为 30 的第 69 个字符插入到文档中,不如说是插入到 luatex 编译文档过程中所产生的缓冲区中。我惯用的缓冲区就是在文本断行之前的内容缓冲区,它可以通过 LuaTeX 的 pre_linebreak_filter () 回调函数来访问,因为 LuaTeX 会将内容缓冲区的入口作为该回调函数的参数传入。下面的示例可以遍历内容缓冲区包含的结点,并打印出它们的类型。

$ cat test.tex
\directlua{
    local function test (head, groupcode)
        for t in node.traverse(head) do
            texio.write_nl (node.type (t.id))
        end
        return true
    end
    callback.register("pre_linebreak_filter", test)
}

\TeX

\bye


$ luatex test
This is LuaTeX, Version beta-0.41.0-2009062400 (Web2C 2009)
 \write18 enabled.
(test.tex (/home/garfileo/dlf/tex/micro-luatex/texmf-luatex/tex/generic/luatex/luatex-basics.tex) (/home/garfileo/dlf/tex/micro-luatex/texmf-luatex/tex/generic/luatex/luatex-fonts.tex <luatex-fonts-merged.lua> <luatex-fonts.lua loaded in 0.031 seconds>)
(/home/garfileo/dlf/tex/micro-luatex/texmf-luatex/tex/generic/luatex/luatex-mplib.tex)
whatsit
hlist
glyph
kern
hlist
kern
glyph
penalty
glue
[1{/home/garfileo/dlf/tex/micro-luatex/texmf/fonts/map/pdftex/plain/pdftex.map}] ){/home/garfileo/dlf/tex/micro-luatex/texmf/fonts/enc/dvips/lm/lm-rep-cmrm.enc}</home/garfileo/dlf/tex/micro-luatex/texmf/fonts/type1/public/lm/lmr10.pfb>
Output written on test.pdf (1 page, 17852 bytes).
Transcript written on test.log.

上述示例的编译结果是 TeX 的 logo:

现在稍微修改一下上面的示例,胡乱地向内容缓冲区插入字体代号为 30 的第 69 个字符结点,这需要借助结点插入函数 node.insert_after () 或者 node.insert_before () 的帮助。

\directlua{
    local function test (head, groupcode)
        for t in node.traverse(head) do
            node.insert_before (head, t, nodes.glyph (30, 69))
        end
        return true
    end
    callback.register("pre_linebreak_filter", test)
}

编译后,可以发现 TeX 的 logo 面目全非了:

我们可以用类似的探究方法来理解 nodes.glue ()、nodes.rule ()……等函数的用法。这里,值得一提的是 nodes.rule () 函数,它对应于 TeX 的 \hrule 宏,可以用来画黑色方块。在 CTeX 论坛里,有人提出这样一个问题:如何在中文段落末行少于两字时给出“丑陋行”的黑色块警告?我们可以利用 nodes.rule () 函数向内容缓冲区中插入黑色块来解决。


登录 *


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