问题
在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会变成这个样子:
图片的标题,横纵坐标轴标签的中文都无法显示,实际上,在运行这段代码后,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环境下,右下角的图片栏中的显示确实已经改成了华文彩云的字体:
如果此时,通过Rstudio中自带的转换pdf的程序,可以输出正常显示的pdf,但是字体不是我们指定的字体,而是宋体:
这个间接说明了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能正常显示出中文字符,并且字体也是我们指定的字体。
但是这还不够,我们的目标是能够自己指定字体,因此还需要更进一步的了解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,字体也改变了:
由于目前对中文字体的了解不够多,因此没有找到手动添加中文字体的方法。
R对pdf图形设备的处理
R在生成pdf图形设备时,似乎不是真正的在绘制pdf上的文字,而是采用声明的方式,它只是在pdf中写入了文字的字体、字形信息,如果用户的操作系统没有pdf中声明的字体,而pdf中的一些文字没有被采用系统缺省的字体正确描述,那么就会出现前面的无法正常显示的问题。R的这种较为“偷懒”的处理方式正是导致中文字符无法正常显示的根源。
解决办法
在探究问题原因的过程中,其实已经出现了一些解决方案,这里把我从网上收集到的解决办法和我自己的理解写在一起。
屏幕输出或图片输出
这种问题不涉及到pdf图形设备,因此是最好解决的,我们只需要预先在R中声明当前图形设备的字体,再更改图形设备的绘图参数就可以达到改变字体的目的:
>windowsFonts(lishu = windowsFont("STLITI"))
>plot(1:10, main = "测试", family = "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字体库的包,根据作者主页上的信息这个包主要做了这几件事情:
- 搜索Windows/Fonts下所有字体
- 提取字体名字,将字体转换成.afm格式
- 将所有.afm字体的信息保存到一张表里
- 建立文件
Fontmap
, 用于将字体嵌入pdf - 在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
(无法正常显示,但图片上的字符和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文件:
由于showtext包其实自己定义了pdf图形设备的生成方式,它把所有字符都当做了多边形处理,所以不论什么系统都可以正常显示,也可以在AI中编辑:
后记
写这篇经验总结花费了不少功夫,查找了网上的很多资料,在有一点原创性的基础上拼凑了一番,虽然花了一天半时间研究这些R中不太让人注意的细节,但还是感到收货颇多,再次叹服R的复杂精妙之处,也体会到了R的一点不为人知(少数的)之处吧。