From b1d7fc52fff0295402f067604a212ea9f56043b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=98=89=E7=94=B7?= <574980606@qq.com> Date: Thu, 9 Jan 2025 05:53:36 +0800 Subject: [PATCH] feat: value support string[] type --- src/QRCodeCanvas.tsx | 6 ++-- src/QRCodeSVG.tsx | 2 ++ src/hooks/useQRCode.tsx | 21 ++++++++--- src/interface.ts | 10 +++++- src/libs/qrcodegen.ts | 80 +++++++++++++++++++++++------------------ src/utils.tsx | 1 + 6 files changed, 78 insertions(+), 42 deletions(-) diff --git a/src/QRCodeCanvas.tsx b/src/QRCodeCanvas.tsx index fa1a236..a46bb2a 100644 --- a/src/QRCodeCanvas.tsx +++ b/src/QRCodeCanvas.tsx @@ -26,6 +26,7 @@ const QRCodeCanvas = React.forwardRef( marginSize, style, imageSettings, + boostLevel, ...otherProps } = props; const imgSrc = imageSettings?.src; @@ -54,6 +55,7 @@ const QRCodeCanvas = React.forwardRef( marginSize, imageSettings, size, + boostLevel, }); React.useEffect(() => { @@ -137,9 +139,7 @@ const QRCodeCanvas = React.forwardRef( src={imgSrc} key={imgSrc} style={{ display: 'none' }} - onLoad={() => { - setIsImageLoaded(true); - }} + onLoad={() => setIsImageLoaded(true)} ref={_image} // when crossOrigin is not set, the image will be tainted // and the canvas cannot be exported to an image diff --git a/src/QRCodeSVG.tsx b/src/QRCodeSVG.tsx index 8a2f899..e29818f 100644 --- a/src/QRCodeSVG.tsx +++ b/src/QRCodeSVG.tsx @@ -24,6 +24,7 @@ const QRCodeSVG = React.forwardRef((props, ref) => { title, marginSize, imageSettings, + boostLevel, ...otherProps } = props; @@ -35,6 +36,7 @@ const QRCodeSVG = React.forwardRef((props, ref) => { marginSize, imageSettings, size, + boostLevel, }); let cellsToDraw = cells; diff --git a/src/hooks/useQRCode.tsx b/src/hooks/useQRCode.tsx index 3af1a32..a20f4a6 100644 --- a/src/hooks/useQRCode.tsx +++ b/src/hooks/useQRCode.tsx @@ -4,13 +4,14 @@ import { ERROR_LEVEL_MAP, getImageSettings, getMarginSize } from '../utils'; import React from 'react'; interface Options { - value: string; + value: string | string[]; level: ErrorCorrectionLevel; minVersion: number; includeMargin: boolean; marginSize?: number; imageSettings?: ImageSettings; size: number; + boostLevel?: boolean; } export const useQRCode = (opt: Options) => { @@ -22,12 +23,24 @@ export const useQRCode = (opt: Options) => { marginSize, imageSettings, size, + boostLevel, } = opt; const memoizedQrcode = React.useMemo(() => { - const segments = QrSegment.makeSegments(value); - return QrCode.encodeSegments(segments, ERROR_LEVEL_MAP[level], minVersion); - }, [value, level, minVersion]); + const values = Array.isArray(value) ? value : [value]; + const segments = values.reduce((acc, v) => { + acc.push(...QrSegment.makeSegments(v)); + return acc; + }, []); + return QrCode.encodeSegments( + segments, + ERROR_LEVEL_MAP[level], + minVersion, + undefined, + undefined, + boostLevel, + ); + }, [value, level, minVersion, boostLevel]); return React.useMemo(() => { const cs = memoizedQrcode.getModules(); diff --git a/src/interface.ts b/src/interface.ts index e531041..0bd8af2 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -60,7 +60,7 @@ export type QRProps = { * The value to encode into the QR Code. An array of strings can be passed in * to represent multiple segments to further optimize the QR Code. */ - value: string; + value: string | string[]; /** * The size, in pixels, to render the QR Code. * @defaultValue 128 @@ -118,6 +118,14 @@ export type QRProps = { * @defaultValue 1 */ minVersion?: number; + + /** + * If enabled, the Error Correction Level of the result may be higher than + * the specified Error Correction Level option if it can be done without + * increasing the version. + * @defaultValue true + */ + boostLevel?: boolean; }; export type QRPropsCanvas = QRProps & diff --git a/src/libs/qrcodegen.ts b/src/libs/qrcodegen.ts index 864f0f0..b971b38 100644 --- a/src/libs/qrcodegen.ts +++ b/src/libs/qrcodegen.ts @@ -15,8 +15,9 @@ function appendBits(val: number, len: number, bb: number[]): void { let i = len - 1; i >= 0; i-- // Append bit by bit - ) + ) { bb.push((val >>> i) & 1); + } } // Returns true iff the i'th bit of x is set to 1. @@ -112,7 +113,9 @@ export class QrSegment { // can be converted to UTF-8 bytes and encoded as a byte mode segment. public static makeBytes(data: Readonly): QrSegment { const bb: number[] = []; - for (const b of data) appendBits(b, 8, bb); + for (const b of data) { + appendBits(b, 8, bb); + } return new QrSegment(Mode.BYTE, data.length, bb); } @@ -258,8 +261,9 @@ export class QrSegment { const str = encodeURI(input); const result: number[] = []; for (let i = 0; i < str.length; i++) { - if (str.charAt(i) != '%') result.push(str.charCodeAt(i)); - else { + if (str.charAt(i) != '%') { + result.push(str.charCodeAt(i)); + } else { result.push(parseInt(str.substring(i + 1, i + 3), 16)); i += 2; } @@ -315,7 +319,7 @@ export class QrCode { // bytes allowed is 2953. The smallest possible QR Code version is automatically chosen for the output. // The ECC level of the result may be higher than the ecl argument if it can be done without increasing the version. public static encodeBinary(data: Readonly, ecl: Ecc): QrCode { - const seg: QrSegment = QrSegment.makeBytes(data); + const seg = QrSegment.makeBytes(data); return QrCode.encodeSegments([seg], ecl); } @@ -346,23 +350,24 @@ export class QrCode { ) || mask < -1 || mask > 7 - ) + ) { throw new RangeError('Invalid value'); + } // Find the minimal version number to use let version: number; let dataUsedBits: number; for (version = minVersion; ; version++) { - const dataCapacityBits: number = - QrCode.getNumDataCodewords(version, oriEcl) * 8; // Number of data bits available + const dataCapacityBits = QrCode.getNumDataCodewords(version, oriEcl) * 8; // Number of data bits available const usedBits: number = QrSegment.getTotalBits(segs, version); if (usedBits <= dataCapacityBits) { dataUsedBits = usedBits; break; // This version number is found to be suitable } - if (version >= maxVersion) + if (version >= maxVersion) { // All versions in the range could not fit the given data throw new RangeError('Data too long'); + } } let ecl: Ecc = oriEcl; // Increase the error correction level while the data still fits in the current version number @@ -408,7 +413,9 @@ export class QrCode { while (dataCodewords.length * 8 < bb.length) { dataCodewords.push(0); } - bb.forEach((b, i) => (dataCodewords[i >>> 3] |= b << (7 - (i & 7)))); + bb.forEach((b, i) => { + dataCodewords[i >>> 3] |= b << (7 - (i & 7)); + }); // Create the QR Code object return new QrCode(version, ecl, dataCodewords, mask); @@ -460,8 +467,9 @@ export class QrCode { this.version = version; this.errorCorrectionLevel = errorCorrectionLevel; // Check scalar arguments - if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION) + if (version < QrCode.MIN_VERSION || version > QrCode.MAX_VERSION) { throw new RangeError('Version value out of range'); + } if (msk < -1 || msk > 7) { throw new RangeError('Mask value out of range'); } @@ -469,7 +477,9 @@ export class QrCode { // Initialize both grids to be size*size arrays of Boolean false const row: boolean[] = []; - for (let i = 0; i < this.size; i++) row.push(false); + for (let i = 0; i < this.size; i++) { + row.push(false); + } for (let i = 0; i < this.size; i++) { this.modules.push(row.slice()); // Initially all light this.isFunction.push(row.slice()); @@ -570,7 +580,9 @@ export class QrCode { assert(bits >>> 15 == 0); // Draw first copy - for (let i = 0; i <= 5; i++) this.setFunctionModule(8, i, getBit(bits, i)); + for (let i = 0; i <= 5; i++) { + this.setFunctionModule(8, i, getBit(bits, i)); + } this.setFunctionModule(8, 7, getBit(bits, 6)); this.setFunctionModule(8, 8, getBit(bits, 7)); this.setFunctionModule(7, 8, getBit(bits, 8)); @@ -658,19 +670,15 @@ export class QrCode { throw new RangeError('Invalid argument'); } // Calculate parameter numbers - const numBlocks: number = - QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver]; - const blockEccLen: number = - QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver]; - const rawCodewords: number = Math.floor( - QrCode.getNumRawDataModules(ver) / 8, - ); - const numShortBlocks: number = numBlocks - (rawCodewords % numBlocks); - const shortBlockLen: number = Math.floor(rawCodewords / numBlocks); + const numBlocks = QrCode.NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver]; + const blockEccLen = QrCode.ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver]; + const rawCodewords = Math.floor(QrCode.getNumRawDataModules(ver) / 8); + const numShortBlocks = numBlocks - (rawCodewords % numBlocks); + const shortBlockLen = Math.floor(rawCodewords / numBlocks); // Split data numbero blocks and append ECC to each block const blocks: number[][] = []; - const rsDiv: number[] = QrCode.reedSolomonComputeDivisor(blockEccLen); + const rsDiv = QrCode.reedSolomonComputeDivisor(blockEccLen); for (let i = 0, k = 0; i < numBlocks; i++) { const dat = data.slice( k, @@ -854,8 +862,9 @@ export class QrCode { // Balance of dark and light modules let dark: number = 0; - for (const row of this.modules) + for (const row of this.modules) { dark = row.reduce((sum, color) => sum + (color ? 1 : 0), dark); + } const total: number = this.size * this.size; // Note that size is odd, so dark/total != 1/2 // Compute the smallest numbereger k >= 0 such that (45-5k)% <= dark/total <= (55+5k)% const k: number = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1; @@ -874,14 +883,15 @@ export class QrCode { if (this.version == 1) { return []; } else { - const numAlign: number = Math.floor(this.version / 7) + 2; - const step: number = + const numAlign = Math.floor(this.version / 7) + 2; + const step = this.version == 32 ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2; const result: number[] = [6]; - for (let pos = this.size - 7; result.length < numAlign; pos -= step) + for (let pos = this.size - 7; result.length < numAlign; pos -= step) { result.splice(1, 0, pos); + } return result; } } @@ -925,7 +935,9 @@ export class QrCode { // Polynomial coefficients are stored from highest to lowest power, excluding the leading term which is always 1. // For example the polynomial x^3 + 255x^2 + 8x + 93 is stored as the unumber8 array [255, 8, 93]. const result: number[] = []; - for (let i = 0; i < degree - 1; i++) result.push(0); + for (let i = 0; i < degree - 1; i++) { + result.push(0); + } result.push(1); // Start off with the monomial x^0 // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x - r^{degree-1}), @@ -949,15 +961,15 @@ export class QrCode { private static reedSolomonComputeRemainder( data: Readonly, divisor: Readonly, - ): number[] { + ) { const result = divisor.map(() => 0); for (const b of data) { // Polynomial division - const factor: number = b ^ (result.shift() as number); + const factor = b ^ result.shift(); result.push(0); - divisor.forEach( - (coef, i) => (result[i] ^= QrCode.reedSolomonMultiply(coef, factor)), - ); + divisor.forEach((coef, i) => { + result[i] ^= QrCode.reedSolomonMultiply(coef, factor); + }); } return result; } @@ -1016,7 +1028,7 @@ export class QrCode { private finderPenaltyAddHistory( oriCurrentRunLength: number, runHistory: number[], - ): void { + ) { let currentRunLength = oriCurrentRunLength; if (runHistory[0] == 0) { currentRunLength += this.size; // Add light border to initial run diff --git a/src/utils.tsx b/src/utils.tsx index 8e8bf2d..e5d6f77 100644 --- a/src/utils.tsx +++ b/src/utils.tsx @@ -75,6 +75,7 @@ export const generatePath = (modules: Modules, margin: number = 0) => { }); return ops.join(''); }; + /** * Excavate modules * @param modules