@crewhaus/tool-media 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lib/qr.js ADDED
@@ -0,0 +1,544 @@
1
+ /**
2
+ * A QR Code encoder, written from ISO/IEC 18004.
3
+ *
4
+ * ## What it does
5
+ *
6
+ * - **Byte mode only.** The payload is encoded as UTF-8 bytes under mode
7
+ * indicator `0100`. Numeric and alphanumeric modes would pack digits and
8
+ * uppercase text more densely, but byte mode encodes every input
9
+ * correctly, and a scanner reads all of them. ECI is not emitted; readers
10
+ * overwhelmingly treat byte mode as UTF-8, which is what this produces.
11
+ * - **Versions 1 to 10** (21x21 up to 57x57 modules), which is 271 bytes at
12
+ * error-correction level L. Beyond version 10 the block layout table grows
13
+ * another 120 rows; rather than carry a table this package cannot
14
+ * meaningfully test, larger payloads are refused by name.
15
+ * - **All four error-correction levels** — L (~7%), M (~15%), Q (~25%),
16
+ * H (~30%) — with Reed-Solomon over GF(2^8) modulo 0x11D, block splitting
17
+ * and interleaving exactly as the standard specifies.
18
+ * - **All eight data masks**, scored with the standard's four penalty rules;
19
+ * the lowest-penalty mask wins, ties going to the lower mask number. That
20
+ * makes mask selection a pure function of the payload.
21
+ * - Format information as BCH(15,5) XOR 0x5412, and version information as
22
+ * BCH(18,6) for versions 7 and up.
23
+ *
24
+ * ## What it does not do
25
+ *
26
+ * Structured Append (splitting one payload across several symbols), Micro QR,
27
+ * Kanji mode, and FNC1/GS1 application indicators. Each of those changes what
28
+ * a scanner does with the result, so none of them is approximated here.
29
+ */
30
+ import { MediaFormatError } from "./bytes";
31
+ export const ECC_LEVELS = Object.freeze(["L", "M", "Q", "H"]);
32
+ /** Lowest and highest symbol version this encoder produces. */
33
+ export const MIN_VERSION = 1;
34
+ export const MAX_VERSION = 10;
35
+ /** ECC level to its two-bit format indicator (note: not the L/M/Q/H order). */
36
+ const ECC_FORMAT_BITS = { L: 1, M: 0, Q: 3, H: 2 };
37
+ /** Error-correction codewords per block, by level, for versions 1..10. */
38
+ const ECC_CODEWORDS_PER_BLOCK = {
39
+ L: [7, 10, 15, 20, 26, 18, 20, 24, 30, 18],
40
+ M: [10, 16, 26, 18, 24, 16, 18, 22, 22, 26],
41
+ Q: [13, 22, 18, 26, 18, 24, 18, 22, 20, 24],
42
+ H: [17, 28, 22, 16, 22, 28, 26, 26, 24, 28],
43
+ };
44
+ /** Error-correction block count, by level, for versions 1..10. */
45
+ const ECC_BLOCKS = {
46
+ L: [1, 1, 1, 1, 1, 2, 2, 2, 2, 4],
47
+ M: [1, 1, 1, 2, 2, 4, 4, 4, 5, 5],
48
+ Q: [1, 1, 2, 2, 4, 4, 6, 6, 8, 8],
49
+ H: [1, 1, 2, 4, 4, 4, 5, 6, 8, 8],
50
+ };
51
+ // --- GF(2^8) -------------------------------------------------------------
52
+ /** Multiply in GF(2^8) modulo the QR primitive polynomial x^8+x^4+x^3+x^2+1. */
53
+ function gfMultiply(x, y) {
54
+ let z = 0;
55
+ for (let i = 7; i >= 0; i--) {
56
+ z = (z << 1) ^ ((z >>> 7) * 0x11d);
57
+ z ^= ((y >>> i) & 1) * x;
58
+ }
59
+ return z & 0xff;
60
+ }
61
+ /** Coefficients of the degree-`degree` Reed-Solomon generator polynomial. */
62
+ function rsDivisor(degree) {
63
+ const result = new Uint8Array(degree);
64
+ result[degree - 1] = 1;
65
+ let root = 1;
66
+ for (let i = 0; i < degree; i++) {
67
+ for (let j = 0; j < degree; j++) {
68
+ result[j] = gfMultiply(result[j], root);
69
+ if (j + 1 < degree)
70
+ result[j] = result[j] ^ result[j + 1];
71
+ }
72
+ root = gfMultiply(root, 0x02);
73
+ }
74
+ return result;
75
+ }
76
+ /** The Reed-Solomon remainder — the error-correction codewords for a block. */
77
+ export function rsRemainder(data, divisor) {
78
+ const result = new Uint8Array(divisor.length);
79
+ for (const byte of data) {
80
+ const factor = byte ^ result[0];
81
+ result.copyWithin(0, 1);
82
+ result[result.length - 1] = 0;
83
+ for (let i = 0; i < result.length; i++) {
84
+ result[i] = result[i] ^ gfMultiply(divisor[i], factor);
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+ // --- Capacity ------------------------------------------------------------
90
+ /** Alignment-pattern centre coordinates for a version, ascending. */
91
+ export function alignmentPositions(version) {
92
+ if (version === 1)
93
+ return [];
94
+ const size = 17 + 4 * version;
95
+ const count = Math.floor(version / 7) + 2;
96
+ const step = Math.ceil((version * 4 + 4) / (count * 2 - 2)) * 2;
97
+ const result = [6];
98
+ for (let pos = size - 7; result.length < count; pos -= step)
99
+ result.splice(1, 0, pos);
100
+ return result;
101
+ }
102
+ /** Data modules available in a symbol before codewords are laid out. */
103
+ function rawDataModules(version) {
104
+ let result = (16 * version + 128) * version + 64;
105
+ if (version >= 2) {
106
+ const count = Math.floor(version / 7) + 2;
107
+ result -= (25 * count - 10) * count - 55;
108
+ if (version >= 7)
109
+ result -= 36;
110
+ }
111
+ return result;
112
+ }
113
+ /** Data codewords (payload plus padding) a version/level pair carries. */
114
+ export function dataCodewordCount(version, ecc) {
115
+ const index = version - 1;
116
+ return (Math.floor(rawDataModules(version) / 8) -
117
+ ECC_CODEWORDS_PER_BLOCK[ecc][index] * ECC_BLOCKS[ecc][index]);
118
+ }
119
+ /**
120
+ * How a version/level pair splits its codewords into blocks. Exported
121
+ * because reversing the interleave — which is what a test that reads a
122
+ * finished symbol back has to do — needs exactly these five numbers.
123
+ */
124
+ export function blockLayout(version, ecc) {
125
+ const index = version - 1;
126
+ const rawCodewords = Math.floor(rawDataModules(version) / 8);
127
+ const numBlocks = ECC_BLOCKS[ecc][index];
128
+ const blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecc][index];
129
+ return {
130
+ rawCodewords,
131
+ numBlocks,
132
+ blockEccLen,
133
+ shortBlockLen: Math.floor(rawCodewords / numBlocks),
134
+ numShortBlocks: numBlocks - (rawCodewords % numBlocks),
135
+ dataCodewords: rawCodewords - blockEccLen * numBlocks,
136
+ };
137
+ }
138
+ /** Remainder bits after the last codeword, which stay light. */
139
+ export function remainderBits(version) {
140
+ return rawDataModules(version) % 8;
141
+ }
142
+ /** Payload bytes that fit in this version at this level, in byte mode. */
143
+ export function byteCapacity(version, ecc) {
144
+ const countBits = version < 10 ? 8 : 16;
145
+ const available = dataCodewordCount(version, ecc) * 8 - 4 - countBits;
146
+ return Math.max(0, Math.floor(available / 8));
147
+ }
148
+ /** The smallest version that fits `byteLength` at `ecc`, or `undefined`. */
149
+ export function smallestVersion(byteLength, ecc) {
150
+ for (let version = MIN_VERSION; version <= MAX_VERSION; version++) {
151
+ if (byteCapacity(version, ecc) >= byteLength)
152
+ return version;
153
+ }
154
+ return undefined;
155
+ }
156
+ // --- Bit buffer ----------------------------------------------------------
157
+ class BitBuffer {
158
+ bits = [];
159
+ append(value, length) {
160
+ for (let i = length - 1; i >= 0; i--)
161
+ this.bits.push((value >>> i) & 1);
162
+ }
163
+ get length() {
164
+ return this.bits.length;
165
+ }
166
+ toCodewords() {
167
+ const out = new Uint8Array(Math.ceil(this.bits.length / 8));
168
+ for (let i = 0; i < this.bits.length; i++) {
169
+ if (this.bits[i] === 1)
170
+ out[i >>> 3] = out[i >>> 3] | (0x80 >>> (i & 7));
171
+ }
172
+ return out;
173
+ }
174
+ }
175
+ /** Mode indicator, character count, payload, terminator and pad codewords. */
176
+ export function encodeDataCodewords(payload, version, ecc) {
177
+ const capacityBits = dataCodewordCount(version, ecc) * 8;
178
+ const countBits = version < 10 ? 8 : 16;
179
+ const buffer = new BitBuffer();
180
+ buffer.append(0b0100, 4);
181
+ buffer.append(payload.length, countBits);
182
+ for (const byte of payload)
183
+ buffer.append(byte, 8);
184
+ if (buffer.length > capacityBits) {
185
+ throw new MediaFormatError(`${payload.length} bytes need ${buffer.length} bits, over version ${version}-${ecc}'s ${capacityBits}`);
186
+ }
187
+ // Terminator: up to four zero bits, then zero-fill to a codeword boundary.
188
+ buffer.append(0, Math.min(4, capacityBits - buffer.length));
189
+ buffer.append(0, (8 - (buffer.length % 8)) % 8);
190
+ const codewords = Array.from(buffer.toCodewords());
191
+ // The standard's pad codewords, alternating, until the block is full.
192
+ for (let pad = 0xec; codewords.length < capacityBits / 8; pad ^= 0xec ^ 0x11) {
193
+ codewords.push(pad);
194
+ }
195
+ return Uint8Array.from(codewords);
196
+ }
197
+ /** Split into blocks, append each block's ECC, and interleave the result. */
198
+ export function addEccAndInterleave(data, version, ecc) {
199
+ const index = version - 1;
200
+ const numBlocks = ECC_BLOCKS[ecc][index];
201
+ const blockEccLen = ECC_CODEWORDS_PER_BLOCK[ecc][index];
202
+ const rawCodewords = Math.floor(rawDataModules(version) / 8);
203
+ const numShortBlocks = numBlocks - (rawCodewords % numBlocks);
204
+ const shortBlockLen = Math.floor(rawCodewords / numBlocks);
205
+ const divisor = rsDivisor(blockEccLen);
206
+ const blocks = [];
207
+ for (let i = 0, k = 0; i < numBlocks; i++) {
208
+ const dataLen = shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1);
209
+ const dat = data.subarray(k, k + dataLen);
210
+ k += dataLen;
211
+ const block = new Uint8Array(shortBlockLen + 1);
212
+ block.set(dat, 0);
213
+ // A short block is padded by one byte here purely so the interleave loop
214
+ // below can index every block alike; that byte is skipped when emitted.
215
+ block.set(rsRemainder(dat, divisor), shortBlockLen + 1 - blockEccLen);
216
+ blocks.push(block);
217
+ }
218
+ const result = new Uint8Array(rawCodewords);
219
+ let at = 0;
220
+ for (let i = 0; i < shortBlockLen + 1; i++) {
221
+ for (let j = 0; j < blocks.length; j++) {
222
+ if (i !== shortBlockLen - blockEccLen || j >= numShortBlocks) {
223
+ result[at++] = blocks[j][i];
224
+ }
225
+ }
226
+ }
227
+ return result;
228
+ }
229
+ function maskAt(mask, row, col) {
230
+ switch (mask) {
231
+ case 0:
232
+ return (row + col) % 2 === 0;
233
+ case 1:
234
+ return row % 2 === 0;
235
+ case 2:
236
+ return col % 3 === 0;
237
+ case 3:
238
+ return (row + col) % 3 === 0;
239
+ case 4:
240
+ return (Math.floor(row / 2) + Math.floor(col / 3)) % 2 === 0;
241
+ case 5:
242
+ return ((row * col) % 2) + ((row * col) % 3) === 0;
243
+ case 6:
244
+ return (((row * col) % 2) + ((row * col) % 3)) % 2 === 0;
245
+ default:
246
+ return (((row + col) % 2) + ((row * col) % 3)) % 2 === 0;
247
+ }
248
+ }
249
+ class Matrix {
250
+ size;
251
+ modules;
252
+ reserved;
253
+ constructor(size) {
254
+ this.size = size;
255
+ this.modules = Array.from({ length: size }, () => new Array(size).fill(false));
256
+ this.reserved = Array.from({ length: size }, () => new Array(size).fill(false));
257
+ }
258
+ setFunction(row, col, dark) {
259
+ this.modules[row][col] = dark;
260
+ this.reserved[row][col] = true;
261
+ }
262
+ isDark(row, col) {
263
+ return this.modules[row][col];
264
+ }
265
+ }
266
+ function drawFinder(matrix, row, col) {
267
+ // The 7x7 finder plus its one-module separator, clipped to the symbol.
268
+ for (let dy = -1; dy <= 7; dy++) {
269
+ for (let dx = -1; dx <= 7; dx++) {
270
+ const r = row + dy;
271
+ const c = col + dx;
272
+ if (r < 0 || r >= matrix.size || c < 0 || c >= matrix.size)
273
+ continue;
274
+ // Chebyshev distance from the centre: 0-1 dark, 2 light, 3 dark,
275
+ // 4 light — the last ring being the mandatory separator.
276
+ const distance = Math.max(Math.abs(dy - 3), Math.abs(dx - 3));
277
+ matrix.setFunction(r, c, distance !== 2 && distance !== 4);
278
+ }
279
+ }
280
+ }
281
+ function drawAlignment(matrix, row, col) {
282
+ for (let dy = -2; dy <= 2; dy++) {
283
+ for (let dx = -2; dx <= 2; dx++) {
284
+ matrix.setFunction(row + dy, col + dx, Math.max(Math.abs(dy), Math.abs(dx)) !== 1);
285
+ }
286
+ }
287
+ }
288
+ function drawFunctionPatterns(matrix, version) {
289
+ const size = matrix.size;
290
+ // Timing patterns, drawn first so the finders overwrite their own corners.
291
+ for (let i = 0; i < size; i++) {
292
+ matrix.setFunction(6, i, i % 2 === 0);
293
+ matrix.setFunction(i, 6, i % 2 === 0);
294
+ }
295
+ drawFinder(matrix, 0, 0);
296
+ drawFinder(matrix, 0, size - 7);
297
+ drawFinder(matrix, size - 7, 0);
298
+ const positions = alignmentPositions(version);
299
+ for (let i = 0; i < positions.length; i++) {
300
+ for (let j = 0; j < positions.length; j++) {
301
+ // The three corners belong to the finders.
302
+ const corner = (i === 0 && j === 0) ||
303
+ (i === 0 && j === positions.length - 1) ||
304
+ (i === positions.length - 1 && j === 0);
305
+ if (!corner)
306
+ drawAlignment(matrix, positions[i], positions[j]);
307
+ }
308
+ }
309
+ // Reserve the format-information modules (written for real later).
310
+ for (let i = 0; i <= 5; i++)
311
+ matrix.setFunction(i, 8, false);
312
+ matrix.setFunction(7, 8, false);
313
+ matrix.setFunction(8, 8, false);
314
+ matrix.setFunction(8, 7, false);
315
+ for (let i = 9; i < 15; i++)
316
+ matrix.setFunction(8, 14 - i, false);
317
+ for (let i = 0; i < 8; i++)
318
+ matrix.setFunction(8, size - 1 - i, false);
319
+ for (let i = 8; i < 15; i++)
320
+ matrix.setFunction(size - 15 + i, 8, false);
321
+ matrix.setFunction(size - 8, 8, true); // the always-dark module
322
+ if (version >= 7) {
323
+ let rem = version;
324
+ for (let i = 0; i < 12; i++)
325
+ rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
326
+ const bits = ((version << 12) | rem) >>> 0;
327
+ for (let i = 0; i < 18; i++) {
328
+ const dark = ((bits >>> i) & 1) !== 0;
329
+ const a = size - 11 + (i % 3);
330
+ const b = Math.floor(i / 3);
331
+ matrix.setFunction(b, a, dark);
332
+ matrix.setFunction(a, b, dark);
333
+ }
334
+ }
335
+ }
336
+ function drawFormatBits(matrix, ecc, mask) {
337
+ const size = matrix.size;
338
+ const data = (ECC_FORMAT_BITS[ecc] << 3) | mask;
339
+ let rem = data;
340
+ for (let i = 0; i < 10; i++)
341
+ rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
342
+ const bits = (((data << 10) | rem) ^ 0x5412) >>> 0;
343
+ const bit = (i) => ((bits >>> i) & 1) !== 0;
344
+ for (let i = 0; i <= 5; i++)
345
+ matrix.setFunction(i, 8, bit(i));
346
+ matrix.setFunction(7, 8, bit(6));
347
+ matrix.setFunction(8, 8, bit(7));
348
+ matrix.setFunction(8, 7, bit(8));
349
+ for (let i = 9; i < 15; i++)
350
+ matrix.setFunction(8, 14 - i, bit(i));
351
+ for (let i = 0; i < 8; i++)
352
+ matrix.setFunction(8, size - 1 - i, bit(i));
353
+ for (let i = 8; i < 15; i++)
354
+ matrix.setFunction(size - 15 + i, 8, bit(i));
355
+ matrix.setFunction(size - 8, 8, true);
356
+ }
357
+ /**
358
+ * The zigzag codeword walk: two-module-wide columns from the right edge
359
+ * leftwards, alternating upward and downward, skipping the vertical timing
360
+ * column. Exported so a test can walk it back the other way.
361
+ */
362
+ export function modulePositions(size) {
363
+ const positions = [];
364
+ for (let right = size - 1; right >= 1; right -= 2) {
365
+ // Column 6 is the vertical timing pattern; the pair that would contain
366
+ // it shifts one column left, and every pair after it shifts with it.
367
+ if (right === 6)
368
+ right = 5;
369
+ const upward = ((right + 1) & 2) === 0;
370
+ for (let step = 0; step < size; step++) {
371
+ const row = upward ? size - 1 - step : step;
372
+ positions.push([row, right]);
373
+ positions.push([row, right - 1]);
374
+ }
375
+ }
376
+ return positions;
377
+ }
378
+ function drawCodewords(matrix, codewords) {
379
+ let bitIndex = 0;
380
+ const totalBits = codewords.length * 8;
381
+ for (const [row, col] of modulePositions(matrix.size)) {
382
+ if (matrix.reserved[row][col] === true)
383
+ continue;
384
+ // Any module past the last codeword is a remainder bit, left light.
385
+ const dark = bitIndex < totalBits &&
386
+ ((codewords[bitIndex >>> 3] >>> (7 - (bitIndex & 7))) & 1) !== 0;
387
+ matrix.modules[row][col] = dark;
388
+ bitIndex++;
389
+ }
390
+ }
391
+ function applyMask(matrix, mask) {
392
+ for (let row = 0; row < matrix.size; row++) {
393
+ for (let col = 0; col < matrix.size; col++) {
394
+ if (matrix.reserved[row][col] === true)
395
+ continue;
396
+ if (maskAt(mask, row, col)) {
397
+ matrix.modules[row][col] = !matrix.modules[row][col];
398
+ }
399
+ }
400
+ }
401
+ }
402
+ const FINDER_RUN = [true, false, true, true, true, false, true];
403
+ function hasFinderLike(line, at) {
404
+ for (let i = 0; i < 7; i++) {
405
+ if (line[at + i] !== FINDER_RUN[i])
406
+ return false;
407
+ }
408
+ return true;
409
+ }
410
+ /** Penalty score under the standard's four rules; lower is better. */
411
+ export function penaltyScore(matrix) {
412
+ const size = matrix.size;
413
+ let score = 0;
414
+ const lines = [];
415
+ for (let row = 0; row < size; row++) {
416
+ lines.push(Array.from({ length: size }, (_, col) => matrix.isDark(row, col)));
417
+ }
418
+ for (let col = 0; col < size; col++) {
419
+ lines.push(Array.from({ length: size }, (_, row) => matrix.isDark(row, col)));
420
+ }
421
+ for (const line of lines) {
422
+ // Rule 1 — runs of five or more identical modules.
423
+ let runLength = 1;
424
+ for (let i = 1; i <= size; i++) {
425
+ if (i < size && line[i] === line[i - 1]) {
426
+ runLength++;
427
+ }
428
+ else {
429
+ if (runLength >= 5)
430
+ score += 3 + (runLength - 5);
431
+ runLength = 1;
432
+ }
433
+ }
434
+ // Rule 3 — the finder-like 1:1:3:1:1 run with four light modules beside
435
+ // it, in either order. Modules past the symbol edge count as light,
436
+ // because that is the quiet zone.
437
+ for (let i = 0; i + 7 <= size; i++) {
438
+ if (!hasFinderLike(line, i))
439
+ continue;
440
+ const window = (from) => Array.from({ length: 4 }, (_, k) => line[from + k] ?? false);
441
+ const clear = (run) => run.every((m) => !m);
442
+ if (clear(window(i - 4)) || clear(window(i + 7)))
443
+ score += 40;
444
+ }
445
+ }
446
+ // Rule 2 — every 2x2 block of one colour.
447
+ for (let row = 0; row + 1 < size; row++) {
448
+ for (let col = 0; col + 1 < size; col++) {
449
+ const value = matrix.isDark(row, col);
450
+ if (matrix.isDark(row, col + 1) === value &&
451
+ matrix.isDark(row + 1, col) === value &&
452
+ matrix.isDark(row + 1, col + 1) === value) {
453
+ score += 3;
454
+ }
455
+ }
456
+ }
457
+ // Rule 4 — how far the dark proportion strays from half.
458
+ let dark = 0;
459
+ for (let row = 0; row < size; row++) {
460
+ for (let col = 0; col < size; col++)
461
+ if (matrix.isDark(row, col))
462
+ dark++;
463
+ }
464
+ // Rule 4 — every whole 5% the dark proportion strays from half, scored in
465
+ // integers so the comparison never depends on floating point.
466
+ const total = size * size;
467
+ const k = Math.floor(Math.abs(dark * 20 - total * 10) / total);
468
+ score += k * 10;
469
+ return score;
470
+ }
471
+ /** Encode `text` (as UTF-8) into a QR symbol. */
472
+ export function encodeQr(text, options = {}) {
473
+ const ecc = options.ecc ?? "M";
474
+ const payload = new TextEncoder().encode(text);
475
+ let version;
476
+ if (options.version !== undefined) {
477
+ version = options.version;
478
+ if (version < MIN_VERSION || version > MAX_VERSION) {
479
+ throw new MediaFormatError(`version ${version} is outside the ${MIN_VERSION}-${MAX_VERSION} range this encoder supports`);
480
+ }
481
+ if (payload.length > byteCapacity(version, ecc)) {
482
+ throw new MediaFormatError(`${payload.length} bytes do not fit version ${version} at level ${ecc} (capacity ${byteCapacity(version, ecc)})`);
483
+ }
484
+ }
485
+ else {
486
+ const found = smallestVersion(payload.length, ecc);
487
+ if (found === undefined) {
488
+ throw new MediaFormatError(`${payload.length} bytes exceed the ${byteCapacity(MAX_VERSION, ecc)}-byte capacity of version ${MAX_VERSION} at level ${ecc}`);
489
+ }
490
+ version = found;
491
+ }
492
+ const codewords = addEccAndInterleave(encodeDataCodewords(payload, version, ecc), version, ecc);
493
+ const size = 17 + 4 * version;
494
+ const build = (mask) => {
495
+ const matrix = new Matrix(size);
496
+ drawFunctionPatterns(matrix, version);
497
+ drawCodewords(matrix, codewords);
498
+ applyMask(matrix, mask);
499
+ drawFormatBits(matrix, ecc, mask);
500
+ return matrix;
501
+ };
502
+ let chosenMask = options.mask;
503
+ let matrix;
504
+ if (chosenMask === undefined) {
505
+ let best = Number.POSITIVE_INFINITY;
506
+ let bestMask = 0;
507
+ let bestMatrix = build(0);
508
+ for (let mask = 0; mask < 8; mask++) {
509
+ const candidate = mask === 0 ? bestMatrix : build(mask);
510
+ const score = penaltyScore(candidate);
511
+ if (score < best) {
512
+ best = score;
513
+ bestMask = mask;
514
+ bestMatrix = candidate;
515
+ }
516
+ }
517
+ chosenMask = bestMask;
518
+ matrix = bestMatrix;
519
+ }
520
+ else {
521
+ if (chosenMask < 0 || chosenMask > 7) {
522
+ throw new MediaFormatError(`mask ${chosenMask} is not one of the eight defined masks`);
523
+ }
524
+ matrix = build(chosenMask);
525
+ }
526
+ return { version, ecc, mask: chosenMask, size, modules: matrix.modules.map((row) => [...row]) };
527
+ }
528
+ /** Render a symbol as text, one line per row, with a quiet zone. */
529
+ export function symbolToText(symbol, dark, light, quietZone) {
530
+ const lines = [];
531
+ const width = symbol.size + quietZone * 2;
532
+ const blank = light.repeat(width);
533
+ for (let i = 0; i < quietZone; i++)
534
+ lines.push(blank);
535
+ for (const row of symbol.modules) {
536
+ let line = light.repeat(quietZone);
537
+ for (const module of row)
538
+ line += module ? dark : light;
539
+ lines.push(line + light.repeat(quietZone));
540
+ }
541
+ for (let i = 0; i < quietZone; i++)
542
+ lines.push(blank);
543
+ return lines.join("\n");
544
+ }
@@ -0,0 +1,18 @@
1
+ import type { Rgb } from "./color";
2
+ import type { RgbaImage } from "./png";
3
+ /** A solid-colour canvas of `width` x `height`. */
4
+ export declare function canvas(width: number, height: number, fill: Rgb): RgbaImage;
5
+ /** Fill a rectangle, clipped to the canvas. */
6
+ export declare function fillRect(image: RgbaImage, x: number, y: number, width: number, height: number, color: Rgb): void;
7
+ /** Largest module scale that keeps `modules` within `maxPixels` of width. */
8
+ export declare function fitScale(modules: number, quietZone: number, maxPixels: number): number;
9
+ /**
10
+ * Render a boolean module grid: `scale` pixels per module, `quietZone`
11
+ * modules of background on every side.
12
+ */
13
+ export declare function renderModuleGrid(grid: ReadonlyArray<ReadonlyArray<boolean>>, scale: number, quietZone: number, dark: Rgb, light: Rgb): RgbaImage;
14
+ /**
15
+ * Render a one-dimensional module string as vertical bars: `barHeight`
16
+ * pixels tall, `scale` pixels per module, `quietZone` modules either side.
17
+ */
18
+ export declare function renderModuleStrip(modules: string, scale: number, barHeight: number, quietZone: number, dark: Rgb, light: Rgb): RgbaImage;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Turning a module grid into pixels.
3
+ *
4
+ * Only what QR codes and linear barcodes need: filled rectangles on a solid
5
+ * background. There is no text rendering here, and there is none anywhere
6
+ * else in this package either — rasterising a label needs a font, this
7
+ * package ships none, and a chart with invented glyphs would be worse than
8
+ * a chart emitted as SVG for something with fonts to render it.
9
+ */
10
+ import { MediaFormatError } from "./bytes";
11
+ /** A solid-colour canvas of `width` x `height`. */
12
+ export function canvas(width, height, fill) {
13
+ if (width <= 0 || height <= 0)
14
+ throw new MediaFormatError(`cannot make a ${width}x${height} canvas`);
15
+ const data = new Uint8Array(width * height * 4);
16
+ for (let i = 0; i < data.length; i += 4) {
17
+ data[i] = fill.r;
18
+ data[i + 1] = fill.g;
19
+ data[i + 2] = fill.b;
20
+ data[i + 3] = fill.a;
21
+ }
22
+ return { width, height, data };
23
+ }
24
+ /** Fill a rectangle, clipped to the canvas. */
25
+ export function fillRect(image, x, y, width, height, color) {
26
+ const x0 = Math.max(0, x);
27
+ const y0 = Math.max(0, y);
28
+ const x1 = Math.min(image.width, x + width);
29
+ const y1 = Math.min(image.height, y + height);
30
+ for (let row = y0; row < y1; row++) {
31
+ let at = (row * image.width + x0) * 4;
32
+ for (let col = x0; col < x1; col++) {
33
+ image.data[at] = color.r;
34
+ image.data[at + 1] = color.g;
35
+ image.data[at + 2] = color.b;
36
+ image.data[at + 3] = color.a;
37
+ at += 4;
38
+ }
39
+ }
40
+ }
41
+ /** Largest module scale that keeps `modules` within `maxPixels` of width. */
42
+ export function fitScale(modules, quietZone, maxPixels) {
43
+ return Math.max(1, Math.floor(maxPixels / (modules + quietZone * 2)));
44
+ }
45
+ /**
46
+ * Render a boolean module grid: `scale` pixels per module, `quietZone`
47
+ * modules of background on every side.
48
+ */
49
+ export function renderModuleGrid(grid, scale, quietZone, dark, light) {
50
+ const rows = grid.length;
51
+ const cols = (grid[0] ?? []).length;
52
+ if (rows === 0 || cols === 0)
53
+ throw new MediaFormatError("cannot render an empty module grid");
54
+ const width = (cols + quietZone * 2) * scale;
55
+ const height = (rows + quietZone * 2) * scale;
56
+ const image = canvas(width, height, light);
57
+ for (let row = 0; row < rows; row++) {
58
+ const line = grid[row];
59
+ for (let col = 0; col < cols; col++) {
60
+ if (line[col] === true) {
61
+ fillRect(image, (col + quietZone) * scale, (row + quietZone) * scale, scale, scale, dark);
62
+ }
63
+ }
64
+ }
65
+ return image;
66
+ }
67
+ /**
68
+ * Render a one-dimensional module string as vertical bars: `barHeight`
69
+ * pixels tall, `scale` pixels per module, `quietZone` modules either side.
70
+ */
71
+ export function renderModuleStrip(modules, scale, barHeight, quietZone, dark, light) {
72
+ if (modules.length === 0)
73
+ throw new MediaFormatError("cannot render an empty barcode");
74
+ const width = (modules.length + quietZone * 2) * scale;
75
+ const image = canvas(width, barHeight, light);
76
+ for (let i = 0; i < modules.length; i++) {
77
+ if (modules[i] === "1") {
78
+ fillRect(image, (i + quietZone) * scale, 0, scale, barHeight, dark);
79
+ }
80
+ }
81
+ return image;
82
+ }
@@ -0,0 +1,21 @@
1
+ export type SparklineRange = {
2
+ readonly min: number;
3
+ readonly max: number;
4
+ };
5
+ /**
6
+ * The series as block characters, one per value. Values at or below `min`
7
+ * get the shortest block and values at or above `max` the tallest, so a
8
+ * caller supplying an explicit range gets clipping rather than a surprise.
9
+ */
10
+ export declare function sparklineBlocks(values: ReadonlyArray<number>, explicit?: Partial<SparklineRange>): string;
11
+ export type SparklineSvgOptions = {
12
+ readonly width?: number;
13
+ readonly height?: number;
14
+ readonly stroke?: string;
15
+ readonly fill?: string;
16
+ readonly background?: string;
17
+ /** Mark the last point, the way a dashboard sparkline usually does. */
18
+ readonly markLast?: boolean;
19
+ };
20
+ /** The series as a small standalone SVG. */
21
+ export declare function sparklineSvg(values: ReadonlyArray<number>, options?: SparklineSvgOptions, explicit?: Partial<SparklineRange>): string;