Cairo 图形设备符号字体的更改



符号字体

在 R 图形中绘制文本时,我们可以指定要使用的字体“系列”,例如通用的系列,如 "sans" 或特定的系列,如 "Helvetica",并且我们可以指定要使用的字体“字形”,例如普通、粗体斜体。R 图形提供了四种标准字体字形,普通、粗体、斜体、粗斜体,以及一种 R 称为“符号”的特殊字体字形。以下代码和输出演示了不同的字体字形。

library(grid)
grid.text(c("plain", "bold", "italic", "bold-italic", "symbol"), 
          y=5:1/6, gp=gpar(fontface=1:5))

前四种字体字形只是当前字体系列的变化,默认情况下是无衬线字体,但符号字体字形实际上是完全独立的字体。

从历史上看,符号字体字形一直作为访问希腊字母和一组数学符号的一种方式。例如,字体字形 5 中的字符“m”是希腊字母“mu”。

grid.text("m", gp=gpar(fontface=5))

此功能不如以前有用,因为随着 Unicode 和涵盖非常广泛字符范围的字体的出现,我们现在可以使用标准字体访问特殊符号,如下所示(注意以下代码中缺少 fontface,但还要注意,生成的 mu 与上面的字体不同)。

grid.text("\u03BC")

但是,符号字体在 R 中仍然有用,因为它用于“plotmath”工具中,用于绘制数学方程式,如下例所示。

grid.text(expression(paste(frac(1, sigma*sqrt(2*pi)), " ",
                           plain(e)^{frac(-(x-mu)^2, 2*sigma^2)})))

选择备用符号字体

在某些图形设备上,可以选择备用符号字体。例如,在 pdf() 设备上,我们可以使用函数 Type1Font() 定义新的字体系列,包括新的符号字体。以下代码和输出显示了 pdf() 设备的默认 "sans" 字体定义(在 Linux 上;注意输出的 metrics 组件中的 "Symbol.afm" 值)。

pdfFonts("sans")
## $sans
## $family
## [1] "Helvetica"
## 
## $metrics
## [1] "Helvetica.afm"            
## [2] "Helvetica-Bold.afm"       
## [3] "Helvetica-Oblique.afm"    
## [4] "Helvetica-BoldOblique.afm"
## [5] "Symbol.afm"               
## 
## $encoding
## [1] "default"
## 
## attr(,"class")
## [1] "Type1Font"

下一个代码定义了一个新字体,它使用相同的字体(Helvetica),但为符号字体选择了 Computer Modern (TeX) 字体。

CMitalic <- Type1Font("ComputerModern2",
                      c("Helvetica.afm", "Helvetica-Bold.afm",   
                        "Helvetica-Oblique.afm", "Helvetica-BoldOblique.afm",
                        "./cairo-symbolfamily-files/cmsyase.afm"))

我们可以使用该新字体及其新的符号字体来生成与之前相同的数学方程式,但为符号使用不同的字体。

pdf("cairo-symbolfamily-files/CMitalic.pdf", family=CMitalic, height=1)
grid.text(expression(paste(frac(1, sigma*sqrt(2*pi)), " ",
                           plain(e)^{frac(-(x-mu)^2, 2*sigma^2)})))
dev.off()
embedFonts("cairo-symbolfamily-files/CMitalic.pdf", 
           outfile="cairo-symbolfamily-files/CMitalic-embedded.pdf", 
           fontpaths=file.path(getwd(), "cairo-symbolfamily-files"))

Cairo 图形设备

R 有几个基于 Cairo 图形系统的图形设备,例如 png(type="cairo")cairo_pdf()。这些设备的一个好处是,指定用于绘制文本的字体非常容易。我们所要做的就是给出字体的名称,Cairo 图形会执行所有工作,将该字体名称映射到我们系统上的字体。不像在 pdf() 设备上那样,不需要设置 Type 1 字体定义。

例如,如果我们的系统上安装了名为“Linux Biolinum Keyboard O”的字体,我们可以在绘制文本时直接使用该字体名称。

grid.text(c("plain", "bold", "italic", "bold-italic", "symbol"), 
          y=5:1/6, 
          gp=gpar(fontface=1:5, 
                  fontfamily="Linux Biolinum Keyboard O"))

然而,在上面的输出中,我们可以看到符号字体看起来与第一个示例中的符号字体完全相同。这是因为它完全是相同的符号字体,问题在于或曾经在于,在 Cairo Graphics 设备上,用户无法或曾经无法更改该默认符号字体。

Fedora 31 来拯救 ?

Cairo Graphics 设备上的这种不便(无法选择备用符号字体)在(Linux 发行版)Fedora 31 发布后发生了更戏剧性的转变。

Fedora 31 更新了其 Cairo Graphics 系统,不再支持 Type 1 字体,这种改变对 R 中的 plotmath 输出产生了不利影响。

(从现在开始的示例要么在 Ubuntu 16.04 系统上,要么在 Fedora 31 系统上;两个系统都是使用来自 R-Hub 项目的 Docker 镜像创建的。Docker 镜像 pmur002/ubuntu-gcc-develpmur002/fedora-gcc-devel-problempmur002/fedora-gcc-devel-solution 可从 DockerHub 获得。)

以下输出显示了 R 从符号字体中使用的全部符号。它在 Ubuntu 16.04 系统(较旧的 Linux 发行版)上运行,并显示预期结果。

##  [1] "Ubuntu 16.04.6 LTS"

下一个输出显示了这组符号在 Fedora 31 系统上的样子。这显然是一个较差的结果。

##  [1] "Fedora 31 (Container Image)"

问题的本质在于,在 Cairo Graphics 设备上,符号字体被硬编码为字体名称“symbol”。在两个 Linux 系统上,这都会导致 Type 1 字体(如下面的 Ubuntu 输出中文件名上的 .pfb 后缀和 Fedora 输出中文件名上的 .t1 后缀所示)。

##  [1] "Ubuntu 16.04.6 LTS"
##  s050000l.pfb: "Standard Symbols L" "Regular"
##  [1] "Fedora 31 (Container Image)"
##  StandardSymbolsPS.t1: "Standard Symbols PS" "Regular"

Fedora 31 上缺少对 Type 1 字体的支持在上述绘图中的所有缺失符号中显而易见。

Cairo Graphics 设备上的新 symbolfamily 参数

解决 Fedora 31 问题的第一个步骤是允许用户在 Cairo Graphics 设备上选择备用符号字体。这意味着在 R 4.0.0 中,以下函数都接受新的 symbolfamily 参数:x11()png()jpeg()tiff()bmp()svg()cairo_pdf()cairo_ps()

与这些函数的 family 参数一样,symbolfamily 参数可以只是已安装字体的名称,Cairo 会处理其余部分。例如,以下代码创建一个 Cairo Graphics png() 设备,其中 "NimbusSans" 作为符号字体,这在 Fedora 31 上产生了更好的结果。

png(type="cairo", symbolfamily="NimbusSans")
##  [1] "Fedora 31 (Container Image)"

以下输出显示了这种方法效果更好的原因,因为 "NimbusSans" 字体规范解析为 OpenType(TrueType)字体(如下面的 .otf 后缀所示)。

##  [1] "Fedora 31 (Container Image)"
##  NimbusSans-Regular.otf: "Nimbus Sans" "Regular"

Cairo Graphics 设备的新 cairoSymbolFont() 函数

上面显示的 "NimbusSans" 结果(对于 Fedora 31)仍然缺少一些符号。这揭示了 R 如何在 Cairo Graphics 设备上生成 plotmath 输出的另一个特点。

在内部,plotmath 使用(单字节)Adobe 符号编码(ASE);每个希腊字符或数学符号对应于 0 到 255 之间的一个数字(实际上,只使用了 32 到 254,并且该范围内还有许多未使用的数字)。Cairo Graphics 设备采用(多字节)UTF-8 编码接受 Unicode 文本,因此 R 必须将 32 到 254 之间的数字转换为 Unicode 代码点。例如,ASE 中的数字 34 是 /universal 或“全称”符号,它被映射到代码点 U+2200。

R 使用 来自 Unicode 联盟的转换表 来执行转换,但这包括一些转换为 Unicode 代码点的转换,这些代码点位于称为专用使用区域 (PUA) 的范围内。例如,ASE 中的数字 230 是 /parenlefttp 或“左括号顶部”符号,它被映射到代码点 U+F8EB。

PUA 中代码点的问题在于它们是私有的(!)——它们没有得到普遍认可——这意味着即使是试图涵盖广泛 Unicode 的字体通常也不会实现它们。这就是 "NimbusSans" 结果中缺少符号的原因。

R 4.0.0 中有一个新的 cairoSymbolFont() 函数,它通过允许用户指定符号字体不使用 PUA 来解决此问题。在这种情况下,Cairo Graphics 设备将使用 ASE 到 Unicode 的替代映射,该映射不使用 PUA。例如,使用替代映射,ASE 中的数字 230 映射到 U+239B(左括号上钩)。

以下代码演示了如何使用此函数。我们再次指定符号字体为 "NimbusSans",但我们还指定字体不使用 PUA。生成的符号表现在已完成。

png(type="cairo", symbolfamily=cairoSymbolFont("NimbusSans", usePUA=FALSE))
##  [1] "Fedora 31 (Container Image)"

grSoftVersion() 输出中的其他组件

解决 Fedora 31 问题的最后一步是确保 Cairo Graphics 设备的默认 symbolfamily 设置适合不同的 Linux 发行版(和其他平台)。例如,为了向后兼容性,Ubuntu 16.04 上的默认 symbolfamily 仍然是 "symbol",但在 Fedora 31 上,默认值变为 cairoSymbolFont("sans", usePUA=FALSE)

为了帮助设置这些默认值,grSoftVersion() 返回的值在 R 4.0.0 中有两个新组件:"cairoFT""pango"。如果 Cairo 没有使用 Pango,则后者为 "",否则它就是正在使用的 Pango 版本(作为字符值)。如果 Cairo 使用 FreeType(加上 FontConfig),则前者为 "yes",否则为 ""

##  [1] "Ubuntu 16.04.6 LTS"
##                     cairo                  cairoFT                    pango 
##                 "1.14.6"                       ""                 "1.38.1" 
##                   libpng                     jpeg                  libtiff 
##                 "1.2.54"                    "8.0" "LIBTIFF, Version 4.0.6"
##  [1] "Fedora 31 (Container Image)"
##                      cairo                   cairoFT                     pango 
##                  "1.16.0"                        ""                  "1.44.7" 
##                    libpng                      jpeg                   libtiff 
##                  "1.6.37"                     "6.2" "LIBTIFF, Version 4.0.10"

"1.44" 或更高版本的 Pango 版本会触发对 cairoSymbolFont("sans", usePUA=FALSE) 的更改。

替代符号字体

尽管上面的符号表是完整的,但提供的符号来自 Nimbus Sans 字体,因此与该字体的样式一致。新的 symbolfamily 参数允许我们探索其他选项。例如,在 Fedora 上,我们可以选择使用 OpenSymbol 字体,如下所示。

dnf install libreoffice-opensymbol-fonts
png(type="cairo", symbolfamily=cairoSymbolFont("OpenSymbol", usePUA=FALSE))
##  [1] "Fedora 31 (Container Image)"

Windows 和 macOS

Cairo Graphics 设备在 Windows 和 macOS 上也可用,并且 symbolfamily 参数和 cairoSymbolFont() 函数也在这些平台上可用,尽管默认的 symbolfamily 可能不同。

Windows 上的单字节区域设置提出了一个特殊情况,因为 R 不会将 ASE 转换为 UTF-8,而是假装 ASE 数字采用 Latin1 编码,并从 Latin1 转换为 UTF-8。此转换适用于默认的 "Symbol" 字体,但不适用于大多数其他字体。在这种情况下,如果 symbolfamily 不是 "Symbol",Cairo Graphics 设备会切换回正常的 ASE 到 UTF-8 转换(使用或不使用 PUA)。

已知在这些平台上提供合理覆盖范围的替代符号字体包括:macOS 上的 "Apply Symbols" 和 Windows 上的 "Cambria Math"(两者都使用 usePUA=FALSE)。

R API 更改

Cairo Graphics 设备从图形引擎接收 UTF-8 文本,但如上所述,该文本可能需要进一步转换,例如,为了避免 Unicode PUA。这些转换发生在 C 代码中,并由 R API 中的函数提供,以便其他图形设备可以使用它们。例如,“Cairo”包一直允许用户从 R 4.0.0 中选择符号字体,现在还将提供不使用 PUA 的选项。

一个现有函数已被修改
Rf_AdobeSymbol2utf8(),有一个额外的 Rboolean usePUA 参数来控制是否使用 Unicode PUA。

已添加三个新函数
Rf_utf8toAdobeSymbol() 从 UTF-8 转换为 ASE,假设 UTF-8 是使用 PUA 生成的。
Rf_utf8Toutf8NoPUA() 从带 PUA 的 UTF-8 转换为不带 PUA 的 UTF-8。
Rf_utf8ToLatin1AdobeSymbol2utf8() 从被视为 Latin1 的 ASE 生成的 UTF-8 转换为 UTF-8(使用或不使用 PUA)。

可重复性

重建此博客所需的所有材料均可在 github 上找到。

致谢

感谢 Gavin Simpson 提交的原始错误报告,感谢 Iñaki Ucar 和 Nicolas Mailhot 协助诊断问题并设计解决方案,感谢 Brian Ripley、Simon Urbanek 和 Gabriel Becker 协助测试新功能。