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/index.js CHANGED
@@ -50,32 +50,32 @@ function mod(a, b) {
50
50
  function fillArr(length, val) {
51
51
  return new Array(length).fill(val);
52
52
  }
53
+ function popcnt(n) {
54
+ n = n - ((n >>> 1) & 0x55555555);
55
+ n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);
56
+ return (((n + (n >>> 4)) & 0x0f0f0f0f) * 0x01010101) >>> 24;
57
+ }
53
58
  /**
54
59
  * Interleaves byte blocks.
55
60
  * @param blocks [[1, 2, 3], [4, 5, 6]]
56
61
  * @returns [1, 4, 2, 5, 3, 6]
57
62
  */
58
- function interleaveBytes(...blocks) {
59
- let len = 0;
60
- for (const b of blocks)
61
- len = Math.max(len, b.length);
62
- const res = [];
63
- for (let i = 0; i < len; i++) {
64
- for (const b of blocks) {
65
- if (i >= b.length)
66
- continue; // outside of block, skip
67
- res.push(b[i]);
63
+ function interleaveBytes(blocks) {
64
+ let maxLen = 0;
65
+ let totalLen = 0;
66
+ for (const block of blocks) {
67
+ maxLen = Math.max(maxLen, block.length);
68
+ totalLen += block.length;
69
+ }
70
+ const result = new Uint8Array(totalLen);
71
+ let idx = 0;
72
+ for (let i = 0; i < maxLen; i++) {
73
+ for (const block of blocks) {
74
+ if (i < block.length)
75
+ result[idx++] = block[i];
68
76
  }
69
77
  }
70
- return new Uint8Array(res);
71
- }
72
- function includesAt(lst, pattern, index) {
73
- if (index < 0 || index + pattern.length > lst.length)
74
- return false;
75
- for (let i = 0; i < pattern.length; i++)
76
- if (pattern[i] !== lst[index + i])
77
- return false;
78
- return true;
78
+ return result;
79
79
  }
80
80
  // Optimize for minimal score/penalty
81
81
  function best() {
@@ -120,6 +120,39 @@ function alphabet(alphabet) {
120
120
  },
121
121
  };
122
122
  }
123
+ // Transpose 32x32 bit matrix in-place
124
+ // a[0..31] are 32 rows of 32 bits each; after transpose they become 32 columns.
125
+ function transpose32(a) {
126
+ if (a.length !== 32)
127
+ throw new Error('expects 32 element matrix');
128
+ const masks = [0x55555555, 0x33333333, 0x0f0f0f0f, 0x00ff00ff, 0x0000ffff];
129
+ // Hello again, FFT
130
+ for (let stage = 0; stage < 5; stage++) {
131
+ const m = masks[stage] >>> 0;
132
+ const s = 1 << stage; // 1,2,4,8,16
133
+ const step = s << 1; // 2,4,8,16,32
134
+ for (let i = 0; i < 32; i += step) {
135
+ for (let k = 0; k < s; k++) {
136
+ const i0 = i + k;
137
+ const i1 = i0 + s;
138
+ const x = a[i0] >>> 0;
139
+ const y = a[i1] >>> 0;
140
+ const t = ((x >>> s) ^ y) & m;
141
+ a[i0] = (x ^ (t << s)) >>> 0;
142
+ a[i1] = (y ^ t) >>> 0;
143
+ }
144
+ }
145
+ }
146
+ }
147
+ const bitMask = (x) => (1 << (x & 31)) >>> 0;
148
+ const rangeMask = (shift, len) => {
149
+ // len in [0..32], shift in [0..31]
150
+ if (len === 0)
151
+ return 0;
152
+ if (len === 32)
153
+ return 0xffffffff;
154
+ return (((1 << len) - 1) << shift) >>> 0;
155
+ };
123
156
  export class Bitmap {
124
157
  static size(size, limit) {
125
158
  if (typeof size === 'number')
@@ -142,8 +175,8 @@ export class Bitmap {
142
175
  s = s.replace(/^\n+/g, '').replace(/\n+$/g, '');
143
176
  const lines = s.split(String.fromCharCode(chCodes.newline));
144
177
  const height = lines.length;
145
- const data = new Array(height);
146
178
  let width;
179
+ const rows = [];
147
180
  for (const line of lines) {
148
181
  const row = line.split('').map((i) => {
149
182
  if (i === 'X')
@@ -154,26 +187,50 @@ export class Bitmap {
154
187
  return undefined;
155
188
  throw new Error(`Bitmap.fromString: unknown symbol=${i}`);
156
189
  });
157
- if (width && row.length !== width)
190
+ if (width !== undefined && row.length !== width)
158
191
  throw new Error(`Bitmap.fromString different row sizes: width=${width} cur=${row.length}`);
159
192
  width = row.length;
160
- data.push(row);
193
+ rows.push(row);
161
194
  }
162
- if (!width)
195
+ if (width === undefined)
163
196
  width = 0;
164
- return new Bitmap({ height, width }, data);
197
+ return new Bitmap({ height, width }, rows);
165
198
  }
166
- data;
199
+ // Two bitsets:
200
+ // defined=0 -> undefined
201
+ // defined=1,value=0 -> false
202
+ // defined=1,value=1 -> true
203
+ defined;
204
+ value;
205
+ tailMask;
206
+ words;
207
+ fullWords;
167
208
  height;
168
209
  width;
169
210
  constructor(size, data) {
170
211
  const { height, width } = Bitmap.size(size);
171
- this.data = data || Array.from({ length: height }, () => fillArr(width, undefined));
172
212
  this.height = height;
173
213
  this.width = width;
214
+ this.tailMask = rangeMask(0, width & 31 || 32);
215
+ this.words = Math.ceil(width / 32) | 0;
216
+ this.fullWords = Math.floor(width / 32) | 0;
217
+ this.value = new Uint32Array(this.words * height);
218
+ this.defined = new Uint32Array(this.value.length);
219
+ if (data) {
220
+ // accept same semantics as old version
221
+ if (data.length !== height)
222
+ throw new Error(`Bitmap: data height mismatch: exp=${height} got=${data.length}`);
223
+ for (let y = 0; y < height; y++) {
224
+ const row = data[y];
225
+ if (!row || row.length !== width)
226
+ throw new Error(`Bitmap: data width mismatch at y=${y}: exp=${width} got=${row?.length}`);
227
+ for (let x = 0; x < width; x++)
228
+ this.set(x, y, row[x]);
229
+ }
230
+ }
174
231
  }
175
232
  point(p) {
176
- return this.data[p.y][p.x];
233
+ return this.get(p.x, p.y);
177
234
  }
178
235
  isInside(p) {
179
236
  return 0 <= p.x && p.x < this.width && 0 <= p.y && p.y < this.height;
@@ -196,27 +253,111 @@ export class Bitmap {
196
253
  c.y = mod(c.y, this.height);
197
254
  return c;
198
255
  }
256
+ /**
257
+ * Return pixel bit index
258
+ */
259
+ wordIndex(x, y) {
260
+ return y * this.words + (x >>> 5);
261
+ }
262
+ bitIndex(x, y) {
263
+ return { word: this.wordIndex(x, y), bit: x & 31 };
264
+ }
265
+ isDefined(x, y) {
266
+ const wi = this.wordIndex(x, y);
267
+ const m = bitMask(x);
268
+ return (this.defined[wi] & m) !== 0;
269
+ }
270
+ get(x, y) {
271
+ const wi = this.wordIndex(x, y);
272
+ const m = bitMask(x);
273
+ return (this.value[wi] & m) !== 0;
274
+ }
275
+ maskWord(wi, mask, v) {
276
+ const { defined, value } = this;
277
+ defined[wi] |= mask;
278
+ value[wi] = (value[wi] & ~mask) | (-v & mask);
279
+ }
280
+ set(x, y, v) {
281
+ if (v === undefined)
282
+ return;
283
+ this.maskWord(this.wordIndex(x, y), bitMask(x), v);
284
+ }
285
+ // word-span fill for constant values (fast path)
286
+ fillRectConst(x0, y0, w, h, v) {
287
+ if (w <= 0 || h <= 0)
288
+ return;
289
+ if (v === undefined)
290
+ return;
291
+ const { value, defined, words } = this;
292
+ const startWord = x0 >>> 5;
293
+ const endWord = (x0 + w - 1) >>> 5;
294
+ const startBit = x0 & 31;
295
+ const endBit = (x0 + w - 1) & 31;
296
+ for (let ry = 0; ry < h; ry++) {
297
+ const rowBase = (y0 + ry) * words;
298
+ if (startWord === endWord) {
299
+ const mask = rangeMask(startBit, endBit - startBit + 1);
300
+ this.maskWord(rowBase + startWord, mask, v);
301
+ continue;
302
+ }
303
+ this.maskWord(rowBase + startWord, rangeMask(startBit, 32 - startBit), v);
304
+ for (let i = startWord + 1; i < endWord; i++) {
305
+ defined[rowBase + i] = 0xffffffff;
306
+ value[rowBase + i] = v ? 0xffffffff : 0;
307
+ }
308
+ this.maskWord(rowBase + endWord, rangeMask(0, endBit + 1), v);
309
+ }
310
+ }
311
+ rectWords(x, y, width, height, cb) {
312
+ for (let yPos = 0; yPos < height; yPos++) {
313
+ const Py = y + yPos;
314
+ for (let xPos = 0; xPos < width;) {
315
+ const bitX = x + xPos;
316
+ const { bit, word } = this.bitIndex(bitX, Py);
317
+ const bitsPerWord = Math.min(32 - bit, width - xPos);
318
+ cb(word, bitX, xPos, yPos, bitsPerWord);
319
+ xPos += bitsPerWord;
320
+ }
321
+ }
322
+ }
199
323
  // Basically every operation can be represented as rect
200
- rect(c, size, value) {
324
+ rect(c, size, fn) {
201
325
  const { x, y } = this.xy(c);
202
326
  const { height, width } = Bitmap.size(size, this.size({ x, y }));
203
- for (let yPos = 0; yPos < height; yPos++) {
204
- for (let xPos = 0; xPos < width; xPos++) {
205
- // NOTE: we use give function relative coordinates inside box
206
- this.data[y + yPos][x + xPos] =
207
- typeof value === 'function'
208
- ? value({ x: xPos, y: yPos }, this.data[y + yPos][x + xPos])
209
- : value;
210
- }
327
+ if (typeof fn !== 'function') {
328
+ this.fillRectConst(x, y, width, height, fn);
329
+ return this;
211
330
  }
331
+ const { defined, value } = this;
332
+ this.rectWords(x, y, width, height, (wi, bitX, xPos, yPos, n) => {
333
+ let defWord = 0;
334
+ let valWord = value[wi];
335
+ for (let b = 0; b < n; b++) {
336
+ const mask = bitMask(bitX + b);
337
+ const res = fn({ x: xPos + b, y: yPos }, (valWord & mask) !== 0);
338
+ if (res === undefined)
339
+ continue;
340
+ defWord |= mask;
341
+ valWord = (valWord & ~mask) | (-res & mask);
342
+ }
343
+ defined[wi] |= defWord;
344
+ value[wi] = valWord;
345
+ });
212
346
  return this;
213
347
  }
214
348
  // returns rectangular part of bitmap
215
349
  rectRead(c, size, fn) {
216
- return this.rect(c, size, (c, cur) => {
217
- fn(c, cur);
218
- return cur;
350
+ const { x, y } = this.xy(c);
351
+ const { height, width } = Bitmap.size(size, this.size({ x, y }));
352
+ const { value } = this;
353
+ this.rectWords(x, y, width, height, (wi, bitX, xPos, yPos, n) => {
354
+ const valWord = value[wi];
355
+ for (let b = 0; b < n; b++) {
356
+ const mask = bitMask(bitX + b);
357
+ fn({ x: xPos + b, y: yPos }, (valWord & mask) !== 0);
358
+ }
219
359
  });
360
+ return this;
220
361
  }
221
362
  // Horizontal & vertical lines
222
363
  hLine(c, len, value) {
@@ -229,25 +370,93 @@ export class Bitmap {
229
370
  border(border = 2, value) {
230
371
  const height = this.height + 2 * border;
231
372
  const width = this.width + 2 * border;
232
- const v = fillArr(border, value);
233
- const h = Array.from({ length: border }, () => fillArr(width, value));
234
- return new Bitmap({ height, width }, [...h, ...this.data.map((i) => [...v, ...i, ...v]), ...h]);
373
+ const out = new Bitmap({ height, width });
374
+ // fill everything with border value, then embed original
375
+ out.rect(0, Infinity, value);
376
+ out.embed({ x: border, y: border }, this);
377
+ return out;
235
378
  }
236
379
  // Embed another bitmap on coordinates
237
- embed(c, bm) {
238
- return this.rect(c, bm.size(), ({ x, y }) => bm.data[y][x]);
380
+ embed(c, src) {
381
+ const { x, y } = this.xy(c);
382
+ const { height, width } = Bitmap.size(src.size(), this.size({ x, y }));
383
+ if (width <= 0 || height <= 0)
384
+ return this;
385
+ const { value, defined } = this;
386
+ const { words: srcStride, value: srcValue } = src;
387
+ for (let yPos = 0; yPos < height; yPos++) {
388
+ const srcRow = yPos * srcStride;
389
+ for (let xPos = 0; xPos < width;) {
390
+ const dstX = x + xPos;
391
+ const { word: dstWord, bit: dstBit } = this.bitIndex(dstX, y + yPos);
392
+ const { word: srcWord, bit: srcBit } = src.bitIndex(xPos, yPos);
393
+ const len = Math.min(32 - dstBit, width - xPos);
394
+ const w0 = srcValue[srcWord];
395
+ const w1 = srcBit && srcWord + 1 < srcRow + srcStride ? srcValue[srcWord + 1] : 0;
396
+ const sVal = srcBit ? ((w0 >>> srcBit) | (w1 << (32 - srcBit))) >>> 0 : w0;
397
+ const dstMask = rangeMask(dstBit, len);
398
+ const valBits = ((sVal & rangeMask(0, len)) << dstBit) >>> 0;
399
+ defined[dstWord] |= dstMask;
400
+ value[dstWord] = (value[dstWord] & ~dstMask) | valBits;
401
+ xPos += len;
402
+ }
403
+ }
404
+ return this;
239
405
  }
240
406
  // returns rectangular part of bitmap
241
407
  rectSlice(c, size = this.size()) {
242
- const rect = new Bitmap(Bitmap.size(size, this.size(this.xy(c))));
243
- this.rect(c, size, ({ x, y }, cur) => (rect.data[y][x] = cur));
408
+ const { x, y } = this.xy(c);
409
+ const { height, width } = Bitmap.size(size, this.size({ x, y }));
410
+ const rect = new Bitmap({ height, width });
411
+ this.rectRead({ x, y }, { height, width }, (p, cur) => {
412
+ if (this.isDefined(x + p.x, y + p.y)) {
413
+ rect.set(p.x, p.y, cur);
414
+ }
415
+ });
244
416
  return rect;
245
417
  }
246
418
  // Change shape, replace rows with columns (data[y][x] -> data[x][y])
247
- inverse() {
248
- const { height, width } = this;
249
- const res = new Bitmap({ height: width, width: height });
250
- return res.rect({ x: 0, y: 0 }, Infinity, ({ x, y }) => this.data[x][y]);
419
+ transpose() {
420
+ const { height, width, value, defined, words } = this;
421
+ const dst = new Bitmap({ height: width, width: height });
422
+ const { words: dstStride, value: dstValue, defined: dstDefined, tailMask: dstTail } = dst;
423
+ const tmpV = new Uint32Array(32);
424
+ const tmpD = new Uint32Array(32);
425
+ // Process src in blocks: y in [by..by+31], x in 32-bit words
426
+ for (let by = 0; by < height; by += 32) {
427
+ for (let bx = 0; bx < words; bx++) {
428
+ const rows = Math.min(32, height - by);
429
+ for (let r = 0; r < rows; r++) {
430
+ const wi = this.wordIndex(32 * bx, by + r);
431
+ tmpV[r] = value[wi];
432
+ tmpD[r] = defined[wi];
433
+ }
434
+ // zero-pad remainder
435
+ tmpV.fill(0, rows);
436
+ tmpD.fill(0, rows);
437
+ transpose32(tmpV);
438
+ transpose32(tmpD);
439
+ for (let i = 0; i < 32; i++) {
440
+ const dstY = bx * 32 + i;
441
+ if (dstY >= width)
442
+ break;
443
+ const dstPos = dst.wordIndex(by, dstY);
444
+ const curMask = by >>> 5 === dstStride - 1 ? dstTail : 0xffffffff;
445
+ dstValue[dstPos] = tmpV[i] & curMask;
446
+ dstDefined[dstPos] = tmpD[i] & curMask;
447
+ }
448
+ }
449
+ }
450
+ return dst;
451
+ }
452
+ // black <-> white (inplace)
453
+ negate() {
454
+ const n = this.defined.length;
455
+ for (let i = 0; i < n; i++) {
456
+ this.value[i] = ~this.value[i];
457
+ this.defined[i] = 0xffffffff;
458
+ }
459
+ return this;
251
460
  }
252
461
  // Each pixel size is multiplied by factor
253
462
  scale(factor) {
@@ -255,34 +464,150 @@ export class Bitmap {
255
464
  throw new Error(`invalid scale factor: ${factor}`);
256
465
  const { height, width } = this;
257
466
  const res = new Bitmap({ height: factor * height, width: factor * width });
258
- return res.rect({ x: 0, y: 0 }, Infinity, ({ x, y }) => this.data[Math.floor(y / factor)][Math.floor(x / factor)]);
467
+ return res.rect({ x: 0, y: 0 }, Infinity, ({ x, y }) => this.get((x / factor) | 0, (y / factor) | 0));
259
468
  }
260
469
  clone() {
261
470
  const res = new Bitmap(this.size());
262
- return res.rect({ x: 0, y: 0 }, this.size(), ({ x, y }) => this.data[y][x]);
471
+ res.defined.set(this.defined);
472
+ res.value.set(this.value);
473
+ return res;
263
474
  }
264
475
  // Ensure that there is no undefined values left
265
476
  assertDrawn() {
266
- this.rectRead(0, Infinity, (_, cur) => {
267
- if (typeof cur !== 'boolean')
268
- throw new Error(`Invalid color type=${typeof cur}`);
269
- });
477
+ const { height, width, defined, tailMask, fullWords, words } = this;
478
+ if (!height || !width)
479
+ return;
480
+ for (let y = 0; y < height; y++) {
481
+ const rowBase = y * words;
482
+ for (let wi = 0; wi < fullWords; wi++) {
483
+ if (defined[rowBase + wi] !== 0xffffffff)
484
+ throw new Error(`Invalid color type=undefined`);
485
+ }
486
+ if (words !== fullWords && (defined[rowBase + fullWords] & tailMask) !== tailMask)
487
+ throw new Error(`Invalid color type=undefined`);
488
+ }
270
489
  }
271
- // Simple string representation for debugging
490
+ countPatternInRow(y, patternLen, ...patterns) {
491
+ if (patternLen <= 0 || patternLen >= 32)
492
+ throw new Error('wrong patternLen');
493
+ const mask = (1 << patternLen) - 1;
494
+ const { width, value, words } = this;
495
+ let count = 0;
496
+ const rowBase = this.wordIndex(0, y);
497
+ for (let i = 0, window = 0; i < words; i++) {
498
+ const w = value[rowBase + i];
499
+ const bitEnd = i === words - 1 ? width & 31 || 32 : 32;
500
+ for (let b = 0; b < bitEnd; b++) {
501
+ window = ((window << 1) | ((w >>> b) & 1)) & mask;
502
+ if (i * 32 + b + 1 < patternLen)
503
+ continue;
504
+ for (const p of patterns) {
505
+ if (window !== p)
506
+ continue;
507
+ count++;
508
+ break;
509
+ }
510
+ }
511
+ }
512
+ return count;
513
+ }
514
+ getRuns(y, fn) {
515
+ const { width, value, words } = this;
516
+ if (width === 0)
517
+ return;
518
+ let runLen = 0;
519
+ let runValue;
520
+ const rowBase = this.wordIndex(0, y);
521
+ for (let i = 0; i < words; i++) {
522
+ const word = value[rowBase + i];
523
+ const bitEnd = i === words - 1 ? width & 31 || 32 : 32;
524
+ for (let b = 0; b < bitEnd; b++) {
525
+ const bit = (word & (1 << b)) !== 0;
526
+ if (bit === runValue) {
527
+ runLen++;
528
+ continue;
529
+ }
530
+ if (runValue !== undefined)
531
+ fn(runLen, runValue);
532
+ runValue = bit;
533
+ runLen = 1;
534
+ }
535
+ }
536
+ if (runValue !== undefined)
537
+ fn(runLen, runValue);
538
+ }
539
+ popcnt() {
540
+ const { height, width, words, fullWords, tailMask } = this;
541
+ if (!height || !width)
542
+ return 0;
543
+ let count = 0;
544
+ for (let y = 0; y < height; y++) {
545
+ const rowBase = y * words;
546
+ for (let wi = 0; wi < fullWords; wi++)
547
+ count += popcnt(this.value[rowBase + wi]);
548
+ if (words !== fullWords)
549
+ count += popcnt(this.value[rowBase + fullWords] & tailMask);
550
+ }
551
+ return count;
552
+ }
553
+ countBoxes2x2(y) {
554
+ const { width, words } = this;
555
+ if (width < 2 || (y | 0) < 0 || y + 1 >= this.height)
556
+ return 0;
557
+ const base0 = this.wordIndex(0, y) | 0;
558
+ const base1 = this.wordIndex(0, y + 1) | 0;
559
+ // valid "left-edge" positions x in [0 .. W-2]
560
+ const tailBits = width & 31;
561
+ const validLast = tailBits === 0 ? 0x7fffffff : rangeMask(0, (width - 1) & 31);
562
+ let boxes = 0;
563
+ for (let wi = 0; wi < words; wi++) {
564
+ const a0 = this.value[base0 + wi];
565
+ const a1 = this.value[base1 + wi];
566
+ // Compare bit x with bit x+1 at same bit position.
567
+ const eqV = ~(a0 ^ a1) >>> 0; // row0[x] == row1[x]
568
+ const n0 = wi + 1 < words ? this.value[base0 + wi + 1] >>> 0 : 0;
569
+ const eqH0 = ~(a0 ^ (((a0 >>> 1) | ((n0 & 1) << 31)) >>> 0)) >>> 0; // row0[x] == row0[x+1]
570
+ const n1 = wi + 1 < words ? this.value[base1 + wi + 1] >>> 0 : 0;
571
+ const eqH1 = ~(a1 ^ (((a1 >>> 1) | ((n1 & 1) << 31)) >>> 0)) >>> 0; // row1[x] == row1[x+1]
572
+ let m = (eqV & eqH0 & eqH1) >>> 0;
573
+ if (wi === words - 1)
574
+ m &= validLast;
575
+ boxes += popcnt(m);
576
+ }
577
+ return boxes;
578
+ }
579
+ // Export
272
580
  toString() {
273
- return this.data
274
- .map((i) => i.map((j) => (j === undefined ? '?' : j ? 'X' : ' ')).join(''))
275
- .join(String.fromCharCode(chCodes.newline));
581
+ const nl = String.fromCharCode(chCodes.newline);
582
+ let out = '';
583
+ for (let y = 0; y < this.height; y++) {
584
+ let line = '';
585
+ for (let x = 0; x < this.width; x++) {
586
+ const v = this.get(x, y);
587
+ line += !this.isDefined(x, y) ? '?' : v ? 'X' : ' ';
588
+ }
589
+ out += line + (y + 1 === this.height ? '' : nl);
590
+ }
591
+ return out;
592
+ }
593
+ toRaw() {
594
+ const out = Array.from({ length: this.height }, () => new Array(this.width));
595
+ for (let y = 0; y < this.height; y++) {
596
+ const row = out[y];
597
+ for (let x = 0; x < this.width; x++)
598
+ row[x] = this.get(x, y);
599
+ }
600
+ return out;
276
601
  }
277
602
  toASCII() {
278
- const { height, width, data } = this;
603
+ const { height, width } = this;
279
604
  let out = '';
280
605
  // Terminal character height is x2 of character width, so we process two rows of bitmap
281
606
  // to produce one row of ASCII
282
607
  for (let y = 0; y < height; y += 2) {
283
608
  for (let x = 0; x < width; x++) {
284
- const first = data[y][x];
285
- const second = y + 1 >= height ? true : data[y + 1][x]; // if last row outside bitmap, make it black
609
+ const first = this.get(x, y);
610
+ const second = y + 1 >= height ? true : this.get(x, y + 1); // if last row outside bitmap, make it black
286
611
  if (!first && !second)
287
612
  out += '█'; // both rows white (empty)
288
613
  else if (!first && second)
@@ -301,9 +626,16 @@ export class Bitmap {
301
626
  const reset = cc + '[0m';
302
627
  const whiteBG = cc + '[1;47m ' + reset;
303
628
  const darkBG = cc + `[40m ` + reset;
304
- return this.data
305
- .map((i) => i.map((j) => (j ? darkBG : whiteBG)).join(''))
306
- .join(String.fromCharCode(chCodes.newline));
629
+ const nl = String.fromCharCode(chCodes.newline);
630
+ let out = '';
631
+ for (let y = 0; y < this.height; y++) {
632
+ for (let x = 0; x < this.width; x++) {
633
+ const v = this.get(x, y); // undefined -> white
634
+ out += v ? darkBG : whiteBG;
635
+ }
636
+ out += nl;
637
+ }
638
+ return out;
307
639
  }
308
640
  toSVG(optimize = true) {
309
641
  let out = `<svg viewBox="0 0 ${this.width} ${this.height}" xmlns="http://www.w3.org/2000/svg">`;
@@ -372,7 +704,7 @@ export class Bitmap {
372
704
  let i = 0;
373
705
  for (let y = 0; y < height; y++) {
374
706
  for (let x = 0; x < width; x++) {
375
- const value = !!this.data[y][x] ? 0 : 255;
707
+ const value = this.get(x, y) ? 0 : 255; // undefined -> white
376
708
  data[i++] = value;
377
709
  data[i++] = value;
378
710
  data[i++] = value;
@@ -740,8 +1072,8 @@ function interleave(ver, ecc) {
740
1072
  eccBlocks.push(rs.encode(bytes.subarray(0, len)));
741
1073
  bytes = bytes.subarray(len);
742
1074
  }
743
- const resBlocks = interleaveBytes(...blocks);
744
- const resECC = interleaveBytes(...eccBlocks);
1075
+ const resBlocks = interleaveBytes(blocks);
1076
+ const resECC = interleaveBytes(eccBlocks);
745
1077
  const res = new Uint8Array(resBlocks.length + resECC.length);
746
1078
  res.set(resBlocks);
747
1079
  res.set(resECC, resBlocks.length);
@@ -798,36 +1130,36 @@ function drawTemplate(ver, ecc, maskIdx, test = false) {
798
1130
  const alignPos = info.alignmentPatterns(ver);
799
1131
  for (const y of alignPos) {
800
1132
  for (const x of alignPos) {
801
- if (b.data[y][x] !== undefined)
1133
+ if (b.isDefined(x, y))
802
1134
  continue;
803
1135
  b.embed({ x: x - 2, y: y - 2 }, align); // center of pattern should be at position
804
1136
  }
805
1137
  }
806
1138
  // Timing patterns
807
1139
  b = b
808
- .hLine({ x: 0, y: 6 }, Infinity, ({ x }, cur) => (cur === undefined ? x % 2 == 0 : cur))
809
- .vLine({ x: 6, y: 0 }, Infinity, ({ y }, cur) => (cur === undefined ? y % 2 == 0 : cur));
1140
+ .hLine({ x: 0, y: 6 }, Infinity, ({ x }) => (b.isDefined(x, 6) ? undefined : x % 2 == 0))
1141
+ .vLine({ x: 6, y: 0 }, Infinity, ({ y }) => (b.isDefined(6, y) ? undefined : y % 2 == 0));
810
1142
  // Format information
811
1143
  {
812
1144
  const bits = info.formatBits(ecc, maskIdx);
813
1145
  const getBit = (i) => !test && ((bits >> i) & 1) == 1;
814
1146
  // vertical
815
1147
  for (let i = 0; i < 6; i++)
816
- b.data[i][8] = getBit(i); // right of top-left finder
1148
+ b.set(8, i, getBit(i)); // right of top-left finder
817
1149
  // TODO: re-write as lines, like:
818
1150
  // b.vLine({ x: 8, y: 0 }, 6, ({ x, y }) => getBit(y));
819
1151
  for (let i = 6; i < 8; i++)
820
- b.data[i + 1][8] = getBit(i); // after timing pattern
1152
+ b.set(8, i + 1, getBit(i)); // after timing pattern
821
1153
  for (let i = 8; i < 15; i++)
822
- b.data[size - 15 + i][8] = getBit(i); // right of bottom-left finder
1154
+ b.set(8, size - 15 + i, getBit(i)); // right of bottom-left finder
823
1155
  // horizontal
824
1156
  for (let i = 0; i < 8; i++)
825
- b.data[8][size - i - 1] = getBit(i); // under top-right finder
1157
+ b.set(size - i - 1, 8, getBit(i)); // under top-right finder
826
1158
  for (let i = 8; i < 9; i++)
827
- b.data[8][15 - i - 1 + 1] = getBit(i); // VVV, after timing
1159
+ b.set(15 - i - 1 + 1, 8, getBit(i)); // VVV, after timing
828
1160
  for (let i = 9; i < 15; i++)
829
- b.data[8][15 - i - 1] = getBit(i); // under top-left finder
830
- b.data[size - 8][8] = !test; // bottom-left finder, right
1161
+ b.set(15 - i - 1, 8, getBit(i)); // under top-left finder
1162
+ b.set(8, size - 8, !test); // bottom-left finder, right
831
1163
  }
832
1164
  // Version information
833
1165
  if (ver >= 7) {
@@ -837,8 +1169,8 @@ function drawTemplate(ver, ecc, maskIdx, test = false) {
837
1169
  const x = Math.floor(i / 3);
838
1170
  const y = (i % 3) + size - 8 - 3;
839
1171
  // two copies
840
- b.data[x][y] = bit;
841
- b.data[y][x] = bit;
1172
+ b.set(y, x, bit);
1173
+ b.set(x, y, bit);
842
1174
  }
843
1175
  }
844
1176
  return b;
@@ -857,7 +1189,7 @@ function zigzag(tpl, maskIdx, fn) {
857
1189
  for (;; y += dir) {
858
1190
  for (let j = 0; j < 2; j += 1) {
859
1191
  const x = xOffset - j;
860
- if (tpl.data[y][x] !== undefined)
1192
+ if (tpl.isDefined(x, y))
861
1193
  continue; // skip already written elements
862
1194
  fn(x, y, pattern(x, y));
863
1195
  }
@@ -888,7 +1220,7 @@ export function utf8ToBytes(str) {
888
1220
  throw new Error(`utf8ToBytes expected string, got ${typeof str}`);
889
1221
  return new Uint8Array(new TextEncoder().encode(str)); // https://bugzil.la/1681809
890
1222
  }
891
- function encode(ver, ecc, data, type) {
1223
+ function encode(ver, ecc, data, type, encoder = utf8ToBytes) {
892
1224
  let encoded = '';
893
1225
  let dataLen = data.length;
894
1226
  if (type === 'numeric') {
@@ -912,7 +1244,7 @@ function encode(ver, ecc, data, type) {
912
1244
  encoded += bin(t[n - 1], 6); // pad if odd number of chars
913
1245
  }
914
1246
  else if (type === 'byte') {
915
- const utf8 = utf8ToBytes(data);
1247
+ const utf8 = encoder(data);
916
1248
  dataLen = utf8.length;
917
1249
  encoded = Array.from(utf8)
918
1250
  .map((i) => bin(i, 8))
@@ -950,73 +1282,57 @@ function drawQR(ver, ecc, data, maskIdx, test = false) {
950
1282
  value = ((data[i >>> 3] >> ((7 - i) & 7)) & 1) !== 0;
951
1283
  i++;
952
1284
  }
953
- b.data[y][x] = value !== mask; // !== as xor
1285
+ b.set(x, y, value !== mask); // !== as xor
954
1286
  });
955
1287
  if (i !== need)
956
1288
  throw new Error('QR: bytes left after draw');
957
1289
  return b;
958
1290
  }
1291
+ const mkPattern = (pattern) => {
1292
+ const s = pattern.map((i) => (i ? '1' : '0')).join('');
1293
+ return { len: s.length, n: Number(`0b${s}`) };
1294
+ };
1295
+ // 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, preceded or followed by light area 4 modules wide
1296
+ const finderPattern = [true, false, true, true, true, false, true]; // dark:light:dark:light:dark
1297
+ const lightPattern = [false, false, false, false]; // light area 4 modules wide
1298
+ const P1 = mkPattern([...finderPattern, ...lightPattern]);
1299
+ const P2 = mkPattern([...lightPattern, ...finderPattern]);
959
1300
  function penalty(bm) {
960
- const inverse = bm.inverse();
1301
+ const { width, height } = bm;
1302
+ const transposed = bm.transpose();
961
1303
  // Adjacent modules in row/column in same | No. of modules = (5 + i) color
962
- const sameColor = (row) => {
963
- let res = 0;
964
- for (let i = 0, same = 1, last = undefined; i < row.length; i++) {
965
- if (last === row[i]) {
966
- same++;
967
- if (i !== row.length - 1)
968
- continue; // handle last element
969
- }
970
- if (same >= 5)
971
- res += 3 + (same - 5);
972
- last = row[i];
973
- same = 1;
974
- }
975
- return res;
976
- };
977
1304
  let adjacent = 0;
978
- bm.data.forEach((row) => (adjacent += sameColor(row)));
979
- inverse.data.forEach((column) => (adjacent += sameColor(column)));
1305
+ for (let y = 0; y < height; y++) {
1306
+ bm.getRuns(y, (len) => {
1307
+ if (len >= 5)
1308
+ adjacent += 3 + (len - 5);
1309
+ });
1310
+ }
1311
+ for (let y = 0; y < width; y++) {
1312
+ transposed.getRuns(y, (len) => {
1313
+ if (len >= 5)
1314
+ adjacent += 3 + (len - 5);
1315
+ });
1316
+ }
980
1317
  // Block of modules in same color (Block size = 2x2)
981
1318
  let box = 0;
982
- let b = bm.data;
983
- const lastW = bm.width - 1;
984
- const lastH = bm.height - 1;
985
- for (let x = 0; x < lastW; x++) {
986
- for (let y = 0; y < lastH; y++) {
987
- const x1 = x + 1;
988
- const y1 = y + 1;
989
- if (b[x][y] === b[x1][y] && b[x1][y] === b[x][y1] && b[x1][y] === b[x1][y1]) {
990
- box += 3;
991
- }
992
- }
993
- }
994
- // 1:1:3:1:1 ratio (dark:light:dark:light:dark) pattern in row/column, preceded or followed by light area 4 modules wide
995
- const finderPattern = (row) => {
996
- const finderPattern = [true, false, true, true, true, false, true]; // dark:light:dark:light:dark
997
- const lightPattern = [false, false, false, false]; // light area 4 modules wide
998
- const p1 = [...finderPattern, ...lightPattern];
999
- const p2 = [...lightPattern, ...finderPattern];
1000
- let res = 0;
1001
- for (let i = 0; i < row.length; i++) {
1002
- if (includesAt(row, p1, i))
1003
- res += 40;
1004
- if (includesAt(row, p2, i))
1005
- res += 40;
1006
- }
1007
- return res;
1008
- };
1319
+ for (let y = 0; y < height - 1; y++)
1320
+ box += 3 * bm.countBoxes2x2(y);
1009
1321
  let finder = 0;
1010
- for (const row of bm.data)
1011
- finder += finderPattern(row);
1012
- for (const column of inverse.data)
1013
- finder += finderPattern(column);
1322
+ for (let y = 0; y < height; y++)
1323
+ finder += 40 * bm.countPatternInRow(y, P1.len, P1.n, P2.n);
1324
+ for (let y = 0; y < width; y++)
1325
+ finder += 40 * transposed.countPatternInRow(y, P1.len, P1.n, P2.n);
1014
1326
  // Proportion of dark modules in entire symbol
1015
1327
  // Add 10 points to a deviation of 5% increment or decrement in the proportion
1016
1328
  // ratio of dark module from the referential 50%
1017
1329
  let darkPixels = 0;
1018
- bm.rectRead(0, Infinity, (_c, val) => (darkPixels += val ? 1 : 0));
1019
- 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;
1020
1336
  const dark = 10 * Math.floor(Math.abs(darkPercent - 50) / 5);
1021
1337
  return adjacent + box + finder + dark;
1022
1338
  }
@@ -1057,14 +1373,14 @@ export function encodeQR(text, output = 'raw', opts = {}) {
1057
1373
  let data, err = new Error('Unknown error');
1058
1374
  if (ver !== undefined) {
1059
1375
  validateVersion(ver);
1060
- data = encode(ver, ecc, text, encoding);
1376
+ data = encode(ver, ecc, text, encoding, opts.textEncoder);
1061
1377
  }
1062
1378
  else {
1063
1379
  // If no version is provided, try to find smallest one which fits
1064
1380
  // Currently just scans all version, can be significantly speedup if needed
1065
1381
  for (let i = 1; i <= 40; i++) {
1066
1382
  try {
1067
- data = encode(i, ecc, text, encoding);
1383
+ data = encode(i, ecc, text, encoding, opts.textEncoder);
1068
1384
  ver = i;
1069
1385
  break;
1070
1386
  }
@@ -1084,7 +1400,7 @@ export function encodeQR(text, output = 'raw', opts = {}) {
1084
1400
  if (opts.scale !== undefined)
1085
1401
  res = res.scale(opts.scale); // Scale image
1086
1402
  if (output === 'raw')
1087
- return res.data;
1403
+ return res.toRaw();
1088
1404
  else if (output === 'ascii')
1089
1405
  return res.toASCII();
1090
1406
  else if (output === 'svg')
@@ -1100,6 +1416,7 @@ export default encodeQR;
1100
1416
  export const utils = {
1101
1417
  best,
1102
1418
  bin,
1419
+ popcnt,
1103
1420
  drawTemplate,
1104
1421
  fillArr,
1105
1422
  info,