openvibers/openvibe.shared ov-qr.js

ov-qr.js

main 20.3 KB · 460 lines · javascript

1'use strict';
2/**
3 * ov-qr.js — a QR code from a link, on the device. The Share sheet shows one for the thing being shared; the link may
4 * hold a key after #, so nothing here touches the network and no dependency is loaded.
5 *
6 * OVQR.matrix('https://openvibe.network/x#key', { ecc: 'M' }) → { size: 29, dark(x, y) → true|false }
7 * OVQR.svg('https://openvibe.network/x', { ecc, margin, dark, light, title }) → an SVG string
8 *
9 * Byte mode (the text as UTF-8), the smallest of versions 1 to 40 that fits, all eight masks with the standard penalty
10 * rules, and the format and version information. A text that does not fit version 40 throws a RangeError.
11 *
12 * Ported from Nayuki's QR Code generator (MIT), https://www.nayuki.io/page/qr-code-generator-library
13 * Copyright (c) Project Nayuki. Permission is hereby granted, free of charge, to any person obtaining a copy of this
14 * software and associated documentation files (the "Software"), to deal in the Software without restriction, including
15 * without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
16 * Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
17 * the above copyright notice and this permission notice shall be included in all copies or substantial portions of the
18 * Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
19 */
20(function (root, factory) {
21 const api = factory();
22 if (typeof module === 'object' && module.exports) module.exports = api;
23 else root.OVQR = api;
24})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
25 const ECC = {
26 L: { ordinal: 0, formatBits: 1 },
27 M: { ordinal: 1, formatBits: 0 },
28 Q: { ordinal: 2, formatBits: 3 },
29 H: { ordinal: 3, formatBits: 2 },
30 };
31 // Error-correction codewords per block and the number of blocks, indexed by [level][version].
32 const ECC_CODEWORDS_PER_BLOCK = [
33 [-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],
34 [-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],
35 [-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],
36 [-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],
37 ];
38 const NUM_ERROR_CORRECTION_BLOCKS = [
39 [-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],
40 [-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],
41 [-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],
42 [-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],
43 ];
44 const PENALTY_N1 = 3;
45 const PENALTY_N2 = 3;
46 const PENALTY_N3 = 40;
47 const PENALTY_N4 = 10;
48
49 // Returns the text's UTF-8 bytes (a lone surrogate becomes U+FFFD, as TextEncoder does).
50 function utf8Bytes(text) {
51 const out = [];
52 for (let i = 0; i < text.length; i++) {
53 let c = text.charCodeAt(i);
54 if (c >= 0xD800 && c <= 0xDBFF && i + 1 < text.length) {
55 const d = text.charCodeAt(i + 1);
56 if (d >= 0xDC00 && d <= 0xDFFF) {
57 c = 0x10000 + ((c - 0xD800) << 10) + (d - 0xDC00);
58 i++;
59 }
60 }
61 if (c >= 0xD800 && c <= 0xDFFF) c = 0xFFFD;
62 if (c < 0x80) out.push(c);
63 else if (c < 0x800) out.push(0xC0 | (c >>> 6), 0x80 | (c & 0x3F));
64 else if (c < 0x10000) out.push(0xE0 | (c >>> 12), 0x80 | ((c >>> 6) & 0x3F), 0x80 | (c & 0x3F));
65 else out.push(0xF0 | (c >>> 18), 0x80 | ((c >>> 12) & 0x3F), 0x80 | ((c >>> 6) & 0x3F), 0x80 | (c & 0x3F));
66 }
67 return out;
68 }
69
70 // Appends the low len bits of val to bb.
71 function appendBits(val, len, bb) {
72 for (let i = len - 1; i >= 0; i--) bb.push((val >>> i) & 1);
73 }
74
75 function getBit(x, i) {
76 return ((x >>> i) & 1) !== 0;
77 }
78
79 // Data bits available in a version after the function modules, including remainder bits.
80 function numRawDataModules(ver) {
81 let result = (16 * ver + 128) * ver + 64;
82 if (ver >= 2) {
83 const numAlign = Math.floor(ver / 7) + 2;
84 result -= (25 * numAlign - 10) * numAlign - 55;
85 if (ver >= 7) result -= 36;
86 }
87 return result;
88 }
89
90 function numDataCodewords(ver, level) {
91 return Math.floor(numRawDataModules(ver) / 8) -
92 ECC_CODEWORDS_PER_BLOCK[level.ordinal][ver] * NUM_ERROR_CORRECTION_BLOCKS[level.ordinal][ver];
93 }
94
95 function byteCountBits(ver) {
96 return ver <= 9 ? 8 : 16;
97 }
98
99 function reedSolomonComputeDivisor(degree) {
100 const result = [];
101 for (let i = 0; i < degree - 1; i++) result.push(0);
102 result.push(1);
103 let root = 1;
104 for (let i = 0; i < degree; i++) {
105 for (let j = 0; j < result.length; j++) {
106 result[j] = reedSolomonMultiply(result[j], root);
107 if (j + 1 < result.length) result[j] ^= result[j + 1];
108 }
109 root = reedSolomonMultiply(root, 0x02);
110 }
111 return result;
112 }
113
114 function reedSolomonComputeRemainder(data, divisor) {
115 const result = divisor.map(() => 0);
116 for (const b of data) {
117 const factor = b ^ result.shift();
118 result.push(0);
119 divisor.forEach((coef, i) => { result[i] ^= reedSolomonMultiply(coef, factor); });
120 }
121 return result;
122 }
123
124 function reedSolomonMultiply(x, y) {
125 let z = 0;
126 for (let i = 7; i >= 0; i--) {
127 z = (z << 1) ^ ((z >>> 7) * 0x11D);
128 z ^= ((y >>> i) & 1) * x;
129 }
130 return z;
131 }
132
133 class QrCode {
134 constructor(version, level, dataCodewords) {
135 this.version = version;
136 this.level = level;
137 this.size = version * 4 + 17;
138 this.modules = [];
139 this.isFunction = [];
140 for (let i = 0; i < this.size; i++) {
141 this.modules.push(new Array(this.size).fill(false));
142 this.isFunction.push(new Array(this.size).fill(false));
143 }
144 this.drawFunctionPatterns();
145 this.drawCodewords(this.addEccAndInterleave(dataCodewords));
146 // The standard penalty rules choose the mask; ties keep the lower number.
147 let bestMask = 0;
148 let bestPenalty = Infinity;
149 for (let mask = 0; mask < 8; mask++) {
150 this.applyMask(mask);
151 this.drawFormatBits(mask);
152 const penalty = this.getPenaltyScore();
153 if (penalty < bestPenalty) {
154 bestPenalty = penalty;
155 bestMask = mask;
156 }
157 this.applyMask(mask);
158 }
159 this.mask = bestMask;
160 this.applyMask(bestMask);
161 this.drawFormatBits(bestMask);
162 this.isFunction = [];
163 }
164
165 drawFunctionPatterns() {
166 for (let i = 0; i < this.size; i++) {
167 this.setFunctionModule(6, i, i % 2 === 0);
168 this.setFunctionModule(i, 6, i % 2 === 0);
169 }
170 this.drawFinderPattern(3, 3);
171 this.drawFinderPattern(this.size - 4, 3);
172 this.drawFinderPattern(3, this.size - 4);
173 const positions = this.alignmentPatternPositions();
174 const n = positions.length;
175 for (let i = 0; i < n; i++) {
176 for (let j = 0; j < n; j++) {
177 if (!((i === 0 && j === 0) || (i === 0 && j === n - 1) || (i === n - 1 && j === 0))) {
178 this.drawAlignmentPattern(positions[i], positions[j]);
179 }
180 }
181 }
182 this.drawFormatBits(0);
183 this.drawVersion();
184 }
185
186 drawFormatBits(mask) {
187 const data = (this.level.formatBits << 3) | mask;
188 let rem = data;
189 for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
190 const bits = ((data << 10) | rem) ^ 0x5412;
191 for (let i = 0; i <= 5; i++) this.setFunctionModule(8, i, getBit(bits, i));
192 this.setFunctionModule(8, 7, getBit(bits, 6));
193 this.setFunctionModule(8, 8, getBit(bits, 7));
194 this.setFunctionModule(7, 8, getBit(bits, 8));
195 for (let i = 9; i < 15; i++) this.setFunctionModule(14 - i, 8, getBit(bits, i));
196 for (let i = 0; i < 8; i++) this.setFunctionModule(this.size - 1 - i, 8, getBit(bits, i));
197 for (let i = 8; i < 15; i++) this.setFunctionModule(8, this.size - 15 + i, getBit(bits, i));
198 this.setFunctionModule(8, this.size - 8, true);
199 }
200
201 drawVersion() {
202 if (this.version < 7) return;
203 let rem = this.version;
204 for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1F25);
205 const bits = (this.version << 12) | rem;
206 for (let i = 0; i < 18; i++) {
207 const dark = getBit(bits, i);
208 const a = this.size - 11 + (i % 3);
209 const b = Math.floor(i / 3);
210 this.setFunctionModule(a, b, dark);
211 this.setFunctionModule(b, a, dark);
212 }
213 }
214
215 drawFinderPattern(x, y) {
216 for (let dy = -4; dy <= 4; dy++) {
217 for (let dx = -4; dx <= 4; dx++) {
218 const xx = x + dx;
219 const yy = y + dy;
220 if (xx >= 0 && xx < this.size && yy >= 0 && yy < this.size) {
221 this.setFunctionModule(xx, yy, Math.max(Math.abs(dx), Math.abs(dy)) !== 2 && Math.max(Math.abs(dx), Math.abs(dy)) !== 4);
222 }
223 }
224 }
225 }
226
227 drawAlignmentPattern(x, y) {
228 for (let dy = -2; dy <= 2; dy++) {
229 for (let dx = -2; dx <= 2; dx++) {
230 this.setFunctionModule(x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
231 }
232 }
233 }
234
235 setFunctionModule(x, y, dark) {
236 this.modules[y][x] = dark;
237 this.isFunction[y][x] = true;
238 }
239
240 alignmentPatternPositions() {
241 if (this.version === 1) return [];
242 const n = Math.floor(this.version / 7) + 2;
243 const step = this.version === 32 ? 26 : Math.ceil((this.version * 4 + 4) / (n * 2 - 2)) * 2;
244 const result = [6];
245 for (let pos = this.size - 7; result.length < n; pos -= step) result.splice(1, 0, pos);
246 return result;
247 }
248
249 addEccAndInterleave(data) {
250 const ver = this.version;
251 const level = this.level;
252 const numBlocks = NUM_ERROR_CORRECTION_BLOCKS[level.ordinal][ver];
253 const blockEccLen = ECC_CODEWORDS_PER_BLOCK[level.ordinal][ver];
254 const rawCodewords = Math.floor(numRawDataModules(ver) / 8);
255 const numShortBlocks = numBlocks - (rawCodewords % numBlocks);
256 const shortBlockLen = Math.floor(rawCodewords / numBlocks);
257 const blocks = [];
258 const rsDiv = reedSolomonComputeDivisor(blockEccLen);
259 for (let i = 0, k = 0; i < numBlocks; i++) {
260 const dat = data.slice(k, k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1));
261 k += dat.length;
262 const ecc = reedSolomonComputeRemainder(dat, rsDiv);
263 if (i < numShortBlocks) dat.push(0);
264 blocks.push(dat.concat(ecc));
265 }
266 const result = [];
267 for (let i = 0; i < blocks[0].length; i++) {
268 blocks.forEach((block, j) => {
269 if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks) result.push(block[i]);
270 });
271 }
272 return result;
273 }
274
275 drawCodewords(data) {
276 let i = 0;
277 for (let right = this.size - 1; right >= 1; right -= 2) {
278 if (right === 6) right = 5;
279 for (let vert = 0; vert < this.size; vert++) {
280 for (let j = 0; j < 2; j++) {
281 const x = right - j;
282 const upward = ((right + 1) & 2) === 0;
283 const y = upward ? this.size - 1 - vert : vert;
284 if (!this.isFunction[y][x] && i < data.length * 8) {
285 this.modules[y][x] = getBit(data[i >>> 3], 7 - (i & 7));
286 i++;
287 }
288 }
289 }
290 }
291 }
292
293 applyMask(mask) {
294 for (let y = 0; y < this.size; y++) {
295 for (let x = 0; x < this.size; x++) {
296 let invert;
297 switch (mask) {
298 case 0: invert = (x + y) % 2 === 0; break;
299 case 1: invert = y % 2 === 0; break;
300 case 2: invert = x % 3 === 0; break;
301 case 3: invert = (x + y) % 3 === 0; break;
302 case 4: invert = (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0; break;
303 case 5: invert = (x * y) % 2 + (x * y) % 3 === 0; break;
304 case 6: invert = ((x * y) % 2 + (x * y) % 3) % 2 === 0; break;
305 default: invert = ((x + y) % 2 + (x * y) % 3) % 2 === 0; break;
306 }
307 if (!this.isFunction[y][x] && invert) this.modules[y][x] = !this.modules[y][x];
308 }
309 }
310 }
311
312 getPenaltyScore() {
313 let result = 0;
314 for (let y = 0; y < this.size; y++) {
315 let runColor = false;
316 let runLen = 0;
317 const history = [0, 0, 0, 0, 0, 0, 0];
318 for (let x = 0; x < this.size; x++) {
319 if (this.modules[y][x] === runColor) {
320 runLen++;
321 if (runLen === 5) result += PENALTY_N1;
322 else if (runLen > 5) result++;
323 } else {
324 this.addRunHistory(runLen, history);
325 if (!runColor) result += this.countFinderPatterns(history) * PENALTY_N3;
326 runColor = this.modules[y][x];
327 runLen = 1;
328 }
329 }
330 result += this.finishRuns(runColor, runLen, history) * PENALTY_N3;
331 }
332 for (let x = 0; x < this.size; x++) {
333 let runColor = false;
334 let runLen = 0;
335 const history = [0, 0, 0, 0, 0, 0, 0];
336 for (let y = 0; y < this.size; y++) {
337 if (this.modules[y][x] === runColor) {
338 runLen++;
339 if (runLen === 5) result += PENALTY_N1;
340 else if (runLen > 5) result++;
341 } else {
342 this.addRunHistory(runLen, history);
343 if (!runColor) result += this.countFinderPatterns(history) * PENALTY_N3;
344 runColor = this.modules[y][x];
345 runLen = 1;
346 }
347 }
348 result += this.finishRuns(runColor, runLen, history) * PENALTY_N3;
349 }
350 for (let y = 0; y < this.size - 1; y++) {
351 for (let x = 0; x < this.size - 1; x++) {
352 const color = this.modules[y][x];
353 if (color === this.modules[y][x + 1] && color === this.modules[y + 1][x] && color === this.modules[y + 1][x + 1]) {
354 result += PENALTY_N2;
355 }
356 }
357 }
358 let dark = 0;
359 for (const row of this.modules) for (const cell of row) if (cell) dark++;
360 const total = this.size * this.size;
361 const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
362 result += k * PENALTY_N4;
363 return result;
364 }
365
366 countFinderPatterns(history) {
367 const n = history[1];
368 const core = n > 0 && history[2] === n && history[3] === n * 3 && history[4] === n && history[5] === n;
369 return (core && history[0] >= n * 4 && history[6] >= n ? 1 : 0) +
370 (core && history[6] >= n * 4 && history[0] >= n ? 1 : 0);
371 }
372
373 finishRuns(runColor, runLen, history) {
374 if (runColor) {
375 this.addRunHistory(runLen, history);
376 runLen = 0;
377 }
378 runLen += this.size;
379 this.addRunHistory(runLen, history);
380 return this.countFinderPatterns(history);
381 }
382
383 addRunHistory(runLen, history) {
384 if (history[0] === 0) runLen += this.size;
385 history.pop();
386 history.unshift(runLen);
387 }
388 }
389
390 function encode(text, level) {
391 const bytes = utf8Bytes(String(text));
392 let version = 0;
393 for (let v = 1; v <= 40; v++) {
394 const capacity = numDataCodewords(v, level) * 8;
395 if (4 + byteCountBits(v) + bytes.length * 8 <= capacity) {
396 version = v;
397 break;
398 }
399 }
400 if (!version) throw new RangeError('text does not fit a QR code (version 40)');
401 const capacity = numDataCodewords(version, level) * 8;
402 const bb = [];
403 appendBits(0x4, 4, bb);
404 appendBits(bytes.length, byteCountBits(version), bb);
405 for (const b of bytes) appendBits(b, 8, bb);
406 appendBits(0, Math.min(4, capacity - bb.length), bb);
407 appendBits(0, (8 - (bb.length % 8)) % 8, bb);
408 for (let pad = 0xEC; bb.length < capacity; pad ^= 0xEC ^ 0x11) appendBits(pad, 8, bb);
409 const data = [];
410 while (data.length * 8 < bb.length) data.push(0);
411 bb.forEach((bit, i) => { data[i >>> 3] |= bit << (7 - (i & 7)); });
412 return new QrCode(version, level, data);
413 }
414
415 function levelOf(ecc) {
416 const key = String(ecc == null ? 'M' : ecc).toUpperCase();
417 const level = ECC[key];
418 if (!level) throw new RangeError('ecc must be L, M, Q or H');
419 return level;
420 }
421
422 function matrix(text, { ecc = 'M' } = {}) {
423 const qr = encode(text, levelOf(ecc));
424 return {
425 size: qr.size,
426 dark: (x, y) => x >= 0 && x < qr.size && y >= 0 && y < qr.size && qr.modules[y][x],
427 };
428 }
429
430 const esc = (s) => String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
431
432 function svg(text, { ecc = 'M', margin = 4, dark = 'currentColor', light = 'transparent', title = '' } = {}) {
433 const qr = encode(text, levelOf(ecc));
434 let m = Number(margin);
435 if (!Number.isFinite(m) || m < 0) throw new RangeError('margin must be a non-negative number');
436 m = Math.floor(m);
437 const dim = qr.size + m * 2;
438 const runs = [];
439 for (let y = 0; y < qr.size; y++) {
440 for (let x = 0; x < qr.size;) {
441 if (!qr.modules[y][x]) { x++; continue; }
442 const start = x;
443 while (x < qr.size && qr.modules[y][x]) x++;
444 runs.push(`M${start + m} ${y + m}h${x - start}v1h-${x - start}z`);
445 }
446 }
447 const label = title ? ` role="img" aria-label="${esc(title)}"` : ' aria-hidden="true"';
448 const out = [
449 `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${dim} ${dim}" width="${dim}" height="${dim}" shape-rendering="crispEdges"${label}>`,
450 title ? `<title>${esc(title)}</title>` : '',
451 light !== 'transparent' ? `<rect width="${dim}" height="${dim}" fill="${esc(light)}"/>` : '',
452 `<path fill="${esc(dark)}" d="${runs.join('')}"/>`,
453 '</svg>',
454 ];
455 return out.join('');
456 }
457
458 return { matrix, svg };
459});
460