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