domtoimage 是怎么实现截图的

这几天做的东西和截图有关,简单来说就是做一个分享卡片组件,然后把他转换成图片用作分享。
不按常理出牌,我安装的是 domtoimage 这个包,因为 html2canvas 生成的结果经常有错位的问题,本文先看一下 domtoimage 的原理。
toPng
先从 toPng 这个 API 入手:
function toPng(node, options) {
return draw(node, options || {})
.then(function (canvas) {
return canvas.toDataURL();
});
}
toPng 用于将元素保存成图片数据,我知道得把元素绘制在 canvas 上,然后 base64 是 canvas 生成的,但我不知道他具体怎么绘制的,接着找到 draw 函数。
draw
function draw(domNode, options) {
return toSvg(domNode, options)
.then(util.makeImage)
.then(util.delay(100))
.then(function (image) {
var canvas = newCanvas(domNode);
canvas.getContext('2d').drawImage(image, 0, 0);
return canvas;
});
function newCanvas(domNode) {
var canvas = document.createElement('canvas');
canvas.width = options.width || util.width(domNode);
canvas.height = options.height || util.height(domNode);
if (options.bgcolor) {
var ctx = canvas.getContext('2d');
ctx.fillStyle = options.bgcolor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
}
他先做了一个内部的 newCanvas 函数,这个函数就是用来创建 canvas 节点的,util.width/height 一眼用于计算元素尺寸,这里可以稍微看一下实现。
width/height
function width(node) {
var leftBorder = px(node, 'border-left-width');
var rightBorder = px(node, 'border-right-width');
return node.scrollWidth + leftBorder + rightBorder;
}
function height(node) {
var topBorder = px(node, 'border-top-width');
var bottomBorder = px(node, 'border-bottom-width');
return node.scrollHeight + topBorder + bottomBorder;
}
他是拿了元素的 border-width 再去加元素总宽高的,scrollWidth/scrollHeight 是包括了滚动条在内的尺寸。
回到刚刚的代码,他又用到了 toSvg 函数(早知道从 toSvg 开始看了)。
toSvg
function toSvg(node, options) {
options = options || {};
copyOptions(options);
return Promise.resolve(node)
.then(function (node) {
return cloneNode(node, options.filter, true);
})
.then(embedFonts)
.then(inlineImages)
.then(applyOptions)
.then(function (clone) {
return makeSvgDataUri(clone,
options.width || util.width(node),
options.height || util.height(node)
);
});
function applyOptions(clone) {
if (options.bgcolor) clone.style.backgroundColor = options.bgcolor;
if (options.width) clone.style.width = options.width + 'px';
if (options.height) clone.style.height = options.height + 'px';
if (options.style)
Object.keys(options.style).forEach(function (property) {
clone.style[property] = options.style[property];
});
return clone;
}
}
copyOptions 可以稍微跳过,他是用来传递参数的,cloneNode 用于深度复制一个元素和他的 children,就相当于对元素进行预处理吧,比如说:
对于
canvas,用 DataUrl 转换成img来显示遍历复制每一个子元素,根据
filter参数判断要不要保留复制元素的样式,包括伪元素的
对于表单元素,拿到输入的内容
对于
svg,会重新设置命名空间
预处理后,按 embedFonts、inlineImages、applyOptions 的顺序依次调用,embedFonts 的作用是收集页面内所有的字体相关样式,然后做一个内部样式去一次性定义他们。
inlineImages 的作用是找到所有的 img 元素,将他们的 src 转换为 DataUrl 的形式,设置为内联样式。
applyOptions 没什么好说的,把 API 传入的参数和实际的元素属性合并,比如外部设置了 width,那么就优先使用,覆盖元素的 width。
makeSvgDataUri
经过多重处理,接下来就是把节点转换成 svg 数据了,这里调用的是 makeSvgDataUri:
function makeSvgDataUri(node, width, height) {
return Promise.resolve(node)
.then(function (node) {
node.setAttribute('xmlns', '<http://www.w3.org/1999/xhtml>');
return new XMLSerializer().serializeToString(node);
})
.then(util.escapeXhtml)
.then(function (xhtml) {
return '<foreignObject x="0" y="0" width="100%" height="100%">' + xhtml + '</foreignObject>';
})
.then(function (foreignObject) {
return '<svg xmlns="<http://www.w3.org/2000/svg>" width="' + width + '" height="' + height + '">' +
foreignObject + '</svg>';
})
.then(function (svg) {
return 'data:image/svg+xml;charset=utf-8,' + svg;
});
}
先给节点设置上 SVG 的命名空间,这一段是用来指定浏览器的解析方式的,避免不同浏览器上的解析错误。
XMLSerializer().serializeToString 用于将元素序列化成 XML 的形式,然后使用 escapeXhtml 转义(针对 # 和 \\n 字符) ,把会引发错误解析的部分去掉。
接着定义一个 foreignObject 去包裹这段 xml,foreignObject 的用处是在 SVG 文档中渲染非 SVG 的内容,这些内容会被浏览器按照自己的语法、样式规则去处理。
下一步就是用 SVG 元素包裹 foreignObject。
所以将一个元素转换成 SVG,实际上是在 SVG 中开辟一个区域,“在符合 SVG 规范的前提下,渲染非 SVG 的文档”,你看起来里面的内容组成都是 HTML,但实际上它符合 SVG 规范,他就是一个 SVG。
对于 SVG 来说,转换成 dataUrl 就简单了,直接在前面加上 MimeType 等信息就好了。
回到 draw
function draw(domNode, options) {
return toSvg(domNode, options)
.then(util.makeImage)
.then(util.delay(100))
.then(function (image) {
var canvas = newCanvas(domNode);
canvas.getContext('2d').drawImage(image, 0, 0);
return canvas;
});
function newCanvas(domNode) {
var canvas = document.createElement('canvas');
canvas.width = options.width || util.width(domNode);
canvas.height = options.height || util.height(domNode);
if (options.bgcolor) {
var ctx = canvas.getContext('2d');
ctx.fillStyle = options.bgcolor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
return canvas;
}
}
回到 draw 函数,现在知道了 makeSvgDataUri 是如何将元素转换成 SVG dataUrl 的,接下来会通过 util.makeImage 将 dataUrl 转换成 Image 对象。
之后的流程就很清楚了,下一步是创建 Canvas,然后用 Canvas API 将 Image 对象绘制到其中,之后再通过 toDataURL 拿到转换成了指定格式(这里是 PNG)图像的二进制数据(以 base64 呈现),最后交由 API 返回。
总结
所以整体的流程是这样的:
一个元素,通过 toPng 等 API 进行截图
对元素和他的子元素进行遍历,进行预处理
Canvas 转 img
远程资源序列化储存
样式收集,字体收集
…
将元素转换成符合 SVG 标准的形式
将 SVG 以 DataUrl 的形式导入到 Image
Cavans 绘制出
Image通过
toDataURL拿到完整图像




