qr 0.5.2 → 0.5.4

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/src/index.ts CHANGED
@@ -62,29 +62,36 @@ function fillArr<T>(length: number, val: T): T[] {
62
62
  return new Array(length).fill(val);
63
63
  }
64
64
 
65
+ function popcnt(n: number): number {
66
+ n = n - ((n >>> 1) & 0x55555555);
67
+ n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
68
+ return (((n + (n >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;
69
+ }
70
+
65
71
  /**
66
72
  * Interleaves byte blocks.
67
73
  * @param blocks [[1, 2, 3], [4, 5, 6]]
68
74
  * @returns [1, 4, 2, 5, 3, 6]
69
75
  */
70
- function interleaveBytes(...blocks: Uint8Array[]): Uint8Array {
71
- let len = 0;
72
- for (const b of blocks) len = Math.max(len, b.length);
73
- const res = [];
74
- for (let i = 0; i < len; i++) {
75
- for (const b of blocks) {
76
- if (i >= b.length) continue; // outside of block, skip
77
- res.push(b[i]);
76
+ function interleaveBytes(blocks: Uint8Array[]): Uint8Array {
77
+ let maxLen = 0;
78
+ let totalLen = 0;
79
+ for (const block of blocks) {
80
+ maxLen = Math.max(maxLen, block.length);
81
+ totalLen += block.length;
82
+ }
83
+
84
+ const result = new Uint8Array(totalLen);
85
+ let idx = 0;
86
+ for (let i = 0; i < maxLen; i++) {
87
+ for (const block of blocks) {
88
+ if (i < block.length) result[idx++] = block[i];
78
89
  }
79
90
  }
80
- return new Uint8Array(res);
81
- }
82
91
 
83
- function includesAt<T>(lst: T[], pattern: T[], index: number): boolean {
84
- if (index < 0 || index + pattern.length > lst.length) return false;
85
- for (let i = 0; i < pattern.length; i++) if (pattern[i] !== lst[index + i]) return false;
86
- return true;
92
+ return result;
87
93
  }
94
+
88
95
  // Optimize for minimal score/penalty
89
96
  function best<T>(): {
90
97
  add(score: number, value: T): void;
@@ -134,6 +141,36 @@ function alphabet(
134
141
  };
135
142
  }
136
143
 
144
+ // Transpose 32x32 bit matrix in-place
145
+ // a[0..31] are 32 rows of 32 bits each; after transpose they become 32 columns.
146
+ function transpose32(a: Uint32Array) {
147
+ if (a.length !== 32) throw new Error('expects 32 element matrix');
148
+ const masks = [0x55555555, 0x33333333, 0x0f0f0f0f, 0x00ff00ff, 0x0000ffff] as const;
149
+ // Hello again, FFT
150
+ for (let stage = 0; stage < 5; stage++) {
151
+ const m = masks[stage] >>> 0;
152
+ const s = 1 << stage; // 1,2,4,8,16
153
+ const step = s << 1; // 2,4,8,16,32
154
+ for (let i = 0; i < 32; i += step) {
155
+ for (let k = 0; k < s; k++) {
156
+ const i0 = i + k;
157
+ const i1 = i0 + s;
158
+ const x = a[i0] >>> 0;
159
+ const y = a[i1] >>> 0;
160
+ const t = ((x >>> s) ^ y) & m;
161
+ a[i0] = (x ^ (t << s)) >>> 0;
162
+ a[i1] = (y ^ t) >>> 0;
163
+ }
164
+ }
165
+ }
166
+ }
167
+ const bitMask = (x: number): number => (1 << (x & 31)) >>> 0;
168
+ const rangeMask = (shift: number, len: number): number => {
169
+ // len in [0..32], shift in [0..31]
170
+ if (len === 0) return 0;
171
+ if (len === 32) return 0xffffffff;
172
+ return (((1 << len) - 1) << shift) >>> 0;
173
+ };
137
174
  /*
138
175
  Basic bitmap structure for two colors (black & white) small images.
139
176
  - undefined is used as a marker whether cell was written or not
@@ -177,8 +214,8 @@ export class Bitmap {
177
214
  s = s.replace(/^\n+/g, '').replace(/\n+$/g, '');
178
215
  const lines = s.split(String.fromCharCode(chCodes.newline));
179
216
  const height = lines.length;
180
- const data = new Array(height);
181
217
  let width: number | undefined;
218
+ const rows: DrawValue[][] = [];
182
219
  for (const line of lines) {
183
220
  const row = line.split('').map((i) => {
184
221
  if (i === 'X') return true;
@@ -186,34 +223,53 @@ export class Bitmap {
186
223
  if (i === '?') return undefined;
187
224
  throw new Error(`Bitmap.fromString: unknown symbol=${i}`);
188
225
  });
189
- if (width && row.length !== width)
226
+ if (width !== undefined && row.length !== width)
190
227
  throw new Error(`Bitmap.fromString different row sizes: width=${width} cur=${row.length}`);
191
228
  width = row.length;
192
- data.push(row);
229
+ rows.push(row);
193
230
  }
194
- if (!width) width = 0;
195
- return new Bitmap({ height, width }, data);
231
+ if (width === undefined) width = 0;
232
+ return new Bitmap({ height, width }, rows);
196
233
  }
197
-
198
- data: DrawValue[][];
234
+ // Two bitsets:
235
+ // defined=0 -> undefined
236
+ // defined=1,value=0 -> false
237
+ // defined=1,value=1 -> true
238
+ private defined: Uint32Array;
239
+ private value: Uint32Array;
240
+ private tailMask: number;
241
+ private words: number;
242
+ private fullWords: number;
199
243
  height: number;
200
244
  width: number;
201
245
  constructor(size: Size | number, data?: DrawValue[][]) {
202
246
  const { height, width } = Bitmap.size(size);
203
- this.data = data || Array.from({ length: height }, () => fillArr(width, undefined));
204
247
  this.height = height;
205
248
  this.width = width;
249
+ this.tailMask = rangeMask(0, width & 31 || 32);
250
+ this.words = Math.ceil(width / 32) | 0;
251
+ this.fullWords = Math.floor(width / 32) | 0;
252
+ this.value = new Uint32Array(this.words * height);
253
+ this.defined = new Uint32Array(this.value.length);
254
+ if (data) {
255
+ // accept same semantics as old version
256
+ if (data.length !== height)
257
+ throw new Error(`Bitmap: data height mismatch: exp=${height} got=${data.length}`);
258
+ for (let y = 0; y < height; y++) {
259
+ const row = data[y];
260
+ if (!row || row.length !== width)
261
+ throw new Error(`Bitmap: data width mismatch at y=${y}: exp=${width} got=${row?.length}`);
262
+ for (let x = 0; x < width; x++) this.set(x, y, row[x]);
263
+ }
264
+ }
206
265
  }
207
266
  point(p: Point): DrawValue {
208
- return this.data[p.y][p.x];
267
+ return this.get(p.x, p.y);
209
268
  }
210
269
  isInside(p: Point): boolean {
211
270
  return 0 <= p.x && p.x < this.width && 0 <= p.y && p.y < this.height;
212
271
  }
213
- size(offset?: Point | number): {
214
- height: number;
215
- width: number;
216
- } {
272
+ size(offset?: Point | number): { height: number; width: number } {
217
273
  if (!offset) return { height: this.height, width: this.width };
218
274
  const { x, y } = this.xy(offset);
219
275
  return { height: this.height - y, width: this.width - x };
@@ -227,27 +283,113 @@ export class Bitmap {
227
283
  c.y = mod(c.y, this.height);
228
284
  return c;
229
285
  }
286
+ /**
287
+ * Return pixel bit index
288
+ */
289
+ private wordIndex(x: number, y: number): number {
290
+ return y * this.words + (x >>> 5);
291
+ }
292
+ private bitIndex(x: number, y: number) {
293
+ return { word: this.wordIndex(x, y), bit: x & 31 };
294
+ }
295
+ isDefined(x: number, y: number): boolean {
296
+ const wi = this.wordIndex(x, y);
297
+ const m = bitMask(x);
298
+ return (this.defined[wi] & m) !== 0;
299
+ }
300
+ get(x: number, y: number): boolean {
301
+ const wi = this.wordIndex(x, y);
302
+ const m = bitMask(x);
303
+ return (this.value[wi] & m) !== 0;
304
+ }
305
+ private maskWord(wi: number, mask: number, v: boolean): void {
306
+ const { defined, value } = this;
307
+ defined[wi] |= mask;
308
+ value[wi] = (value[wi] & ~mask) | (-v & mask);
309
+ }
310
+ set(x: number, y: number, v: DrawValue): void {
311
+ if (v === undefined) return;
312
+ this.maskWord(this.wordIndex(x, y), bitMask(x), v);
313
+ }
314
+ // word-span fill for constant values (fast path)
315
+ private fillRectConst(x0: number, y0: number, w: number, h: number, v: DrawValue) {
316
+ if (w <= 0 || h <= 0) return;
317
+ if (v === undefined) return;
318
+ const { value, defined, words } = this;
319
+ const startWord = x0 >>> 5;
320
+ const endWord = (x0 + w - 1) >>> 5;
321
+ const startBit = x0 & 31;
322
+ const endBit = (x0 + w - 1) & 31;
323
+ for (let ry = 0; ry < h; ry++) {
324
+ const rowBase = (y0 + ry) * words;
325
+ if (startWord === endWord) {
326
+ const mask = rangeMask(startBit, endBit - startBit + 1);
327
+ this.maskWord(rowBase + startWord, mask, v);
328
+ continue;
329
+ }
330
+ this.maskWord(rowBase + startWord, rangeMask(startBit, 32 - startBit), v);
331
+ for (let i = startWord + 1; i < endWord; i++) {
332
+ defined[rowBase + i] = 0xffffffff;
333
+ value[rowBase + i] = v ? 0xffffffff : 0;
334
+ }
335
+ this.maskWord(rowBase + endWord, rangeMask(0, endBit + 1), v);
336
+ }
337
+ }
338
+ private rectWords(
339
+ x: number,
340
+ y: number,
341
+ width: number,
342
+ height: number,
343
+ cb: (wi: number, bitX: number, xPos: number, yPos: number, bitsInWord: number) => void
344
+ ): void {
345
+ for (let yPos = 0; yPos < height; yPos++) {
346
+ const Py = y + yPos;
347
+ for (let xPos = 0; xPos < width; ) {
348
+ const bitX = x + xPos;
349
+ const { bit, word } = this.bitIndex(bitX, Py);
350
+ const bitsPerWord = Math.min(32 - bit, width - xPos);
351
+ cb(word, bitX, xPos, yPos, bitsPerWord);
352
+ xPos += bitsPerWord;
353
+ }
354
+ }
355
+ }
230
356
  // Basically every operation can be represented as rect
231
- rect(c: Point | number, size: Size | number, value: DrawFn): this {
357
+ rect(c: Point | number, size: Size | number, fn: DrawFn): this {
232
358
  const { x, y } = this.xy(c);
233
359
  const { height, width } = Bitmap.size(size, this.size({ x, y }));
234
- for (let yPos = 0; yPos < height; yPos++) {
235
- for (let xPos = 0; xPos < width; xPos++) {
236
- // NOTE: we use give function relative coordinates inside box
237
- this.data[y + yPos][x + xPos] =
238
- typeof value === 'function'
239
- ? value({ x: xPos, y: yPos }, this.data[y + yPos][x + xPos])
240
- : value;
241
- }
360
+ if (typeof fn !== 'function') {
361
+ this.fillRectConst(x, y, width, height, fn);
362
+ return this;
242
363
  }
364
+ const { defined, value } = this;
365
+ this.rectWords(x, y, width, height, (wi, bitX, xPos, yPos, n) => {
366
+ let defWord = 0;
367
+ let valWord = value[wi];
368
+ for (let b = 0; b < n; b++) {
369
+ const mask = bitMask(bitX + b);
370
+ const res = fn({ x: xPos + b, y: yPos }, (valWord & mask) !== 0);
371
+ if (res === undefined) continue;
372
+ defWord |= mask;
373
+ valWord = (valWord & ~mask) | (-res & mask);
374
+ }
375
+ defined[wi] |= defWord;
376
+ value[wi] = valWord;
377
+ });
243
378
  return this;
244
379
  }
245
380
  // returns rectangular part of bitmap
246
381
  rectRead(c: Point | number, size: Size | number, fn: ReadFn): this {
247
- return this.rect(c, size, (c, cur) => {
248
- fn(c, cur);
249
- return cur;
382
+ const { x, y } = this.xy(c);
383
+ const { height, width } = Bitmap.size(size, this.size({ x, y }));
384
+ const { value } = this;
385
+ this.rectWords(x, y, width, height, (wi, bitX, xPos, yPos, n) => {
386
+ const valWord = value[wi];
387
+ for (let b = 0; b < n; b++) {
388
+ const mask = bitMask(bitX + b);
389
+ fn({ x: xPos + b, y: yPos }, (valWord & mask) !== 0);
390
+ }
250
391
  });
392
+ return this;
251
393
  }
252
394
  // Horizontal & vertical lines
253
395
  hLine(c: Point | number, len: number, value: DrawFn): this {
@@ -260,25 +402,91 @@ export class Bitmap {
260
402
  border(border = 2, value: DrawValue): Bitmap {
261
403
  const height = this.height + 2 * border;
262
404
  const width = this.width + 2 * border;
263
- const v = fillArr(border, value);
264
- const h: DrawValue[][] = Array.from({ length: border }, () => fillArr(width, value));
265
- return new Bitmap({ height, width }, [...h, ...this.data.map((i) => [...v, ...i, ...v]), ...h]);
405
+ const out = new Bitmap({ height, width });
406
+ // fill everything with border value, then embed original
407
+ out.rect(0, Infinity, value);
408
+ out.embed({ x: border, y: border }, this);
409
+ return out;
266
410
  }
267
411
  // Embed another bitmap on coordinates
268
- embed(c: Point | number, bm: Bitmap): this {
269
- return this.rect(c, bm.size(), ({ x, y }) => bm.data[y][x]);
412
+ embed(c: Point | number, src: Bitmap): this {
413
+ const { x, y } = this.xy(c);
414
+ const { height, width } = Bitmap.size(src.size(), this.size({ x, y }));
415
+ if (width <= 0 || height <= 0) return this;
416
+ const { value, defined } = this;
417
+ const { words: srcStride, value: srcValue } = src;
418
+ for (let yPos = 0; yPos < height; yPos++) {
419
+ const srcRow = yPos * srcStride;
420
+ for (let xPos = 0; xPos < width; ) {
421
+ const dstX = x + xPos;
422
+ const { word: dstWord, bit: dstBit } = this.bitIndex(dstX, y + yPos);
423
+ const { word: srcWord, bit: srcBit } = src.bitIndex(xPos, yPos);
424
+ const len = Math.min(32 - dstBit, width - xPos);
425
+ const w0 = srcValue[srcWord];
426
+ const w1 = srcBit && srcWord + 1 < srcRow + srcStride ? srcValue[srcWord + 1] : 0;
427
+ const sVal = srcBit ? ((w0 >>> srcBit) | (w1 << (32 - srcBit))) >>> 0 : w0;
428
+ const dstMask = rangeMask(dstBit, len);
429
+ const valBits = ((sVal & rangeMask(0, len)) << dstBit) >>> 0;
430
+ defined[dstWord] |= dstMask;
431
+ value[dstWord] = (value[dstWord] & ~dstMask) | valBits;
432
+ xPos += len;
433
+ }
434
+ }
435
+ return this;
270
436
  }
271
437
  // returns rectangular part of bitmap
272
438
  rectSlice(c: Point | number, size: Size | number = this.size()): Bitmap {
273
- const rect = new Bitmap(Bitmap.size(size, this.size(this.xy(c))));
274
- this.rect(c, size, ({ x, y }, cur) => (rect.data[y][x] = cur));
439
+ const { x, y } = this.xy(c);
440
+ const { height, width } = Bitmap.size(size, this.size({ x, y }));
441
+ const rect = new Bitmap({ height, width });
442
+ this.rectRead({ x, y }, { height, width }, (p, cur) => {
443
+ if (this.isDefined(x + p.x, y + p.y)) {
444
+ rect.set(p.x, p.y, cur);
445
+ }
446
+ });
275
447
  return rect;
276
448
  }
277
449
  // Change shape, replace rows with columns (data[y][x] -> data[x][y])
278
- inverse(): Bitmap {
279
- const { height, width } = this;
280
- const res = new Bitmap({ height: width, width: height });
281
- return res.rect({ x: 0, y: 0 }, Infinity, ({ x, y }) => this.data[x][y]);
450
+ transpose(): Bitmap {
451
+ const { height, width, value, defined, words } = this;
452
+ const dst = new Bitmap({ height: width, width: height });
453
+ const { words: dstStride, value: dstValue, defined: dstDefined, tailMask: dstTail } = dst;
454
+ const tmpV = new Uint32Array(32);
455
+ const tmpD = new Uint32Array(32);
456
+ // Process src in blocks: y in [by..by+31], x in 32-bit words
457
+ for (let by = 0; by < height; by += 32) {
458
+ for (let bx = 0; bx < words; bx++) {
459
+ const rows = Math.min(32, height - by);
460
+ for (let r = 0; r < rows; r++) {
461
+ const wi = this.wordIndex(32 * bx, by + r);
462
+ tmpV[r] = value[wi];
463
+ tmpD[r] = defined[wi];
464
+ }
465
+ // zero-pad remainder
466
+ tmpV.fill(0, rows);
467
+ tmpD.fill(0, rows);
468
+ transpose32(tmpV);
469
+ transpose32(tmpD);
470
+ for (let i = 0; i < 32; i++) {
471
+ const dstY = bx * 32 + i;
472
+ if (dstY >= width) break;
473
+ const dstPos = dst.wordIndex(by, dstY);
474
+ const curMask = by >>> 5 === dstStride - 1 ? dstTail : 0xffffffff;
475
+ dstValue[dstPos] = tmpV[i] & curMask;
476
+ dstDefined[dstPos] = tmpD[i] & curMask;
477
+ }
478
+ }
479
+ }
480
+ return dst;
481
+ }
482
+ // black <-> white (inplace)
483
+ negate(): Bitmap {
484
+ const n = this.defined.length;
485
+ for (let i = 0; i < n; i++) {
486
+ this.value[i] = ~this.value[i];
487
+ this.defined[i] = 0xffffffff;
488
+ }
489
+ return this;
282
490
  }
283
491
  // Each pixel size is multiplied by factor
284
492
  scale(factor: number): Bitmap {
@@ -286,37 +494,138 @@ export class Bitmap {
286
494
  throw new Error(`invalid scale factor: ${factor}`);
287
495
  const { height, width } = this;
288
496
  const res = new Bitmap({ height: factor * height, width: factor * width });
289
- return res.rect(
290
- { x: 0, y: 0 },
291
- Infinity,
292
- ({ x, y }) => this.data[Math.floor(y / factor)][Math.floor(x / factor)]
497
+ return res.rect({ x: 0, y: 0 }, Infinity, ({ x, y }) =>
498
+ this.get((x / factor) | 0, (y / factor) | 0)
293
499
  );
294
500
  }
295
501
  clone(): Bitmap {
296
502
  const res = new Bitmap(this.size());
297
- return res.rect({ x: 0, y: 0 }, this.size(), ({ x, y }) => this.data[y][x]);
503
+ res.defined.set(this.defined);
504
+ res.value.set(this.value);
505
+ return res;
298
506
  }
299
507
  // Ensure that there is no undefined values left
300
508
  assertDrawn(): void {
301
- this.rectRead(0, Infinity, (_, cur) => {
302
- if (typeof cur !== 'boolean') throw new Error(`Invalid color type=${typeof cur}`);
303
- });
509
+ const { height, width, defined, tailMask, fullWords, words } = this;
510
+ if (!height || !width) return;
511
+ for (let y = 0; y < height; y++) {
512
+ const rowBase = y * words;
513
+ for (let wi = 0; wi < fullWords; wi++) {
514
+ if (defined[rowBase + wi] !== 0xffffffff) throw new Error(`Invalid color type=undefined`);
515
+ }
516
+ if (words !== fullWords && (defined[rowBase + fullWords] & tailMask) !== tailMask)
517
+ throw new Error(`Invalid color type=undefined`);
518
+ }
304
519
  }
305
- // Simple string representation for debugging
520
+ countPatternInRow(y: number, patternLen: number, ...patterns: number[]): number {
521
+ if (patternLen <= 0 || patternLen >= 32) throw new Error('wrong patternLen');
522
+ const mask = (1 << patternLen) - 1;
523
+ const { width, value, words } = this;
524
+ let count = 0;
525
+ const rowBase = this.wordIndex(0, y);
526
+ for (let i = 0, window = 0; i < words; i++) {
527
+ const w = value[rowBase + i];
528
+ const bitEnd = i === words - 1 ? width & 31 || 32 : 32;
529
+ for (let b = 0; b < bitEnd; b++) {
530
+ window = ((window << 1) | ((w >>> b) & 1)) & mask;
531
+ if (i * 32 + b + 1 < patternLen) continue;
532
+ for (const p of patterns) {
533
+ if (window !== p) continue;
534
+ count++;
535
+ break;
536
+ }
537
+ }
538
+ }
539
+ return count;
540
+ }
541
+ getRuns(y: number, fn: (len: number, value: boolean) => void): void {
542
+ const { width, value, words } = this;
543
+ if (width === 0) return;
544
+ let runLen = 0;
545
+ let runValue: boolean | undefined;
546
+ const rowBase = this.wordIndex(0, y);
547
+ for (let i = 0; i < words; i++) {
548
+ const word = value[rowBase + i];
549
+ const bitEnd = i === words - 1 ? width & 31 || 32 : 32;
550
+ for (let b = 0; b < bitEnd; b++) {
551
+ const bit = (word & (1 << b)) !== 0;
552
+ if (bit === runValue) {
553
+ runLen++;
554
+ continue;
555
+ }
556
+ if (runValue !== undefined) fn(runLen, runValue);
557
+ runValue = bit;
558
+ runLen = 1;
559
+ }
560
+ }
561
+ if (runValue !== undefined) fn(runLen, runValue);
562
+ }
563
+ popcnt(): number {
564
+ const { height, width, words, fullWords, tailMask } = this;
565
+ if (!height || !width) return 0;
566
+ let count = 0;
567
+ for (let y = 0; y < height; y++) {
568
+ const rowBase = y * words;
569
+ for (let wi = 0; wi < fullWords; wi++) count += popcnt(this.value[rowBase + wi]);
570
+ if (words !== fullWords) count += popcnt(this.value[rowBase + fullWords] & tailMask);
571
+ }
572
+ return count;
573
+ }
574
+ countBoxes2x2(y: number): number {
575
+ const { width, words } = this;
576
+ if (width < 2 || (y | 0) < 0 || y + 1 >= this.height) return 0;
577
+ const base0 = this.wordIndex(0, y) | 0;
578
+ const base1 = this.wordIndex(0, y + 1) | 0;
579
+ // valid "left-edge" positions x in [0 .. W-2]
580
+ const tailBits = width & 31;
581
+ const validLast = tailBits === 0 ? 0x7fffffff : rangeMask(0, (width - 1) & 31);
582
+ let boxes = 0;
583
+ for (let wi = 0; wi < words; wi++) {
584
+ const a0 = this.value[base0 + wi];
585
+ const a1 = this.value[base1 + wi];
586
+ // Compare bit x with bit x+1 at same bit position.
587
+ const eqV = ~(a0 ^ a1) >>> 0; // row0[x] == row1[x]
588
+ const n0 = wi + 1 < words ? this.value[base0 + wi + 1] >>> 0 : 0;
589
+ const eqH0 = ~(a0 ^ (((a0 >>> 1) | ((n0 & 1) << 31)) >>> 0)) >>> 0; // row0[x] == row0[x+1]
590
+ const n1 = wi + 1 < words ? this.value[base1 + wi + 1] >>> 0 : 0;
591
+ const eqH1 = ~(a1 ^ (((a1 >>> 1) | ((n1 & 1) << 31)) >>> 0)) >>> 0; // row1[x] == row1[x+1]
592
+ let m = (eqV & eqH0 & eqH1) >>> 0;
593
+ if (wi === words - 1) m &= validLast;
594
+ boxes += popcnt(m);
595
+ }
596
+ return boxes;
597
+ }
598
+ // Export
306
599
  toString(): string {
307
- return this.data
308
- .map((i) => i.map((j) => (j === undefined ? '?' : j ? 'X' : ' ')).join(''))
309
- .join(String.fromCharCode(chCodes.newline));
600
+ const nl = String.fromCharCode(chCodes.newline);
601
+ let out = '';
602
+ for (let y = 0; y < this.height; y++) {
603
+ let line = '';
604
+ for (let x = 0; x < this.width; x++) {
605
+ const v = this.get(x, y);
606
+ line += !this.isDefined(x, y) ? '?' : v ? 'X' : ' ';
607
+ }
608
+ out += line + (y + 1 === this.height ? '' : nl);
609
+ }
610
+ return out;
611
+ }
612
+ toRaw(): DrawValue[][] {
613
+ const out: DrawValue[][] = Array.from({ length: this.height }, () => new Array(this.width));
614
+ for (let y = 0; y < this.height; y++) {
615
+ const row = out[y];
616
+ for (let x = 0; x < this.width; x++) row[x] = this.get(x, y);
617
+ }
618
+ return out;
310
619
  }
311
620
  toASCII(): string {
312
- const { height, width, data } = this;
621
+ const { height, width } = this;
313
622
  let out = '';
314
623
  // Terminal character height is x2 of character width, so we process two rows of bitmap
315
624
  // to produce one row of ASCII
316
625
  for (let y = 0; y < height; y += 2) {
317
626
  for (let x = 0; x < width; x++) {
318
- const first = data[y][x];
319
- const second = y + 1 >= height ? true : data[y + 1][x]; // if last row outside bitmap, make it black
627
+ const first = this.get(x, y);
628
+ const second = y + 1 >= height ? true : this.get(x, y + 1); // if last row outside bitmap, make it black
320
629
  if (!first && !second)
321
630
  out += '█'; // both rows white (empty)
322
631
  else if (!first && second)
@@ -334,9 +643,16 @@ export class Bitmap {
334
643
  const reset = cc + '[0m';
335
644
  const whiteBG = cc + '[1;47m ' + reset;
336
645
  const darkBG = cc + `[40m ` + reset;
337
- return this.data
338
- .map((i) => i.map((j) => (j ? darkBG : whiteBG)).join(''))
339
- .join(String.fromCharCode(chCodes.newline));
646
+ const nl = String.fromCharCode(chCodes.newline);
647
+ let out = '';
648
+ for (let y = 0; y < this.height; y++) {
649
+ for (let x = 0; x < this.width; x++) {
650
+ const v = this.get(x, y); // undefined -> white
651
+ out += v ? darkBG : whiteBG;
652
+ }
653
+ out += nl;
654
+ }
655
+ return out;
340
656
  }
341
657
  toSVG(optimize = true): string {
342
658
  let out = `<svg viewBox="0 0 ${this.width} ${this.height}" xmlns="http://www.w3.org/2000/svg">`;
@@ -407,7 +723,7 @@ export class Bitmap {
407
723
  let i = 0;
408
724
  for (let y = 0; y < height; y++) {
409
725
  for (let x = 0; x < width; x++) {
410
- const value = !!this.data[y][x] ? 0 : 255;
726
+ const value = this.get(x, y) ? 0 : 255; // undefined -> white
411
727
  data[i++] = value;
412
728
  data[i++] = value;
413
729
  data[i++] = value;
@@ -753,8 +1069,8 @@ function interleave(ver: Version, ecc: ErrorCorrection): Coder<Uint8Array, Uint8
753
1069
  eccBlocks.push(rs.encode(bytes.subarray(0, len)));
754
1070
  bytes = bytes.subarray(len);
755
1071
  }
756
- const resBlocks = interleaveBytes(...blocks);
757
- const resECC = interleaveBytes(...eccBlocks);
1072
+ const resBlocks = interleaveBytes(blocks);
1073
+ const resECC = interleaveBytes(eccBlocks);
758
1074
  const res = new Uint8Array(resBlocks.length + resECC.length);
759
1075
  res.set(resBlocks);
760
1076
  res.set(resECC, resBlocks.length);
@@ -814,29 +1130,29 @@ function drawTemplate(
814
1130
  const alignPos = info.alignmentPatterns(ver);
815
1131
  for (const y of alignPos) {
816
1132
  for (const x of alignPos) {
817
- if (b.data[y][x] !== undefined) continue;
1133
+ if (b.isDefined(x, y)) continue;
818
1134
  b.embed({ x: x - 2, y: y - 2 }, align); // center of pattern should be at position
819
1135
  }
820
1136
  }
821
1137
  // Timing patterns
822
1138
  b = b
823
- .hLine({ x: 0, y: 6 }, Infinity, ({ x }, cur) => (cur === undefined ? x % 2 == 0 : cur))
824
- .vLine({ x: 6, y: 0 }, Infinity, ({ y }, cur) => (cur === undefined ? y % 2 == 0 : cur));
1139
+ .hLine({ x: 0, y: 6 }, Infinity, ({ x }) => (b.isDefined(x, 6) ? undefined : x % 2 == 0))
1140
+ .vLine({ x: 6, y: 0 }, Infinity, ({ y }) => (b.isDefined(6, y) ? undefined : y % 2 == 0));
825
1141
  // Format information
826
1142
  {
827
1143
  const bits = info.formatBits(ecc, maskIdx);
828
1144
  const getBit = (i: number) => !test && ((bits >> i) & 1) == 1;
829
1145
  // vertical
830
- for (let i = 0; i < 6; i++) b.data[i][8] = getBit(i); // right of top-left finder
1146
+ for (let i = 0; i < 6; i++) b.set(8, i, getBit(i)); // right of top-left finder
831
1147
  // TODO: re-write as lines, like:
832
1148
  // b.vLine({ x: 8, y: 0 }, 6, ({ x, y }) => getBit(y));
833
- for (let i = 6; i < 8; i++) b.data[i + 1][8] = getBit(i); // after timing pattern
834
- for (let i = 8; i < 15; i++) b.data[size - 15 + i][8] = getBit(i); // right of bottom-left finder
1149
+ for (let i = 6; i < 8; i++) b.set(8, i + 1, getBit(i)); // after timing pattern
1150
+ for (let i = 8; i < 15; i++) b.set(8, size - 15 + i, getBit(i)); // right of bottom-left finder
835
1151
  // horizontal
836
- for (let i = 0; i < 8; i++) b.data[8][size - i - 1] = getBit(i); // under top-right finder
837
- for (let i = 8; i < 9; i++) b.data[8][15 - i - 1 + 1] = getBit(i); // VVV, after timing
838
- for (let i = 9; i < 15; i++) b.data[8][15 - i - 1] = getBit(i); // under top-left finder
839
- b.data[size - 8][8] = !test; // bottom-left finder, right
1152
+ for (let i = 0; i < 8; i++) b.set(size - i - 1, 8, getBit(i)); // under top-right finder
1153
+ for (let i = 8; i < 9; i++) b.set(15 - i - 1 + 1, 8, getBit(i)); // VVV, after timing
1154
+ for (let i = 9; i < 15; i++) b.set(15 - i - 1, 8, getBit(i)); // under top-left finder
1155
+ b.set(8, size - 8, !test); // bottom-left finder, right
840
1156
  }
841
1157
  // Version information
842
1158
  if (ver >= 7) {
@@ -846,8 +1162,8 @@ function drawTemplate(
846
1162
  const x = Math.floor(i / 3);
847
1163
  const y = (i % 3) + size - 8 - 3;
848
1164
  // two copies
849
- b.data[x][y] = bit;
850
- b.data[y][x] = bit;
1165
+ b.set(y, x, bit);
1166
+ b.set(x, y, bit);
851
1167
  }
852
1168
  }
853
1169
  return b;
@@ -869,7 +1185,7 @@ function zigzag(
869
1185
  for (; ; y += dir) {
870
1186
  for (let j = 0; j < 2; j += 1) {
871
1187
  const x = xOffset - j;
872
- if (tpl.data[y][x] !== undefined) continue; // skip already written elements
1188
+ if (tpl.isDefined(x, y)) continue; // skip already written elements
873
1189
  fn(x, y, pattern(x, y));
874
1190
  }
875
1191
  if (y + dir < 0 || y + dir >= size) break;
@@ -901,7 +1217,13 @@ export function utf8ToBytes(str: string): Uint8Array {
901
1217
  return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
902
1218
  }
903
1219
 
904
- function encode(ver: Version, ecc: ErrorCorrection, data: string, type: EncodingType): Uint8Array {
1220
+ function encode(
1221
+ ver: Version,
1222
+ ecc: ErrorCorrection,
1223
+ data: string,
1224
+ type: EncodingType,
1225
+ encoder: (value: string) => Uint8Array = utf8ToBytes
1226
+ ): Uint8Array {
905
1227
  let encoded = '';
906
1228
  let dataLen = data.length;
907
1229
  if (type === 'numeric') {
@@ -919,7 +1241,7 @@ function encode(ver: Version, ecc: ErrorCorrection, data: string, type: Encoding
919
1241
  for (let i = 0; i < n - 1; i += 2) encoded += bin(t[i] * 45 + t[i + 1], 11);
920
1242
  if (n % 2 == 1) encoded += bin(t[n - 1], 6); // pad if odd number of chars
921
1243
  } else if (type === 'byte') {
922
- const utf8 = utf8ToBytes(data);
1244
+ const utf8 = encoder(data);
923
1245
  dataLen = utf8.length;
924
1246
  encoded = Array.from(utf8)
925
1247
  .map((i) => bin(i, 8))
@@ -961,70 +1283,60 @@ function drawQR(
961
1283
  value = ((data[i >>> 3] >> ((7 - i) & 7)) & 1) !== 0;
962
1284
  i++;
963
1285
  }
964
- b.data[y][x] = value !== mask; // !== as xor
1286
+ b.set(x, y, value !== mask); // !== as xor
965
1287
  });
966
1288
  if (i !== need) throw new Error('QR: bytes left after draw');
967
1289
  return b;
968
1290
  }
969
1291
 
1292
+ const mkPattern = (pattern: boolean[]) => {
1293
+ const s = pattern.map((i) => (i ? '1' : '0')).join('');
1294
+ return { len: s.length, n: Number(`0b${s}`) };
1295
+ };
1296
+ // 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, preceded or followed by light area 4 modules wide
1297
+ const finderPattern = [true, false, true, true, true, false, true]; // dark:light:dark:light:dark
1298
+ const lightPattern = [false, false, false, false]; // light area 4 modules wide
1299
+ const P1 = mkPattern([...finderPattern, ...lightPattern]);
1300
+ const P2 = mkPattern([...lightPattern, ...finderPattern]);
1301
+
970
1302
  function penalty(bm: Bitmap): number {
971
- const inverse = bm.inverse();
1303
+ const { width, height } = bm;
1304
+ const transposed = bm.transpose();
972
1305
  // Adjacent modules in row/column in same | No. of modules = (5 + i) color
973
- const sameColor = (row: DrawValue[]) => {
974
- let res = 0;
975
- for (let i = 0, same = 1, last = undefined; i < row.length; i++) {
976
- if (last === row[i]) {
977
- same++;
978
- if (i !== row.length - 1) continue; // handle last element
979
- }
980
- if (same >= 5) res += 3 + (same - 5);
981
- last = row[i];
982
- same = 1;
983
- }
984
- return res;
985
- };
986
1306
  let adjacent = 0;
987
- bm.data.forEach((row) => (adjacent += sameColor(row)));
988
- inverse.data.forEach((column) => (adjacent += sameColor(column)));
1307
+ for (let y = 0; y < height; y++) {
1308
+ bm.getRuns(y, (len) => {
1309
+ if (len >= 5) adjacent += 3 + (len - 5);
1310
+ });
1311
+ }
1312
+ for (let y = 0; y < width; y++) {
1313
+ transposed.getRuns(y, (len) => {
1314
+ if (len >= 5) adjacent += 3 + (len - 5);
1315
+ });
1316
+ }
989
1317
  // Block of modules in same color (Block size = 2x2)
990
1318
  let box = 0;
991
- let b = bm.data;
992
- const lastW = bm.width - 1;
993
- const lastH = bm.height - 1;
994
- for (let x = 0; x < lastW; x++) {
995
- for (let y = 0; y < lastH; y++) {
996
- const x1 = x + 1;
997
- const y1 = y + 1;
998
- if (b[x][y] === b[x1][y] && b[x1][y] === b[x][y1] && b[x1][y] === b[x1][y1]) {
999
- box += 3;
1000
- }
1001
- }
1002
- }
1003
- // 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, preceded or followed by light area 4 modules wide
1004
- const finderPattern = (row: DrawValue[]) => {
1005
- const finderPattern = [true, false, true, true, true, false, true]; // dark:light:dark:light:dark
1006
- const lightPattern = [false, false, false, false]; // light area 4 modules wide
1007
- const p1 = [...finderPattern, ...lightPattern];
1008
- const p2 = [...lightPattern, ...finderPattern];
1009
- let res = 0;
1010
- for (let i = 0; i < row.length; i++) {
1011
- if (includesAt(row, p1, i)) res += 40;
1012
- if (includesAt(row, p2, i)) res += 40;
1013
- }
1014
- return res;
1015
- };
1319
+ for (let y = 0; y < height - 1; y++) box += 3 * bm.countBoxes2x2(y);
1320
+
1016
1321
  let finder = 0;
1017
- for (const row of bm.data) finder += finderPattern(row);
1018
- for (const column of inverse.data) finder += finderPattern(column);
1322
+ for (let y = 0; y < height; y++) finder += 40 * bm.countPatternInRow(y, P1.len, P1.n, P2.n);
1323
+ for (let y = 0; y < width; y++)
1324
+ finder += 40 * transposed.countPatternInRow(y, P1.len, P1.n, P2.n);
1325
+
1019
1326
  // Proportion of dark modules in entire symbol
1020
1327
  // Add 10 points to a deviation of 5% increment or decrement in the proportion
1021
1328
  // ratio of dark module from the referential 50%
1022
1329
  let darkPixels = 0;
1023
- bm.rectRead(0, Infinity, (_c, val) => (darkPixels += val ? 1 : 0));
1024
- const darkPercent = (darkPixels / (bm.height * bm.width)) * 100;
1330
+ darkPixels = bm.popcnt();
1331
+ //bm.rectRead(0, Infinity, (_c, val) => (darkPixels += val ? 1 : 0));
1332
+ // for (let y = 0; y < height; y++) {
1333
+ // for (let x = 0; x < width; x++) if (bm.get(x, y)) darkPixels++;
1334
+ // }
1335
+ const darkPercent = (darkPixels / (height * width)) * 100;
1025
1336
  const dark = 10 * Math.floor(Math.abs(darkPercent - 50) / 5);
1026
1337
  return adjacent + box + finder + dark;
1027
1338
  }
1339
+
1028
1340
  // Selects best mask according to penalty, if no mask is provided
1029
1341
  function drawQRBest(ver: Version, ecc: ErrorCorrection, data: Uint8Array, maskIdx?: Mask) {
1030
1342
  if (maskIdx === undefined) {
@@ -1041,6 +1353,7 @@ function drawQRBest(ver: Version, ecc: ErrorCorrection, data: Uint8Array, maskId
1041
1353
  export type QrOpts = {
1042
1354
  ecc?: ErrorCorrection | undefined;
1043
1355
  encoding?: EncodingType | undefined;
1356
+ textEncoder?: (text: string) => Uint8Array;
1044
1357
  version?: Version | undefined;
1045
1358
  mask?: number | undefined;
1046
1359
  border?: number | undefined;
@@ -1112,13 +1425,13 @@ export function encodeQR(text: string, output: Output = 'raw', opts: QrOpts & Sv
1112
1425
  err = new Error('Unknown error');
1113
1426
  if (ver !== undefined) {
1114
1427
  validateVersion(ver);
1115
- data = encode(ver, ecc, text, encoding);
1428
+ data = encode(ver, ecc, text, encoding, opts.textEncoder);
1116
1429
  } else {
1117
1430
  // If no version is provided, try to find smallest one which fits
1118
1431
  // Currently just scans all version, can be significantly speedup if needed
1119
1432
  for (let i = 1; i <= 40; i++) {
1120
1433
  try {
1121
- data = encode(i, ecc, text, encoding);
1434
+ data = encode(i, ecc, text, encoding, opts.textEncoder);
1122
1435
  ver = i;
1123
1436
  break;
1124
1437
  } catch (e) {
@@ -1133,7 +1446,7 @@ export function encodeQR(text: string, output: Output = 'raw', opts: QrOpts & Sv
1133
1446
  if (!Number.isSafeInteger(border)) throw new Error(`invalid border type=${typeof border}`);
1134
1447
  res = res.border(border, false); // Add border
1135
1448
  if (opts.scale !== undefined) res = res.scale(opts.scale); // Scale image
1136
- if (output === 'raw') return res.data;
1449
+ if (output === 'raw') return res.toRaw();
1137
1450
  else if (output === 'ascii') return res.toASCII();
1138
1451
  else if (output === 'svg') return res.toSVG(opts.optimize);
1139
1452
  else if (output === 'gif') return res.toGIF();
@@ -1146,6 +1459,7 @@ export default encodeQR;
1146
1459
  export const utils: {
1147
1460
  best: typeof best;
1148
1461
  bin: typeof bin;
1462
+ popcnt: typeof popcnt;
1149
1463
  drawTemplate: typeof drawTemplate;
1150
1464
  fillArr: typeof fillArr;
1151
1465
  info: {
@@ -1191,6 +1505,7 @@ export const utils: {
1191
1505
  } = {
1192
1506
  best,
1193
1507
  bin,
1508
+ popcnt,
1194
1509
  drawTemplate,
1195
1510
  fillArr,
1196
1511
  info,