Published on

从 Icon Fonts 到 SVG Icons

Authors

Icon Fonts 的困境

作为一个前端开发,在做项目,尤其是做个人项目的时候,使用icon fonts这件事常常让我感到很挫败。因为通常一个 icon fonts 库无法涵盖项目所有的图标需求,而混用不同的 icon fonts 库会带来进一步的问题:有时候你发现命名空间有冲突,有时候你发现两个库的图标 padding 不一致,从而有一堆修修补补的事情要做。如果项目需要用到彩色的图标,或者要基于图标做一些复杂的动画效果,又要引入 SVG 或者 gif 了。

SVG Icons 方案

要解决这些问题,统一用 SVG icons 是一个可行的办法。当然,用 icon fonts 还是 SVG icons 这个话题太大,不在本文讨论之列。有兴趣的同学可以看看这些文章。

我自己总结了一下,如果不考虑浏览器兼容性的话,SVG icons 从易用性/可维护性/表现力等各方面都比传统的 icon fonts 更有优势。从根本上说,一个是矢量文字(font),一个是表现矢量图形的 XML(SVG),有点降维打击的意思。

不过有个问题,现存的大部分开源图标库都是 icon fonts 的,包括影响力巨大的 FontAwesome 项目。相对而言,SVG icons 方案可用的开源资源并不多。于是一个想法自然而言地诞生:能不能把现有的 icon fonts 直接转换成 SVG icons?如果可以的话,从 icon fonts 升级到 SVG icons 的过程就非常平滑了。

Icon Fonts 到 SVG Icons 的转换

最直接的办法是从 icon fonts 图标库里的SVG font文件(一般的 icon fonts 库都会带的一个 SVG font 文件,譬如 FontAwesome 的fontawesome-webfont.svg。如果没有,也可以很简单地从 ttf 文件转换得到:ttf2svg)入手转换。

得到这个 SVG font 文件之后,转换成可用的 SVG 文件就很简单了。仔细看看这个 SVG font 文件,会发现每个图标就是内部定义的一个 glyph 元素,这个元素内部就是一段 SVG。取出这些 glyph 元素,我们就得到了一堆可独立使用的 SVG 片段。这里只需要注意一点:SVG font 里的 glyph 的坐标系和 SVG 内嵌到 HTML 内时的坐标系是不一样的。glyph 和普通的文字一样,左下角是坐标轴原点,而内嵌的 SVG 则和 Canvas 一样,左上角是坐标轴原点。所以第一步转换后还要在每个 SVG 片段最外层加一个用于坐标转换的<g>节点。

<!-- 原始的glyph元素 -->
<glyph unicode="xxx"><!-- Outline of xxx glyph --></glyph>

<!-- 转换后的SVG片段 -->
<svg xmlns="http://www.w3.org/2000/svg">
  <g transform="scale(1, -1)">
    <!-- Outline of xxx glyph -->
  </g>
</svg>

其中transform="scale(1, -1)"就是负责转换坐标轴的关键。至此,我们已经从一个传统的 icon fonts 图标库里提取出可用的 SVG icon 了,似乎就可以拿这些 SVG icon 合并成SVG sprite直接使用了?

还是不行。首先你会发现简单粗暴的坐标变换(y 轴反转)会导致图标矢量在显示的时候是偏离中心线的,所以作为 inline 图标内嵌到 HTML 里会有问题。为解决这个问题,可以在得到的 SVG 片段上再加一个坐标偏移的 transform。

<svg xmlns="http://www.w3.org/2000/svg">
  <g transform="scale(1, -1) translate(0 -${iconHeight})">
    <!-- Outline of xxx glyph -->
  </g>
</svg>

这里的-${iconHeight}就是矢量图形的高度,对应原本的 SVG fonts 文件中的<font-face>节点的units-per-em值。具体细节上的调整不少,关键点还是在坐标转换上。

解决了这个问题之后基本上和原来使用 icon fonts 的体验差不多了,还顺带解决了命名冲突/表现力等各方面的问题。不过慢慢地你会发现原来 icon fonts 方案的一个致命问题没有解决:如果两个图标分别来自两个不同的图标库,padding 等还是有不统一的问题(怎么同一行同样样式的两个图标看着大小不一样?)!而且现在你手里的 SVG 片段全部都包了一层用来做坐标转换的<g>元素,怎么看怎么别扭。

先解决第二个问题,把这层碍眼的<g>元素干掉。幸好这件事不用太操心,已经有人做掉了。用svgo这个库就可以把这些杂七杂八的坐标转换干掉,还你一个清爽的 SVG 片段。

第一个问题有点棘手。最理想的结果是,我们把所有来自不同 icon fonts 库得到的 SVG 片段都清理一遍,去掉所有的 padding,只留下表示矢量图形的片段,和一个viewBox属性标示这个矢量图形的实际宽高。这样只要给每个 SVG 片段设置同样的widthheight属性,就可以得到统一的视觉效果了。

根据前面的经验,我们只要设置恰当的transform把整个矢量图形移动到其边缘和两条坐标轴相切,剩下的事情就可以交给 svgo 了。关键就在于,我们怎么知道目前矢量图形偏离两个坐标轴多远(top 和 left)?

图标的 SVG 片段我们有了。通过遍历这个 SVG 片段内部的各种图形(Rect, Path, etc)和它们的各种属性,计算出与两个坐标轴的最短距离就能得到 top 和 left 的值。不过这件事相当难,举个例子,如果矢量图形里有个曲线,那计算起来真的是要了命了。另外,要得到正确的结果就一定要遍历所有的情况。而如果依靠穷举来做,最终代码维护一定是个深坑。

那么,最笨的办法是什么呢?创建一个 SVG 文件把这个 SVG 片段写进去,打开浏览器,打开调试控制台,看矢量图形部分的 top 和 left 属性。然后编辑这个文件,transform里加上translate=(-${left}, -${top})。返回浏览器刷新,我们得到了想要的结果。

自动化转换

好了,思路有了。既然浏览器能做,那直接拿一个无头浏览器也可以做,然后就可以脚本化、自动化了。经过试验,PhantomJSElectron都符合要求。一旦祭出这个终极方案,前面很多工作都可以省略了,譬如解释 SVG font 文件,计算坐标转换的各种参数等。

最终从 icon fonts 得到 SVG icons 的整个流程可以描述如下:由 icon fonts 库得到 SVG fonts 文件(可能要转换),然后抽取各个 glyph 片段,翻转坐标系得到 SVG 片段,用无头浏览器把矢量图形对齐到坐标轴,用 svgo 优化输出。

然后就可以享用 SVG icon 了。内嵌到页面/sprite/做动画/加彩色/更好的渲染效果......新世界的大门已经向你打开。

这个事情说起来逻辑还算简单清晰,似乎没有什么特别难的地方。不过说到底,icon fonts 是一套标准,SVG fonts 又是一套标准,SVG symbol/sprites又是不同的标准。在标准的转换之间需要特别严谨,兼容各种开源icon fonts 库,兼容它们背后各种不同的设计风格等等又是另一堆问题。还有个比较尴尬的点:一旦转换的图标数量上来了,性能就成了不得不考虑的问题:每处理一个图标就要开一个无头浏览器进程,每次优化完还要重新所有图标处理一遍。不做任何优化的话,NodeJS 进程是会挂掉的(弱爆了)。

好消息是,我已经把整个流程自动化了。绝大多数的坑也已经填好。还准备了 10k+ 个从开源图标库里转换过来的 SVG icons。如果你还在犹豫要不要从 icon fonts 转为 SVG icons,那么你最后的借口已经没了。项目地址:svg-icon。目前项目在重构当中,欢迎各种 issue 和 pull request。

相关链接