1、需求:
最近做项目PC端经常遇到html转图片的需求,预览时是htmldom,点击下载按钮,将html转换成图片的格式,下载到本地。以下专门整理收纳了相关的点。
2、思路:
html转canvas:
- html2canvas.js可以将htmldom转为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*w
、scale=2
2. canvasdom.width=2*w
、scale=1
3. canvasdom.width=1*w
、scale=1
4. canvasdom.width=1*w
、scale=2
总结:
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");
}
可以看到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
的属性width
和height
属性放大为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像素等于多少个物理像素,对于绘制相同的对象,使用更多的物理像素绘制,就会获得更清晰的图像。 手机屏幕分为:
- 非视网膜屏幕(物理像素320 该设备的CSS像素/视区宽度也是320)window.devicePixelRatio=1
- 视网膜屏幕(物理像素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: *
- 设置配置项
allowTaint: true
,允许外部图片污染画布。
canvas 的 CanvasRenderingContext2D 属于浏览器的对象,如果渲染过跨域资源,浏览器就认定 canvas 已经被污染了。该方法不能调用 toBlob(), toDataURL() 或 getImageData() 方法,调用它们会抛出安全错误。
- 设置配置项
useCORS: true
,允许画布图片跨域。
表示允许跨域资源共享,注意不能与 allowTaint 同时配置为 true. 如果没有开启
html2canvas
的useCORS
配置项,html2canvas
会正常执行且不会报错,但是不会输出对应的CDN图片
- img 标签中添加
crossOrigin = "anonymous"
理解一下:
-
加了 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
踩坑记录:
- 加了
box-shadow: 0 3.48px 13.94px 0 rgba(0,0,0,0.10);
,生成的图片会有阴影,如下图,解决方法:去掉该属性
2.需求需要生成图片比例为1:1,但是在页面展示的HTML结构为缩小0.75倍的效果。于是我给外层Wrap添加了属性transform:scale(0.75)
,发现生成的图片字体重叠,且图片绘制内容区域也会缩小0.75倍,如下图
解决方法:外层Wrap使用属性zoom:0.75
,生成图片正常
9、不能保留特效
在图片的转化前
,必须停止
或者删除动效后才能正确渲染出图片,否则生成的图片是破裂的。