中文字库嵌入Web时的分割与裁剪

立泉

可能注意到这个博客站点的字体有点奇怪,并不是常见的无衬线黑体,而是一种接近宋体但又像手写楷书的字体。其实是一款兼顾排版和中文美感的霞鹜文楷开源字体,但要将它嵌入博客中,需要解决一些应用中文字库的典型问题。

分割

其中最大的问题是中文字库的庞大体积,英文字库无非是字母和符号,如JetBrains MonoWOFF2文件体积仅为92KB,可以非常方便的在Web中无感加载。但中文字库收录的汉字数以千计,而且笔画复杂,以8000字的霞鹜文楷GB为例,Regular字重的TTF文件为18.3MB,如果加上常用的Bold字重,体积就已经超过36MB,这么大的单体资源对网页加载是不可接受的。

所以中文字库必须分割,按常用字顺序以百KB为单位分割为多个WOFF2文件,再由页面按需加载。

/* CSS支持为字体文件指定其包含的字符编码范围 */
@font-face {
    font-family: "LXGW WenKai GB";
    src: url("https://hosturl.com/01.woff2") format("woff2");
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    /* 字符编码范围 */
    unicode-range: U+305e3, U+305f6, U+3067d ...;
}
@font-face {
    font-family: "LXGW WenKai GB";
    src: url("https://hosturl.com/02.woff2") format("woff2");
    font-style: normal;
    font-weight: 400;
    font-display: swap;
    /* 字符编码范围 */
    unicode-range: U+2ce29-2ce2a, U+2ce31, U+2ce7c ...;
}

其实用过Google Fonts就会知道它对中文字库也是这么处理的,所以现在问题就变成了如何分割字库。

网上冲浪发现中文网字计划cn-font-split项目很不错,一个简单命令就可以把TTF文件分割为数百个60KB的WOFF2文件并生成CSS代码,而且也支持指定要裁剪的字符编码范围,只生成包含所需字符的字库文件。

# 执行分割,指定输入TTF文件和输出目录
cn-font-split -i=/Users/apqx/Downloads/Input.ttf -o=/Users/apqx/Downloads/Output

配合浏览器缓存策略,每个页面加载的字体资源至多也不过几百KB,已属正常范围。这个问题解决后,选择字体就再不用为此纠结,放心使用喜欢的中文字库。

裁剪

我的博文中会经常嵌入代码块,之前一直使用等宽的JetBrains Mono字体,但应用霞鹜文楷后发现英文黑体和中文楷体混在一起总感觉怪怪的…所幸霞鹜文楷也提供等宽版本,但却是包含完整8000汉字的TTF字库,这些汉字与已有字库重叠,而我所需只是其中的英文和符号。

借助cn-font-split的裁剪功能,可以从TTF中提取出指定范围的字符,生成独立的WOFF2文件。

import {fontSplit} from "cn-font-split";

await fontSplit({
    // 要分割的字体文件
    FontPath: `/Users/apqx/Downloads/Input.ttf`,
    // 输出目录
    destFold: `/Users/apqx/Downloads/Output`,
    // 输出格式 woff2
    targetType: "woff2",
    // 分包大小 70KB
    chunkSize: 70 * 1024,
    // 关闭自动分包,只打包 subsets 中指定的字符
    autoChunk: false,
    // 指定要打包的字符
    subsets: [
        // 数字、字母、符号
        '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~!@#$%^&*()-_=+\\|{}[];:\'"<>,.?/ '
            .split("")
            .filter(Boolean)
            .map((i) => i.charCodeAt(0))
    ],
    testHTML: true,
    threads: {},
    previewImage: {},
});

同理我的博客中还存在一些使用手写字体的中文片段,没必要为它们引入整个字库,也是裁剪处理。

arrow_upward