wx-qr
Version:
微信小程序生成二维码组件,支持图标、底色、dotScale等各种配置
431 lines (430 loc) • 21.1 kB
JavaScript
import { getRoundNum, loadImage } from "../util";
import { QRCodeModel, QRErrorCorrectLevel, QRUtil } from "./qrcode";
const defaultScale = 0.4;
export class AwesomeQR {
canvas;
canvasContext;
qrCode;
options;
static CorrectLevel = QRErrorCorrectLevel;
static defaultComponentOptions = {
data: {
scale: 1,
},
timing: {
scale: 1,
protectors: false,
},
alignment: {
scale: 1,
protectors: false,
},
cornerAlignment: {
scale: 1,
protectors: true,
},
};
static defaultOptions = {
text: "",
size: 400,
margin: 20,
colorDark: "#000000",
colorLight: "#ffffff",
correctLevel: QRErrorCorrectLevel.M,
backgroundImage: undefined,
backgroundDimming: "rgba(0,0,0,0)",
logoImage: undefined,
logoScale: 0.2,
logoMargin: 4,
logoCornerRadius: 8,
whiteMargin: true,
components: AwesomeQR.defaultComponentOptions,
autoColor: true,
};
constructor(options) {
this.setOptions(options);
// this.canvas = new Canvas(options.size!, options.size!);
}
draw() {
return new Promise((resolve) => this._draw().then(resolve));
}
setOptions(options) {
const _options = Object.assign({}, options);
Object.keys(AwesomeQR.defaultOptions).forEach((k) => {
if (!(k in _options)) {
Object.defineProperty(_options, k, { value: AwesomeQR.defaultOptions[k], enumerable: true, writable: true });
}
});
if (!_options.components) {
_options.components = AwesomeQR.defaultComponentOptions;
}
else if (typeof _options.components === "object") {
Object.keys(AwesomeQR.defaultComponentOptions).forEach((k) => {
if (!(k in _options.components)) {
Object.defineProperty(_options.components, k, {
value: AwesomeQR.defaultComponentOptions[k],
enumerable: true,
writable: true,
});
}
else {
Object.defineProperty(_options.components, k, {
value: { ...AwesomeQR.defaultComponentOptions[k], ..._options.components[k] },
enumerable: true,
writable: true,
});
}
});
}
if (_options.dotScale !== null && _options.dotScale !== undefined) {
if (_options.dotScale <= 0 || _options.dotScale > 1) {
throw new Error("dotScale should be in range (0, 1].");
}
_options.components.data.scale = _options.dotScale;
_options.components.timing.scale = _options.dotScale;
_options.components.alignment.scale = _options.dotScale;
}
this.options = _options;
this.canvas = options.canvasContainer.qrMainContainer;
this.canvasContext = this.canvas.getContext("2d");
this.qrCode = new QRCodeModel(-1, this.options.correctLevel);
if (Number.isInteger(this.options.maskPattern)) {
this.qrCode.maskPattern = this.options.maskPattern;
}
if (Number.isInteger(this.options.version)) {
this.qrCode.typeNumber = this.options.version;
}
this.qrCode.addData(this.options.text);
this.qrCode.make();
return _options;
}
_clear() {
this.canvasContext.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
static _prepareRoundedCornerClip(canvasContext, x, y, w, h, r) {
canvasContext.beginPath();
canvasContext.moveTo(x, y);
canvasContext.arcTo(x + w, y, x + w, y + h, r);
canvasContext.arcTo(x + w, y + h, x, y + h, r);
canvasContext.arcTo(x, y + h, x, y, r);
canvasContext.arcTo(x, y, x + w, y, r);
canvasContext.closePath();
}
static _prepareRoundedCornerClipReverse(canvasContext, x, y, w, h, r, size) {
canvasContext.beginPath();
canvasContext.lineTo(0, 0);
canvasContext.lineTo(0, size);
canvasContext.lineTo(size, size);
canvasContext.lineTo(size, 0);
canvasContext.lineTo(0, 0);
canvasContext.lineTo(x, y);
canvasContext.arcTo(x + w, y, x + w, y + h, r);
canvasContext.arcTo(x + w, y + h, x, y + h, r);
canvasContext.arcTo(x, y + h, x, y, r);
canvasContext.arcTo(x, y, x + w, y, r);
canvasContext.closePath();
}
static _getAverageRGB(image, options) {
const blockSize = 5;
const defaultRGB = {
r: 0,
g: 0,
b: 0,
};
let width, height;
let i = -4;
const rgb = {
r: 0,
g: 0,
b: 0,
};
let count = 0;
// @ts-ignore
height = image.naturalHeight || image.height;
// @ts-ignore
width = image.naturalWidth || image.width;
const canvas = options.canvasContainer.qrMainContainer;
const context = canvas.getContext("2d");
if (!context) {
return defaultRGB;
}
let data;
try {
data = context.getImageData(0, 0, width, height);
}
catch (e) {
return defaultRGB;
}
while ((i += blockSize * 4) < data.data.length) {
if (data.data[i] > 200 || data.data[i + 1] > 200 || data.data[i + 2] > 200)
continue;
++count;
rgb.r += data.data[i];
rgb.g += data.data[i + 1];
rgb.b += data.data[i + 2];
}
rgb.r = ~~(rgb.r / count);
rgb.g = ~~(rgb.g / count);
rgb.b = ~~(rgb.b / count);
return rgb;
}
static _drawDot(canvasContext, centerX, centerY, nSize, xyOffset = 0, dotScale = 1) {
canvasContext.fillRect((centerX + xyOffset) * nSize, (centerY + xyOffset) * nSize, dotScale * nSize, dotScale * nSize);
}
static _drawAlignProtector(canvasContext, centerX, centerY, nSize) {
canvasContext.clearRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize);
canvasContext.fillRect((centerX - 2) * nSize, (centerY - 2) * nSize, 5 * nSize, 5 * nSize);
}
static _drawAlign(canvasContext, centerX, centerY, nSize, xyOffset = 0, dotScale = 1, colorDark, hasProtector) {
const oldFillStyle = canvasContext.fillStyle;
canvasContext.fillStyle = colorDark;
new Array(4).fill(0).map((_, i) => {
AwesomeQR._drawDot(canvasContext, centerX - 2 + i, centerY - 2, nSize, xyOffset, dotScale);
AwesomeQR._drawDot(canvasContext, centerX + 2, centerY - 2 + i, nSize, xyOffset, dotScale);
AwesomeQR._drawDot(canvasContext, centerX + 2 - i, centerY + 2, nSize, xyOffset, dotScale);
AwesomeQR._drawDot(canvasContext, centerX - 2, centerY + 2 - i, nSize, xyOffset, dotScale);
});
AwesomeQR._drawDot(canvasContext, centerX, centerY, nSize, xyOffset, dotScale);
if (!hasProtector) {
canvasContext.fillStyle = "rgba(255, 255, 255, 0.6)";
new Array(2).fill(0).map((_, i) => {
AwesomeQR._drawDot(canvasContext, centerX - 1 + i, centerY - 1, nSize, xyOffset, dotScale);
AwesomeQR._drawDot(canvasContext, centerX + 1, centerY - 1 + i, nSize, xyOffset, dotScale);
AwesomeQR._drawDot(canvasContext, centerX + 1 - i, centerY + 1, nSize, xyOffset, dotScale);
AwesomeQR._drawDot(canvasContext, centerX - 1, centerY + 1 - i, nSize, xyOffset, dotScale);
});
}
canvasContext.fillStyle = oldFillStyle;
}
async _draw() {
const nCount = this.qrCode?.moduleCount;
const rawSize = this.options.size;
let rawMargin = this.options.margin;
if (rawMargin < 0 || rawMargin * 2 >= rawSize) {
rawMargin = 0;
}
const margin = Math.ceil(rawMargin);
const rawViewportSize = rawSize - 2 * rawMargin;
const whiteMargin = this.options.whiteMargin;
const backgroundDimming = this.options.backgroundDimming;
const nSize = getRoundNum(rawViewportSize / nCount, 3);
const viewportSize = nSize * nCount;
let size = getRoundNum(viewportSize + 2 * margin, 3);
// console.log({ rawViewportSize, size, correctLevel: this.options.correctLevel, nCount })
// const mainCanvas = new Canvas(size, size);
const mainCanvas = this.options.canvasContainer.qrMainContainer;
const mainCanvasContext = mainCanvas.getContext("2d");
this._clear();
// Fill the margin
if (whiteMargin) {
mainCanvasContext.save();
mainCanvasContext.fillStyle = "#FFFFFF";
mainCanvasContext.fillRect(0, 0, mainCanvas.width, mainCanvas.height);
mainCanvasContext.restore();
}
// Translate to make the top and left margins off the viewport
mainCanvasContext.save();
mainCanvasContext.translate(margin, margin);
// const backgroundCanvas = new Canvas(size, size);
const backgroundCanvas = this.options.canvasContainer.qrMainContainer;
const backgroundCanvasContext = backgroundCanvas.getContext("2d");
let backgroundImage;
if (!!this.options.backgroundImage) {
backgroundImage = await loadImage(backgroundCanvas, this.options.backgroundImage);
backgroundCanvasContext.drawImage(
// @ts-ignore
backgroundImage, 0, 0, backgroundImage.width, backgroundImage.height, 0, 0, rawViewportSize, rawViewportSize);
if (this.options.autoColor) {
const avgRGB = AwesomeQR._getAverageRGB(backgroundImage, this.options);
this.options.colorDark = `rgb(${avgRGB.r},${avgRGB.g},${avgRGB.b})`;
}
backgroundCanvasContext.rect(-margin, -margin, size, size);
backgroundCanvasContext.fillStyle = backgroundDimming;
backgroundCanvasContext.fill();
}
else {
backgroundCanvasContext.rect(-margin, -margin, size, size);
backgroundCanvasContext.fillStyle = this.options.colorLight;
backgroundCanvasContext.fill();
}
const alignmentPatternCenters = QRUtil.getPatternPosition(this.qrCode.typeNumber);
const dataScale = this.options.components?.data?.scale || defaultScale;
const dataXyOffset = (1 - dataScale) * 0.5;
// 提前预备好logo margin的空
if (!!this.options.logoImage && this.options.logoMargin) {
let logoMargin = this.options.logoMargin;
let logoScale = this.options.logoScale;
let logoCornerRadius = this.options.logoCornerRadius;
const logoSize = viewportSize * logoScale;
const x = 0.5 * (size - logoSize);
const y = x;
// mainCanvasContext.save();
AwesomeQR._prepareRoundedCornerClipReverse(mainCanvasContext, x - logoMargin - rawMargin, y - logoMargin - rawMargin, logoSize + 2 * logoMargin, logoSize + 2 * logoMargin, logoCornerRadius + logoMargin, size);
mainCanvasContext.fill();
// return
// mainCanvasContext.globalCompositeOperation = "destination-over";
mainCanvasContext.clip();
}
for (let row = 0; row < nCount; row++) {
for (let col = 0; col < nCount; col++) {
const bIsDark = this.qrCode.isDark(row, col);
const isBlkPosCtr = (col < 8 && (row < 8 || row >= nCount - 8)) || (col >= nCount - 8 && row < 8);
const isTiming = (row == 6 && col >= 8 && col <= nCount - 8) || (col == 6 && row >= 8 && row <= nCount - 8);
let isProtected = isBlkPosCtr || isTiming;
for (let i = 1; i < alignmentPatternCenters.length - 1; i++) {
isProtected =
isProtected ||
(row >= alignmentPatternCenters[i] - 2 &&
row <= alignmentPatternCenters[i] + 2 &&
col >= alignmentPatternCenters[i] - 2 &&
col <= alignmentPatternCenters[i] + 2);
}
const nLeft = col * nSize + (isProtected ? 0 : dataXyOffset * nSize);
const nTop = row * nSize + (isProtected ? 0 : dataXyOffset * nSize);
mainCanvasContext.strokeStyle = bIsDark ? this.options.colorDark : this.options.colorLight;
mainCanvasContext.lineWidth = 0.5;
mainCanvasContext.fillStyle = bIsDark ? this.options.colorDark : "rgba(255, 255, 255, 0.6)";
if (alignmentPatternCenters.length === 0) {
if (!isProtected) {
mainCanvasContext.fillRect(nLeft, nTop, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize);
}
}
else {
const inAgnRange = col < nCount - 4 && col >= nCount - 4 - 5 && row < nCount - 4 && row >= nCount - 4 - 5;
if (!isProtected && !inAgnRange) {
// if align pattern list is empty, then it means that we don't need to leave room for the align patterns
mainCanvasContext.fillRect(nLeft, nTop, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize, (isProtected ? (isBlkPosCtr ? 1 : 1) : dataScale) * nSize);
}
}
}
}
const cornerAlignmentCenter = alignmentPatternCenters[alignmentPatternCenters.length - 1];
// - PROTECTORS
const protectorStyle = "rgba(255, 255, 255, 0.6)";
// - FINDER PROTECTORS
mainCanvasContext.fillStyle = protectorStyle;
mainCanvasContext.fillRect(0, 0, 8 * nSize, 8 * nSize);
mainCanvasContext.fillRect(0, (nCount - 8) * nSize, 8 * nSize, 8 * nSize);
mainCanvasContext.fillRect((nCount - 8) * nSize, 0, 8 * nSize, 8 * nSize);
// - TIMING PROTECTORS
if (this.options.components?.timing?.protectors) {
mainCanvasContext.fillRect(8 * nSize, 6 * nSize, (nCount - 8 - 8) * nSize, nSize);
mainCanvasContext.fillRect(6 * nSize, 8 * nSize, nSize, (nCount - 8 - 8) * nSize);
}
// - CORNER ALIGNMENT PROTECTORS
if (this.options.components?.cornerAlignment?.protectors) {
AwesomeQR._drawAlignProtector(mainCanvasContext, cornerAlignmentCenter, cornerAlignmentCenter, nSize);
}
// - ALIGNMENT PROTECTORS
if (this.options.components?.alignment?.protectors) {
for (let i = 0; i < alignmentPatternCenters.length; i++) {
for (let j = 0; j < alignmentPatternCenters.length; j++) {
const agnX = alignmentPatternCenters[j];
const agnY = alignmentPatternCenters[i];
if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) {
continue;
}
else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) {
continue;
}
else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) {
continue;
}
else {
AwesomeQR._drawAlignProtector(mainCanvasContext, agnX, agnY, nSize);
}
}
}
}
// - FINDER
mainCanvasContext.fillStyle = this.options.colorDark;
mainCanvasContext.fillRect(0, 0, 7 * nSize, nSize);
mainCanvasContext.fillRect((nCount - 7) * nSize, 0, 7 * nSize, nSize);
mainCanvasContext.fillRect(0, 6 * nSize, 7 * nSize, nSize);
mainCanvasContext.fillRect((nCount - 7) * nSize, 6 * nSize, 7 * nSize, nSize);
mainCanvasContext.fillRect(0, (nCount - 7) * nSize, 7 * nSize, nSize);
mainCanvasContext.fillRect(0, (nCount - 7 + 6) * nSize, 7 * nSize, nSize);
mainCanvasContext.fillRect(0, 0, nSize, 7 * nSize);
mainCanvasContext.fillRect(6 * nSize, 0, nSize, 7 * nSize);
mainCanvasContext.fillRect((nCount - 7) * nSize, 0, nSize, 7 * nSize);
mainCanvasContext.fillRect((nCount - 7 + 6) * nSize, 0, nSize, 7 * nSize);
mainCanvasContext.fillRect(0, (nCount - 7) * nSize, nSize, 7 * nSize);
mainCanvasContext.fillRect(6 * nSize, (nCount - 7) * nSize, nSize, 7 * nSize);
mainCanvasContext.fillRect(2 * nSize, 2 * nSize, 3 * nSize, 3 * nSize);
mainCanvasContext.fillRect((nCount - 7 + 2) * nSize, 2 * nSize, 3 * nSize, 3 * nSize);
mainCanvasContext.fillRect(2 * nSize, (nCount - 7 + 2) * nSize, 3 * nSize, 3 * nSize);
// - TIMING
const timingScale = this.options.components?.timing?.scale || defaultScale;
const timingXyOffset = (1 - timingScale) * 0.5;
for (let i = 0; i < nCount - 8; i += 2) {
AwesomeQR._drawDot(mainCanvasContext, 8 + i, 6, nSize, timingXyOffset, timingScale);
AwesomeQR._drawDot(mainCanvasContext, 6, 8 + i, nSize, timingXyOffset, timingScale);
}
// - CORNER ALIGNMENT PROTECTORS
const cornerAlignmentScale = this.options.components?.cornerAlignment?.scale || defaultScale;
const cornerAlignmentXyOffset = (1 - cornerAlignmentScale) * 0.5;
AwesomeQR._drawAlign(mainCanvasContext, cornerAlignmentCenter, cornerAlignmentCenter, nSize, cornerAlignmentXyOffset, cornerAlignmentScale, this.options.colorDark, this.options.components?.cornerAlignment?.protectors || false);
// - ALIGNEMNT
const alignmentScale = this.options.components?.alignment?.scale || defaultScale;
const alignmentXyOffset = (1 - alignmentScale) * 0.5;
for (let i = 0; i < alignmentPatternCenters.length; i++) {
for (let j = 0; j < alignmentPatternCenters.length; j++) {
const agnX = alignmentPatternCenters[j];
const agnY = alignmentPatternCenters[i];
if (agnX === 6 && (agnY === 6 || agnY === cornerAlignmentCenter)) {
continue;
}
else if (agnY === 6 && (agnX === 6 || agnX === cornerAlignmentCenter)) {
continue;
}
else if (agnX === cornerAlignmentCenter && agnY === cornerAlignmentCenter) {
continue;
}
else {
AwesomeQR._drawAlign(mainCanvasContext, agnX, agnY, nSize, alignmentXyOffset, alignmentScale, this.options.colorDark, this.options.components?.alignment?.protectors || false);
}
}
}
// mainCanvasContext.lineTo(1, 1, 10, 10)
if (!!this.options.logoImage) {
const logoImage = await loadImage(mainCanvas, this.options.logoImage);
let logoScale = this.options.logoScale;
let logoMargin = this.options.logoMargin;
let logoCornerRadius = this.options.logoCornerRadius;
if (logoScale <= 0 || logoScale >= 1.0) {
logoScale = 0.2;
}
if (logoMargin < 0) {
logoMargin = 0;
}
if (logoCornerRadius < 0) {
logoCornerRadius = 0;
}
const logoSize = viewportSize * logoScale;
const x = 0.5 * (size - logoSize);
const y = x;
mainCanvasContext.restore();
// @ts-ignore
mainCanvasContext.drawImage(logoImage, x, y, logoSize, logoSize);
}
this.qrCode = undefined;
this.canvas = mainCanvas;
return new Promise((reslove, reject) => {
wx.canvasToTempFilePath({
canvas: this.canvas,
quality: 1,
destWidth: this.canvas.width,
destHeight: this.canvas.height
}).then(rsp => {
reslove(rsp.tempFilePath);
}).catch(err => {
console.error('canvasToTempFilePath 失败', err);
reject(err);
});
});
// Promise.resolve(this.canvas.toDataURL(format, 1));
}
getDataUrl(type = 'png', encoderOptions = 1) {
return this.canvas.toDataURL(type, encoderOptions);
}
}