qr.js (15.0 KB)


  1 // Self-contained QR encoder (byte mode, EC level M, versions 1-40).
  2 // createMatrix(text) returns a square matrix[row][col] of 0/1 modules
  3 
  4 (function () {
  5 	// Galois Field tables for Reed-Solomon
  6 	var GF_EXP = new Array(512);
  7 	var GF_LOG = new Array(256);
  8 
  9 	(function initGF() {
 10 		var x = 1;
 11 		for (var i = 0; i < 255; i++) {
 12 			GF_EXP[i] = x;
 13 			GF_LOG[x] = i;
 14 			x <<= 1;
 15 			if (x & 0x100) x ^= 0x11d;
 16 		}
 17 		for (var j = 255; j < 512; j++) {
 18 			GF_EXP[j] = GF_EXP[j - 255];
 19 		}
 20 	})();
 21 
 22 	function gfMul(a, b) {
 23 		if (a === 0 || b === 0) return 0;
 24 		return GF_EXP[GF_LOG[a] + GF_LOG[b]];
 25 	}
 26 
 27 	// Alignment pattern positions lookup table (from QR spec)
 28 	var ALIGNMENT_POSITIONS = [
 29 		null,
 30 		[],
 31 		[6, 18],
 32 		[6, 22],
 33 		[6, 26],
 34 		[6, 30],
 35 		[6, 34],
 36 		[6, 22, 38],
 37 		[6, 24, 42],
 38 		[6, 26, 46],
 39 		[6, 28, 50],
 40 		[6, 30, 54],
 41 		[6, 32, 58],
 42 		[6, 34, 62],
 43 		[6, 26, 46, 66],
 44 		[6, 26, 48, 70],
 45 		[6, 26, 50, 74],
 46 		[6, 30, 54, 78],
 47 		[6, 30, 56, 82],
 48 		[6, 30, 58, 86],
 49 		[6, 34, 62, 90],
 50 		[6, 28, 50, 72, 94],
 51 		[6, 26, 50, 74, 98],
 52 		[6, 30, 54, 78, 102],
 53 		[6, 28, 54, 80, 106],
 54 		[6, 32, 58, 84, 110],
 55 		[6, 30, 58, 86, 114],
 56 		[6, 34, 62, 90, 118],
 57 		[6, 26, 50, 74, 98, 122],
 58 		[6, 30, 54, 78, 102, 126],
 59 		[6, 26, 52, 78, 104, 130],
 60 		[6, 30, 56, 82, 108, 134],
 61 		[6, 34, 60, 86, 112, 138],
 62 		[6, 30, 58, 86, 114, 142],
 63 		[6, 34, 62, 90, 118, 146],
 64 		[6, 30, 54, 78, 102, 126, 150],
 65 		[6, 24, 50, 76, 102, 128, 154],
 66 		[6, 28, 54, 80, 106, 132, 158],
 67 		[6, 32, 58, 84, 110, 136, 162],
 68 		[6, 26, 54, 82, 110, 138, 166],
 69 		[6, 30, 58, 86, 114, 142, 170]
 70 	];
 71 
 72 	// EC_TABLE: [totalDataCodewords, ecPerBlock, group1Blocks, group1Data, group2Blocks, group2Data]
 73 	var EC_TABLE = [
 74 		null,
 75 		[16, 10, 1, 16, 0, 0],     // V1
 76 		[28, 16, 1, 28, 0, 0],     // V2
 77 		[44, 26, 1, 44, 0, 0],     // V3
 78 		[64, 18, 2, 32, 0, 0],     // V4
 79 		[86, 24, 2, 43, 0, 0],     // V5
 80 		[108, 16, 4, 27, 0, 0],    // V6
 81 		[124, 18, 4, 31, 0, 0],    // V7
 82 		[154, 22, 2, 38, 2, 39],   // V8
 83 		[182, 22, 3, 36, 2, 37],   // V9
 84 		[216, 26, 4, 43, 1, 44],   // V10
 85 		[254, 30, 1, 50, 4, 51],   // V11
 86 		[290, 22, 6, 36, 2, 37],   // V12
 87 		[334, 22, 8, 37, 1, 38],   // V13
 88 		[365, 24, 4, 40, 5, 41],   // V14
 89 		[415, 24, 5, 41, 5, 42],   // V15
 90 		[453, 28, 7, 45, 3, 46],   // V16
 91 		[507, 28, 10, 46, 1, 47],  // V17
 92 		[563, 26, 9, 43, 4, 44],   // V18
 93 		[627, 26, 3, 44, 11, 45],  // V19
 94 		[669, 26, 3, 41, 13, 42],  // V20
 95 		[714, 26, 17, 42, 0, 0],   // V21
 96 		[782, 28, 17, 46, 0, 0],   // V22
 97 		[860, 28, 4, 47, 14, 48],  // V23
 98 		[914, 28, 6, 45, 14, 46],  // V24
 99 		[1000, 28, 8, 47, 13, 48], // V25
100 		[1062, 28, 19, 46, 4, 47], // V26
101 		[1128, 28, 22, 45, 3, 46], // V27
102 		[1193, 28, 3, 45, 23, 46], // V28
103 		[1267, 28, 21, 45, 7, 46], // V29
104 		[1373, 28, 19, 47, 10, 48],// V30
105 		[1455, 28, 2, 46, 29, 47], // V31
106 		[1541, 28, 10, 46, 23, 47],// V32
107 		[1631, 28, 14, 46, 21, 47],// V33
108 		[1725, 28, 14, 46, 23, 47],// V34
109 		[1812, 28, 12, 47, 26, 48],// V35
110 		[1914, 28, 6, 47, 34, 48], // V36
111 		[1992, 28, 29, 46, 14, 47],// V37
112 		[2102, 28, 13, 46, 32, 47],// V38
113 		[2216, 28, 40, 47, 7, 48], // V39
114 		[2334, 28, 18, 47, 31, 48] // V40
115 	];
116 
117 	function getVersion(byteLength) {
118 		for (var v = 1; v <= 40; v++) {
119 			var charCountBits = v <= 9 ? 8 : 16;
120 			var dataBits = 4 + charCountBits + byteLength * 8;
121 			var dataBytes = Math.ceil(dataBits / 8);
122 			if (dataBytes <= EC_TABLE[v][0]) return v;
123 		}
124 		return 40;
125 	}
126 
127 	function createQR(text) {
128 		var bytes = new TextEncoder().encode(text);
129 		var version = getVersion(bytes.length);
130 		var size = version * 4 + 17;
131 		var matrix = Array.from({ length: size }, function () { return Array(size).fill(null); });
132 
133 		addFinderPatterns(matrix, size);
134 		addSeparators(matrix, size);
135 		addTimingPatterns(matrix, size);
136 		addAlignmentPatterns(matrix, version, size);
137 		addDarkModule(matrix, version);
138 
139 		reserveFormatAreas(matrix, size);
140 		if (version >= 7) reserveVersionAreas(matrix, size);
141 
142 		var reserved = createReservedMap(matrix, size);
143 
144 		var data = encodeData(bytes, version);
145 		var ecData = addErrorCorrection(data, version);
146 		placeData(matrix, ecData, size);
147 
148 		var mask = applyBestMask(matrix, size, reserved);
149 		addFormatInfo(matrix, size, mask);
150 		if (version >= 7) addVersionInfo(matrix, version);
151 
152 		return matrix;
153 	}
154 
155 	// Finder pattern: 7x7 with specific structure
156 	// Outer black border, inner white border, center 3x3 black
157 	function addFinderPatterns(matrix, size) {
158 		var positions = [
159 			[0, 0],           // top-left
160 			[0, size - 7],    // top-right
161 			[size - 7, 0]     // bottom-left
162 		];
163 
164 		for (var p = 0; p < positions.length; p++) {
165 			var startRow = positions[p][0], startCol = positions[p][1];
166 			for (var r = 0; r < 7; r++) {
167 				for (var c = 0; c < 7; c++) {
168 					var isBlack;
169 					if (r === 0 || r === 6 || c === 0 || c === 6) {
170 						isBlack = true; // outer border
171 					} else if (r === 1 || r === 5 || c === 1 || c === 5) {
172 						isBlack = false; // inner white border
173 					} else {
174 						isBlack = true; // center 3x3
175 					}
176 					matrix[startRow + r][startCol + c] = isBlack ? 1 : 0;
177 				}
178 			}
179 		}
180 	}
181 
182 	// Separators: 1-module white border around finder patterns
183 	function addSeparators(matrix, size) {
184 		for (var i = 0; i < 8; i++) {
185 			matrix[7][i] = 0;
186 			matrix[i][7] = 0;
187 		}
188 		for (var j = 0; j < 8; j++) {
189 			matrix[7][size - 8 + j] = 0;
190 			matrix[j][size - 8] = 0;
191 		}
192 		for (var k = 0; k < 8; k++) {
193 			matrix[size - 8][k] = 0;
194 			matrix[size - 8 + k][7] = 0;
195 		}
196 	}
197 
198 	// Timing patterns: alternating modules on row 6 and column 6
199 	function addTimingPatterns(matrix, size) {
200 		for (var i = 0; i < size; i++) {
201 			var bit = i % 2 === 0 ? 1 : 0;
202 			if (matrix[6][i] === null) matrix[6][i] = bit;
203 			if (matrix[i][6] === null) matrix[i][6] = bit;
204 		}
205 	}
206 
207 	// Alignment patterns: 5x5 with black border, white inner, black center
208 	function addAlignmentPatterns(matrix, version, size) {
209 		if (version < 2) return;
210 
211 		var positions = ALIGNMENT_POSITIONS[version];
212 
213 		for (var ri = 0; ri < positions.length; ri++) {
214 			for (var ci = 0; ci < positions.length; ci++) {
215 				var row = positions[ri], col = positions[ci];
216 				if (isInFinderPattern(row, col, size)) continue; // skip finder overlap
217 
218 				for (var r = -2; r <= 2; r++) {
219 					for (var c = -2; c <= 2; c++) {
220 						var isBlack;
221 						if (Math.abs(r) === 2 || Math.abs(c) === 2) {
222 							isBlack = true;  // outer border
223 						} else if (r === 0 && c === 0) {
224 							isBlack = true;  // center
225 						} else {
226 							isBlack = false; // inner white ring
227 						}
228 						matrix[row + r][col + c] = isBlack ? 1 : 0;
229 					}
230 				}
231 			}
232 		}
233 	}
234 
235 	function isInFinderPattern(row, col, size) {
236 		if (row <= 7 && col <= 7) return true;
237 		if (row <= 7 && col >= size - 8) return true;
238 		if (row >= size - 8 && col <= 7) return true;
239 		return false;
240 	}
241 
242 	// Dark module: always at matrix[4*version+9][8] per spec
243 	function addDarkModule(matrix, version) {
244 		matrix[4 * version + 9][8] = 1;
245 	}
246 
247 	// Reserve format info areas (filled in later)
248 	function reserveFormatAreas(matrix, size) {
249 		for (var i = 0; i < 9; i++) {
250 			if (matrix[8][i] === null) matrix[8][i] = 0;
251 			if (matrix[i][8] === null) matrix[i][8] = 0;
252 		}
253 		for (var j = 0; j < 8; j++) {
254 			if (matrix[8][size - 1 - j] === null) matrix[8][size - 1 - j] = 0;
255 		}
256 		for (var k = 0; k < 7; k++) {
257 			if (matrix[size - 1 - k][8] === null) matrix[size - 1 - k][8] = 0;
258 		}
259 	}
260 
261 	// Reserve version info areas (version 7+)
262 	function reserveVersionAreas(matrix, size) {
263 		for (var i = 0; i < 6; i++) {
264 			for (var j = 0; j < 3; j++) {
265 				matrix[i][size - 11 + j] = 0;
266 			}
267 		}
268 		for (var k = 0; k < 6; k++) {
269 			for (var m = 0; m < 3; m++) {
270 				matrix[size - 11 + m][k] = 0;
271 			}
272 		}
273 	}
274 
275 	function createReservedMap(matrix, size) {
276 		var reserved = Array.from({ length: size }, function () { return Array(size).fill(false); });
277 
278 		for (var row = 0; row < size; row++) {
279 			for (var col = 0; col < size; col++) {
280 				if (matrix[row][col] !== null) {
281 					reserved[row][col] = true;
282 				}
283 			}
284 		}
285 
286 		return reserved;
287 	}
288 
289 	function encodeData(bytes, version) {
290 		var bits = [];
291 		var charCountBits = version <= 9 ? 8 : 16;
292 
293 		bits.push(0, 1, 0, 0); // mode: byte
294 
295 		for (var i = charCountBits - 1; i >= 0; i--) {
296 			bits.push((bytes.length >> i) & 1); // character count
297 		}
298 
299 		for (var b = 0; b < bytes.length; b++) {
300 			for (var k = 7; k >= 0; k--) {
301 				bits.push((bytes[b] >> k) & 1);
302 			}
303 		}
304 
305 		// terminator (up to 4 zeros)
306 		var capacity = EC_TABLE[version][0] * 8;
307 		for (var t = 0; t < 4 && bits.length < capacity; t++) {
308 			bits.push(0);
309 		}
310 
311 		while (bits.length % 8 !== 0 && bits.length < capacity) {
312 			bits.push(0); // pad to byte boundary
313 		}
314 
315 		// pad codewords
316 		var padBytes = [0xEC, 0x11];
317 		var padIndex = 0;
318 		while (bits.length < capacity) {
319 			var pad = padBytes[padIndex++ % 2];
320 			for (var pb = 7; pb >= 0; pb--) {
321 				bits.push((pad >> pb) & 1);
322 			}
323 		}
324 
325 		return bits;
326 	}
327 
328 	function addErrorCorrection(data, version) {
329 		var row = EC_TABLE[version];
330 		var totalCap = row[0], ecPerBlock = row[1];
331 		var g1Count = row[2], g1Data = row[3], g2Count = row[4], g2Data = row[5];
332 
333 		var dataBytes = [];
334 		for (var i = 0; i < data.length; i += 8) {
335 			var byte = 0;
336 			for (var j = 0; j < 8; j++) byte = (byte << 1) | (data[i + j] || 0);
337 			dataBytes.push(byte);
338 		}
339 
340 		while (dataBytes.length < totalCap) {
341 			dataBytes.push(dataBytes.length % 2 === 0 ? 0xEC : 0x11); // pad to capacity
342 		}
343 
344 		var blocks = [];
345 		var ecBlocks = [];
346 		var offset = 0;
347 
348 		for (var a = 0; a < g1Count; a++) {
349 			var b1 = dataBytes.slice(offset, offset + g1Data);
350 			blocks.push(b1);
351 			ecBlocks.push(generateECBytes(b1, ecPerBlock));
352 			offset += g1Data;
353 		}
354 
355 		for (var c = 0; c < g2Count; c++) {
356 			var b2 = dataBytes.slice(offset, offset + g2Data);
357 			blocks.push(b2);
358 			ecBlocks.push(generateECBytes(b2, ecPerBlock));
359 			offset += g2Data;
360 		}
361 
362 		var resultBytes = [];
363 		var maxDataSize = Math.max(g1Data, g2Data);
364 
365 		// interleave data byte-by-byte across all blocks
366 		for (var d = 0; d < maxDataSize; d++) {
367 			for (var e = 0; e < blocks.length; e++) {
368 				if (d < blocks[e].length) {
369 					resultBytes.push(blocks[e][d]);
370 				}
371 			}
372 		}
373 
374 		// interleave EC byte-by-byte (EC sizes are always equal)
375 		for (var f = 0; f < ecPerBlock; f++) {
376 			for (var g = 0; g < ecBlocks.length; g++) {
377 				resultBytes.push(ecBlocks[g][f]);
378 			}
379 		}
380 
381 		var resultBits = [];
382 		for (var rb = 0; rb < resultBytes.length; rb++) {
383 			for (var h = 7; h >= 0; h--) resultBits.push((resultBytes[rb] >> h) & 1);
384 		}
385 		return resultBits;
386 	}
387 
388 	function generateECBytes(data, ecCount) {
389 		var gen = [1];
390 		for (var i = 0; i < ecCount; i++) {
391 			var next = new Array(gen.length + 1).fill(0);
392 			for (var j = 0; j < gen.length; j++) {
393 				next[j] ^= gen[j];
394 				next[j + 1] ^= gfMul(gen[j], GF_EXP[i]);
395 			}
396 			for (var k = 0; k < next.length; k++) gen[k] = next[k];
397 			gen.length = next.length;
398 		}
399 
400 		var remainder = new Array(ecCount).fill(0);
401 		for (var d = 0; d < data.length; d++) {
402 			var factor = data[d] ^ remainder[0];
403 			remainder.shift();
404 			remainder.push(0);
405 			for (var m = 0; m < ecCount; m++) {
406 				remainder[m] ^= gfMul(gen[m + 1], factor);
407 			}
408 		}
409 
410 		return remainder;
411 	}
412 
413 	// Place data in zigzag pattern from bottom-right, skipping column 6
414 	function placeData(matrix, data, size) {
415 		var bitIndex = 0;
416 		var up = true;
417 
418 		for (var col = size - 1; col >= 1; col -= 2) {
419 			if (col === 6) col = 5;  // skip timing column
420 
421 			for (var i = 0; i < size; i++) {
422 				var row = up ? size - 1 - i : i;
423 
424 				for (var j = 0; j < 2; j++) {
425 					var c = col - j;
426 					if (matrix[row][c] === null) {
427 						matrix[row][c] = bitIndex < data.length ? data[bitIndex++] : 0;
428 					}
429 				}
430 			}
431 			up = !up;
432 		}
433 	}
434 
435 	function applyBestMask(matrix, size, reserved) {
436 		var bestMask = 0;
437 		var bestPenalty = Infinity;
438 
439 		for (var mask = 0; mask < 8; mask++) {
440 			var copy = matrix.map(function (r) { return r.slice(); });
441 			applyMask(copy, size, mask, reserved);
442 			var penalty = calculatePenalty(copy, size);
443 
444 			if (penalty < bestPenalty) {
445 				bestPenalty = penalty;
446 				bestMask = mask;
447 			}
448 		}
449 
450 		applyMask(matrix, size, bestMask, reserved);
451 		return bestMask;
452 	}
453 
454 	function applyMask(matrix, size, mask, reserved) {
455 		for (var row = 0; row < size; row++) {
456 			for (var col = 0; col < size; col++) {
457 				if (reserved[row][col]) continue;
458 
459 				var invert = false;
460 				switch (mask) {
461 					case 0: invert = (row + col) % 2 === 0; break;
462 					case 1: invert = row % 2 === 0; break;
463 					case 2: invert = col % 3 === 0; break;
464 					case 3: invert = (row + col) % 3 === 0; break;
465 					case 4: invert = (Math.floor(row / 2) + Math.floor(col / 3)) % 2 === 0; break;
466 					case 5: invert = (row * col) % 2 + (row * col) % 3 === 0; break;
467 					case 6: invert = ((row * col) % 2 + (row * col) % 3) % 2 === 0; break;
468 					case 7: invert = ((row + col) % 2 + (row * col) % 3) % 2 === 0; break;
469 				}
470 
471 				if (invert) matrix[row][col] ^= 1;
472 			}
473 		}
474 	}
475 
476 	function calculatePenalty(matrix, size) {
477 		var penalty = 0;
478 
479 		// rule 1: 5+ consecutive same-color modules
480 		for (var i = 0; i < size; i++) {
481 			var rowRun = 1, colRun = 1;
482 			for (var j = 1; j < size; j++) {
483 				rowRun = matrix[i][j] === matrix[i][j - 1] ? rowRun + 1 : 1;
484 				if (rowRun === 5) penalty += 3;
485 				else if (rowRun > 5) penalty += 1;
486 
487 				colRun = matrix[j][i] === matrix[j - 1][i] ? colRun + 1 : 1;
488 				if (colRun === 5) penalty += 3;
489 				else if (colRun > 5) penalty += 1;
490 			}
491 		}
492 
493 		// rule 2: 2x2 blocks of same color
494 		for (var r = 0; r < size - 1; r++) {
495 			for (var c = 0; c < size - 1; c++) {
496 				var v = matrix[r][c];
497 				if (v === matrix[r][c + 1] && v === matrix[r + 1][c] && v === matrix[r + 1][c + 1]) {
498 					penalty += 3;
499 				}
500 			}
501 		}
502 
503 		return penalty;
504 	}
505 
506 	function addFormatInfo(matrix, size, mask) {
507 		var format = (0 << 3) | mask; // Level M (00) + Mask
508 		var rem = format;
509 		for (var i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
510 		var bits = ((format << 10) | rem) ^ 0x5412;
511 
512 		function getBit(i) { return (bits >> i) & 1; }
513 
514 		var coords = [
515 			[8, 0], [8, 1], [8, 2], [8, 3], [8, 4], [8, 5], [8, 7], [8, 8], [7, 8], [5, 8], [4, 8], [3, 8], [2, 8], [1, 8], [0, 8]
516 		];
517 
518 		for (var c = 0; c < 15; c++) {
519 			var r = coords[c][0], col = coords[c][1];
520 			var bit = getBit(c);
521 			matrix[r][col] = bit; // first copy
522 			// second copy, mirrored around the other finders
523 			if (c < 8) {
524 				matrix[8][size - 1 - c] = bit;
525 			} else {
526 				matrix[size - 1 - (14 - c)][8] = bit;
527 			}
528 		}
529 	}
530 
531 	function addVersionInfo(matrix, version) {
532 		if (version < 7) return;
533 
534 		// 18-bit BCH Error Corrected Version Codes
535 		var versionBits = {
536 			7:  0x07C94, 8:  0x085BC, 9:  0x09A99, 10: 0x0A4D3,
537 			11: 0x0BBF6, 12: 0x0C762, 13: 0x0D847, 14: 0x0E60D,
538 			15: 0x0F928, 16: 0x10B78, 17: 0x1145D, 18: 0x12A17,
539 			19: 0x13532, 20: 0x149A6, 21: 0x15683, 22: 0x168C9,
540 			23: 0x177EC, 24: 0x18EC4, 25: 0x191E1, 26: 0x1AFAB,
541 			27: 0x1B08E, 28: 0x1CC1A, 29: 0x1D33F, 30: 0x1ED75,
542 			31: 0x1F250, 32: 0x209D5, 33: 0x216F0, 34: 0x228BA,
543 			35: 0x2379F, 36: 0x24B0B, 37: 0x2542E, 38: 0x26A64,
544 			39: 0x27541, 40: 0x28C69
545 		};
546 
547 		var bits = versionBits[version];
548 		var size = matrix.length;
549 
550 		for (var i = 0; i < 18; i++) {
551 			var bit = (bits >> i) & 1;
552 			var a = Math.floor(i / 3);
553 			var b = i % 3;
554 
555 			matrix[size - 11 + b][a] = bit; // bottom-left block
556 			matrix[a][size - 11 + b] = bit; // top-right block
557 		}
558 	}
559 
560 	window.qr = { createMatrix: createQR };
561 })();