在 uniapp 中使用 canvas 绘制海报

512 阅读3分钟

最近遇到了一个场景,在uniapp 微信小程序中,需要兼容支付宝支付。但是微信小程序里面不支持使用支付宝,所以就只能让后端对接支付宝的 api 生成一个收款链接,然后在前端绘制出一个收款海报,在海报里面生成一个二维码来展示这个收款链接,然后在海报里写上订单相关的信息,让需要使用支付宝支付的用户手动保存海报到手机,再支付宝扫码支付。生成的海报类似这样:

pic0.8650634939113644.png

刚开始直接在uniapp中用 canvas 绘制这个海报,因为要先在页面上预先显示一个二维码,所以引入了 tki-qrcode 这个库来生成二维码,还可以很方便的拿到生成的二维码的临时路径。

    <view class="close-space-t20 flex-column-justify">
          <tki-qrcode
            ref="qrcode"
            :val="qrCode.val"
            :size="qrCode.size"
            :background="qrCode.background"
            :foreground="qrCode.foreground"
            :pdground="qrCode.pdground"
            :icon="qrCode.icon"
            :iconSize="qrCode.iconsize"
            :lv="qrCode.lv"
            :onval="qrCode.onval"
            :loadMake="qrCode.loadMake"
            :showLoading="qrCode.showLoading"
            :loadingText="qrCode.loadingText"
            @result="qrR"
          />
          <button class="solidBlueBtn close-space-t20" @click="saveImageToPhotosAlbum1">
            保存二维码
          </button>
        </view>

然后在 js 里面绘制海报,嵌入二维码,再保存图片到手机。一切都很顺利,可是最后在安卓手机上测试的时候,发现 uniapp 微信小程序中绘制的海报在安卓手机上不兼容,文字样式会出现各种混乱异常情况,比如颜色,大小随机变化,甚至有时候渲染不出来文字,在微信小程序社区和一些官方论坛多方寻找,发现这个安卓的兼容性问题很多人遇到,但是一直没有被修复,所以就更换了思路,在 node 服务里面去绘制海报,然后再把绘制好的海报文件返回给小程序端保存,这样就可以避开设备兼容性问题。 首先,在node里面引入 canvas ,fs ,path 这些模块,绘图的 api 与小程序里面的 canvas 几乎一样,下面贴一下代码:

  async createCanvasImg(data) {
    const currentUuid = uuidv4();
    const { payQrCode, orderInfo } = data;
    console.log(data, '请求体', currentUuid, orderInfo);
    // 把请求体里面带的base64图片(中间的支付二维码)存在本地
    const bufferData = Buffer.from(payQrCode.split(',')[1], 'base64');
    const qrImgFilePath = path.resolve(`app/public/qr-code${currentUuid}.png`);
    // 将 Buffer 对象写入文件系统
    fs.writeFileSync(qrImgFilePath, bufferData, err => {
      if (err) throw err;
    });
    // 缩放倍数,提升canvas图像清晰度
    const scaleMultiple = 2;
    // rpx 用于适配各种屏幕的尺寸
    const rpx = 0.5706666666666667;
    // 创建整个画报
    const canvas = createCanvas(750 * rpx, 1300 * rpx);
    canvas.width *= scaleMultiple;
    canvas.height *= scaleMultiple;
    const ctx = canvas.getContext('2d');
    ctx.imageSmoothingEnabled = true;

    // 上面订单信息和收货地址区域
    ctx.fillStyle = '#fff';
    ctx.fillRect(0, 0, 750 * rpx * scaleMultiple, 448 * rpx * scaleMultiple);
    // 添加背景水印
    ctx.drawImage(
      await loadImage(path.resolve('app/public/watermark.png')),
      454 * rpx * scaleMultiple,
      48 * rpx * scaleMultiple,
      400 * rpx * scaleMultiple,
      400 * rpx * scaleMultiple
    );
    ctx.fillStyle = 'rgba(0,0,0,0.9)';
    ctx.font = `600 ${16 * scaleMultiple}px SIMHEI`;
    ctx.fillText('订单信息', 48 * rpx * scaleMultiple, 80 * rpx * scaleMultiple);
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    ctx.font = `400 ${14 * scaleMultiple}px SIMHEI`;
    ctx.fillText('订单编号:2023023048576748', 48 * rpx * scaleMultiple, 132 * rpx * scaleMultiple);
    ctx.fillText(
      '下单时间:2023-04-24 20:12:12',
      48 * rpx * scaleMultiple,
      180 * rpx * scaleMultiple
    );
    ctx.fillText(
      `下单商品:${this.canvasTextEllipse(
        ctx,
        '浙江省 杭州市 上城区 文一西路356号商会大厦B座5单元6楼 姚女士 15612345678',
        170 * rpx * scaleMultiple, // x轴坐标位置(视实际情况而定)
        228 * rpx * scaleMultiple, // y轴坐标位置(视实际情况而定)
        360 * rpx * scaleMultiple // 文字显示最大宽度(视实际情况而定)
      )}`,
      48 * rpx * scaleMultiple,
      228 * rpx * scaleMultiple
    );
    ctx.fillText('等100种商品', 570 * rpx * scaleMultiple, 228 * rpx * scaleMultiple);
    ctx.fillStyle = 'rgba(0,0,0,0.9)';
    ctx.font = `600 ${16 * scaleMultiple}px SIMHEI`;
    ctx.fillText('收货地址', 48 * rpx * scaleMultiple, 296 * rpx * scaleMultiple);
    ctx.font = `400 ${14 * scaleMultiple}px SIMHEI`;
    ctx.fillStyle = 'rgba(0,0,0,0.6)';
    this.canvasTextAutoLine(
      ctx,
      '浙江省 杭州市 上城区 文一西路356号商会大厦B座5单元6楼 姚女士 15612345678',
      48 * rpx * scaleMultiple, // x轴坐标位置(视实际情况而定)
      348 * rpx * scaleMultiple, // y轴坐标位置(视实际情况而定)
      654 * rpx * scaleMultiple, // 文字显示最大宽度(视实际情况而定)
      40 * rpx * scaleMultiple // 文字行高(视实际情况而定)
    );
    ctx.fillStyle = '#1678FF';
    ctx.fillRect(
      0,
      448 * rpx * scaleMultiple,
      750 * rpx * scaleMultiple,
      820 * rpx * scaleMultiple
    );
    ctx.closePath();
    // 开启新的绘制
    ctx.fillStyle = '#fff';
    ctx.font = `bold ${20 * scaleMultiple}px SIMHEI`;
    ctx.fillText('打开支付宝【扫一扫】', 214 * rpx * scaleMultiple, 528 * rpx * scaleMultiple);
    ctx.fillRect(
      128 * rpx * scaleMultiple,
      588 * rpx * scaleMultiple,
      500 * rpx * scaleMultiple,
      610 * rpx * scaleMultiple
    ); // 二维码盒子

    ctx.drawImage(
      await loadImage(path.resolve(`app/public/qr-code${currentUuid}.png`)),
      184 * rpx * scaleMultiple,
      640 * rpx * scaleMultiple,
      400 * rpx * scaleMultiple,
      400 * rpx * scaleMultiple
    );
    ctx.font = `500 ${18 * scaleMultiple}px SIMHEI`;
    ctx.fillStyle = '#000';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText('XXXX有限公司', 380 * rpx * scaleMultiple, 1086 * rpx * scaleMultiple);
    ctx.font = `bold ${20 * scaleMultiple}px SIMHEI`;
    ctx.fillStyle = '#EB2533';
    ctx.fillText(
      ${this.slicePrice('178.9')[0]}${this.slicePrice('178.9')[1]}`,
      380 * rpx * scaleMultiple,
      1150 * rpx * scaleMultiple
    );
    ctx.closePath();
    ctx.scale(2, 2);
    const buffer = canvas.toBuffer('image/png');
    // 写入本地预览生成图片
    // fs.writeFileSync('output.png', buffer);
    // 最后,删除本次生成的所有图片
    const allFiles = [`app/public/qr-code${currentUuid}.png`];
    this.deleteFiles(allFiles);
    return buffer;
  }