2909 lines
109 KiB
JavaScript
2909 lines
109 KiB
JavaScript
class BitMatrix {
|
|
constructor(data, width) {
|
|
this.width = width;
|
|
this.height = data.length / width;
|
|
this.data = data;
|
|
}
|
|
static createEmpty(width, height) {
|
|
return new BitMatrix(new Uint8ClampedArray(width * height), width);
|
|
}
|
|
get(x, y) {
|
|
if (x < 0 || x >= this.width || y < 0 || y >= this.height) {
|
|
return false;
|
|
}
|
|
return !!this.data[y * this.width + x];
|
|
}
|
|
set(x, y, v) {
|
|
this.data[y * this.width + x] = v ? 1 : 0;
|
|
}
|
|
setRegion(left, top, width, height, v) {
|
|
for (let y = top; y < top + height; y++) {
|
|
for (let x = left; x < left + width; x++) {
|
|
this.set(x, y, !!v);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const REGION_SIZE = 8;
|
|
const MIN_DYNAMIC_RANGE = 24;
|
|
function numBetween(value, min, max) {
|
|
return value < min ? min : value > max ? max : value;
|
|
}
|
|
// Like BitMatrix but accepts arbitry Uint8 values
|
|
class Matrix {
|
|
constructor(width, height, buffer) {
|
|
this.width = width;
|
|
const bufferSize = width * height;
|
|
if (buffer && buffer.length !== bufferSize) {
|
|
throw new Error("Wrong buffer size");
|
|
}
|
|
this.data = buffer || new Uint8ClampedArray(bufferSize);
|
|
}
|
|
get(x, y) {
|
|
return this.data[y * this.width + x];
|
|
}
|
|
set(x, y, value) {
|
|
this.data[y * this.width + x] = value;
|
|
}
|
|
}
|
|
function binarize(data, width, height, returnInverted, greyscaleWeights, canOverwriteImage) {
|
|
const pixelCount = width * height;
|
|
if (data.length !== pixelCount * 4) {
|
|
throw new Error("Malformed data passed to binarizer.");
|
|
}
|
|
// assign the greyscale and binary image within the rgba buffer as the rgba image will not be needed after conversion
|
|
let bufferOffset = 0;
|
|
// Convert image to greyscale
|
|
let greyscaleBuffer;
|
|
if (canOverwriteImage) {
|
|
greyscaleBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount);
|
|
bufferOffset += pixelCount;
|
|
}
|
|
const greyscalePixels = new Matrix(width, height, greyscaleBuffer);
|
|
if (greyscaleWeights.useIntegerApproximation) {
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const pixelPosition = (y * width + x) * 4;
|
|
const r = data[pixelPosition];
|
|
const g = data[pixelPosition + 1];
|
|
const b = data[pixelPosition + 2];
|
|
greyscalePixels.set(x, y,
|
|
// tslint:disable-next-line no-bitwise
|
|
(greyscaleWeights.red * r + greyscaleWeights.green * g + greyscaleWeights.blue * b + 128) >> 8);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const pixelPosition = (y * width + x) * 4;
|
|
const r = data[pixelPosition];
|
|
const g = data[pixelPosition + 1];
|
|
const b = data[pixelPosition + 2];
|
|
greyscalePixels.set(x, y, greyscaleWeights.red * r + greyscaleWeights.green * g + greyscaleWeights.blue * b);
|
|
}
|
|
}
|
|
}
|
|
const horizontalRegionCount = Math.ceil(width / REGION_SIZE);
|
|
const verticalRegionCount = Math.ceil(height / REGION_SIZE);
|
|
const blackPointsCount = horizontalRegionCount * verticalRegionCount;
|
|
let blackPointsBuffer;
|
|
if (canOverwriteImage) {
|
|
blackPointsBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, blackPointsCount);
|
|
bufferOffset += blackPointsCount;
|
|
}
|
|
const blackPoints = new Matrix(horizontalRegionCount, verticalRegionCount, blackPointsBuffer);
|
|
for (let verticalRegion = 0; verticalRegion < verticalRegionCount; verticalRegion++) {
|
|
for (let hortizontalRegion = 0; hortizontalRegion < horizontalRegionCount; hortizontalRegion++) {
|
|
let min = Infinity;
|
|
let max = 0;
|
|
for (let y = 0; y < REGION_SIZE; y++) {
|
|
for (let x = 0; x < REGION_SIZE; x++) {
|
|
const pixelLumosity = greyscalePixels.get(hortizontalRegion * REGION_SIZE + x, verticalRegion * REGION_SIZE + y);
|
|
min = Math.min(min, pixelLumosity);
|
|
max = Math.max(max, pixelLumosity);
|
|
}
|
|
}
|
|
// We could also compute the real average of all pixels but following the assumption that the qr code consists
|
|
// of bright and dark pixels and essentially not much in between, by (min + max)/2 we make the cut really between
|
|
// those two classes. If using the average over all pixel in a block of mostly bright pixels and few dark pixels,
|
|
// the avg would tend to the bright side and darker bright pixels could be interpreted as dark.
|
|
let average = (min + max) / 2;
|
|
// Small bias towards black by moving the threshold up. We do this, as in the finder patterns white holes tend
|
|
// to appear which makes them undetectable.
|
|
const blackBias = 1.11;
|
|
average = Math.min(255, average * blackBias);
|
|
if (max - min <= MIN_DYNAMIC_RANGE) {
|
|
// If variation within the block is low, assume this is a block with only light or only
|
|
// dark pixels. In that case we do not want to use the average, as it would divide this
|
|
// low contrast area into black and white pixels, essentially creating data out of noise.
|
|
//
|
|
// Default the blackpoint for these blocks to be half the min - effectively white them out
|
|
average = min / 2;
|
|
if (verticalRegion > 0 && hortizontalRegion > 0) {
|
|
// Correct the "white background" assumption for blocks that have neighbors by comparing
|
|
// the pixels in this block to the previously calculated black points. This is based on
|
|
// the fact that dark barcode symbology is always surrounded by some amount of light
|
|
// background for which reasonable black point estimates were made. The bp estimated at
|
|
// the boundaries is used for the interior.
|
|
// The (min < bp) is arbitrary but works better than other heuristics that were tried.
|
|
const averageNeighborBlackPoint = (blackPoints.get(hortizontalRegion, verticalRegion - 1) +
|
|
(2 * blackPoints.get(hortizontalRegion - 1, verticalRegion)) +
|
|
blackPoints.get(hortizontalRegion - 1, verticalRegion - 1)) / 4;
|
|
if (min < averageNeighborBlackPoint) {
|
|
average = averageNeighborBlackPoint; // no need to apply black bias as already applied to neighbors
|
|
}
|
|
}
|
|
}
|
|
blackPoints.set(hortizontalRegion, verticalRegion, average);
|
|
}
|
|
}
|
|
let binarized;
|
|
if (canOverwriteImage) {
|
|
const binarizedBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount);
|
|
bufferOffset += pixelCount;
|
|
binarized = new BitMatrix(binarizedBuffer, width);
|
|
}
|
|
else {
|
|
binarized = BitMatrix.createEmpty(width, height);
|
|
}
|
|
let inverted = null;
|
|
if (returnInverted) {
|
|
if (canOverwriteImage) {
|
|
const invertedBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount);
|
|
inverted = new BitMatrix(invertedBuffer, width);
|
|
}
|
|
else {
|
|
inverted = BitMatrix.createEmpty(width, height);
|
|
}
|
|
}
|
|
for (let verticalRegion = 0; verticalRegion < verticalRegionCount; verticalRegion++) {
|
|
for (let hortizontalRegion = 0; hortizontalRegion < horizontalRegionCount; hortizontalRegion++) {
|
|
const left = numBetween(hortizontalRegion, 2, horizontalRegionCount - 3);
|
|
const top = numBetween(verticalRegion, 2, verticalRegionCount - 3);
|
|
let sum = 0;
|
|
for (let xRegion = -2; xRegion <= 2; xRegion++) {
|
|
for (let yRegion = -2; yRegion <= 2; yRegion++) {
|
|
sum += blackPoints.get(left + xRegion, top + yRegion);
|
|
}
|
|
}
|
|
const threshold = sum / 25;
|
|
for (let xRegion = 0; xRegion < REGION_SIZE; xRegion++) {
|
|
for (let yRegion = 0; yRegion < REGION_SIZE; yRegion++) {
|
|
const x = hortizontalRegion * REGION_SIZE + xRegion;
|
|
const y = verticalRegion * REGION_SIZE + yRegion;
|
|
const lum = greyscalePixels.get(x, y);
|
|
binarized.set(x, y, lum <= threshold);
|
|
if (returnInverted) {
|
|
inverted.set(x, y, !(lum <= threshold));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (returnInverted) {
|
|
return { binarized, inverted };
|
|
}
|
|
return { binarized };
|
|
}
|
|
|
|
// tslint:disable:no-bitwise
|
|
class BitStream {
|
|
constructor(bytes) {
|
|
this.byteOffset = 0;
|
|
this.bitOffset = 0;
|
|
this.bytes = bytes;
|
|
}
|
|
readBits(numBits) {
|
|
if (numBits < 1 || numBits > 32 || numBits > this.available()) {
|
|
throw new Error("Cannot read " + numBits.toString() + " bits");
|
|
}
|
|
let result = 0;
|
|
// First, read remainder from current byte
|
|
if (this.bitOffset > 0) {
|
|
const bitsLeft = 8 - this.bitOffset;
|
|
const toRead = numBits < bitsLeft ? numBits : bitsLeft;
|
|
const bitsToNotRead = bitsLeft - toRead;
|
|
const mask = (0xFF >> (8 - toRead)) << bitsToNotRead;
|
|
result = (this.bytes[this.byteOffset] & mask) >> bitsToNotRead;
|
|
numBits -= toRead;
|
|
this.bitOffset += toRead;
|
|
if (this.bitOffset === 8) {
|
|
this.bitOffset = 0;
|
|
this.byteOffset++;
|
|
}
|
|
}
|
|
// Next read whole bytes
|
|
if (numBits > 0) {
|
|
while (numBits >= 8) {
|
|
result = (result << 8) | (this.bytes[this.byteOffset] & 0xFF);
|
|
this.byteOffset++;
|
|
numBits -= 8;
|
|
}
|
|
// Finally read a partial byte
|
|
if (numBits > 0) {
|
|
const bitsToNotRead = 8 - numBits;
|
|
const mask = (0xFF >> bitsToNotRead) << bitsToNotRead;
|
|
result = (result << numBits) | ((this.bytes[this.byteOffset] & mask) >> bitsToNotRead);
|
|
this.bitOffset += numBits;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
available() {
|
|
return 8 * (this.bytes.length - this.byteOffset) - this.bitOffset;
|
|
}
|
|
}
|
|
|
|
// tslint:disable:no-bitwise
|
|
var Mode;
|
|
(function (Mode) {
|
|
Mode["Numeric"] = "numeric";
|
|
Mode["Alphanumeric"] = "alphanumeric";
|
|
Mode["Byte"] = "byte";
|
|
Mode["Kanji"] = "kanji";
|
|
Mode["ECI"] = "eci";
|
|
Mode["StructuredAppend"] = "structuredappend";
|
|
})(Mode || (Mode = {}));
|
|
var ModeByte;
|
|
(function (ModeByte) {
|
|
ModeByte[ModeByte["Terminator"] = 0] = "Terminator";
|
|
ModeByte[ModeByte["Numeric"] = 1] = "Numeric";
|
|
ModeByte[ModeByte["Alphanumeric"] = 2] = "Alphanumeric";
|
|
ModeByte[ModeByte["Byte"] = 4] = "Byte";
|
|
ModeByte[ModeByte["Kanji"] = 8] = "Kanji";
|
|
ModeByte[ModeByte["ECI"] = 7] = "ECI";
|
|
ModeByte[ModeByte["StructuredAppend"] = 3] = "StructuredAppend";
|
|
// FNC1FirstPosition = 0x5,
|
|
// FNC1SecondPosition = 0x9,
|
|
})(ModeByte || (ModeByte = {}));
|
|
function decodeNumeric(stream, size) {
|
|
const bytes = [];
|
|
let text = "";
|
|
const characterCountSize = [10, 12, 14][size];
|
|
let length = stream.readBits(characterCountSize);
|
|
// Read digits in groups of 3
|
|
while (length >= 3) {
|
|
const num = stream.readBits(10);
|
|
if (num >= 1000) {
|
|
throw new Error("Invalid numeric value above 999");
|
|
}
|
|
const a = Math.floor(num / 100);
|
|
const b = Math.floor(num / 10) % 10;
|
|
const c = num % 10;
|
|
bytes.push(48 + a, 48 + b, 48 + c);
|
|
text += a.toString() + b.toString() + c.toString();
|
|
length -= 3;
|
|
}
|
|
// If the number of digits aren't a multiple of 3, the remaining digits are special cased.
|
|
if (length === 2) {
|
|
const num = stream.readBits(7);
|
|
if (num >= 100) {
|
|
throw new Error("Invalid numeric value above 99");
|
|
}
|
|
const a = Math.floor(num / 10);
|
|
const b = num % 10;
|
|
bytes.push(48 + a, 48 + b);
|
|
text += a.toString() + b.toString();
|
|
}
|
|
else if (length === 1) {
|
|
const num = stream.readBits(4);
|
|
if (num >= 10) {
|
|
throw new Error("Invalid numeric value above 9");
|
|
}
|
|
bytes.push(48 + num);
|
|
text += num.toString();
|
|
}
|
|
return { bytes, text };
|
|
}
|
|
const AlphanumericCharacterCodes = [
|
|
"0", "1", "2", "3", "4", "5", "6", "7", "8",
|
|
"9", "A", "B", "C", "D", "E", "F", "G", "H",
|
|
"I", "J", "K", "L", "M", "N", "O", "P", "Q",
|
|
"R", "S", "T", "U", "V", "W", "X", "Y", "Z",
|
|
" ", "$", "%", "*", "+", "-", ".", "/", ":",
|
|
];
|
|
function decodeAlphanumeric(stream, size) {
|
|
const bytes = [];
|
|
let text = "";
|
|
const characterCountSize = [9, 11, 13][size];
|
|
let length = stream.readBits(characterCountSize);
|
|
while (length >= 2) {
|
|
const v = stream.readBits(11);
|
|
const a = Math.floor(v / 45);
|
|
const b = v % 45;
|
|
bytes.push(AlphanumericCharacterCodes[a].charCodeAt(0), AlphanumericCharacterCodes[b].charCodeAt(0));
|
|
text += AlphanumericCharacterCodes[a] + AlphanumericCharacterCodes[b];
|
|
length -= 2;
|
|
}
|
|
if (length === 1) {
|
|
const a = stream.readBits(6);
|
|
bytes.push(AlphanumericCharacterCodes[a].charCodeAt(0));
|
|
text += AlphanumericCharacterCodes[a];
|
|
}
|
|
return { bytes, text };
|
|
}
|
|
function decodeByte(stream, size) {
|
|
const bytes = [];
|
|
let text = "";
|
|
const characterCountSize = [8, 16, 16][size];
|
|
const length = stream.readBits(characterCountSize);
|
|
for (let i = 0; i < length; i++) {
|
|
const b = stream.readBits(8);
|
|
bytes.push(b);
|
|
}
|
|
try {
|
|
text += decodeURIComponent(bytes.map(b => `%${("0" + b.toString(16)).substr(-2)}`).join(""));
|
|
}
|
|
catch (_a) {
|
|
// failed to decode
|
|
}
|
|
return { bytes, text };
|
|
}
|
|
function decodeKanji(stream, size) {
|
|
const bytes = [];
|
|
const characterCountSize = [8, 10, 12][size];
|
|
const length = stream.readBits(characterCountSize);
|
|
for (let i = 0; i < length; i++) {
|
|
const k = stream.readBits(13);
|
|
let c = (Math.floor(k / 0xC0) << 8) | (k % 0xC0);
|
|
if (c < 0x1F00) {
|
|
c += 0x8140;
|
|
}
|
|
else {
|
|
c += 0xC140;
|
|
}
|
|
bytes.push(c >> 8, c & 0xFF);
|
|
}
|
|
const text = new TextDecoder("shift-jis").decode(Uint8Array.from(bytes));
|
|
return { bytes, text };
|
|
}
|
|
function decode$2(data, version) {
|
|
const stream = new BitStream(data);
|
|
// There are 3 'sizes' based on the version. 1-9 is small (0), 10-26 is medium (1) and 27-40 is large (2).
|
|
const size = version <= 9 ? 0 : version <= 26 ? 1 : 2;
|
|
const result = {
|
|
text: "",
|
|
bytes: [],
|
|
chunks: [],
|
|
version,
|
|
};
|
|
while (stream.available() >= 4) {
|
|
const mode = stream.readBits(4);
|
|
if (mode === ModeByte.Terminator) {
|
|
return result;
|
|
}
|
|
else if (mode === ModeByte.ECI) {
|
|
if (stream.readBits(1) === 0) {
|
|
result.chunks.push({
|
|
type: Mode.ECI,
|
|
assignmentNumber: stream.readBits(7),
|
|
});
|
|
}
|
|
else if (stream.readBits(1) === 0) {
|
|
result.chunks.push({
|
|
type: Mode.ECI,
|
|
assignmentNumber: stream.readBits(14),
|
|
});
|
|
}
|
|
else if (stream.readBits(1) === 0) {
|
|
result.chunks.push({
|
|
type: Mode.ECI,
|
|
assignmentNumber: stream.readBits(21),
|
|
});
|
|
}
|
|
else {
|
|
// ECI data seems corrupted
|
|
result.chunks.push({
|
|
type: Mode.ECI,
|
|
assignmentNumber: -1,
|
|
});
|
|
}
|
|
}
|
|
else if (mode === ModeByte.Numeric) {
|
|
const numericResult = decodeNumeric(stream, size);
|
|
result.text += numericResult.text;
|
|
result.bytes.push(...numericResult.bytes);
|
|
result.chunks.push({
|
|
type: Mode.Numeric,
|
|
text: numericResult.text,
|
|
});
|
|
}
|
|
else if (mode === ModeByte.Alphanumeric) {
|
|
const alphanumericResult = decodeAlphanumeric(stream, size);
|
|
result.text += alphanumericResult.text;
|
|
result.bytes.push(...alphanumericResult.bytes);
|
|
result.chunks.push({
|
|
type: Mode.Alphanumeric,
|
|
text: alphanumericResult.text,
|
|
});
|
|
}
|
|
else if (mode === ModeByte.Byte) {
|
|
const byteResult = decodeByte(stream, size);
|
|
result.text += byteResult.text;
|
|
result.bytes.push(...byteResult.bytes);
|
|
result.chunks.push({
|
|
type: Mode.Byte,
|
|
bytes: byteResult.bytes,
|
|
text: byteResult.text,
|
|
});
|
|
}
|
|
else if (mode === ModeByte.Kanji) {
|
|
const kanjiResult = decodeKanji(stream, size);
|
|
result.text += kanjiResult.text;
|
|
result.bytes.push(...kanjiResult.bytes);
|
|
result.chunks.push({
|
|
type: Mode.Kanji,
|
|
bytes: kanjiResult.bytes,
|
|
text: kanjiResult.text,
|
|
});
|
|
}
|
|
else if (mode === ModeByte.StructuredAppend) {
|
|
result.chunks.push({
|
|
type: Mode.StructuredAppend,
|
|
currentSequence: stream.readBits(4),
|
|
totalSequence: stream.readBits(4),
|
|
parity: stream.readBits(8),
|
|
});
|
|
}
|
|
}
|
|
// If there is no data left, or the remaining bits are all 0, then that counts as a termination marker
|
|
if (stream.available() === 0 || stream.readBits(stream.available()) === 0) {
|
|
return result;
|
|
}
|
|
}
|
|
|
|
class GenericGFPoly {
|
|
constructor(field, coefficients) {
|
|
if (coefficients.length === 0) {
|
|
throw new Error("No coefficients.");
|
|
}
|
|
this.field = field;
|
|
const coefficientsLength = coefficients.length;
|
|
if (coefficientsLength > 1 && coefficients[0] === 0) {
|
|
// Leading term must be non-zero for anything except the constant polynomial "0"
|
|
let firstNonZero = 1;
|
|
while (firstNonZero < coefficientsLength && coefficients[firstNonZero] === 0) {
|
|
firstNonZero++;
|
|
}
|
|
if (firstNonZero === coefficientsLength) {
|
|
this.coefficients = field.zero.coefficients;
|
|
}
|
|
else {
|
|
this.coefficients = new Uint8ClampedArray(coefficientsLength - firstNonZero);
|
|
for (let i = 0; i < this.coefficients.length; i++) {
|
|
this.coefficients[i] = coefficients[firstNonZero + i];
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
this.coefficients = coefficients;
|
|
}
|
|
}
|
|
degree() {
|
|
return this.coefficients.length - 1;
|
|
}
|
|
isZero() {
|
|
return this.coefficients[0] === 0;
|
|
}
|
|
getCoefficient(degree) {
|
|
return this.coefficients[this.coefficients.length - 1 - degree];
|
|
}
|
|
addOrSubtract(other) {
|
|
if (this.isZero()) {
|
|
return other;
|
|
}
|
|
if (other.isZero()) {
|
|
return this;
|
|
}
|
|
let smallerCoefficients = this.coefficients;
|
|
let largerCoefficients = other.coefficients;
|
|
if (smallerCoefficients.length > largerCoefficients.length) {
|
|
[smallerCoefficients, largerCoefficients] = [largerCoefficients, smallerCoefficients];
|
|
}
|
|
const sumDiff = new Uint8ClampedArray(largerCoefficients.length);
|
|
const lengthDiff = largerCoefficients.length - smallerCoefficients.length;
|
|
for (let i = 0; i < lengthDiff; i++) {
|
|
sumDiff[i] = largerCoefficients[i];
|
|
}
|
|
for (let i = lengthDiff; i < largerCoefficients.length; i++) {
|
|
sumDiff[i] = addOrSubtractGF(smallerCoefficients[i - lengthDiff], largerCoefficients[i]);
|
|
}
|
|
return new GenericGFPoly(this.field, sumDiff);
|
|
}
|
|
multiply(scalar) {
|
|
if (scalar === 0) {
|
|
return this.field.zero;
|
|
}
|
|
if (scalar === 1) {
|
|
return this;
|
|
}
|
|
const size = this.coefficients.length;
|
|
const product = new Uint8ClampedArray(size);
|
|
for (let i = 0; i < size; i++) {
|
|
product[i] = this.field.multiply(this.coefficients[i], scalar);
|
|
}
|
|
return new GenericGFPoly(this.field, product);
|
|
}
|
|
multiplyPoly(other) {
|
|
if (this.isZero() || other.isZero()) {
|
|
return this.field.zero;
|
|
}
|
|
const aCoefficients = this.coefficients;
|
|
const aLength = aCoefficients.length;
|
|
const bCoefficients = other.coefficients;
|
|
const bLength = bCoefficients.length;
|
|
const product = new Uint8ClampedArray(aLength + bLength - 1);
|
|
for (let i = 0; i < aLength; i++) {
|
|
const aCoeff = aCoefficients[i];
|
|
for (let j = 0; j < bLength; j++) {
|
|
product[i + j] = addOrSubtractGF(product[i + j], this.field.multiply(aCoeff, bCoefficients[j]));
|
|
}
|
|
}
|
|
return new GenericGFPoly(this.field, product);
|
|
}
|
|
multiplyByMonomial(degree, coefficient) {
|
|
if (degree < 0) {
|
|
throw new Error("Invalid degree less than 0");
|
|
}
|
|
if (coefficient === 0) {
|
|
return this.field.zero;
|
|
}
|
|
const size = this.coefficients.length;
|
|
const product = new Uint8ClampedArray(size + degree);
|
|
for (let i = 0; i < size; i++) {
|
|
product[i] = this.field.multiply(this.coefficients[i], coefficient);
|
|
}
|
|
return new GenericGFPoly(this.field, product);
|
|
}
|
|
evaluateAt(a) {
|
|
let result = 0;
|
|
if (a === 0) {
|
|
// Just return the x^0 coefficient
|
|
return this.getCoefficient(0);
|
|
}
|
|
const size = this.coefficients.length;
|
|
if (a === 1) {
|
|
// Just the sum of the coefficients
|
|
this.coefficients.forEach((coefficient) => {
|
|
result = addOrSubtractGF(result, coefficient);
|
|
});
|
|
return result;
|
|
}
|
|
result = this.coefficients[0];
|
|
for (let i = 1; i < size; i++) {
|
|
result = addOrSubtractGF(this.field.multiply(a, result), this.coefficients[i]);
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
|
|
function addOrSubtractGF(a, b) {
|
|
return a ^ b; // tslint:disable-line:no-bitwise
|
|
}
|
|
class GenericGF {
|
|
constructor(primitive, size, genBase) {
|
|
this.primitive = primitive;
|
|
this.size = size;
|
|
this.generatorBase = genBase;
|
|
this.expTable = new Array(this.size);
|
|
this.logTable = new Array(this.size);
|
|
let x = 1;
|
|
for (let i = 0; i < this.size; i++) {
|
|
this.expTable[i] = x;
|
|
x = x * 2;
|
|
if (x >= this.size) {
|
|
x = (x ^ this.primitive) & (this.size - 1); // tslint:disable-line:no-bitwise
|
|
}
|
|
}
|
|
for (let i = 0; i < this.size - 1; i++) {
|
|
this.logTable[this.expTable[i]] = i;
|
|
}
|
|
this.zero = new GenericGFPoly(this, Uint8ClampedArray.from([0]));
|
|
this.one = new GenericGFPoly(this, Uint8ClampedArray.from([1]));
|
|
}
|
|
multiply(a, b) {
|
|
if (a === 0 || b === 0) {
|
|
return 0;
|
|
}
|
|
return this.expTable[(this.logTable[a] + this.logTable[b]) % (this.size - 1)];
|
|
}
|
|
inverse(a) {
|
|
if (a === 0) {
|
|
throw new Error("Can't invert 0");
|
|
}
|
|
return this.expTable[this.size - this.logTable[a] - 1];
|
|
}
|
|
buildMonomial(degree, coefficient) {
|
|
if (degree < 0) {
|
|
throw new Error("Invalid monomial degree less than 0");
|
|
}
|
|
if (coefficient === 0) {
|
|
return this.zero;
|
|
}
|
|
const coefficients = new Uint8ClampedArray(degree + 1);
|
|
coefficients[0] = coefficient;
|
|
return new GenericGFPoly(this, coefficients);
|
|
}
|
|
log(a) {
|
|
if (a === 0) {
|
|
throw new Error("Can't take log(0)");
|
|
}
|
|
return this.logTable[a];
|
|
}
|
|
exp(a) {
|
|
return this.expTable[a];
|
|
}
|
|
}
|
|
|
|
function runEuclideanAlgorithm(field, a, b, R) {
|
|
// Assume a's degree is >= b's
|
|
if (a.degree() < b.degree()) {
|
|
[a, b] = [b, a];
|
|
}
|
|
let rLast = a;
|
|
let r = b;
|
|
let tLast = field.zero;
|
|
let t = field.one;
|
|
// Run Euclidean algorithm until r's degree is less than R/2
|
|
while (r.degree() >= R / 2) {
|
|
const rLastLast = rLast;
|
|
const tLastLast = tLast;
|
|
rLast = r;
|
|
tLast = t;
|
|
// Divide rLastLast by rLast, with quotient in q and remainder in r
|
|
if (rLast.isZero()) {
|
|
// Euclidean algorithm already terminated?
|
|
return null;
|
|
}
|
|
r = rLastLast;
|
|
let q = field.zero;
|
|
const denominatorLeadingTerm = rLast.getCoefficient(rLast.degree());
|
|
const dltInverse = field.inverse(denominatorLeadingTerm);
|
|
while (r.degree() >= rLast.degree() && !r.isZero()) {
|
|
const degreeDiff = r.degree() - rLast.degree();
|
|
const scale = field.multiply(r.getCoefficient(r.degree()), dltInverse);
|
|
q = q.addOrSubtract(field.buildMonomial(degreeDiff, scale));
|
|
r = r.addOrSubtract(rLast.multiplyByMonomial(degreeDiff, scale));
|
|
}
|
|
t = q.multiplyPoly(tLast).addOrSubtract(tLastLast);
|
|
if (r.degree() >= rLast.degree()) {
|
|
return null;
|
|
}
|
|
}
|
|
const sigmaTildeAtZero = t.getCoefficient(0);
|
|
if (sigmaTildeAtZero === 0) {
|
|
return null;
|
|
}
|
|
const inverse = field.inverse(sigmaTildeAtZero);
|
|
return [t.multiply(inverse), r.multiply(inverse)];
|
|
}
|
|
function findErrorLocations(field, errorLocator) {
|
|
// This is a direct application of Chien's search
|
|
const numErrors = errorLocator.degree();
|
|
if (numErrors === 1) {
|
|
return [errorLocator.getCoefficient(1)];
|
|
}
|
|
const result = new Array(numErrors);
|
|
let errorCount = 0;
|
|
for (let i = 1; i < field.size && errorCount < numErrors; i++) {
|
|
if (errorLocator.evaluateAt(i) === 0) {
|
|
result[errorCount] = field.inverse(i);
|
|
errorCount++;
|
|
}
|
|
}
|
|
if (errorCount !== numErrors) {
|
|
return null;
|
|
}
|
|
return result;
|
|
}
|
|
function findErrorMagnitudes(field, errorEvaluator, errorLocations) {
|
|
// This is directly applying Forney's Formula
|
|
const s = errorLocations.length;
|
|
const result = new Array(s);
|
|
for (let i = 0; i < s; i++) {
|
|
const xiInverse = field.inverse(errorLocations[i]);
|
|
let denominator = 1;
|
|
for (let j = 0; j < s; j++) {
|
|
if (i !== j) {
|
|
denominator = field.multiply(denominator, addOrSubtractGF(1, field.multiply(errorLocations[j], xiInverse)));
|
|
}
|
|
}
|
|
result[i] = field.multiply(errorEvaluator.evaluateAt(xiInverse), field.inverse(denominator));
|
|
if (field.generatorBase !== 0) {
|
|
result[i] = field.multiply(result[i], xiInverse);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
function decode$1(bytes, twoS) {
|
|
const outputBytes = new Uint8ClampedArray(bytes.length);
|
|
outputBytes.set(bytes);
|
|
const field = new GenericGF(0x011D, 256, 0); // x^8 + x^4 + x^3 + x^2 + 1
|
|
const poly = new GenericGFPoly(field, outputBytes);
|
|
const syndromeCoefficients = new Uint8ClampedArray(twoS);
|
|
let error = false;
|
|
for (let s = 0; s < twoS; s++) {
|
|
const evaluation = poly.evaluateAt(field.exp(s + field.generatorBase));
|
|
syndromeCoefficients[syndromeCoefficients.length - 1 - s] = evaluation;
|
|
if (evaluation !== 0) {
|
|
error = true;
|
|
}
|
|
}
|
|
if (!error) {
|
|
return outputBytes;
|
|
}
|
|
const syndrome = new GenericGFPoly(field, syndromeCoefficients);
|
|
const sigmaOmega = runEuclideanAlgorithm(field, field.buildMonomial(twoS, 1), syndrome, twoS);
|
|
if (sigmaOmega === null) {
|
|
return null;
|
|
}
|
|
const errorLocations = findErrorLocations(field, sigmaOmega[0]);
|
|
if (errorLocations == null) {
|
|
return null;
|
|
}
|
|
const errorMagnitudes = findErrorMagnitudes(field, sigmaOmega[1], errorLocations);
|
|
for (let i = 0; i < errorLocations.length; i++) {
|
|
const position = outputBytes.length - 1 - field.log(errorLocations[i]);
|
|
if (position < 0) {
|
|
return null;
|
|
}
|
|
outputBytes[position] = addOrSubtractGF(outputBytes[position], errorMagnitudes[i]);
|
|
}
|
|
return outputBytes;
|
|
}
|
|
|
|
const VERSIONS = [
|
|
{
|
|
infoBits: null,
|
|
versionNumber: 1,
|
|
alignmentPatternCenters: [],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 7,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 19 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 10,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 16 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 13,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 13 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 17,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 9 }],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: null,
|
|
versionNumber: 2,
|
|
alignmentPatternCenters: [6, 18],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 10,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 34 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 16,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 28 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 22 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 16 }],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: null,
|
|
versionNumber: 3,
|
|
alignmentPatternCenters: [6, 22],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 15,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 55 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 44 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 18,
|
|
ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 17 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 13 }],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: null,
|
|
versionNumber: 4,
|
|
alignmentPatternCenters: [6, 26],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 20,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 80 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 18,
|
|
ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 32 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 24 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 16,
|
|
ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 9 }],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: null,
|
|
versionNumber: 5,
|
|
alignmentPatternCenters: [6, 30],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [{ numBlocks: 1, dataCodewordsPerBlock: 108 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 43 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 18,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 11 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 12 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: null,
|
|
versionNumber: 6,
|
|
alignmentPatternCenters: [6, 34],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 18,
|
|
ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 68 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 16,
|
|
ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 27 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 19 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 15 }],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x07C94,
|
|
versionNumber: 7,
|
|
alignmentPatternCenters: [6, 22, 38],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 20,
|
|
ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 78 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 18,
|
|
ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 31 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 18,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 14 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 15 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 13 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 14 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x085BC,
|
|
versionNumber: 8,
|
|
alignmentPatternCenters: [6, 24, 42],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 97 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 38 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 39 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 18 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 19 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 14 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 15 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x09A99,
|
|
versionNumber: 9,
|
|
alignmentPatternCenters: [6, 26, 46],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [{ numBlocks: 2, dataCodewordsPerBlock: 116 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 36 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 37 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 20,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 16 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 17 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 12 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 13 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x0A4D3,
|
|
versionNumber: 10,
|
|
alignmentPatternCenters: [6, 28, 50],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 18,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 68 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 69 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 43 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 44 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 19 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 20 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x0BBF6,
|
|
versionNumber: 11,
|
|
alignmentPatternCenters: [6, 30, 54],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 20,
|
|
ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 81 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 50 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 51 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 22 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 23 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 12 },
|
|
{ numBlocks: 8, dataCodewordsPerBlock: 13 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x0C762,
|
|
versionNumber: 12,
|
|
alignmentPatternCenters: [6, 32, 58],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 92 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 93 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 36 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 37 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 20 },
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 21 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 14 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 15 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x0D847,
|
|
versionNumber: 13,
|
|
alignmentPatternCenters: [6, 34, 62],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [{ numBlocks: 4, dataCodewordsPerBlock: 107 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [
|
|
{ numBlocks: 8, dataCodewordsPerBlock: 37 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 38 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 8, dataCodewordsPerBlock: 20 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 21 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [
|
|
{ numBlocks: 12, dataCodewordsPerBlock: 11 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 12 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x0E60D,
|
|
versionNumber: 14,
|
|
alignmentPatternCenters: [6, 26, 46, 66],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 115 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 116 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 40 },
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 41 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 20,
|
|
ecBlocks: [
|
|
{ numBlocks: 11, dataCodewordsPerBlock: 16 },
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 17 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 11, dataCodewordsPerBlock: 12 },
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 13 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x0F928,
|
|
versionNumber: 15,
|
|
alignmentPatternCenters: [6, 26, 48, 70],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 22,
|
|
ecBlocks: [
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 87 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 88 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 41 },
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 42 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 11, dataCodewordsPerBlock: 12 },
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 13 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x10B78,
|
|
versionNumber: 16,
|
|
alignmentPatternCenters: [6, 26, 50, 74],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 98 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 99 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 45 },
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 46 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [
|
|
{ numBlocks: 15, dataCodewordsPerBlock: 19 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 20 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 13, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x1145D,
|
|
versionNumber: 17,
|
|
alignmentPatternCenters: [6, 30, 54, 78],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 107 },
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 108 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 46 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 47 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 22 },
|
|
{ numBlocks: 15, dataCodewordsPerBlock: 23 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 14 },
|
|
{ numBlocks: 17, dataCodewordsPerBlock: 15 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x12A17,
|
|
versionNumber: 18,
|
|
alignmentPatternCenters: [6, 30, 56, 82],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 120 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 121 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 9, dataCodewordsPerBlock: 43 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 44 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 17, dataCodewordsPerBlock: 22 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 23 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 14 },
|
|
{ numBlocks: 19, dataCodewordsPerBlock: 15 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x13532,
|
|
versionNumber: 19,
|
|
alignmentPatternCenters: [6, 30, 58, 86],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 113 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 114 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 44 },
|
|
{ numBlocks: 11, dataCodewordsPerBlock: 45 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 17, dataCodewordsPerBlock: 21 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 22 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 9, dataCodewordsPerBlock: 13 },
|
|
{ numBlocks: 16, dataCodewordsPerBlock: 14 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x149A6,
|
|
versionNumber: 20,
|
|
alignmentPatternCenters: [6, 34, 62, 90],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 107 },
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 108 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 41 },
|
|
{ numBlocks: 13, dataCodewordsPerBlock: 42 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 15, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 15, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x15683,
|
|
versionNumber: 21,
|
|
alignmentPatternCenters: [6, 28, 50, 72, 94],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 116 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 117 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [{ numBlocks: 17, dataCodewordsPerBlock: 42 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 17, dataCodewordsPerBlock: 22 },
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 23 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 19, dataCodewordsPerBlock: 16 },
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 17 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x168C9,
|
|
versionNumber: 22,
|
|
alignmentPatternCenters: [6, 26, 50, 74, 98],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 111 },
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 112 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [{ numBlocks: 17, dataCodewordsPerBlock: 46 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 16, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 24,
|
|
ecBlocks: [{ numBlocks: 34, dataCodewordsPerBlock: 13 }],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x177EC,
|
|
versionNumber: 23,
|
|
alignmentPatternCenters: [6, 30, 54, 74, 102],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 121 },
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 122 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 47 },
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 48 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 11, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 16, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x18EC4,
|
|
versionNumber: 24,
|
|
alignmentPatternCenters: [6, 28, 54, 80, 106],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 117 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 118 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 45 },
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 46 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 11, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 16, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 30, dataCodewordsPerBlock: 16 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 17 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x191E1,
|
|
versionNumber: 25,
|
|
alignmentPatternCenters: [6, 32, 58, 84, 110],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 26,
|
|
ecBlocks: [
|
|
{ numBlocks: 8, dataCodewordsPerBlock: 106 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 107 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 8, dataCodewordsPerBlock: 47 },
|
|
{ numBlocks: 13, dataCodewordsPerBlock: 48 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 22, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 22, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 13, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x1AFAB,
|
|
versionNumber: 26,
|
|
alignmentPatternCenters: [6, 30, 58, 86, 114],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 114 },
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 115 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 19, dataCodewordsPerBlock: 46 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 47 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 28, dataCodewordsPerBlock: 22 },
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 23 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 33, dataCodewordsPerBlock: 16 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 17 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x1B08E,
|
|
versionNumber: 27,
|
|
alignmentPatternCenters: [6, 34, 62, 90, 118],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 8, dataCodewordsPerBlock: 122 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 123 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 22, dataCodewordsPerBlock: 45 },
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 46 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 8, dataCodewordsPerBlock: 23 },
|
|
{ numBlocks: 26, dataCodewordsPerBlock: 24 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 12, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 28, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x1CC1A,
|
|
versionNumber: 28,
|
|
alignmentPatternCenters: [6, 26, 50, 74, 98, 122],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 117 },
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 118 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 45 },
|
|
{ numBlocks: 23, dataCodewordsPerBlock: 46 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 31, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 11, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 31, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x1D33F,
|
|
versionNumber: 29,
|
|
alignmentPatternCenters: [6, 30, 54, 78, 102, 126],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 116 },
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 117 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 21, dataCodewordsPerBlock: 45 },
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 46 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 23 },
|
|
{ numBlocks: 37, dataCodewordsPerBlock: 24 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 19, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 26, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x1ED75,
|
|
versionNumber: 30,
|
|
alignmentPatternCenters: [6, 26, 52, 78, 104, 130],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 5, dataCodewordsPerBlock: 115 },
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 116 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 19, dataCodewordsPerBlock: 47 },
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 48 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 15, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 25, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 23, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 25, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x1F250,
|
|
versionNumber: 31,
|
|
alignmentPatternCenters: [6, 30, 56, 82, 108, 134],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 13, dataCodewordsPerBlock: 115 },
|
|
{ numBlocks: 3, dataCodewordsPerBlock: 116 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 46 },
|
|
{ numBlocks: 29, dataCodewordsPerBlock: 47 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 42, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 23, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 28, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x209D5,
|
|
versionNumber: 32,
|
|
alignmentPatternCenters: [6, 34, 60, 86, 112, 138],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [{ numBlocks: 17, dataCodewordsPerBlock: 115 }],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 46 },
|
|
{ numBlocks: 23, dataCodewordsPerBlock: 47 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 35, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 19, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 35, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x216F0,
|
|
versionNumber: 33,
|
|
alignmentPatternCenters: [6, 30, 58, 86, 114, 142],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 17, dataCodewordsPerBlock: 115 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 116 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 46 },
|
|
{ numBlocks: 21, dataCodewordsPerBlock: 47 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 29, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 19, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 11, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 46, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x228BA,
|
|
versionNumber: 34,
|
|
alignmentPatternCenters: [6, 34, 62, 90, 118, 146],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 13, dataCodewordsPerBlock: 115 },
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 116 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 46 },
|
|
{ numBlocks: 23, dataCodewordsPerBlock: 47 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 44, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 59, dataCodewordsPerBlock: 16 },
|
|
{ numBlocks: 1, dataCodewordsPerBlock: 17 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x2379F,
|
|
versionNumber: 35,
|
|
alignmentPatternCenters: [6, 30, 54, 78, 102, 126, 150],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 12, dataCodewordsPerBlock: 121 },
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 122 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 12, dataCodewordsPerBlock: 47 },
|
|
{ numBlocks: 26, dataCodewordsPerBlock: 48 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 39, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 22, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 41, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x24B0B,
|
|
versionNumber: 36,
|
|
alignmentPatternCenters: [6, 24, 50, 76, 102, 128, 154],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 121 },
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 122 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 47 },
|
|
{ numBlocks: 34, dataCodewordsPerBlock: 48 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 46, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 2, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 64, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x2542E,
|
|
versionNumber: 37,
|
|
alignmentPatternCenters: [6, 28, 54, 80, 106, 132, 158],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 17, dataCodewordsPerBlock: 122 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 123 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 29, dataCodewordsPerBlock: 46 },
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 47 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 49, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 24, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 46, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x26A64,
|
|
versionNumber: 38,
|
|
alignmentPatternCenters: [6, 32, 58, 84, 110, 136, 162],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 122 },
|
|
{ numBlocks: 18, dataCodewordsPerBlock: 123 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 13, dataCodewordsPerBlock: 46 },
|
|
{ numBlocks: 32, dataCodewordsPerBlock: 47 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 48, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 14, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 42, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 32, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x27541,
|
|
versionNumber: 39,
|
|
alignmentPatternCenters: [6, 26, 54, 82, 110, 138, 166],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 20, dataCodewordsPerBlock: 117 },
|
|
{ numBlocks: 4, dataCodewordsPerBlock: 118 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 40, dataCodewordsPerBlock: 47 },
|
|
{ numBlocks: 7, dataCodewordsPerBlock: 48 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 43, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 22, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 10, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 67, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
infoBits: 0x28C69,
|
|
versionNumber: 40,
|
|
alignmentPatternCenters: [6, 30, 58, 86, 114, 142, 170],
|
|
errorCorrectionLevels: [
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 19, dataCodewordsPerBlock: 118 },
|
|
{ numBlocks: 6, dataCodewordsPerBlock: 119 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 28,
|
|
ecBlocks: [
|
|
{ numBlocks: 18, dataCodewordsPerBlock: 47 },
|
|
{ numBlocks: 31, dataCodewordsPerBlock: 48 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 34, dataCodewordsPerBlock: 24 },
|
|
{ numBlocks: 34, dataCodewordsPerBlock: 25 },
|
|
],
|
|
},
|
|
{
|
|
ecCodewordsPerBlock: 30,
|
|
ecBlocks: [
|
|
{ numBlocks: 20, dataCodewordsPerBlock: 15 },
|
|
{ numBlocks: 61, dataCodewordsPerBlock: 16 },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
// tslint:disable:no-bitwise
|
|
function numBitsDiffering(x, y) {
|
|
let z = x ^ y;
|
|
let bitCount = 0;
|
|
while (z) {
|
|
bitCount++;
|
|
z &= z - 1;
|
|
}
|
|
return bitCount;
|
|
}
|
|
function pushBit(bit, byte) {
|
|
return (byte << 1) | bit;
|
|
}
|
|
// tslint:enable:no-bitwise
|
|
const FORMAT_INFO_TABLE = [
|
|
{ bits: 0x5412, formatInfo: { errorCorrectionLevel: 1, dataMask: 0 } },
|
|
{ bits: 0x5125, formatInfo: { errorCorrectionLevel: 1, dataMask: 1 } },
|
|
{ bits: 0x5E7C, formatInfo: { errorCorrectionLevel: 1, dataMask: 2 } },
|
|
{ bits: 0x5B4B, formatInfo: { errorCorrectionLevel: 1, dataMask: 3 } },
|
|
{ bits: 0x45F9, formatInfo: { errorCorrectionLevel: 1, dataMask: 4 } },
|
|
{ bits: 0x40CE, formatInfo: { errorCorrectionLevel: 1, dataMask: 5 } },
|
|
{ bits: 0x4F97, formatInfo: { errorCorrectionLevel: 1, dataMask: 6 } },
|
|
{ bits: 0x4AA0, formatInfo: { errorCorrectionLevel: 1, dataMask: 7 } },
|
|
{ bits: 0x77C4, formatInfo: { errorCorrectionLevel: 0, dataMask: 0 } },
|
|
{ bits: 0x72F3, formatInfo: { errorCorrectionLevel: 0, dataMask: 1 } },
|
|
{ bits: 0x7DAA, formatInfo: { errorCorrectionLevel: 0, dataMask: 2 } },
|
|
{ bits: 0x789D, formatInfo: { errorCorrectionLevel: 0, dataMask: 3 } },
|
|
{ bits: 0x662F, formatInfo: { errorCorrectionLevel: 0, dataMask: 4 } },
|
|
{ bits: 0x6318, formatInfo: { errorCorrectionLevel: 0, dataMask: 5 } },
|
|
{ bits: 0x6C41, formatInfo: { errorCorrectionLevel: 0, dataMask: 6 } },
|
|
{ bits: 0x6976, formatInfo: { errorCorrectionLevel: 0, dataMask: 7 } },
|
|
{ bits: 0x1689, formatInfo: { errorCorrectionLevel: 3, dataMask: 0 } },
|
|
{ bits: 0x13BE, formatInfo: { errorCorrectionLevel: 3, dataMask: 1 } },
|
|
{ bits: 0x1CE7, formatInfo: { errorCorrectionLevel: 3, dataMask: 2 } },
|
|
{ bits: 0x19D0, formatInfo: { errorCorrectionLevel: 3, dataMask: 3 } },
|
|
{ bits: 0x0762, formatInfo: { errorCorrectionLevel: 3, dataMask: 4 } },
|
|
{ bits: 0x0255, formatInfo: { errorCorrectionLevel: 3, dataMask: 5 } },
|
|
{ bits: 0x0D0C, formatInfo: { errorCorrectionLevel: 3, dataMask: 6 } },
|
|
{ bits: 0x083B, formatInfo: { errorCorrectionLevel: 3, dataMask: 7 } },
|
|
{ bits: 0x355F, formatInfo: { errorCorrectionLevel: 2, dataMask: 0 } },
|
|
{ bits: 0x3068, formatInfo: { errorCorrectionLevel: 2, dataMask: 1 } },
|
|
{ bits: 0x3F31, formatInfo: { errorCorrectionLevel: 2, dataMask: 2 } },
|
|
{ bits: 0x3A06, formatInfo: { errorCorrectionLevel: 2, dataMask: 3 } },
|
|
{ bits: 0x24B4, formatInfo: { errorCorrectionLevel: 2, dataMask: 4 } },
|
|
{ bits: 0x2183, formatInfo: { errorCorrectionLevel: 2, dataMask: 5 } },
|
|
{ bits: 0x2EDA, formatInfo: { errorCorrectionLevel: 2, dataMask: 6 } },
|
|
{ bits: 0x2BED, formatInfo: { errorCorrectionLevel: 2, dataMask: 7 } },
|
|
];
|
|
const DATA_MASKS = [
|
|
(p) => ((p.y + p.x) % 2) === 0,
|
|
(p) => (p.y % 2) === 0,
|
|
(p) => p.x % 3 === 0,
|
|
(p) => (p.y + p.x) % 3 === 0,
|
|
(p) => (Math.floor(p.y / 2) + Math.floor(p.x / 3)) % 2 === 0,
|
|
(p) => ((p.x * p.y) % 2) + ((p.x * p.y) % 3) === 0,
|
|
(p) => ((((p.y * p.x) % 2) + (p.y * p.x) % 3) % 2) === 0,
|
|
(p) => ((((p.y + p.x) % 2) + (p.y * p.x) % 3) % 2) === 0,
|
|
];
|
|
function buildFunctionPatternMask(version) {
|
|
const dimension = 17 + 4 * version.versionNumber;
|
|
const matrix = BitMatrix.createEmpty(dimension, dimension);
|
|
matrix.setRegion(0, 0, 9, 9, true); // Top left finder pattern + separator + format
|
|
matrix.setRegion(dimension - 8, 0, 8, 9, true); // Top right finder pattern + separator + format
|
|
matrix.setRegion(0, dimension - 8, 9, 8, true); // Bottom left finder pattern + separator + format
|
|
// Alignment patterns
|
|
for (const x of version.alignmentPatternCenters) {
|
|
for (const y of version.alignmentPatternCenters) {
|
|
if (!(x === 6 && y === 6 || x === 6 && y === dimension - 7 || x === dimension - 7 && y === 6)) {
|
|
matrix.setRegion(x - 2, y - 2, 5, 5, true);
|
|
}
|
|
}
|
|
}
|
|
matrix.setRegion(6, 9, 1, dimension - 17, true); // Vertical timing pattern
|
|
matrix.setRegion(9, 6, dimension - 17, 1, true); // Horizontal timing pattern
|
|
if (version.versionNumber > 6) {
|
|
matrix.setRegion(dimension - 11, 0, 3, 6, true); // Version info, top right
|
|
matrix.setRegion(0, dimension - 11, 6, 3, true); // Version info, bottom left
|
|
}
|
|
return matrix;
|
|
}
|
|
function readCodewords(matrix, version, formatInfo) {
|
|
const dataMask = DATA_MASKS[formatInfo.dataMask];
|
|
const dimension = matrix.height;
|
|
const functionPatternMask = buildFunctionPatternMask(version);
|
|
const codewords = [];
|
|
let currentByte = 0;
|
|
let bitsRead = 0;
|
|
// Read columns in pairs, from right to left
|
|
let readingUp = true;
|
|
for (let columnIndex = dimension - 1; columnIndex > 0; columnIndex -= 2) {
|
|
if (columnIndex === 6) { // Skip whole column with vertical alignment pattern;
|
|
columnIndex--;
|
|
}
|
|
for (let i = 0; i < dimension; i++) {
|
|
const y = readingUp ? dimension - 1 - i : i;
|
|
for (let columnOffset = 0; columnOffset < 2; columnOffset++) {
|
|
const x = columnIndex - columnOffset;
|
|
if (!functionPatternMask.get(x, y)) {
|
|
bitsRead++;
|
|
let bit = matrix.get(x, y);
|
|
if (dataMask({ y, x })) {
|
|
bit = !bit;
|
|
}
|
|
currentByte = pushBit(bit, currentByte);
|
|
if (bitsRead === 8) { // Whole bytes
|
|
codewords.push(currentByte);
|
|
bitsRead = 0;
|
|
currentByte = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
readingUp = !readingUp;
|
|
}
|
|
return codewords;
|
|
}
|
|
function readVersion(matrix) {
|
|
const dimension = matrix.height;
|
|
const provisionalVersion = Math.floor((dimension - 17) / 4);
|
|
if (provisionalVersion <= 6) { // 6 and under dont have version info in the QR code
|
|
return VERSIONS[provisionalVersion - 1];
|
|
}
|
|
let topRightVersionBits = 0;
|
|
for (let y = 5; y >= 0; y--) {
|
|
for (let x = dimension - 9; x >= dimension - 11; x--) {
|
|
topRightVersionBits = pushBit(matrix.get(x, y), topRightVersionBits);
|
|
}
|
|
}
|
|
let bottomLeftVersionBits = 0;
|
|
for (let x = 5; x >= 0; x--) {
|
|
for (let y = dimension - 9; y >= dimension - 11; y--) {
|
|
bottomLeftVersionBits = pushBit(matrix.get(x, y), bottomLeftVersionBits);
|
|
}
|
|
}
|
|
let bestDifference = Infinity;
|
|
let bestVersion;
|
|
for (const version of VERSIONS) {
|
|
if (version.infoBits === topRightVersionBits || version.infoBits === bottomLeftVersionBits) {
|
|
return version;
|
|
}
|
|
let difference = numBitsDiffering(topRightVersionBits, version.infoBits);
|
|
if (difference < bestDifference) {
|
|
bestVersion = version;
|
|
bestDifference = difference;
|
|
}
|
|
difference = numBitsDiffering(bottomLeftVersionBits, version.infoBits);
|
|
if (difference < bestDifference) {
|
|
bestVersion = version;
|
|
bestDifference = difference;
|
|
}
|
|
}
|
|
// We can tolerate up to 3 bits of error since no two version info codewords will
|
|
// differ in less than 8 bits.
|
|
if (bestDifference <= 3) {
|
|
return bestVersion;
|
|
}
|
|
}
|
|
function readFormatInformation(matrix) {
|
|
let topLeftFormatInfoBits = 0;
|
|
for (let x = 0; x <= 8; x++) {
|
|
if (x !== 6) { // Skip timing pattern bit
|
|
topLeftFormatInfoBits = pushBit(matrix.get(x, 8), topLeftFormatInfoBits);
|
|
}
|
|
}
|
|
for (let y = 7; y >= 0; y--) {
|
|
if (y !== 6) { // Skip timing pattern bit
|
|
topLeftFormatInfoBits = pushBit(matrix.get(8, y), topLeftFormatInfoBits);
|
|
}
|
|
}
|
|
const dimension = matrix.height;
|
|
let topRightBottomRightFormatInfoBits = 0;
|
|
for (let y = dimension - 1; y >= dimension - 7; y--) { // bottom left
|
|
topRightBottomRightFormatInfoBits = pushBit(matrix.get(8, y), topRightBottomRightFormatInfoBits);
|
|
}
|
|
for (let x = dimension - 8; x < dimension; x++) { // top right
|
|
topRightBottomRightFormatInfoBits = pushBit(matrix.get(x, 8), topRightBottomRightFormatInfoBits);
|
|
}
|
|
let bestDifference = Infinity;
|
|
let bestFormatInfo = null;
|
|
for (const { bits, formatInfo } of FORMAT_INFO_TABLE) {
|
|
if (bits === topLeftFormatInfoBits || bits === topRightBottomRightFormatInfoBits) {
|
|
return formatInfo;
|
|
}
|
|
let difference = numBitsDiffering(topLeftFormatInfoBits, bits);
|
|
if (difference < bestDifference) {
|
|
bestFormatInfo = formatInfo;
|
|
bestDifference = difference;
|
|
}
|
|
if (topLeftFormatInfoBits !== topRightBottomRightFormatInfoBits) { // also try the other option
|
|
difference = numBitsDiffering(topRightBottomRightFormatInfoBits, bits);
|
|
if (difference < bestDifference) {
|
|
bestFormatInfo = formatInfo;
|
|
bestDifference = difference;
|
|
}
|
|
}
|
|
}
|
|
// Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits differing means we found a match
|
|
if (bestDifference <= 3) {
|
|
return bestFormatInfo;
|
|
}
|
|
return null;
|
|
}
|
|
function getDataBlocks(codewords, version, ecLevel) {
|
|
const ecInfo = version.errorCorrectionLevels[ecLevel];
|
|
const dataBlocks = [];
|
|
let totalCodewords = 0;
|
|
ecInfo.ecBlocks.forEach(block => {
|
|
for (let i = 0; i < block.numBlocks; i++) {
|
|
dataBlocks.push({ numDataCodewords: block.dataCodewordsPerBlock, codewords: [] });
|
|
totalCodewords += block.dataCodewordsPerBlock + ecInfo.ecCodewordsPerBlock;
|
|
}
|
|
});
|
|
// In some cases the QR code will be malformed enough that we pull off more or less than we should.
|
|
// If we pull off less there's nothing we can do.
|
|
// If we pull off more we can safely truncate
|
|
if (codewords.length < totalCodewords) {
|
|
return null;
|
|
}
|
|
codewords = codewords.slice(0, totalCodewords);
|
|
const shortBlockSize = ecInfo.ecBlocks[0].dataCodewordsPerBlock;
|
|
// Pull codewords to fill the blocks up to the minimum size
|
|
for (let i = 0; i < shortBlockSize; i++) {
|
|
for (const dataBlock of dataBlocks) {
|
|
dataBlock.codewords.push(codewords.shift());
|
|
}
|
|
}
|
|
// If there are any large blocks, pull codewords to fill the last element of those
|
|
if (ecInfo.ecBlocks.length > 1) {
|
|
const smallBlockCount = ecInfo.ecBlocks[0].numBlocks;
|
|
const largeBlockCount = ecInfo.ecBlocks[1].numBlocks;
|
|
for (let i = 0; i < largeBlockCount; i++) {
|
|
dataBlocks[smallBlockCount + i].codewords.push(codewords.shift());
|
|
}
|
|
}
|
|
// Add the rest of the codewords to the blocks. These are the error correction codewords.
|
|
while (codewords.length > 0) {
|
|
for (const dataBlock of dataBlocks) {
|
|
dataBlock.codewords.push(codewords.shift());
|
|
}
|
|
}
|
|
return dataBlocks;
|
|
}
|
|
function decodeMatrix(matrix) {
|
|
const version = readVersion(matrix);
|
|
if (!version) {
|
|
return null;
|
|
}
|
|
const formatInfo = readFormatInformation(matrix);
|
|
if (!formatInfo) {
|
|
return null;
|
|
}
|
|
const codewords = readCodewords(matrix, version, formatInfo);
|
|
const dataBlocks = getDataBlocks(codewords, version, formatInfo.errorCorrectionLevel);
|
|
if (!dataBlocks) {
|
|
return null;
|
|
}
|
|
// Count total number of data bytes
|
|
const totalBytes = dataBlocks.reduce((a, b) => a + b.numDataCodewords, 0);
|
|
const resultBytes = new Uint8ClampedArray(totalBytes);
|
|
let resultIndex = 0;
|
|
for (const dataBlock of dataBlocks) {
|
|
const correctedBytes = decode$1(dataBlock.codewords, dataBlock.codewords.length - dataBlock.numDataCodewords);
|
|
if (!correctedBytes) {
|
|
return null;
|
|
}
|
|
for (let i = 0; i < dataBlock.numDataCodewords; i++) {
|
|
resultBytes[resultIndex++] = correctedBytes[i];
|
|
}
|
|
}
|
|
try {
|
|
return decode$2(resultBytes, version.versionNumber);
|
|
}
|
|
catch (_a) {
|
|
return null;
|
|
}
|
|
}
|
|
function decode(matrix) {
|
|
if (matrix == null) {
|
|
return null;
|
|
}
|
|
const result = decodeMatrix(matrix);
|
|
if (result) {
|
|
return result;
|
|
}
|
|
// Decoding didn't work, try mirroring the QR across the topLeft -> bottomRight line.
|
|
for (let x = 0; x < matrix.width; x++) {
|
|
for (let y = x + 1; y < matrix.height; y++) {
|
|
if (matrix.get(x, y) !== matrix.get(y, x)) {
|
|
matrix.set(x, y, !matrix.get(x, y));
|
|
matrix.set(y, x, !matrix.get(y, x));
|
|
}
|
|
}
|
|
}
|
|
return decodeMatrix(matrix);
|
|
}
|
|
|
|
function squareToQuadrilateral(p1, p2, p3, p4) {
|
|
const dx3 = p1.x - p2.x + p3.x - p4.x;
|
|
const dy3 = p1.y - p2.y + p3.y - p4.y;
|
|
if (dx3 === 0 && dy3 === 0) { // Affine
|
|
return {
|
|
a11: p2.x - p1.x,
|
|
a12: p2.y - p1.y,
|
|
a13: 0,
|
|
a21: p3.x - p2.x,
|
|
a22: p3.y - p2.y,
|
|
a23: 0,
|
|
a31: p1.x,
|
|
a32: p1.y,
|
|
a33: 1,
|
|
};
|
|
}
|
|
else {
|
|
const dx1 = p2.x - p3.x;
|
|
const dx2 = p4.x - p3.x;
|
|
const dy1 = p2.y - p3.y;
|
|
const dy2 = p4.y - p3.y;
|
|
const denominator = dx1 * dy2 - dx2 * dy1;
|
|
const a13 = (dx3 * dy2 - dx2 * dy3) / denominator;
|
|
const a23 = (dx1 * dy3 - dx3 * dy1) / denominator;
|
|
return {
|
|
a11: p2.x - p1.x + a13 * p2.x,
|
|
a12: p2.y - p1.y + a13 * p2.y,
|
|
a13,
|
|
a21: p4.x - p1.x + a23 * p4.x,
|
|
a22: p4.y - p1.y + a23 * p4.y,
|
|
a23,
|
|
a31: p1.x,
|
|
a32: p1.y,
|
|
a33: 1,
|
|
};
|
|
}
|
|
}
|
|
function quadrilateralToSquare(p1, p2, p3, p4) {
|
|
// Here, the adjoint serves as the inverse:
|
|
const sToQ = squareToQuadrilateral(p1, p2, p3, p4);
|
|
return {
|
|
a11: sToQ.a22 * sToQ.a33 - sToQ.a23 * sToQ.a32,
|
|
a12: sToQ.a13 * sToQ.a32 - sToQ.a12 * sToQ.a33,
|
|
a13: sToQ.a12 * sToQ.a23 - sToQ.a13 * sToQ.a22,
|
|
a21: sToQ.a23 * sToQ.a31 - sToQ.a21 * sToQ.a33,
|
|
a22: sToQ.a11 * sToQ.a33 - sToQ.a13 * sToQ.a31,
|
|
a23: sToQ.a13 * sToQ.a21 - sToQ.a11 * sToQ.a23,
|
|
a31: sToQ.a21 * sToQ.a32 - sToQ.a22 * sToQ.a31,
|
|
a32: sToQ.a12 * sToQ.a31 - sToQ.a11 * sToQ.a32,
|
|
a33: sToQ.a11 * sToQ.a22 - sToQ.a12 * sToQ.a21,
|
|
};
|
|
}
|
|
function times(a, b) {
|
|
return {
|
|
a11: a.a11 * b.a11 + a.a21 * b.a12 + a.a31 * b.a13,
|
|
a12: a.a12 * b.a11 + a.a22 * b.a12 + a.a32 * b.a13,
|
|
a13: a.a13 * b.a11 + a.a23 * b.a12 + a.a33 * b.a13,
|
|
a21: a.a11 * b.a21 + a.a21 * b.a22 + a.a31 * b.a23,
|
|
a22: a.a12 * b.a21 + a.a22 * b.a22 + a.a32 * b.a23,
|
|
a23: a.a13 * b.a21 + a.a23 * b.a22 + a.a33 * b.a23,
|
|
a31: a.a11 * b.a31 + a.a21 * b.a32 + a.a31 * b.a33,
|
|
a32: a.a12 * b.a31 + a.a22 * b.a32 + a.a32 * b.a33,
|
|
a33: a.a13 * b.a31 + a.a23 * b.a32 + a.a33 * b.a33,
|
|
};
|
|
}
|
|
function extract(image, location) {
|
|
const qToS = quadrilateralToSquare({ x: 3.5, y: 3.5 }, { x: location.dimension - 3.5, y: 3.5 }, { x: location.dimension - 6.5, y: location.dimension - 6.5 }, { x: 3.5, y: location.dimension - 3.5 });
|
|
const sToQ = squareToQuadrilateral(location.topLeft, location.topRight, location.alignmentPattern, location.bottomLeft);
|
|
const transform = times(sToQ, qToS);
|
|
const matrix = BitMatrix.createEmpty(location.dimension, location.dimension);
|
|
const mappingFunction = (x, y) => {
|
|
const denominator = transform.a13 * x + transform.a23 * y + transform.a33;
|
|
return {
|
|
x: (transform.a11 * x + transform.a21 * y + transform.a31) / denominator,
|
|
y: (transform.a12 * x + transform.a22 * y + transform.a32) / denominator,
|
|
};
|
|
};
|
|
for (let y = 0; y < location.dimension; y++) {
|
|
for (let x = 0; x < location.dimension; x++) {
|
|
const xValue = x + 0.5;
|
|
const yValue = y + 0.5;
|
|
const sourcePixel = mappingFunction(xValue, yValue);
|
|
matrix.set(x, y, image.get(Math.floor(sourcePixel.x), Math.floor(sourcePixel.y)));
|
|
}
|
|
}
|
|
return {
|
|
matrix,
|
|
mappingFunction,
|
|
};
|
|
}
|
|
|
|
const MAX_FINDERPATTERNS_TO_SEARCH = 5;
|
|
const MIN_QUAD_RATIO = 0.5;
|
|
const MAX_QUAD_RATIO = 1.5;
|
|
const distance = (a, b) => Math.sqrt(Math.pow((b.x - a.x), 2) + Math.pow((b.y - a.y), 2));
|
|
function sum(values) {
|
|
return values.reduce((a, b) => a + b);
|
|
}
|
|
// Takes three finder patterns and organizes them into topLeft, topRight, etc
|
|
function reorderFinderPatterns(pattern1, pattern2, pattern3) {
|
|
// Find distances between pattern centers
|
|
const oneTwoDistance = distance(pattern1, pattern2);
|
|
const twoThreeDistance = distance(pattern2, pattern3);
|
|
const oneThreeDistance = distance(pattern1, pattern3);
|
|
let bottomLeft;
|
|
let topLeft;
|
|
let topRight;
|
|
// Assume one closest to other two is B; A and C will just be guesses at first
|
|
if (twoThreeDistance >= oneTwoDistance && twoThreeDistance >= oneThreeDistance) {
|
|
[bottomLeft, topLeft, topRight] = [pattern2, pattern1, pattern3];
|
|
}
|
|
else if (oneThreeDistance >= twoThreeDistance && oneThreeDistance >= oneTwoDistance) {
|
|
[bottomLeft, topLeft, topRight] = [pattern1, pattern2, pattern3];
|
|
}
|
|
else {
|
|
[bottomLeft, topLeft, topRight] = [pattern1, pattern3, pattern2];
|
|
}
|
|
// Use cross product to figure out whether bottomLeft (A) and topRight (C) are correct or flipped in relation to topLeft (B)
|
|
// This asks whether BC x BA has a positive z component, which is the arrangement we want. If it's negative, then
|
|
// we've got it flipped around and should swap topRight and bottomLeft.
|
|
if (((topRight.x - topLeft.x) * (bottomLeft.y - topLeft.y)) - ((topRight.y - topLeft.y) * (bottomLeft.x - topLeft.x)) < 0) {
|
|
[bottomLeft, topRight] = [topRight, bottomLeft];
|
|
}
|
|
return { bottomLeft, topLeft, topRight };
|
|
}
|
|
// Computes the dimension (number of modules on a side) of the QR Code based on the position of the finder patterns
|
|
function computeDimension(topLeft, topRight, bottomLeft, matrix) {
|
|
const moduleSize = (sum(countBlackWhiteRun(topLeft, bottomLeft, matrix, 5)) / 7 + // Divide by 7 since the ratio is 1:1:3:1:1
|
|
sum(countBlackWhiteRun(topLeft, topRight, matrix, 5)) / 7 +
|
|
sum(countBlackWhiteRun(bottomLeft, topLeft, matrix, 5)) / 7 +
|
|
sum(countBlackWhiteRun(topRight, topLeft, matrix, 5)) / 7) / 4;
|
|
if (moduleSize < 1) {
|
|
throw new Error("Invalid module size");
|
|
}
|
|
const topDimension = Math.round(distance(topLeft, topRight) / moduleSize);
|
|
const sideDimension = Math.round(distance(topLeft, bottomLeft) / moduleSize);
|
|
let dimension = Math.floor((topDimension + sideDimension) / 2) + 7;
|
|
switch (dimension % 4) {
|
|
case 0:
|
|
dimension++;
|
|
break;
|
|
case 2:
|
|
dimension--;
|
|
break;
|
|
}
|
|
return { dimension, moduleSize };
|
|
}
|
|
// Takes an origin point and an end point and counts the sizes of the black white run from the origin towards the end point.
|
|
// Returns an array of elements, representing the pixel size of the black white run.
|
|
// Uses a variant of http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
|
|
function countBlackWhiteRunTowardsPoint(origin, end, matrix, length) {
|
|
const switchPoints = [{ x: Math.floor(origin.x), y: Math.floor(origin.y) }];
|
|
const steep = Math.abs(end.y - origin.y) > Math.abs(end.x - origin.x);
|
|
let fromX;
|
|
let fromY;
|
|
let toX;
|
|
let toY;
|
|
if (steep) {
|
|
fromX = Math.floor(origin.y);
|
|
fromY = Math.floor(origin.x);
|
|
toX = Math.floor(end.y);
|
|
toY = Math.floor(end.x);
|
|
}
|
|
else {
|
|
fromX = Math.floor(origin.x);
|
|
fromY = Math.floor(origin.y);
|
|
toX = Math.floor(end.x);
|
|
toY = Math.floor(end.y);
|
|
}
|
|
const dx = Math.abs(toX - fromX);
|
|
const dy = Math.abs(toY - fromY);
|
|
let error = Math.floor(-dx / 2);
|
|
const xStep = fromX < toX ? 1 : -1;
|
|
const yStep = fromY < toY ? 1 : -1;
|
|
let currentPixel = true;
|
|
// Loop up until x == toX, but not beyond
|
|
for (let x = fromX, y = fromY; x !== toX + xStep; x += xStep) {
|
|
// Does current pixel mean we have moved white to black or vice versa?
|
|
// Scanning black in state 0,2 and white in state 1, so if we find the wrong
|
|
// color, advance to next state or end if we are in state 2 already
|
|
const realX = steep ? y : x;
|
|
const realY = steep ? x : y;
|
|
if (matrix.get(realX, realY) !== currentPixel) {
|
|
currentPixel = !currentPixel;
|
|
switchPoints.push({ x: realX, y: realY });
|
|
if (switchPoints.length === length + 1) {
|
|
break;
|
|
}
|
|
}
|
|
error += dy;
|
|
if (error > 0) {
|
|
if (y === toY) {
|
|
break;
|
|
}
|
|
y += yStep;
|
|
error -= dx;
|
|
}
|
|
}
|
|
const distances = [];
|
|
for (let i = 0; i < length; i++) {
|
|
if (switchPoints[i] && switchPoints[i + 1]) {
|
|
distances.push(distance(switchPoints[i], switchPoints[i + 1]));
|
|
}
|
|
else {
|
|
distances.push(0);
|
|
}
|
|
}
|
|
return distances;
|
|
}
|
|
// Takes an origin point and an end point and counts the sizes of the black white run in the origin point
|
|
// along the line that intersects with the end point. Returns an array of elements, representing the pixel sizes
|
|
// of the black white run. Takes a length which represents the number of switches from black to white to look for.
|
|
function countBlackWhiteRun(origin, end, matrix, length) {
|
|
const rise = end.y - origin.y;
|
|
const run = end.x - origin.x;
|
|
const towardsEnd = countBlackWhiteRunTowardsPoint(origin, end, matrix, Math.ceil(length / 2));
|
|
const awayFromEnd = countBlackWhiteRunTowardsPoint(origin, { x: origin.x - run, y: origin.y - rise }, matrix, Math.ceil(length / 2));
|
|
const middleValue = towardsEnd.shift() + awayFromEnd.shift() - 1; // Substract one so we don't double count a pixel
|
|
return awayFromEnd.concat(middleValue).concat(...towardsEnd);
|
|
}
|
|
// Takes in a black white run and an array of expected ratios. Returns the average size of the run as well as the "error" -
|
|
// that is the amount the run diverges from the expected ratio
|
|
function scoreBlackWhiteRun(sequence, ratios) {
|
|
const averageSize = sum(sequence) / sum(ratios);
|
|
let error = 0;
|
|
ratios.forEach((ratio, i) => {
|
|
error += Math.pow((sequence[i] - ratio * averageSize), 2);
|
|
});
|
|
return { averageSize, error };
|
|
}
|
|
// Takes an X,Y point and an array of sizes and scores the point against those ratios.
|
|
// For example for a finder pattern takes the ratio list of 1:1:3:1:1 and checks horizontal, vertical and diagonal ratios
|
|
// against that.
|
|
function scorePattern(point, ratios, matrix) {
|
|
try {
|
|
const horizontalRun = countBlackWhiteRun(point, { x: -1, y: point.y }, matrix, ratios.length);
|
|
const verticalRun = countBlackWhiteRun(point, { x: point.x, y: -1 }, matrix, ratios.length);
|
|
const topLeftPoint = {
|
|
x: Math.max(0, point.x - point.y) - 1,
|
|
y: Math.max(0, point.y - point.x) - 1,
|
|
};
|
|
const topLeftBottomRightRun = countBlackWhiteRun(point, topLeftPoint, matrix, ratios.length);
|
|
const bottomLeftPoint = {
|
|
x: Math.min(matrix.width, point.x + point.y) + 1,
|
|
y: Math.min(matrix.height, point.y + point.x) + 1,
|
|
};
|
|
const bottomLeftTopRightRun = countBlackWhiteRun(point, bottomLeftPoint, matrix, ratios.length);
|
|
const horzError = scoreBlackWhiteRun(horizontalRun, ratios);
|
|
const vertError = scoreBlackWhiteRun(verticalRun, ratios);
|
|
const diagDownError = scoreBlackWhiteRun(topLeftBottomRightRun, ratios);
|
|
const diagUpError = scoreBlackWhiteRun(bottomLeftTopRightRun, ratios);
|
|
const ratioError = Math.sqrt(horzError.error * horzError.error +
|
|
vertError.error * vertError.error +
|
|
diagDownError.error * diagDownError.error +
|
|
diagUpError.error * diagUpError.error);
|
|
const avgSize = (horzError.averageSize + vertError.averageSize + diagDownError.averageSize + diagUpError.averageSize) / 4;
|
|
const sizeError = (Math.pow((horzError.averageSize - avgSize), 2) +
|
|
Math.pow((vertError.averageSize - avgSize), 2) +
|
|
Math.pow((diagDownError.averageSize - avgSize), 2) +
|
|
Math.pow((diagUpError.averageSize - avgSize), 2)) / avgSize;
|
|
return ratioError + sizeError;
|
|
}
|
|
catch (_a) {
|
|
return Infinity;
|
|
}
|
|
}
|
|
function recenterLocation(matrix, p) {
|
|
let leftX = Math.round(p.x);
|
|
while (matrix.get(leftX, Math.round(p.y))) {
|
|
leftX--;
|
|
}
|
|
let rightX = Math.round(p.x);
|
|
while (matrix.get(rightX, Math.round(p.y))) {
|
|
rightX++;
|
|
}
|
|
const x = (leftX + rightX) / 2;
|
|
let topY = Math.round(p.y);
|
|
while (matrix.get(Math.round(x), topY)) {
|
|
topY--;
|
|
}
|
|
let bottomY = Math.round(p.y);
|
|
while (matrix.get(Math.round(x), bottomY)) {
|
|
bottomY++;
|
|
}
|
|
const y = (topY + bottomY) / 2;
|
|
return { x, y };
|
|
}
|
|
function locate(matrix) {
|
|
const finderPatternQuads = [];
|
|
let activeFinderPatternQuads = [];
|
|
const alignmentPatternQuads = [];
|
|
let activeAlignmentPatternQuads = [];
|
|
for (let y = 0; y <= matrix.height; y++) {
|
|
let length = 0;
|
|
let lastBit = false;
|
|
let scans = [0, 0, 0, 0, 0];
|
|
for (let x = -1; x <= matrix.width; x++) {
|
|
const v = matrix.get(x, y);
|
|
if (v === lastBit) {
|
|
length++;
|
|
}
|
|
else {
|
|
scans = [scans[1], scans[2], scans[3], scans[4], length];
|
|
length = 1;
|
|
lastBit = v;
|
|
// Do the last 5 color changes ~ match the expected ratio for a finder pattern? 1:1:3:1:1 of b:w:b:w:b
|
|
const averageFinderPatternBlocksize = sum(scans) / 7;
|
|
const validFinderPattern = Math.abs(scans[0] - averageFinderPatternBlocksize) < averageFinderPatternBlocksize &&
|
|
Math.abs(scans[1] - averageFinderPatternBlocksize) < averageFinderPatternBlocksize &&
|
|
Math.abs(scans[2] - 3 * averageFinderPatternBlocksize) < 3 * averageFinderPatternBlocksize &&
|
|
Math.abs(scans[3] - averageFinderPatternBlocksize) < averageFinderPatternBlocksize &&
|
|
Math.abs(scans[4] - averageFinderPatternBlocksize) < averageFinderPatternBlocksize &&
|
|
!v; // And make sure the current pixel is white since finder patterns are bordered in white
|
|
// Do the last 3 color changes ~ match the expected ratio for an alignment pattern? 1:1:1 of w:b:w
|
|
const averageAlignmentPatternBlocksize = sum(scans.slice(-3)) / 3;
|
|
const validAlignmentPattern = Math.abs(scans[2] - averageAlignmentPatternBlocksize) < averageAlignmentPatternBlocksize &&
|
|
Math.abs(scans[3] - averageAlignmentPatternBlocksize) < averageAlignmentPatternBlocksize &&
|
|
Math.abs(scans[4] - averageAlignmentPatternBlocksize) < averageAlignmentPatternBlocksize &&
|
|
v; // Is the current pixel black since alignment patterns are bordered in black
|
|
if (validFinderPattern) {
|
|
// Compute the start and end x values of the large center black square
|
|
const endX = x - scans[3] - scans[4];
|
|
const startX = endX - scans[2];
|
|
const line = { startX, endX, y };
|
|
// Is there a quad directly above the current spot? If so, extend it with the new line. Otherwise, create a new quad with
|
|
// that line as the starting point.
|
|
const matchingQuads = activeFinderPatternQuads.filter(q => (startX >= q.bottom.startX && startX <= q.bottom.endX) ||
|
|
(endX >= q.bottom.startX && startX <= q.bottom.endX) ||
|
|
(startX <= q.bottom.startX && endX >= q.bottom.endX && ((scans[2] / (q.bottom.endX - q.bottom.startX)) < MAX_QUAD_RATIO &&
|
|
(scans[2] / (q.bottom.endX - q.bottom.startX)) > MIN_QUAD_RATIO)));
|
|
if (matchingQuads.length > 0) {
|
|
matchingQuads[0].bottom = line;
|
|
}
|
|
else {
|
|
activeFinderPatternQuads.push({ top: line, bottom: line });
|
|
}
|
|
}
|
|
if (validAlignmentPattern) {
|
|
// Compute the start and end x values of the center black square
|
|
const endX = x - scans[4];
|
|
const startX = endX - scans[3];
|
|
const line = { startX, y, endX };
|
|
// Is there a quad directly above the current spot? If so, extend it with the new line. Otherwise, create a new quad with
|
|
// that line as the starting point.
|
|
const matchingQuads = activeAlignmentPatternQuads.filter(q => (startX >= q.bottom.startX && startX <= q.bottom.endX) ||
|
|
(endX >= q.bottom.startX && startX <= q.bottom.endX) ||
|
|
(startX <= q.bottom.startX && endX >= q.bottom.endX && ((scans[2] / (q.bottom.endX - q.bottom.startX)) < MAX_QUAD_RATIO &&
|
|
(scans[2] / (q.bottom.endX - q.bottom.startX)) > MIN_QUAD_RATIO)));
|
|
if (matchingQuads.length > 0) {
|
|
matchingQuads[0].bottom = line;
|
|
}
|
|
else {
|
|
activeAlignmentPatternQuads.push({ top: line, bottom: line });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finderPatternQuads.push(...activeFinderPatternQuads.filter(q => q.bottom.y !== y && q.bottom.y - q.top.y >= 2));
|
|
activeFinderPatternQuads = activeFinderPatternQuads.filter(q => q.bottom.y === y);
|
|
alignmentPatternQuads.push(...activeAlignmentPatternQuads.filter(q => q.bottom.y !== y));
|
|
activeAlignmentPatternQuads = activeAlignmentPatternQuads.filter(q => q.bottom.y === y);
|
|
}
|
|
finderPatternQuads.push(...activeFinderPatternQuads.filter(q => q.bottom.y - q.top.y >= 2));
|
|
alignmentPatternQuads.push(...activeAlignmentPatternQuads);
|
|
// Refactored from cozmo/jsQR to (hopefully) circumvent an issue in Safari 13+ on both Mac and iOS (also including
|
|
// iOS Chrome and other Safari iOS derivatives). Safari was very occasionally and apparently not deterministically
|
|
// throwing a "RangeError: Array size is not a small enough positive integer." exception seemingly within the second
|
|
// .map of the original code (here the second for-loop). This second .map contained a nested .map call over the same
|
|
// array instance which was the chained result from previous calls to .map, .filter and .sort which potentially caused
|
|
// this bug in Safari?
|
|
// Also see https://github.com/cozmo/jsQR/issues/157 and https://bugs.webkit.org/show_bug.cgi?id=211619#c3
|
|
const scoredFinderPatternPositions = [];
|
|
for (const quad of finderPatternQuads) {
|
|
if (quad.bottom.y - quad.top.y < 2) {
|
|
// All quads must be at least 2px tall since the center square is larger than a block
|
|
continue;
|
|
}
|
|
// calculate quad center
|
|
const x = (quad.top.startX + quad.top.endX + quad.bottom.startX + quad.bottom.endX) / 4;
|
|
const y = (quad.top.y + quad.bottom.y + 1) / 2;
|
|
if (!matrix.get(Math.round(x), Math.round(y))) {
|
|
continue;
|
|
}
|
|
const lengths = [quad.top.endX - quad.top.startX, quad.bottom.endX - quad.bottom.startX, quad.bottom.y - quad.top.y + 1];
|
|
const size = sum(lengths) / lengths.length;
|
|
// Initial scoring of finder pattern quads by looking at their ratios, not taking into account position
|
|
const score = scorePattern({ x: Math.round(x), y: Math.round(y) }, [1, 1, 3, 1, 1], matrix);
|
|
scoredFinderPatternPositions.push({ score, x, y, size });
|
|
}
|
|
if (scoredFinderPatternPositions.length < 3) {
|
|
// A QR code has 3 finder patterns, therefore we need at least 3 candidates.
|
|
return null;
|
|
}
|
|
scoredFinderPatternPositions.sort((a, b) => a.score - b.score);
|
|
// Now take the top finder pattern options and try to find 2 other options with a similar size.
|
|
const finderPatternGroups = [];
|
|
for (let i = 0; i < Math.min(scoredFinderPatternPositions.length, MAX_FINDERPATTERNS_TO_SEARCH); ++i) {
|
|
const point = scoredFinderPatternPositions[i];
|
|
const otherPoints = [];
|
|
for (const otherPoint of scoredFinderPatternPositions) {
|
|
if (otherPoint === point) {
|
|
continue;
|
|
}
|
|
otherPoints.push(Object.assign(Object.assign({}, otherPoint), { score: otherPoint.score + (Math.pow((otherPoint.size - point.size), 2)) / point.size }));
|
|
}
|
|
otherPoints.sort((a, b) => a.score - b.score);
|
|
finderPatternGroups.push({
|
|
points: [point, otherPoints[0], otherPoints[1]],
|
|
score: point.score + otherPoints[0].score + otherPoints[1].score, // total combined score of the three points in the group
|
|
});
|
|
}
|
|
finderPatternGroups.sort((a, b) => a.score - b.score);
|
|
const bestFinderPatternGroup = finderPatternGroups[0];
|
|
const { topRight, topLeft, bottomLeft } = reorderFinderPatterns(...bestFinderPatternGroup.points);
|
|
const alignment = findAlignmentPattern(matrix, alignmentPatternQuads, topRight, topLeft, bottomLeft);
|
|
const result = [];
|
|
if (alignment) {
|
|
result.push({
|
|
alignmentPattern: { x: alignment.alignmentPattern.x, y: alignment.alignmentPattern.y },
|
|
bottomLeft: { x: bottomLeft.x, y: bottomLeft.y },
|
|
dimension: alignment.dimension,
|
|
topLeft: { x: topLeft.x, y: topLeft.y },
|
|
topRight: { x: topRight.x, y: topRight.y },
|
|
});
|
|
}
|
|
// We normally use the center of the quads as the location of the tracking points, which is optimal for most cases and will account
|
|
// for a skew in the image. However, In some cases, a slight skew might not be real and instead be caused by image compression
|
|
// errors and/or low resolution. For those cases, we'd be better off centering the point exactly in the middle of the black area. We
|
|
// compute and return the location data for the naively centered points as it is little additional work and allows for multiple
|
|
// attempts at decoding harder images.
|
|
const midTopRight = recenterLocation(matrix, topRight);
|
|
const midTopLeft = recenterLocation(matrix, topLeft);
|
|
const midBottomLeft = recenterLocation(matrix, bottomLeft);
|
|
const centeredAlignment = findAlignmentPattern(matrix, alignmentPatternQuads, midTopRight, midTopLeft, midBottomLeft);
|
|
if (centeredAlignment) {
|
|
result.push({
|
|
alignmentPattern: { x: centeredAlignment.alignmentPattern.x, y: centeredAlignment.alignmentPattern.y },
|
|
bottomLeft: { x: midBottomLeft.x, y: midBottomLeft.y },
|
|
topLeft: { x: midTopLeft.x, y: midTopLeft.y },
|
|
topRight: { x: midTopRight.x, y: midTopRight.y },
|
|
dimension: centeredAlignment.dimension,
|
|
});
|
|
}
|
|
if (result.length === 0) {
|
|
return null;
|
|
}
|
|
return result;
|
|
}
|
|
function findAlignmentPattern(matrix, alignmentPatternQuads, topRight, topLeft, bottomLeft) {
|
|
// Now that we've found the three finder patterns we can determine the blockSize and the size of the QR code.
|
|
// We'll use these to help find the alignment pattern but also later when we do the extraction.
|
|
let dimension;
|
|
let moduleSize;
|
|
try {
|
|
({ dimension, moduleSize } = computeDimension(topLeft, topRight, bottomLeft, matrix));
|
|
}
|
|
catch (e) {
|
|
return null;
|
|
}
|
|
// Now find the alignment pattern
|
|
const bottomRightFinderPattern = {
|
|
x: topRight.x - topLeft.x + bottomLeft.x,
|
|
y: topRight.y - topLeft.y + bottomLeft.y,
|
|
};
|
|
const modulesBetweenFinderPatterns = ((distance(topLeft, bottomLeft) + distance(topLeft, topRight)) / 2 / moduleSize);
|
|
const correctionToTopLeft = 1 - (3 / modulesBetweenFinderPatterns);
|
|
const expectedAlignmentPattern = {
|
|
x: topLeft.x + correctionToTopLeft * (bottomRightFinderPattern.x - topLeft.x),
|
|
y: topLeft.y + correctionToTopLeft * (bottomRightFinderPattern.y - topLeft.y),
|
|
};
|
|
const alignmentPatterns = alignmentPatternQuads
|
|
.map(q => {
|
|
const x = (q.top.startX + q.top.endX + q.bottom.startX + q.bottom.endX) / 4;
|
|
const y = (q.top.y + q.bottom.y + 1) / 2;
|
|
if (!matrix.get(Math.floor(x), Math.floor(y))) {
|
|
return;
|
|
}
|
|
const sizeScore = scorePattern({ x: Math.floor(x), y: Math.floor(y) }, [1, 1, 1], matrix);
|
|
const score = sizeScore + distance({ x, y }, expectedAlignmentPattern);
|
|
return { x, y, score };
|
|
})
|
|
.filter(v => !!v)
|
|
.sort((a, b) => a.score - b.score);
|
|
// If there are less than 15 modules between finder patterns it's a version 1 QR code and as such has no alignmemnt pattern
|
|
// so we can only use our best guess.
|
|
const alignmentPattern = modulesBetweenFinderPatterns >= 15 && alignmentPatterns.length ? alignmentPatterns[0] : expectedAlignmentPattern;
|
|
return { alignmentPattern, dimension };
|
|
}
|
|
|
|
function scan(matrix) {
|
|
const locations = locate(matrix);
|
|
if (!locations) {
|
|
return null;
|
|
}
|
|
for (const location of locations) {
|
|
const extracted = extract(matrix, location);
|
|
const decoded = decode(extracted.matrix);
|
|
if (decoded) {
|
|
return {
|
|
binaryData: decoded.bytes,
|
|
data: decoded.text,
|
|
chunks: decoded.chunks,
|
|
version: decoded.version,
|
|
location: {
|
|
topRightCorner: extracted.mappingFunction(location.dimension, 0),
|
|
topLeftCorner: extracted.mappingFunction(0, 0),
|
|
bottomRightCorner: extracted.mappingFunction(location.dimension, location.dimension),
|
|
bottomLeftCorner: extracted.mappingFunction(0, location.dimension),
|
|
topRightFinderPattern: location.topRight,
|
|
topLeftFinderPattern: location.topLeft,
|
|
bottomLeftFinderPattern: location.bottomLeft,
|
|
bottomRightAlignmentPattern: location.alignmentPattern,
|
|
},
|
|
matrix: extracted.matrix,
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
const defaultOptions = {
|
|
inversionAttempts: "attemptBoth",
|
|
greyScaleWeights: {
|
|
red: 0.2126,
|
|
green: 0.7152,
|
|
blue: 0.0722,
|
|
useIntegerApproximation: false,
|
|
},
|
|
canOverwriteImage: true,
|
|
};
|
|
function mergeObject(target, src) {
|
|
Object.keys(src).forEach(opt => {
|
|
target[opt] = src[opt];
|
|
});
|
|
}
|
|
function jsQR(data, width, height, providedOptions = {}) {
|
|
const options = Object.create(null);
|
|
mergeObject(options, defaultOptions);
|
|
mergeObject(options, providedOptions);
|
|
const tryInvertedFirst = options.inversionAttempts === "onlyInvert" || options.inversionAttempts === "invertFirst";
|
|
const shouldInvert = options.inversionAttempts === "attemptBoth" || tryInvertedFirst;
|
|
const { binarized, inverted } = binarize(data, width, height, shouldInvert, options.greyScaleWeights, options.canOverwriteImage);
|
|
let result = scan(tryInvertedFirst ? inverted : binarized);
|
|
if (!result && (options.inversionAttempts === "attemptBoth" || options.inversionAttempts === "invertFirst")) {
|
|
result = scan(tryInvertedFirst ? binarized : inverted);
|
|
}
|
|
return result;
|
|
}
|
|
jsQR.default = jsQR;
|
|
|
|
export { jsQR as default }; |