写个 Icon Font Viewer: 终

终于到了 Icon Fontr 最重要的一部分了。这一次要把已经读入为NSBezierPath 的 Icon 导出成 SVG 和图片,以便于在桌面和移动应用或者 UI 设计软件当中使用。

NSBezierPath To SVG Path

首先是将NSBezierPath转换为 SVG 中的 Path 的方法。这一部分实际上是相当容易的。

我们先来了解一下 SVG Path 的基础知识,根据 MDN 上的介绍 对于一个<path></path>标签,由名为d的属性来指定了遗传 一串简写的图形绘制命令。其可用参数可以总结如下:

  • M: Move To,移动光标到某一位置,接受一组 x y 参数
  • L: Line To,从光标位置起始绘制一条直线到目标点,接受一组 x y 参数
  • H, V: L 的变形,在水平和竖直方向绘制,只接受一个参数,分别是 x, y
  • Z: Close Path,闭合曲线。只有将曲线闭合之后才能对 Path 进行颜色填充
  • C: Cubic Curve To,绘制三阶 Bezier 曲线,接受三组 x y 参数,分别是两个控制点和一个结束点的位置
  • Q: Quadratic Curve To,绘制二阶 Bezier 曲线,接受两组 x y 参数,分别是一个控制点和一个结束点的位置
  • A: Arc To, 绘制圆弧,这个命令的参数比较复杂,这里就不赘述了。在一些绘制 SVG 的实现中使用 A 可以 绘制出比较完美的圆弧。但是大多绘制 API (包括 Cocoa Drawing)都是使用 Bezier 曲线来拟合圆弧的

除了以上的绘制命令,SVG 还提供两个用来续接 SVG 曲线的命令,ST,分别对应的是CQ。 接受比CQ少一组 x y 的参数。这两个命令用于在原来的 SVG 上续接 G2 平滑的曲线 (在衔接点一阶导数连续)。

对于所有这些大写字母组成的命令,其参数都是相对于画布的绝对值。此外还分别有一套与之对应的小写字母组成的命令。 接受相同数量的参数,区别在于所有参数的意义都是增量值。此外需要注意的是, 这些命令都省略了第一个控制点。比如说,对于一条三阶 Bezier 曲线,是需要 4 个控制点才能定义出来的, 而命令C只有三组参数,省略的第一个控制点就是画布光标的当前位置(也就是M命令指定的点)。

一个 SVG 绘制曲线的例子如下:

1
2
3
4
<?xml version="1.0" standalone="no"?>
<svg width="190px" height="160px" version="1.1" xmlns="http://www.w3.org/2000/svg">
<path d="M10 80 C 40 10, 65 10, 95 80 S 150 150, 180 80" stroke="black" fill="transparent"/>
</svg>

直接使用内联 SVG 显示出来就是下面的效果(需支持 SVG 的浏览器支持才能显示)。

接下来回到NSBezierPath这边,阅读 NSBezierPath文档 可以发现,其绘制的命令与 SVG 基本上是对应的。 比如说它包含如下一组函数:

1
2
3
4
– (void)moveToPoint:(NSPoint)point;
– (void)lineToPoint:(NSPoint)point
– (void)curveToPoint:(NSPoint)point controlPoint1:(NSPoint)point1 controlPoint2:(NSPoint)point2
– (void)closePath

基本上就对应了 SVG 中的MLCZ了。因此,只要能够将NSBezierPath当中储存的信息, 按照这种命令方式取出一个序列,我们就可以将其转化为 SVG 的绘制命令了。

这当然是一件可以实现的事情咯,我们需要做的就是使用下面两个NSBezierPath的对象方法:

1
2
- (NSInteger)elementCount;
- (NSBezierPathElement)elementAtIndex:(NSInteger)index associatedPoints:(NSPointArray)points

其中第一个方法可以得到NSBezierPath对象包含的元素个数,而第二个方法提供了取出元素及其相关的控制点的方法。 NSBezierPathElement的定义是:

1
2
3
4
5
6
typedef enum {
NSMoveToBezierPathElement,
NSLineToBezierPathElement,
NSCurveToBezierPathElement,
NSClosePathBezierPathElement
} NSBezierPathElement;

看到这里就应该明白了,NSBezierPath包含的元素就是控制的命令,而且这些控制命令是 SVG 的控制命令的子集。 因此我们能很方便的将元素分别映射到MLCZ上。

这里需要注意两个地方:

其一,NSPointArray其实是一个指向NSPoint的指针,在NSBezierPath里,一个元素的控制点最多有三个。 因此可以malloc三倍NSPoint的长度的空间,并将指针传入associatedPoints:中, 最后的控制点数据将会借由指针传出。

其二,SVG 的坐标系统和NSBezierPath的坐标系统是不一样的,对于 SVG 来说,原点在左上角,Y 轴朝下。因此需要进行坐标变换。 关于坐标系统的问题上一篇文章曾经讨论过。

OK,有了这些基础知识,将NSBezierPath曲线转换为 SVG 也就不在话下了。 我的实现代码在这里。 其实有了这些知识,将 SVG 转换为NSBezierPath也基本足够了。要点就在于要把NSBezierPath不支持的一些控制命令, 如QH等转换为原来的CL等。这部分就等以后有机会再详细说明吧。

NSBezierPath To PNG

NSBezierPath转为 PNG 位图输出也很容易,之前在这里 里给出了在 View 中绘制图标的代码。得益于Cocoa Drawing框架的良好设计,我们可以直接复用这些代码来绘制到图片甚至PDF。 绘制到图片的方法有很多。主要包括:

  • 先绘制到 View,再从 View 中获得绘制出的位图图像
  • 绘制到一个 Off Screen 的 GraphicContext 再从 GraphicContext 中抽出图像,好处是不需要在屏幕中显示出来
  • 使用NSBitmapImageRep创建一个NSGraphicContext,再在这个 Context 上进行绘制。 和上一种方法的区别是绘制完成后不需要再手动抽取图像。因为图像已经被直接写入对应NSBitmapImageRep

我们使用最后一种方法来绘制,创建一个NSImageRep的函数参数比较复杂,如下所示:

1
2
3
4
5
6
7
8
9
10
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
pixelsWide:width
pixelsHigh:height
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSCalibratedRGBColorSpace
bytesPerRow:0
bitsPerPixel:32];

接下来详细介绍下这些参数:

  • bitmapDataPlanes:指定图片的色彩通道,这个参数可以理解为事先指定一部分空间用来存储产生的像素数据。 如果指定为NULL,函数会通过其他参数来估计空间以分配内存。
  • pixelsWide, pixelsHigh: 这两个参数指定位图的像素宽和高
  • bitsPerSample: 每个Sample的位宽,这里的Sample可以翻译为位图色彩的采样或者分量,对于我们通常用的RGB 色彩空间,红色分量R就是一个Sample。常用的 RGB 颜色值,每个分量的大小最高是255, 这就代表这样一个Sample是一个 8 bits 的数,因此这里我们把位宽设为 8。 NSBitmapImageRep最高支持 16 bits 的位宽
  • hasAlpha: 是否支持透明度
  • isPlanar: Plane 这个概念和 PhotoShop 中的通道类似。如果这个参数为YES,那么色彩值将会分通道储存。 如果参数为NO,那么同一个像素的颜色分量将会紧挨着存储在一起。
  • colorSpaceName: 颜色空间的名称,这里使用的是 RGB 色彩空间,可选的还有 CMYK 色彩空间、灰度色彩空间等。
  • bytesPerRow: 一行像素需要的空间,这个值可以依据bitsPerSamplepixelsWide来计算出来, 但是如果在实际使用中分配的空间不够,那么超出的部分将会被截断。这里我取了零,让程序自己确定。
  • bitsPerPixel: 这个参数其实也可以不指定,它指的是一个像素的位宽,我们前面指定了每个颜色分量的位宽是8, 每个像素有 4 个分量,那么这个值就应该是32

总体来说,初始化一个NSBitmapImageRep虽然参数很多,但是其中大多是为了保证安全和某些特殊情况而要求指定的。 大概了解这些参数的含义,在必要的时候指定正确的值即可。

创建了NSBitmapImageRep对象之后,可以再从这个对象创建一个NSGraphicContext,并把后者指定为currentContext。 此后就可以像在 View 里一样绘制图形了。

1
2
3
4
5
6
7
[NSGraphicsContext saveGraphicsState];
NSGraphicsContext *g = [NSGraphicsContext graphicsContextWithBitmapImageRep:rep];
[NSGraphicsContext setCurrentContext:g];
// ...
// drawing code here
// ...
[NSGraphicsContext restoreGraphicsState];

如何将绘制完成的NSBitmapImageRep转换为 PNG 文件的格式呢?只需要调用Rep对象的入下方法即可。 这个函数返回的是一个可以直接写入文件的NSData类型的对象。 Cocoa 支持的文件格式,除了 PNG 以外,还有 TIFF、JPG 等等。

1
[rep representationUsingType:NSPNGFileType properties:nil]

至此,图像的输出功能的核心就完成了。

自食狗粮,为 Icon Fontr 设计一个图标

所谓自食狗粮,就是要自己用自己开发的东西来帮助自己开发。具体到这里, 就是要用 IconFontr 导出来的图标为它自己设计一个图标。我从 ionicons 中选取了四个图标,稍加处理,最后得到了下面的样子

Icon

结语

至此,一个完整功能的 Icon Viewer 就完成啦。其实 IconFontr 还有很多可以改进的地方, 比如说可以有一个批量输出 Icon 的功能,下图是我实现出来的样子:

Multiple Resolutions

我还提供了几种预设的分辨率,比如 iOS 的 Tabbar 图标的尺寸等等。 这样就比较方便在 Desktop 和 iOS 应用里使用,也可以用来画原型图等等。

有一些文章(1,2 认为,我们应该尽量使用内联 SVG 而不是 Icon Font 在网页中显示图标。Icon Fontr 也为这种需求提供了方便。 我为其添加了一个复制 SVG 的功能:

Copy SVG

这个系列的文章今天就告一段落了,不过在接下来的时间里,我还会继续维护 Icon Fontr,为其添加各种好用的功能, 各位看官如果有什么需求,欢迎在 Comment 里告诉我。 也欢迎点击这里下载一份目前的测试版本试用,并反馈 Bug。 IconFontr的代码托管在 Github 上,别忘了来这里 Star 一下。