DEV Community

Cover image for 微信小程序实现动态二维码海报生成与保存 | 高效便捷的前端方案
Jessie Chen
Jessie Chen

Posted on

微信小程序实现动态二维码海报生成与保存 | 高效便捷的前端方案

WEBSITE:Welcome to Jessie's World

GitHub:GitHub

前言

在微信小程序开发中,经常需要实现分享海报功能,通常包含动态二维码。本文将详细介绍如何在小程序中生成带有二维码的海报,并实现保存到手机相册的功能。

实现原理

整个功能的实现主要包含以下几个步骤:

  1. 生成二维码
  2. 绘制海报背景
  3. 将二维码绘制到海报上
  4. 将画布导出为图片
  5. 保存图片到相册

核心代码实现

1. 二维码生成工具类

首先,我们需要一个二维码生成的工具类。这里使用优化后的 QRCode 工具:

qrcode.js:绘制二维码

// qrcode.js 绘制二维码

!(function () {
  // alignment pattern
  var adelta = [
    0, 11, 15, 19, 23, 27, 31, 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24, 26, 26, 28, 28, 24, 24, 26, 26,
    26, 28, 28, 24, 26, 26, 26, 28, 28
  ];

  // version block
  var vpat = [
    0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d, 0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9, 0x7ec, 0xec4,
    0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75, 0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64, 0x541, 0xc69
  ];

  // final format bits with mask: level << 3 | mask
  var fmtword = [
    0x77c4,
    0x72f3,
    0x7daa,
    0x789d,
    0x662f,
    0x6318,
    0x6c41,
    0x6976, //L
    0x5412,
    0x5125,
    0x5e7c,
    0x5b4b,
    0x45f9,
    0x40ce,
    0x4f97,
    0x4aa0, //M
    0x355f,
    0x3068,
    0x3f31,
    0x3a06,
    0x24b4,
    0x2183,
    0x2eda,
    0x2bed, //Q
    0x1689,
    0x13be,
    0x1ce7,
    0x19d0,
    0x0762,
    0x0255,
    0x0d0c,
    0x083b //H
  ];

  // 4 per version: number of blocks 1,2; data width; ecc width
  var eccblocks = [
    1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17, 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22, 1, 0, 16, 28, 1, 0, 55, 15, 1,
    0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22, 1, 0, 80, 20, 2, 0, 32, 18, 2, 0, 24, 26, 4, 0, 9, 16, 1, 0, 108, 26, 2, 0, 43, 24, 2,
    2, 15, 18, 2, 2, 11, 22, 2, 0, 68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28, 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4,
    1, 13, 26, 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26, 2, 0, 116, 30, 3, 2, 36, 22, 4, 4, 16, 20, 4, 4, 12, 24, 2,
    2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28, 4, 0, 81, 20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24, 2, 2, 92, 24, 6,
    2, 36, 22, 4, 6, 20, 26, 7, 4, 14, 28, 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22, 3, 1, 115, 30, 4, 5, 40, 24,
    11, 5, 16, 20, 11, 5, 12, 24, 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24, 5, 1, 98, 24, 7, 3, 45, 28, 15, 2, 19,
    24, 3, 13, 15, 30, 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2, 17, 14, 28, 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2,
    19, 14, 28, 3, 4, 113, 28, 3, 11, 44, 26, 17, 4, 21, 26, 9, 16, 13, 26, 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10,
    15, 28, 4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30, 2, 7, 111, 28, 17, 0, 46, 28, 7, 16, 24, 30, 34, 0, 13,
    24, 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30, 6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30,
    8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30, 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30, 8,
    4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30, 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30, 7, 7,
    116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30, 5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30, 13, 3,
    115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30, 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30, 17, 1,
    115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30, 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30, 12, 7,
    121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30, 6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30, 17, 4,
    122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30, 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30, 20,
    4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30, 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30
  ];

  // Galois field log table
  var glog = [
    0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b, 0x04, 0x64, 0xe0, 0x0e, 0x34,
    0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71, 0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93,
    0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45, 0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72,
    0xa6, 0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88, 0x36, 0xd0, 0x94, 0xce,
    0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40, 0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b,
    0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d, 0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3,
    0xa7, 0x57, 0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18, 0xe3, 0xa5, 0x99,
    0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e, 0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd,
    0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61, 0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47,
    0x6d, 0x41, 0xa2, 0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6, 0x6c, 0xa1,
    0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a, 0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0,
    0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7, 0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea,
    0xa8, 0x50, 0x58, 0xaf
  ];

  // Galios field exponent table
  var gexp = [
    0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26, 0x4c, 0x98, 0x2d, 0x5a, 0xb4,
    0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4,
    0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23, 0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde,
    0xa1, 0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0, 0xfd, 0xe7, 0xd3, 0xbb,
    0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2, 0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d,
    0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce, 0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33,
    0x66, 0xcc, 0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54, 0xa8, 0x4d, 0x9a,
    0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73, 0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e,
    0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff, 0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5,
    0x57, 0xae, 0x41, 0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6, 0x51, 0xa2,
    0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09, 0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4,
    0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16, 0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8,
    0xad, 0x47, 0x8e, 0x00
  ];

  // Working buffers:
  // data input and ecc append, image working buffer, fixed part of image, run lengths for badness
  var strinbuf = [],
    eccbuf = [],
    qrframe = [],
    framask = [],
    rlens = [];
  // Control values - width is based on version, last 4 are from table.
  var version, width, neccblk1, neccblk2, datablkw, eccblkwid;
  var ecclevel = 2;
  // set bit to indicate cell in qrframe is immutable.  symmetric around diagonal
  function setmask(x, y) {
    var bt;
    if (x > y) {
      bt = x;
      x = y;
      y = bt;
    }
    // y*y = 1+3+5...
    bt = y;
    bt *= y;
    bt += y;
    bt >>= 1;
    bt += x;
    framask[bt] = 1;
  }

  // enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask)
  function putalign(x, y) {
    var j;

    qrframe[x + width * y] = 1;
    for (j = -2; j < 2; j++) {
      qrframe[x + j + width * (y - 2)] = 1;
      qrframe[x - 2 + width * (y + j + 1)] = 1;
      qrframe[x + 2 + width * (y + j)] = 1;
      qrframe[x + j + 1 + width * (y + 2)] = 1;
    }
    for (j = 0; j < 2; j++) {
      setmask(x - 1, y + j);
      setmask(x + 1, y - j);
      setmask(x - j, y - 1);
      setmask(x + j, y + 1);
    }
  }

  //========================================================================
  // Reed Solomon error correction
  // exponentiation mod N
  function modnn(x) {
    while (x >= 255) {
      x -= 255;
      x = (x >> 8) + (x & 255);
    }
    return x;
  }

  var genpoly = [];

  // Calculate and append ECC data to data block.  Block is in strinbuf, indexes to buffers given.
  function appendrs(data, dlen, ecbuf, eclen) {
    var i, j, fb;

    for (i = 0; i < eclen; i++) strinbuf[ecbuf + i] = 0;
    for (i = 0; i < dlen; i++) {
      fb = glog[strinbuf[data + i] ^ strinbuf[ecbuf]];
      if (fb != 255)
        /* fb term is non-zero */
        for (j = 1; j < eclen; j++) strinbuf[ecbuf + j - 1] = strinbuf[ecbuf + j] ^ gexp[modnn(fb + genpoly[eclen - j])];
      else for (j = ecbuf; j < ecbuf + eclen; j++) strinbuf[j] = strinbuf[j + 1];
      strinbuf[ecbuf + eclen - 1] = fb == 255 ? 0 : gexp[modnn(fb + genpoly[0])];
    }
  }

  //========================================================================
  // Frame data insert following the path rules

  // check mask - since symmetrical use half.
  function ismasked(x, y) {
    var bt;
    if (x > y) {
      bt = x;
      x = y;
      y = bt;
    }
    bt = y;
    bt += y * y;
    bt >>= 1;
    bt += x;
    return framask[bt];
  }

  //========================================================================
  //  Apply the selected mask out of the 8.
  function applymask(m) {
    var x, y, r3x, r3y;

    switch (m) {
      case 0:
        for (y = 0; y < width; y++)
          for (x = 0; x < width; x++) if (!((x + y) & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
        break;
      case 1:
        for (y = 0; y < width; y++) for (x = 0; x < width; x++) if (!(y & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
        break;
      case 2:
        for (y = 0; y < width; y++)
          for (r3x = 0, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!r3x && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        break;
      case 3:
        for (r3y = 0, y = 0; y < width; y++, r3y++) {
          if (r3y == 3) r3y = 0;
          for (r3x = r3y, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!r3x && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        }
        break;
      case 4:
        for (y = 0; y < width; y++)
          for (r3x = 0, r3y = (y >> 1) & 1, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) {
              r3x = 0;
              r3y = !r3y;
            }
            if (!r3y && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        break;
      case 5:
        for (r3y = 0, y = 0; y < width; y++, r3y++) {
          if (r3y == 3) r3y = 0;
          for (r3x = 0, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!((x & y & 1) + !(!r3x | !r3y)) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        }
        break;
      case 6:
        for (r3y = 0, y = 0; y < width; y++, r3y++) {
          if (r3y == 3) r3y = 0;
          for (r3x = 0, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!(((x & y & 1) + (r3x && r3x == r3y)) & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        }
        break;
      case 7:
        for (r3y = 0, y = 0; y < width; y++, r3y++) {
          if (r3y == 3) r3y = 0;
          for (r3x = 0, x = 0; x < width; x++, r3x++) {
            if (r3x == 3) r3x = 0;
            if (!(((r3x && r3x == r3y) + ((x + y) & 1)) & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1;
          }
        }
        break;
    }
    return;
  }

  // Badness coefficients.
  var N1 = 3,
    N2 = 3,
    N3 = 40,
    N4 = 10;

  // Using the table of the length of each run, calculate the amount of bad image
  // - long runs or those that look like finders; called twice, once each for X and Y
  function badruns(length) {
    var i;
    var runsbad = 0;
    for (i = 0; i <= length; i++) if (rlens[i] >= 5) runsbad += N1 + rlens[i] - 5;
    // BwBBBwB as in finder
    for (i = 3; i < length - 1; i += 2)
      if (
        rlens[i - 2] == rlens[i + 2] &&
        rlens[i + 2] == rlens[i - 1] &&
        rlens[i - 1] == rlens[i + 1] &&
        rlens[i - 1] * 3 == rlens[i] &&
        // white around the black pattern? Not part of spec
        (rlens[i - 3] == 0 || // beginning
          i + 3 > length || // end
          rlens[i - 3] * 3 >= rlens[i] * 4 ||
          rlens[i + 3] * 3 >= rlens[i] * 4)
      )
        runsbad += N3;
    return runsbad;
  }

  // Calculate how bad the masked image is - blocks, imbalance, runs, or finders.
  function badcheck() {
    var x, y, h, b, b1;
    var thisbad = 0;
    var bw = 0;

    // blocks of same color.
    for (y = 0; y < width - 1; y++)
      for (x = 0; x < width - 1; x++)
        if (
          (qrframe[x + width * y] &&
            qrframe[x + 1 + width * y] &&
            qrframe[x + width * (y + 1)] &&
            qrframe[x + 1 + width * (y + 1)]) || // all black
          !(
            qrframe[x + width * y] ||
            qrframe[x + 1 + width * y] ||
            qrframe[x + width * (y + 1)] ||
            qrframe[x + 1 + width * (y + 1)]
          )
        )
          // all white
          thisbad += N2;

    // X runs
    for (y = 0; y < width; y++) {
      rlens[0] = 0;
      for (h = b = x = 0; x < width; x++) {
        if ((b1 = qrframe[x + width * y]) == b) rlens[h]++;
        else rlens[++h] = 1;
        b = b1;
        bw += b ? 1 : -1;
      }
      thisbad += badruns(h);
    }

    // black/white imbalance
    if (bw < 0) bw = -bw;

    var big = bw;
    var count = 0;
    big += big << 2;
    big <<= 1;
    while (big > width * width) (big -= width * width), count++;
    thisbad += count * N4;

    // Y runs
    for (x = 0; x < width; x++) {
      rlens[0] = 0;
      for (h = b = y = 0; y < width; y++) {
        if ((b1 = qrframe[x + width * y]) == b) rlens[h]++;
        else rlens[++h] = 1;
        b = b1;
      }
      thisbad += badruns(h);
    }
    return thisbad;
  }

  function genframe(instring) {
    var x, y, k, t, v, i, j, m;

    // find the smallest version that fits the string
    t = instring.length;
    version = 0;
    do {
      version++;
      k = (ecclevel - 1) * 4 + (version - 1) * 16;
      neccblk1 = eccblocks[k++];
      neccblk2 = eccblocks[k++];
      datablkw = eccblocks[k++];
      eccblkwid = eccblocks[k];
      k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9);
      if (t <= k) break;
    } while (version < 40);

    // FIXME - insure that it fits insted of being truncated
    width = 17 + 4 * version;

    // allocate, clear and setup data structures
    v = datablkw + (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
    for (t = 0; t < v; t++) eccbuf[t] = 0;
    strinbuf = instring.slice(0);

    for (t = 0; t < width * width; t++) qrframe[t] = 0;

    for (t = 0; t < (width * (width + 1) + 1) / 2; t++) framask[t] = 0;

    // insert finders - black to frame, white to mask
    for (t = 0; t < 3; t++) {
      k = 0;
      y = 0;
      if (t == 1) k = width - 7;
      if (t == 2) y = width - 7;
      qrframe[y + 3 + width * (k + 3)] = 1;
      for (x = 0; x < 6; x++) {
        qrframe[y + x + width * k] = 1;
        qrframe[y + width * (k + x + 1)] = 1;
        qrframe[y + 6 + width * (k + x)] = 1;
        qrframe[y + x + 1 + width * (k + 6)] = 1;
      }
      for (x = 1; x < 5; x++) {
        setmask(y + x, k + 1);
        setmask(y + 1, k + x + 1);
        setmask(y + 5, k + x);
        setmask(y + x + 1, k + 5);
      }
      for (x = 2; x < 4; x++) {
        qrframe[y + x + width * (k + 2)] = 1;
        qrframe[y + 2 + width * (k + x + 1)] = 1;
        qrframe[y + 4 + width * (k + x)] = 1;
        qrframe[y + x + 1 + width * (k + 4)] = 1;
      }
    }

    // alignment blocks
    if (version > 1) {
      t = adelta[version];
      y = width - 7;
      for (;;) {
        x = width - 7;
        while (x > t - 3) {
          putalign(x, y);
          if (x < t) break;
          x -= t;
        }
        if (y <= t + 9) break;
        y -= t;
        putalign(6, y);
        putalign(y, 6);
      }
    }

    // single black
    qrframe[8 + width * (width - 8)] = 1;

    // timing gap - mask only
    for (y = 0; y < 7; y++) {
      setmask(7, y);
      setmask(width - 8, y);
      setmask(7, y + width - 7);
    }
    for (x = 0; x < 8; x++) {
      setmask(x, 7);
      setmask(x + width - 8, 7);
      setmask(x, width - 8);
    }

    // reserve mask-format area
    for (x = 0; x < 9; x++) setmask(x, 8);
    for (x = 0; x < 8; x++) {
      setmask(x + width - 8, 8);
      setmask(8, x);
    }
    for (y = 0; y < 7; y++) setmask(8, y + width - 7);

    // timing row/col
    for (x = 0; x < width - 14; x++)
      if (x & 1) {
        setmask(8 + x, 6);
        setmask(6, 8 + x);
      } else {
        qrframe[8 + x + width * 6] = 1;
        qrframe[6 + width * (8 + x)] = 1;
      }

    // version block
    if (version > 6) {
      t = vpat[version - 7];
      k = 17;
      for (x = 0; x < 6; x++)
        for (y = 0; y < 3; y++, k--)
          if (1 & (k > 11 ? version >> (k - 12) : t >> k)) {
            qrframe[5 - x + width * (2 - y + width - 11)] = 1;
            qrframe[2 - y + width - 11 + width * (5 - x)] = 1;
          } else {
            setmask(5 - x, 2 - y + width - 11);
            setmask(2 - y + width - 11, 5 - x);
          }
    }

    // sync mask bits - only set above for white spaces, so add in black bits
    for (y = 0; y < width; y++) for (x = 0; x <= y; x++) if (qrframe[x + width * y]) setmask(x, y);

    // convert string to bitstream
    // 8 bit data to QR-coded 8 bit data (numeric or alphanum, or kanji not supported)
    v = strinbuf.length;

    // string to array
    for (i = 0; i < v; i++) eccbuf[i] = strinbuf.charCodeAt(i);
    strinbuf = eccbuf.slice(0);

    // calculate max string length
    x = datablkw * (neccblk1 + neccblk2) + neccblk2;
    if (v >= x - 2) {
      v = x - 2;
      if (version > 9) v--;
    }

    // shift and repack to insert length prefix
    i = v;
    if (version > 9) {
      strinbuf[i + 2] = 0;
      strinbuf[i + 3] = 0;
      while (i--) {
        t = strinbuf[i];
        strinbuf[i + 3] |= 255 & (t << 4);
        strinbuf[i + 2] = t >> 4;
      }
      strinbuf[2] |= 255 & (v << 4);
      strinbuf[1] = v >> 4;
      strinbuf[0] = 0x40 | (v >> 12);
    } else {
      strinbuf[i + 1] = 0;
      strinbuf[i + 2] = 0;
      while (i--) {
        t = strinbuf[i];
        strinbuf[i + 2] |= 255 & (t << 4);
        strinbuf[i + 1] = t >> 4;
      }
      strinbuf[1] |= 255 & (v << 4);
      strinbuf[0] = 0x40 | (v >> 4);
    }
    // fill to end with pad pattern
    i = v + 3 - (version < 10);
    while (i < x) {
      strinbuf[i++] = 0xec;
      // buffer has room    if (i == x)      break;
      strinbuf[i++] = 0x11;
    }

    // calculate and append ECC

    // calculate generator polynomial
    genpoly[0] = 1;
    for (i = 0; i < eccblkwid; i++) {
      genpoly[i + 1] = 1;
      for (j = i; j > 0; j--) genpoly[j] = genpoly[j] ? genpoly[j - 1] ^ gexp[modnn(glog[genpoly[j]] + i)] : genpoly[j - 1];
      genpoly[0] = gexp[modnn(glog[genpoly[0]] + i)];
    }
    for (i = 0; i <= eccblkwid; i++) genpoly[i] = glog[genpoly[i]]; // use logs for genpoly[] to save calc step

    // append ecc to data buffer
    k = x;
    y = 0;
    for (i = 0; i < neccblk1; i++) {
      appendrs(y, datablkw, k, eccblkwid);
      y += datablkw;
      k += eccblkwid;
    }
    for (i = 0; i < neccblk2; i++) {
      appendrs(y, datablkw + 1, k, eccblkwid);
      y += datablkw + 1;
      k += eccblkwid;
    }
    // interleave blocks
    y = 0;
    for (i = 0; i < datablkw; i++) {
      for (j = 0; j < neccblk1; j++) eccbuf[y++] = strinbuf[i + j * datablkw];
      for (j = 0; j < neccblk2; j++) eccbuf[y++] = strinbuf[neccblk1 * datablkw + i + j * (datablkw + 1)];
    }
    for (j = 0; j < neccblk2; j++) eccbuf[y++] = strinbuf[neccblk1 * datablkw + i + j * (datablkw + 1)];
    for (i = 0; i < eccblkwid; i++) for (j = 0; j < neccblk1 + neccblk2; j++) eccbuf[y++] = strinbuf[x + i + j * eccblkwid];
    strinbuf = eccbuf;

    // pack bits into frame avoiding masked area.
    x = y = width - 1;
    k = v = 1; // up, minus
    /* inteleaved data and ecc codes */
    m = (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2;
    for (i = 0; i < m; i++) {
      t = strinbuf[i];
      for (j = 0; j < 8; j++, t <<= 1) {
        if (0x80 & t) qrframe[x + width * y] = 1;
        do {
          // find next fill position
          if (v) x--;
          else {
            x++;
            if (k) {
              if (y != 0) y--;
              else {
                x -= 2;
                k = !k;
                if (x == 6) {
                  x--;
                  y = 9;
                }
              }
            } else {
              if (y != width - 1) y++;
              else {
                x -= 2;
                k = !k;
                if (x == 6) {
                  x--;
                  y -= 8;
                }
              }
            }
          }
          v = !v;
        } while (ismasked(x, y));
      }
    }

    // save pre-mask copy of frame
    strinbuf = qrframe.slice(0);
    t = 0; // best
    y = 30000; // demerit
    // for instead of while since in original arduino code
    // if an early mask was "good enough" it wouldn't try for a better one
    // since they get more complex and take longer.
    for (k = 0; k < 8; k++) {
      applymask(k); // returns black-white imbalance
      x = badcheck();
      if (x < y) {
        // current mask better than previous best?
        y = x;
        t = k;
      }
      if (t == 7) break; // don't increment i to a void redoing mask
      qrframe = strinbuf.slice(0); // reset for next pass
    }
    if (t != k)
      // redo best mask - none good enough, last wasn't t
      applymask(t);

    // add in final mask/ecclevel bytes
    y = fmtword[t + ((ecclevel - 1) << 3)];
    // low byte
    for (k = 0; k < 8; k++, y >>= 1)
      if (y & 1) {
        qrframe[width - 1 - k + width * 8] = 1;
        if (k < 6) qrframe[8 + width * k] = 1;
        else qrframe[8 + width * (k + 1)] = 1;
      }
    // high byte
    for (k = 0; k < 7; k++, y >>= 1)
      if (y & 1) {
        qrframe[8 + width * (width - 7 + k)] = 1;
        if (k) qrframe[6 - k + width * 8] = 1;
        else qrframe[7 + width * 8] = 1;
      }
    return qrframe;
  }

  var _canvas = null;

  var api = {
    get ecclevel() {
      return ecclevel;
    },

    set ecclevel(val) {
      ecclevel = val;
    },

    get size() {
      return _size;
    },

    set size(val) {
      _size = val;
    },

    get canvas() {
      return _canvas;
    },

    set canvas(el) {
      _canvas = el;
    },

    drawRoundRectPath: function (cxt, width, height, radius) {
      cxt.beginPath(0);
      //从右下角顺时针绘制,弧度从0到1/2PI
      cxt.arc(width - radius, height - radius, radius, 0, Math.PI / 2);

      //矩形下边线
      cxt.lineTo(radius, height);

      //左下角圆弧,弧度从1/2PI到PI
      cxt.arc(radius, height - radius, radius, Math.PI / 2, Math.PI);

      //矩形左边线
      cxt.lineTo(0, radius);

      //左上角圆弧,弧度从PI到3/2PI
      cxt.arc(radius, radius, radius, Math.PI, (Math.PI * 3) / 2);

      //上边线
      cxt.lineTo(width - radius, 0);

      //右上角圆弧
      cxt.arc(width - radius, radius, radius, (Math.PI * 3) / 2, Math.PI * 2);

      //右边线
      cxt.lineTo(width, height - radius);
      cxt.closePath();
    },
    /**该方法用来绘制一个有填充色的圆角矩形
     *@param cxt:canvas的上下文环境
     *@param x:左上角x轴坐标
     *@param y:左上角y轴坐标
     *@param width:矩形的宽度
     *@param height:矩形的高度
     *@param radius:圆的半径
     *@param fillColor:填充颜色
     **/
    fillRoundRect: function (cxt, x, y, width, height, radius, /*optional*/ fillColor) {
      //圆的直径必然要小于矩形的宽高
      if (2 * radius > width || 2 * radius > height) {
        return false;
      }

      cxt.save();
      cxt.translate(x, y);
      //绘制圆角矩形的各个边
      this.drawRoundRectPath(cxt, width, height, radius);
      cxt.fillStyle = fillColor || '#000'; //若是给定了值就用给定的值否则给予默认值
      cxt.fill();
      cxt.restore();
    },
    getFrame: function (string) {
      return genframe(string);
    },
    //这里的utf16to8(str)是对Text中的字符串进行转码,让其支持中文
    utf16to8: function (str) {
      var out, i, len, c;

      out = '';
      len = str && str.length;
      for (i = 0; i < len; i++) {
        c = str.charCodeAt(i);
        if (c >= 0x0001 && c <= 0x007f) {
          out += str.charAt(i);
        } else if (c > 0x07ff) {
          out += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f));
          out += String.fromCharCode(0x80 | ((c >> 6) & 0x3f));
          out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));
        } else {
          out += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f));
          out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));
        }
      }
      return out;
    },

    /**
     * 新的绘制方法,用于在指定的画布上绘制二维码。
     * @param {Function} createSelectorQuery - 用于创建选择器查询的函数。
     * @param {string} str - 要编码为二维码的字符串。
     * @param {string} _canvasId - 画布的ID(可选)。
     * @param {number} cavW - 画布的宽度。
     * @param {number} cavH - 画布的高度。
     * @param {Object} $this - 组件的上下文,允许在组件中生成二维码。
     * @param {number} ecc - 错误纠正级别(可选)。
     * @param {Function} callBack - 绘制完成后的回调函数。
     * @param {HTMLCanvasElement} _Canvas - 画布元素。
     * @param {number} dpr - 设备像素比,用于高分辨率屏幕。
     */
    newDraw: async function (
      createSelectorQuery,
      str,
      _canvasId,
      cavW,
      cavH,
      $this,
      ecc,
      callBack,
      _Canvas,
      dpr,
      fillStyle,
      fillBgStyle = '#ffffff'
    ) {
      var that = this; // 保存当前上下文
      ecclevel = ecc || ecclevel; // 设置错误纠正级别,如果未提供则使用默认值
      const canvasId = _canvasId || _canvas; // 获取画布ID,如果未提供则使用默认画布

      // 检查是否提供了画布
      if (!canvasId) {
        console.warn('No canvas provided to draw QR code in!'); // 如果没有画布,发出警告
        return; // 退出函数
      }

      var size = Math.min(cavW, cavH); // 计算画布的最小尺寸
      str = that.utf16to8(str); // 将字符串转换为UTF-8格式,以支持中文

      var frame = that.getFrame(str), // 获取二维码的帧数据
        px = Math.round(size / (width + 8)); // 计算每个二维码单元的像素大小
      var ctx = _Canvas.getContext('2d'); // 获取2D绘图上下文
      _Canvas.width = cavW * dpr; // 设置画布宽度,考虑设备像素比
      _Canvas.height = cavH * dpr; // 设置画布高度,考虑设备像素比
      ctx.scale(dpr, dpr); // 缩放上下文以适应高分辨率

      var roundedSize = px * (width + 8), // 计算绘制的二维码的总大小
        offset = Math.floor((size - roundedSize) / 2); // 计算偏移量以居中二维码
      size = roundedSize; // 更新大小变量
      ctx.fillStyle = '#FEF5E0'; // 设置填充颜色为白色
      this.fillRoundRect(ctx, 0, 0, cavW, cavW, 10, /*optional*/ fillBgStyle); // 绘制圆角矩形背景
      ctx.fillStyle = '#FF7D00'; // 设置填充颜色为黑色

      // 绘制二维码的每个模块
      for (var i = 0; i < width; i++) {
        for (var j = 0; j < width; j++) {
          if (frame[j * width + i]) {
            // 如果当前模块是黑色
            ctx.fillRect(px * (4 + i) + offset, px * (4 + j) + offset, px, px); // 绘制黑色矩形
          }
        }
      }

      callBack && callBack(); // 如果提供了回调函数,则执行它
    }
  };
  module.exports = { api };
  // exports.draw = api;
})();

Enter fullscreen mode Exit fullscreen mode

qrcode-helper.ts:创建二维码的增强辅助函数,支持自定义样式和高清显示

// qrcode-helper.ts
/**
 * 创建二维码的增强辅助函数,支持自定义样式和高清显示
 * 
 * @param {Object} _this - 组件实例的this引用,用于上下文绑定
 * @param {string} qrCode - 需要生成二维码的字符串内容
 * @param {string} canvasId - canvas元素的ID标识符
 * @param {number} cavW - canvas的宽度(单位:px)
 * @param {number} cavH - canvas的高度(单位:px)
 * @param {Function} success - 成功回调函数,参数为生成的临时文件路径
 * @param {Function} fail - 失败回调函数,参数为错误信息对象
 * @param {Function} complete - 完成回调函数,无论成功失败都会执行
 * @param {HTMLCanvasElement} _Canvas - canvas DOM元素实例
 * @param {number} dpr - 设备像素比,用于处理高清屏幕显示
 * @param {string} [fillStyle] - 二维码前景色,可选参数,默认为黑色
 * @param {string} [fillBgStyle] - 二维码背景色,可选参数,默认为白色
 * 
 * @example
 *
 * newCreateQrCodeHelper(
 *   this,
 *   'https://example.com',
 *   'qrCanvas',
 *   200,
 *   200,
 *   (tempFilePath) => console.log('成功:', tempFilePath),
 *   (error) => console.error('失败:', error),
 *   () => console.log('完成'),
 *   canvasElement,
 *   2,
 *   '#000000',
 *   '#ffffff'
 * );
 * 
 * 
 * @description
 * 该函数主要用于在小程序环境中生成二维码,具有以下特点:
 * 1. 支持高清显示,通过dpr参数适配不同设备
 * 2. 可自定义二维码颜色样式
 * 3. 支持成功/失败/完成三种回调
 * 4. 仅支持微信小程序环境(WEAPP)
 * 
 * @throws {Error} 当运行环境不是微信小程序时,会通过fail回调返回错误
 * 
 * @returns {void}
 */
export function newCreateQrCodeHelper(
  _this,
  qrCode,
  canvasId,
  cavW,
  cavH,
  success,
  fail,
  complete,
  _Canvas,
  dpr,
  fillStyle,
  fillBgStyle
) {
  // 获取当前运行环境
  const env = Taro.getEnv();

  // 判断是否在微信小程序环境中
  if (env === 'WEAPP') {
    // 调用QR.api.newDraw方法绘制二维码
    QR.api.newDraw(
      Taro.createSelectorQuery,  // 创建选择器查询对象的函数
      qrCode,                    // 二维码内容
      canvasId,                  // Canvas ID
      cavW,                      // 画布宽度
      cavH,                      // 画布高度
      _this,                     // 组件实例引用
      null,                      // 二维码配置项,这里使用默认值
      // 绘制完成后的回调,将画布内容转换为图片
      () => newCanvasToTempFilePath(
        _Canvas,    // Canvas实例
        cavW,       // 宽度
        cavH,       // 高度
        success,    // 成功回调
        fail,       // 失败回调
        complete,   // 完成回调
        _this       // 组件实例引用
      ),
      _Canvas,      // Canvas实例
      dpr,          // 设备像素比
      fillStyle,    // 二维码前景色
      fillBgStyle   // 二维码背景色
    );
  } else {
    // 如果不是在微信小程序环境中,调用失败回调
    fail({ errorMessage: `不支持的平台:${env}` });
  }
}
Enter fullscreen mode Exit fullscreen mode

2. 海报组件实现

export default class InvitePoster extends Component {
  state = {
    isLoading: false,
    posterImage: '',
  }

  /**
   * 创建动态二维码并处理相关逻辑
   * @param str 需要生成二维码的字符串
   * @param canvasId canvas元素的ID
   * @param cavW canvas宽度
   * @param cavH canvas高度
   * @param retryCount 重试次数,默认3次
   */
  async createQrCode(str: string, canvasId: string, cavW: number, cavH: number, retryCount = 3) {
    // 检查重试次数
    if (retryCount <= 0) {
      console.error('二维码生成重试次数已用完');
      Taro.showToast({
        title: '生成失败,请重试',
        icon: 'none'
      });
      return;
    }

    let isDrawing = false;

    try {
      // 获取canvas实例
      const _canvas = await this.getCanvas(canvasId);
      const dpr = Taro.getSystemInfoSync().pixelRatio;

      // 生成二维码
      newCreateQrCodeHelper(
        this,
        str,
        canvasId,
        cavW,
        cavH,
        (tempFilePath: string) => {
          isDrawing = true;
          this.createPoster(tempFilePath);
        },
        (error: Error) => {
          this.handleQrCodeError(error, str, canvasId, cavW, cavH, retryCount);
        },
        () => {
          if (!isDrawing) {
            console.warn('二维码绘制可能未完成,请检查');
          }
        },
        _canvas,
        dpr,
        null,
        '#FEF5E0'
      );
    } catch (error) {
      this.handleCanvasError();
    }
  }

  /**
 * 创建分享海报
 * 该函数负责将二维码和背景图片合成为一张完整的分享海报
 * 
 * @param {string} qrcode - 已生成的二维码图片的临时路径
 * 
 * @description
 * 海报生成流程:
 * 1. 创建画布并设置尺寸(适配不同设备)
 * 2. 绘制背景图片
 * 3. 绘制二维码背景色
 * 4. 绘制二维码
 * 5. 将画布导出为图片
 * 
 * @example
 * 
 * this.createPoster('tempFilePath/qrcode.png');
 *
 */
createPoster(qrcode) {
  // 检查二维码参数
  console.log('qrcodeqrcodeqrcodeqrcode', qrcode);
  var _this = this;

  // 参数验证:确保二维码路径存在
  if (!qrcode) {
    console.error('二维码图片路径为空');
    Taro.showToast({
      title: '海报生成失败',
      icon: 'none'
    });
    return;
  }

  // 获取画布尺寸配置
  let size = this.setCanvasSize();

  // 获取设备像素比,用于高清适配
  const dpr = Taro.getSystemInfoSync().pixelRatio;

  // 获取画布上下文
  Taro.createSelectorQuery()
    .select('#poster') // 选择海报画布节点
    .node(({ node: canvas }) => {
      const context = canvas.getContext('2d');
      // 清空画布
      context.clearRect(0, 0, canvas.width, canvas.height);

      // 设置画布尺寸,考虑设备像素比
      canvas.width = windowWidth * dpr; // 画布宽度
      canvas.height = ((812 * windowWidth) / 375) * dpr; // 画布高度,保持宽高比
      context.scale(dpr, dpr); // 缩放上下文以适应高分辨率

      // 创建并加载背景图片
      const image1 = canvas.createImage();
      image1.onload = () => {
        // 绘制背景图片,适配屏幕宽度
        context.drawImage(image1, 0, 0, windowWidth, (812 * windowWidth) / 375);

        // 绘制二维码背景
        context.fillStyle = '#FEF5E0'; // 设置二维码背景色
        context.fillRect(size.qrX, size.qrY, size.qw, size.qh);

        // 保存当前绘图状态
        context.save();

        // 创建并加载二维码图片
        const image2 = canvas.createImage();
        image2.onload = () => {
          // 绘制二维码到指定位置
          context.drawImage(image2, size.qrX, size.qrY, size.qw, size.qh);

          // 延迟导出图片,确保绘制完成
          setTimeout(() => {
            // 将画布内容导出为图片
            wx.canvasToTempFilePath(
              {
                width: 750, // 输出图片宽度
                height: 1624, // 输出图片高度
                destWidth: 750 * 2, // 输出图片实际宽度(乘2用于高清显示)
                destHeight: 1624 * 2, // 输出图片实际高度
                canvas: canvas, // canvas实例,2D接口需要传入
                x: 0, // 裁剪起点x坐标
                y: 0, // 裁剪起点y坐标
                canvasId: 'poster', // 画布标识符
                success: function (res) {
                  // 导出成功,更新状态
                  console.log('海报成功:', res.tempFilePath);
                  console.log({ posterImage: res });
                  _this.setState({
                    posterImage: res.tempFilePath,
                    isLoading: false
                  });
                },
                fail: err => {
                  // 导出失败,提示重试
                  Taro.showModal({
                    title: '生成海报失败,请重试',
                    showCancel: false,
                    success: () => {
                      _this.createPoster(qrcode); // 失败后重试
                    }
                  });
                }
              },
              canvas
            );
          }, 200); // 给予200ms延迟确保绘制完成
        };
        // 设置二维码图片源
        image2.src = qrcode;
      };
      // 设置背景图片源,本地图片
      image1.src = require('./images/29.jpg');
    })
    .exec();
}

  /**
   * 保存到相册
   */
  saveToPhone = () => {
    Taro.getSetting({
      success: res => {
        if (!res.authSetting['scope.writePhotosAlbum']) {
          this.requestAlbumAuthorization();
        } else {
          this.downloadImage();
        }
      }
    });
  };
}
Enter fullscreen mode Exit fullscreen mode

3. 页面模板

const Index = () => {
    useEffect(()=>{
        let shareUrl = getGlobalData('europeanShareUrl');
    console.log('海报中的shareUrl', shareUrl);

    let size = this.setCanvasSize();

        // 开始绘制二维码
    setTimeout(() => this.createQrCode(shareUrl, 'mycanvas', size.w, size.h), 300);
    },[])

    return  <View className="invite-poster-page">
      {isLoading ? (
        <Loading type="modal" show={true} />
      ) : (
        <View className="container">
          <Image className="poster-image" src={posterImage} mode="aspectFill" />
          <View className="save-btn" onClick={this.saveToPhone}>
            保存到手机
          </View>
        </View>
      )}
      <Canvas className="qr-canvas" type="2d" id="mycanvas" />
      <Canvas className="poster-canvas" type="2d" id="poster" />
    </View>
}
Enter fullscreen mode Exit fullscreen mode

关键技术点说明

1. 画布适配

为了确保在不同设备上显示正常,我们需要处理好画布的尺寸适配:

setCanvasSize() {
  const systemInfo = Taro.getSystemInfoSync();
  const windowWidth = systemInfo.windowWidth;
  const designWidth = 750;
  const designQrSize = 200;

  const scale = windowWidth / designWidth;
  const qrSize = Math.round(designQrSize * scale);

  return {
    w: qrSize,
    h: qrSize,
    qw: qrSize,
    qh: qrSize,
    qrX: Math.round(275 * scale),
    qrY: Math.round(1280 * scale)
  };
}
Enter fullscreen mode Exit fullscreen mode

2. 权限处理

在保存图片到相册时,需要处理权限问题:

requestAlbumAuthorization() {
  Taro.authorize({
    scope: 'scope.writePhotosAlbum',
    success: () => {
      this.downloadImage();
    },
    fail: () => {
      Taro.showModal({
        content: '未授权,无法保存图片,请前往授权',
        success: res => {
          if (res.confirm) {
            Taro.openSetting();
          }
        }
      });
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

3. 错误处理

为了提高用户体验,我们需要做好错误处理和重试机制:

handleQrCodeError(error: Error, str: string, canvasId: string, cavW: number, cavH: number, retryCount: number) {
  console.error('二维码生成失败:', error);
  Taro.showToast({
    title: '二维码生成失败,正在重试',
    icon: 'none',
    duration: 2000
  });

  setTimeout(() => {
    this.createQrCode(str, canvasId, cavW, cavH, retryCount - 1);
  }, 1000);
}
Enter fullscreen mode Exit fullscreen mode

注意事项

  1. Canvas 必须使用 type="2d" 属性
  2. 需要处理设备像素比(dpr)以支持高清屏幕
  3. 图片绘制时需要给予足够的延时确保加载完成
  4. 要合理处理授权失败的情况
  5. 建议实现重试机制提高成功率

总结

通过以上实现,我们可以在小程序中生成美观的分享海报。关键在于:

  1. 使用 Canvas 2D API 进行绘制
  2. 处理好设备适配问题
  3. 实现完善的错误处理机制
  4. 注意权限和授权处理

AWS Q Developer image

Your AI Code Assistant

Automate your code reviews. Catch bugs before your coworkers. Fix security issues in your code. Built to handle large projects, Amazon Q Developer works alongside you from idea to production code.

Get started free in your IDE

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay