关于tex:LuainConTeXt12zhfonts-模块备忘录

3次阅读

共计 8325 个字符,预计需要花费 21 分钟才能阅读完成。

上一篇:源码彩化

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 结点了。

正文完
 0