在 R 里指定字体,不只是把 family 填进 theme()。一张图能否稳定显示,取决于三个彼此独立的环节:
- 字体可用性:当前机器能否找到目标字体及其字重;
- 绘图系统:ggplot2、base graphics 或 grid 是否把正确的 family 交给设备;
- 图形设备:PNG、PDF 或 SVG 最终怎样绘制、嵌入或替代字形。
最常见的问题——unknown family、中文方框、PDF 换字体、屏幕和导出不一致——通常都是把这三层混在一起造成的。
先按输出格式选择方案
| 目标 | 推荐设备 | 字体处理 | 主要特点 |
|---|---|---|---|
| PNG、JPEG、TIFF | ragg |
直接使用系统字体 | 跨平台一致,抗锯齿和旋转文字质量好 |
| PDF,文字需要选择和搜索 | grDevices::cairo_pdf() |
在支持的平台嵌入使用的字体 | 适合论文、报告和后续排版 |
| PDF/SVG,优先保证任何机器都长得一样 | showtext + 对应设备 |
把字形转成轮廓或栅格 | 不依赖查看端字体,但文字通常不再是普通文本 |
旧项目依赖 extrafont |
pdf() + Ghostscript |
导入字体后再嵌入 | 可维护旧流程,不建议作为新项目默认方案 |
一个实用的默认组合是:systemfonts 负责发现字体,ragg 负责位图,cairo_pdf() 负责 PDF。 只有字体文件不能安装、CJK 字形仍不稳定,或必须跨机器保持外观时,再使用 showtext。
检查 R 实际能看到的字体
操作系统里显示的字体名、字体文件名和 R 应使用的 family 可能不同。不要根据 .ttf 或 .otf 文件名猜 family,先查询字体元数据。
library(systemfonts)
fonts <- system_fonts()
subset(
fonts,
grepl("Source Han|Noto", family, ignore.case = TRUE),
select = c(family, style, weight, path)
)
确认一个 family 会匹配到哪个文件:
match_fonts("Source Han Sans SC")
match_fonts("Source Han Sans SC", weight = "bold")
match_fonts() 即使找不到精确名称也会回退到默认字体,因此“返回了路径”不等于目标字体确实存在。需要严格校验时,应先在 system_fonts()$family 中检查名称:
font_family <- "Source Han Sans SC"
if (!font_family %in% system_fonts()$family) {
stop("缺少字体:", font_family)
}
系统安装与项目内字体
系统安装适合日常交互使用:在 Windows、macOS 或 Linux 中正常安装字体,重启 R/RStudio,然后再用 system_fonts() 检查。它的缺点是换到服务器、CI 或同事电脑时,字体未必存在。
需要可复现时,把获得合法授权的字体文件放进项目的 fonts/ 目录。systemfonts 加载时会扫描项目下的这个目录,也可以显式加入文件:
library(systemfonts)
add_fonts(c(
"fonts/SourceHanSansSC-Regular.otf",
"fonts/SourceHanSansSC-Bold.otf"
))
显式注册适合脚本启动时立即使用字体;项目目录则便于把代码与字体版本一起管理。无论哪种方式,都应先确认字体许可是否允许随项目分发或嵌入文档。
如果字体可以从公共字体仓库取得,也可以在脚本中声明需求:
systemfonts::require_font(
"Noto Sans",
fallback = "sans"
)
这比悄悄回退更可靠:关键图形可以让缺字体直接报错,非关键图形则明确指定 fallback。
在三套绘图系统中指定 family
字体发现和注册完成后,绘图代码只负责传递 family 名称。
ggplot2
library(ggplot2)
font_family <- "Source Han Sans SC"
p <- ggplot(mtcars, aes(mpg, wt)) +
geom_point() +
labs(
title = "油耗与车重",
x = "每加仑英里数",
y = "车重"
) +
theme_minimal(base_family = font_family) +
theme(
plot.title = element_text(face = "bold")
)
base_family 会统一主题中的标题、坐标轴和图例文字;只有局部需要不同字体时,再在对应的 element_text() 中覆盖。
base graphics
font_family <- "Source Han Sans SC"
old_par <- par(family = font_family)
plot(1:10, main = "Base graphics 字体测试")
par(old_par)
在 Windows 的旧屏幕设备中,有时需要用 windowsFonts() 创建别名;但别名只解决该设备的 family 映射,不能保证另一个导出设备也认识它。
grid
library(grid)
grid.newpage()
grid.text(
"效应值(95% CI)",
gp = gpar(
fontfamily = "Source Han Sans SC",
fontsize = 14
)
)
forestploter、ComplexHeatmap 等 grid 生态包最终也通过 gpar(fontfamily = ...) 或各自主题参数传递字体。
用 ragg 输出高质量位图
位图已经把文字变成像素,不存在查看端缺字体或 PDF 字体嵌入的问题。ragg 可以直接访问系统字体,并提供稳定的抗锯齿、字体回退和旋转文字渲染。
ggsave(
"figure.png",
plot = p,
device = ragg::agg_png,
width = 6,
height = 4,
units = "in",
dpi = 300,
background = "white"
)
直接使用设备时,ragg 的 width 和 height 默认是像素:
ragg::agg_png(
"figure.png",
width = 1800,
height = 1200,
res = 300
)
print(p)
dev.off()
Quarto 或 knitr 中可以把代码块设备设为 ragg_png,避免预览和单独导出使用两套不同的文字渲染路径:
knitr::opts_chunk$set(dev = "ragg_png", dpi = 300)
用 Cairo 输出带字体的 PDF
Base R 的 pdf() 默认不嵌入字体。对使用系统字体、中文或较宽 Unicode 字形范围的图,优先使用 Cairo PDF 设备:
if (!capabilities("cairo")) {
stop("当前 R 构建不支持 Cairo")
}
ggsave(
"figure.pdf",
plot = p,
device = grDevices::cairo_pdf,
width = 6,
height = 4,
units = "in"
)
cairo_pdf() 能使用更广的 UTF-8 字形,并在合适的平台嵌入所用字体。PDF 中的文字仍是文字,通常可以搜索、复制,也更适合交给 Illustrator、LaTeX 或出版流程继续处理。
需要注意:Cairo 支持是 R 构建时的可选能力;透明度、某些字形或设备不支持的效果也可能触发局部栅格化。导出后仍应在目标 PDF 阅读器中检查中文、数学符号和字体属性。
何时使用 showtext
showtext 使用 FreeType 读取字体文件,再把字形交给当前图形设备:矢量设备中通常成为轮廓,位图设备中成为栅格。查看文件的机器不再需要安装字体,也不需要额外调用 Ghostscript。
library(sysfonts)
library(showtext)
font_add(
family = "source-han-sans",
regular = "fonts/SourceHanSansSC-Regular.otf",
bold = "fonts/SourceHanSansSC-Bold.otf"
)
showtext_auto()
p_showtext <- p +
theme(text = element_text(family = "source-han-sans"))
ggsave(
"figure-showtext.pdf",
plot = p_showtext,
width = 6,
height = 4
)
showtext_auto(FALSE)
它适合以下情况:
- 字体只有本地文件,不能或不想安装进系统;
- CJK 字体在当前设备中反复出现缺字或替换;
- 首要目标是跨平台外观一致,而不是保留可搜索文本。
代价也要明确:矢量 PDF/SVG 中的文字会成为图形轮廓,文本选择、搜索和无障碍能力会下降,文件也可能变大。位图输出时还应让 showtext_opts(dpi = ...) 与目标设备分辨率一致。
extrafont 留给旧流程
旧项目常见的路径是:
library(extrafont)
font_import(prompt = FALSE)
loadfonts(device = "pdf")
# 绘图并关闭 pdf() 设备后
embed_fonts("figure.pdf")
这套流程依赖 extrafontdb 保存字体信息,并通过 Ghostscript 嵌入 PDF。已有项目运行稳定时可以继续维护;新项目通常不必先扫描全部系统字体,也不必为了常规 PNG/PDF 输出额外引入 Ghostscript。
如果必须使用传统 pdf(),R 自带的 grDevices::embedFonts() 也可以通过 Ghostscript 后处理。无论使用哪个函数,都要检查最终嵌入的是否真是目标字体,因为 Ghostscript 也可能发生字体替换。
一套可复用的检查顺序
字体问题按下面顺序排查,通常比反复更换包更快:
- 用
system_fonts()确认 family 的准确拼写和所需字重; - 用包含中文、英文、数字和数学符号的测试字符串检查字形覆盖;
- 明确当前使用的是屏幕、
ragg、Cairo、pdf()还是 showtext 设备; - 用最终交付设备导出,不只看 RStudio Plot 窗格;
- 在另一款 PDF 阅读器或另一台机器复查;
- PDF 需要编辑和搜索时保留文字,需要绝对外观一致时再转轮廓;
- 分发字体或嵌入文档前确认许可。
最重要的判断不是“哪个字体包最好”,而是:字体从哪里来、由哪个设备绘制、交付文件是否还要保留真正的文字。