简介:面向需要在Windows环境中补齐系统字体库的日常办公、平面排版、网页前端及软件兼容性测试人员,这份字体集合可用于解决字体缺失、换机后版式错乱、界面文字显示不一致等常见问题。压缩包共160个文件,包含159个TTF字体文件和1个XML配置文件,整体约106.14MB;TTF便于直接安装调用,XML可辅助查看或管理字体条目,方便按需整理与备份。内容覆盖宋体、黑体、楷体、仿宋、等线以及Malgun Gothic等多款中韩文字体,适合搭建较完整的本地字体环境,也便于设计稿复原、旧文档打开、多语言界面预览与跨设备字体比对。已有2608人学习下载,可作为常用Windows字体库留存,减少临时找字、反复替换与版本混乱带来的时间消耗,并为排版校对、打印预览和兼容性排查提供稳定基础。
1. 导出的 PDF 里中文全是方块:一套系统字体包能补哪些坑
上周把一份图表导成 PDF 发出去,对方回一句「中文全是方块」。本地跑得好好的,一上批处理服务就翻车,查到最后不是代码问题,是那台机器上压根没有中文字体,渲染器只能拿缺字形的方框顶上。这套 windows字体Fonts 资源,就是一台干净 Windows 机器上系统字体目录的完整拷贝:宋体、黑体、楷体、仿宋、等线这几套中文字族,加上一批西文与符号字体,格式以 TTF、TTC 为主,夹着少量 OTF 和更老的 FON。
它的价值不在字形好不好看,而在于把「目标环境里到底有哪些字形」变成一个可控变量。三类人最容易用上:要给报表、出图、PDF 渲染服务补字体的后端;要把文档转换、页面截图搬进容器的运维和测试;还有本地装了上百套字体、结果互相打架的设计和前端。下面按「认格式 → 装进系统 → 装进容器 → 排错 → 瘦身」的顺序拆,每一步都给出能直接抄的代码和参数。
2. 先分清 TTF / OTF / TTC / FON:字体包里的四类文件与装载差异
拿到一个字体目录,第一件事不是双击安装,而是搞清楚里面装的是什么。同一个「宋体」可能是一个单体 TTF,也可能是打包在一个 TTC 里的若干个字面,装载方式和注册表写法完全不同。搞混这一步,后面所有「装了但不生效」的怪现象都能从这找到解释。
2.1 四类文件在 Windows 上的装载差异
字体文件的扩展名只是外壳,真正决定行为的是内部结构和字面数量。把常见几类摆在一起对比,选型时心里就有数了:
| 扩展名 | 内部轮廓 | 单文件字面数 | 注册表登记方式 | 典型翻车点 |
|---|---|---|---|---|
| .ttf | TrueType 二次贝塞尔 | 1 | 一条 | 同名不同版本互相覆盖,字形悄悄回退 |
| .otf | CFF / PostScript 轮廓 | 1 | 一条 | 老式打印 RIP、部分嵌入式渲染器不认 CFF |
| .ttc | 多个 TrueType 字面打包 | 2~10 个以上 | 按字面分别登记 | 只登记一个,粗体、斜体在应用里不出现 |
| .fon | 位图点阵 | 1 | 一条 | 只在老式对话框、系统 UI 里生效,正文没用 |
| 可变 .ttf | 带 fvar / gvar 表 | 1 文件多实例 | 一条 | 应用只认默认实例,字重调不动 |
TTC 是最容易出问题的一类。系统自带的宋体常常是一个 TTC,里面同时装着常规、粗体等多个字面;如果安装脚本只按文件名登记了一条,很多老应用在枚举字族时只能看到一个,加粗时改用几何模拟,字形会变得又扁又糊。我的习惯是:面向服务端渲染的场景,优先用单体 TTF;只有在必须还原系统默认外观时,才整包丢 TTC 进去。
OTF 的 CFF 轮廓在屏幕上没问题,但一些老的 PDF 生成链路和嵌入式设备会把 CFF 当成未知轮廓,直接退化成方块或空字。做报表、出图的机器上,TTF 的兼容面明显更宽。
提示:字体文件的版权与授权范围各不相同,随项目或镜像分发字体文件之前,先确认许可条款允许这么用,别等上线了才发现要换。
2.2 文件名、族名、注册表值名:三个名字别搞混
新手最容易犯的错,是拿文件名当字体名去写代码。一个字体至少有四个名字:文件名(如某个 .ttc)、族名(应用里font-family写的那个)、全名/PostScript 名(PDF 嵌入和部分设计软件用)、本地化名(中文系统里显示的「宋体」)。
注册表里的值名又是另一套规则:通常是「显示名 + (TrueType)」后缀,值数据才是文件名。所以你会看到值名叫某某 (TrueType)、数据是xxxx.ttf这种组合。应用程序按族名查找,安装脚本按文件名复制,注册表按显示名登记,三个体系各说各话——这就是「明明装上了,程序里搜不到」的根源。
还有一层是替换与回退链。系统在HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontSubstitutes下维护了一张替换表,当请求的字体不存在时按表换一个顶上;...\FontLink\SystemLink则决定中文缺字时回退到哪套字体。服务端为了「不管请求什么字体都别出方块」,常见做法是给替换表补几条映射:
# 需管理员权限;把常见的「请求名」映射到本机确实存在的字族 $sub = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\FontSubstitutes' $map = @{ 'SimSun' = 'SimSun' '宋体' = 'SimSun' 'SimHei' = 'SimHei' 'Microsoft YaHei' = 'SimHei' # 目标机没有雅黑时兜底 } foreach ($k in $map.Keys) { New-ItemProperty -Path $sub -Name $k -Value $map[$k] -PropertyType String -Force | Out-Null }这段脚本的作用不是「装字体」,而是给找不到的字体名留一条后路,值统一写成本机存在的族名。改完要重启对应服务进程才读得到新表。注意别乱映射到符号字体或等宽字体上,排版会整体错位,比方块更难查。
2.3 用 PowerShell 做一次字体资产盘点
动手装之前,先摸清现状:机器上到底登记了多少字体、哪些是机器级、哪些是用户级、有没有同名残留。下面这段脚本把两个注册表分支拉平成一张表:
$keys = @( 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts', # 机器级 'HKCU:\Software\Microsoft\Windows NT\CurrentVersion\Fonts' # 当前用户级 ) $rows = foreach ($k in $keys) { if (-not (Test-Path $k)) { continue } $props = (Get-ItemProperty $k).PSObject.Properties foreach ($p in $props) { if ($p.Name -like 'PS*') { continue } # 跳过 PSPath 等元数据 [pscustomobject]@{ Scope = if ($k -like 'HKLM*') { 'Machine' } else { 'User' } RegName = $p.Name # 显示名 + (TrueType) Data = $p.Value # 纯文件名 或 绝对路径 } } } $rows | Sort-Object Scope, RegName | Format-Table -AutoSize "总计: {0} 条" -f $rows.Count判断规则很简单:Data是纯文件名,说明字体躺在系统字体目录里;Data是带盘符的绝对路径,基本都是用户级安装,指向当前用户的本地目录。后者在服务、计划任务、以其他账号运行的进程里统统看不见——这是后面避坑章节里最常出现的一条。
再看一层:注册表登记了,不等于当前进程认了。用 GDI 的已安装字体集合再列一次,两边对不上就说明缓存或权限出了问题:
Add-Type -AssemblyName System.Drawing (New-Object System.Drawing.Text.InstalledFontCollection).Families | Select-Object -ExpandProperty Name | Sort-Object -Unique把这份列表和注册表清单做一次差集,多出来的(注册了但进程不认)和少掉的(进程有但没登记)都是线索。做批量部署时,我一般把这两份输出都存成文本随包归档,出问题直接对比,比凭记忆猜快得多。
3. 装到系统还是只装给当前用户:两条路径与一份可复用的静默脚本
安装字体看着是「双击下一步」,实际背后有三件事同时发生:文件落到某个目录、注册表多一条记录、字体缓存被通知刷新。三件事缺一件,就会出现「文件在、程序里没有」的诡异状态。批量装机时手动双击更不现实,得用脚本一把梭。
3.1 双击安装背后的三件事与两种作用域
先把两种作用域的差别摆清楚,选错了后面全是坑:
| 对比项 | 机器级安装 | 当前用户级安装 |
|---|---|---|
| 文件落点 | 系统字体目录(需管理员) | 用户本地字体目录 |
| 注册表位置 | HKLM 分支 | HKCU 分支 |
| 服务/计划任务可见 | 可见 | 不可见 |
| 其他账号登录后可见 | 可见 | 不可见 |
| 卸载方式 | 设置里删除或脚本清理 | 同上,但只影响本账号 |
| 适用场景 | 出图服务、共享机、CI 镜像母版 | 个人临时试用、无管理员权限 |
关键差别在第 3 行。以服务身份运行的渲染进程,加载的是机器级配置;用户级装的字体对它完全透明。很多人本地调试一切正常、部署到服务上就变方块,问题就出在这——装的时候图省事点了「仅为我安装」。
3.2 一份可复用的静默安装脚本
下面这段是机器级批量安装,管理员 PowerShell 里跑,源目录换成你解压出来的字体目录即可:
# 机器级批量安装;必须以管理员身份运行 $src = 'D:\fonts\win' # 字体源目录 $dst = Join-Path $env:WINDIR 'Fonts' $reg = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' $exts = @('.ttf', '.otf', '.ttc') Get-ChildItem $src -Recurse -File | Where-Object { $exts -contains $_.Extension.ToLower() } | ForEach-Object { $target = Join-Path $dst $_.Name try { Copy-Item $_.FullName $target -Force -ErrorAction Stop } catch { # 系统字体目录偶尔会拒绝直接复制,用 Shell 动词兜底 $shell = New-Object -ComObject Shell.Application $folder = $shell.Namespace($dst) $folder.CopyHere($_.FullName, 0x14) # 0x14 = 不弹进度 + 全部回答“是” } $regName = '{0} (TrueType)' -f $_.BaseName New-ItemProperty -Path $reg -Name $regName -Value $_.Name ` -PropertyType String -Force | Out-Null }几个参数值得说明。-Force让同名文件直接覆盖,升级字体版本时不用先删;-ErrorAction Stop是为了让catch真的能接住复制失败,默认的非终止错误会把异常吞掉,脚本一路绿灯跑完,字体一个没进。0x14这个标志位是 Shell 复制动词的组合值,含义是静默复制且对冲突一路确认,绕开某些环境里直接写系统目录被拒的问题。
$regName用文件名当显示名,是「能跑起来」的最低要求,好处是简单、不会重名;代价是应用里的字体列表会出现一堆拼音名。正式环境我一般改成读取字体表里的族名,脚本会多十几行,但清单干净得多。
注意:TTC 文件按这条脚本只会登记一条记录。如果这个 TTC 里含粗体、斜体等多个字面,且下游应用依赖独立字面,需要额外把每个字面的族名都登记一遍,值都指向同一个文件名。
3.3 让已经在跑的进程立刻认到新字体
文件复制完、注册表写完,新开的程序能看到字体了,但已经在跑的服务进程往往还抱着旧的字体缓存不放。这时不用重启整台机器,调一次 GDI 的加载接口再广播一条系统消息就够了:
Add-Type @" using System; using System.Runtime.InteropServices; public class FontApi { [DllImport("gdi32.dll", CharSet = CharSet.Unicode)] public static extern int AddFontResourceW(string lpFileName); [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern IntPtr SendMessageTimeout( IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam, uint fuFlags, uint uTimeout, out IntPtr lpdwResult); } "@ $fontFile = Join-Path $env:WINDIR 'Fonts\your-font.ttf' [void][FontApi]::AddFontResourceW($fontFile) # 返回值 >0 表示加载成功 # HWND_BROADCAST=0xFFFF, WM_FONTCHANGE=0x001D, SMTO_ABORTIFHUNG=0x0002 $r = [IntPtr]::Zero [void][FontApi]::SendMessageTimeout( [IntPtr]0xFFFF, 0x001D, [IntPtr]::Zero, [IntPtr]::Zero, 0x0002, 1000, [ref]$r)AddFontResourceW的返回值是成功加载的字面数,返回 0 基本就是路径不对或文件损坏。SendMessageTimeout的超时给 1000 毫秒足够,用超时版本而不是同步版本,是为了防止某个卡死的窗口把整个安装卡住——这条是踩过坑之后加的。
3.4 卸载与回滚:先备份那条注册表分支
字体装多了会打架,尤其是同名不同版本的宋体、黑体。清理顺序必须是:删注册表值 → 删文件 → 重建缓存,顺序颠倒会出现「文件删了但列表还在」的幽灵条目。
# 回滚:删除指定字体(示例按显示名前缀匹配) $reg = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts' Get-ItemProperty $reg | ForEach-Object { $_.PSObject.Properties | Where-Object { $_.Name -like 'YourFont*' } | ForEach-Object { Remove-ItemProperty -Path $reg -Name $_.Name -Force } } # 重建字体缓存,随后重启 FontCache 服务 Stop-Service FontCache -Force -ErrorAction SilentlyContinue Remove-Item (Join-Path $env:WINDIR 'System32\FNTCACHE.DAT') -Force -ErrorAction SilentlyContinue Start-Service FontCache改注册表之前先导出那一支做备份,是我现在的固定动作:reg export "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" fonts-backup.reg。真出问题时双击回滚,比一条条回想删了什么快太多。
4. 塞进容器与 CI:让无头浏览器和文档导出认得中文的三步
本地 Windows 上那套逻辑搬到 Linux 容器里几乎全部失效:没有注册表,字体发现靠 fontconfig 扫目录,缓存文件也换了地方。这也是「本地正常、容器出方块」最常见的来源。
4.1 容器里字体装不上的三个真实原因
第一,只复制了文件,没让 fontconfig 扫到。字体必须落在 fontconfig 配置里声明的目录下,默认是/usr/share/fonts及其子目录;丢到/opt/fonts又不写配置,等于没装。
第二,镜像层缓存。字体拷进去了、fc-cache也跑了,但那条RUN在更早的层里被缓存复用,实际镜像里字体目录是空的。判断方法很简单,进容器fc-list | wc -l一看便知。
第三,应用有自己的字体路径。有些运行时读$JAVA_HOME下的字体目录,有些办公套件读用户 profile 里的配置,完全绕过 fontconfig。这类情况得单独指定,光改系统字体没用。
4.2 Dockerfile 与 fontconfig 配置
最省事的做法是把字体目录拷进标准路径,然后强制重建一次缓存:
FROM debian:bookworm-slim RUN apt-get update \ && apt-get install -y --no-install-recommends fontconfig \ && rm -rf /var/lib/apt/lists/* # 把 Windows 字体包整个拷进标准目录,注意保持可读权限 COPY ./win-fonts /usr/share/fonts/truetype/win/ RUN chmod -R 0644 /usr/share/fonts/truetype/win/ \ && fc-cache -fv \ && fc-list :lang=zh | wc -l # 构建期就能看到中文字体条数,为 0 直接失败fc-cache -fv的-f是强制重建、不信任已有缓存,-v打日志方便在构建输出里核对。最后那行fc-list :lang=zh | wc -l建议保留——它相当于一个构建期断言,中文字体条数为 0 时镜像直接构建失败,比上线后发现方块强。
如果字体必须放在自定义目录(比如挂载卷),就得写一份 fontconfig 配置,并用环境变量指过去:
<?xml version="1.0"?> <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd"> <fontconfig> <dir>/opt/fonts/win</dir> <cachedir>/var/cache/fontconfig</cachedir> <!-- 请求中文名时映射到实际字族,减少回退到默认无衬线体的概率 --> <match target="pattern"> <test name="family"><string>宋体</string></test> <edit name="family" mode="assign" binding="same"><string>SimSun</string></edit> </match> </fontconfig>配好后FONTCONFIG_FILE=/etc/fonts/local.conf指过去即可。cachedir必须可写,否则每次进程启动都要重扫一遍字体目录,容器冷启动会明显变慢;只读根文件系统的情况下,把它挂成 tmpfs 或者干脆在构建期固定下来。
4.3 三步验证:能匹配、有字形、能出图
装完必须验,而且要分三层验,任何一层缺失后面都会以方块的形式暴露出来。
第一层,fontconfig 能不能匹配到:
fc-match -f '%{file}\n' 'SimSun' # 应输出具体的 .ttf/.ttc 路径 fc-list :charset=4e2d family | sort -u # 列出含“中”字( U+4E2D )字形的字族fc-match只要不报错就一定返回点什么,所以必须看输出路径是不是你期望的那个文件;返回一个无关的默认字体,说明你的映射没生效。fc-list :charset=是更硬的指标,直接按字符查覆盖。
第二层,字体文件本身有没有这些字形。这一步绕开 fontconfig,直接读 cmap 表,能排除「匹配到了但缺字」的情况:
from fontTools.ttLib import TTFont, TTCollection def cmap_of(path): """兼容单体和 TTC:TTC 里逐个字面检查,任一命中即算覆盖""" if path.lower().endswith('.ttc'): return [f.getBestCmap() or {} for f in TTCollection(path).fonts] return [TTFont(path).getBestCmap() or {}] need = '汉字报表导出测试' for p in ['/usr/share/fonts/truetype/win/simsun.ttc']: maps = cmap_of(p) missing = [ch for ch in need if all(ord(ch) not in m for m in maps)] print(p, '缺失:', missing or '无')getBestCmap()会自动挑 Unicode 平台的最佳映射表,比手动遍历 cmap 子表稳。TTC 用TTCollection迭代,逐个字面查,只要有一个字面覆盖就算过。
第三层,端到端出图。用一个固定文案跑一次真实的渲染链路(页面截图、PDF 导出、图表服务),把输出的 PNG 或 PDF 存档,肉眼或脚本比对一次。前两层过了第三层还失败,基本就是应用自己的字体路径问题,按 4.1 的第三条去查。
5. 避坑记录:字体装了却不生效的五种现场与排查顺序
下面这五条都是实际撞过的,每条按「现象 → 原因 → 解决」记,遇到类似症状可以直接对号入座。排查顺序建议从上往下,因为前面的概率明显更高。
5.1 装完在应用里搜不到
现象:字体目录里文件确实在,注册表也查得到,但在设计软件、办公套件的字体下拉里就是没有这个名字,重启软件也没用。
原因:装成了当前用户级。文件落在当前用户的本地目录,注册表写在 HKCU 分支,而以另一个账号身份运行的应用、服务、计划任务读的是 HKLM 那一套,天然看不见。
解决:改成机器级安装,把文件复制到系统字体目录、写 HKLM 分支,再广播一次字体变更消息。已有用户级安装的先清理掉,两处同名共存会让排查变得非常混乱。
5.2 复制到系统目录时报拒绝访问
现象:脚本明明用管理员跑的,Copy-Item到系统字体目录却报无权限,或者报成功了但字体列表里没有。
原因:系统字体目录有特殊的访问控制,直接文件写入在某些环境下会被拦;更常见的是脚本只做了复制、没写注册表,Windows 不会主动扫描目录登记新文件。
解决:复制和登记必须成对出现。复制失败时用 Shell 的CopyHere动词兜底(见 3.2 的脚本),登记用New-ItemProperty写进 HKLM 分支,最后调AddFontResourceW加广播。三步全做完再判断成败。
5.3 同名不同版本打架,字形变成旧的
现象:升级了字体版本,重启后一部分应用显示新字形,另一部分还是老的,同一台机器两种样子。
原因:旧版本还躺在另一处(用户级目录或另一个字体目录),系统里有两条记录指向不同文件,不同进程按各自加载顺序取到了不同的那一份。
解决:先把两处注册表分支都导出对比,找出所有指向同族名的记录,删掉多余条目和对应文件,然后停掉字体缓存服务、删掉缓存文件、重启服务,让系统彻底重建一次。
5.4 容器里字体装了还是方块
现象:Dockerfile 里明明COPY了字体,fc-list也能列出来,业务跑起来中文依旧是方块。
原因:多半是字体落在了 fontconfig 没扫描的目录,或者构建缓存复用了旧层;也可能是业务进程用的是自带字体路径的运行时,压根没走 fontconfig。
解决:先fc-list :lang=zh | wc -l确认条数;为零就检查目录是否在<dir>列表里、缓存有没有重建。条数正常还出方块,就往应用层查——找它的字体配置项,直接指向具体字体文件。
5.5 TTC 里只有常规体生效,加粗变成模拟加粗
现象:正文正常,一加粗字形就发糊、发扁,或者干脆还是一个粗细。
原因:TTC 里多个字面需要分别登记,安装脚本只按文件名登记了一条;下游应用枚举不到独立的粗体字面,就用几何方式把常规体横向拉伸来凑。
解决:要么把每个字面的族名都登记一遍,值统一指向该 TTC 文件;要么干脆把 TTC 拆成单体 TTF 再装。面向服务端渲染的场景,我更倾向后者,少一层不确定性。
6. 子集化与嵌入校验:把二十多兆的字体压到几百 KB
整包字体塞进前端产物或者移动端包里,体积是很难接受的。一个中文字库动辄十几二十兆,而页面上真正用到的可能就几百个字。子集化就是把用不到的字形裁掉,只保留需要的码位。
# 按文本文件 + 码位区间做子集,输出 woff2 供前端使用(需要 brotli 支持) pyftsubset SimSun.ttf \ --text-file=used-chars.txt \ --unicodes="U+0020-007E,U+3000-303F,U+FF00-FFEF" \ --layout-features='kern,liga,vert,vrt2' \ --no-hinting \ --desubroutinize \ --flavor=woff2 \ --output-file=simsun-sub.woff2参数逐条说一下。--text-file是页面实际用到的字符清单,从模板里抽一遍就行;--unicodes补上必须保留的码位区间——基本拉丁、中文标点、全角字符,这三段漏了就会出现「英文正常、标点变方块」的尴尬。--layout-features里kern是字距调整,vert/vrt2是竖排特性,做中文竖排排版必须留。--no-hinting去掉微调指令能省一截体积,屏幕渲染损失不大。--desubroutinize只对 CFF 轮廓有意义,展开子程序后兼容性更好但体积略增,TTF 源文件可以不加。--flavor=woff2依赖 brotli,装不上就退回woff。
| 参数 | 作用 | 常见误设 |
|---|---|---|
--text-file | 按实际用字裁剪 | 直接拿整篇文档,等于没裁 |
--unicodes | 强制保留码位区间 | 漏掉全角标点,导出全是方块 |
--layout-features | 保留排印特性 | 删掉 kern,标题字距散架 |
--no-hinting | 去掉微调指令 | 小字号屏显略糊,需实测 |
--flavor | 输出格式 | 容器里没装 brotli 直接报错 |
子集化最大的坑不在参数,在适用边界。它只适合文案固定的场景:静态海报、PDF 模板、打包进客户端的界面文字。凡是用户能自由输入的内容——搜索框、评论、昵称——一律不能用子集字体,否则用户打出个生僻字就是一个方块。这种场景要么用系统字体栈,要么保留完整字库并按需分片加载。
校验这一步我会写成脚本固定下来,跑完子集立刻比对一遍:
from fontTools.ttLib import TTFont cmap = TTFont('simsun-sub.woff2').getBestCmap() need = open('used-chars.txt', encoding='utf-8').read().strip() missing = [c for c in need if ord(c) not in cmap] print('缺失字形:', missing or '无') # 非空就别发版从那以后,我每次做字体裁剪都强制走一遍「抽字符清单 → 子集 → cmap 比对 → 真实渲染截图存档」,四步少一步都不发版。字体这东西出问题不会报错,只会安静地把字变成方块,等你从用户嘴里听到的时候,已经是一轮返工了。这套 Windows 字体包我现在的用法也很固定:一份留在本地做渲染基准,一份进容器镜像做兜底,剩下的按需裁剪。希望帮到你。
本文还有配套的精品资源,点击获取