上一篇:源码彩化
zhfonts 模块实现了 ConTeXt (>= MkIV) 对汉字字体的加载、简体汉字标点符号(全角)间距的压缩以及边界对齐。该模块成型于 2011 年,2023 年初对代码进行了一番梳理,心愿它能工作到 2033 年……装置和应用办法可参考 https://github.com/liyanrui/zhfonts/blob/master/README.md,本文仅对其一些技术细节予以阐明,一则备忘,二则或者能帮忙一些同好对该模块予以改良。
默认字体
zhfonts 默认应用 simsun.ttc(宋体),simhei.ttf(黑体)和 simkai.ttf(楷体)三种汉字字体:
- simsun.ttc 的子字体 nsimsun 作为衬线字族(Serif,对应 ConTeXt 字体切换命令
\tf
)的正体(Regular,对应\rm
)和斜体(Italic,对应\it
); - simhei.ttf 作为无衬线字族(Sans,对应
\ss
)的所有字体; - simkai.ttf 作为等宽字族(MonoSpace,对应
\tt
)的正体和斜体; - simhei.ttf 作为衬线,无衬线以及等宽字族的粗体和粗斜体,对应这三种字族的
\bf
和\bi
命令。
具体设定可参考 t-zhfonts.lua 的设定:
f.chinese = {serif = {regular = {name = "nsimsun", rscale = "1.0"},
bold = {name = "simhei", rscale = "1.0"},
italic = {name = "nsimsun", rscale = "1.0"},
bolditalic = {name = "simhei", rscale = "1.0"}},
sans = {regular = {name = "simhei", rscale = "1.0"},
bold = {name = "simhei", rscale = "1.0"},
italic = {name = "simhei", rscale = "1.0"},
bolditalic = {name = "simhei", rscale = "1.0"}},
mono = {regular = {name = "kaiti", rscale = "1.0"},
bold = {name = "simhei", rscale = "1.0"},
italic = {name = "kaiti", rscale = "1.0"},
bolditalic = {name = "simhei", rscale = "1.0"}}
}
至于拉丁字体,zhfonts 默认应用 ConTeXt 自带的 LatinModern 字族,可参考 t-zhfonts.lua 的设定:
f.latin = {
serif = {regular = "lmroman10regular", bold = "lmroman10bold",
italic = "lmroman10italic", bolditalic = "lmroman10bolditalic"},
sans = {regular = "lmsans10regular", bold = "lmsans10bold",
italic = "lmsans10oblique", bolditalic = "lmsans10boldoblique"},
mono = {regular = "lmmono10regular", bold = "lmmonolt10bold",
italic = "lmmonolt10oblique", bolditalic = "lmmonolt10boldoblique"}
}
边界标点对齐
在 ConTeXt 的段落断行后果中,若标点符号落在一行文字的结尾或开端,对于文字横排,令其向两侧侧伸出,使其近似落在版心右侧边线上,可使排版后果更为粗劣。例如
\usemodule[zhfonts]
\setuppapersize[A6][A6]
\showframe
\starttext
\dorecurse{10}{“左引号应该与左边界对齐,右引号应该与右边界对齐”}
\stoptext
若勾销标点边界对齐,则排版后果为
ConTeXt 仅对落在版心右侧边线的东方文字的标点提供了伸出反对,详见 https://wiki.contextgarden.net/Protrusion,对汉字全角标点未提供反对,然而在 font-imp-quality.lua 脚本中给出了用户本人管制边界标点伸出的办法,zhfonts 便是利用了该办法实现了汉字简体全角标点的边界伸出反对。
首先,定义标点在左右边界伸出的字宽倍数(\quad
宽度的倍数 ):
fonts.protrusions.vectors["myvector"] = {[0xFF0c] = {0, 0.60}, --,[0x3002] = {0, 0.60}, --。[0x2018] = {0.60, 0}, --‘[0x2019] = {0, 0.60}, --’[0x201C] = {0.50, 0}, --“[0x201D] = {0, 0.50}, --”[0xFF1F] = {0, 0.60}, --?[0x300A] = {0.60, 0}, --《[0x300B] = {0, 0.60}, --》[0xFF08] = {0.50, 0}, --([0xFF09] = {0, 0.50}, --)[0x3001] = {0, 0.50}, --、[0xFF0E] = {0, 0.50}, -- .
}
而后,让 fonts.protrusions.classes["个性名称"].vector
指向上表:
fonts.protrusions.classes["zhspuncs"] = {
vector = "myvector",
factor = 1
}
zhspuncs
即个性名称,可在定义字体个性时应用。例如
\definefontfeature[hanzi][default][protrusion=zhspuncs]
在定义字体时,但凡应用该个性的汉字字体,皆具备标点边界伸出能力,例如
\startluacode
fonts.protrusions.vectors["myvector"] = {
... ... ...
[0x201C] = {0.50, 0}, --“[0x201D] = {0, 0.50}, --”... ... ...
}
fonts.protrusions.classes["zhspuncs"] = {
vector = "myvector",
factor = 1
}
\stopluacode
\definefontfeature[hanzi][default][mode=node,protrusion=zhspuncs]
\definefont[myfont][name:nsimsun*hanzi at 12pt]
\setupalign[hz,hanging] % 使 protrusion 失效
\setscript[hanzi] % 加载 scrp-cjk.lua 提供的中文断行规定和标点禁则
\starttext
\myfont
\dorecurse{100}{“我能吞下玻璃而不伤身材”}
\stoptext
须要留神的是,上述对边界标点伸出比例的设定未必适宜所有汉字字体,利用时可依据审美需要,予以调整。
标点间距压缩
汉字的两个全角标点相邻时,通常须要对其间距予以压缩。例如,
老子说:「道可道也,非恒道也。」
若未压缩标点间距,ConTeXt 的排版后果为
若压缩标点间距,则后果为
标点间距压缩后的后果是否更好看,属于集体偏好,然而显然压缩后,在满足排版需要的前提下,更能节俭排版空间,若用于打印,能够少砍许多树……这是我能为排版惟一找回的意义。
在标点符号之间插入负值的 \kern
便可实现标点间距压缩,例如,
老子说:\kern -1em「道可道也,非恒道也。\kern -.5em」
能够对 ConTeXt 源文件进行解决,在相邻的汉字标点间插入 \kern
命令实现间距压缩,但此举不适宜抄录环境,例如
\starttyping
老子说:\kern -1em「道可道也,非恒道也。\kern -.5em」\stoptyping
\kern
指令非但不起作用,反而扰乱了排版内容。最适宜解决标点间距压缩的层面是在 ConTeXt 所用的 TeX 引擎(MkIV 版本用的是 LuaTeX,LMTX 版本用的是 LuaMetaTeX)对 ConTeXt 源文件解决后生成的结点列表。
结点列表
LuaTeX 在将 TeX 源文件解决为 TeX 记号(TeX Token)后,对 TeX 宏予以开展至 TeX 原语层面,待执行完所有原语后,在将排版后果输入至后端(dvi,ps 或 pdf)之前,所有的排版内容以结点列表的模式存储。用户可通过 Lua 程序拜访并操作结点列表。LuaMetaTeX 是 LuaTeX 的后继者,它对 LuaTeX 进行了清理,目标是与 ConTeXt 零碎获得紧密联系,亦即 LuaMetaTeX 实质上是一个不能独立运行的 TeX 引擎,目前仅能在 ConTeXt 环境里应用。LuaMetaTeX 同样反对用户通过 Lua 程序拜访并操作结点列表。
因为 TeX 所解决的排版内容非常复杂,因而结点的类型繁多。例如,\kern
命令对应 kern 结点类型,\hbox
命令对应 hlist 结点类型,字符对应 glyph 结点类型。以下示例有助于直观感触 LuaTeX 的结点类型。
\starttext
\setbox0\hbox{有生于无。}
\startluacode
local box = tex.box[0]
local head = box.list
local types = node.type(box.id) .. ":\n"
for x, id in nodes.traverse(head) do
types = types .. "\t" .. node.type(id) .. "\n"
end
context.tobuffer("foo", types)
\stopluacode
\typebuffer[foo]
\stoptext
尽管在上述源文件里并未为汉字设定字体,然而在结点列表层面,此时尚未将内容输入至后端,因而 LuaTeX 无需关怀字体的问题。
假使在在上述代码之前增加
\setscript[hanzi]
以加载 ConTeXt 提供的 scrp-cjk.lua 脚本实现的汉字断行性能,后果则是另一番状况:
这是因为 scrp-cjk.lua 脚本在相邻汉字之间插入了粘连(glue)结点(对应 \hskip
之类),并在标点前插入了罚点(对应 \penalty
)以禁止 LuaTeX 在某些标点之前断行。若非如此,汉字无奈断行,因为在 TeX 引擎看来,汉字形成的段落等同于一个西文单词。
注:应用「`mtxrun –script base –find scrp-cjk.lua」命令可取得 scrp-cjk.lua 门路。
ConTeXt 的工作回调机制
既然 scrp-cjk.lua 可能在相邻汉字对应的 glyph 结点之间插入粘连结点,借鉴它的工作形式,实现汉字标点间距压缩,不失为上策,可怜的是,像 scrp-cjk.lua 这样的脚本该如何编写,不足文档阐明。间接批改 scrp-cjk.lua 或仿写,尽管也能解决问题,然而不能确定这些未成文的机制在未来是否会产生变动。zhfonts 模块的做法是应用 ConTeXt 提供的工作回调机制:
nodes.tasks.appendaction("processors","after", ...)
将实现标点间距压缩性能的函数作为回调函数传给 nodes.tasks.appendaction
。ConTeXt 的工作回调机制有相干的文档阐明,见 hybrid.pdf 的「Callbacks」章。
注:应用「
mtxrun --script base --find hybrid.pdf
」可取得 hybrid.pdf 门路。
以下示例展现了如何向 ConTeXt 的 processors
工作列表的 after
阶段增加回调函数:
\startluacode
my = my or {}
function my.foo(head)
print(">>>> my.foo:")
for x, id in nodes.traverse(head) do
print(node.type(id))
end
return head, true
end
nodes.tasks.appendaction("processors", "after", "my.foo")
\stopluacode
\starttext
\setbox0\hbox{有生于无。}
\stoptext
processors
工作列表里的所有工作都产生在断行之前,此时但凡参加排版的字符皆已与字体有了关联,然而上述示例定义的 \box0
中的内容并未参加排版,因而即便未定义汉字字体,也不会障碍 my.foo
函数被 ConTeXt 调用执行,只是输入内容是间接输入到终端窗口了,不能再像上一个示例那样写入 ConTeXt 的缓冲区并通过 \getbuffer
获取了。ConTeXt 缓冲区仅在 processors
工作列表之前的机会无效,否则写入缓冲区的内容会再次触发 my.foo
的调用,会造成死循环。
字形结点
glyph 结点将字符与字形关联了起来。上面的示例可能输入每个字符对应的字形的宽度、高度和深度信息:
\startluacode
my = my or {}
function my.foo(head)
print(">>>> my.foo:")
for x, id in nodes.traverse(head) do
if id == nodes.nodecodes.glyph then
print(x.width, x.height, x.depth)
end
end
return head, true
end
nodes.tasks.appendaction("processors", "after", "my.foo")
\stopluacode
\starttext
\setbox0\hbox{有生于无。}
\stoptext
然而 my.foo
函数的输入后果是
>>>> my.foo:
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
因为 ConTeXt 默认加载的西文字族是 Latin Modern 字族,其中任何一个字体都不蕴含汉字,导致结点 x
没有字形信息。
当初,批改上例的注释局部,
\starttext
% 加载 simsun.ttc 的子字体 nsimsun
\definefont[myfont][name:nsimsun]
\setbox0\hbox{\myfont 有生于无。}
\stoptext
my.foo
函数的输入后果变为
>>>> my.foo:
786432 645120 76800
786432 645120 36864
786432 617472 64512
786432 626688 76800
786432 165888 3072
这些数字都是尺寸值,单位是 sp——TeX 的根本尺寸单位,与单位 pt 的换算关系是 1 pt = 65536 sp。若将 my.foo
函数批改为
function my.foo(head)
local pt = tex.sp("1pt")
print(">>>> my.foo:")
for x, id in nodes.traverse(head) do
if id == nodes.nodecodes.glyph then
local sizes = string.format("%.1f\t%.3f\t%.3f",
x.width /pt,
x.height/pt,
x.depth/pt)
print(sizes)
end
end
return head, true
end
则输入后果变为
>>>> my.foo:
12.0 9.844 1.172
12.0 9.844 0.562
12.0 9.422 0.984
12.0 9.562 1.172
12.0 2.531 0.047
tex.sp
函数可将文本模式的尺寸转换为单位为 sp 的尺寸。my.foo
输入的信息说,在上例中,\box0
中的 5 个汉字,它们的字形宽度皆为 12.0 pt,汉字字体根本每个字形都是等宽的,但高度和深度不等。在定义字体时,若设定字体尺寸,例如
\definefont[myfont][name:nsimsun at 11pt]
则 my.fooo
输入信息会发生变化。
字形边界盒
glyph 结点 x
将字符 x.char
和字体 x.font
关联了起来。通过 fonts.hashes.identifiers[x.font]
便可拜访 x.char
对应的字形信息。
给 ConTeXt 装置新字体时,须要将字体文件复制到 TeX 目录构造的适当地位,而后执行
$ mtxrun --script fonts --reload --force
该命令可从字体文件获取字形信息并将其以 Lua 表构造存储在 ConTeXt 的 cache 目录。这些 Lua 表可通过 fonts.hashes.identifiers[字体 id]
拜访,例如
\startluacode
my = my or {}
local tfmdata = fonts.hashes.identifiers
function my.foo(head)
print(">>>> my.foo:")
for x, id in nodes.traverse(head) do
if id == nodes.nodecodes.glyph then
-- x.font 是字体 id, x.char 是字符的 Unicode 编码
local desc = tfmdata[x.font].descriptions[x.char]
if desc then
local bbox = desc.boundingbox
print(bbox[1], bbox[2], bbox[3], bbox[4])
end
end
end
return head, true
end
nodes.tasks.appendaction("processors", "after", "my.foo")
\stopluacode
\setuppapersize[A7,landscape][A7,landscape]
\definefont[myfont][name:simsun at 11pt]
\starttext
\myfont 有生于无。\stoptext
排版后果为
my.foo
的输入后果为
>>>> my.foo:
11 -25 243 210
12 -12 244 210
14 -21 241 201
12 -25 245 204
35 -1 91 54
>>>> my.foo:
89 0 419 666
之所以有两次输入,是因为 ConTeXt 传给 my.foo
的除了注释字符,还有页码。
my.foo
中的 bbox
字符对应字形的边界盒信息,它的前两个数值是边界盒左下角顶点的坐标,后两个数值则是边界盒右上角顶点的坐标。例如
35 -1 91 54
是汉字句号的边界盒坐标。
如果应用 fontforge 关上 simsun.ttc 的子字体 nsimsun。在菜单「View/Goto」关上的对话框里输出汉字句号的 Unicode 码 0x3002
,便可定位到汉字句号对应的字形,双击关上该字形的设计界面,而后通过菜单「View/Show/Side Bearing」显示该字形的边界尺寸,后果如下图所示:
显然 ConTeXt 是将字体基线的纵坐标作为纵轴的 0 点,所以汉字句号边界盒左下角顶点的纵坐标是 -1。另外,在 fontforge 中,每个字形的设计空间是 256 x 256(至多 simsun.ttc 如此),据此能够印证 ConTeXt 给出的字形边界盒信息是正确的。
在 ConTeXt 装置目录的 tex/texmf-cache/luatex 或 luametatex
门路中能够找到 ConTeXt 每次加载新字体时生成的字形信息文件,其扩大名为 .tma,例如 simsun.ttc 的子字体 nsumsun 对应的字形信息文件是 simsun-nsimsun.tma,其格局如下:
return {["cache_uuid"]="36b92b1d-4f4e-9e43-9147-2fe016be390c",
["cache_version"]=0x1.910624dd2f1aap+1,
["compacted"]=true,
["condensed"]=true,
["creator"]="context mkiv",
["descriptions"]={[32]={ -- 10 进制编码的 Unicode 码
["boundingbox"]=1, -- 无字形边界盒
["index"]=3,
["unicode"]=32,
["vheight"]=256,
["width"]=128,
},
[33]={["boundingbox"]={49, 1, 77, 180}, -- 字形边界盒坐标
["index"]=4,
["unicode"]=33,
["vheight"]=256,
["width"]=128,
},
... ... ...
}
能取得每个字形的边界盒信息,对单个汉字标点以及多个汉字标点的间距压缩便时,便可依据字形边界盒结构绝对尺寸的 kern 结点了。