UNPKG

@nuintun/qrcode

Version:

A pure JavaScript QRCode encode and decode library.

302 lines (298 loc) 9.77 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 */ import { isApplyMask } from '../../common/mask.js'; import { BitArray } from '../../common/BitArray.js'; import { ByteMatrix } from '../../common/ByteMatrix.js'; import { calculateBCHCode } from '../../common/utils.js'; import { VERSIONS } from '../../common/Version.js'; /** * @module matrix */ // Format information poly: 101 0011 0111. const FORMAT_INFO_POLY = 0x537; // Format information mask. const FORMAT_INFO_MASK = 0x5412; // Version information poly: 1 1111 0010 0101. const VERSION_INFO_POLY = 0x1f25; // Finder pattern shape. const FINDER_PATTERN_SHAPE = [ [1, 1, 1, 1, 1, 1, 1], [1, 0, 0, 0, 0, 0, 1], [1, 0, 1, 1, 1, 0, 1], [1, 0, 1, 1, 1, 0, 1], [1, 0, 1, 1, 1, 0, 1], [1, 0, 0, 0, 0, 0, 1], [1, 1, 1, 1, 1, 1, 1] ]; // Alignment pattern shape. const ALIGNMENT_PATTERN_SHAPE = [ [1, 1, 1, 1, 1], [1, 0, 0, 0, 1], [1, 0, 1, 0, 1], [1, 0, 0, 0, 1], [1, 1, 1, 1, 1] ]; // Format information coordinates. const FORMAT_INFO_COORDINATES = [ [8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8], [7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8] ]; // Is empty point. function isEmpty(matrix, x, y) { return matrix.get(x, y) === -1; } function embedFinderPattern(matrix, x, y) { for (let i = 0; i < 7; i++) { const pattern = FINDER_PATTERN_SHAPE[i]; for (let j = 0; j < 7; j++) { matrix.set(x + j, y + i, pattern[j]); } } } function embedHorizontalSeparator(matrix, x, y) { for (let j = 0; j < 8; j++) { matrix.set(x + j, y, 0); } } function embedVerticalSeparator(matrix, x, y) { for (let i = 0; i < 7; i++) { matrix.set(x, y + i, 0); } } // Embed finder patterns and surrounding vertical/horizontal separators. function embedFinderPatternsAndSeparators(matrix) { // Embed three big squares at corners. const pdpWidth = 7; // Embed horizontal separation patterns around the squares. const hspWidth = 8; // Embed vertical separation patterns around the squares. const vspHeight = 7; // Matrix width const { size } = matrix; // Left top corner. embedFinderPattern(matrix, 0, 0); // Right top corner. embedFinderPattern(matrix, size - pdpWidth, 0); // Left bottom corner. embedFinderPattern(matrix, 0, size - pdpWidth); // Left top corner. embedHorizontalSeparator(matrix, 0, hspWidth - 1); // Right top corner. embedHorizontalSeparator(matrix, size - hspWidth, hspWidth - 1); // Left bottom corner. embedHorizontalSeparator(matrix, 0, size - hspWidth); // Left top corner. embedVerticalSeparator(matrix, vspHeight, 0); // Right top corner. embedVerticalSeparator(matrix, size - vspHeight - 1, 0); // Left bottom corner. embedVerticalSeparator(matrix, vspHeight, size - vspHeight); } function embedTimingPatterns(matrix) { const size = matrix.size - 8; // -8 is for skipping position detection patterns (7: size) // separation patterns (1: size). Thus, 8 = 7 + 1. for (let x = 8; x < size; x++) { const bit = (x + 1) & 0x01; // Horizontal line. if (isEmpty(matrix, x, 6)) { matrix.set(x, 6, bit); } } // -8 is for skipping position detection patterns (7: size) // separation patterns (1: size). Thus, 8 = 7 + 1. for (let y = 8; y < size; y++) { const bit = (y + 1) & 0x01; // Vertical line. if (isEmpty(matrix, 6, y)) { matrix.set(6, y, bit); } } } function embedAlignmentPattern(matrix, x, y) { for (let i = 0; i < 5; i++) { const pattern = ALIGNMENT_PATTERN_SHAPE[i]; for (let j = 0; j < 5; j++) { matrix.set(x + j, y + i, pattern[j]); } } } // Embed position alignment patterns if need be. function embedAlignmentPatterns(matrix, { version }) { if (version >= 2) { const { alignmentPatterns } = VERSIONS[version - 1]; const { length } = alignmentPatterns; for (let i = 0; i < length; i++) { const y = alignmentPatterns[i]; for (let j = 0; j < length; j++) { const x = alignmentPatterns[j]; if (isEmpty(matrix, x, y)) { // If the cell is unset, we embed the position alignment pattern here. // -2 is necessary since the x/y coordinates point to the center of the pattern, not the // left top corner. embedAlignmentPattern(matrix, x - 2, y - 2); } } } } } // Embed the lonely dark dot at left bottom corner. ISO/IEC 18004:2015(E)(p.56) function embedDarkModule(matrix) { matrix.set(8, matrix.size - 8, 1); } // Make bit vector of format information. On success, store the result in "bits". // Encode error correction level and mask pattern. See 8.9 of // ISO/IEC 18004:2015(E)(p.55) for details. function makeFormatInfoBits(bits, ecLevel, mask) { const formatInfo = (ecLevel.bits << 3) | mask; bits.append(formatInfo, 5); const bchCode = calculateBCHCode(formatInfo, FORMAT_INFO_POLY); bits.append(bchCode, 10); const maskBits = new BitArray(); maskBits.append(FORMAT_INFO_MASK, 15); bits.xor(maskBits); } // Embed format information. On success, modify the matrix. function embedFormatInfo(matrix, ecLevel, mask) { const formatInfoBits = new BitArray(); makeFormatInfoBits(formatInfoBits, ecLevel, mask); const { size } = matrix; const { length } = formatInfoBits; for (let i = 0; i < length; i++) { // Type info bits at the left top corner. const [x, y] = FORMAT_INFO_COORDINATES[i]; // Place bits in LSB to MSB order. LSB (least significant bit) is the last value in formatInfoBits. const bit = formatInfoBits.get(length - 1 - i); matrix.set(x, y, bit); if (i < 8) { // Right top corner. matrix.set(size - i - 1, 8, bit); } else { // Left bottom corner. matrix.set(8, size - 7 + (i - 8), bit); } } // Then, embed the dark dot at the left bottom corner. embedDarkModule(matrix); } // Make bit vector of version information. On success, store the result in "bits". // See 7.10 of ISO/IEC 18004:2015(E)(p.58) for details. function makeVersionInfoBits(bits, version) { bits.append(version, 6); const bchCode = calculateBCHCode(version, VERSION_INFO_POLY); bits.append(bchCode, 12); } // Embed version information if need be. On success, modify the matrix. // See 7.10 of ISO/IEC 18004:2015(E)(p.58) for how to embed version information. function embedVersionInfo(matrix, { version }) { if (version >= 7) { const versionInfoBits = new BitArray(); makeVersionInfoBits(versionInfoBits, version); // It will decrease from 17 to 0. let bitIndex = 6 * 3 - 1; const { size } = matrix; for (let i = 0; i < 6; i++) { for (let j = 0; j < 3; j++) { // Place bits in LSB (least significant bit) to MSB order. const bit = versionInfoBits.get(bitIndex--); // Left bottom corner. matrix.set(i, size - 11 + j, bit); // Right bottom corner. matrix.set(size - 11 + j, i, bit); } } } } // Embed "codewords". On success, modify the matrix. // See 7.7.3 of ISO/IEC 18004:2015(E)(p.46) for how to embed codewords. function embedCodewords(matrix, codewords, mask) { let bitIndex = 0; const { size } = matrix; const { length } = codewords; // Start from the right bottom cell. for (let x = size - 1; x >= 1; x -= 2) { // Skip the vertical timing pattern. if (x === 6) { x = 5; } for (let y = 0; y < size; y++) { for (let i = 0; i < 2; i++) { const offsetX = x - i; const upward = ((x + 1) & 2) === 0; const offsetY = upward ? size - 1 - y : y; // Skip the cell if it's not empty. if (isEmpty(matrix, offsetX, offsetY)) { // Padding bit. If there is no bit left, we'll fill the left cells with 0. let bit = 0; if (bitIndex < length) { bit = codewords.get(bitIndex++); } // Is apply mask. if (isApplyMask(mask, offsetX, offsetY)) { bit ^= 1; } matrix.set(offsetX, offsetY, bit); } } } } } // Embed function patterns. On success, modify the matrix. // The function patterns are: // - Finder patterns and separators // - Alignment patterns, if version >= 2 // - Timing patterns function embedFunctionPatterns(matrix, version) { // Let's get started with embedding big squares at corners. embedFinderPatternsAndSeparators(matrix); // Alignment patterns appear if version >= 2. embedAlignmentPatterns(matrix, version); // Timing patterns should be embedded after position adj. patterns. embedTimingPatterns(matrix); } // Embed encoding region. On success, modify the matrix. // The encoding region are: // - Format Info // - Version Info, if version >= 7 // - Data with correction function embedEncodingRegion(matrix, codewords, version, ecLevel, mask) { // Type information appear with any version. embedFormatInfo(matrix, ecLevel, mask); // Version info appear if version >= 7. embedVersionInfo(matrix, version); // Data should be embedded at end. embedCodewords(matrix, codewords, mask); } // Build 2D matrix of QR Code from "codewords" with "ecLevel", "version" and "getMaskPattern". On // success, store the result in "matrix". function buildMatrix(codewords, version, ecLevel, mask) { const matrix = new ByteMatrix(version.size); // Clear matrix. matrix.clear(-1); // Embed function patterns. embedFunctionPatterns(matrix, version); // Embed encoding region. embedEncodingRegion(matrix, codewords, version, ecLevel, mask); return matrix; } export { buildMatrix };