R画图中文显示问题与showtext包

解决 R 画图输出 PDF 时中文无法正常显示的问题,深挖了一下,最终的思路是使用 cairo_pdf,而showtext 包作为备选方案。

先看 R 中底层画图函数的原理。

pdf()工作原理

R 默认的 pdf() 设备并不真正“绘制”文字,而只是“描述”文字信息(如字符和字体)。这导致文字显示效果依赖系统是否安装了对应字体。

当你用 pdf() 输出时,R 并没有在硬盘上把“中”这个字的线条曲线计算出来并画进文件里。它只是在 PDF 文件中写入了一行代码,大意是:“此处需显示 Unicode 字符 20013,请使用名为 SimSun 的字体来渲染。”’

后果(依赖系统):这份 PDF 文件相当于一张“蓝图”,它把渲染文字的具体工作甩锅给了 PDF 阅读器(如 Adobe Reader)。当别人打开这个 PDF 时,阅读器会去打开的那台电脑的系统中查找有没有 SimSun 字体。

  • → 正常显示。
  • 没有 → 阅读器懵了,只能显示空白或方框(这就是为什么你发给别人的 PDF 中文会乱码)。

showtext 则不同,它利用 FreeType 库将每个字符的轮廓分析并转换成当前图形设备能直接绘制的多边形或位图。因此,它生成的图形是设备无关的。(图形一旦生成,其显示效果不再依赖于后续运行环境的硬件或软件配置

pdf()中的字体(family参数)

pdf() 设备的 family 参数只能使用一个非常有限的、内置的字体列表。

  • 可用字体:仅限 “AvantGarde”, “Bookman”, “Courier”, “Helvetica”, “Helvetica-Narrow”, “NewCenturySchoolbook”, “Palatino” 或 “Times”。这些都是PostScript基础字体,不支持中文
  • 查看方式:使用 names(pdfFonts()) 可以列出当前 R 会话中所有已注册的 PDF 字体名称。
  • 添加字体:理论上可以使用 pdfFonts() 函数添加,但它要求提供 Adobe Font Metrics (.afm) 文件,操作复杂且不常用。
  • 设置错误:如果指定的 family 不在上述“白名单”中,pdf() 不会报错,而是会静默地回退到默认字体 “Helvetica”。这就是为什么即使设置了 family="SimSun",生成的 PDF 依然无法显示中文。

png() 工作原理

pdf() 只写“描述信息”不同,png() 是一个光栅(位图)设备。它的工作流程是:

  • 即时渲染:当你运行 png()plot() 时,R 会立刻调用你电脑操作系统底层的图形引擎(比如 Windows 的 GDI,或 Linux/macOS 的 Cairo 库)。
  • 画成像素:这个底层引擎会去你电脑的字体库里找到对应的字体文件,把“中”这个字渲染成一堆有颜色深浅的像素点(位图),然后把这堆像素点写入 .png 文件。
  • 结果固化:一旦 .png 文件保存完成,它就只是一张由像素组成的图片了。

png() 中的字体(family参数)

png() 设备的行为则与 pdf() 完全不同,它依赖操作系统来渲染文字。

  • 可用字体:理论上,它能使用你操作系统安装的任意字体。在 Windows 上,它使用 GDI 引擎;在 Linux/macOS 上,如果使用 type = "cairo"(貌似这是默认值),则能访问系统的全部字体。
  • 查看方式:最可靠的方法是使用第三方包来列出系统字体(查看方式下面会说)。
  • 添加字体:在操作系统层面安装字体即可,无需在 R 中特别“添加”。
  • 设置错误:如果指定的 family 在系统中不存在,png() 也不会报错,而是会触发操作系统的字体回退机制,自动用一个相近的字体替代。所以有时指定了错误的字体名,图表仍能显示文字,但可能不是你想要的字体。

为什么 PNG 有时能显示中文?

例如代码中写的是 family = "Times"Times 字体并不包含中文字形,那为什么最终能正常显示中文呢?

  • 在生成 PNG 时,R 底层的位图渲染引擎(如 agg 或默认 png 设备)在遇到字体缺失的字形时,会触发操作系统的字体回退机制
  • 系统会“悄悄”地从你电脑里找一个能显示中文的默认字体(比如 Windows 的“微软雅黑”或 macOS 的“PingFang”)替你把中文字画出来。因为最终输出的是像素点阵,只要画面上显示了字,你就看不出来它用了替身。

总结:pdf()png() 的本质区别

特性 默认 pdf() 默认 png()
存储内容 存储文本代码(如 Unicode 编码)和字体名字 存储绘制好的像素点阵(已经是图片了)。
渲染时机 阅读时(你或别人打开 PDF 的那一瞬间)。 生成时(你运行 R 代码保存图片的那一瞬间)。
依赖主体 依赖读者的电脑是否安装对应字体。 依赖作者(你)的电脑是否安装对应字体。
跨设备表现 换个电脑打开,字体可能变方框。 换个电脑打开,图片永远长那样(因为已经是像素了)。

如何查看系统字体

R-systemfonts包

  • systemfonts包systemfonts::system_fonts() 可以返回一个数据框,列出你系统上安装的所有字体。

    用下面命令只返回所有可用字体名称。

    1
    systemfonts::system_fonts()$family

linux系统 fc-list 命令

fc-listfontconfig 库自带的命令行工具,几乎所有现代 Linux 发行版都预装了它。

  • 列出所有字体:直接在终端输入以下命令,即可列出系统上所有已安装的字体及其文件路径。

    1
    fc-list

  • 只查看字体家族名称:输出可能会很长。如果你只关心字体的“家族名称”(即可以在 family 参数中使用的名字),可以用下面这个命令。

    1
    fc-list : family | sort -u

    这个命令的输出会更简洁,例如:

    1
    2
    3
    DejaVu Serif
    DejaVu Sans Mono
    ...
    1
    2
    3
    4
    5
    6
    7
        
    * **按名称搜索特定字体**:如果你知道字体名称的一部分,可以用 `grep` 进行过滤。

    ```bash
    fc-list : family | grep -i "noto" # 查找 Noto 字体
    fc-list : family | grep -i "song" # 查找宋体类
    fc-list : family | grep -i "hei" # 查找黑体类

  • 精确查找某个字体家族:要查找一个特定的字体家族(如 “DejaVu Serif”),可以使用更精确的查询。

    1
    fc-list :family="DejaVu Serif"

查看中文字体

1
2
3
4
5
# 只看中文(lang=zh)
fc-list :lang=zh

# 只看中文,只打家族名(去重)
fc-list :lang=zh family | sort -u

直接查看字体目录

另一种更原始的方法是直接查看系统存放字体文件的目录。

  • 系统字体目录:通常为 /usr/share/fonts/

    1
    ls /usr/share/fonts/

  • 用户字体目录:通常为 ~/.local/share/fonts/

    1
    ls ~/.fonts/
  • 递归查看:字体文件可能存放在这些目录的子文件夹中,可以递归查看。

    1
    find /usr/share/fonts/ -name "*.ttf" -o -name "*.otf"

注意:直接查看目录只能看到文件名(如 Arial.ttf),其对应的、在 family 参数中使用的字体名称可能不同,因此这种方法不如 fc-list 直接。

其他相关命令

  • fc-match:这个命令用于匹配一个字体模式,并告诉你系统会实际使用哪个字体文件。这对于排查“为什么我指定的字体不生效”这类问题非常有用。
    1
    fc-match "Arial"
  • luafindfont:如果你安装了 TeX 系统(如 TeX Live),可能会有 luafindfont 命令,它也能列出系统字体。

安装思源黑体字体

建议安装 Noto Sans CJK SC(思源黑体),这是 Google 和 Adobe 联合发布的目前 Linux 下最完美的开源简体中文字体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 1. 下载 Noto Sans CJK SC(约 30MB)
# 同时下载下载常规和粗体
mkdir -p ~/.local/share/fonts/
cd ~/.local/share/fonts/

wget -O NotoSansCJKsc-Regular.otf \
https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Regular.otf

wget -O NotoSansCJKsc-Bold.otf \
https://github.com/googlefonts/noto-cjk/raw/main/Sans/OTF/SimplifiedChinese/NotoSansCJKsc-Bold.otf

# 2. 刷新字体缓存
fc-cache -fv ~/.local/share/fonts/

# 3. 验证
fc-list :lang=zh family | grep -i "noto"
fc-list | grep "Noto Sans CJK SC" | head

安装后,在 R 里直接使用 family = "Noto Sans CJK SC"

不然我这里只能用 WenQuanYi Micro Hei(文泉驿微米黑)

showtext 优势

其核心优势在于:

  • 字体支持广泛:支持 TrueType、OpenType、Type 1 以及网页字体等多种格式。
  • 输出格式多样:支持 PNG、PDF、SVG 等绝大多数 R 图形输出格式。
  • 设备无关,一劳永逸:它将文本字符转换为多边形(矢量图)或光栅图像(位图)。这意味着图形生成后不再依赖原始字体文件,在任何设备上显示效果都一致。
    • 当设备是矢量设备(pdfsvgcairo_pdf)时,showtext 提取轮廓(矢量路径/多边形)并将其作为矢量对象直接绘制到该设备上。
    • 当设备是光栅设备(pngjpegtiff,或 windows()/quartz() 中的屏幕设备,取决于具体实现方式)时,showtext 在内存中渲染一个光栅位图(使用 FreeType 的着色器),然后将该位图绘制到设备上。
  • 无需外部软件:与 extrafont 包不同,showtext 不依赖 Ghostscript 等外部软件。

基于上面的资料,根据我的理解,showtext 主要还是用于 pdf ,png 本身并不特别需要 showtext 。

showtext 用法

showtext 依赖于 sysfontsshowtextdb 包,安装 showtext 时会自动安装。

基本用法

以下是使用 showtext 的基本流程:

  1. 加载并启用

    1
    2
    library(showtext)
    showtext_auto() # 全局启用
  2. 添加字体

    • 从本地文件添加:使用 font_add(),需要指定字体名称和文件路径。

      1
      2
      3
      4
      # Windows 示例
      font_add(family = "my_song", regular = "C:/Windows/Fonts/simsun.ttc")
      # macOS 示例
      font_add(family = "my_pingfang", regular = "/System/Library/Fonts/PingFang.ttc")
    • 从 Google Fonts 添加:使用 font_add_google(),只需提供字体名称。

      1
      font_add_google("Noto Sans SC", "noto_sc") # 支持中文的思源黑体
  3. 在绘图时使用ggplot2 或基础绘图中,通过 family 参数指定你刚刚添加的字体名称。

    1
    2
    # 在 ggplot2 中
    p + theme(text = element_text(family = "my_song"))

常用函数一览

  • font_add(): 从本地路径加载字体文件。
  • font_add_google(): 从 Google Fonts 加载在线字体。
  • showtext_auto(): 开启全局自动使用 showtext 模式。
  • showtext_begin() / showtext_end(): 在指定代码块内启用/停用 showtext
  • showtext_opts(): 设置 showtext 的选项。

font_add()函数

font_add() 函数来自 sysfonts 包,它的核心作用是为 R 的绘图环境注册并加载新的字体。它本质上是 showtext 等包的“字体后勤官”。

font_add() 的基本用法和参数如下:

1
font_add(family, regular, bold = NULL, italic = NULL, bolditalic = NULL, symbol = NULL)

基本只用到前2个参数,其他就不介绍了。就是你起的字体别名以及字体文件的路径。

参数 类型 是否必需 说明
family 字符型 你为这个字体起的别名。可以是任意字符串,不必是字体的真实名称。之后在R里就靠这个名字来调用它。
regular 字符型 常规字重的字体文件路径。这是必须提供的核心参数。

font_add_google()函数

font_add_google() 函数同样来自 sysfonts 包,它提供了一种极其便利的方式来使用 Google Fonts 项目中的海量开源字体。

与需要提供本地字体文件路径的 font_add() 不同,font_add_google() 会自动从网络下载并注册字体,让你可以轻松使用成千上万种字体。

font_add_google() 的基本用法和参数如下:

1
2
3
font_add_google(name, family = name, regular.wt = 400, bold.wt = 700, 
repo = "http://fonts.gstatic.com/", db_cache = TRUE,
handle = curl::new_handle())
参数 类型 是否必需 说明
name 字符型 在 Google Fonts 官网上查到的字体真实名称,例如 "Gochi Hand"
family 字符型 在 R 中使用的字体别名,可以不与 name 相同。默认与 name 一致。

原理

font_add_google() 的工作流程是全自动的:

  1. 搜索:根据 name 参数在 Google Fonts 仓库中查找对应的字体家族。
  2. 下载:自动下载该字体家族的所有常用样式(regular, bold, italic, bold italic)的字体文件,保存在 R 会话的临时目录中。下载的字体文件会一直保留,直到你关闭当前的 R 会话。每次新会话调用 font_add_google() 都需要联网重新下载。
  3. 注册:将下载的字体注册到 sysfonts 中,并赋予 family 参数指定的别名。

如果你需要永久安装:如果你希望某个 Google Font 能永久使用,无论是否联网,最佳做法是:

  • Google Fonts 官网手动下载字体文件(.ttf.otf)。
  • 将文件安装到你的操作系统字体目录中(Windows 是 C:\Windows\Fonts,macOS 是 ~/Library/Fonts/)。
  • 然后使用 font_add() 并指定本地文件路径来加载它。

showtext_opts()函数

根据官方文档,showtext_opts() 函数用于设置影响 showtext 包绘图外观的全局参数。它目前主要接受两个参数:nsegdpi

参数 作用对象 功能说明 备注
nseg 矢量图形设备 (如 pdf(), svg()) 控制字符轮廓的平滑度。它指定了用多少条线段来逼近字符曲线上的一个片段。 值越大,文字轮廓越平滑,但生成的矢量图文件体积也越大。通常设置在 5 到 20 之间即可获得不错的效果。
dpi 位图及屏幕图形设备 (如 png(), x11()) 设置设备的分辨率。它用于根据点(point)大小确定文字的像素(pixel)大小。 对 pdf() 等矢量设备无效。默认值为 96

另一种方法:修改设备驱动为 cairo_pdf

在不使用 showtext 的情况下,只要将 ggsave 的图形设备换成支持 Cairo 渲染的 PDF 设备,它就能像 PNG 一样利用系统字体回显中文。

修改方式:在 ggsave 中显式指定 device

1
2
3
4
5
6
# 把原来的 ggsave 改成这样(注意 file 扩展名为 .pdf)
myplot <- p + theme(text = element_text(family = "SimSun"))
ggsave(file = "Sites_number.pdf", myplot,
width = 6, height = 4,
device = cairo_pdf, # 关键就在这里
)

字体回退

这个好处是在 pdf 中还是文本对象,可以正常复制。而 showtext 是转变成了矢量化图像,不可选择和复制。另外cairo_pdf() 具备字体回退机制。当指定的 family 不支持中文时,它会尝试自动寻找一个系统中已安装的合适字体来替代。

cairo_pdf() 回退原理:它使用 fontconfig 来查找和管理字体。当指定的字体缺失某个字符(如中文字符)时,fontconfig 会根据其规则和策略,自动从系统已安装的字体中,挑选一个能显示该字符的字体进行替换。这使得 cairo_pdf() 能支持更广泛的 UTF-8 字符。

关键注意事项

虽然 cairo_pdf() 能回退,但在实际使用中有两个重要限制:

  1. 回退依赖系统字体cairo_pdf() 的字体查找仅限系统已安装的字体。如果系统缺乏任何包含中文字形的字体,回退机制将失效。
  2. 回退可能“全局化”:一个常见的副作用是,一旦 fontconfig 为了显示中文而启用了某个中文字体,这个字体可能会被用于渲染图表中的所有文本(包括英文)。这可能导致图表中英文的字体风格与预期不符。

因此,cairo_pdf() 确实能像 png() 一样进行字体回退,甚至更智能。但为了获得最可控、最专业的输出效果,特别是对于有严格字体要求的科研图表,更推荐的做法是主动指定一个支持中文的字体

优势

只要你是用 cairo_pdf() 生成的 PDF,对方无论用 Windows、macOS 还是 Linux,无论有没有安装对应的中文字体,打开后都能正常显示中文

为什么能这么确定?这要从 cairo_pdf() 的核心机制说起。

cairo_pdf() 之所以强大,是因为它默认采用了 “字体子集化嵌入” 技术。这意味着:

  1. 它不只是“引用”字体:默认的 pdf() 只是告诉阅读器“这里要用宋体”,至于宋体长什么样,它不管。
  2. 它把“笔迹”直接写进文件cairo_pdf() 会把你图中实际用到的每一个字(比如“染”“色”“体”这三个字)的矢量轮廓(笔画的几何曲线)提取出来,压缩成一个非常小的“字体子集”,然后直接打包放进 PDF 文件里。

因为 PDF 文件里已经自带了这几个字的“笔迹数据”,所以对方打开 PDF 时,PDF 阅读器会优先使用文件内部的数据来渲染这些文字。它根本不需要去对方的系统里找字体,自然绝对不会变成方框

与默认 pdf() 的终极对比

特性 默认 pdf() cairo_pdf()
存储内容 只存字符编码(如“中”的 Unicode 码) 存字符编码 + 完整的字形轮廓数据
对方无字体时 变方框 正常显示
文件大小 极小(几 KB) 稍大(嵌入了几何数据,通常几十到几百 KB)
文字是否可选/可搜索 ✅ 可以 可以(因为保留了文本编码映射)
跨平台一致性 极差 极好

两种方法怎么选 - 用 cairo_pdf

cairo_pdf() 是科研绘图的“正规军”,showtext 是解决特殊缺字体环境的“特种兵”。正常情况下,请坚定选择 cairo_pdf(),它更专业、更轻量、更符合学术出版规范。

无脑用 cairo_pdf()showtext 可以作为“备胎”,仅在特定极端环境下使用。

为什么这么选?下面我结合科研绘图的硬指标,为你进行深度拆解。

1. 科研图片的“命门”:文字必须可选可搜索

在学术投稿中,PDF 通常用于生成高分辨率预览图或直接用于出版。

  • cairo_pdf() 生成的是“真·文本”:它嵌入字体子集,但文字在 PDF 里依然保留为 “文本对象(Text Object)”。这意味着,编辑或审稿人可以用鼠标选中、复制你图里的“染色体”三个字;文献数据库抓取 PDF 时,也能提取出你的图例文字。这是学术出版最规范的做法。
  • showtext 生成的是“假·文本”(轮廓路径):它将文字全部转成了“贝塞尔曲线(多边形)”。在 PDF 里,这些文字在视觉上没问题,但在软件层面,它们是一堆图形,不可选中、不可复制、不可搜索。如果你的图例被出版社系统 OCR(光学字符识别)或索引,showtext 出来的东西是盲文。

2. 后续转换 PNG:cairo_pdf() 更省心、质量更稳

既然你提到“后续可能会将 PDF 转为 PNG”,那这里有一个很多人踩过的坑:

  • cairo_pdf() + Ghostscript/ImageMagick 转换:因为 PDF 里存的是标准文本和嵌入字体,转换工具(如 pdftoppm)在渲染时,能直接识别出这些文字并以极高的精度栅格化,抗锯齿效果很好。无需额外设置
  • showtext + 转换:因为文字是“多边形”,转换工具需要去渲染复杂的几何路径。这通常没问题,但如果你在 ggsaveshowtext_opts() 中没有正确设置参数,转换时可能会出现文字边缘毛刺、粗细不一的情况。

3. 性能与文件大小

  • cairo_pdf():将字体子集化嵌入,文件体积非常小(通常几十 KB),且生成速度极快。
  • showtext:将成千上万个汉字轮廓拆解成成千上万条 tiny 的 move toline to 路径。如果一张图里标注较多,showtext 生成的 PDF 文件可能从几十 KB 膨胀到 几十 MB,这会严重影响你插入 Word 或 LaTeX 时的编译速度。

🚨 什么情况下才用 showtext

showtext 也有它的“高光时刻”,只在以下两种情况下它才是必选项:

  1. 你想用极其小众的 Google Fonts(如手写艺术字),而系统没装,你又不想折腾系统字体安装。
  2. 你跑在极其精简的 Linux Docker 容器里,连 Cairo 库版本都老掉牙,且无法安装系统中文字体,但你手头有 .ttf 文件可以随代码一起带走。
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2019-2026 Vincere Zhou
  • 访问人数: | 浏览次数:

请我喝杯茶吧~

支付宝
微信