在字体层面处理标点挤压
我之前有提到这个博客的标点挤压是用Pandoc filter + CSS实现的。那种方法表面看起来不错,但会在HTML上留下很多<span>——就像掀起一张美丽的地毯,发现下面全是小蜘蛛。呕,太可怕了。那有没有不包含小蜘蛛的方法?有:直接写OpenType特性。
做法
写OpenType特性最方便的途径是编写OpenType Feature File——它的语法没有名字,但一般人管它叫fea——然后用fontTools把特性编译进字体文件里:
vim features.fea # 编辑你想要的特性
fonttools feaLib -o "$OUTPUT_FONT" features.fea "$INPUT_FONT"
在fea中可以用字的名字指代字,而不同字体的同一个字不一定有同一个名字。 所以我有必要声明下我用的字体是思源宋体 CN VF 2.001版。如果你不知道你想改的字体里每个字都叫什么,可以用fontTools把字体的Character to Glyph Index Mapping Table(cmap)转成人类可读的XML:
ttx -t cmap -o cmap.xml "$INPUT_FONT"
cat cmap.xml
# ……
# <map code="0x300a" name="uni300A"/><!-- LEFT DOUBLE ANGLE BRACKET -->
# ……
知晓名字后就可以写fea了!
连用标点
回顾一下我在之前提到的挤压算法:
- 如果当前字符是“《(之一:
且该字符是字符串里的第一个字符——挤压该字符;- 且该字符前面是除”》)以外的标点——挤压该字符;
- 如果当前字符是任意标点,且后面跟着”》)之一——挤压该字符;
- 如果当前字符是”》)之一,且后面跟着任意标点——挤压该字符。
在fea中,出现a字符紧接着b字符时,挤压a的命令是pos a b <valuerecord>;
;挤压b的命令是pos a b' <valuerecord>;
(注意撇号)。所以除去被划掉的1.1,其他三条可以如下表达:
@open = [
quoteleft
quotedblleft
uni300A #《
uniFF08 #(
uni300C #「
];
@close = [
quoteright
quotedblright
uni300B # 》
uniFF09 # )
uni300D # 」
];
@full = [
uni2E3A # two em dash
ellipsis
];
@other = [
uniFF0C # ,
uni3002 # 。
uni3001 # 、
uniFF1A # :
uniFF1B # ;
uniFF1F # ?
uniFF01 # !
];
valueRecordDef < 0 0 -500 0 > HALF;
valueRecordDef < -500 0 -500 0 > OPEN_HALF;
feature chws {
lookup chws_normal {
# 2 Current char is any punctuation followed by a closing bracket
pos @other @close < HALF >;
# 3 Current char is a closing bracket followed by any punctuation
pos @close @open < HALF >;
pos @close @close < HALF >;
pos @close @other < HALF >;
pos @close @full < HALF >;
} chws_normal;
lookup chws_backtrack {
# 1.2 Current char is following any punctuation except a closing bracket
pos @open @open' < OPEN_HALF >;
pos @full @open' < OPEN_HALF >;
pos @other @open' < OPEN_HALF >;
} chws_backtrack;
} chws;
问:
为什么要把这个特性命名为chws?
答:
因为Contextual Half-width Spacing(chws)是一个已经被注册专门用于汉字标点挤压的特性。其实这里介绍的方法就来自注册该特性的小林剑博士写的《Contextual Spacing GPOS Features—Redux》(文中的链接坏掉了,应该是这个)。不过貌似没什么字体支持这个特性,而且火狐甚至不会默认开启它(尽管按标准来看它应该开启)。所以其实叫个别的名字也无所谓。
问:
为什么要划掉1.1?
答:
因为我不清楚如何在fea中表达句首这一概念。OpenType有个Initial Forms(init)特性会作用于词首的第一个字母。但貌似shaper认为中文每个字都是一个词,所以init特性的效果会在每一个字上体现。
也可以调转思路,把@open的默认宽度改成半宽。然后用程序提取字体里的所有字写进@all。最后在@open前面有@all出现时,补上@open的空白。我之所以放弃是因为这样做要列举每个字,有些恶心。
问:
还有什么局限么?
答:
lookup会在<sup>前停下,所以没法对注释标号前的句号进行挤压。
撇号
除去连续出现的括号引号书名号,还有一个标点需要挤压:撇号。撇号、英文单反引号还有中文单反引号在UTF-8中是同一个字,所以用默认字体是中文的浏览器浏览英文网页时经常能看到撇号后面留了巨大空白。我之前讨论过解决方法,但不够本质。既然可以用fea解决中文标点的挤压,也能用它根据撇号后面的字符是否是英文来判定消不消去中文单反引号的空白:
feature kern {
@apostrophe_suffix = [space a-z A-Z];
lookup apostrophe {
pos [quoteright] @apostrophe_suffix -700;
} apostrophe;
} kern;
其中-700就是< 0 0 -700 0 >的简写。Kerning(kern)特性是专门用来调整不同字母间间距的,也许把这条规则写在kern里并不好,但反正fontTools会替换原字体的所有特性,所以也没关系。
问:
什么?不是添加特性,而是替换特性?
答:
没错,把自己写的特性编译进思源宋体之后,连破折号都会断开——因为hwid特性没了。其实用sub命令把两个em dash合并成一个two-em dash,再写个pos命令调整two-em dash的位置后充当破折号效果也不错。
问:
说了半天,你怎么不在自己的博客里用?
答:
其实我短暂用了一天,但是对OpenType有所了解后我不满足于此了。我想干一件更彻底的事,而且我不喜欢展示半成品,所以现在退回以前的方法了。