把网页导出为图片的两种方案以及其适用场景

5,281 阅读9分钟
原文链接: zhuanlan.zhihu.com

先列出解决方案给出结论,再开启啰嗦模式:

  1. html网页通过html2canvas插件--->canvas画布通过(toDataURL/toBlob)--->png/pdf/jpg--->通过a标签 download属性下载(方案一:纯前端方案)
  2. 直接用phantom.js 或casper.js 之类的node插件,用headless WebKit 去模拟打开页面,然后导出为图片/pdf,接着上传到OSS,把对应链接返回给前端,实现下载功能。(方案二:后端方案)

结论:当html内容在canvas可绘制能力范围内的,用方案一(版本2:blob方式),当html内容超过了canvas可绘制能力范围,用方案二。

啰嗦模式中,主要描述的是使用方案一在开发过程中遇到的一些问题以及探究引起这些问题的原因,寻找解决方案的过程。

===============啰嗦模式分割线,下面开启啰嗦模式===================

直观地对比两种方案

方案一:只需要一个前端参与 + 引入一个第三方插件就能搞定,而且实现的链路也不长。

方案二:需要前后端协同 + 项目需另起一个node环境 + OSS资源 + 多用户并发问题 不管是从实现复杂度还是成本上,都比较大。

直观对比结果:在理想情况下,显然方案一更优

方案一在开发中遇到的问题

最直观的表现为:当html网页内容过多时,图片下载失败

先贴一下方案一(版本1)的代码

// canvas对象为 html2canvas 插件得到的canvas对象
var base64 = canvas.toDataURL();
var link = document.createElement('a');
link.textContent = 'download image';
link.href = base64;
link.download = "mypainting.jpeg";
link.click()

图片下载失败有两个原因:1. base64图片通过a标签href+download方式下载对图片的尺寸也有上限。2.canvas 画布绘制能力有上限。

随后我抛出了这么几个问题:

  1. base64图片本地实测大于1.6M的为什么下载不下来?具体的下载零界点怎么界定?
  2. 浏览器本地下载大图(大于1.6M,网上查到一些资料说是2M)有什么替代方案?
  3. 影响canvas画布容积上限的因素有哪些?(画布宽?高?面积?颜色复杂程度?)上限具体是多少?

为了回答这几个问题,我写了个小demo(代码详见本文最后): 页面上总共两个元素,一个按钮和一张canvas画布,点下按钮就可以把canvas作为图片导出。我们可以改变canvas的宽高或者颜色复杂度去观察canvas的渲染状态和图片的下载状态。

可以贴下我实验测得的结果【canvas画布采用纯黑色渲染,下载方式是base64(demo中mode的值为base64)】:

  宽*高       canvas渲染状态     base64长度            图片可下载与否  图片尺寸
  1000*32700 canvas可以被渲染    base64长度 511403     图片可下载
  1000*32800 canvas不能被渲染
  2400*32700 canvas可以被渲染    base64长度 1226803    图片可下载     图片尺寸1.4M
  2600*32700 canvas可以被渲染    base64长度 2084390    图片可下载     图片尺寸1.6M
  2700*32700 canvas可以被渲染    base64长度 2152518    图片不可下载  
  8200*32700 canvas可以被渲染    base64长度 6397386    图片不可下载
  8300*32700 canvas不能被渲染

回答问题1

从实验结果可以看到,当base64长度为 2084390 时候,图片可下载,当base64长度为2152518 时候,图片下载失败。先推测一下,这个下载极限就是base64长度为2 X 1024 X 1024 = 2097152。那为什么会下载失败呢?先来看下canvas.toDataURL(),MDN文档上对Data URLs 的定义

Data URLs,即为前缀为 data:scheme 的URL,其允许内容创建者向文档中嵌入小文件。

在data URLs相关的常见问题中能看到

长度限制
虽然 Firefox 支持无限长度的 data URLs,但是标准中并没有规定浏览器必须支持任意长度的 data URIs。比如,Opera 11浏览器限制 URLs 最长为 65535 个字符,这意外着 data URLs 最长为 65529 个字符(如果你使用纯文本 data:, 而不是指定一个 MIME 类型的话,那么 65529 字符长度是编码后的长度,而不是源文件)。

所以data URLs 当初的提出就是为了小文件,不同的浏览器对最长data URLs 有自己的字符限制,chrome下载限度是 base64长度 2M(而非图片大小2M)。

另附chrome 上的一个 issue,在2011年的时候已经有一哥们反应这个问题了,到现在这个issue的status 还是 available。可以去围观一下:bugs.chromium.org/p/chromium/…。没找到chrome官方对data URLs的长度limit申明,暂且也可认为这是一个chrome一个历史悠久的bug。

回答问题2:

从问题1可以知道引发不能下载的原因就是data URL 的长度导致图片不可下载。有两个方法来解决这个问题:

方法1:拿到base64编码后,把它发给后端,让后端转成 www.xxx.com/a.jpg 类似的oss网络图片。该方法可行,但传过长的base64编码需要后台去改一些一个请求大小上限之类的配置,速度体验也非常的慢,不建议用此方法。

方法2:用canvas toBlob() 方式把canvas 转化成 blob对象,再用 url = URL.createObjectURL(blob); 为blob对象生成一个指向blob图片对象URL,url的长度一般就40多(类似blob:null/580f6db8-8a79-43c1-baef-f07e84484492)。实测只要是canvas能渲染出来的,通过这种方式都能下载成功。(实测用canvas画了幅极其复杂的图,就是文章开头的那副五颜六色怪图,通过此方法能导出成功,图片大小为196M。)

方案一(版本2)代码

canvas.toBlob(function(blob) {
            url = URL.createObjectURL(blob);
            var link = document.createElement('a');
            link.textContent = 'download image';
            link.href = url;
            link.download = "mypainting.jpeg";
            link.click()
            // no longer need to read the blob so it's revoked
            URL.revokeObjectURL(url);
});

回答问题3:

canvas画布容积会有一个上限,和宽,高,颜色复杂程度,电脑显卡性能有关

不相关

  • 和面积(宽*高)不太相关。因为例子中 8200*32700 画布可以被渲染,而1000*32800却不可被渲染。
  • 此容积也无法用导出图片的体积来衡量,通过此方法可以导出大小为196M的图片,已经是非常大了依旧可以导出,但在全黑模式下增加canvas宽高,导出的极限尺寸在4.8M。

因为canvas画布容积和画面的颜色复杂程度,电脑显卡性能相关,所以上面测出的宽高并没有代表性。从我的角度,暂时找不到用具体的数值来量化。(如果你有更好的想法,非常欢迎评论交流)。

总之,在方案一图片下载失败的两个原因中,浏览器base64图片下载上限问题可以通过blob方式(方案一版本2)解决,但canvas画布绘制能力上限问题没法通过前端来解决,需采用方案二。

方案二我相信这是图片下载的一个终极实现(虽然实现是麻烦了点儿),没有任何限制,之前做过类似的项目,用casperjs的capture()函数去做这个事情,几百M的pdf导出问题都不大。在现在这个项目中,因为后台是java,所以会采用java的方案去做这个事情。

所以回到开头的那个结论:当html内容在canvas可绘制能力范围内的,用方案一(版本2:blob方式),当html内容超过了canvas可绘制能力范围,用方案二。

最后附上demo代码(其中 mode,canvasWidth,canvasHeight,drawTimes可自定义调节)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>canvas test</title>
</head>
<body>
    <button id="btn">下载canvas</button>
    <canvas id="canvas"></canvas>
</body>
<script type="text/javascript">
/**
 * canvas全黑的状况下,mode = 'base64'
 * 宽*高       canvas渲染状态     base64长度            图片可下载与否  图片尺寸
 * 1000*32700 canvas可以被渲染    base64长度 511403     图片可下载
 * 1000*32800 canvas不能被渲染
 * 2400*32700 canvas可以被渲染    base64长度 1226803    图片可下载     图片尺寸1.4M
 * 2600*32700 canvas可以被渲染    base64长度 2084390    图片可下载     图片尺寸1.6M
 * 2700*32700 canvas可以被渲染    base64长度 2152518    图片不可下载  
 * 8200*32700 canvas可以被渲染    base64长度 6397386    图片不可下载
 * 8300*32700 canvas不能被渲染
 *
 * 结论 
 * 1.canvas 渲染画布有一个上限。相关因素:宽,高(但与宽*高不呈现正相关关系),画面颜色复杂程度
 * 2.相同尺寸的画布,增加颜色会增加图片的尺寸。==》所以上面测试出来的宽高数据没代表性。
 * 3.base64模式下,可支持下载的零界点 在 2*1024*1024 = 2097152 左右
 */
/**
 * 可选值 base64, blob
 * @type {String}
 */
// var mode = 'base64';
var mode = 'blob';
var canvasWidth = 2400
var canvasHeight = 32700;
/**
 * 更改drawTimes 可以增加画面颜色复杂度
 * 可选值 0 ~ 10020000
 */
var drawTimes = 0;
// var drawTimes = 10020000;
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

canvas.width = canvasWidth;
canvas.height = canvasHeight;

ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvasWidth, canvasHeight);
/**
 * [根据drawTimes随机化一些色块,增加canvas颜色复杂度]
 */
for(var i =0;i<drawTimes;i++){
    var colors = ['red','yellow','grey','green','blue','white']
    ctx.fillStyle = colors[i%6];
    ctx.fillRect(i/drawTimes*canvasWidth, Math.random()*canvasHeight, 10, 10);
} 
if (mode === 'base64') {
    document.getElementById("btn").addEventListener("click", function(event) {
        var base64 = canvas.toDataURL();
        console.log(base64.length)
        var link = document.createElement('a');
        link.textContent = 'download image';
        link.href = base64;
        link.download = "mypainting.jpeg";
        link.click()
    }, false);
} else if (mode === 'blob') {
    document.getElementById("btn").addEventListener("click", function(event) {
        canvas.toBlob(function(blob) {
            url = URL.createObjectURL(blob);
            console.log(url)
            console.log(url.length)
            var link = document.createElement('a');
            link.textContent = 'download image';
            link.href = url;
            link.download = "mypainting.jpeg";
            link.click()
            // no longer need to read the blob so it's revoked
            URL.revokeObjectURL(url);
        });
    }, false);
}
</script>

</html>