html2canvas.js实现html生成图片

2,144 阅读8分钟

1、需求:

最近做项目PC端经常遇到html转图片的需求,预览时是htmldom,点击下载按钮,将html转换成图片的格式,下载到本地。以下专门整理收纳了相关的点。

2、思路:

html转canvas:

canvas转png:

  • 方案1:基于原生canvas的toDataURL方法将canvas输出为base64格式,通过a标签下载
  • 方案2:使用第三方库Canvas2Image.js,调用其convertToImage方法即可

实际上,Canvas2Image.js也是基于canvas.toDataURL的封装,相比原生的canvas API对于转为图片的功能上考虑更为具体(未压缩的包大小为7.4KB),适合项目使用。

3、代码实现:

   import html2canvas from 'html2canvas';

   // dom元素转为图片
  const handleDomToImg = async () => {
    // 获取dom元素
    const graphImg = document.getElementById('htmlToImgBox');
    // 创建canvas元素
    const canvasdom = document.createElement('canvas');

    // 获取dom宽高
    const w = parseInt(window.getComputedStyle(graphImg).width, 10);
    const h = parseInt(window.getComputedStyle(graphImg).height, 10);
    
    // 设定 canvas 元素属性宽高为 DOM 节点宽高 * 像素比 
    const scaleBy = 2; //也可以用window.devicePixelRatio,
    canvasdom.width = width * scaleBy; 
    canvasdom.height = height * scaleBy;
    
    //scale:2 按比例增加分辨率,将绘制内容放大对应比例
    const canvas = await html2canvas(graphImg, { canvas: canvasdom, scale: scaleBy,useCORS:true });
    
    //将canvas转为base64
    const url = canvas.toDataURL();
    
    //配置下载的文件名
    const fileName = `下载报告${new Date().valueOf()}`;
    downloadFile(url, fileName);
  };

 const downloadFile = (href, fileName = '报告') => {
    const downloadElement = document.createElement('a');
    downloadElement.href = href;
    downloadElement.download = `${fileName}.png`;
    document.body.appendChild(downloadElement);
    downloadElement.click();
    document.body.removeChild(downloadElement);
    window.URL.revokeObjectURL(href);
  };

    <div className={style.preview} id="htmlToImgBox">
          <div className={style['preview-title']}>
            <span>测试学校</span>
            <span>学习笔记完成情况</span>
          </div>
          <div className={style['preview-sub-title']}>
            <span>2022-12-12 18:00:00</span>
            <span>高一年级</span>
          </div>
          <div className={style['preview-content']}>
            <div className={classnames(style['preview-card'], style.blue)}>
              <div>笔记上传数</div>
              <div>
                共计
                <span className={style['preview-num']}>3000</span></div>
            </div>
            <div className={classnames(style['preview-card'], style.orange)}>
              <div>优秀笔记上传数 </div>
              <div>
                共计
                <span className={style['preview-num']}>1002</span></div>
            </div>
            <div className={classnames(style['preview-card'], style.zise)}>
              <div>参与人数</div>
              <div>
                共计
                <span className={style['preview-num']}>3002</span></div>
            </div>
            <div className={classnames(style['preview-card'], style.red)}>
              <div>参与人数占比</div>
              <div>
                <span className={style['preview-num']}>37</span>
                %
              </div>
            </div>
          </div>
          <div className={style['preview-footer']}>
            学习笔记有助于帮助学生养成良好的学习习惯,促进对内容的理解,加强记忆知识点
          </div>
        </div>

4、生成倍图

看了上面代码,有小伙伴是不是对上面canvasdom.width以及html2canvas的scale配置,迷糊了,下面一起看一下不同设置出来的效果对比。这里w=1000px h=1415px

1.canvasdom.width=2*wscale=2

image.png 2. canvasdom.width=2*wscale=1

image.png 3. canvasdom.width=1*wscale=1

image.png 4. canvasdom.width=1*wscale=2

image.png

总结: canvasdom.width影响最终生成的图片的尺寸,scale配置项代表将绘制内容放大对应比例,如果需要生成2倍图,则需要同时将两个地方设置为2。

5、图片清晰度优化

5-1、canvas.width和canvas.style.width

  • canvas.width / canvas.height 表示画布真实大小,我们并不可见
  • canvas.style.width / canvas.style.height 表示画布输出到浏览器我们最终可见的的大小
  • 不提供canvas真实大小时,默认按300*150处理,如果指定canvas.width / canvas.height,没有指定canvas.style,则两者相同
  • canvas绘制的时候先是参考自己本身画布大小进行绘制,绘制完毕,由style指定的大小,渲染在浏览器页面上。

canvas.width和canvas.style.width也是影响图片模糊的关键。举个栗子

 <canvas id="diagonal" style="border:1px solid;width:200px;height:200px;" width="100px" height="100px">
 
 function drawDiagonal(id) {
      var canvas = document.getElementById(id);
      var context = canvas.getContext("2d");
      context.beginPath();
      context.moveTo(0, 0);
      context.lineTo(100, 100);
      context.stroke();
 }

window.onload = function() {
   drawDiagonal("diagonal");
}

image.png

可以看到canvas.style就是在将我们的画布放大2倍,从图也能看到,这条线指向对角没问题,但是变粗了,变模糊了。那为什么这条线仍指向了对角?计算line的终点公式如下。

  • x轴:100(canvas.width)/200(canvas.style.width)=100(line.x)/x(line.实际的x)

计算得出x=200

  • y轴:100(canvas.height)/200(canvas.style.height)=100(line.y)/y(line.实际的y)

计算得出y=200

总结:

canvas的属性widthheight属性放大为2倍(或者设置为devicePixelRatio倍),最后将canvas的CSS样式width和height设置为原先1倍的大小,可以达到提高清晰度的目的

convert2canvas() {
    var shareContent = YourTargetElem; 
    var width = shareContent.offsetWidth; 
    var height = shareContent.offsetHeight; 
    var canvas = document.createElement("canvas"); 
    var scale = 2; 

    canvas.width = width * scale; 
    canvas.height = height * scale; 
    canvas.getContext("2d").scale(scale, scale); 

    var opts = {
        scale: scale, 
        canvas: canvas, 
        logging: true, 
        width: width, 
        height: height 
    };
    html2canvas(shareContent, opts).then(function (canvas) {
        var context = canvas.getContext('2d');
        var img = Canvas2Image.convertToImage(canvas, canvas.width, canvas.height);
        document.body.appendChild(img);
        $(img).css({ 
            "width": canvas.width / 2 + "px", //缩小图片尺寸
            "height": canvas.height / 2 + "px",
        })
    });
}

5-2、window.devicePixelRatio

window.devicePixelRatio返回当前设备的物理像素分辨率与设备独立像素(CSS像素)分辨率的比率。一个CSS像素等于多少个物理像素,对于绘制相同的对象,使用更多的物理像素绘制,就会获得更清晰的图像。 手机屏幕分为:

  1. 非视网膜屏幕(物理像素320 该设备的CSS像素/视区宽度也是320)window.devicePixelRatio=1
  2. 视网膜屏幕(物理像素640 该设备的CSS像素/视区宽度还是320)window.devicePixelRatio=2

<meta name="viewport" content="width=device-width">这个代码的作用就是让视图区域撑满手机物理屏幕。 对于视网膜屏幕,要显示相同的物理大小,视网膜屏幕就要用双倍的物理像素显示,所以在给手机做图的时候,图片要放大一倍。

同样对于<canvas>可能在视网膜屏幕上显得模糊,使用window.devicePixelRatio确定应添加多少额外的像素密度使图像更清晰。

5-3、关闭canvas默认的抗锯齿设置

默认情况下,canvas的抗锯齿是开启的,需要关闭抗锯齿来实现图像的锐化

 var context = canvas.getContext('2d');
//关闭抗锯齿
context.mozImageSmoothingEnabled = false;
context.webkitImageSmoothingEnabled = false;
context.msImageSmoothingEnabled = false;
context.imageSmoothingEnabled = false;

6、 img引入外部图片

由于canvas对于图片资源的同源限制,如果画布中包含跨域的图片资源则会污染画布,造成生成图片样式混乱或者html2canvas方法不执行等问题。

当html2canvas中生成的截图中包含外部图片,那么外部图片在引入的时候就需要设置允许跨域。必要前提是需要服务器设置Access-Control-Allow-Origin: *

  1. 设置配置项 allowTaint: true,允许外部图片污染画布。

canvas 的 CanvasRenderingContext2D 属于浏览器的对象,如果渲染过跨域资源,浏览器就认定 canvas 已经被污染了。该方法不能调用 toBlob(), toDataURL() 或 getImageData() 方法,调用它们会抛出安全错误。

  1. 设置配置项 useCORS: true,允许画布图片跨域。

表示允许跨域资源共享,注意不能与 allowTaint 同时配置为 true. 如果没有开启html2canvasuseCORS配置项,html2canvas会正常执行且不会报错,但是不会输出对应的CDN图片

  1. img 标签中添加 crossOrigin = "anonymous" image.png

理解一下:

  • 加了 crossorigin 属性,则表明图片就一定会按照 CORS 来请求图片。而通过CORS 请求到的图片可以再次被复用到 canvas 上进行绘制。换言之,如果不加 crossorigin 属性的话,那么图片是不能再次被复用到 canvas 上去的。

  • 可以设置的值有 anonymous 以及 use-credentials,2 个 value 的作用都是设置通过 CORS 来请求图片,区别在于 use-credentials 是加了证书的 CORS。

  • 如果默认用户不进行任何设置,那么就不会发起 CORS 请求。但如果设置了除 anonymous 和 use-credentials 以外的其他值,包括空字串在内,默认会当作 anonymous来处理。

  • 通过 'img' 加载的图片,浏览器默认情况下会将其缓存起来。

总结:

  • 过 dom 节点的 'img' 标签来直接访问是没有问题,因为浏览器本身不会有跨域问题。

  • 当我们从 JS 的代码中创建的 'img' 再去访问同一个图片时,浏览器就不会再发起新的请求,而是直接访问缓存的图片。但是由于 JS 中的 'img' 设置了 crossorigin,也就意味着它将要以 CORS 的方式请求,但缓存中的图片显然不是的,所以浏览器直接就拒绝了。连网络请求都没有发起。

  • 在 Chrome 的调试器中,在 network 面板中,我们勾选了 disable cache 选项,验证了问题确实如第 2 点所述,浏览器这时发起了请求并且 JS 的 'img' 也能正常请求到图片。但不能指望用户会这样使用。 解决的办法是让 'img' 标签和 JS 中的访问都走跨域访问的方式,这样既可以解决跨域访问的问题,也可以解决跨域图片在 canvas 中的复用。即在 'img' 中和 JS 中的 'img' 都加上 crossorigin = "anonymous",

7、 截图不全

解决方案:截图之前将页面滚动到顶部

document.body.scrollTop = document.documentElement.scrollTop = 0;
const imgBlobData = await convertToImage(element);

8、不支持的css3 属性

html2canvas 本质读取网页上的目标DOM节点的信息来绘制canvas,所以它并不支持所有的css属性。暂不支持的 CSS 样式属性:

background-blend-mode、background-clip: text、box-decoration-break、repeating-linear-gradient()、font-variant-ligatures、mix-blend-mode、writing-mode、writing-mode、border-image、box-shadow、filter、zoom、transform

踩坑记录:

  1. 加了box-shadow: 0 3.48px 13.94px 0 rgba(0,0,0,0.10);,生成的图片会有阴影,如下图,解决方法:去掉该属性

2022年10月12日11_38_27 样式2.png

2.需求需要生成图片比例为1:1,但是在页面展示的HTML结构为缩小0.75倍的效果。于是我给外层Wrap添加了属性transform:scale(0.75),发现生成的图片字体重叠,且图片绘制内容区域也会缩小0.75倍,如下图

2022年10月18日14_09_56 样式2.png

解决方法:外层Wrap使用属性zoom:0.75,生成图片正常

2022年10月18日14_21_12 样式2.png

题外话:zoom和transform:scale的区别

9、不能保留特效

在图片的转化前,必须停止或者删除动效后才能正确渲染出图片,否则生成的图片是破裂的。