/** * A minimal QR Code generator implementation * Based on qrcode-generator by Kazuhiko Arase */ type QRMode = 1 | 2 | 4 | 8; type QRErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H'; interface QRBitBuffer { buffer: number[]; length: number; get(index: number): boolean; put(num: number, length: number): void; putBit(bit: boolean): void; } export class QRCodeModel { typeNumber: number; errorCorrectionLevel: QRErrorCorrectionLevel; modules: (boolean | null)[][] = []; moduleCount: number = 0; dataCache: number[] | null = null; dataList: { mode: QRMode; data: string }[] = []; constructor(typeNumber: number, errorCorrectionLevel: QRErrorCorrectionLevel) { this.typeNumber = typeNumber; this.errorCorrectionLevel = errorCorrectionLevel; } addData(data: string): void { const mode = 4; // Byte mode this.dataList.push({ mode, data }); this.dataCache = null; } isDark(row: number, col: number): boolean { if (row < 0 || this.moduleCount <= row || col < 0 || this.moduleCount <= col) { throw new Error(row + ',' + col); } return this.modules[row][col] || false; } getModuleCount(): number { return this.moduleCount; } make(): void { if (this.typeNumber < 1) { this.typeNumber = this.getBestTypeNumber(); } this.makeImpl(false, this.getBestMaskPattern()); } private makeImpl(test: boolean, maskPattern: number): void { this.moduleCount = this.typeNumber * 4 + 17; this.modules = new Array(this.moduleCount); for (let row = 0; row < this.moduleCount; row++) { this.modules[row] = new Array(this.moduleCount); for (let col = 0; col < this.moduleCount; col++) { this.modules[row][col] = null; } } this.setupPositionProbePattern(0, 0); this.setupPositionProbePattern(this.moduleCount - 7, 0); this.setupPositionProbePattern(0, this.moduleCount - 7); this.setupPositionAdjustPattern(); this.setupTimingPattern(); this.setupTypeInfo(test, maskPattern); if (this.typeNumber >= 7) { this.setupTypeNumber(test); } if (this.dataCache == null) { this.dataCache = QRCodeModel.createData( this.typeNumber, this.errorCorrectionLevel, this.dataList ); } this.mapData(this.dataCache, maskPattern); } private setupPositionProbePattern(row: number, col: number): void { for (let r = -1; r <= 7; r++) { if (row + r <= -1 || this.moduleCount <= row + r) continue; for (let c = -1; c <= 7; c++) { if (col + c <= -1 || this.moduleCount <= col + c) continue; if ( (0 <= r && r <= 6 && (c === 0 || c === 6)) || (0 <= c && c <= 6 && (r === 0 || r === 6)) || (2 <= r && r <= 4 && 2 <= c && c <= 4) ) { this.modules[row + r][col + c] = true; } else { this.modules[row + r][col + c] = false; } } } } private getBestMaskPattern(): number { let minLostPoint = 0; let pattern = 0; for (let i = 0; i < 8; i++) { this.makeImpl(true, i); const lostPoint = QRUtil.getLostPoint(this); if (i === 0 || minLostPoint > lostPoint) { minLostPoint = lostPoint; pattern = i; } } return pattern; } private setupTimingPattern(): void { for (let i = 8; i < this.moduleCount - 8; i++) { if (this.modules[i][6] != null) continue; this.modules[i][6] = i % 2 === 0; } for (let i = 8; i < this.moduleCount - 8; i++) { if (this.modules[6][i] != null) continue; this.modules[6][i] = i % 2 === 0; } } private setupPositionAdjustPattern(): void { const pos = QRUtil.getPatternPosition(this.typeNumber); for (let i = 0; i < pos.length; i++) { for (let j = 0; j < pos.length; j++) { const row = pos[i]; const col = pos[j]; if (this.modules[row][col] != null) continue; for (let r = -2; r <= 2; r++) { for (let c = -2; c <= 2; c++) { if (Math.abs(r) === 2 || Math.abs(c) === 2 || (r === 0 && c === 0)) { this.modules[row + r][col + c] = true; } else { this.modules[row + r][col + c] = false; } } } } } } private setupTypeNumber(test: boolean): void { const bits = QRUtil.getBCHTypeNumber(this.typeNumber); for (let i = 0; i < 18; i++) { const mod = !test && ((bits >> i) & 1) === 1; this.modules[Math.floor(i / 3)][(i % 3) + this.moduleCount - 8 - 3] = mod; } for (let i = 0; i < 18; i++) { const mod = !test && ((bits >> i) & 1) === 1; this.modules[(i % 3) + this.moduleCount - 8 - 3][Math.floor(i / 3)] = mod; } } private setupTypeInfo(test: boolean, maskPattern: number): void { const data = (QRUtil.getErrorCorrectionLevelValue(this.errorCorrectionLevel) << 3) | maskPattern; const bits = QRUtil.getBCHTypeInfo(data); // vertical for (let i = 0; i < 15; i++) { const mod = !test && ((bits >> i) & 1) === 1; if (i < 6) { this.modules[i][8] = mod; } else if (i < 7) { this.modules[7][8] = mod; } else { this.modules[this.moduleCount - 15 + i][8] = mod; } } // horizontal for (let i = 0; i < 15; i++) { const mod = !test && ((bits >> i) & 1) === 1; if (i < 8) { this.modules[8][this.moduleCount - i - 1] = mod; } else if (i < 9) { this.modules[8][15 - i - 1 + 1] = mod; } else { this.modules[8][15 - i - 1] = mod; } } // fixed module this.modules[this.moduleCount - 8][8] = !test; } private mapData(data: number[], maskPattern: number): void { let inc = -1; let row = this.moduleCount - 1; let bitIndex = 7; let byteIndex = 0; for (let col = this.moduleCount - 1; col > 0; col -= 2) { if (col === 6) col--; while (true) { for (let c = 0; c < 2; c++) { if (this.modules[row][col - c] == null) { let dark = false; if (byteIndex < data.length) { dark = ((data[byteIndex] >>> bitIndex) & 1) === 1; } const mask = QRUtil.getMask(maskPattern, row, col - c); if (mask) { dark = !dark; } this.modules[row][col - c] = dark; bitIndex--; if (bitIndex === -1) { byteIndex++; bitIndex = 7; } } } row += inc; if (row < 0 || this.moduleCount <= row) { row -= inc; inc = -inc; break; } } } } private getBestTypeNumber(): number { for (let i = 1; i < 40; i++) { const rsBlocks = QRUtil.getRSBlocks(i, this.errorCorrectionLevel); const buffer = new QRBitBufferImpl(); let totalDataCount = 0; for (let j = 0; j < rsBlocks.length; j++) { totalDataCount += rsBlocks[j].dataCount; } for (let j = 0; j < this.dataList.length; j++) { const data = this.dataList[j]; buffer.put(data.mode, 4); buffer.put(data.data.length, QRUtil.getLengthInBits(data.mode, i)); for (let k = 0; k < data.data.length; k++) { buffer.put(data.data.charCodeAt(k), 8); } } if (buffer.length <= totalDataCount * 8) return i; } return 40; } static createData( typeNumber: number, errorCorrectionLevel: QRErrorCorrectionLevel, dataList: { mode: QRMode; data: string }[] ): number[] { const rsBlocks = QRUtil.getRSBlocks(typeNumber, errorCorrectionLevel); const buffer = new QRBitBufferImpl(); for (let i = 0; i < dataList.length; i++) { const data = dataList[i]; buffer.put(data.mode, 4); buffer.put(data.data.length, QRUtil.getLengthInBits(data.mode, typeNumber)); for (let j = 0; j < data.data.length; j++) { buffer.put(data.data.charCodeAt(j), 8); } } let totalDataCount = 0; for (let i = 0; i < rsBlocks.length; i++) { totalDataCount += rsBlocks[i].dataCount; } if (buffer.length > totalDataCount * 8) { throw new Error('code length overflow. (' + buffer.length + '>' + totalDataCount * 8 + ')'); } if (buffer.length + 4 <= totalDataCount * 8) { buffer.put(0, 4); } while (buffer.length % 8 !== 0) { buffer.putBit(false); } while (true) { if (buffer.length >= totalDataCount * 8) break; buffer.put(0xec, 8); if (buffer.length >= totalDataCount * 8) break; buffer.put(0x11, 8); } return QRCodeModel.createBytes(buffer, rsBlocks); } static createBytes(buffer: QRBitBuffer, rsBlocks: QRRSBlock[]): number[] { let offset = 0; let maxDcCount = 0; let maxEcCount = 0; const dcdata = new Array(rsBlocks.length); const ecdata = new Array(rsBlocks.length); for (let r = 0; r < rsBlocks.length; r++) { const dcCount = rsBlocks[r].dataCount; const ecCount = rsBlocks[r].totalCount - dcCount; maxDcCount = Math.max(maxDcCount, dcCount); maxEcCount = Math.max(maxEcCount, ecCount); dcdata[r] = new Array(dcCount); for (let i = 0; i < dcdata[r].length; i++) { dcdata[r][i] = 0xff & buffer.buffer[i + offset]; } offset += dcCount; const rsPoly = QRUtil.getErrorCorrectionPolynomial(ecCount); const rawPoly = new QRPolynomial(dcdata[r], rsPoly.getLength() - 1); const modPoly = rawPoly.mod(rsPoly); ecdata[r] = new Array(rsPoly.getLength() - 1); for (let i = 0; i < ecdata[r].length; i++) { const modIndex = i + modPoly.getLength() - ecdata[r].length; ecdata[r][i] = modIndex >= 0 ? modPoly.get(modIndex) : 0; } } let totalCodeCount = 0; for (let i = 0; i < rsBlocks.length; i++) { totalCodeCount += rsBlocks[i].totalCount; } const data = new Array(totalCodeCount); let index = 0; for (let i = 0; i < maxDcCount; i++) { for (let r = 0; r < rsBlocks.length; r++) { if (i < dcdata[r].length) { data[index++] = dcdata[r][i]; } } } for (let i = 0; i < maxEcCount; i++) { for (let r = 0; r < rsBlocks.length; r++) { if (i < ecdata[r].length) { data[index++] = ecdata[r][i]; } } } return data; } } class QRPolynomial { num: number[]; constructor(num: number[], shift: number) { let offset = 0; while (offset < num.length && num[offset] === 0) { offset++; } this.num = new Array(num.length - offset + shift); for (let i = 0; i < num.length - offset; i++) { this.num[i] = num[i + offset]; } } get(index: number): number { return this.num[index]; } getLength(): number { return this.num.length; } multiply(e: QRPolynomial): QRPolynomial { const num = new Array(this.getLength() + e.getLength() - 1); for (let i = 0; i < this.getLength(); i++) { for (let j = 0; j < e.getLength(); j++) { num[i + j] ^= QRUtil.gexp(QRUtil.glog(this.get(i)) + QRUtil.glog(e.get(j))); } } return new QRPolynomial(num, 0); } mod(e: QRPolynomial): QRPolynomial { if (this.getLength() - e.getLength() < 0) return this; const ratio = QRUtil.glog(this.get(0)) - QRUtil.glog(e.get(0)); const num = new Array(this.getLength()); for (let i = 0; i < this.getLength(); i++) { num[i] = this.get(i); } for (let i = 0; i < e.getLength(); i++) { num[i] ^= QRUtil.gexp(QRUtil.glog(e.get(i)) + ratio); } return new QRPolynomial(num, 0).mod(e); } } class QRRSBlock { totalCount: number; dataCount: number; constructor(totalCount: number, dataCount: number) { this.totalCount = totalCount; this.dataCount = dataCount; } static getRSBlocks(typeNumber: number, errorCorrectionLevel: QRErrorCorrectionLevel): QRRSBlock[] { const list = QRRSBlock.getRsBlockTable(typeNumber, errorCorrectionLevel); if (list == null) { throw new Error('bad rs block @ typeNumber:' + typeNumber + '/errorCorrectionLevel:' + errorCorrectionLevel); } const length = list.length / 3; const rsBlocks: QRRSBlock[] = []; for (let i = 0; i < length; i++) { const count = list[i * 3 + 0]; const totalCount = list[i * 3 + 1]; const dataCount = list[i * 3 + 2]; for (let j = 0; j < count; j++) { rsBlocks.push(new QRRSBlock(totalCount, dataCount)); } } return rsBlocks; } static getRsBlockTable(typeNumber: number, errorCorrectionLevel: QRErrorCorrectionLevel): number[] | null { switch (errorCorrectionLevel) { case 'L': return QRRSBlock.rsBlockTableL[typeNumber - 1]; case 'M': return QRRSBlock.rsBlockTableM[typeNumber - 1]; case 'Q': return QRRSBlock.rsBlockTableQ[typeNumber - 1]; case 'H': return QRRSBlock.rsBlockTableH[typeNumber - 1]; default: return null; } } static rsBlockTableL = [ [1, 26, 19], [1, 44, 34], [1, 70, 55], [1, 100, 80], [1, 134, 108], [2, 86, 68], [2, 98, 78], [2, 121, 97], [2, 146, 116], [2, 192, 156], [2, 259, 213], [2, 332, 274], [4, 193, 159], [2, 339, 279], [2, 423, 349], [4, 311, 255], [4, 415, 341], [4, 535, 439], [4, 603, 497], [4, 666, 548] ]; static rsBlockTableM = [ [1, 26, 16], [1, 44, 28], [1, 70, 44], [2, 50, 32], [2, 64, 48], [4, 43, 27], [4, 49, 31], [4, 60, 38], [4, 73, 46], [4, 96, 60], [4, 119, 74], [4, 137, 86], [4, 155, 98], [4, 177, 110], [6, 155, 95], [6, 193, 119], [6, 230, 142], [6, 253, 156], [7, 283, 177], [8, 341, 213] ]; static rsBlockTableQ = [ [1, 26, 13], [1, 44, 22], [2, 35, 17], [2, 50, 24], [2, 81, 43], [4, 50, 26], [6, 50, 26], [6, 53, 29], [8, 61, 31], [8, 72, 36], [8, 91, 45], [10, 93, 46], [12, 107, 53], [12, 115, 54], [12, 131, 62], [14, 151, 71], [14, 175, 83], [16, 211, 100], [18, 239, 113], [21, 270, 128] ]; static rsBlockTableH = [ [1, 26, 9], [1, 44, 16], [2, 35, 13], [4, 25, 9], [2, 33, 15], [4, 24, 11], [4, 28, 13], [4, 33, 15], [4, 39, 17], [4, 45, 19], [4, 58, 25], [6, 69, 29], [6, 74, 31], [8, 86, 36], [10, 95, 39], [12, 116, 48], [14, 140, 58], [16, 168, 70], [17, 197, 83], [19, 224, 94] ]; } const QRUtil = { PATTERN_POSITION_TABLE: [ [], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90] ], G15: (1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) | (1 << 0), G18: (1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) | (1 << 2) | (1 << 0), G15_MASK: (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1), getBCHTypeInfo(data: number): number { let d = data << 10; while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15) >= 0) { d ^= QRUtil.G15 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G15)); } return ((data << 10) | d) ^ QRUtil.G15_MASK; }, getBCHTypeNumber(data: number): number { let d = data << 12; while (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18) >= 0) { d ^= QRUtil.G18 << (QRUtil.getBCHDigit(d) - QRUtil.getBCHDigit(QRUtil.G18)); } return (data << 12) | d; }, getBCHDigit(data: number): number { let digit = 0; while (data !== 0) { digit++; data >>>= 1; } return digit; }, getPatternPosition(typeNumber: number): number[] { return QRUtil.PATTERN_POSITION_TABLE[typeNumber - 1]; }, getMask(maskPattern: number, i: number, j: number): boolean { switch (maskPattern) { case 0: return (i + j) % 2 === 0; case 1: return i % 2 === 0; case 2: return j % 3 === 0; case 3: return (i + j) % 3 === 0; case 4: return (Math.floor(i / 2) + Math.floor(j / 3)) % 2 === 0; case 5: return ((i * j) % 2) + ((i * j) % 3) === 0; case 6: return (((i * j) % 2) + ((i * j) % 3)) % 2 === 0; case 7: return (((i * j) % 3) + ((i + j) % 2)) % 2 === 0; default: throw new Error('bad maskPattern:' + maskPattern); } }, getErrorCorrectionPolynomial(errorCorrectionLength: number): QRPolynomial { let a = new QRPolynomial([1], 0); for (let i = 0; i < errorCorrectionLength; i++) { a = a.multiply(new QRPolynomial([1, QRUtil.gexp(i)], 0)); } return a; }, getLengthInBits(mode: QRMode, type: number): number { if (1 <= type && type < 10) { switch (mode) { case 1: return 10; case 2: return 9; case 4: return 8; case 8: return 8; default: throw new Error('mode:' + mode); } } else if (type < 27) { switch (mode) { case 1: return 12; case 2: return 11; case 4: return 16; case 8: return 10; default: throw new Error('mode:' + mode); } } else if (type < 41) { switch (mode) { case 1: return 14; case 2: return 13; case 4: return 16; case 8: return 12; default: throw new Error('mode:' + mode); } } else { throw new Error('type:' + type); } }, getLostPoint(qrCode: QRCodeModel): number { const moduleCount = qrCode.getModuleCount(); let lostPoint = 0; for (let row = 0; row < moduleCount; row++) { for (let col = 0; col < moduleCount; col++) { let sameCount = 0; const dark = qrCode.isDark(row, col); for (let r = -1; r <= 1; r++) { if (row + r < 0 || moduleCount <= row + r) continue; for (let c = -1; c <= 1; c++) { if (col + c < 0 || moduleCount <= col + c) continue; if (r === 0 && c === 0) continue; if (dark === qrCode.isDark(row + r, col + c)) { sameCount++; } } } if (sameCount > 5) { lostPoint += 3 + sameCount - 5; } } } for (let row = 0; row < moduleCount - 1; row++) { for (let col = 0; col < moduleCount - 1; col++) { let count = 0; if (qrCode.isDark(row, col)) count++; if (qrCode.isDark(row + 1, col)) count++; if (qrCode.isDark(row, col + 1)) count++; if (qrCode.isDark(row + 1, col + 1)) count++; if (count === 0 || count === 4) { lostPoint += 3; } } } for (let row = 0; row < moduleCount; row++) { for (let col = 0; col < moduleCount - 6; col++) { if ( qrCode.isDark(row, col) && !qrCode.isDark(row, col + 1) && qrCode.isDark(row, col + 2) && qrCode.isDark(row, col + 3) && qrCode.isDark(row, col + 4) && !qrCode.isDark(row, col + 5) && qrCode.isDark(row, col + 6) ) { lostPoint += 40; } } } for (let col = 0; col < moduleCount; col++) { for (let row = 0; row < moduleCount - 6; row++) { if ( qrCode.isDark(row, col) && !qrCode.isDark(row + 1, col) && qrCode.isDark(row + 2, col) && qrCode.isDark(row + 3, col) && qrCode.isDark(row + 4, col) && !qrCode.isDark(row + 5, col) && qrCode.isDark(row + 6, col) ) { lostPoint += 40; } } } let darkCount = 0; for (let col = 0; col < moduleCount; col++) { for (let row = 0; row < moduleCount; row++) { if (qrCode.isDark(row, col)) { darkCount++; } } } const ratio = Math.abs((100 * darkCount) / moduleCount / moduleCount - 50) / 5; lostPoint += ratio * 10; return lostPoint; }, getErrorCorrectionLevelValue(level: QRErrorCorrectionLevel): number { switch (level) { case 'L': return 1; case 'M': return 0; case 'Q': return 3; case 'H': return 2; default: throw new Error('level:' + level); } }, getRSBlocks(typeNumber: number, errorCorrectionLevel: QRErrorCorrectionLevel): QRRSBlock[] { return QRRSBlock.getRSBlocks(typeNumber, errorCorrectionLevel); }, EXP_TABLE: new Array(256), LOG_TABLE: new Array(256), gexp(n: number): number { while (n < 0) n += 255; while (n >= 256) n -= 255; return QRUtil.EXP_TABLE[n]; }, glog(n: number): number { if (n < 1) throw new Error('glog(' + n + ')'); return QRUtil.LOG_TABLE[n]; } }; let v = 1; for (let i = 0; i < 255; i++) { QRUtil.EXP_TABLE[i] = v; QRUtil.LOG_TABLE[v] = i; v <<= 1; if (v & 0x100) { v ^= 0x11d; } } QRUtil.EXP_TABLE[255] = QRUtil.EXP_TABLE[0]; class QRBitBufferImpl implements QRBitBuffer { buffer: number[] = []; length: number = 0; get(index: number): boolean { const bufIndex = Math.floor(index / 8); return ((this.buffer[bufIndex] >>> (7 - (index % 8))) & 1) === 1; } put(num: number, length: number): void { for (let i = 0; i < length; i++) { this.putBit(((num >>> (length - i - 1)) & 1) === 1); } } putBit(bit: boolean): void { const bufIndex = Math.floor(this.length / 8); if (this.buffer.length <= bufIndex) { this.buffer.push(0); } if (bit) { this.buffer[bufIndex] |= 0x80 >>> (this.length % 8); } this.length++; } }