/* * QR Code generator library (compact byte-mode port) * Based on "QR Code generator library" by Project Nayuki (MIT License). * https://www.nayuki.io/page/qr-code-generator-library * * Copyright (c) Project Nayuki. (MIT License) * Permission is hereby granted, free of charge, to any person obtaining a copy of * this software and associated documentation files (the "Software"), to deal in * the Software without restriction, including without limitation the rights to * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of * the Software, and to permit persons to whom the Software is furnished to do so, * subject to the following conditions: * - The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * The Software is provided "as is", without warranty of any kind. */ (function (global) { "use strict"; function assert(cond) { if (!cond) throw new Error("Assertion error"); } var MIN_VERSION = 1, MAX_VERSION = 40; // Error correction level constants: ordinal + format bits. var Ecc = { LOW: { ordinal: 0, formatBits: 1 }, MEDIUM: { ordinal: 1, formatBits: 0 }, QUARTILE: { ordinal: 2, formatBits: 3 }, HIGH: { ordinal: 3, formatBits: 2 } }; function QrCode(version, ecl, dataCodewords, msk) { this.version = version; this.errorCorrectionLevel = ecl; this.size = version * 4 + 17; var size = this.size; var row = []; for (var i = 0; i < size; i++) row.push(false); this.modules = []; this.isFunction = []; for (var j = 0; j < size; j++) { this.modules.push(row.slice()); this.isFunction.push(row.slice()); } this.drawFunctionPatterns(); var allCodewords = this.addEccAndInterleave(dataCodewords); this.drawCodewords(allCodewords); if (msk == -1) { var minPenalty = 1000000000; for (var m = 0; m < 8; m++) { this.applyMask(m); this.drawFormatBits(m); var penalty = this.getPenaltyScore(); if (penalty < minPenalty) { msk = m; minPenalty = penalty; } this.applyMask(m); } } assert(0 <= msk && msk <= 7); this.mask = msk; this.applyMask(msk); this.drawFormatBits(msk); this.isFunction = null; } QrCode.prototype.getModule = function (x, y) { return 0 <= x && x < this.size && 0 <= y && y < this.size && this.modules[y][x]; }; QrCode.prototype.drawFunctionPatterns = function () { var size = this.size; for (var i = 0; i < size; i++) { this.setFunctionModule(6, i, i % 2 == 0); this.setFunctionModule(i, 6, i % 2 == 0); } this.drawFinderPattern(3, 3); this.drawFinderPattern(size - 4, 3); this.drawFinderPattern(3, size - 4); var alignPatPos = this.getAlignmentPatternPositions(); var numAlign = alignPatPos.length; for (var a = 0; a < numAlign; a++) { for (var b = 0; b < numAlign; b++) { if (!(a == 0 && b == 0 || a == 0 && b == numAlign - 1 || a == numAlign - 1 && b == 0)) this.drawAlignmentPattern(alignPatPos[a], alignPatPos[b]); } } this.drawFormatBits(0); this.drawVersion(); }; QrCode.prototype.drawFormatBits = function (msk) { var data = this.errorCorrectionLevel.formatBits << 3 | msk; var rem = data; for (var i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537); var bits = (data << 10 | rem) ^ 0x5412; assert(bits >>> 15 == 0); for (var j = 0; j <= 5; j++) this.setFunctionModule(8, j, getBit(bits, j)); this.setFunctionModule(8, 7, getBit(bits, 6)); this.setFunctionModule(8, 8, getBit(bits, 7)); this.setFunctionModule(7, 8, getBit(bits, 8)); for (var k = 9; k < 15; k++) this.setFunctionModule(14 - k, 8, getBit(bits, k)); var size = this.size; for (var m = 0; m < 8; m++) this.setFunctionModule(size - 1 - m, 8, getBit(bits, m)); for (var n = 8; n < 15; n++) this.setFunctionModule(8, size - 15 + n, getBit(bits, n)); this.setFunctionModule(8, size - 8, true); }; QrCode.prototype.drawVersion = function () { if (this.version < 7) return; var rem = this.version; for (var i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1F25); var bits = this.version << 12 | rem; assert(bits >>> 18 == 0); for (var j = 0; j < 18; j++) { var bit = getBit(bits, j); var a = this.size - 11 + j % 3; var b = Math.floor(j / 3); this.setFunctionModule(a, b, bit); this.setFunctionModule(b, a, bit); } }; QrCode.prototype.drawFinderPattern = function (x, y) { for (var dy = -4; dy <= 4; dy++) { for (var dx = -4; dx <= 4; dx++) { var dist = Math.max(Math.abs(dx), Math.abs(dy)); var xx = x + dx, yy = y + dy; if (0 <= xx && xx < this.size && 0 <= yy && yy < this.size) this.setFunctionModule(xx, yy, dist != 2 && dist != 4); } } }; QrCode.prototype.drawAlignmentPattern = function (x, y) { for (var dy = -2; dy <= 2; dy++) { for (var dx = -2; dx <= 2; dx++) { this.setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) != 1); } } }; QrCode.prototype.setFunctionModule = function (x, y, isDark) { this.modules[y][x] = isDark; this.isFunction[y][x] = true; }; QrCode.prototype.addEccAndInterleave = function (data) { var ver = this.version; var ecl = this.errorCorrectionLevel; assert(data.length == QrCode.getNumDataCodewords(ver, ecl)); var numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver]; var blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver]; var rawCodewords = Math.floor(getNumRawDataModules(ver) / 8); var numShortBlocks = numBlocks - rawCodewords % numBlocks; var shortBlockLen = Math.floor(rawCodewords / numBlocks); var blocks = []; var rsDiv = reedSolomonComputeDivisor(blockEccLen); for (var i = 0, k = 0; i < numBlocks; i++) { var dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1)); k += dat.length; var ecc = reedSolomonComputeRemainder(dat, rsDiv); if (i < numShortBlocks) dat.push(0); blocks.push(dat.concat(ecc)); } var result = []; for (var m = 0; m < blocks[0].length; m++) { for (var n = 0; n < blocks.length; n++) { if (m != shortBlockLen - blockEccLen || n >= numShortBlocks) result.push(blocks[n][m]); } } assert(result.length == rawCodewords); return result; }; QrCode.prototype.drawCodewords = function (data) { var size = this.size; assert(data.length == Math.floor(getNumRawDataModules(this.version) / 8)); var i = 0; for (var right = size - 1; right >= 1; right -= 2) { if (right == 6) right = 5; for (var vert = 0; vert < size; vert++) { for (var j = 0; j < 2; j++) { var x = right - j; var upward = ((right + 1) & 2) == 0; var y = upward ? size - 1 - vert : vert; if (!this.isFunction[y][x] && i < data.length * 8) { this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7)); i++; } } } } assert(i == data.length * 8); }; QrCode.prototype.applyMask = function (msk) { for (var y = 0; y < this.size; y++) { for (var x = 0; x < this.size; x++) { var invert; switch (msk) { case 0: invert = (x + y) % 2 == 0; break; case 1: invert = y % 2 == 0; break; case 2: invert = x % 3 == 0; break; case 3: invert = (x + y) % 3 == 0; break; case 4: invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 == 0; break; case 5: invert = x * y % 2 + x * y % 3 == 0; break; case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break; case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break; default: throw new Error("Unreachable"); } if (!this.isFunction[y][x] && invert) this.modules[y][x] = !this.modules[y][x]; } } }; QrCode.prototype.getPenaltyScore = function () { var size = this.size; var result = 0; var mods = this.modules; for (var y = 0; y < size; y++) { var runColor = false, runX = 0; var runHistory = [0, 0, 0, 0, 0, 0, 0]; for (var x = 0; x < size; x++) { if (mods[y][x] == runColor) { runX++; if (runX == 5) result += 3; else if (runX > 5) result++; } else { this.finderPenaltyAddHistory(runX, runHistory); if (!runColor) result += this.finderPenaltyCountPatterns(runHistory) * 40; runColor = mods[y][x]; runX = 1; } } result += this.finderPenaltyTerminateAndCount(runColor, runX, runHistory) * 40; } for (var xx = 0; xx < size; xx++) { var runColorV = false, runY = 0; var runHistoryV = [0, 0, 0, 0, 0, 0, 0]; for (var yy = 0; yy < size; yy++) { if (mods[yy][xx] == runColorV) { runY++; if (runY == 5) result += 3; else if (runY > 5) result++; } else { this.finderPenaltyAddHistory(runY, runHistoryV); if (!runColorV) result += this.finderPenaltyCountPatterns(runHistoryV) * 40; runColorV = mods[yy][xx]; runY = 1; } } result += this.finderPenaltyTerminateAndCount(runColorV, runY, runHistoryV) * 40; } for (var y2 = 0; y2 < size - 1; y2++) { for (var x2 = 0; x2 < size - 1; x2++) { var color = mods[y2][x2]; if (color == mods[y2][x2 + 1] && color == mods[y2 + 1][x2] && color == mods[y2 + 1][x2 + 1]) result += 3; } } var dark = 0; for (var y3 = 0; y3 < size; y3++) for (var x3 = 0; x3 < size; x3++) if (mods[y3][x3]) dark++; var total = size * size; var k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1; result += k * 10; return result; }; QrCode.prototype.getAlignmentPatternPositions = function () { if (this.version == 1) return []; var numAlign = Math.floor(this.version / 7) + 2; var step = (this.version == 32) ? 26 : Math.ceil((this.version * 4 + 4) / (numAlign * 2 - 2)) * 2; var result = [6]; for (var pos = this.size - 7; result.length < numAlign; pos -= step) result.splice(1, 0, pos); return result; }; QrCode.prototype.finderPenaltyCountPatterns = function (runHistory) { var n = runHistory[1]; var core = n > 0 && runHistory[2] == n && runHistory[3] == n * 3 && runHistory[4] == n && runHistory[5] == n; return (core && runHistory[0] >= n * 4 && runHistory[6] >= n ? 1 : 0) + (core && runHistory[6] >= n * 4 && runHistory[0] >= n ? 1 : 0); }; QrCode.prototype.finderPenaltyTerminateAndCount = function (currentRunColor, currentRunLength, runHistory) { if (currentRunColor) { this.finderPenaltyAddHistory(currentRunLength, runHistory); currentRunLength = 0; } currentRunLength += this.size; this.finderPenaltyAddHistory(currentRunLength, runHistory); return this.finderPenaltyCountPatterns(runHistory); }; QrCode.prototype.finderPenaltyAddHistory = function (currentRunLength, runHistory) { if (runHistory[0] == 0) currentRunLength += this.size; runHistory.pop(); runHistory.unshift(currentRunLength); }; QrCode.getNumDataCodewords = function (ver, ecl) { return Math.floor(getNumRawDataModules(ver) / 8) - ECC_CODEWORDS_PER_BLOCK[ecl.ordinal][ver] * NUM_ERROR_CORRECTION_BLOCKS[ecl.ordinal][ver]; }; function getNumRawDataModules(ver) { assert(MIN_VERSION <= ver && ver <= MAX_VERSION); var result = (16 * ver + 128) * ver + 64; if (ver >= 2) { var numAlign = Math.floor(ver / 7) + 2; result -= (25 * numAlign - 10) * numAlign - 55; if (ver >= 7) result -= 36; } return result; } function reedSolomonComputeDivisor(degree) { assert(1 <= degree && degree <= 255); var result = []; for (var i = 0; i < degree - 1; i++) result.push(0); result.push(1); var root = 1; for (var j = 0; j < degree; j++) { for (var k = 0; k < result.length; k++) { result[k] = reedSolomonMultiply(result[k], root); if (k + 1 < result.length) result[k] ^= result[k + 1]; } root = reedSolomonMultiply(root, 0x02); } return result; } function reedSolomonComputeRemainder(data, divisor) { var result = divisor.map(function () { return 0; }); data.forEach(function (b) { var factor = b ^ result.shift(); result.push(0); divisor.forEach(function (coef, i) { result[i] ^= reedSolomonMultiply(coef, factor); }); }); return result; } function reedSolomonMultiply(x, y) { assert(x >>> 8 == 0 && y >>> 8 == 0); var z = 0; for (var i = 7; i >= 0; i--) { z = (z << 1) ^ ((z >>> 7) * 0x11D); z ^= ((y >>> i) & 1) * x; } assert(z >>> 8 == 0); return z; } function getBit(x, i) { return ((x >>> i) & 1) != 0; } // --- Segment (byte mode only) --- function makeBytes(data) { var bb = []; for (var i = 0; i < data.length; i++) appendBits(data[i], 8, bb); return { mode: "byte", numChars: data.length, bitData: bb }; } function appendBits(val, len, bb) { if (len < 0 || len > 31 || (val >>> len) != 0) throw new RangeError("Value out of range"); for (var i = len - 1; i >= 0; i--) bb.push((val >>> i) & 1); } function utf8ToBytes(str) { var utf8 = unescape(encodeURIComponent(str)); var bytes = []; for (var i = 0; i < utf8.length; i++) bytes.push(utf8.charCodeAt(i)); return bytes; } function encodeText(text, ecl) { var seg = makeBytes(utf8ToBytes(text)); return encodeSegments([seg], ecl); } function encodeSegments(segs, ecl) { var minVersion = MIN_VERSION, maxVersion = MAX_VERSION; var version, dataUsedBits; for (version = minVersion; ; version++) { var dataCapacityBits = QrCode.getNumDataCodewords(version, ecl) * 8; var usedBits = getTotalBits(segs, version); if (usedBits <= dataCapacityBits) { dataUsedBits = usedBits; break; } if (version >= maxVersion) throw new Error("Data too long"); } // Boost ECC where free. [Ecc.MEDIUM, Ecc.QUARTILE, Ecc.HIGH].forEach(function (newEcl) { if (dataUsedBits <= QrCode.getNumDataCodewords(version, newEcl) * 8) ecl = newEcl; }); var bb = []; segs.forEach(function (seg) { appendBits(modeBits(seg.mode), 4, bb); appendBits(seg.numChars, numCharCountBits(seg.mode, version), bb); seg.bitData.forEach(function (b) { bb.push(b); }); }); var dataCapacityBits = QrCode.getNumDataCodewords(version, ecl) * 8; appendBits(0, Math.min(4, dataCapacityBits - bb.length), bb); appendBits(0, (8 - bb.length % 8) % 8, bb); for (var padByte = 0xEC; bb.length < dataCapacityBits; padByte ^= 0xEC ^ 0x11) appendBits(padByte, 8, bb); var dataCodewords = []; while (dataCodewords.length * 8 < bb.length) dataCodewords.push(0); bb.forEach(function (b, i) { dataCodewords[i >>> 3] |= b << (7 - (i & 7)); }); return new QrCode(version, ecl, dataCodewords, -1); } function modeBits(mode) { return mode == "byte" ? 0x4 : 0x4; } function numCharCountBits(mode, version) { // byte mode: 8 bits for v1-9, 16 bits for v10-40. return version <= 9 ? 8 : 16; } function getTotalBits(segs, version) { var result = 0; for (var i = 0; i < segs.length; i++) { var ccbits = numCharCountBits(segs[i].mode, version); if (segs[i].numChars >= (1 << ccbits)) return Infinity; result += 4 + ccbits + segs[i].bitData.length; } return result; } var ECC_CODEWORDS_PER_BLOCK = [ // LOW [-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // MEDIUM [-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28], // QUARTILE [-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30], // HIGH [-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30] ]; var NUM_ERROR_CORRECTION_BLOCKS = [ // LOW [-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25], // MEDIUM [-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49], // QUARTILE [-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68], // HIGH [-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81] ]; function toSvgString(qr, border, lightColor, darkColor) { if (border < 0) throw new RangeError("Border must be non-negative"); var parts = []; for (var y = 0; y < qr.size; y++) { for (var x = 0; x < qr.size; x++) { if (qr.getModule(x, y)) parts.push("M" + (x + border) + "," + (y + border) + "h1v1h-1z"); } } var dim = qr.size + border * 2; return '' + '' + ''; } global.QrCode = { Ecc: Ecc, encodeText: encodeText, toSvgString: toSvgString }; })(window);