html2canvas适配总结

缘起

近期在使用 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);
});

这里有 nodesToRecovernodesToRemove 两个变量,猜测应该是方便回滚用, 但是并没有回滚的相关代码。

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 …… )


他山之石

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350