简介
这几天空闲时间搓了个 修改字体的工具 Morphio,利用字体特性,把特定单词渲染为其它单词。例如可以把 Microsoft 渲染为 Microslop {:301_998:}:
实际上不止单词,特殊符号或者长文本也可以,所以你甚至可以做一个 $\LaTeX$ 字体:
当然,中文字符也是支持的:
网页端可以直接导入/导出 TOML 格式的配方,允许你轻松分享修改流程。一个简单的配方:
[options]
word_match_start = true
word_match_end = true
skip_missing_glyphs = false
[[rules]]
from = "Microsoft"
to = "Microslop"
可以在 tests/recipes 目录下查看更多示例配方,目前包含:
原理
OpenType 与字形替换表 (GSUB))
OpenType 是现代字体的唯一通用标准。相较于老旧的 TrueType (TTF),它提供了连字、变体字、可变字体等排版特性。实际上,现在绝大部分 TTF 字体用的是 OpenType 标准。
许多字体都利用了 OpenType 的连字功能,例如英文中 fi 和 fl 的连字:
也有很多编程字体也提供了诸如 !=, == 这些特殊符号的连字。
那么,怎么实现连字呢?就是通过字体内的字形替换表 (GSUB, Glyph Substitution Table)。简单来说,它允许你将一系列字形 (glyph) 替换为另一系列字形。它主要提供了下面 6 类子表,每类都可以添加多条规则:
- Single substitution:单一字形替换 (1 -> 1)。把一个字形替换为另一个字形
- Multiple substitution:多重字形替换 (1 -> N)。把一个字形替换为多个字形
- Alternate substitution:把一个字形替换为一系列字形中的任意一个(允许用户选择)
- Ligature substitution:连字替换 (N -> 1)。把多个字形替换为一个字形
- Contextual substitution:根据上下文替换字形。指定一系列输入字形,并提供一系列 Sequence Lookup Record。每一条记录指定输入字形的序号以及修改方式(另一个 GSUB 子表的索引)
- Chained contexts substitution:根据上下文替换字形,在 Contextual substitution 的基础上允许回溯和前瞻(指定这些字形之前和之后的字形)
为了匹配整个单词,我们需要忽略字形前后有字母、数字和下划线的情况,所以我们采用 Chained contexts substitution。
N -> N
既然如此,若两个单词长度相等 (例如 Microsoft 和 Microslop),我们只需要:
- 插入一个 Single 类型的表,规则为:
o 替换为 l
f 替换为 o
t 替换为 p
- 插入一个 Chained contexts 类型的表,当匹配到
Microsoft 单词时按照下面的规则执行上面的 Single 替换表:
- 第 7 个字形执行规则 1
- 第 8 个字形执行规则 2
- 第 9 个字形执行规则 3
注意到如果字符相同就可以跳过,同时如果替换规则重复也可以重复利用。
N -> 1, 1 -> M
若输入单词 abc,输出单词 d,长度为 1:
- 插入 Ligature 类型的表,规则为将
abc 替换为 d
- 插入一个 Chained contexts 类型的表,当匹配到
abc 时执行上面的替换表
输入单词长度为 1 的情况类似,使用 Multiple substitution 实现字形替换即可。
N -> M
如果 N > M,则先使用 N -> N 替换前面的字形,再使用 N - M -> 1 替换。例如 abc -> de:
- 插入 Single 类型的表,规则为将
a 替换为 d
- 插入 Ligature 类型的表,规则为将
bc 替换为 e3. 插入一个 Chained contexts 类型的表,当匹配到 abc 时执行上面的替换表:
- 第 1 个字形执行 Single 表的替换
- 第 2-3 个字形执行 Ligature 表的替换
否则,使用 M -> M 替换前面的字形,再使用 1 -> M - N 替换。方法类似,此处不再赘述。
关键代码
fn append_word_substitution_lookups(
font: &FontRef<'_>,
gsub: &mut Gsub,
rules: &[ResolvedMorphRule],
word_match_start: bool,
word_match_end: bool,
) -> Result<Vec<u16>, MorphError> {
let word_glyph_ranges = if word_match_start || word_match_end {
word_glyph_ranges(font)?
} else {
Vec::new()
};
let mut lookup_indices = Vec::new();
for rule in rules {
let mut single_cache = SingleSubstitutionCache::default();
let mut sequence_records = Vec::new();
if rule.from_glyphs.len() == rule.to_glyphs.len() { // N -> N
sequence_records.extend(build_n_to_n_records(
gsub,
&rule.from_glyphs,
&rule.to_glyphs,
0,
&mut single_cache,
)?);
} else if rule.from_glyphs.len() == 1 { // 1 -> M
sequence_records.push(build_one_to_n_record(
gsub,
0,
rule.from_glyphs[0],
&rule.to_glyphs,
)?);
} else if rule.to_glyphs.len() == 1 { // N -> 1
sequence_records.push(build_n_to_one_record(
gsub,
0,
&rule.from_glyphs,
rule.to_glyphs[0],
)?);
} else if rule.from_glyphs.len() < rule.to_glyphs.len() { // N < M
let prefix_len = rule.from_glyphs.len() - 1;
sequence_records.extend(build_n_to_n_records(
gsub,
&rule.from_glyphs[..prefix_len],
&rule.to_glyphs[..prefix_len],
0,
&mut single_cache,
)?);
sequence_records.push(build_one_to_n_record(
gsub,
prefix_len,
rule.from_glyphs[prefix_len],
&rule.to_glyphs[prefix_len..],
)?);
} else { // N > M
let prefix_len = rule.to_glyphs.len() - 1;
sequence_records.extend(build_n_to_n_records(
gsub,
&rule.from_glyphs[..prefix_len],
&rule.to_glyphs[..prefix_len],
0,
&mut single_cache,
)?);
sequence_records.push(build_n_to_one_record(
gsub,
prefix_len,
&rule.from_glyphs[prefix_len..],
rule.to_glyphs[prefix_len],
)?);
}
if sequence_records.is_empty() {
continue;
}
let contextual_lookup = create_contextual_lookup(
&rule.from_glyphs,
word_glyph_ranges.clone(),
sequence_records,
word_match_start,
word_match_end,
);
lookup_indices.push(push_lookup(gsub, contextual_lookup)?);
}
Ok(lookup_indices)
}
链接