14R 字体与图形输出

从系统字体发现、项目内字体注册和 family 设置,到用 ragg、Cairo 或 showtext 稳定输出 PNG 与 PDF。

2026-02-28
RFontsGraphics Devicessystemfonts
本章目录 · 13

在 R 里指定字体,不只是把 family 填进 theme()。一张图能否稳定显示,取决于三个彼此独立的环节:

  1. 字体可用性:当前机器能否找到目标字体及其字重;
  2. 绘图系统:ggplot2、base graphics 或 grid 是否把正确的 family 交给设备;
  3. 图形设备: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"
)

直接使用设备时,raggwidthheight 默认是像素:

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 也可能发生字体替换。

一套可复用的检查顺序

字体问题按下面顺序排查,通常比反复更换包更快:

  1. system_fonts() 确认 family 的准确拼写和所需字重;
  2. 用包含中文、英文、数字和数学符号的测试字符串检查字形覆盖;
  3. 明确当前使用的是屏幕、ragg、Cairo、pdf() 还是 showtext 设备;
  4. 用最终交付设备导出,不只看 RStudio Plot 窗格;
  5. 在另一款 PDF 阅读器或另一台机器复查;
  6. PDF 需要编辑和搜索时保留文字,需要绝对外观一致时再转轮廓;
  7. 分发字体或嵌入文档前确认许可。

最重要的判断不是“哪个字体包最好”,而是:字体从哪里来、由哪个设备绘制、交付文件是否还要保留真正的文字。

参考