把一份带图的中文报告交出去,对方回一句"这字不是黑体",接着又补一句"你这字体没嵌进去,我这边打开是空白"——这类问题我这些年遇到的次数,比想象中多得多。更前置的一个疑问是:装字体的时候,列表里"黑体"和"黑体GB2312"并排躺着,看起来一模一样,到底该选哪个?选错了会怎样?再往下走一步,就是很多用 R 出图的人绕不开的坑:明明系统里字体装好了,pdf()一出,中文全是方框、全是点,或者在自己电脑上好好的,换台机器就变成一片空白。
这篇东西想解决的就是这条链路上的三个环节:黑体与黑体GB2312在字符集、字形覆盖、命名体系上到底差在哪;R 的 PDF 设备处理中文字体有几条技术路线,各自的行为边界是什么;在真实交付场景里,怎么把"指定的那个黑体"稳定、可复现地写进 PDF。读者对象不限基础——刚上手 R 画图的人能照着抄步骤,出过几十份报告被字体问题折磨过的人,应该能在排查表和几个隐蔽坑里找到共鸣。
1. 先搞清楚"黑体"这两个词到底在说什么
1.1 GB2312 后缀不是风格后缀,而是字符集后缀
很多人第一反应是"黑体GB2312 是不是黑体的加粗版/精简版/老版本"。都不是。"黑体"描述的是字形风格——无衬线、笔画粗细均匀、横竖对比小、端点平切;"GB2312"描述的是这个字体文件里装了多少个字。这是两个完全正交的维度,只是被塞进同一个名字里了。
GB 2312 是一套简体中文字符编码标准,收录一级汉字 3755 个、二级汉字 3008 个,合计 6763 个汉字,外加约 682 个非汉字图形符号(标点、序号、希腊字母、俄文字母、日文假名、制表符等)。带 GB2312 后缀的字体,通常意味着它的字符覆盖范围就落在这个圈里。这在早期是有明确工程理由的:字体文件大小、内存占用、渲染性能都受限,只做常用字能省下大量空间。放到今天的硬件环境里这个约束基本消失了,但历史字库版本还留在很多人的字体列表里。
对照一下就能明白差距有多大:
| 字符集/标准 | 汉字数量(约) | 典型覆盖场景 |
|---|---|---|
| GB 2312 | 6763 | 常用简体文本、早期排版系统 |
| GBK | 21000+ | 简体为主、兼容繁体与部分生僻字 |
| GB 18030 | 27000+(随版本扩充) | 全面覆盖,含大量生僻字、人名用字 |
| Unicode 全量 | 90000+(含扩展区) | 学术、古籍、方志、少数民族文字 |
真正的问题出在"缺字"这件事上。文本里只要冒出一个 GB2312 圈外的字——某些人名用字、地名用字、异体字、繁体字、生僻的科技名词——字体就渲染不出来。这时候不同软件的处理策略不一样:有的直接画一个空心方框(俗称"豆腐块"),有的静默回退到系统里的另一款字体去渲染这个字。回退这件事最阴险,因为它在屏幕上看起来"能显示",但同一行文字里两个字的字形、字重、字面大小完全不是一套,混排出来非常难看,而且你在自己的机器上看不出来,到别人机器上才暴露。
注意:判断一个字体够不够用,别靠眼睛看"我这段文字显示正常",要靠工具查覆盖范围。中文正文里出现一个缺字,整段排版就会被阅读器重新分字,行宽也会跟着变。
1.2 就算同一个字,两版字体的字形也可能不一样
这一层更少人注意到。GB2312 时代的字库,很多是按当年的字形规范设计的,笔画写法、部件比例、字面率(字形实际占据字身框的比例)、重心位置,跟后来重制的现代黑体有肉眼可见的差别。举几个典型的表现:
- 字面率偏大。老字体的字面撑得更满,一行字排下来视觉上更"挤",行距显得不够;现代黑体(比如开源的思源黑体系列)字面率收敛一些,同样字号同样行距,读起来更松。
- 全角标点位置不同。逗号、句号、顿号在老字体里常常更靠左下,现代字体倾向于居中偏下。混用两套字体时,标点会像"跳"出来一样。
- 竖排与横排的标点处理。老字体的标点旋转、避头尾规则支持往往不完整,一旦要排竖排或者严格避头尾,就会出问题。
- 字重只有一档。绝大多数 GB2312 版本的字体只提供一个 Regular 字重。你在 R 里写
fontface = "bold",设备找不到粗体字形,要么忽略,要么做"伪粗体"——把同一串文字偏移一点点重复画一遍。伪粗体在 PDF 里是致命的,因为它不是矢量轮廓的加粗,放大看边缘会糊、会有重影,打印出来尤其难看。
所以"用黑体GB2312"和"用黑体",不只是字数够不够的问题,而是整份文档的排版气质会变。如果一份报告里既有正文(用了带 GB2312 的字体)又有图(用了现代黑体),细看会觉得图文不是一个人做的。
1.3 字体名、文件名、族名、PostScript 名:四套命名各管一段
这一节是纯经验,也是很多人卡住的地方。一个字体在系统里至少有四个"名字",不同软件认的不是同一个:
| 名称类型 | 例子 | 谁在用 |
|---|---|---|
| 文件名 | simhei.ttf | 文件管理器、font_add()这类需要路径的函数 |
| 族名(Family) | SimHei/黑体 | 大部分排版软件、R 的family参数(部分设备) |
| 全名(Full name) | SimHei Regular | 部分字体选择对话框 |
| PostScript 名 | SimHei | PDF 内部记录、打印工作流、部分检查工具 |
"黑体GB2312"这种带后缀的字体,往往族名里就实打实带着GB2312字样,比如XXX黑体GB2312。这时候你在 R 里写family = "黑体"是匹配不上的,必须写成完整族名。更麻烦的是中文族名在某些设备路径上会变成乱码或者匹配失败,用英文/拼音的 PostScript 名通常更稳。
我这几年养成的习惯是:不确定名字的时候,绝不去字体列表里凭印象猜,一律用工具查实际注册名,见第 3 节的实操。名字对不上是"中文出方框"最常见的原因,比字体没装还常见。
2. R 的 PDF 设备是怎么处理中文的
2.1pdf()的内置 CID 路线:family = "GB1"到底做了什么
R 的基础pdf()设备,family参数默认是"sans",可选项里除了serif/mono/symbol,还有一类很多文档里一笔带过的取值:"GB1"(简体中文)、"CNS1"(繁体中文)、"Japan1"、"Korea1"。这些是 Adobe 定义的 CID 字体集合,pdf(family = "GB1")输出的 PDF 里,文字被标记为"请用 GB1 这个 CID 集合里的字体来渲染"。
这里有一个关键行为要理解清楚:这条路线不嵌入字体。PDF 里只记录了"这是 GB1 集合的某个字形编号",具体长什么样,由打开 PDF 的软件自己去找字体渲染。这就解释了那个经典现象——你在自己电脑上打开有中文,发给别人打开是一片空白或者方框,尤其在手机、某些轻量阅读器、某些打印机 RIP 上。
顺带说一句:pdf()的family参数只认设备内置的那几张映射表,你直接写family = "SimHei"是没用的,会被静默回退到默认字体。这一点和windowsFonts()注册出来的设备完全是两套机制,混着记特别容易出错。
所以pdf(family = "GB1")的正确使用场景是:只在自己机器上看、不交付、不打印的临时草图。真要做交付件,别走这条路。
2.2 三条落地路线对比:pdf()/cairo_pdf()/showtext
R 里往 PDF 写中文,基本上就三条路,行为差别很大,我整理成一张表,后面再逐条拆:
| 路线 | 核心调用 | 是否嵌入字体 | 文字可否选中/复制 | 跨平台一致性 | 适用场景 |
|---|---|---|---|---|---|
| 内置 CID | pdf(family = "GB1") | 否 | 可以,但依赖阅读器字体 | 差 | 临时预览 |
| Cairo 设备 | cairo_pdf(family = "...") | 是(子集嵌入) | 可以,且可搜索 | 好(需字体存在) | 正式交付首选 |
| 字形绘路径 | showtext+pdf() | 变相"嵌入"(转矢量路径) | 不可以 | 好 | 图形标题、装饰性文字 |
| 字体嵌入后处理 | extrafont+embed_fonts() | 是 | 可以 | 一般 | 历史项目维护 |
cairo_pdf()是我现在的默认选择。它是基于 Cairo 图形库的 PDF 设备,会真正把字体子集嵌进文件里,输出件在别人机器上打开字形稳定,文字还能选中复制。代价是文件里会多出几十到几百 KB 的字体数据,以及一个隐含前提:你调用时写的字体名,必须能被系统的字体查找机制(fontconfig 一类)解析到。Windows 上偶尔会遇到cairo_pdf(family = "黑体")报找不到字体,换成"SimHei"就好了,本质还是 1.3 节说的命名匹配问题。
showtext的原理不一样,它是用 FreeType 把文字渲染成字形轮廓,再当作普通绘图指令画到设备上。好处是"只要给得出字体文件路径,就一定画得出来",完全绕开系统字体查找;坏处是PDF 里那不再是文字,而是一堆矢量路径——不能选中、不能复制、不能全文搜索、不能被辅助阅读软件识别。做论文插图、封面标题这类"只要好看"的内容很合适,做正文报告就难受了。另外它会显著增大文件体积,一页几百个汉字的图,路径数据能轻松吃掉几百 KB。
extrafont这条线现在用得少了。它的主力是 Type1 字体,对 TTF 的导入在部分平台上有各种依赖问题,font_import()跑十几分钟是常事。老项目里还能见到,新项目我基本不碰。
2.3 为什么"能显示"和"能交付"是两件事
这句话我想单独强调一下,因为它是所有字体问题的分水岭。"我屏幕上看着没问题",和"这个 PDF 拿到任何一台机器上都长这样",是两个完全不同级别的保证。
判断一份 PDF 是否合格,只看一个指标:字体是否嵌入。检查方法很直接,R 里就能做:
# 列出 PDF 里用到的字体,以及是否嵌入、是否子集 pdftools::pdf_fonts("report.pdf")输出里重点看两列:embedded为TRUE才算过关;subset为TRUE说明只嵌入了用到的字形,这是正常且推荐的做法(体积小得多),不用紧张。如果embedded是FALSE,那这份文件在别人那里随时可能"变脸"。
还有一个次要但真实存在的指标:PDF 版本。cairo_pdf()默认输出 PDF 1.4 左右,兼容性足够好。某些打印工作流会挑剔字体子集的编码方式,遇到打印店说"字体报错",先确认嵌入状态,再考虑换成全量嵌入或者把文字转曲(转成路径,代价是失去可搜索性)。
3. 实操:把指定的黑体稳定写进 PDF
3.1 第一步:把系统里到底有哪些字体查清楚
别猜,直接查。我一般用两条命令交叉验证:
# 模糊匹配字体名,返回命中的文件路径 systemfonts::match_font("SimHei") systemfonts::match_font("黑体") # 列出所有字体,按族名筛 all_fonts <- systemfonts::system_fonts() hit <- subset(all_fonts, grepl("Hei|黑", family, ignore.case = TRUE)) hit[, c("path", "family", "style", "weight", "italic")]path这一列是重点,它告诉你这个族名最终指向哪个字体文件。这一步能帮你确认两件事:一是"黑体"和"黑体GB2312"各自的真实文件路径是不是同一个(有时是同一文件被注册了多个别名,有时真是两个文件);二是当你需要用showtext时,直接拿这个路径去font_add(),避免名字解析问题。
Linux 和 macOS 上还有fc-list可以用,输出信息更原始但更可靠:
# 查所有含 Hei 的字体及其族名、样式 fc-list : family style file | grep -i hei # 只看某个字体文件包含多少字形 fc-query --format '%{charset}\n' /path/to/simhei.ttf | wc -w最后那条fc-query挺有用,能粗看字符覆盖范围。一个只覆盖 GB2312 的字体和一个覆盖 GBK 的字体,输出长度差得很明显。
3.2 第二步:用showtext打通绘图与设备
showtext的用法很固定,四步走:
install.packages(c("showtext", "sysfonts")) library(showtext) # 1. 注册一个自己起的别名,指向真实字体文件 font_add("myhei", regular = "/path/to/simhei.ttf", bold = "/path/to/simhei-bold.ttf", # 没有就留空 italic = "/path/to/simhei-italic.ttf") # 2. 打开自动接管 showtext_auto() showtext_opts(dpi = 300) # 影响位图设备,PDF 下不生效但写上无害 # 3. 正常画图 pdf("report.pdf", width = 7.5, height = 5) plot(1:10, main = "中文标题测试", xlab = "横轴", ylab = "纵轴") text(5, 5, "标注文字", family = "myhei") dev.off() # 4. 如果只想在局部生效,可以用配对函数 showtext_begin() # ... 绘图 ... showtext_end()几个实操细节值得单独说:
bold参数留空会怎样?如果字体本身没有粗体字形,font_add里不指定bold,绘图时请求粗体会回退到 regular,看起来"加粗无效"。想真加粗,得另找一个粗体文件(比如同族的 Heavy/Bold 版本)。不要指望showtext帮你合成粗体,它不做这件事。
showtext_auto()的作用域是全局的,包加载之后所有新开的图形设备都会被接管。这在交互式会话里很方便,但在脚本里批量出图时容易踩到"某些设备被意外接管"的坑,比如你想让某张图走cairo_pdf原生的字体嵌入,结果被showtext提前转成路径了。习惯做法是:批处理脚本里显式用showtext_begin()/showtext_end()包住需要的那一段,而不是全局开启。
字体文件路径别写死绝对路径,尤其是跨机器协作的项目。我现在的惯例是在项目根目录建一个assets/fonts/放 ttf 文件,代码里用here::here("assets/fonts/simhei.ttf")拼路径。这样换机器、换系统都不会崩。
3.3 第三步:ggplot2里的字体设置与全局统一
ggplot2的字体控制有三层,很多人只改了一层,结果图里的标题改了、坐标轴没改。
第一层是几何对象和annotate/geom_text的family参数,只影响单个元素。第二层是theme()里的text元素——它是所有文字元素的父级,改它能一次覆盖标题、轴标签、图例、分面标签:
library(ggplot2) p <- ggplot(mtcars, aes(wt, mpg)) + geom_point() + labs(title = "中文标题", x = "车重", y = "油耗") + theme_bw() + theme( text = element_text(family = "myhei", size = 11), plot.title = element_text(family = "myhei", size = 14, face = "bold"), axis.text = element_text(family = "myhei", size = 9) )第三层是设备层的family参数,也就是保存文件那一刻。
# 路线 A:showtext 路线,设备不需要指定 family pdf("fig_showtext.pdf", width = 7.5, height = 5) print(p) dev.off() # 路线 B:cairo 路线,字体嵌入更干净 cairo_pdf("fig_cairo.pdf", width = 7.5, height = 5, family = "SimHei") print(p) dev.off()路线 B 值得多说一句。cairo_pdf(family = ...)相当于给整个设备设了一个兜底字体族,theme()里没指定family的元素会用它。但如果theme()里指定了"myhei"这种只有showtext认识的别名,cairo 是找不到的,会静默回退。所以这两条路线不要混着用:要么全走showtext的别名体系,要么全走系统真实族名体系。
还有一个小细节:ggsave()默认根据扩展名选设备,.pdf后缀走的是pdf()。如果项目里必须用 cairo,得显式指定:
ggsave("fig.pdf", plot = p, device = cairo_pdf, width = 7.5, height = 5, units = "in", family = "SimHei")ggsave的family参数是传给设备的,不是传给theme的,这一点搞混的人不少。
3.4 字号、画布、字重的连带影响
这块属于"参数层面说不清就会返工"的部分,我按算式说。
PDF 是矢量格式,内部长度单位是点(pt),1 pt = 1/72 英寸。pdf(width = 7.5, height = 5)的单位是英寸,所以画布宽 7.5 × 72 = 540 pt。ggplot2的base_size或者theme里的size,在这个设备上就是 pt。
于是可以反推:如果正文指望 11 pt,一行想放 40 个汉字,那正文字宽需要 40 × 11 = 440 pt = 6.11 英寸。画布 7.5 英寸减去左右边距(比如各 0.6 英寸),正好 6.3 英寸,能放下。要是把size调到 13 pt,同样宽度只能放 33 个字,标题一长就得换行或者被截断。字号和画布宽度是绑在一起的一对参数,改一个就得复核另一个。
字重的问题在 1.2 节提过,这里给具体的处理方式:如果一个字体族只有一个字重,face = "bold"在cairo_pdf下的表现是——要么静默使用 regular(看起来没加粗),要么 Cairo 自己做伪粗体。验伪粗体的办法很简单:把 PDF 放大到 400%,看笔画边缘。真正的粗体是轮廓加宽,伪粗体是原字重复偏移,边缘会有重影。
提示:正式交付的图,尽量别依赖
face = "bold"去"要"一个粗体。要么找一个真有 Bold 字重的字体族(现代开源黑体普遍提供多字重),要么用颜色、字号、下划线来做视觉区分,后者更稳。
4. 常见问题与排查手册
4.1 症状速查表
| 症状 | 最可能原因 | 处理方式 |
|---|---|---|
| 中文全变方框 | pdf()走了默认 sans,未做任何中文处理 | 改用cairo_pdf()或接入showtext |
| 自己电脑正常,别人打开空白 | 用了pdf(family = "GB1"),字体未嵌入 | 换cairo_pdf(),用pdffonts复查 |
| 部分字变方框,其他正常 | 字体字符集只到 GB2312,遇到圈外字 | 换 GBK/GB18030 覆盖的字体,或换全量字体 |
| 同一行字字形不一致 | 缺字触发了阅读器的 fallback | 同上,从根上补覆盖 |
family写了没反应 | 名字与系统注册名不匹配 | 用systemfonts::match_font()查真实名 |
| 中文能显示但无法搜索复制 | showtext把文字转成了路径 | 换cairo_pdf原生嵌入 |
| 加粗没效果 | 字体族只有一个字重 | 引入带 Bold 的字体,或改用其他视觉区分 |
| 文件体积异常大 | 字体全量嵌入或路径数据过多 | 用子集嵌入;检查是否误开了showtext |
| 打印店说字体报错 | 嵌入不完整或字体授权限制 | 确认embedded = TRUE;必要时转曲 |
4.2 几个特别隐蔽的坑
第一个坑:RStudio 的预览窗口和导出的不是一个设备。RStudio 图形面板里看到的是它自己的一套渲染路径,它对中文的容错比pdf()高得多,很容易造成"看着挺好"的错觉。判断依据永远以导出文件为准——导出后拿独立的阅读器打开看,别信 IDE 面板。
第二个坑:pdf()的onefile参数与字体。多页 PDF 时onefile = TRUE(默认)会把所有页放进一个文件,字体信息在文件头统一记录。这本身没问题,但如果中途dev.off()了又重开一个同名文件,前面那个会被覆盖,而不是追加。批量出图时我见过有人写循环忘了dev.off()的位置,最后只剩最后一页,还以为是字体问题。
第三个坑:字体子集化后被"二次编辑"工具改坏。有些 PDF 编辑/合并工具处理子集字体时会丢字符映射表,合并完之后文字看着在、复制出来是乱码,或者干脆显示异常。交付件最好不要用可视化工具去合并、加水印,能脚本化就脚本化,中间的每一步都可追溯。
第四个坑:中英文混排的行高。中文全角字的高度和西文的 x-height 不一样,同一个size下,纯中文行看着比中西混排行"松"。ggplot2的lineheight参数可以调,我一般在theme(text = element_text(lineheight = 1.15))这个量级试。这个值没有标准答案,跟字体本身的度量有关,得实际出图看。
第五个坑:目录里的字体,脚本里读不到。换了台机器,~/.fonts或者系统字体目录里没装那个字体,systemfonts就查不到,cairo_pdf会静默回退到别的字体,出图完全不报错,但字形不对。规避方式是把字体文件随项目走,用绝对路径或项目相对路径加载,不依赖系统安装状态。这一条我觉得是团队协作里最重要的一条。
4.3 交付前的自检清单
我自己的习惯是出完图跑一个小脚本,让机器替我检查,而不是靠眼睛:
check_pdf <- function(path) { fonts <- pdftools::pdf_fonts(path) bad <- subset(fonts, !embedded) if (nrow(bad) > 0) { message("以下字体未嵌入:", paste(unique(bad$name), collapse = ", ")) } else { message("字体嵌入检查通过,共 ", nrow(fonts), " 个字体条目") } invisible(fonts) } check_pdf("report.pdf")除此之外,人工还要过三关:一是渲染抽查,把 PDF 在非本机的阅读器里打开,或者用命令行工具转成图片看一眼;二是文本抽查,随便复制一两行中文粘到记事本里,看是不是乱码或者缺字;三是打印抽查,如果有纸质需求,先打一页黑白稿,看字重和灰度。
5. 多平台与批量出图的工程化做法
5.1 把字体配置收敛成一个函数
项目里图一多,散落在各处的family参数就是灾难。我的做法是写一个初始化函数,把字体路径、别名注册、设备选择全部收在一起,脚本开头调用一次:
init_fonts <- function(project_root = here::here()) { font_dir <- file.path(project_root, "assets", "fonts") stopifnot(dir.exists(font_dir)) hei_reg <- file.path(font_dir, "heiti-regular.ttf") hei_bold <- file.path(font_dir, "heiti-bold.ttf") stopifnot(file.exists(hei_reg)) sysfonts::font_add( family = "proj_hei", regular = hei_reg, bold = if (file.exists(hei_bold)) hei_bold else NULL ) showtext::showtext_auto() options(proj.font = "proj_hei") invisible(TRUE) }之后所有出图代码只引用getOption("proj.font"),再也不出现硬编码的字体名字。这个改造做完之后,换字体就是换目录里的 ttf 文件,代码一行不用动。别小看这一步,字体需求变更是常态,尤其是甲方中途说"标题换成稍粗一点的黑体"的时候。
5.2 不同平台的字体名差异要提前想好
同一款字体在不同平台上注册的名字可能不一样。常见的差异是:桌面系统上族名可能是SimHei也可能是黑体,Linux 上 fontconfig 可能把它登记成SimHei或者带厂商前缀的完整名,而移动端或者精简系统上可能压根没有这款字体。
我的处理原则比较简单粗暴:不依赖系统字体,全部走随项目分发的字体文件 +showtext或cairo_pdf的显式路径。这样跨平台的一致性最好,代价是项目体积多几 MB。对于要长期维护的报告项目,这几 MB 花得很值。
如果非要用系统字体(比如某些环境限制不能分发字体文件),那就得写一层探测逻辑:
pick_font <- function(candidates) { for (f in candidates) { hit <- tryCatch(systemfonts::match_font(f), error = function(e) NULL) if (!is.null(hit) && nzchar(hit$path[[1]]) && file.exists(hit$path[[1]])) { return(f) } } stop("候选字体一个都没找到,请检查安装情况") } font_name <- pick_font(c("SimHei", "Source Han Sans SC", "Noto Sans CJK SC"))按优先级列表探测,第一个命中的就用。这样在三种平台上都能跑,不至于某台机器上直接崩掉。
5.3 批量出图时的设备管理
批量的时候最容易出的问题不是字体本身,而是设备没关干净。每开一个pdf()或cairo_pdf(),就必须配一个dev.off(),而且要在tryCatch或on.exit里做保护,不然中间某个图报错,设备就悬在那儿,后面所有图都往一个错乱的文件里写。
render_all <- function(plots, out_dir, family = getOption("proj.font")) { dir.create(out_dir, showWarnings = FALSE, recursive = TRUE) for (nm in names(plots)) { out <- file.path(out_dir, paste0(nm, ".pdf")) cairo_pdf(out, width = 7.5, height = 5, family = family) on.exit(dev.off(), add = TRUE) print(plots[[nm]]) dev.off() } invisible(TRUE) }on.exit(dev.off(), add = TRUE)这一行是关键,函数退出时一定关设备,不管中途有没有报错。这种小防御写一次省无数次排查时间。
还有一点:批量出图如果混用了showtext和cairo_pdf,务必确认showtext没有把设备提前接管。我遇到过的情况是,同一个循环里有的图走了cairo_pdf的原生字体嵌入,有的图因为前面某处调了showtext_auto()而被转成路径,最后检查字体嵌入状态时,一堆图显示"无嵌入字体"——因为它们的文字根本不是文字了。
回到最开始那个问题:黑体和黑体GB2312 该怎么选。我的答案很直接——除非有明确的兼容性要求(比如必须和老系统、老版本排版软件对齐),否则不要选带 GB2312 后缀的版本。字符覆盖不够,字形偏旧,字重单一,在今天的交付场景里几乎没有优势。真正需要关心的是字体有没有嵌入、字符覆盖够不够、字重够不够用,这三件事决定了 PDF 出了门还能不能看。至于 R 里的具体做法,我个人现在固定用cairo_pdf + 系统真实族名出正式件,用showtext + 项目内字体文件兜底特殊情况,两套并行跑了几年,基本没再因为字体返过工。