@akamfoad/qr
Version:
Fully typed QRCode encoding implementation in JavaScript with no dependencies!
439 lines (359 loc) • 11.3 kB
text/typescript
import BitByte from './8BitByte';
import RSBlock from './RSBlock';
import BitBuffer from './BitBuffer';
import util from './util';
import Polynomial from './Polynomial';
export default class QRCode {
typeNumber: number;
errorCorrectLevel: number;
modules: (number | boolean | null)[][] | null;
moduleCount: number;
dataCache: number[] | null;
dataList: BitByte[];
constructor(typeNumber: number, errorCorrectLevel: number) {
this.typeNumber = typeNumber;
this.errorCorrectLevel = errorCorrectLevel;
this.modules = null;
this.moduleCount = 0;
this.dataCache = null;
this.dataList = [];
}
// TODO data may be anything, but we start with string
addData(data: string) {
const newData = new BitByte(data);
this.dataList.push(newData);
this.dataCache = null;
}
isDark(row: number, col: number) {
if (
row < 0 ||
this.moduleCount <= row ||
col < 0 ||
this.moduleCount <= col
) {
throw new Error(row + ',' + col);
}
if (this.modules === null) {
throw new Error('this.modules is null');
}
return this.modules[row][col];
}
getModuleCount() {
return this.moduleCount;
}
make() {
// Calculate automatically typeNumber if provided is < 1
if (this.typeNumber < 1) {
let typeNumber = 1;
for (typeNumber = 1; typeNumber < 40; typeNumber++) {
const rsBlocks = RSBlock.getRSBlocks(
typeNumber,
this.errorCorrectLevel,
);
const buffer = new BitBuffer();
let totalDataCount = 0;
for (let i = 0; i < rsBlocks.length; i++) {
totalDataCount += rsBlocks[i].dataCount;
}
for (let i = 0; i < this.dataList.length; i++) {
const data = this.dataList[i];
buffer.put(data.mode, 4);
buffer.put(
data.getLength(),
util.getLengthInBits(data.mode, typeNumber),
);
data.write(buffer);
}
if (buffer.getLengthInBits() <= totalDataCount * 8) break;
}
this.typeNumber = typeNumber;
}
this.makeImpl(false, this.getBestMaskPattern());
}
makeImpl(test: boolean, maskPattern: number) {
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; //(col + row) % 3;
}
}
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 = QRCode.createData(
this.typeNumber,
this.errorCorrectLevel,
this.dataList,
);
}
this.mapData(this.dataCache, maskPattern);
}
setupPositionProbePattern(row: number, col: number) {
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)
) {
if (this.modules === null) {
throw new Error('this.modules is null');
}
this.modules[row + r][col + c] = true;
} else {
if (this.modules === null) {
throw new Error('this.modules is null');
}
this.modules[row + r][col + c] = false;
}
}
}
}
getBestMaskPattern() {
let minLostPoint = 0;
let pattern = 0;
for (let i = 0; i < 8; i++) {
this.makeImpl(true, i);
const lostPoint = util.getLostPoint(this);
if (i == 0 || minLostPoint > lostPoint) {
minLostPoint = lostPoint;
pattern = i;
}
}
return pattern;
}
setupTimingPattern() {
if (this.modules === null) {
throw new Error('this.modules is null');
}
for (let r = 8; r < this.moduleCount - 8; r++) {
if (this.modules[r][6] != null) {
continue;
}
this.modules[r][6] = r % 2 == 0;
}
for (let c = 8; c < this.moduleCount - 8; c++) {
if (this.modules[6][c] != null) {
continue;
}
this.modules[6][c] = c % 2 == 0;
}
}
setupPositionAdjustPattern() {
if (this.modules === null) {
throw new Error('this.modules is null');
}
const pos = util.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 (r == -2 || r == 2 || c == -2 || c == 2 || (r == 0 && c == 0)) {
this.modules[row + r][col + c] = true;
} else {
this.modules[row + r][col + c] = false;
}
}
}
}
}
}
setupTypeNumber(test: boolean) {
if (this.modules === null) {
throw new Error('this.modules is null');
}
const bits = util.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;
}
}
setupTypeInfo(test: boolean, maskPattern: number) {
if (this.modules === null) {
throw new Error('this.modules is null');
}
const data = (this.errorCorrectLevel << 3) | maskPattern;
const bits = util.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 < 8) {
this.modules[i + 1][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;
}
mapData(data: number[], maskPattern: number) {
if (this.modules === null) {
throw new Error('this.modules is null');
}
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--;
// eslint-disable-next-line no-constant-condition
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 = util.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;
}
}
}
}
static PAD0 = 0xec;
static PAD1 = 0x11;
static createData(
typeNumber: number,
errorCorrectLevel: number,
dataList: BitByte[],
) {
const rsBlocks = RSBlock.getRSBlocks(typeNumber, errorCorrectLevel);
const buffer = new BitBuffer();
for (let i = 0; i < dataList.length; i++) {
const data = dataList[i];
buffer.put(data.mode, 4);
buffer.put(data.getLength(), util.getLengthInBits(data.mode, typeNumber));
data.write(buffer);
}
// calc num max data.
let totalDataCount = 0;
for (let i = 0; i < rsBlocks.length; i++) {
totalDataCount += rsBlocks[i].dataCount;
}
if (buffer.getLengthInBits() > totalDataCount * 8) {
throw new Error(
'code length overflow. (' +
buffer.getLengthInBits() +
'>' +
totalDataCount * 8 +
')',
);
}
// end code
if (buffer.getLengthInBits() + 4 <= totalDataCount * 8) {
buffer.put(0, 4);
}
// padding
while (buffer.getLengthInBits() % 8 != 0) {
buffer.putBit(false);
}
// padding
// eslint-disable-next-line no-constant-condition
while (true) {
if (buffer.getLengthInBits() >= totalDataCount * 8) {
break;
}
buffer.put(QRCode.PAD0, 8);
if (buffer.getLengthInBits() >= totalDataCount * 8) {
break;
}
buffer.put(QRCode.PAD1, 8);
}
return QRCode.createBytes(buffer, rsBlocks);
}
static createBytes(buffer: BitBuffer, rsBlocks: RSBlock[]): number[] {
let offset = 0;
let maxDcCount = 0;
let maxEcCount = 0;
const dcdata = new Array<number[]>(rsBlocks.length);
const ecdata = new Array<number[]>(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<number>(dcCount);
for (let i = 0; i < dcdata[r].length; i++) {
dcdata[r][i] = 0xff & buffer.buffer[i + offset];
}
offset += dcCount;
const rsPoly = util.getErrorCorrectPolynomial(ecCount);
const rawPoly = new Polynomial(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<number>(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;
}
}