@sythos/js_barcode_universal 1.5.10 → 1.5.12
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/README.md +35 -3
- package/bundle/sythos-barcode.esm.js +56 -14
- package/bundle/sythos-barcode.js +56 -14
- package/examples/read.html +38 -11
- package/package.json +2 -2
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/js/image/luminance.js +54 -12
- package/src/js/render/options.js +48 -6
- package/src/ts/image/luminance.ts +62 -14
- package/src/ts/index.d.ts +1 -1
- package/src/ts/index.ts +1 -1
- package/src/ts/render/options.ts +63 -6
package/README.md
CHANGED
|
@@ -112,8 +112,8 @@ The `unpkg` and `jsdelivr` fields point at the IIFE bundle, so a CDN needs no in
|
|
|
112
112
|
|
|
113
113
|
```html
|
|
114
114
|
<script src="https://unpkg.com/@sythos/js_barcode_universal"></script>
|
|
115
|
-
<script src="https://unpkg.com/@sythos/js_barcode_universal@1.5.
|
|
116
|
-
<script src="https://cdn.jsdelivr.net/npm/@sythos/js_barcode_universal@1.5.
|
|
115
|
+
<script src="https://unpkg.com/@sythos/js_barcode_universal@1.5.12"></script>
|
|
116
|
+
<script src="https://cdn.jsdelivr.net/npm/@sythos/js_barcode_universal@1.5.12"></script>
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
Pin the version for anything you ship; the unpinned form resolves to `latest` and will move under
|
|
@@ -670,6 +670,29 @@ zero-dependency at runtime.
|
|
|
670
670
|
These security workflows report findings and propose maintenance updates. They do not replace
|
|
671
671
|
review of barcode conformance, licensing, patent status or release attestations.
|
|
672
672
|
|
|
673
|
+
Security reports follow the [security policy](SECURITY.md). Please use GitHub's private reporting
|
|
674
|
+
channel for every suspected vulnerability; public Issues are for non-security bugs after security
|
|
675
|
+
impact has been ruled out. Reports involving code execution, data or secret exposure, CI/release/npm
|
|
676
|
+
integrity or host/runner compromise should also be sent to `devsec@sythos.net`. Exploit details are
|
|
677
|
+
better kept private than turned into an accidental community fireworks show.
|
|
678
|
+
|
|
679
|
+
The image I/O boundary treats camera and file rasters as untrusted input. It validates finite
|
|
680
|
+
positive dimensions, bounds allocations to 16,777,216 pixels — still a high limit, roughly twice
|
|
681
|
+
the pixel count of a standard 4K image — and accepts only byte-valued channels,
|
|
682
|
+
and snapshots greyscale buffers before decoding. Malformed or oversized rasters are rejected
|
|
683
|
+
before detector work begins.
|
|
684
|
+
|
|
685
|
+
Rendering applies the same allocation discipline before creating an SVG, PNG, `ImageData`, canvas,
|
|
686
|
+
WebGL or WebGPU output. Matrix dimensions, `scale`, `margin` and `barHeight` must be safe integers
|
|
687
|
+
within documented bounds, and the final image must be no larger than 16,384 pixels on either side
|
|
688
|
+
or 16,777,216 pixels in total. Invalid, fractional or oversized values are rejected with
|
|
689
|
+
`RangeError`; callers that expose rendering controls should handle that result rather than silently
|
|
690
|
+
coercing it.
|
|
691
|
+
|
|
692
|
+
The browser examples treat decoded payloads and browser error messages as text. They create DOM
|
|
693
|
+
nodes and set `textContent` instead of interpolating those values into HTML, so scanned content is
|
|
694
|
+
not interpreted as markup.
|
|
695
|
+
|
|
673
696
|
---
|
|
674
697
|
|
|
675
698
|
## Build provenance and artifact attestations
|
|
@@ -683,6 +706,15 @@ Attestations are related but separate records:
|
|
|
683
706
|
- **GitHub Artifact Attestations** bind a build artifact, its digest and its build context to the
|
|
684
707
|
GitHub Actions workflow that produced it. They can be verified independently of the npm registry.
|
|
685
708
|
|
|
709
|
+
Every third-party GitHub Action referenced by the repository workflows is pinned to its immutable
|
|
710
|
+
commit SHA. Dependabot tracks the action references, but an update is still reviewed and then
|
|
711
|
+
records a new explicit SHA rather than relying on a movable tag.
|
|
712
|
+
|
|
713
|
+
The development toolchain is also reproducible: `package-lock.json` records the resolved packages,
|
|
714
|
+
TypeScript is declared at an exact version, and the pull-request and release validation workflows
|
|
715
|
+
install the lockfile with `npm ci --ignore-scripts`. The published SDK remains free of runtime
|
|
716
|
+
dependencies.
|
|
717
|
+
|
|
686
718
|
The release-specific attestation workflow is expected at
|
|
687
719
|
[`.github/workflows/release.yml`](.github/workflows/release.yml). Release assets must be verified
|
|
688
720
|
against this repository and their exact local file contents:
|
|
@@ -696,7 +728,7 @@ attestation confirms build provenance; it is not an ISO barcode-conformance cert
|
|
|
696
728
|
clearance, or a guarantee that the implementation is vulnerability-free.
|
|
697
729
|
|
|
698
730
|
Release automation validates the package version against the selected Git tag; it does not invent
|
|
699
|
-
or increment versions by itself. The current release is `1.5.
|
|
731
|
+
or increment versions by itself. The current release is `1.5.12`.
|
|
700
732
|
|
|
701
733
|
---
|
|
702
734
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Sythos Barcode Suite v1.5.
|
|
2
|
+
* Sythos Barcode Suite v1.5.12
|
|
3
3
|
*
|
|
4
4
|
* MIT License
|
|
5
5
|
*
|
|
@@ -312,6 +312,41 @@ __modules["js/image/luminance.js"] = function (__require, __exports) {
|
|
|
312
312
|
* @module image/luminance
|
|
313
313
|
*/
|
|
314
314
|
const { NotFoundError } = __require("js/core/errors.js");
|
|
315
|
+
// Camera and file inputs are untrusted. Keep allocations bounded before any
|
|
316
|
+
// raster is copied into the decoder pipeline.
|
|
317
|
+
const MAX_IMAGE_DIMENSION = 16384;
|
|
318
|
+
const MAX_IMAGE_PIXELS = 16777216;
|
|
319
|
+
function validateDimensions(width, height) {
|
|
320
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)
|
|
321
|
+
|| width < 1 || height < 1
|
|
322
|
+
|| width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) {
|
|
323
|
+
throw new NotFoundError(`Image dimensions must be positive safe integers no larger than ${MAX_IMAGE_DIMENSION}`);
|
|
324
|
+
}
|
|
325
|
+
const pixels = width * height;
|
|
326
|
+
if (pixels > MAX_IMAGE_PIXELS) {
|
|
327
|
+
throw new NotFoundError(`Image contains too many pixels: ${pixels} (maximum ${MAX_IMAGE_PIXELS})`);
|
|
328
|
+
}
|
|
329
|
+
return pixels;
|
|
330
|
+
}
|
|
331
|
+
function isByteArray(data) {
|
|
332
|
+
return data instanceof Uint8Array || data instanceof Uint8ClampedArray;
|
|
333
|
+
}
|
|
334
|
+
function validateByteData(data, expected, label) {
|
|
335
|
+
if (!isByteArray(data) && !Array.isArray(data)) {
|
|
336
|
+
throw new NotFoundError(`${label} must be a Uint8Array, Uint8ClampedArray, or number[]`);
|
|
337
|
+
}
|
|
338
|
+
if (!Number.isSafeInteger(data.length) || data.length < expected) {
|
|
339
|
+
throw new NotFoundError(`${label} is shorter than the required ${expected} bytes`);
|
|
340
|
+
}
|
|
341
|
+
if (Array.isArray(data)) {
|
|
342
|
+
for (let i = 0; i < expected; i++) {
|
|
343
|
+
const value = data[i];
|
|
344
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
345
|
+
throw new NotFoundError(`${label} contains a non-byte value at index ${i}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
315
350
|
/**
|
|
316
351
|
* @typedef {object} ImageLike
|
|
317
352
|
* @property {Uint8ClampedArray | Uint8Array | number[]} data RGBA, 4 bytes per pixel.
|
|
@@ -325,6 +360,10 @@ class LuminanceSource {
|
|
|
325
360
|
* @param {number} height
|
|
326
361
|
*/
|
|
327
362
|
constructor(grey, width, height) {
|
|
363
|
+
const pixels = validateDimensions(width, height);
|
|
364
|
+
if (!isByteArray(grey) || grey.length < pixels) {
|
|
365
|
+
throw new NotFoundError(`Greyscale buffer is shorter than the required ${pixels} bytes`);
|
|
366
|
+
}
|
|
328
367
|
this.grey = grey;
|
|
329
368
|
this.width = width;
|
|
330
369
|
this.height = height;
|
|
@@ -340,15 +379,14 @@ class LuminanceSource {
|
|
|
340
379
|
* @returns {LuminanceSource}
|
|
341
380
|
*/
|
|
342
381
|
static fromImageData(image) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
throw new NotFoundError('Image must be { data, width, height } with RGBA data');
|
|
382
|
+
if (!image || typeof image !== 'object') {
|
|
383
|
+
throw new NotFoundError('Image must be an object with RGBA data');
|
|
346
384
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const grey = new Uint8Array(
|
|
385
|
+
const { data, width, height } = image;
|
|
386
|
+
const pixels = validateDimensions(width, height);
|
|
387
|
+
const expected = pixels * 4;
|
|
388
|
+
validateByteData(data, expected, 'Image data');
|
|
389
|
+
const grey = new Uint8Array(pixels);
|
|
352
390
|
for (let i = 0, p = 0; i < grey.length; i++, p += 4) {
|
|
353
391
|
const a = data[p + 3];
|
|
354
392
|
let r = data[p], g = data[p + 1], b = data[p + 2];
|
|
@@ -373,10 +411,11 @@ class LuminanceSource {
|
|
|
373
411
|
* @returns {LuminanceSource}
|
|
374
412
|
*/
|
|
375
413
|
static fromGrey(grey, width, height) {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
414
|
+
const pixels = validateDimensions(width, height);
|
|
415
|
+
validateByteData(grey, pixels, 'Greyscale buffer');
|
|
416
|
+
// Snapshot caller-owned data so later mutations cannot alter a decode in progress.
|
|
417
|
+
const copy = Uint8Array.from(Array.isArray(grey) ? grey.slice(0, pixels) : grey.subarray(0, pixels));
|
|
418
|
+
return new LuminanceSource(copy, width, height);
|
|
380
419
|
}
|
|
381
420
|
/**
|
|
382
421
|
* @param {number} x @param {number} y
|
|
@@ -391,6 +430,9 @@ class LuminanceSource {
|
|
|
391
430
|
* @returns {Uint8Array}
|
|
392
431
|
*/
|
|
393
432
|
getRow(y, out) {
|
|
433
|
+
if (!Number.isSafeInteger(y) || y < 0 || y >= this.height) {
|
|
434
|
+
throw new RangeError(`LuminanceSource row is outside the image: ${y}`);
|
|
435
|
+
}
|
|
394
436
|
const row = out && out.length >= this.width ? out : new Uint8Array(this.width);
|
|
395
437
|
row.set(this.grey.subarray(y * this.width, (y + 1) * this.width));
|
|
396
438
|
return row;
|
|
@@ -17554,7 +17596,7 @@ function decodeStrict(image, options) {
|
|
|
17554
17596
|
return results[0];
|
|
17555
17597
|
}
|
|
17556
17598
|
/** Library version, matching package.json. */
|
|
17557
|
-
const VERSION = '1.5.
|
|
17599
|
+
const VERSION = '1.5.12';
|
|
17558
17600
|
|
|
17559
17601
|
__exports.listFormats = listFormats;
|
|
17560
17602
|
__exports.encode = encode;
|
package/bundle/sythos-barcode.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Sythos Barcode Suite v1.5.
|
|
2
|
+
* Sythos Barcode Suite v1.5.12
|
|
3
3
|
*
|
|
4
4
|
* MIT License
|
|
5
5
|
*
|
|
@@ -313,6 +313,41 @@ __modules["js/image/luminance.js"] = function (__require, __exports) {
|
|
|
313
313
|
* @module image/luminance
|
|
314
314
|
*/
|
|
315
315
|
const { NotFoundError } = __require("js/core/errors.js");
|
|
316
|
+
// Camera and file inputs are untrusted. Keep allocations bounded before any
|
|
317
|
+
// raster is copied into the decoder pipeline.
|
|
318
|
+
const MAX_IMAGE_DIMENSION = 16384;
|
|
319
|
+
const MAX_IMAGE_PIXELS = 16777216;
|
|
320
|
+
function validateDimensions(width, height) {
|
|
321
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)
|
|
322
|
+
|| width < 1 || height < 1
|
|
323
|
+
|| width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) {
|
|
324
|
+
throw new NotFoundError(`Image dimensions must be positive safe integers no larger than ${MAX_IMAGE_DIMENSION}`);
|
|
325
|
+
}
|
|
326
|
+
const pixels = width * height;
|
|
327
|
+
if (pixels > MAX_IMAGE_PIXELS) {
|
|
328
|
+
throw new NotFoundError(`Image contains too many pixels: ${pixels} (maximum ${MAX_IMAGE_PIXELS})`);
|
|
329
|
+
}
|
|
330
|
+
return pixels;
|
|
331
|
+
}
|
|
332
|
+
function isByteArray(data) {
|
|
333
|
+
return data instanceof Uint8Array || data instanceof Uint8ClampedArray;
|
|
334
|
+
}
|
|
335
|
+
function validateByteData(data, expected, label) {
|
|
336
|
+
if (!isByteArray(data) && !Array.isArray(data)) {
|
|
337
|
+
throw new NotFoundError(`${label} must be a Uint8Array, Uint8ClampedArray, or number[]`);
|
|
338
|
+
}
|
|
339
|
+
if (!Number.isSafeInteger(data.length) || data.length < expected) {
|
|
340
|
+
throw new NotFoundError(`${label} is shorter than the required ${expected} bytes`);
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(data)) {
|
|
343
|
+
for (let i = 0; i < expected; i++) {
|
|
344
|
+
const value = data[i];
|
|
345
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
346
|
+
throw new NotFoundError(`${label} contains a non-byte value at index ${i}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
316
351
|
/**
|
|
317
352
|
* @typedef {object} ImageLike
|
|
318
353
|
* @property {Uint8ClampedArray | Uint8Array | number[]} data RGBA, 4 bytes per pixel.
|
|
@@ -326,6 +361,10 @@ class LuminanceSource {
|
|
|
326
361
|
* @param {number} height
|
|
327
362
|
*/
|
|
328
363
|
constructor(grey, width, height) {
|
|
364
|
+
const pixels = validateDimensions(width, height);
|
|
365
|
+
if (!isByteArray(grey) || grey.length < pixels) {
|
|
366
|
+
throw new NotFoundError(`Greyscale buffer is shorter than the required ${pixels} bytes`);
|
|
367
|
+
}
|
|
329
368
|
this.grey = grey;
|
|
330
369
|
this.width = width;
|
|
331
370
|
this.height = height;
|
|
@@ -341,15 +380,14 @@ class LuminanceSource {
|
|
|
341
380
|
* @returns {LuminanceSource}
|
|
342
381
|
*/
|
|
343
382
|
static fromImageData(image) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
throw new NotFoundError('Image must be { data, width, height } with RGBA data');
|
|
383
|
+
if (!image || typeof image !== 'object') {
|
|
384
|
+
throw new NotFoundError('Image must be an object with RGBA data');
|
|
347
385
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const grey = new Uint8Array(
|
|
386
|
+
const { data, width, height } = image;
|
|
387
|
+
const pixels = validateDimensions(width, height);
|
|
388
|
+
const expected = pixels * 4;
|
|
389
|
+
validateByteData(data, expected, 'Image data');
|
|
390
|
+
const grey = new Uint8Array(pixels);
|
|
353
391
|
for (let i = 0, p = 0; i < grey.length; i++, p += 4) {
|
|
354
392
|
const a = data[p + 3];
|
|
355
393
|
let r = data[p], g = data[p + 1], b = data[p + 2];
|
|
@@ -374,10 +412,11 @@ class LuminanceSource {
|
|
|
374
412
|
* @returns {LuminanceSource}
|
|
375
413
|
*/
|
|
376
414
|
static fromGrey(grey, width, height) {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
415
|
+
const pixels = validateDimensions(width, height);
|
|
416
|
+
validateByteData(grey, pixels, 'Greyscale buffer');
|
|
417
|
+
// Snapshot caller-owned data so later mutations cannot alter a decode in progress.
|
|
418
|
+
const copy = Uint8Array.from(Array.isArray(grey) ? grey.slice(0, pixels) : grey.subarray(0, pixels));
|
|
419
|
+
return new LuminanceSource(copy, width, height);
|
|
381
420
|
}
|
|
382
421
|
/**
|
|
383
422
|
* @param {number} x @param {number} y
|
|
@@ -392,6 +431,9 @@ class LuminanceSource {
|
|
|
392
431
|
* @returns {Uint8Array}
|
|
393
432
|
*/
|
|
394
433
|
getRow(y, out) {
|
|
434
|
+
if (!Number.isSafeInteger(y) || y < 0 || y >= this.height) {
|
|
435
|
+
throw new RangeError(`LuminanceSource row is outside the image: ${y}`);
|
|
436
|
+
}
|
|
395
437
|
const row = out && out.length >= this.width ? out : new Uint8Array(this.width);
|
|
396
438
|
row.set(this.grey.subarray(y * this.width, (y + 1) * this.width));
|
|
397
439
|
return row;
|
|
@@ -17555,7 +17597,7 @@ function decodeStrict(image, options) {
|
|
|
17555
17597
|
return results[0];
|
|
17556
17598
|
}
|
|
17557
17599
|
/** Library version, matching package.json. */
|
|
17558
|
-
const VERSION = '1.5.
|
|
17600
|
+
const VERSION = '1.5.12';
|
|
17559
17601
|
|
|
17560
17602
|
__exports.listFormats = listFormats;
|
|
17561
17603
|
__exports.encode = encode;
|
package/examples/read.html
CHANGED
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
|
|
150
150
|
function show(hits, source) {
|
|
151
151
|
if (!hits.length) return false;
|
|
152
|
-
results.
|
|
152
|
+
results.textContent = '';
|
|
153
153
|
hits.forEach(function (h) {
|
|
154
154
|
var div = document.createElement('div');
|
|
155
155
|
div.className = 'hit';
|
|
@@ -243,7 +243,11 @@
|
|
|
243
243
|
el('fileStatus').textContent =
|
|
244
244
|
w + '×' + h + ' · ' + ms.toFixed(0) + ' ms · ' + hits.length + ' found';
|
|
245
245
|
if (!show(hits, 'image')) {
|
|
246
|
-
results.
|
|
246
|
+
results.textContent = '';
|
|
247
|
+
var status = document.createElement('div');
|
|
248
|
+
status.className = 'status';
|
|
249
|
+
status.textContent = 'No barcode found in that image.';
|
|
250
|
+
results.appendChild(status);
|
|
247
251
|
}
|
|
248
252
|
};
|
|
249
253
|
img.onerror = function () {
|
|
@@ -259,20 +263,44 @@
|
|
|
259
263
|
var stream = null;
|
|
260
264
|
var raf = null;
|
|
261
265
|
|
|
266
|
+
function setCameraNoteNode(note) {
|
|
267
|
+
var container = el('camNote');
|
|
268
|
+
container.textContent = '';
|
|
269
|
+
container.appendChild(note);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function setCameraNote(text) {
|
|
273
|
+
var note = document.createElement('div');
|
|
274
|
+
note.className = 'note';
|
|
275
|
+
note.textContent = text;
|
|
276
|
+
setCameraNoteNode(note);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function appendNoteCode(note, text) {
|
|
280
|
+
var code = document.createElement('code');
|
|
281
|
+
code.textContent = text;
|
|
282
|
+
note.appendChild(code);
|
|
283
|
+
}
|
|
284
|
+
|
|
262
285
|
var secure = window.isSecureContext ||
|
|
263
286
|
location.protocol === 'https:' ||
|
|
264
287
|
location.hostname === 'localhost';
|
|
265
288
|
|
|
266
289
|
if (!secure) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
'
|
|
271
|
-
|
|
290
|
+
var secureNote = document.createElement('div');
|
|
291
|
+
secureNote.className = 'note';
|
|
292
|
+
secureNote.appendChild(document.createTextNode(
|
|
293
|
+
'The camera needs a secure context, so it is unavailable when this page is opened from '));
|
|
294
|
+
appendNoteCode(secureNote, 'file://');
|
|
295
|
+
secureNote.appendChild(document.createTextNode(
|
|
296
|
+
'. Serve the folder over http and reload — for example '));
|
|
297
|
+
appendNoteCode(secureNote, 'npx serve');
|
|
298
|
+
secureNote.appendChild(document.createTextNode(
|
|
299
|
+
' — or use the image input above, which works anywhere.'));
|
|
300
|
+
setCameraNoteNode(secureNote);
|
|
272
301
|
el('camStart').disabled = true;
|
|
273
302
|
} else if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
274
|
-
|
|
275
|
-
'<div class="note">This browser does not expose a camera API.</div>';
|
|
303
|
+
setCameraNote('This browser does not expose a camera API.');
|
|
276
304
|
el('camStart').disabled = true;
|
|
277
305
|
}
|
|
278
306
|
|
|
@@ -292,8 +320,7 @@
|
|
|
292
320
|
}).then(function () {
|
|
293
321
|
loop();
|
|
294
322
|
}).catch(function (e) {
|
|
295
|
-
|
|
296
|
-
'<div class="note">Camera unavailable: ' + String(e.message || e) + '</div>';
|
|
323
|
+
setCameraNote('Camera unavailable: ' + String(e.message || e));
|
|
297
324
|
});
|
|
298
325
|
});
|
|
299
326
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sythos/js_barcode_universal",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.12",
|
|
4
4
|
"description": "Read and write barcodes in JavaScript and TypeScript with zero runtime dependencies. TypeScript sources compile to the JavaScript runtime and bundles. QR Code, Micro QR, rMQR, FrameQR Code, Aztec Code and Rune, PDF417 variants, GS1 DataBar Omnidirectional/Truncated, EAN supplements and one-dimensional formats.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Sythos",
|
|
@@ -191,6 +191,6 @@
|
|
|
191
191
|
"types:api": "node tools/check-public-types.mjs"
|
|
192
192
|
},
|
|
193
193
|
"devDependencies": {
|
|
194
|
-
"typescript": "
|
|
194
|
+
"typescript": "7.0.2"
|
|
195
195
|
}
|
|
196
196
|
}
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -39,6 +39,41 @@
|
|
|
39
39
|
* @module image/luminance
|
|
40
40
|
*/
|
|
41
41
|
import { NotFoundError } from '../core/errors.js';
|
|
42
|
+
// Camera and file inputs are untrusted. Keep allocations bounded before any
|
|
43
|
+
// raster is copied into the decoder pipeline.
|
|
44
|
+
const MAX_IMAGE_DIMENSION = 16384;
|
|
45
|
+
const MAX_IMAGE_PIXELS = 16777216;
|
|
46
|
+
function validateDimensions(width, height) {
|
|
47
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)
|
|
48
|
+
|| width < 1 || height < 1
|
|
49
|
+
|| width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) {
|
|
50
|
+
throw new NotFoundError(`Image dimensions must be positive safe integers no larger than ${MAX_IMAGE_DIMENSION}`);
|
|
51
|
+
}
|
|
52
|
+
const pixels = width * height;
|
|
53
|
+
if (pixels > MAX_IMAGE_PIXELS) {
|
|
54
|
+
throw new NotFoundError(`Image contains too many pixels: ${pixels} (maximum ${MAX_IMAGE_PIXELS})`);
|
|
55
|
+
}
|
|
56
|
+
return pixels;
|
|
57
|
+
}
|
|
58
|
+
function isByteArray(data) {
|
|
59
|
+
return data instanceof Uint8Array || data instanceof Uint8ClampedArray;
|
|
60
|
+
}
|
|
61
|
+
function validateByteData(data, expected, label) {
|
|
62
|
+
if (!isByteArray(data) && !Array.isArray(data)) {
|
|
63
|
+
throw new NotFoundError(`${label} must be a Uint8Array, Uint8ClampedArray, or number[]`);
|
|
64
|
+
}
|
|
65
|
+
if (!Number.isSafeInteger(data.length) || data.length < expected) {
|
|
66
|
+
throw new NotFoundError(`${label} is shorter than the required ${expected} bytes`);
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(data)) {
|
|
69
|
+
for (let i = 0; i < expected; i++) {
|
|
70
|
+
const value = data[i];
|
|
71
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
72
|
+
throw new NotFoundError(`${label} contains a non-byte value at index ${i}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
42
77
|
/**
|
|
43
78
|
* @typedef {object} ImageLike
|
|
44
79
|
* @property {Uint8ClampedArray | Uint8Array | number[]} data RGBA, 4 bytes per pixel.
|
|
@@ -52,6 +87,10 @@ export class LuminanceSource {
|
|
|
52
87
|
* @param {number} height
|
|
53
88
|
*/
|
|
54
89
|
constructor(grey, width, height) {
|
|
90
|
+
const pixels = validateDimensions(width, height);
|
|
91
|
+
if (!isByteArray(grey) || grey.length < pixels) {
|
|
92
|
+
throw new NotFoundError(`Greyscale buffer is shorter than the required ${pixels} bytes`);
|
|
93
|
+
}
|
|
55
94
|
this.grey = grey;
|
|
56
95
|
this.width = width;
|
|
57
96
|
this.height = height;
|
|
@@ -67,15 +106,14 @@ export class LuminanceSource {
|
|
|
67
106
|
* @returns {LuminanceSource}
|
|
68
107
|
*/
|
|
69
108
|
static fromImageData(image) {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw new NotFoundError('Image must be { data, width, height } with RGBA data');
|
|
73
|
-
}
|
|
74
|
-
if (data.length < width * height * 4) {
|
|
75
|
-
throw new NotFoundError(`Image data too short: ${data.length} bytes for ${width}x${height} RGBA ` +
|
|
76
|
-
`(expected ${width * height * 4})`);
|
|
109
|
+
if (!image || typeof image !== 'object') {
|
|
110
|
+
throw new NotFoundError('Image must be an object with RGBA data');
|
|
77
111
|
}
|
|
78
|
-
const
|
|
112
|
+
const { data, width, height } = image;
|
|
113
|
+
const pixels = validateDimensions(width, height);
|
|
114
|
+
const expected = pixels * 4;
|
|
115
|
+
validateByteData(data, expected, 'Image data');
|
|
116
|
+
const grey = new Uint8Array(pixels);
|
|
79
117
|
for (let i = 0, p = 0; i < grey.length; i++, p += 4) {
|
|
80
118
|
const a = data[p + 3];
|
|
81
119
|
let r = data[p], g = data[p + 1], b = data[p + 2];
|
|
@@ -100,10 +138,11 @@ export class LuminanceSource {
|
|
|
100
138
|
* @returns {LuminanceSource}
|
|
101
139
|
*/
|
|
102
140
|
static fromGrey(grey, width, height) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
141
|
+
const pixels = validateDimensions(width, height);
|
|
142
|
+
validateByteData(grey, pixels, 'Greyscale buffer');
|
|
143
|
+
// Snapshot caller-owned data so later mutations cannot alter a decode in progress.
|
|
144
|
+
const copy = Uint8Array.from(Array.isArray(grey) ? grey.slice(0, pixels) : grey.subarray(0, pixels));
|
|
145
|
+
return new LuminanceSource(copy, width, height);
|
|
107
146
|
}
|
|
108
147
|
/**
|
|
109
148
|
* @param {number} x @param {number} y
|
|
@@ -118,6 +157,9 @@ export class LuminanceSource {
|
|
|
118
157
|
* @returns {Uint8Array}
|
|
119
158
|
*/
|
|
120
159
|
getRow(y, out) {
|
|
160
|
+
if (!Number.isSafeInteger(y) || y < 0 || y >= this.height) {
|
|
161
|
+
throw new RangeError(`LuminanceSource row is outside the image: ${y}`);
|
|
162
|
+
}
|
|
121
163
|
const row = out && out.length >= this.width ? out : new Uint8Array(this.width);
|
|
122
164
|
row.set(this.grey.subarray(y * this.width, (y + 1) * this.width));
|
|
123
165
|
return row;
|
package/src/js/render/options.js
CHANGED
|
@@ -41,6 +41,40 @@ import { BitMatrix } from '../core/bit-matrix.js';
|
|
|
41
41
|
* @property {string} [light] Colour of clear modules, or 'none' for transparent.
|
|
42
42
|
* @property {number} [barHeight] For 1D symbols: total bar height in pixels.
|
|
43
43
|
*/
|
|
44
|
+
// Keep renderer allocations aligned with the image decoder's resource limits.
|
|
45
|
+
const MAX_RENDER_DIMENSION = 16384;
|
|
46
|
+
const MAX_RENDER_PIXELS = 16777216;
|
|
47
|
+
const MAX_RENDER_SCALE = MAX_RENDER_DIMENSION;
|
|
48
|
+
const MAX_RENDER_MARGIN = Math.floor((MAX_RENDER_DIMENSION - 1) / 2);
|
|
49
|
+
const MAX_RENDER_BAR_HEIGHT = MAX_RENDER_DIMENSION;
|
|
50
|
+
function boundedInteger(value, name, defaultValue, minimum, maximum) {
|
|
51
|
+
const resolved = value ?? defaultValue;
|
|
52
|
+
if (!Number.isSafeInteger(resolved)) {
|
|
53
|
+
throw new RangeError(`Render option "${name}" must be a finite safe integer between ${minimum} and ${maximum}, got ${resolved}`);
|
|
54
|
+
}
|
|
55
|
+
if (resolved < minimum || resolved > maximum) {
|
|
56
|
+
throw new RangeError(`Render option "${name}" must be between ${minimum} and ${maximum}, got ${resolved}`);
|
|
57
|
+
}
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
60
|
+
function validateMatrixDimensions(matrix) {
|
|
61
|
+
if (!matrix || !Number.isSafeInteger(matrix.width) || !Number.isSafeInteger(matrix.height)
|
|
62
|
+
|| matrix.width < 1 || matrix.height < 1
|
|
63
|
+
|| matrix.width > MAX_RENDER_DIMENSION || matrix.height > MAX_RENDER_DIMENSION) {
|
|
64
|
+
throw new RangeError(`Render matrix dimensions must be positive safe integers no larger than ${MAX_RENDER_DIMENSION}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function validatePixelDimensions(width, height) {
|
|
68
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)
|
|
69
|
+
|| width < 1 || height < 1
|
|
70
|
+
|| width > MAX_RENDER_DIMENSION || height > MAX_RENDER_DIMENSION) {
|
|
71
|
+
throw new RangeError(`Render dimensions must be positive safe integers no larger than ${MAX_RENDER_DIMENSION}`);
|
|
72
|
+
}
|
|
73
|
+
const pixels = width * height;
|
|
74
|
+
if (!Number.isSafeInteger(pixels) || pixels > MAX_RENDER_PIXELS) {
|
|
75
|
+
throw new RangeError(`Render image contains too many pixels: ${pixels} (maximum ${MAX_RENDER_PIXELS})`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
44
78
|
/**
|
|
45
79
|
* Expand and pad the matrix, and resolve every dimension.
|
|
46
80
|
*
|
|
@@ -53,18 +87,26 @@ import { BitMatrix } from '../core/bit-matrix.js';
|
|
|
53
87
|
* @param {RenderOptions} options
|
|
54
88
|
*/
|
|
55
89
|
export function normalizeOptions(matrix, options = {}) {
|
|
56
|
-
|
|
57
|
-
const
|
|
90
|
+
validateMatrixDimensions(matrix);
|
|
91
|
+
const scale = boundedInteger(options.scale, 'scale', 8, 1, MAX_RENDER_SCALE);
|
|
92
|
+
const margin = boundedInteger(options.margin, 'margin', 4, 0, MAX_RENDER_MARGIN);
|
|
58
93
|
const dark = options.dark ?? '#000000';
|
|
59
94
|
const light = options.light ?? '#ffffff';
|
|
60
|
-
const barHeight = options.barHeight
|
|
61
|
-
|
|
95
|
+
const barHeight = options.barHeight == null
|
|
96
|
+
? null
|
|
97
|
+
: boundedInteger(options.barHeight, 'barHeight', 1, 1, MAX_RENDER_BAR_HEIGHT);
|
|
62
98
|
const is1D = matrix.height === 1;
|
|
99
|
+
const targetPixels = is1D
|
|
100
|
+
? barHeight ?? Math.max(40, Math.round(matrix.width * scale * 0.15))
|
|
101
|
+
: null;
|
|
102
|
+
const rows = is1D ? Math.max(1, Math.round(targetPixels / scale)) : matrix.height;
|
|
103
|
+
const sourceWidth = matrix.width + margin * 2;
|
|
104
|
+
const sourceHeight = rows + margin * 2;
|
|
105
|
+
validatePixelDimensions(sourceWidth * scale, sourceHeight * scale);
|
|
106
|
+
let base = matrix;
|
|
63
107
|
if (is1D) {
|
|
64
108
|
// Default to a bar height that stays scannable: tall enough that a laser
|
|
65
109
|
// crossing at a slight angle still passes through the whole symbol.
|
|
66
|
-
const targetPixels = barHeight ?? Math.max(40, Math.round(matrix.width * scale * 0.15));
|
|
67
|
-
const rows = Math.max(1, Math.round(targetPixels / scale));
|
|
68
110
|
base = new BitMatrix(matrix.width, rows);
|
|
69
111
|
for (let x = 0; x < matrix.width; x++) {
|
|
70
112
|
if (!matrix.get(x, 0))
|
|
@@ -42,6 +42,49 @@
|
|
|
42
42
|
|
|
43
43
|
import { NotFoundError } from '../core/errors.js';
|
|
44
44
|
|
|
45
|
+
// Camera and file inputs are untrusted. Keep allocations bounded before any
|
|
46
|
+
// raster is copied into the decoder pipeline.
|
|
47
|
+
const MAX_IMAGE_DIMENSION = 16_384;
|
|
48
|
+
const MAX_IMAGE_PIXELS = 16_777_216;
|
|
49
|
+
|
|
50
|
+
function validateDimensions(width, height) {
|
|
51
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)
|
|
52
|
+
|| width < 1 || height < 1
|
|
53
|
+
|| width > MAX_IMAGE_DIMENSION || height > MAX_IMAGE_DIMENSION) {
|
|
54
|
+
throw new NotFoundError(
|
|
55
|
+
`Image dimensions must be positive safe integers no larger than ${MAX_IMAGE_DIMENSION}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const pixels = width * height;
|
|
59
|
+
if (pixels > MAX_IMAGE_PIXELS) {
|
|
60
|
+
throw new NotFoundError(
|
|
61
|
+
`Image contains too many pixels: ${pixels} (maximum ${MAX_IMAGE_PIXELS})`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return pixels;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isByteArray(data) {
|
|
68
|
+
return data instanceof Uint8Array || data instanceof Uint8ClampedArray;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validateByteData(data, expected, label) {
|
|
72
|
+
if (!isByteArray(data) && !Array.isArray(data)) {
|
|
73
|
+
throw new NotFoundError(`${label} must be a Uint8Array, Uint8ClampedArray, or number[]`);
|
|
74
|
+
}
|
|
75
|
+
if (!Number.isSafeInteger(data.length) || data.length < expected) {
|
|
76
|
+
throw new NotFoundError(`${label} is shorter than the required ${expected} bytes`);
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(data)) {
|
|
79
|
+
for (let i = 0; i < expected; i++) {
|
|
80
|
+
const value = data[i];
|
|
81
|
+
if (!Number.isInteger(value) || value < 0 || value > 255) {
|
|
82
|
+
throw new NotFoundError(`${label} contains a non-byte value at index ${i}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
45
88
|
/**
|
|
46
89
|
* @typedef {object} ImageLike
|
|
47
90
|
* @property {Uint8ClampedArray | Uint8Array | number[]} data RGBA, 4 bytes per pixel.
|
|
@@ -56,6 +99,10 @@ export class LuminanceSource {
|
|
|
56
99
|
* @param {number} height
|
|
57
100
|
*/
|
|
58
101
|
constructor(grey, width, height) {
|
|
102
|
+
const pixels = validateDimensions(width, height);
|
|
103
|
+
if (!isByteArray(grey) || grey.length < pixels) {
|
|
104
|
+
throw new NotFoundError(`Greyscale buffer is shorter than the required ${pixels} bytes`);
|
|
105
|
+
}
|
|
59
106
|
this.grey = grey;
|
|
60
107
|
this.width = width;
|
|
61
108
|
this.height = height;
|
|
@@ -72,18 +119,15 @@ export class LuminanceSource {
|
|
|
72
119
|
* @returns {LuminanceSource}
|
|
73
120
|
*/
|
|
74
121
|
static fromImageData(image) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
throw new NotFoundError('Image must be { data, width, height } with RGBA data');
|
|
78
|
-
}
|
|
79
|
-
if (data.length < width * height * 4) {
|
|
80
|
-
throw new NotFoundError(
|
|
81
|
-
`Image data too short: ${data.length} bytes for ${width}x${height} RGBA ` +
|
|
82
|
-
`(expected ${width * height * 4})`
|
|
83
|
-
);
|
|
122
|
+
if (!image || typeof image !== 'object') {
|
|
123
|
+
throw new NotFoundError('Image must be an object with RGBA data');
|
|
84
124
|
}
|
|
125
|
+
const { data, width, height } = image;
|
|
126
|
+
const pixels = validateDimensions(width, height);
|
|
127
|
+
const expected = pixels * 4;
|
|
128
|
+
validateByteData(data, expected, 'Image data');
|
|
85
129
|
|
|
86
|
-
const grey = new Uint8Array(
|
|
130
|
+
const grey = new Uint8Array(pixels);
|
|
87
131
|
for (let i = 0, p = 0; i < grey.length; i++, p += 4) {
|
|
88
132
|
const a = data[p + 3];
|
|
89
133
|
let r = data[p], g = data[p + 1], b = data[p + 2];
|
|
@@ -109,10 +153,11 @@ export class LuminanceSource {
|
|
|
109
153
|
* @returns {LuminanceSource}
|
|
110
154
|
*/
|
|
111
155
|
static fromGrey(grey, width, height) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
156
|
+
const pixels = validateDimensions(width, height);
|
|
157
|
+
validateByteData(grey, pixels, 'Greyscale buffer');
|
|
158
|
+
// Snapshot caller-owned data so later mutations cannot alter a decode in progress.
|
|
159
|
+
const copy = Uint8Array.from(Array.isArray(grey) ? grey.slice(0, pixels) : grey.subarray(0, pixels));
|
|
160
|
+
return new LuminanceSource(copy, width, height);
|
|
116
161
|
}
|
|
117
162
|
|
|
118
163
|
/**
|
|
@@ -129,6 +174,9 @@ export class LuminanceSource {
|
|
|
129
174
|
* @returns {Uint8Array}
|
|
130
175
|
*/
|
|
131
176
|
getRow(y, out) {
|
|
177
|
+
if (!Number.isSafeInteger(y) || y < 0 || y >= this.height) {
|
|
178
|
+
throw new RangeError(`LuminanceSource row is outside the image: ${y}`);
|
|
179
|
+
}
|
|
132
180
|
const row = out && out.length >= this.width ? out : new Uint8Array(this.width);
|
|
133
181
|
row.set(this.grey.subarray(y * this.width, (y + 1) * this.width));
|
|
134
182
|
return row;
|
package/src/ts/index.d.ts
CHANGED
package/src/ts/index.ts
CHANGED
package/src/ts/render/options.ts
CHANGED
|
@@ -45,6 +45,54 @@ import { BitMatrix } from '../core/bit-matrix.js';
|
|
|
45
45
|
* @property {number} [barHeight] For 1D symbols: total bar height in pixels.
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
|
+
// Keep renderer allocations aligned with the image decoder's resource limits.
|
|
49
|
+
const MAX_RENDER_DIMENSION = 16_384;
|
|
50
|
+
const MAX_RENDER_PIXELS = 16_777_216;
|
|
51
|
+
const MAX_RENDER_SCALE = MAX_RENDER_DIMENSION;
|
|
52
|
+
const MAX_RENDER_MARGIN = Math.floor((MAX_RENDER_DIMENSION - 1) / 2);
|
|
53
|
+
const MAX_RENDER_BAR_HEIGHT = MAX_RENDER_DIMENSION;
|
|
54
|
+
|
|
55
|
+
function boundedInteger(value, name, defaultValue, minimum, maximum) {
|
|
56
|
+
const resolved = value ?? defaultValue;
|
|
57
|
+
if (!Number.isSafeInteger(resolved)) {
|
|
58
|
+
throw new RangeError(
|
|
59
|
+
`Render option "${name}" must be a finite safe integer between ${minimum} and ${maximum}, got ${resolved}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (resolved < minimum || resolved > maximum) {
|
|
63
|
+
throw new RangeError(
|
|
64
|
+
`Render option "${name}" must be between ${minimum} and ${maximum}, got ${resolved}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return resolved;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validateMatrixDimensions(matrix) {
|
|
71
|
+
if (!matrix || !Number.isSafeInteger(matrix.width) || !Number.isSafeInteger(matrix.height)
|
|
72
|
+
|| matrix.width < 1 || matrix.height < 1
|
|
73
|
+
|| matrix.width > MAX_RENDER_DIMENSION || matrix.height > MAX_RENDER_DIMENSION) {
|
|
74
|
+
throw new RangeError(
|
|
75
|
+
`Render matrix dimensions must be positive safe integers no larger than ${MAX_RENDER_DIMENSION}`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validatePixelDimensions(width, height) {
|
|
81
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)
|
|
82
|
+
|| width < 1 || height < 1
|
|
83
|
+
|| width > MAX_RENDER_DIMENSION || height > MAX_RENDER_DIMENSION) {
|
|
84
|
+
throw new RangeError(
|
|
85
|
+
`Render dimensions must be positive safe integers no larger than ${MAX_RENDER_DIMENSION}`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
const pixels = width * height;
|
|
89
|
+
if (!Number.isSafeInteger(pixels) || pixels > MAX_RENDER_PIXELS) {
|
|
90
|
+
throw new RangeError(
|
|
91
|
+
`Render image contains too many pixels: ${pixels} (maximum ${MAX_RENDER_PIXELS})`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
48
96
|
/**
|
|
49
97
|
* Expand and pad the matrix, and resolve every dimension.
|
|
50
98
|
*
|
|
@@ -57,20 +105,29 @@ import { BitMatrix } from '../core/bit-matrix.js';
|
|
|
57
105
|
* @param {RenderOptions} options
|
|
58
106
|
*/
|
|
59
107
|
export function normalizeOptions(matrix, options = {}) {
|
|
60
|
-
|
|
61
|
-
const
|
|
108
|
+
validateMatrixDimensions(matrix);
|
|
109
|
+
const scale = boundedInteger(options.scale, 'scale', 8, 1, MAX_RENDER_SCALE);
|
|
110
|
+
const margin = boundedInteger(options.margin, 'margin', 4, 0, MAX_RENDER_MARGIN);
|
|
62
111
|
const dark = options.dark ?? '#000000';
|
|
63
112
|
const light = options.light ?? '#ffffff';
|
|
64
|
-
const barHeight = options.barHeight
|
|
113
|
+
const barHeight = options.barHeight == null
|
|
114
|
+
? null
|
|
115
|
+
: boundedInteger(options.barHeight, 'barHeight', 1, 1, MAX_RENDER_BAR_HEIGHT);
|
|
65
116
|
|
|
66
|
-
let base = matrix;
|
|
67
117
|
const is1D = matrix.height === 1;
|
|
118
|
+
const targetPixels = is1D
|
|
119
|
+
? barHeight ?? Math.max(40, Math.round(matrix.width * scale * 0.15))
|
|
120
|
+
: null;
|
|
121
|
+
const rows = is1D ? Math.max(1, Math.round(targetPixels / scale)) : matrix.height;
|
|
122
|
+
const sourceWidth = matrix.width + margin * 2;
|
|
123
|
+
const sourceHeight = rows + margin * 2;
|
|
124
|
+
validatePixelDimensions(sourceWidth * scale, sourceHeight * scale);
|
|
125
|
+
|
|
126
|
+
let base = matrix;
|
|
68
127
|
|
|
69
128
|
if (is1D) {
|
|
70
129
|
// Default to a bar height that stays scannable: tall enough that a laser
|
|
71
130
|
// crossing at a slight angle still passes through the whole symbol.
|
|
72
|
-
const targetPixels = barHeight ?? Math.max(40, Math.round(matrix.width * scale * 0.15));
|
|
73
|
-
const rows = Math.max(1, Math.round(targetPixels / scale));
|
|
74
131
|
base = new BitMatrix(matrix.width, rows);
|
|
75
132
|
for (let x = 0; x < matrix.width; x++) {
|
|
76
133
|
if (!matrix.get(x, 0)) continue;
|