@nuintun/qrcode
Version:
A pure JavaScript QRCode encode and decode library.
209 lines (204 loc) • 7.74 kB
JavaScript
/**
* @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
*/
;
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;