零、 我就不写成《LaTeX 笔记(一):NFSS 那点事儿》了,省得你们指望还有二…… 本文比较靠近 TeXnique 这个主题。想理解本文的内容,至少也得读一下 TeXbook 或者 TeX by topic 之类的。 一、 NFSS 框架是 LaTeX2e 的核心内容,占了 LaTeX2e 源码的 30% 左右的篇幅。如果你不理解它的重要性,还有一个数据:在完全安装的 TeX Live 发行版中,各类字体宏包编写的字体定义文件 (*.fd) 有 3000+。有了 NFSS,大家才能快乐地使用 \bfseries \itshape 等命令切换字体,随心所欲不逾矩了。 那么没有 NFSS 的情况下怎么切换字体呢?如果你像第 0 条里面说的,读过了两本书之一,那么你就知道是这么来的:

% 以 txfonts 宏包的字体 txr 为例
\font\txrm=txr at 12pt
\txrm Hello, \TeX\ and its fonts!
需要定义一个控制序列(命令)为字体,然后在切换字体的时候用一下。plain TeX 已经定义了一些,如 \bf \it \tt 之类。问题在于,\bf 是直立的粗体,\it 是普通字重的意大利斜体,如果你想要粗斜体,这个字体跟它俩一毛钱关系都没有,必须你自己重新定义个诸如 \bfit 之类的字体命令才行。就连不同字号的字体,都需要重新定义。 那么话说回来,其实 \bfseries \itshape 和 \mdseries \itshape 、\bfseries \upshape 等命令给出的字体也全都不一样啊,这怎么搞的?往下看。 <!-- 往下看之前建议仔细看看 TeX 发行版里两篇重要的文档 encguide 和 fntguide。--> <!-- 什么,你看完了?--> <!-- 我也不知道你真看完了还是假看完了,好吧,继续 --> 二、 上面让看的两篇文档,介绍了 NFSS 重要的属性——坐标。一个字体有 encoding / family / series / shape 和字号这五个坐标,比如默认字体,10pt 的 Computer Modern Roman (cmr10),在 NFSS 的坐标是 OT1/cmr/m/n/10。这里就不铺开讲了,需要你真的看完那两篇文档。 哦,你似乎在哪儿见到过这么一串写法?对了,在 Overfull \hbox 等信息里满都是这种写法。好了,是时候告诉你真相了:在调用这个字体前,实际上就有这么个定义:
\global\expandafter\font\csname OT1/cmr/m/n/10\endcsname=cmr10
也就是说,真实的字体命令,是由这些坐标拼起来构建的。不同的坐标会构造出来不同的字体,你每切换一次坐标,很可能就要生成一个新的字体命令。 三、 真的就是把字体坐标拼起来这么简单?当然不止,LaTeX 需要额外的信息找到上面代码里等号后面要赋值的字体名。把坐标与字体名称建立映射的东西,就是每个字体宏包必不可少的 .fd 文件。这个放到后面穿插着说。 四、 现在要提到 NFSS 里的一个比较核心的命令——\selectfont。在 \bfseries \itshape 或者 \small \large 等的定义里都有它。你光用 \fontseries{bx} 或者 \fontsize{10}{12} 改坐标还起不到改字体的作用,它只是把坐标信息存到了一些内部宏,如 \f@encoding \f@series 等。\selectfont 才是改变字体的实际操作。 那么 \selectfont 主要干了些啥呢?

\DeclareRobustCommand\selectfont
        {%
    \ifx\f@linespread\baselinestretch \else
      \set@fontsize\baselinestretch\f@size\f@baselineskip \fi
    \xdef\font@name{%
      \csname\curr@fontshape/\f@size\endcsname}%
    \pickup@font
    \font@name
    \size@update
    \enc@update
    }
定义里,前两行先不细看,它的作用是处理修改行距的两个命令 \linespread{***} 和 \renewcommand {\baselinestretch}{***} 可能导致的冲突; 第3-4行就是定义出来所谓的 \OT1/cmr/m/n/10 了,这里定义了一个全局的 \font@name,后面很多地方用得着,另外一个重要的内部宏是 \curr@fontshape,它储存着前四个坐标。 第6行则是使用这个 \OT1/cmr/m/n/10 改变字体了;第7行起到的作用是更新 \baselineskip 等一系列工作;第8行是改了 encoding 之后的一些处理。 重点看第5行:
\def\pickup@font{%
    \expandafter \ifx \font@name \relax
       \define@newfont
    \fi}
意思不难,如果 \OT1/cmr/m/n/10 已经定义过,拿来现成用就好,否则需要定义新字体了。接下来看:

\def\define@newfont{%
  \begingroup
    \let\typeout\@font@info
    \escapechar\m@ne
    \expandafter\expandafter\expandafter
       \split@name\expandafter\string\font@name\@nil
      \try@load@fontshape % try always
    \expandafter\ifx
       \csname\curr@fontshape\endcsname \relax
      \wrong@fontshape\else
      \extract@font\fi
  \endgroup}
group 之内,第一行影响到向终端输出的信息;第2-4行是对 \font@name 内容的一个拆包,把它里头的字体坐标存进 \f@encoding \f@seris 等。为什么不直接用那几个缓存的结果呢,不得而知,可能有考虑 \font@name 本身被动过手脚,跟缓存的结果不一致的情况。 后面的一个关键宏是 \try@load@fontshape:

\def\try@load@fontshape{%
   \expandafter
   \ifx\csname \f@encoding+\f@family\endcsname\relax
     \@font@info{Try loading font information for
                   \f@encoding+\f@family}%
    \global\expandafter\let
       \csname\f@encoding+\f@family\endcsname\@empty
     \nfss@catcodes
     \let\nfss@catcodes\relax
     \edef\reserved@a{%
       \lowercase{%
         \noexpand\InputIfFileExists{\f@encoding\f@family.fd}}}%
     \reserved@a\relax
          {\@input@{\f@encoding\f@family.fd}}%
   \fi}
我们可以看到,这个宏起到的作用就是载入 .fd 字体定义文件了。在此解释两个宏:
  • \csname \f@encoding+\f@family\endcsname,形如 \OT1+cmr。它的定义标志着 .fd 文件被载入。它是 .fd 文件里 \DeclareFontEncoding 命令定义出来的。
  • \csname \curr@fontshape\endcsname,形如 \OT1/cmr/m/n,它的定义标志着 .fd 文件被载入,并且前四个坐标被 .fd 文件里的 \DeclareFontFamily 命令定义过,这样就可以拿来把最后一块拼图,也就是字号信息拼上。
那么好了,往下走,如果 .fd 文件载入了,各条 \DeclareFontFamily 也过了一遍,仍然找不到对应的字体怎么办?这就表明,字体包里可能没有定义你要的坐标,这时就要进行回退(fallback),这个工作由 \wrong@fontshape 完成。每个 encoding 的定义文件如 t1enc.def 里,都会用 \DeclareFontSubstitution 命令给出用来回退的三个坐标(OT1 的是在 LaTeX2e 内核里给的)。 \wrong@fontshape 的代码就不继续贴了,举个例子,如果我弄了一个错误的坐标出来, \OT1/cmr/xxx/xxx,LaTeX 载入了 ot1cmr.fd 发现没有这个坐标,它就回退到 \OT1/cmr/xxx/n,还找不到,就回退到 \OT1/cmr/m/n。如果进行了回退,你会看到 "Font shape *** undefined, use *** instead" 的提示。 如果找到了前四个坐标的信息,或者回退到了合适的前四个坐标,接下来就要通过字号这个坐标去找字体(TFM)了。这个功能由 \extract@font 完成,它调用了 \get@external@font 去解析 \DeclareFontShape 命令里各种复杂的字号和 tfm 映射关系的定义式,后者内部用了 \try@size@range 完成实际的解析,用 \try@size@substitution 完成字号的 fallback。 五、 啰啰嗦嗦地写完了,不知道有多少人有足够的耐心看到这儿,以及为了看懂它去看更多的书。LaTeX 对字体的这套解决方案也许不是最优的,不过恐怕是无法替代的,不然那乌泱乌泱的字体宏包都得改。 嗯我不是说了有 30% 的代码是 NFSS 嘛?事实上今天讨论到的这部分连 5% 都占不到。其余的大头是被 encoding 和数学字体占了。encoding 的部分,要解决的是不同字体编码下的一些命令和重音的实现,比如 OT1 下的 \"o 是两个字符拼起来的,而 T1 则可以找到单个的字符。数学字体就不在这里说了,要说起来肯定要另开一帖。 \endinput 作者:Louis Stuart 链接:https://zhuanlan.zhihu.com/p/21339128

点赞(2)

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部