UNPKG

@nuintun/qrcode

Version:

A pure JavaScript QRCode encode and decode library.

265 lines (260 loc) 8.4 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 utils = require('../../common/utils.cjs'); const BitSource = require('../../common/BitSource.cjs'); const Mode = require('../../common/Mode.cjs'); const Charset = require('../../common/Charset.cjs'); const mapping = require('../../common/encoding/mapping.cjs'); /** * @module source */ function parseECIValue(source) { const firstByte = source.read(8); if ((firstByte & 0x80) === 0) { // Just one byte. return firstByte & 0x7f; } if ((firstByte & 0xc0) === 0x80) { // Two bytes. const secondByte = source.read(8); return ((firstByte & 0x3f) << 8) | secondByte; } if ((firstByte & 0xe0) === 0xc0) { // Three bytes. const secondThirdBytes = source.read(16); return ((firstByte & 0x1f) << 16) | secondThirdBytes; } throw new Error('illegal extended channel interpretation value'); } const GS = String.fromCharCode(0x1d); function processGSCharacter(content) { return content.replace(/%+/g, match => { const isOdd = match.length & 0x01; match = match.replace(/%%/g, '%'); return isOdd ? match.replace(/%$/, GS) : match; }); } function decodeAlphanumericSegment(source, count, fnc1) { let content = ''; while (count > 1) { if (source.available() < 11) { throw new Error('illegal bits length'); } const nextTwoCharsBits = source.read(11); content += utils.charAt(mapping.ALPHANUMERIC_CHARACTERS, nextTwoCharsBits / 45); content += utils.charAt(mapping.ALPHANUMERIC_CHARACTERS, nextTwoCharsBits % 45); count -= 2; } if (count === 1) { // Special case: one character left. if (source.available() < 6) { throw new Error('illegal bits length'); } content += utils.charAt(mapping.ALPHANUMERIC_CHARACTERS, source.read(6)); } return fnc1 ? processGSCharacter(content) : content; } function decodeByteSegment(source, count, decode, fnc1, eciValue) { // Don't crash trying to read more bits than we have available. if (source.available() < 8 * count) { throw new Error('illegal bits length'); } const bytes = new Uint8Array(count); const charset = eciValue != null ? Charset.fromCharsetValue(eciValue) : Charset.Charset.ISO_8859_1; for (let i = 0; i < count; i++) { bytes[i] = source.read(8); } const content = decode(bytes, charset); return fnc1 ? processGSCharacter(content) : content; } function decodeHanziSegment(source, count) { if (source.available() < 13 * count) { throw new Error('illegal bits length'); } let offset = 0; const bytes = new Uint8Array(2 * count); while (count > 0) { const twoBytes = source.read(13); let assembledTwoBytes = ((twoBytes / 0x060) << 8) | twoBytes % 0x060; if (assembledTwoBytes < 0x00a00) { // In the 0xA1A1 to 0xAAFE range. assembledTwoBytes += 0x0a1a1; } else { // In the 0xB0A1 to 0xFAFE range. assembledTwoBytes += 0x0a6a1; } bytes[offset] = (assembledTwoBytes >> 8) & 0xff; bytes[offset + 1] = assembledTwoBytes & 0xff; count--; offset += 2; } return new TextDecoder('gb2312').decode(bytes); } function decodeKanjiSegment(source, count) { if (source.available() < 13 * count) { throw new Error('illegal bits length'); } let offset = 0; const bytes = new Uint8Array(2 * count); while (count > 0) { const twoBytes = source.read(13); let assembledTwoBytes = ((twoBytes / 0x0c0) << 8) | twoBytes % 0x0c0; if (assembledTwoBytes < 0x01f00) { // In the 0x8140 to 0x9FFC range. assembledTwoBytes += 0x08140; } else { // In the 0xE040 to 0xEBBF range. assembledTwoBytes += 0x0c140; } bytes[offset] = (assembledTwoBytes >> 8) & 0xff; bytes[offset + 1] = assembledTwoBytes & 0xff; count--; offset += 2; } return new TextDecoder('shift-jis').decode(bytes); } function decodeNumericSegment(source, count) { let content = ''; // Read three digits at a time. while (count >= 3) { // Each 10 bits encodes three digits. if (source.available() < 10) { throw new Error('illegal bits length'); } const threeDigitsBits = source.read(10); if (threeDigitsBits >= 1000) { throw new Error('illegal numeric codeword'); } content += utils.charAt(mapping.NUMERIC_CHARACTERS, threeDigitsBits / 100); content += utils.charAt(mapping.NUMERIC_CHARACTERS, (threeDigitsBits / 10) % 10); content += utils.charAt(mapping.NUMERIC_CHARACTERS, threeDigitsBits % 10); count -= 3; } if (count === 2) { // Two digits left over to read, encoded in 7 bits. if (source.available() < 7) { throw new Error('illegal bits length'); } const twoDigitsBits = source.read(7); if (twoDigitsBits >= 100) { throw new Error('illegal numeric codeword'); } content += utils.charAt(mapping.NUMERIC_CHARACTERS, twoDigitsBits / 10); content += utils.charAt(mapping.NUMERIC_CHARACTERS, twoDigitsBits % 10); } else if (count === 1) { // One digit left over to read. if (source.available() < 4) { throw new Error('illegal bits length'); } const digitBits = source.read(4); if (digitBits >= 10) { throw new Error('illegal numeric codeword'); } content += utils.charAt(mapping.NUMERIC_CHARACTERS, digitBits); } return content; } function decode(codewords, version, decode) { let content = ''; let indicator = -1; let modifier; let hasFNC1First = false; let hasFNC1Second = false; let mode; let fnc1 = false; let currentECIValue; let structured = false; const source = new BitSource.BitSource(codewords); do { // While still another segment to read... if (source.available() < 4) { // OK, assume we're done. Really, a TERMINATOR mode should have been recorded here. mode = Mode.Mode.TERMINATOR; } else { mode = Mode.fromModeBits(source.read(4)); } switch (mode) { case Mode.Mode.TERMINATOR: break; case Mode.Mode.FNC1_FIRST_POSITION: hasFNC1First = true; break; case Mode.Mode.FNC1_SECOND_POSITION: hasFNC1Second = true; indicator = source.read(8); break; case Mode.Mode.STRUCTURED_APPEND: if (source.available() < 16) { throw new Error('illegal structured append'); } structured = Object.freeze({ index: source.read(4), count: source.read(4) + 1, parity: source.read(8) }); break; case Mode.Mode.ECI: currentECIValue = parseECIValue(source); break; default: if (mode === Mode.Mode.HANZI) { const subset = source.read(4); if (subset !== 1) { throw new Error('illegal hanzi subset'); } } const count = source.read(mode.getCharacterCountBits(version)); switch (mode) { case Mode.Mode.ALPHANUMERIC: content += decodeAlphanumericSegment(source, count, hasFNC1First || hasFNC1Second); break; case Mode.Mode.BYTE: content += decodeByteSegment(source, count, decode, hasFNC1First || hasFNC1Second, currentECIValue); break; case Mode.Mode.HANZI: content += decodeHanziSegment(source, count); break; case Mode.Mode.KANJI: content += decodeKanjiSegment(source, count); break; case Mode.Mode.NUMERIC: content += decodeNumericSegment(source, count); break; default: throw new Error('illegal mode'); } } } while (mode !== Mode.Mode.TERMINATOR); if (hasFNC1First) { fnc1 = Object.freeze(['GS1']); } else if (hasFNC1Second) { fnc1 = Object.freeze(['AIM', indicator]); } if (currentECIValue != null) { if (hasFNC1First) { modifier = 4; } else if (hasFNC1Second) { modifier = 6; } else { modifier = 2; } } else { if (hasFNC1First) { modifier = 3; } else if (hasFNC1Second) { modifier = 5; } else { modifier = 1; } } return { content, codewords, structured, symbology: `]Q${modifier}`, fnc1 }; } exports.decode = decode;