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

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

published_date
最新编辑 2025年03月17日
slug
了解如何在微信小程序中实现动态二维码海报的生成与保存,支持用户自定义内容、高效绘制与一键保存,适用于活动推广、营销分享等多种场景。
short_link
tags
小程序
WEBSITE:
GitHub:

前言

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

实现原理

整个功能的实现主要包含以下几个步骤:
  1. 生成二维码
  1. 绘制海报背景
  1. 将二维码绘制到海报上
  1. 将画布导出为图片
  1. 保存图片到相册

核心代码实现

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; })();
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 * ```typescript * 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}` }); } }

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 * ```typescript * 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(); } } }); }; }
 

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> }

关键技术点说明

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) }; }

2. 权限处理

在保存图片到相册时,需要处理权限问题:
requestAlbumAuthorization() { Taro.authorize({ scope: 'scope.writePhotosAlbum', success: () => { this.downloadImage(); }, fail: () => { Taro.showModal({ content: '未授权,无法保存图片,请前往授权', success: res => { if (res.confirm) { Taro.openSetting(); } } }); } }); }

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); }

注意事项

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

总结

通过以上实现,我们可以在小程序中生成美观的分享海报。关键在于:
  1. 使用 Canvas 2D API 进行绘制
  1. 处理好设备适配问题
  1. 实现完善的错误处理机制
  1. 注意权限和授权处理