上一篇:时间戳
这次会用到 Lua,我保证。
页码
在我的浅薄的审美范畴里,card.pdf 的页码没有在页脚(footer)的留白(Margin)区域居中,甚为不美。然而,card-env.tex 里的
\setuppagenumbering[location={footer,inmargin}]
对此却无能为力。既然如此,还要它作甚,破而后立吧。先将上述代码修改为
\setuppagenumbering[location=] % 关闭页码
然后使用 \setupfootertexts
在页脚的留白区域安放页码:
\setupfootertexts[margin][][\hfill 1\hfill]
当然不可能所有页面的页码都为 1,所以应当使用 \pagenumber
获得每一页对应的页码:
\setupfootertexts[margin][][\hfill\pagenumber\hfill]
现在将以下两行代码添加到 card-env.tex 里:
\setuppagenumbering[location=]
\setupfootertexts[margin][][\hfill\pagenumber\hfill]
时间戳
我想让时间戳出现在版心(或正文区域)右侧的留白区域,\setuptexttexts
可成就此事:
\setuptexttexts[margin][foo][bar]
可在正文区域的左侧和右侧的留白区域居中放置 foo
和 bar
:
\setuptexttexts[margin][\hfill foo\hfill][\hfill bar\hfill]
去掉 foo
,将 bar
换成时间戳:
\setuptexttexts
[margin]
[][\hfill 2023 年 01 月 26 日 凌晨 4 时 44 分\hfill]
结果是时间戳文字大部分出界了。这在预料之中,留白区域太窄,时间戳太长。
使用 \rotate
可以根据指定角度逆时针旋转文本,
\setuptexttexts
[margin]
[][\hfill\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 时 44 分}\hfill]
在生成 PDF 文件的过程中,上述代码会导致 context
命令报错:
tex error > tex error on line 1 in file ./card.tex: Argument of \rotate has an extra }
context
命令(严肃地说是 TeX 引擎)无法理解我在 \setuptexttexts
里传入的信息是什么,反而认为我传入的是错误的信息。可以用 {
和 }
构造一个编组(Group),将 \rotate[...]{...}
语句囊括于其中,从而让 context
命令认为传入 \setuptexttexts
的是一段挺正常的文本:
\setuptexttexts
[margin]
[][\hfill{\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 时 44 分}}\hfill]
现在已将时间戳完整地显现于版心右侧的留白区域,但其中每个字是躺着的,需要设法让其中的汉字站立起来。
在进行下文之前, 需要给出上述最终排版结果对应的 card-env.tex 和 card.tex。
card.tex:
\environment card-env
\setuptexttexts
[margin]
[][\hfill{\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 时 44 分}}\hfill]
\showframe
\starttext
看版心右侧 $\rightarrow$
\stoptext
card-env.tex:
% 页面布局
\definepapersize[card][width=85.6mm,height=53.98mm]
\setuppapersize[card]
\setuplayout
[backspace=.1\paperwidth,
width=.8\paperwidth,
topspace=.015\paperheight,
height=.97\paperheight,
leftmargin=.666\backspace,
rightmargin=.666\cutspace,
headerdistance=.025\makeupheight,
footerdistance=.025\makeupheight,
textheight=.95\makeupheight]
% 字体
\definefontfamily[myfont][serif][sourcehanserifcn]
\setscript[hanzi]
\setupbodyfont[myfont,7pt]
% 页码
\setuppagenumbering[location=]
\setupfootertexts[margin][][\hfill\pagenumber\hfill]
% 标题
\setuphead[title][align=middle]
TeX 宏
接下来,焦点是 card.tex 文件中的
\setuptexttexts
[margin]
[]
[\hfill{\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 时 44 分}}\hfill]
可以定义一个宏,用于简化 \setuptexttexts
语句。例如
\def\timestamp{\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 时 44 分}}
\setuptexttexts
[margin]
[][\hfill\timestamp\hfill]
我试验过了,排版结果依然与上一节最后给出的排版结果相同。\timestamp
是一个宏。它在 \stemptexttexts
语句里出现的时候,可称为它被调用了。宏调用的结果便是它的定义。
最简单的宏定义,形式如下
\def\foo{...宏定义...}
宏又了定义之后,TeX 遇到 \foo
,就会用它的定义替换它,这个过程称为宏的展开。例如
\environment card-env
\starttext
\def\hello{汉字!} % 宏定义
\hello % 宏调用,\hello 会被 TeX 编译器替换为「汉字!」
\stoptext
宏可以接受参数,例如:
\def\timestamp#1{\rotate[rotation=270]{#1}}
其中 #1
表示 \timestamp
的第一个参数。带参数的宏,用法通常是
\宏{参数}
例如,
\timestamp{2023 年 01 月 26 日 凌晨 4 时 44 分}
展开结果为:
\rotate[rotation=270]{2023 年 01 月 26 日 凌晨 4 时 44 分}
仅需要知道这些知识,便可进入 Lua 的世界。
Hello,Lua!
我要用 Lua 语言对 \timestamp
所接受的参数里的每个汉字逆时针旋转 90 度角。对于 Lua 语言,完成此事的关键在于遍历一个字符串里的每个字符,稍微有些难度的是,这个字符串里含有汉字,这需要 Lua 支持文字的 UTF-8 编码。不需要解释太多,汉字虽然在计算机软件技术里也是疼痛了很久,但现在是 UTF-8 的时代。
假设有 Lua 字符串变量 x
x = "我喜欢汉字"
Lua 语言已经不需要我们再为它做什么额外的工作,它能够理解 UTF-8。可遍历 x
的每个字符的代码如下:
for _, c in utf8.codes(x) do
print(utf8.char(c))
end
变量 c
的值是 Unicode 码位(codepoint),需要使用 Lua 语言的 utf8 库提供的 utf8.char
函数将其转换为 UTF-8 编码,然后方能被 print
之类的函数视为字符串而输出至程序外部——终端或文本文件。 之所以从 UTF-8 编码的字符串里获得 Unicode 码位,再将 Unicode 码位转化为 UTF-8 编码,字符串里每个字符的编码长度并不固定,先将字符串转化为固定长度的 Unicode 码位序列,对字符串的解析会更方便。
倘若系统里并未安装 Lua 解释器,没有关系,因为 ConTeXt 的 TeX 解释器里内嵌了 Lua 解释器,因此可将上述 Lua 代码存入 .lua 文件,例如 foo.lua,然后在在终端执行以下命令:
$ context --noconsole foo.lua
可得到以下输出:
resolvers ... ... ...
我
喜
欢
汉
字
system | total runtime: 0.489 seconds of 0.539 seconds
虽然 context
命令输出了很多它觉得有必要输出的信息,但是也输出了我想看到的信息。
要旋转字符串里的每个字符,只需对上述的字符串遍历代码略作修改,例如
for _, c in utf8.codes(x) do
print(string.format("\\rotate[rotation=90]{%s}", utf8.char(c)))
end
可在终端输出
\rotate[rotation=90]{我}
\rotate[rotation=90]{喜}
\rotate[rotation=90]{欢}
\rotate[rotation=90]{汉}
\rotate[rotation=90]{字}
string.format
是 Lua 的字符串格式化函数,在上述代码里,它可将 utf8.char(c)
生成的汉字信息作为字符串嵌入
"\\rotate[rotation=90]{%s}"
中的 %s
位置,并取代 %s
。这就是所谓的字符串格式化。不使用字符串格式化函数也能产生与上述代码等价的输出,只需使用字符串连接符号 ..
,例如:
print("\\rotate[rotation=90]{" .. utf8.char(c) .. "}"))
至于上述代码里,为何输出 \\rotate...
需要用两个反斜线符号 \
,因为在 Lua 语言里,\
符号用于对一些特殊符号进行转义,而 \
自身也是此类特殊符号。
至此,关键技术已然解决,但如何将上述的 Lua 代码嵌入 ConTeXt 源文件呢?可使用 \ctxlua
。
\ctxlua
\ctxlua
说,看我的!
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{%
for _, c in utf8.codes(x) do
context("\\rotate[rotation=90]{%s}", utf8.char(c))
end}}
将上一节给出的字符串遍历代码嵌入 \timestamp
的定义之后,变动的仅仅是将 Lua 函数 print
替换为函数 context
,因为后者可将信息输出到 PDF 文件里,而前者仅能将信息输出到终端。此外,string.format
也不需要了,因为 context
函数自身支持字符串格式化。
现在,对一下 card.tex 吧……
\environment card-env
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{%
x = "#1"
for _, c in utf8.codes(x) do
context("\\rotate[rotation=90]{%s}", utf8.char(c))
end}}
\setuptexttexts
[margin]
[]
[\hfill\timestamp{我喜欢汉字}\hfill]
\starttext
向右看 $\rightarrow$
\stoptext
ConTeXt 的 TeX 编译器(LuaTeX)在处理 card.tex 时,会报错:
tex error > tex error on line 13 in file ./card.tex: The file ended when scanning a definition.
这个错误让我一整天徘徊不前,且百思不得其解。直到我去 ConTeXt 的 Wiki 上查阅了 \ctxlua
的文档:
https://wiki.contextgarden.ne...
文档里说:
Use this command to quickly execute some Lua code. TeX expands the argument before Lua receives it. Advantage: you can pass the contents of macro parameters like #1 to Lua. Disadvantage: everything after a percent sign is ignored, and once the comments are processed out the linebreaks are stripped, too.
意思时,字符串格式化里的 %
被 TeX 编译器误以为是 TeX 源文件里的注释符 %
。此问题无解。避开方法是用字符串连接符 ..
代替字符串格式化:
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{%
x = "#1"
for _, c in utf8.codes(x) do
context("\\rotate[rotation=90]{" .. utf8.char(c) .. "}")
end}}}
经过上述修正,card.tex 可通过 TeX 编译器,顺利转化为 card.pdf。
站立起来的这几个汉字,其间距过于紧密,可通过 TeX 命令 \kern
构造指定宽度的空白空间予以调解:
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{%
x = "#1"
for _, c in utf8.codes(x) do
context("\\rotate[rotation=90]{" .. utf8.char(c) .. "}\\kern.25em")
end}}}
\ctxlua
虽然能解决问题,但是让 \timestamp
宏的定义甚为丑陋。倘若在 \startluacode
... \stopluacode
里定义一个 rotate
函数,便可让 \timestamp
的定义大幅简化。
试试看,
\startluacode
my = {}
function my.rotate(x, a)
for _, c in utf8.codes(x) do
context("\\rotate[rotation=%d]{%s}\\kern.25em",
a,
utf8.char(c))
end
end
\stopluacode
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{my.rotate("#1", 90)}}}
我试过了,没问题,依然能生成上一节最后的排版结果。顺便解释一下,my
是我为 rotate
的命名空间。使用命名空间的好处是,可避免函数同名而造成一些误会。
竖排的时间戳
好了,现在我们可以再对一下 card.tex 了。
\environment card-env
\startluacode
my = {}
function my.rotate(x, a)
for _, c in utf8.codes(x) do
context("\\rotate[rotation=%d]{%s}\\kern.25em", a, utf8.char(c))
end
end
\stopluacode
\def\timestamp#1{\rotate[rotation=270]{\ctxlua{my.rotate("#1", 90)}}}
\setuptexttexts
[margin]
[]
[\hfill{\timestamp{2023 年 01 月 26 日 凌晨 04 时 44 分}}\hfill]
\starttext
向右看 $\rightarrow$
\stoptext
结果很丑:
如果仅仅让汉字竖立,其它符号保持躺平,结果会美观一些。要实现该想法,需要对 \timestamp
的参数里的汉字进行识别。下面定义一个函数,使之能够基于汉字的 Unicode 码区识别汉字:
function my.is_cjk_char(c)
if c >= 0x3400 and c <= 0x4db5
or c >= 0x4e00 and c <= 0x9fa5
or c >= 0x9fa6 and c <= 0x9fbb
or c >= 0xf900 and c <= 0xfa2d
or c >= 0xfa30 and c <= 0xfa6a
or c >= 0xfa70 and c <= 0xfad9
or c >= 0x20000 and c <= 0x2a6d6
or c >= 0x2f800 and c <= 0x2fa1d
or c >= 0xff00 and c <= 0xffef
or c >= 0x2e80 and c <= 0x2eff
or c >= 0x3000 and c <= 0x303f
or c >= 0x31c0 and c <= 0x31ef then
return true;
else
return false;
end
end
修改 my.rotate
函数:
function my.rotate(x, a)
for _, c in utf8.codes(x) do
if my.is_cjk_char(c) then
context("{\\rotate[rotation=%d]{%s}}\\kern.25em", a, utf8.char(c))
else
context(utf8.char(c))
end
end
end
结果如下:
该结果依然不尽人意,数字和汉字没有竖直居中对齐。若要解决这个问题,需要整些暴力手段(希望以后能找到更为简单的方法):
function my.rotate(x, a)
for _, c in utf8.codes(x) do
if my.is_cjk_char(c) then
context("{\\rotate[rotation=%d]{\\kern-.5\\maxdepth %s}}\\kern.25em",
a, utf8.char(c))
else
context(utf8.char(c))
end
end
end
\kery-.5\maxdepth
可将竖排的汉字向左偏置 0.5 倍的 \maxdepth
。
\maxdepth
是 ConTeXt 排版时为一行文字定义的最大深度值,其含义可参考: