R的中文字符输出:探究以及解决方案

Posted by rogerclarkgc on 周四 22 十二月 2016

问题

在R中画图编程有一段时间了,在遇到输出图片的情景时,总会碰到中文字符无法正常输出的问题,只要图片中有中文字符,那么输出的pdf必然无法正常显示中文,下面来看这个简单的例子:

#R中默认没有显式设定字体的类型
pdf("nonefamily.pdf")
plot(1:10, 1:10, type = "b", main = "测试", 
xlab = "x轴", ylab = "y轴", col = "red")
dev.off()

这个例子输出了一张折线图到pdf文件中,使用R中默认的字体,输出的pdf会变成这个样子:

nonefamily

图片的标题,横纵坐标轴标签的中文都无法显示,实际上,在运行这段代码后,R还给出如下warnings():

> warnings()
警告信息:
1: In title(...) : 'mbcsToSbcs'里转换'娴嬭瘯'出错:<e6>代替了dot
2: In title(...) : 'mbcsToSbcs'里转换'娴嬭瘯'出错:<b5>代替了dot
3: In title(...) : 'mbcsToSbcs'里转换'娴嬭瘯'出错:<8b>代替了dot
4: In title(...) : 'mbcsToSbcs'里转换'娴嬭瘯'出错:<e8>代替了dot
5: In title(...) : 'mbcsToSbcs'里转换'娴嬭瘯'出错:<af>代替了dot

似乎在生成pdf时,R不能正确编码这些中文。

探究

在仔细看了帮助文档后,发现这个很现实的问题似乎在R中埋有很深的机理,我尽量些写下自己的理解。

par()控制输出到屏幕的图形设备

在R中,输出的图像,不论是pdf还是png,都是一种图形设备,当输出到屏幕时,图形设备的参数由par()中的一系列参数控制,而与字体有关的主要是这些参数:

> mypar <- par()
> mypar$family
[1] ""
> mypar$font
[1] 1
> mypar$font.lab
[1] 1
> mypar$font.axis
[1] 1
> mypar$font.main
[1] 2
> mypar$font.sub
[1] 1

这些参数中,family控制图像中的字体族,而font系列则控制字体,font有如下几个取值

  • font = 1正常的字体
  • font = 2加粗的字体
  • font = 3斜体
  • font = 4加粗和斜体
  • font = 5使用Adobe Symbol字体

要改变输出到屏幕的图像的字体比较简单,可以通过下面的函数来自定义自己的字体:

> windowsFonts(caiyun = windowsFont("STCAIYUN"))
> par(family = "caiyun")
> par("family")
[1] "caiyun"

windowsFonts()windowsFont()函数可以在一个字符串和windows字体建立映射,这样设置的字体可以在par()中以映射的字符串中指定。要查看windows字体库,可以在控制面板中的字体选项卡查看。

> plot(1:10, 1:10, main = "测试")

在Rstudio环境下,右下角的图片栏中的显示确实已经改成了华文彩云的字体:

caiyun

如果此时,通过Rstudio中自带的转换pdf的程序,可以输出正常显示的pdf,但是字体不是我们指定的字体,而是宋体:

songti

这个间接说明了R的pdf图形设备参数应该是通过另外的函数控制的。

R中的pdf图形设备

我们一般使用pdf()来打开一个pdf图形设备来作图,在作图完毕后使用dev.off()来关闭当前活动的图形设备,而pdf()的参数列表如下

pdf(file = ifelse(onefile, "Rplots.pdf", "Rplot%03d.pdf"),
width, height, onefile, family, title, fonts, version,
paper, encoding, bg, fg, pointsize, pagecentre, colormodel,
useDingbats, useKerning, fillOddEven, compress)

这几个参数中family是直接控制输出pdf的字体族的,而它的缺省设置是"Helvetica",这个字体族是英文字体族,没有对中文进行编码,所以我们在输出含有中文的pdf时,会出现无法正常显示的情况。

然而要想在pdf中指定自己的字体没有那么简单,R中的pdf图形设备的字体参数之间有一个比较复杂的映射关系,因此在实际操作中很容易让人疑惑不解。

首先,pdf(family=)接收的是一个字符串,这个字符串要求已经与一个字体族(文件)建立了映射关系,前面提到的缺省设置就是一个已经映射好的字符串,它对应的字体族是'sans serif',在R中,系统已经默认设置好了一批映射关系,但是没有几个是对应了中文字符。要查看已建立的映射,可以通过如下方式:

>fontlist <- postscriptFonts()
#或fontlist <- pdfFonts()
> names(fontlist)
[1] "serif"                "sans"                 "mono"                 "AvantGarde"           "Bookman"              "Courier"             
[7] "Helvetica"            "Helvetica-Narrow"     "NewCenturySchoolbook" "Palatino"             "Times"                "URWGothic"           
[13] "URWBookman"           "NimbusMon"            "NimbusSan"            "URWHelvetica"         "NimbusSanCond"        "CenturySch"          
[19] "URWPalladio"          "NimbusRom"            "URWTimes"             "ArialMT"              "ComputerModern"       "ComputerModernItalic"
[25] "Japan1"               "Japan1HeiMin"         "Japan1GothicBBB"      "Japan1Ryumin"         "Korea1"               "Korea1deb"           
[31] "CNS1"                 "GB1"

可以看到R中已经建立32个字体映射,但是只有两种中文字体,都是宋体类型的,其中"CNS1"是繁体中文的字体族,而"GB1"是简体中文的字体族,这两种字体中只有"GB1"能同时兼容简体和繁体。

知道了这一点,在生成pdf文件时,可以在函数中事先声明要采用的字体族,但只能从"GB1"和"CNS1"中选择:

>pdf("cns1.pdf", family = "CNS1")
#或者pdf("gb1.pdf", family = "GB1")
> plot(1:10, 1:10, main = "測試")
> dev.off()

这样生成的pdf能正常显示出中文字符,并且字体也是我们指定的字体。

cns1

但是这还不够,我们的目标是能够自己指定字体,因此还需要更进一步的了解R中的pdf图形设备中字体设置的原理。

利用pdfFonts()或者postscriptFonts()获取的是一个记录了每个字体族信息的列表,它详细记录了这些字体族的映射信息,对于英文的字体族:

> fontlist[1]
$serif
$family
[1] "Times"

$metrics
[1] "Times-Roman.afm"      "Times-Bold.afm"       
"Times-Italic.afm"     "Times-BoldItalic.afm" 
"Symbol.afm"

$encoding
[1] "default"

attr(,"class")
[1] "Type1Font"

这个叫做serif的字体族一共有5个属性来描述,这几个属性的作用如下:

  • name:该字体族映射的名字(字符串),在R中用"serif"代表
  • family:该字体族的名字,是用户自己指定的,并且每个字体唯一对应一个名字
  • metrics:字体族文件的文件名,这些文件都在R_HOME/library/grDevices/afm下,它们都是Adobe字体族。
  • endoding:编码文件,默认是"WinAnsi.enc",编码文件位于R_HOME/library/grDevices/enc目录下。
  • attr(, "class"):这是一个类名表识,表明这是Type1Font类字体

对于非西文的字体族,也有类似的列表存储了映射信息:

> fontlist[29]
$CNS1
$family
[1] "MSungStd-Light-Acro"

$metrics
[1] ""           ""           ""           ""           "Symbol.afm"

$cmap
[1] "B5pc-H"

$cmapEncoding
[1] "CP950"

$pdfresource
[1] "/FontDescriptor\n      <<\n        /Type /FontDescriptor\n        /
CapHeight 662 /Ascent 1071 /Descent -249 /StemV 66\n        /FontBBox 
[-160 -249 1015 1071]\n        /ItalicAngle 0 /Flags 6 /XHeight 400\n    
/Style << /Panose <000001000600000000000000> >>\n      >>\n      
/CIDSystemInfo << /Registry(Adobe) /Ordering(CNS1) /Supplement  0 >>\n      
/DW 1000\n      /W [\n           1 33 500\n           
34 [749 673 679 679 685 671 738 736 333 494 739 696 902 720 
750 674 746 672 627 769 707 777 887 709 616]\n           60 65 500\n           
66 [500 511 502 549 494 356 516 550 321 321 510 317 738 533 535 545 533 
376 443 261 529 742 534 576 439]\n           92 95 500\n           
13648 13742 500\n           17603 [500]\n      ]\n"

attr(,"class")
[1] "CIDFont"
  • family:字体族的名字
  • metrics:中文字体没有西文的斜体,所以默认没有这些映射
  • cmap:对应的中文字体的文件名
  • cmapEncoding:编码方式
  • pdfresource:用于指定pdf中CID字型信息的代码
  • attr(, "class"):表示这个字体为"CIDFont"

可以看到,R中对西文和中文字体处理方式不大相同,英文使用Type1Font类字体,而中文使用CIDFont,并且两种字体的映射关系也有差别,这使得要自定义字体变得比较麻烦,要想手动注册字体比较费时费力,而且容易出错。

要添加自己的英文字体,可以把要加入的.afm字形文件加入到R_HOME/library/grDevices/afm下,然后用Type1Font()函数建立字体映射关系,最后用pdfFonts()或者postscriptFonts()注册新字体

#已经把字体文件放到了R_HOME/library/grDevices/afm下
>utopia <- Type1Font("utopia", 
c("putb8a.afm", "putbi8a.afm", "putr8a.afm", "putri8a.afm"))
> pdfFonts(utopia = utopia)

如果参数都正确,注册的字体能够被pdf()family作为参数接受:

> pdf("utopia.pdf", family = "utopia")
> plot(1:10, main = "test utopia")
> dev.off()
null device 
      1

成功输出pdf,字体也改变了:

utopia

由于目前对中文字体的了解不够多,因此没有找到手动添加中文字体的方法。

R对pdf图形设备的处理

R在生成pdf图形设备时,似乎不是真正的在绘制pdf上的文字,而是采用声明的方式,它只是在pdf中写入了文字的字体、字形信息,如果用户的操作系统没有pdf中声明的字体,而pdf中的一些文字没有被采用系统缺省的字体正确描述,那么就会出现前面的无法正常显示的问题。R的这种较为“偷懒”的处理方式正是导致中文字符无法正常显示的根源。

解决办法

在探究问题原因的过程中,其实已经出现了一些解决方案,这里把我从网上收集到的解决办法和我自己的理解写在一起。

屏幕输出或图片输出

这种问题不涉及到pdf图形设备,因此是最好解决的,我们只需要预先在R中声明当前图形设备的字体,再更改图形设备的绘图参数就可以达到改变字体的目的:

>windowsFonts(lishu = windowsFont("STLITI"))
>plot(1:10, main = "测试", family = "lishu")

lishu

pdf输出

前面已经提到,pdf图形设备比较复杂,因此解决方法也更多,在不使用其他包的情况下,我只能自己指定英文字体,但也不是一步到位:

#已经把字体文件放到了R_HOME/library/grDevices/afm下
>utopia <- Type1Font("utopia", 
c("putb8a.afm", "putbi8a.afm", "putr8a.afm", "putri8a.afm"))
> pdfFonts(utopia = utopia)
> pdf("utopia.pdf", family = "utopia")
> plot(1:10, main = "test utopia")
> dev.off()
null device 
      1

因此,要想更好解决,必须借助别的手段了。

直接生成

这是目前我发现的最简单的方法,输出效果也是最不好的,先把图片输出到屏幕(必须先指定字体),再利用Rstudio中的"save as pdf",就可以获得正常显示的pdf,但是只能使用宋体,排版也不尽人意,而且pdf还是不能被AI正常显示。

利用福昕pdf打印机

这里利用了福昕pdf浏览器的pdf打印功能,同样先输出到屏幕,再进行打印

>dev.new()
>plot(1:10, main = "测试", family = "lishu")

生成的pdf能被pdf浏览器正确打开,也能被AI打开,可能是我的AI缺少字体,文字显示为缺省字体。

利用extrafont包

extrafont包是Winston Chang编写的一个用于解决在R中使用windows字体库的包,根据作者主页上的信息这个包主要做了这几件事情:

  1. 搜索Windows/Fonts下所有字体
  2. 提取字体名字,将字体转换成.afm格式
  3. 将所有.afm字体的信息保存到一张表里
  4. 建立文件Fontmap, 用于将字体嵌入pdf
  5. 在R中注册刚刚建立表格中的所有字体(使用pdfFonts())函数

在Rstudio环境下运行:

>install.packages("extrafont")
>font_import() #导入字体,根据字体多少决定时间,只需要运行一次
>loadfonts() #注册字体,只需运行一次
>fontlist <- pdfFonts()
>length(fontlist)
[1] 425

在完成这几步后,我们已经可以使用新的字体了。

但是,经过测试,我发现extrafont包对中文支持不好,仍然出现无法正常使用字体的问题:

> pdf("youyuan.pdf", family = "YouYuan")
> plot(1:10, main = "test 测试")
There were 50 or more warnings (use warnings() to see the first 50)
> warnings()
警告信息:
1: In axis(side = side, at = at, labels = labels, ...) :
字符0x6d不带字体宽度这样的设置
2: In axis(side = side, at = at, labels = labels, ...) :
字符0x32不带字体宽度这样的设置
3: In axis(side = side, at = at, labels = labels, ...) :
字符0x32不带字体宽度这样的设置
4: In axis(side = side, at = at, labels = labels, ...) : 字符0x4d不带字体度量
#省略部分报警信息
> dev.off()
null device 
      1

youyuan

(无法正常显示,但图片上的字符和AI打开后看到的字符一样,AI能识别这种pdf的字体,但可能编码有误)

估计是extraFont包在转换.afm文件和注册字体中出了问题,因为前面提到R对构造非西文字体映射采用了CIDFont(),而构造映射西文的.afm字体采用Type1Font(), 两者之间的映射关系不相同。

利用showtext包

这个包是由最早的中文R社区“统计之都”的大神yixuan制作的,它能够将R图形设备中的文字符号直接“绘制”到pdf上,因此不需要考虑系统字库问题,因为此时的字体已经相当于一个图像了。 可以在showtext的github主页上看到更详细的信息。

安装好showtext后,使用十分简单:

> font.add("lishu", "SIMLI.TTF")
> pdf("lishu2.pdf")
> showtext.begin()
> plot(1:10, cex = 1.5, col = "red")
> text(5, 5, "测试 隶书", family = "lishu", col = "blue")
> showtext.end()
> dev.off()
RStudioGD 
    2

利用font.add()先添加字体,这个字体是来自windows字库中的字体,第一个参数是自己指定的字体名字,后一个是字体文件名,在打开pdf图形设备后,必须出现showtext.begin()才能正确应用字体,在绘图结束后,必须出现showtext.end(), 这样,经过这几步,可以轻松得到正确字体的pdf文件:

lishuceshi

由于showtext包其实自己定义了pdf图形设备的生成方式,它把所有字符都当做了多边形处理,所以不论什么系统都可以正常显示,也可以在AI中编辑:

inai

后记

写这篇经验总结花费了不少功夫,查找了网上的很多资料,在有一点原创性的基础上拼凑了一番,虽然花了一天半时间研究这些R中不太让人注意的细节,但还是感到收货颇多,再次叹服R的复杂精妙之处,也体会到了R的一点不为人知(少数的)之处吧。

tags: R