在电子书中使用SVG

在之前的文章中,我们讨论了《Kindle电子书中该用多大的图片》,以及《详解ImageMagick中SVG的支持》,本文可以看作它们的后续。

在高版本的电子书格式中是可以支持使用.svg作为图片的,并且看起来与.png/.jpg的使用并没有什么不同。但是,如何制作合适大小的.svg却是一个相当烦恼的问题。

因为电子书在.svg文件上采用了与.png/.jpg不一样的排版规则。

一些.svg相关的基础知识

与.pdf一样,事实上.svg文件中也是没有DPI信息的,因为它们都是以矢量格式为基础的。而且它们也都相同的使用Points(磅值/点)来记录图片大小,或称之为页面信息。

一般来说,.svg不支持多页。尽管多页格式已经发布(SVG 1.2),但多数系统并不支持它。

所谓『.svg文件有多大的精度,或多少DPI』其实是个伪命题。因为这其实是不同的处理软件的一个假设/约定——这一点与.pdf是一样的。但不同的是:

  • .pdf约定默认的分辨率是72DPI;而
  • .svg通常约定的是90DPI。

然而即使是同一个软件的不同版本,也可能修改这个约定。例如Inkscape 从v0.92版本开始就默认使用96DPI的约定——而之前它是90DPI(参见这里,以及这里)。

相对好一些的消息是:能够实现SVG处理的引擎并不太多——所以上述的一些不确定的限制条件也就相对来说容易穷举。这些引擎主要包括Cario、Inkscape、Apache Batik和一些浏览器上的Canvas。当然,此前讲过的ImageMagick内置的MSVG也算一个,不过它实在弱到没朋友,不提也罢。很多工具其实是以上述引擎为内核或库的,其中最常用的是Cario,包括rsvg在内的许多库/包都只是Cario上的一个封装。

注1:Cario库也支持PDF,所以Inkscape、Poppler等都使用它来输出PDF和PostScript文档(生成的.pdf文件元信息中Creator也是Cario)。但是Inkscape的SVG渲染引擎是自有的,相关信息可以参考这里

本文中提到的一些工具可以用如下方式安装:

## rsvg, rsvg-convert, and cario ...
> brew install libsvg librsvg libsvg-cairo cairo

## pdf2svg, mactex(pdfcrop), Inkscape, more...
> brew install pdf2svg
> brew cask install xquartz mactex inkscape

# ImageMagick(identify,convert,mogrify...), and pdfinfo or cpdf
> brew install ghostscript imagemagick cpdf poppler

## cairosvg for python
> pip3 install cairosvg

## svginfo
> git clone http://github.com/aimingoo/svginfo
> install svginfo/svginfo /usr/local/bin/

从PDF到SVG

你可能已经习惯使用种种矢量图形工具并用来制作.svg,反正我不是。我通常是用PowerPoint或Keynote来制作它们(尤其是表格)并存储为PDF,然后由.pdf转为.svg。

这个过程看起来麻烦,但确实靠谱。

总的来说,从PDF到SVG的工具主要有两个(Wiki上也是这么说的)。其中最方便、快捷和安全的应该是cityinthesky的pdf2svg,他使用Poppler+Cario;另一个是inkscape,除了在字体处理上表现不稳定之外,它还需要XQuartz的支持(这就比较庞大了)。

pdf2svg可以直接使用brew安装,它的开源项目在这里,或这里。采用相同技术实现的还有PerlVala的版本——因为使用的都是相同的库,因此效果应该没差。其它可选的方案包括JavaC#版的等等,但采用的是各自语言下的引擎,我没有一一试过。

使用pdf2svg的参数如下:

> pdf2svg --help
Usage: pdf2svg <in file.pdf> <out file.svg> [<page no>]

其中<page no>可以使用字符串all

注意pdf2svg并不能指定输出的.svg的大小、DPI等等。其实你使用inkscape来转换也一样没有这类参数——在转换目标格式为.svg时,inkscape仅会多出--export-text-to-path--export-id两个参数可用。此外,inkscape只能处理单页的PDF:

> inkscape -z --export-plain-svg=<out file.svg> <in file.pdf>

最后,pdf2svg与inkscape转换出来的.svg文件的页面大小其实是一样,但是它们使用的单位并不相同。如下:

## inkscape
> svginfo by-inkscape.svg
width: 152.5
height: 77.5
viewBox: 0 0 152.5 77.5

## pdf2svg, and more...
> svginfo by-cario.svg
width: 122pt
height: 62pt
viewBox: 0 0 122 62

注意inkscape转出时使用了缺省单位,这通常是指px;而cario转出时使用的是pt。对于上例来说,原始的pdf文件信息如下:

> pdfinfo my.pdf | grep 'Page size'
Page size:      122 x 62 pts

可见,两种引擎事实上都是直接以该页面大小的pt值作为.svg的宽高,只不过inkscape转换成了相应的px值。

注1:需要强调的是,上述版本的inkscape的采用的是90DPI的.svg设定,而PDF默认为72DPI。所以从.PDF的Pts.SVG的px的转换系数是90/72,即width = 122pt * 90/72 = 152.5px

注2:就我的测试来说,Inkscape在处理PDF -> png的转换时不能正确的识别中文字体,其它类型下的转换我并没有一一试过。

svginfo: 不要相信ImageMagick的显示

如果你使用identify -verbose your.svg来查看svg的信息的话,那么由于ImageMagick可能工作在不同的SVG模式下,所以输出的信息也就不同。——这就是我的上一篇文章为什么叫《详解ImageMagick中SVG的支持》的原因。

我测试了一下上文中提到的4种模式,如下:

## test information of 'identify -verbose your.svg'
===> MODE: internal, use MSVG
  Geometry: 485x306+0+0
  Units: Undefined
===> MODE: build-in, use RSVG/librsvg
  Geometry: 517x327+0+0
  Resolution: 37.8x37.8
  Print size: 13.6772x8.65079
  Units: PixelsPerCentimeter
===> MODE: delegate(internal), use inkscape
  Geometry: 517x327+0+0
  Resolution: 37.8x37.8
  Print size: 13.6772x8.65079
  Units: PixelsPerCentimeter
===> MODE: delegate, use rsvg-convert
  Geometry: 517x327+0+0
  Units: Undefined

这四种模式中,MSVG是唯一一个不转换成图片(因为内置xml解析和svg支持)的方式,也是它与别的模式看起来有差异的原因。此外,由于缺省使用了PixelsPerCentimeter单位,所以Resolution显示是37.8x37.8,换算成英寸则是96DPI。

那么这个your.svg的大小到底是多少呢?很不幸,它真实的数据是388x245 pts

因为svg缺省是工作在90dpi模式下的,所以MSVG用90dpi来换算.svg的大小,就得到了388 * 90/72 = 485px的宽,而其它模式都是先转换成图片,并指定了参数96DPI,这时宽度就成了388 * 96/72 = 517px

所以无论如何(除非你能确切指定使用某一种SVG引擎),对于.svg来说,ImageMagick显示的大小是作不得准的。更好的办法是直接显示.svg中的元信息,例如使用svginfo这个工具(下载在这里):

> svginfo your.svg
width: 388pt
height: 245pt
viewBox: 0 0 388 245
zoomAndPan: magnify
preserveAspectRatio: none

在.svg的元信息中保存的width/height值可能有很多种单位(参见这里,或中文译本,缺省是px,但与具体设备有关)。

电子书并其实不能『很正确』地显示.svg文件

现在我们得到了.svg,并且我们确切地知道它的大小与.pdf的Page Size的Pts值存在90:72的关系(因为默认精度不同)。所以我们只需要控制.pdf的大小,就可以得到合适的.svg文件了。——不考虑.svg的工具的话,在.pdf的工具套件中还有cpdf,还有gs等等可用,很丰富。

但是,真正的一张520pts(或者是520px)大小的.svg,会如何显示在电子书上呢?

答案是:不一定。

如果你在电子书中用<img src="xxx.png">来插入一张图片而没有指定它的宽高,那么几乎所有的排版引擎都能理解这个行为:

  • 将xxx.png图片信息中的宽高信息作为显示宽高;
  • 如果这个宽高大于设备的可见区域,则缩小到合适的大小。

——所以,在电子书中使用图片时只需要考虑图片的精度,其它方面几乎是完美的、自适应的。

然而如果你用同样的方法来插入一个.svg文件,那么由于pt这个单位的存在,所以设备如何显示就成了『谜之问题』了。首先我们要知道,

如果一个.svg是使用px作为单位(也包括没有单位而缺省的),那么将采用与图片一致的规则。

这是一条重要的原则,也成为检测其它.svg单位的关键钥匙。

例如以前面的your.svg为例——它使用了pt作为单位,是388pt。由于.svg是缺省以90DPI为来计算pt值的,所以MSVG还原成px时,显示的是485px的宽。但是到了实际物理设备时,却不一定是以90DPI——这个假想值——来处理的了。例如在Macbook上的iBooks中显示时,iBooks的SVG渲染引擎采用了96DPI的预设值,所以就将显示成388 * 96/72 = 517px;又例如我是要在Kindle Paperwhite3上来显示它,那么由于Kindle使用它的PPI(300DPI)来作为预设值,所以显示出来就应该是惊人的1616px。——好吧,你知道Kindle显示不下了,而.svg是矢量缩放的,所以最终只是按可见区域的全宽来显示它。

电子书的这个处理方案正确不呢?答案是『正确』。因为这其实就是pt作为单位的原始用意。它看起来不是那么『很正确』的原因在于:这样一来,我们就不知道到底该做多大的.svg才能在不同的物理设备上适用了。

那么,最终只剩下了两条规则适用:

  • 在文件中明确使用px作为单位来指定大小的.svg;或者,
  • 在使用<img ...>标签插入.svg时,指定它的宽高信息。

使用px作为单位来指定大小的.svg

使用Inkscape转出的.svg是作用缺省单位px的。但是这个转换是采用.pdf的Points换算过来的,为此你必须将.pdf放大一些,才能在Inkscape中得到你预期的的px值大小。这个换算关系如下:

# 步骤1:将.PDF的大小换算成300DPI下比例(放大300/90倍)
#	- 因为pdf转成svg时还要再放大90/72倍,所以这里不需要直接用300/72作系数
> cpdf -scale-page "3.33333 3.33333" -scale-to-fit-scale 3.33333 -o new_300DPI.pdf my.pdf

# 步骤2:将新文件转换成.svg
> inkscape -z --export-plain-svg=my.svg new_300DPI.pdf

# 查看原始的my.pdf的信息
> pdfinfo my.pdf | grep 'Page size'
Page size:      122 x 62 pts

# 查看中间文件.pdf的大小
> pdfinfo new_300DPI.pdf | grep 'Page size'
Page size:      406.666 x 206.666 pts

# 查看新的.svg的信息
> svginfo my.svg
width: 508.33282
height: 258.33307
viewBox: 0 0 508.33282 258.33307
zoomAndPan: magnify
preserveAspectRatio: none

验证方法:制作一张508 x 258的图片,与my.svg一起插入到电子书中,它们显示起来将是相同大小。

正确插入.svg的宽高信息

我们也可以选择如下的途径(在这个过程中不需要对调整中间文件与.svg的大小):

  • 从.ppt/.key转换到.pdf,再
  • 从.pdf转换到.svg,最后
  • 在电子书中插入.svg的宽度信息。

我们假设以.html作为生成电子书的中间格式(如果你真做过电子书的话,你应该知道我为什么这么假设),那么事实上我们就只需要扫描所有的.svg,通过svginfo来查看信息,经过计算并将正确的宽高信息写到.html就好了。

我们需要知道相应的.svg与最初预设的宽高上限(注意考虑到实际使用,我只预设了x方向上限,并不处理y方向)。我们之前说过:合理的预设是520px。所以,例如480px宽的.svg,那么我们就将<img …>中的width值回写成92.3%

我想,你可能已经发现问题了:这个92.3%应该是指的图片相对于当前设备宽的比例啊,而不是图片自己的宽度值。——是的,我的确是这样考虑的,至于为什么,你得仔细去思考了。

其它(建议综合我最近的几篇文章来理解下述过程):

  • 我们原始设计的宽是520px,在300DPI情况下的物理宽是1.73333inch;
  • 当把上述文档输出成PDF时,物理宽(Print Size)不变;而精度变成了PDF默认的72,所以在转换成Points值时变成了124.8pt,即1.73333inch * 72pt/inch
  • 我们再从.pdf转到.svg时是pt by pt的,所以相应的.svg值是124.8pt;
    • 如果使用Inkscape,则转换出的.svg是width = 156,使用了缺省单位px;
  • 使用identify msvg:your.svg取值最快,但要注意它总是以px为单位显示宽高信息;
    • 在不支持MSVG的ImageMagick的低版本中,建议用svginfo取值并转换单位

相关操作如下(下载standard_image_templet_520.pdf):

# 步骤1:使用hires参数和高的resolution值可以使crop操作更精确
#	- 输出的.pdf的精度与该参数值是无关的
> pdfcrop --hires --resolution 1200 standard_image_templet_520.pdf croped.pdf

# 步骤2:转换pdf到svg
> pdf2svg croped.pdf standard_image_templet_520.svg 1

# 使用pdfinfo查看信息
> pdfinfo croped.pdf | grep 'Page size'
Page size:      124.8 x 73.26 pts

# 使用svginfo查看信息
> svginfo standard_image_templet_520.svg
width: 124.8pt
height: 73.26pt
viewBox: 0 0 124.8 73.26
zoomAndPan: magnify
preserveAspectRatio: none

# 使用identify查看信息(强制使用MSVG引擎)
> identify msvg:standard_image_templet_520.svg | xargs -n1
msvg:standard_image_templet_520.svg=>standard_image_templet_520.svg
MSVG
156x92
156x92+0+0
...

黑科技:最后一点补充

如果:

  • 你确实想让.svg用px作为单位,以避免修改电子书中的width/height信息;并且,
  • 你确实又憎恨Inkscape的巨大安装包和超慢速度,以及它只能处理单页的问题;并且,
  • 你不太担心pdf2svg转换后的.svg文件较大的问题;并且,
  • ……

也许你只是纯粹的点处理爱好者(px fans?),那么好吧,我们讲一种最简单(目前看来也挺安全,但……我不做任何保证)的黑科技。下面仍然以standard_image_templet_520.pdf这个文件为例:

# 步骤1:在300DPI下crop,以提高crop的计算精度
#	- 输出的croped.pdf的精度与该参数值是无关的
> pdfcrop --hires --resolution 300 standard_image_templet_520.pdf croped.pdf

# 步骤1:将.PDF的大小假想成px单位,直接换算成300DPI下的大小
#	- 也就说无脑放大300/72倍就行啦
> cpdf -scale-page "4.166667 4.166667" -scale-to-fit-scale 4.166667 -o scaled.pdf croped.pdf

# 步骤3:使用pdf2svg转换
#	- pdf2svg得到的.svg是使用pt为单位的, 无脑改成px就行啦
#	- pdf2svg可处理多页(会成批生成文件,所以不要用stdout;这里只做单页的示例)
#	- 使用sed时只替换了<svg ...>标签中的width/height,其它的不作处理
> pdf2svg scaled.pdf /dev/stdout 1 |\
	sed -E '1,/^<svg/s/(width|height)="([0-9.]*)pt"/\1="\2px"/g' >\
	standard_image_templet_520.svg

# 查看最终的svg的大小
> svginfo standard_image_templet_520.svg
width: 520.000042px
height: 305.000024px
viewBox: 0 0 520.000042 305.000024
zoomAndPan: magnify
preserveAspectRatio: none

# 使用ImageMagick的MSVG模式查看
> identify msvg:standard_image_templet_520.svg | xargs -n2
msvg:standard_image_templet_520.svg=>standard_image_templet_520.svg MSVG
520x305 520x305+0+0
16-bit sRGB
...

为什么能这么做呢?

首先,我们知道.svg文件中其实并没有精度信息,所有的元素都是基于页面的大小来做换算的。而且,我们在这里用pdf2svg转换出的.svg还有两个特性:

  • 字体已经被转换成矢量信息,因此.svg中不包含字体及字体大小信息;
  • 只有<svg …/>元素包含了页面宽高信息,其它元素是基于该信息的矢量。

所以我们可以安全地将pt值直接替换成px。而这个方法,对于Inkscape生成的.svg就不适用,对比一下上面的两个特性你就明白了。反过来说,正是由于Cario引擎(pdf2svg使用的svg引擎)并不完整支持以px为单位的svg,所以使用该引擎——以及使用该引擎实现的工具来转换上述(修改后的).svg到.png或其它格式的时候也是会出问题;而相对应的,Inkscape却能正确处理——它的引擎支持以px为单位的.svg。

最后,多啰嗦一句。由于pdf2svg转换时将字体转换成了矢量信息,这使得通过这种.svg转换成图片或其它格式时不会因为缺少字体而失真(相对应的,Inkscape就存在字体问题)。但是缺点也是有的,就是.svg文件会比较大一些,可能相应的处理速度也会略慢。