5
12
2026
6

自定义系统默认中文字体

本文来自依云's Blog,转载请注明。

一开始使用Linux系统的时候,并没有多少自由开源的中文字体。那时候几乎所有人的选择都是文泉驿正黑。我就一直用啊用了好多年,直到后来截图时被网友说该换字体了,我才知道原来文泉驿项目已经停止很久了,网站上的新闻截止于2008年——都快20年啦。

文泉驿正黑的字形比较「旧时代」——以屏幕清晰度为优先,由于当时的屏幕普遍dpi低,笔画迁就像素风格,所以比较丑。另外也有些bug,比如「撨䑾詺㘃㞈㟯㫥」这几个字会有一片漆黑的区域。

文泉驿正黑看起来是这样的。注意截图中只有除标题外的中文部分用的是文泉驿正黑字体。

文泉驿正黑渲染的网页

2014年,Google联合Adobe发布了思源黑体和Noto Sans CJK字体——这两款字体的汉字部分是相同的,区别只在于思源黑体会根据文本的区域设置来自动选择字形(通常不管使用哪个语言的字族名来指定),而Noto Sans CJK字族名只有英文版本,并且不同的字族名后缀会选择不同的地区字形。

我在2024年终于决定切换到思源黑体试试。它长这样:

思源黑体渲染的网页

图中的日文部分也是使用的思源黑体,只不过是日文字形。标题则使用的是思源宋体。读者可以在图片上点「右键」然后新建标签页打开图像,然后来回切换着对比。

可以看出,思源黑体是比文泉驿正黑好看多啦。而且思源字体有粗体版本,文泉驿正黑是没有粗体的,只有合成出来的所谓「伪粗体」。

但是,你有没有发现有什么地方不对劲?注意看大标题下方那行字,「条目」和「阅读」下边的装饰线与该行下方的分隔线有一段距离,而「大陆简体」和「工具」两处字偏低。事实上,思源黑体的问题远不止这些。思源黑体文字上方空出来的空间比下方多不少,造成行高太高、终端里文字不居中等各种问题。

思源黑体的布局

(这个图片是使用命令pango-view --dpi=1024 --font=思源黑体 --annotate=glyph,layout,baselines -t A测试中文Test -o out.png生成的。)

因此我用了几天就换回文泉驿正黑了,但是把思源黑体作为部分网页字体在用(主要用于非简体中文内容,以及通过stylus指定的特定几个网站)。

最近,oldherl说更纱黑体修了行高的问题。于是我又试了几天这个基于思源黑体的字体。它的效果是这样的:

更纱黑体渲染的网页

行高的问题确实解决了!但是——又要「但是」了——它的字怎么矮胖矮胖的?是我没看习惯的原因吗?于是我使用了几天,最后并没有习惯,反而是和思源黑体一对照,发现确实是许多字都变矮了一点。

于是我只好又换回已经用习惯了的文泉驿正黑。

诶等等!既然更纱黑体能修行高,我为什么不行?因为我不会,可是,今非昔比了呀——Gemini,给我来个脚本!

于是就有了这么个脚本——当然这个脚本是我改过的最终版本了。

#!/usr/bin/python3

from io import BytesIO

from fontTools.ttLib import TTCollection, TTFont

def adjust_font(font):
    target_ascent = 1025
    target_descent = -265
    
    # 修改 hhea 表 (macOS/Pango 渲染常用)
    font['hhea'].ascent = target_ascent
    font['hhea'].descent = target_descent
    font['hhea'].lineGap = 92

    # 修改 OS/2 表 (Windows/Linux 合规性)
    font['OS/2'].sTypoAscender = target_ascent
    font['OS/2'].sTypoDescender = target_descent
    font['OS/2'].sTypoLineGap = 92
    # usWin 参数决定了红线(剪切区域),设为相同值可消除额外间距
    font['OS/2'].usWinAscent = target_ascent
    font['OS/2'].usWinDescent = abs(target_descent)

    # 2. 修改 Font Family 名称
    name_table = font['name']
    for record in name_table.names:
        name_str = record.toUnicode()
        new_record_str = name_str.replace('Source Han', 'Lily Han') \
                .replace('思源', '百合') \
                .replace('源ノ角ゴシック', '百合ノ角ゴシック') \
                .replace('본고딕', '백합고딕') \
                .replace('SourceHan', 'LilyHan')
        # 针对不同 ID 进行替换
        # ID 1: Family Name, ID 4: Full Name, ID 6: PostScript Name 等
        if name_str != new_record_str:
            name_table.setName(new_record_str, record.nameID, record.platformID, record.platEncID, record.langID)

    # Medium as Semibold
    if font['OS/2'].usWeightClass == 500:
        # save then read to copy the font without referencing existing data structures
        buf = BytesIO()
        font.save(buf)
        buf.seek(0)
        sb_font = TTFont(buf)

        sb_font['OS/2'].usWeightClass = 600

        for record in sb_font['name'].names:
            name_str = record.toUnicode()
            if "Medium" in name_str:
                new_name = name_str.replace("Medium", "Semibold")
                record.string = new_name.encode(record.getEncoding())

        cff = sb_font['CFF '].cff

        for i in range(len(cff.fontNames)):
            if "Medium" in cff.fontNames[i]:
                cff.fontNames[i] = cff.fontNames[i].replace("Medium", "Semibold")
        
        # 修改 TopDict 内部的名称
        for topDict in cff.topDictIndex:
            for attr in ['FullName', 'FamilyName', 'Weight']:
                if val := getattr(topDict, attr):
                    if isinstance(val, str) and "Medium" in val:
                        setattr(topDict, attr, val.replace("Medium", "Semibold"))

        return sb_font

def main(input_ttc, output_ttc):
    ttc = TTCollection(input_ttc)
    new_fonts = list(ttc.fonts)
    for font in ttc.fonts:
        newfont = adjust_font(font)
        if newfont:
            new_fonts.append(newfont)
    ttc.fonts = new_fonts
    ttc.save(output_ttc)
    print(f"成功保存至: {output_ttc}")

if __name__ == '__main__':
    import nicelogger
    nicelogger.enable_pretty_logging('DEBUG')
    input_ttc = "SourceHanSans.ttc"
    output_ttc = "LilyHanSans.otc"
    main(input_ttc, output_ttc)

脚本使用fonttools这个Python库,把ascent、descent和lineGap这三个参数改成和文泉驿正黑一样的了。之所以要照着文泉驿正黑来改,是因为我的终端最大化之后,使用13pt字号差不多刚好填满35行(只空出来两行像素)。而使用别的参数,我调整字号好久,都会空出来小半到大半甚至接近一行文字的高度。

另外,这个脚本运行起来非常耗资源:它保存的时候会重新计算每一个字形,持续占用一个CPU核心长达六分多钟,内存只分配不释放,最终用掉接近20GiB。倒是生成的文件差异不大,用rsync很快就能同步回来。

最终效果图:

百合黑体渲染的网页

可惜的是思源黑体只有粗体和Medium,不像更纱黑体那样有半粗(Semibold)版本。思源黑体的粗体挺粗的。

由于我在使用思源黑体时遇到的问题都是在UI部分,而这里又不需要用到宋体,所以我没有去改思源宋体,其它地区的字形也还是用的原本的思源系列。

如果有人想试试我生成的这个字体的话,在这里下载(112 MiB)。


2026年05月13日更新:更新了脚本和字体文件。现在把Medium复制一份然后改叫Semibold了,因此有了半粗字重,在用到的地方会更接近设计者的意图,也看着更舒服。

Category: 中文支持 | Tags: 字体 中文支持 | Read Count: 4548
cds 说:
May 12, 2026 01:55:39 PM

嫌粗体太粗的话,现在有vf变体了,程序可以指定想要的face。不过bug也不少,似乎之前的qt不听这个,默认100

Avatar_small
依云 说:
May 13, 2026 11:03:05 AM

我试过「思源黑体 VF」,火狐似乎不认为它是可变字体,用起来跟非VF的版本差不多的,除了笔画要稍微糊一点、宽度在所有精细都一样。

我现在把Medium当Semibold用了。

weijarz 说:
Jun 02, 2026 09:48:56 AM

确实以前试过Noto中文行高太高了,表现为连firefox之类的菜单条目都高了50%,非常不习惯。后来发现macos的苹方中文没这个行高问题,就一直用到现在。

vioulo 说:
Jun 06, 2026 11:12:20 AM

没想到文泉驿这么久了,我用 arch 一直用的这个,感觉英文、中文都很舒服。

石樱灯笼 说:
Jun 17, 2026 10:57:00 AM

因为从2019年开始用Linux作为操作系统,然后2021年正式成为主要操作系统,所以我一直用Noto Sans CJK SC,后来自己写了一个CSS专门对付中文字体问题。
一开始Noto Sans CJK就有跑偏的问题,看着比较不顺眼。因为我2008年的时候就折腾过Ubuntu所以也试了文泉驿系列,结果是更歪歪,去官网一看发现已经严重年久失修了,作者应该是弃坑了。

再后来Noto Sans CJK更新之后,跑偏的问题解决的差不多,也就没太当回事了,毕竟安卓手机上字体没法更新,跑偏都存在十多年了,就算跑偏也看习惯了。

再说本来微软雅黑更是错到离谱几十年了,也没人抱怨过,全民瞎。瞎就瞎吧,反正自己知道长啥样就行了。

Avatar_small
依云 说:
Jun 18, 2026 05:58:04 AM

我就时常抱怨微软雅黑参差不齐啊(


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter

Mastodon | Theme: Aeros 2.0 by TheBuckmaker.com