简介面向需要在Windows环境中补齐系统字体库的日常办公、平面排版、网页前端及软件兼容性测试人员这份字体集合可用于解决字体缺失、换机后版式错乱、界面文字显示不一致等常见问题。压缩包共160个文件包含159个TTF字体文件和1个XML配置文件整体约106.14MBTTF便于直接安装调用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 上的装载差异字体文件的扩展名只是外壳真正决定行为的是内部结构和字面数量。把常见几类摆在一起对比选型时心里就有数了扩展名内部轮廓单文件字面数注册表登记方式典型翻车点.ttfTrueType 二次贝塞尔1一条同名不同版本互相覆盖字形悄悄回退.otfCFF / 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_BROADCAST0xFFFF, WM_FONTCHANGE0x001D, SMTO_ABORTIFHUNG0x0002 $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 :langzh | wc -l # 构建期就能看到中文字体条数为 0 直接失败fc-cache -fv的-f是强制重建、不信任已有缓存-v打日志方便在构建输出里核对。最后那行fc-list :langzh | wc -l建议保留——它相当于一个构建期断言中文字体条数为 0 时镜像直接构建失败比上线后发现方块强。如果字体必须放在自定义目录比如挂载卷就得写一份 fontconfig 配置并用环境变量指过去?xml version1.0? !DOCTYPE fontconfig SYSTEM urn:fontconfig:fonts.dtd fontconfig dir/opt/fonts/win/dir cachedir/var/cache/fontconfig/cachedir !-- 请求中文名时映射到实际字族减少回退到默认无衬线体的概率 -- match targetpattern test namefamilystring宋体/string/test edit namefamily modeassign bindingsamestringSimSun/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 :charset4e2d family | sort -u # 列出含“中”字( U4E2D )字形的字族fc-match只要不报错就一定返回点什么所以必须看输出路径是不是你期望的那个文件返回一个无关的默认字体说明你的映射没生效。fc-list :charset是更硬的指标直接按字符查覆盖。第二层字体文件本身有没有这些字形。这一步绕开 fontconfig直接读 cmap 表能排除「匹配到了但缺字」的情况from fontTools.ttLib import TTFont, TTCollection def cmap_of(path): 兼容单体和 TTCTTC 里逐个字面检查任一命中即算覆盖 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 :langzh | wc -l确认条数为零就检查目录是否在dir列表里、缓存有没有重建。条数正常还出方块就往应用层查——找它的字体配置项直接指向具体字体文件。5.5 TTC 里只有常规体生效加粗变成模拟加粗现象正文正常一加粗字形就发糊、发扁或者干脆还是一个粗细。原因TTC 里多个字面需要分别登记安装脚本只按文件名登记了一条下游应用枚举不到独立的粗体字面就用几何方式把常规体横向拉伸来凑。解决要么把每个字面的族名都登记一遍值统一指向该 TTC 文件要么干脆把 TTC 拆成单体 TTF 再装。面向服务端渲染的场景我更倾向后者少一层不确定性。6. 子集化与嵌入校验把二十多兆的字体压到几百 KB整包字体塞进前端产物或者移动端包里体积是很难接受的。一个中文字库动辄十几二十兆而页面上真正用到的可能就几百个字。子集化就是把用不到的字形裁掉只保留需要的码位。# 按文本文件 码位区间做子集输出 woff2 供前端使用需要 brotli 支持 pyftsubset SimSun.ttf \ --text-fileused-chars.txt \ --unicodesU0020-007E,U3000-303F,UFF00-FFEF \ --layout-featureskern,liga,vert,vrt2 \ --no-hinting \ --desubroutinize \ --flavorwoff2 \ --output-filesimsun-sub.woff2参数逐条说一下。--text-file是页面实际用到的字符清单从模板里抽一遍就行--unicodes补上必须保留的码位区间——基本拉丁、中文标点、全角字符这三段漏了就会出现「英文正常、标点变方块」的尴尬。--layout-features里kern是字距调整vert/vrt2是竖排特性做中文竖排排版必须留。--no-hinting去掉微调指令能省一截体积屏幕渲染损失不大。--desubroutinize只对 CFF 轮廓有意义展开子程序后兼容性更好但体积略增TTF 源文件可以不加。--flavorwoff2依赖 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, encodingutf-8).read().strip() missing [c for c in need if ord(c) not in cmap] print(缺失字形:, missing or 无) # 非空就别发版从那以后我每次做字体裁剪都强制走一遍「抽字符清单 → 子集 → cmap 比对 → 真实渲染截图存档」四步少一步都不发版。字体这东西出问题不会报错只会安静地把字变成方块等你从用户嘴里听到的时候已经是一轮返工了。这套 Windows 字体包我现在的用法也很固定一份留在本地做渲染基准一份进容器镜像做兜底剩下的按需裁剪。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?