UNPKG

@nuintun/qrcode

Version:

A pure JavaScript QRCode encode and decode library.

209 lines (204 loc) 7.74 kB
/** * @module QRCode * @package @nuintun/qrcode * @license MIT * @version 5.0.2 * @author nuintun <nuintun@qq.com> * @description A pure JavaScript QRCode encode and decode library. * @see https://github.com/nuintun/qrcode#readme */ 'use strict'; const Mode = require('../../common/Mode.cjs'); const matrix = require('./matrix.cjs'); const BitArray = require('../../common/BitArray.cjs'); const BlockPair = require('../BlockPair.cjs'); const Version = require('../../common/Version.cjs'); const mask = require('../../common/mask.cjs'); const Encoder = require('../../common/reedsolomon/Encoder.cjs'); /** * @module encoder */ function generateECCodewords(codewords, numECCodewords) { const numDataCodewords = codewords.length; const buffer = new Int32Array(numDataCodewords + numECCodewords); // Copy data codewords. buffer.set(codewords); // Reed solomon encode. new Encoder.Encoder().encode(buffer, numECCodewords); // Get ec codewords. return new Uint8Array(buffer.subarray(numDataCodewords)); } function injectECCodewords(bits, { ecBlocks, numECCodewordsPerBlock }) { // Step 1. Divide data bytes into blocks and generate error correction bytes for them. We'll // store the divided data bytes blocks and error correction bytes blocks into "blocks". let maxNumECCodewords = 0; let maxNumDataCodewords = 0; let dataCodewordsOffset = 0; // Block pair. const blocks = []; for (const { count, numDataCodewords } of ecBlocks) { for (let i = 0; i < count; i++) { const dataCodewords = new Uint8Array(numDataCodewords); bits.writeToUint8Array(dataCodewordsOffset * 8, dataCodewords, 0, numDataCodewords); const ecCodewords = generateECCodewords(dataCodewords, numECCodewordsPerBlock); blocks.push(new BlockPair.BlockPair(dataCodewords, ecCodewords)); dataCodewordsOffset += numDataCodewords; maxNumECCodewords = Math.max(maxNumECCodewords, ecCodewords.length); maxNumDataCodewords = Math.max(maxNumDataCodewords, numDataCodewords); } } const codewords = new BitArray.BitArray(); // First, place data blocks. for (let i = 0; i < maxNumDataCodewords; i++) { for (const { dataCodewords } of blocks) { if (i < dataCodewords.length) { codewords.append(dataCodewords[i], 8); } } } // Then, place error correction blocks. for (let i = 0; i < maxNumECCodewords; i++) { for (const { ecCodewords } of blocks) { if (i < ecCodewords.length) { codewords.append(ecCodewords[i], 8); } } } return codewords; } function appendTerminator(bits, numDataCodewords) { const capacity = numDataCodewords * 8; // Append terminator if there is enough space (value is 0000). for (let i = 0; i < 4 && bits.length < capacity; i++) { bits.append(0); } // Append terminator. See 7.4.9 of ISO/IEC 18004:2015(E)(p.32) for details. // If the last byte isn't 8-bit aligned, we'll add padding bits. const numBitsInLastByte = bits.length & 0x07; if (numBitsInLastByte > 0) { for (let i = numBitsInLastByte; i < 8; i++) { bits.append(0); } } // If we have more space, we'll fill the space with padding patterns defined in 8.4.9 (p.24). const numPaddingCodewords = numDataCodewords - bits.byteLength; for (let i = 0; i < numPaddingCodewords; i++) { bits.append(i & 0x01 ? 0x11 : 0xec, 8); } } function isByteMode(segment) { return segment.mode === Mode.Mode.BYTE; } function isHanziMode(segment) { return segment.mode === Mode.Mode.HANZI; } function appendModeInfo(bits, mode) { bits.append(mode.bits, 4); } function appendECI(bits, segment, currentECIValue) { if (isByteMode(segment)) { const [value] = segment.charset.values; if (value !== currentECIValue) { bits.append(Mode.Mode.ECI.bits, 4); // See 7.4.2.2 of ISO/IEC 18004:2015(E)(p.24) for details. if (value <= 127) { bits.append(value, 8); } else if (value <= 16383) { bits.append(0x8000 | value, 16); } else { bits.append(0xc00000 | value, 24); } return value; } } return currentECIValue; } function appendFNC1Info(bits, fnc1) { const [mode, indicator] = fnc1; // Append FNC1 if applicable. switch (mode) { case 'GS1': // GS1 formatted codes are prefixed with a FNC1 in first position mode header. appendModeInfo(bits, Mode.Mode.FNC1_FIRST_POSITION); break; case 'AIM': // AIM formatted codes are prefixed with a FNC1 in first position mode header. appendModeInfo(bits, Mode.Mode.FNC1_SECOND_POSITION); // Append AIM application indicator. bits.append(indicator, 8); break; } } function getSegmentLength(segment, bits) { // Byte segment use byte Length. if (isByteMode(segment)) { return bits.byteLength; } // Other segments use the real length of characters. // All rest segments content codePointAt at 0x0000 to 0xffff, so use length directly. return segment.content.length; } function appendLengthInfo(bits, mode, version, numLetters) { bits.append(numLetters, mode.getCharacterCountBits(version)); } function willFit(numInputBits, version, ecLevel) { // In the following comments, we use numbers of Version 7-H. const ecBlocks = version.getECBlocks(ecLevel); const numInputCodewords = Math.ceil(numInputBits / 8); return ecBlocks.numTotalDataCodewords >= numInputCodewords; } function chooseVersion(numInputBits, ecLevel) { for (const version of Version.VERSIONS) { if (willFit(numInputBits, version, ecLevel)) { return version; } } throw new Error('data too big for all versions'); } function calculateBitsNeeded(segmentBlocks, version) { let bitsNeeded = 0; for (const { mode, head, body } of segmentBlocks) { bitsNeeded += head.length + mode.getCharacterCountBits(version) + body.length; } return bitsNeeded; } function chooseRecommendVersion(segmentBlocks, ecLevel) { // Hard part: need to know version to know how many bits length takes. But need to know how many // bits it takes to know version. First we take a guess at version by assuming version will be // the minimum, 1: const provisionalBitsNeeded = calculateBitsNeeded(segmentBlocks, Version.VERSIONS[0]); const provisionalVersion = chooseVersion(provisionalBitsNeeded, ecLevel); // Use that guess to calculate the right version. I am still not sure this works in 100% of cases. const bitsNeeded = calculateBitsNeeded(segmentBlocks, provisionalVersion); return chooseVersion(bitsNeeded, ecLevel); } function chooseBestMaskAndMatrix(codewords, version, ecLevel) { let bestMask = 0; let bestMatrix = matrix.buildMatrix(codewords, version, ecLevel, bestMask); let minPenalty = mask.calculateMaskPenalty(bestMatrix); // We try all rest mask patterns to choose the best one. for (let mask$1 = 1; mask$1 < 8; mask$1++) { const matrix$1 = matrix.buildMatrix(codewords, version, ecLevel, mask$1); const penalty = mask.calculateMaskPenalty(matrix$1); // Lower penalty is better. if (penalty < minPenalty) { bestMask = mask$1; bestMatrix = matrix$1; minPenalty = penalty; } } return [bestMask, bestMatrix]; } exports.appendECI = appendECI; exports.appendFNC1Info = appendFNC1Info; exports.appendLengthInfo = appendLengthInfo; exports.appendModeInfo = appendModeInfo; exports.appendTerminator = appendTerminator; exports.calculateBitsNeeded = calculateBitsNeeded; exports.chooseBestMaskAndMatrix = chooseBestMaskAndMatrix; exports.chooseRecommendVersion = chooseRecommendVersion; exports.getSegmentLength = getSegmentLength; exports.injectECCodewords = injectECCodewords; exports.isByteMode = isByteMode; exports.isHanziMode = isHanziMode; exports.willFit = willFit;