0%

Canvas与Svg互转

Canvas与Svg互转

本文主要以两个库canvgcanvas2svg为例。尝试探寻Svg与Canvas如何实现互转,以及上述两个库的可用性问题。

简单尝试

Svg转Canvas

以一个简单的静态svg图表作为案例,采用canvg转为canvas结果如下图所示。其中最顶部的装饰线条不见了,具体原因我们稍后再分析。

image-20210308162020513

Canvas转Svg

原本的期望为将上述转化完的canvas在由canvas2svg转回svg执行报错Attempted to apply path command to node g。说明canvg在渲染过程中可能存在某些问题导致绘图操作不能被canvas2svg识别。接下来将以canvas2svg的示例demo绘制探究其绘制原理。

image-20210308180255843

小结

SvgCanvas直接基于canvg来转可能会有些错误(另外,直接基于canvgsvg导出可能会遇到svg中引用的class、图片等资源不生效的问题,需要先将class的样式解析成svg标签中的属性,这里不做展开,详情见saveSvgAsPng)。

canvassvg是通过记录canvas绘图操作,解析操作、基于svg渲染器渲染的方式来实现的。在没有canvascontext或者说在没有canvas的操作记录的情况下canvassvg就会退化成位图转矢量图。由于位图和矢量图存储信息方式的不同,几乎不存在不失真的情况话做转换。另外的确存在一些基于图像识别、图像追踪的位图转矢量图方案,详情见[附录](# 附录)。目前未发现基于NPM生态的位图转矢量图方案。

Svg转Canvas具体实现

canvg实现svgcanvas的方案为将svg文件根据标签实别为一个一个单独的元素,每个的元素会有对应的渲染方法。

核心方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
render(ctx: RenderingContext2D) {
// don't render display=none
// don't render visibility=hidden
// 为空直接跳过
if (this.getStyle('display').getString() === 'none'
|| this.getStyle('visibility').getString() === 'hidden'
) {
return;
}

ctx.save();

if (this.getStyle('mask').hasValue()) { // mask
const mask = this.getStyle('mask').getDefinition<MaskElement>();

if (mask) {
this.applyEffects(ctx);
mask.apply(ctx, this);
}
} else
if (this.getStyle('filter').getValue('none') !== 'none') { // filter
const filter = this.getStyle('filter').getDefinition<FilterElement>();

if (filter) {
this.applyEffects(ctx);
filter.apply(ctx, this as unknown as PathElement);
}
} else {
// 写入context
this.setContext(ctx);
// 渲染元素本身及子元素
this.renderChildren(ctx);
// 清空context为下一次渲染做准备
this.clearContext(ctx);
}

ctx.restore();
}

例:绘制圆形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export default class CircleElement extends PathElement {
type = 'circle';
path(ctx: RenderingContext2D) {
// 实别标签中的圆心 半径
const cx = this.getAttribute('cx').getPixels('x');
const cy = this.getAttribute('cy').getPixels('y');
const r = this.getAttribute('r').getPixels();
// 调用canvas绘图api
if (ctx && r > 0) {
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2, false);
ctx.closePath();
}
return new BoundingBox(
cx - r,
cy - r,
cx + r,
cy + r
);
}
getMarkers() {
return null;
}
}

以上,从原理分析canvg根据svg绘制canvas的方案为解析svg标签及样式、根据根据解析结果调用对应的canvas绘制对应的元素。至于上文顶部的装饰线条不见的问题,暂时当作普通bug不做深究。

Canvas转Svg具体实现

canvas2svg实现canvassvg的方案为重写canvasAPI在绘制canvas的同时绘制svg

canvas2svg源码,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* adds a rectangle element
*/
ctx.prototype.fillRect = function (x, y, width, height) {
var rect, parent;
// 调用fillRect的同时创建了rect的svg元素
rect = this.__createElement("rect", {
x : x,
y : y,
width : width,
height : height
}, true);
parent = this.__closestGroupOrSvg();
parent.appendChild(rect);
this.__currentElement = rect;
this.__applyStyleToCurrentElement("fill");
};

结论

实现方案

canvg实现svgcanvas的方案为将svg文件根据标签实别为一个一个单独的元素,每个的元素会有对应的渲染方法。

canvas2svg实现canvassvg的方案为重写canvasAPI在绘制canvas的同时绘制svg

本质上均为从svg标签canvasAPI入手在绘制canvassvg的同时记录操作,做镜像绘制。

互转可行性

由于svg是根据标签自解释的,所以可以实现由svg文件png文件这种操作。但canvas绘制完毕之后会丢失context即丢失操作步骤,而且基于位图的数据存储方式无法通过简单的方式直接转换为svg

上述两库可用性

canvg可用,但仍有bug,canvas2svg未做深入分析,理应可用,需基于canvas context。另:zRender提供类似解决方案,同时对canvasAPI做了一层封装。

附录

adobe illustratorpng to svg演示视频。这种实现方式应该为基于图像追踪的元素实别,会将一些色值相同的块实别成具体的元素。

项目示例:canvas-svg

参考 & 引用

canvas2svg.js:577 Attempted to apply path command to node text · Issue # 52 · gliffy/canvas2svg (github.com)

canvas2svg (gliffy.github.io)

https://elearning.adobe.com/2020/02/convert-your-png-to-svg-images/

https://github.com/canvg/canvg

https://github.com/exupero/saveSvgAsPng

android - Bitmap to SVG Programmatically conversion - Stack Overflow

How to make or convert png to SVG file in Illustrator - Quora