luatex 字体加载
前两篇文章自以为是的介绍了如何构建一个较小的 luatex plain 包以及传统的 TeX 字体支持,所属内容皆为 luatex 自 pdftex 那里所继承的传统的一面,而未触及 luatex 的特性。这篇文章尝试分析 TrueType、OpenType (CID) 字体在 luatex 的加载过程,然后介绍如何使用 Hans 所写的 luatex 字体加载脚本以产生可支持 TrueType、OpenType (CID) 字体加载的 Plain TeX 格式。
理论上的揣测
没读过 luatex 的源码(因为看不懂),所以只好在比较露怯的状态下揣测一番。若有不实,路人不管是甲乙丙丁还是戊己庚辛均可砸砖。
luatex 是借助 fontforge 库读取字体并获取字体相关信息,例如字体名称、字符数以及各个字形(glyph)的编码、宽度、高度与深度等信息,而早先的 TeX 引擎,例如 tex、pdftex 均是通过 .tfm 文件获取字形信息,.tfm 文件需由用户提供。
luatex 的字体处理方式可有效降低 TrueType 与 OpenType 字体使用难度。例如,我有一款 simsun.ttc 字体,若在 luatex 中使用该字体,只需:
\font\song=simsun at 10pt \song 中文宋体
当然,为了达到上述效果,我们还需要写出 TryeType 字体加载代码形成字体表供 luatex 使用。因为 luatex 实现的只是字体加载机制,而非策略。这样做虽然让用户感觉有些麻烦,但是对字体加载过程可进行灵活控制,例如可据此构造虚拟字体。另一款现代的 TeX 引擎 xetex,它的字体处理方式与 luatex 相似,不同的是它采用的是 freetype 库,并且可直接支持 TrueType 与 OpenType 字体的载入,不需要用户去写字体加载代码。
TeX(包括 knuth tex、pdftex、luatex、xetex 等)与具体的字体没有很密切的关联,只是 TeX 的前端和后端与字体有所关联。TeX 的前端包含了字体加载的功能,后端包含了嵌入字体的排版结果输出的功能(当然,knuth tex 的输出是 dvi 文件,不与字体有什么密切关联,但是 div 阅读器需要与字体有关联)。
luatex 的字体表
传统的 TeX 引擎是通过 tfm 文件来解除对具体字体的依赖,这一点从“luatex plain 从零构建”文中产生 Plain TeX 格式时编便已有所体现。luatex 虽然不再需要 tfm 文件,但是它需要字体表。
luatex 参考手册(可从 luatex-src/manual 目录中找到)的第 7 章讲述了 luatex 字体表的数据结构。luatex 字体表是由一个顶层表以及一些子表构成的,下图显示了 luatex 字体表的主要成员。
也就是说,只要我们构建出这样的字体表,并将其交给 luatex,那么 luatex 便会认为我们向它提供了一款字体,哪怕我们在字体表中施展一些诡计欺骗它。
TryeType 字体表
我们通过加载 simsun.ttc 字体来理解 luatex 的字体表结构。
首先,编写用于加载字体的 lua 脚本 test.lua,其内容如下:
local function load_truetype_font (name) local filename, metrics, font = nil, nil, nil filename = kpse.find_file (name,"truetype fonts") if filename then font = fontloader.open(filename) if font then metrics = fontloader.to_table(font) fontloader.close(font) end end return filename, metrics end function define_truetype_font (name, size) local filename, metrics = load_truetype_font (name) local fontdata = {} if metrics then fontdata.name = metrics.fontname fontdata.filename = filename fontdata.type = 'real' fontdata.format = 'truetype' fontdata.characters = {} fontdata.characters[27979] = { index = 9083, width = 655360, } else fontdata = font.read_tfm (name, size) end return fontdata end callback.register('define_font', define_truetype_font)
函数 load_truetype_font 用于打开指定的字体文件,并将提取到的字体信息存储于一个表中,该函数的主要基于由 luatex 提供的 fontloader.open 与 fontloader.to_table 等 API 实现。
在函数 define_truetype_font 函数所做的主要工作是根据 load_truetype_font 返回的字体信息表定义 luatex font 结构,即 fontdata 表,见代码第 18 行~26 行。
注 意,在上述代码中构造 fontdata.characters 表时,采用了很直白的方式指定字符编码,并且仅在 characters 表中存储了中文字符“测”的信息(该硬性的编码仅对 simsun.ttc 字体有效)。这意味着,虽然实际的字体文件中有大量的字符,但是我们在此向 luatex 传递的“字体”却只包含了一个字符。两个硬性编码中,27972 是中文字符“测”的 Unicode 编码的十进制数,而 9083 是中文字符“测”在 simsun.ttc 字体中的字符编号。此外,width 值应当是根据字体的 size 参数以及字体的设计参数计算而来,此处我直接给出了它的数值计算结果,在下文中会给出计算公式。
在 fontdata.characters 表中,index 和 width 是必须要有的成员(width 在版面横排的情况下是必须的),所以我们直接忽略了 height、depth 等成员。
callback.register 函数是将 define_truetype_font 设置为 luatex 在定义字体时的回调函数。这样,当 luatex 编译 .tex 文件时,如果遇到类似“\font\xxxx=yyyy”的代码时,会自动调用 define_truetype_font 函数,尝试加载所需字体。
为了测试上述的 test.lua 脚本中的字体加载代码是否可用,我们需要编写一个 test.tex 文件,其内容如下:
\directlua {% dofile (kpse.find_file ('test', 'lua')) } \font\song=simsun at 10pt\song 测 \bye
因为前面构建的 fontdata.characters 表中只有字符“测”的信息,所以在 test.tex 文件中,也只能对这一个字符进行排版(当然,连续写很多的“测”也是可以的)。
只需对 test.lua 脚本中的 define_truetype_font 略加改动,即可实现任意的 TrueType 字体的加载。修改后的 define_truetype_font 函数如下:
function define_truetype_font (name, size) local filename, metrics = load_truetype_font (name) local fontdata = {} if metrics then fontdata.name = metrics.fontname fontdata.filename = filename fontdata.type = 'real' fontdata.format = 'truetype' fontdata.characters = {} for char, n in pairs (metrics.map.map) do fontdata.characters[char] = { index = n, width = metrics.glyphs[n].width * size / metrics.units_per_em } end else fontdata = font.read_tfm (name, size) end return fontdata end
若使用 luatex plain 包编译上述的 test.tex 文件,我们需要对 texmf.cnf 文件进行更新,添加 TTFFONT 变量的定义。修改后的 texmf.cnf 文件内容如下:
TEXMFMAIN = $SELFAUTOPARENT/texmf TEXMFSYSVAR = $SELFAUTOPARENT/texmf-var TEXMF = {!!$TEXMFMAIN,!!$TEXMFSYSVAR} TEXMFDBS = $TEXMF TEXFORMATS = .;$TEXMF/web2c/luatex TEXINPUTS = .;$TEXMF/tex/{plain,generic}/{base,hyphen,luatex}// TEXFONTMAPS = .;$TEXMF/fonts/map/pdftex// ENCFONTS = .;$TEXMF/fonts/enc/dvips// TFMFONTS = .;$TEXMF/fonts/tfm// TTFONTS = .;$TEXMF/fonts/truetype//:$OSFONTDIR// OPENTYPEFONTS = .;$TEXMF/fonts/opentype//:$OSFONTDIR//
假设 simsun.ttc 位于 /usr/share/fonts/winfonts 目录,那么采用下面的命令即可成功编译 test.tex。
$ export OSFONTDIR=/usr/share/fonts/winfonts $ luatex test
OpenType (CID) 的字体表
CID 字体的加载与普通的 TrueType 字体有所区别。因为 CID 字体,在 fontforge 中需要 .cidmap 文件的支持方可载入。luatex font 的 characters 表的字符 Unicode 编码与字体中的字符索引数值均记录与 .cidmap 文件。
现在,我们可以暂且不管 .cidmap 文件,而是像上文构建 TrueType 字体表时采用硬性编码的方式先做一个试验。
首先,重写 test.lua 文件,主要内容与上文构建 TrueType 字体表所采用的 lua 脚本类似,如下:
local function load_opentype_font (name) local filename, metrics, font = nil, nil, nil filename = kpse.find_file (name,"opentype fonts") if filename then font = fontloader.open(filename) if font then metrics = fontloader.to_table(font) fontloader.close(font) end end return filename, metrics end function define_opentype_font (name, size) local filename, metrics = load_opentype_font (name) local fontdata = {} if metrics then fontdata.name = metrics.fontname fontdata.filename = filename fontdata.type = 'real' fontdata.format = 'opentype' fontdata.characters = {} fontdata.characters[27979] = { index = 1193, width = 655360 } else fontdata = font.read_tfm (name, size) end return fontdata end callback.register('define_font', define_opentype_font)
事实上,与 TrueType 字体表真正有区别的是字符“测”在 CID 字体(AdobeSongStd-Light.otf)的索引值是“1193”。
与上述 test.lua 文件相对应的 test.tex 内容如下:
\directlua{% dofile (kpse.find_file ('test', 'lua')) } \font\song=AdobeSongStd-Light at 10pt \song 测 \bye
为了将对 CID 字体中的所有字符都写入 fontdata.characters 表中,我们的 test.lua 将会变的很复杂。因为我们需要解析 .cidmap 文件(例如对于中文 CID 字体而言,可以使用 Adobe-GB1-4.cidmap 文件)构建类似 TrueType 字体的 metrics.map.map 那样的表。但是,我们现在没必要这么做。因为……
Hans 的字体脚本很强大
通过上面的三个示例,大致可了解一些 luatex 的字体加载方式,真正的让 luatex 支持各种字体的加载,我们最好是将 Hans Hagen 为 luatex/Plain TeX 用户所写的一些脚本拿过来用。
首先,可从 http://minimals.contextgarden.net/current/context/beta/tex/generic/context/ 下 载 luatex-basics.tex、luatex-fonts.tex、luatex-fonts.lua、luatex-fonts- merged.lua 四个文件,并将其存储于 luatex-plain/texmf/tex/generic/luatex 目录。
然后,修改 luatex-plain/texmf/tex/generic/luatex 目录已有的 luatex-plain.tex 文件,向其中添加所下载的 .tex 文件。修改后的 luatex-plain.tex 文件内容如下:
\input plain \directlua {tex.enableprimitives('', tex.extraprimitives())} \pdfoutput=1 \everyjob \expandafter {% \the\everyjob \input luatex-basics\relax \input luatex-fonts\relax } \edef\fmtversion{\fmtversion+luatex} \dump
从 http://download.openpkg.org/components/versioned/fontforge/fontforge/cidmap/ 下载 Adobe-GB1-5.cidmap 文件(如果系统中装有 fontforge 包,可从 /usr/share/fontforge 目录中得到这一文件),构建 luatex-plain/texmf/fonts/cid/fontforge 目录,将 Adobe-GB1-5.cidmap 文件存于其中,并修改 texmf.cnf 文件,向其中添加 FONTCIDMAPS 变量的定义。修改后的 texmf.cnf 文件内容如下:
TEXMFMAIN = $SELFAUTOPARENT/texmf TEXMFSYSVAR = $SELFAUTOPARENT/texmf-var TEXMF = {!!$TEXMFMAIN,!!$TEXMFSYSVAR} TEXMFDBS = $TEXMF TEXFORMATS = .;$TEXMF/web2c/luatex TEXINPUTS = .;$TEXMF/tex/{plain,generic}/{base,hyphen,luatex}// TEXFONTMAPS = .;$TEXMF/fonts/map/pdftex// ENCFONTS = .;$TEXMF/fonts/enc/dvips// TFMFONTS = .;$TEXMF/fonts/tfm// TTFONTS = .;$TEXMF/fonts/truetype//:$OSFONTDIR// OPENTYPEFONTS = .;$TEXMF/fonts/opentype//:$OSFONTDIR// FONTCIDMAPS = .;$TEXMF/fonts/cid//
继而,执行 `mktexlsr` 命令刷新 TeX 目录结构。
最后,使用 `luatex --ini --jobname=luatex --progname=luatex luatex-plain` 命令重新生成 luatex.fmt,并将其存放于 luatex-plain/texmf-var/web2c/luatex 目录。
使用下面的 test.tex 文件测试新的 luatex.fmt 是否可以工作:
\font\adobesong=AdobeSongStd-Light \font\simsong=simsun \adobesong 中文测试 \simsong 中文测试 \bye
假设 AdobeSongStd-Light.otf 与 simsun.ttc 分别存放于 /usr/share/fonts/adobe 目录与 /usr/share/fonts/winfonts 目录,那么编译 test.tex 文件之前需要按下面的方式设定 OSFONTDIR 变量:
$ export OSFONTDIR=/usr/share/fonts/{adobe,winfonts}
建立 Cache
之 所以说 Hans 的脚本强大,是因为他的脚本除了支持各种字体的载入,而且还提供了字体信息的 Cache。在加载字体时,如果是首次加载,那么便使用 fontloader.open 和 fontloader.to_table 函数打开字体文件并转化为初始的字体信息表,进而可得到 luatex 字体表,然后 Hans 提供的 cache 机制会将所得 luatex 字体表序列化写入 texmf.cnf 文件中 TEXMFCACHE 变量所定义的位置保存起来。这样,当再次载入相同字体时,便可直接从 cache 得到 luatex 字体表了。
默认情况下,Hans 的字体脚本会将 cache 目录设置在所编译的 .tex 文件所在的目录,建议将其设定在 luatex-plain/texmf-cache 目录,步骤如下:
- 建立 luatex-plain/texmf-cache 目录;
-
修改 texmf.cnf 文件,修改后的内容如下:
TEXMFMAIN = $SELFAUTOPARENT/texmf TEXMFSYSVAR = $SELFAUTOPARENT/texmf-var TEXMF = {!!$TEXMFMAIN,!!$TEXMFSYSVAR} TEXMFDBS = $TEXMF TEXFORMATS = .;$TEXMF/web2c/luatex TEXINPUTS = .;$TEXMF/tex/{plain,generic}/{base,hyphen,luatex}// TEXFONTMAPS = .;$TEXMF/fonts/map/pdftex// ENCFONTS = .;$TEXMF/fonts/enc/dvips// TFMFONTS = .;$TEXMF/fonts/tfm// TTFONTS = .;$TEXMF/fonts/truetype//:$OSFONTDIR// OPENTYPEFONTS = .;$TEXMF/fonts/opentype//:$OSFONTDIR// FONTCIDMAPS = .;$TEXMF/fonts/cid// TEXMFCACHE = $SELFAUTOPARENT/texmf-cache
- 执行 `mktexlsr` 命令,刷新 TeX 目录结构。
初步可用的 luatex plain 包
我将连续三篇文章中各种修改堆叠在一起,最终形成的 luatex plain 包可从 http://code.google.com/p/way2ctx/downloads/list 页面下载 luatex-plain-2010.09.26.tar.gz。
转载时,希望不要链接文中图片,另外请保留本文原始出处:http://garfileo.is-programmer.com