背景
项目要求编辑配置相关信息后,点击表格行间按钮,直接生成二维码并下载带有二维码名称的高清图片。类似下图
需要考虑以下几点:
- 根据二维码内容生成二维码
- 设置二维码图和二维码名称居中对齐
- 截取二维码图和二维码名称区域,设置合适的边缘空白
- 下载图片
前置知识
webkitBackingStorePixelRatio 和 devicePixelRatio
webkitBackingStorePixelRatio: canvas context 相关属性,仅safari和chrome,该属性的值决定了浏览器在渲染canvas之前会用几个像素来来存储画布信息。
devicePixelRatio:设备像素比,是一个物理概念,返回当前显示设备的物理像素分辨率与CSS 像素分辨率之比
浏览器绘制 Canvas 渲染到屏幕中分两个过程:
- 绘制过程:webkitBackingStorePixelRatio
webkitBackingStorePixelRatio表示浏览器在绘制 Canvas 到缓存区时的绘制比例,若图片宽高为200px,webkitBackingStorePixelRatio为 2,那么 Canvas 绘制这个图片到缓存区时,宽高就为400px - 渲染过程:devicePixelRatio Canvas 显示到屏幕中还需要渲染过程,渲染过程根据
devicePixelRatio参数将缓存区中的 Canvas 进行缩放渲染到屏幕中
举例说明, 在大部分高清屏中,例如 Macbook Pro 中:
webkitBackingStorePixelRatio = 1
devicePixelRatio = 2
将一个 200px * 200px 的图片 Cavnas 绘制到该屏幕中的流程:
webkitBackingStorePixelRatio = 1,绘制到缓存区的大小也为:200px * 200pxdevicePixelRatio = 2200px * 200px的图片对应到屏幕像素为400px * 400px,devicePixelRatio = 2浏览器就把缓存区的200px * 200px宽高分别放大两倍渲染到屏幕中,所以就导致模糊
实际开发中可暂不考虑 canvas 的 webkitBackingStorePixelRatio
生成二维码
链接: qrcode-vue
适配于 vue2,支持属性较少,但也够用了,主打轻量级,用法如下,github readme 可查看所有属性配置,此处不再赘述。
<qrcode-vue
:size="size"
:value="value"
:logo="logo"
:bgColor="bgColor"
:fgColor="fgColor"
/>
截取 dom 生成图片
链接: html2canvas
需要传入截图所需的 dom 位置,然后支持一些属性配置,详细配置请查看 html2canvas 配置,用法如下:
function generateImg() {
const element = document.getElementById('qrcode-img');
const canvasContent = await html2canvas(element, options);
}
问题解决
实际开发过程中遇到了几个问题,在此记录一下,后文也会依次介绍解决方法。
- 生成图片需要实际绘制到 dom ,可以使用 CSS 将其放到不可见视图内
- 二维码尺寸、清晰度也和浏览器分辨率关联,如不控制,改变分辨率,会导致二维码和文字样式错乱
- 生成图片清晰度和浏览器分辨率有关,如果不额外配置,改变浏览器分辨率,都会影响图片清晰度
- canvas 图片下载
将图片放到不可见范围
使用 css 就可以实现,此处介绍一种方式:
#qrcode-img {
position: absolute;
padding: 40px 80px;
top: -100vh;
right: -100vw;
z-index: -99999;
}
二维码尺寸不能固定
在使用 qrcode-vue 时,传入属性 size 原本是打算控制二维码的宽高,实际却发现,分辨率变化时,用来生成二维码的canvas尺寸根本是不固定的。
<qrcode-vue size="120" value="testtest" />
我使用的设备是 MacBook Pro 3-inch, 2020款,浏览器是 chrome,当浏览器缩放是 100% 时,打印得知 devicePixelRatio 为2:
当浏览器缩放是75%时,打印得知 devicePixelRatio 为1.5:
可以发现生成的二维码根本不是按照我预想的那样,宽高都是120,而是跟随浏览器分辨率变化而变化。
查看 qrcode-vue 源码发现,二维码的尺寸是根据 scale 动态计算的,相关源码如下,原来 qrcode-vue 为了保证图片的清晰度,根据 webkitBackingStorePixelRatio 和 devicePixelRatio 做了处理,改变用于生成图片的 canvas 尺寸
const canvas = this.$refs.canvas
const ctx = canvas.getContext('2d')
const size = this.size / qrcode.moduleCount
const scale = window.devicePixelRatio / this.getPixelRatio(ctx)
canvas.height = canvas.width = this.size * scale
// getPixelRatio (ctx) {
// return ctx.webkitBackingStorePixelRatio || ctx.backingStorePixelRatio || 1
// }
看过源码以后,要控制图片的宽高每次都是固定的,方法就很明确了,size 也根据 scale 动态计算就可以了
const scale = window.devicePixelRatio / this.getPixelRatio(ctx)
this.qrCodeImgConfig = {
qrUrlContent: 'testtest',
qrName: '测试二维码',
size: 400 / scale,
};
<div id="qrcode-img">
<qrcode-vue :size="qrCodeImgConfig.size" :value="qrCodeImgConfig.qrUrlContent" />
<div class="qr-name">{{ qrCodeImgConfig.qrName }}</div>
</div>
生成图片清晰度
将二维码和名称的 dom 生成截图,使用的是 html2canvas 插件,配置的 options 默认 scale 是 window.devicePixelRatio,也会出现问题 2 描述的放大问题,应该将其变为 1,这样截图尺寸就不会随着浏览器分辨率变化了,但是注意浏览器分辨率调的过低时,仍然会出现清晰度过低问题。
某些特殊场景,可能需要给一个 canvas 容器承载截图,此处也给出示例。
async generateImg() {
try {
// 要截图的 dom
const element = document.getElementById('qrcode-img');
// 新建 canvas 承载截图
const canvas = document.createElement('canvas');
const height = element.clientHeight;
const width = element.clientWidth;
// 调整画布大小
canvas.width = width;
canvas.height = height;
const canvasContent = await html2canvas(element, {
canvas,
scrollY: 0, // 必须设置,否则 canvas 会出现绘制偏移
scrollX: 0,
width,
height,
scale: 1,
useCORS: true, // 图片跨域相关
allowTaint: false, // 图片跨域相关
});
return canvasContent
} catch (error) {
return Promise.reject();
}
完整代码
tempalte
<a-button type="link" size="small" @click="handleDownload(record)">下载二维码</a-button>
<div v-show="showQRCode" id="qrcode-img">
<qrcode-vue :size="qrCodeImgConfig.size" :value="qrCodeImgConfig.qrUrlContent" />
<div class="qr-name">{{ qrCodeImgConfig.qrName }}</div>
</div>
script
async handleDownload(record = {}) {
const { qrUrlContent, qrName } = record;
const scale = window.devicePixelRatio || 1;
// 保证二维码宽高固定是 400px
this.qrCodeImgConfig = {
qrUrlContent,
qrName,
size: 400 / scale,
};
this.showQRCode = true;
setTimeout(async () => {
try {
// canvas对象生成成功
const canvasContent = await this.generateImg();
this.downloadFile(`${qrName}.png`, canvasContent);
this.showQRCode = false;
} catch (error) {
this.showQRCode = false;
throw new Error(error);
}
});
},
async generateImg() {
try {
const element = document.getElementById('qrcode-img');
const canvas = document.createElement('canvas');
const height = element.clientHeight;
const width = element.clientWidth;
// 调整画布大小
canvas.width = width;
canvas.height = height;
const canvasContent = await html2canvas(element, {
canvas,
scrollY: 0,
scrollX: 0,
width,
height,
scale: 1,
useCORS: true, // 图片跨域相关
allowTaint: false, // 图片跨域相关
});
return canvasContent
} catch (error) {
return Promise.reject();
}
},
// 下载
downloadFile(fileName, canvas) {
canvas.toBlob((blob) => {
if (blob) {
const aLink = document.createElement('a');
const evt = document.createEvent('HTMLEvents');
evt.initEvent('click', true, true);
aLink.download = fileName;
aLink.href = URL.createObjectURL(blob);
aLink.click();
} else {
// 转换失败的处理
return Promise.reject();
}
});
},
style
#qrcode-img {
position: absolute;
padding: 40px 80px;
top: -100vh;
right: -100vw;
z-index: -99999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.qr-name {
margin-top: 12px;
}
}