前端绘制高清二维码并下载

335 阅读4分钟

背景

项目要求编辑配置相关信息后,点击表格行间按钮,直接生成二维码并下载带有二维码名称的高清图片。类似下图

test1726804305759+4

需要考虑以下几点:

  1. 根据二维码内容生成二维码
  2. 设置二维码图和二维码名称居中对齐
  3. 截取二维码图和二维码名称区域,设置合适的边缘空白
  4. 下载图片

前置知识

webkitBackingStorePixelRatio 和 devicePixelRatio

webkitBackingStorePixelRatio: canvas context 相关属性,仅safari和chrome,该属性的值决定了浏览器在渲染canvas之前会用几个像素来来存储画布信息。

devicePixelRatio:设备像素比,是一个物理概念,返回当前显示设备的物理像素分辨率与CSS 像素分辨率之比

浏览器绘制 Canvas 渲染到屏幕中分两个过程:

  • 绘制过程:webkitBackingStorePixelRatio webkitBackingStorePixelRatio 表示浏览器在绘制 Canvas 到缓存区时的绘制比例,若图片宽高为 200pxwebkitBackingStorePixelRatio 为 2,那么 Canvas 绘制这个图片到缓存区时,宽高就为 400px
  • 渲染过程:devicePixelRatio Canvas 显示到屏幕中还需要渲染过程,渲染过程根据 devicePixelRatio 参数将缓存区中的 Canvas 进行缩放渲染到屏幕中

举例说明, 在大部分高清屏中,例如 Macbook Pro 中:

webkitBackingStorePixelRatio = 1
devicePixelRatio = 2

将一个 200px * 200px 的图片 Cavnas 绘制到该屏幕中的流程:

  • webkitBackingStorePixelRatio = 1,绘制到缓存区的大小也为:200px * 200px
  • devicePixelRatio = 2 200px * 200px 的图片对应到屏幕像素为 400px * 400pxdevicePixelRatio = 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:

image-20240920152240538

当浏览器缩放是75%时,打印得知 devicePixelRatio 为1.5:

image-20240920152142123

可以发现生成的二维码根本不是按照我预想的那样,宽高都是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;
    }
}

refer

objcer.com/2017/10/10/…

html2canvas.hertzen.com/configurati…