缘起
近期在使用 html2canvas
插件生成图片时,发现对于 svg
元素支持不是很好。
而且查阅网友的解决方案时, 发现是一份原文和N份copy, 总体来说解决方案有以下几种:
0、使用 canvg
插件转换 svg 元素
//以下是对svg的处理
var nodesToRecover = [];
var nodesToRemove = [];
var svgElem = $("#divReport").find('svg');//divReport为需要截取成图片的dom的id
svgElem.each(function (index, node) {
var parentNode = node.parentNode;
var svg = node.outerHTML.trim();
var canvas = document.createElement('canvas');
canvg(canvas, svg);
if (node.style.position) {
canvas.style.position += node.style.position;
canvas.style.left += node.style.left;
canvas.style.top += node.style.top;
}
nodesToRecover.push({
parent: parentNode,
child: node
});
parentNode.removeChild(node);
nodesToRemove.push({
parent: parentNode,
child: canvas
});
parentNode.appendChild(canvas);
});
这里有 nodesToRecover
和 nodesToRemove
两个变量,猜测应该是方便回滚用, 但是并没有回滚的相关代码。
1、把 svg 元素转换为图片,然后再转换成 canvas元素
//允许跨域获取,否则百度地图不能生成图片
const opts = {
useCORS: true,
ignoreElements: el => {
const tagName = el.tagName.toLowerCase();
const list = ['head', 'body', 'style', 'title', 'meta']
if(list.includes(tagName)) return false;
// id="extra" 下所有节点忽略
if(el.id === 'extra') return true;
return false;
},
// TODO:: SVG to canvas
onclone(cloneDom) {
const svgElems = $(cloneDom).find('svg');
svgElems.each(function (index, node) {
let parentNode = node.parentNode;
const svg_string = (node.outerHTML || xmlserializer.serializeToString(node)).trim()
const img = new Image();
img.src = 'data:image/svg+xml;charset=utf-8,' + svg_string;
img.crossOrigin = 'anonymous';
img.onload = function(){
const width = parseFloat($(node).css('width'));
const height = parseFloat($(node).css('height'));
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
parentNode.appendChild(canvas).
parentNode.removeChild(node);
}
});
}
}
我发现百度地图上的 svg
元素是有绝对定位和偏移的, 他们的方案不能解决偏移的问题。
性空
后来测试方案0并不成功。 测试方案一,是因为没有追加图片到document 里面,导致没有触发onload方法失败的, 这里进行如下改进:
- 0、把当前页面的svg元素转换为 canvas 元素:
// 把svg转换为canvas
async convertSvg2Canvas() {
const svgElms = document.getElementsByTagName('svg');
// 回调
const callbacks = [];
for(let svg of svgElms) {
const parentElement = svg.parentElement;
const img = new Image();
img.src = `data:image/svg+xml,${encodeURIComponent((new XMLSerializer()).serializeToString(svg))}`;
img.crossOrigin = 'anonymous';
img.onload = async () => {
const width = parseFloat(svg.style.width);
const height = parseFloat(svg.style.height);
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, width, height);
parentElement.append(canvas);
svg.remove();
img.remove();
};
parentElement.append(img);
callbacks.push(img.onload);
}
//await this.axios.all(callbacks);
await Promise.all(callbacks);
},
- 1、等待svg转换完成之后,再使用html2canvas 截图:
async getPreviewImg() {
//显示加载图标
this.$store.dispatch('showDataloader');
await Promise.all([this.loadScript('/static/js/html2canvas.min.js'), this.convertSvg2Canvas()]);
//允许跨域获取,否则百度地图不能生成图片
const opts = {
useCORS: true
}
const canvasObj = await html2canvas(document.getElementById('poster_context'), opts);
var context = canvasObj.getContext('2d');
//防止图片模糊的设置
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;
// png 格式图片
let imgType = "image/png";
this.canvasSrc = canvasObj.toDataURL(imgType);
this.previewShowFlag = true;
//隐藏加载图标
this.$store.dispatch('hideDataloader');
},
……
说明: 截屏的时候,必须等待 svg
元素全部转换成为 canvas
元素才可以,否则是截取不成功的
loadScript
方法用来异步加载js, 本人是全局mixin的, 下附 loadScript 方法:
// 动态加载 js 及 回调
async loadScript(src, callback = null) {
await new Promise(resolve => {
// 如果已经加载了本js,直接调用回调
if (this.checkScriptLoaded(src)) {
return resolve(callback);
}
let scriptNode = document.createElement("script");
scriptNode.setAttribute("type", "text/javascript");
scriptNode.setAttribute("src", src);
document.body.appendChild(scriptNode);
if (scriptNode.readyState) { //IE 判断
scriptNode.onreadystatechange = () => {
if (scriptNode.readyState == "complete" || scriptNode.readyState == 'loaded') {
return resolve(callback);
}
}
} else {
scriptNode.onload = () => resolve(callback);
}
})
},
// 检测是否加载了 js 文件
checkScriptLoaded(src) {
const scriptObjs = Array.from(document.getElementsByTagName('script'));
return scriptObjs.find(ele => ele.src.includes(src));
},
2021-11-19 更新:
H5适配ios系统多行文本时截图错行的问题
近期使用html2canvas插件截图时,发现 iphone手机上,当文本超过一行时,截图后文本排版错乱了(第二行会缩进一个字,而且右边把空白都占满了)
查看CanvasRenderer渲染文本的时候,发现有个 letter-spacing 属性:
CanvasRenderer.prototype.renderTextWithLetterSpacing = function (text, letterSpacing) {
var _this = this;
if (letterSpacing === 0) {
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
}
else {
var letters = toCodePoints(text.text).map(function (i) { return fromCodePoint(i); });
letters.reduce(function (left, letter) {
_this.ctx.fillText(letter, left, text.bounds.top + text.bounds.height);
return left + _this.ctx.measureText(letter).width;
}, text.bounds.left);
}
};
如上,如果没有设置 letter-spacing的样式,则会使用 canvas 的 fillText方法把文本渲染到画布上。(但是fillText 对换行文字排版等支持不够友好), 设置了 letter-spacing属性,会使用measureText 方法把文字渲染到画布,虽然不会溢出box,但是第二行还是会有缩进的问题……
解决方案就是: 尽量避免文本超过两行,当文本超过两行的时候,每行头尾用 行内元素 包一下, 这样就会当做 html元素去渲染到画布,极限的可以把每个字符都用行内元素包一下……
(to be continued …… )