@sythos/js_barcode_universal 1.5.9 → 1.5.11

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 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.9"></script>
116
- <script src="https://cdn.jsdelivr.net/npm/@sythos/js_barcode_universal@1.5.9"></script>
115
+ <script src="https://unpkg.com/@sythos/js_barcode_universal@1.5.11"></script>
116
+ <script src="https://cdn.jsdelivr.net/npm/@sythos/js_barcode_universal@1.5.11"></script>
117
117
  ```
118
118
 
119
119
  Pin the version for anything you ship; the unpinned form resolves to `latest` and will move under
@@ -656,6 +656,55 @@ and reports which backend actually drew.
656
656
 
657
657
  ---
658
658
 
659
+ ## Security automation
660
+
661
+ The repository is covered by GitHub's configured [CodeQL default setup](https://github.com/Sythos/JS_Barcode_Universal/security/code-scanning)
662
+ for JavaScript and TypeScript code-scanning analysis. GitHub manages the analysis schedule and
663
+ the security-events upload; this avoids running a duplicate advanced workflow alongside the
664
+ repository-level default setup.
665
+
666
+ [Dependabot](.github/dependabot.yml) checks the development TypeScript toolchain and GitHub Actions
667
+ references weekly. Updates are repository-development controls only; the published SDK remains
668
+ zero-dependency at runtime.
669
+
670
+ These security workflows report findings and propose maintenance updates. They do not replace
671
+ review of barcode conformance, licensing, patent status or release attestations.
672
+
673
+ The image I/O boundary treats camera and file rasters as untrusted input. It validates finite
674
+ positive dimensions, bounds allocations to 16,777,216 pixels, accepts only byte-valued channels,
675
+ and snapshots greyscale buffers before decoding. Malformed or oversized rasters are rejected
676
+ before detector work begins.
677
+
678
+ ---
679
+
680
+ ## Build provenance and artifact attestations
681
+
682
+ The package publication workflow in [`.github/workflows/npm-publish.yml`](.github/workflows/npm-publish.yml)
683
+ uses npm provenance when publishing to the public npm registry. npm provenance and GitHub Artifact
684
+ Attestations are related but separate records:
685
+
686
+ - **npm provenance** is issued by npm for a package publication and links the published package to
687
+ its source repository and trusted build workflow;
688
+ - **GitHub Artifact Attestations** bind a build artifact, its digest and its build context to the
689
+ GitHub Actions workflow that produced it. They can be verified independently of the npm registry.
690
+
691
+ The release-specific attestation workflow is expected at
692
+ [`.github/workflows/release.yml`](.github/workflows/release.yml). Release assets must be verified
693
+ against this repository and their exact local file contents:
694
+
695
+ ```sh
696
+ gh attestation verify path/to/release-asset.tgz -R Sythos/JS_Barcode_Universal
697
+ ```
698
+
699
+ The same command can be used for any other attested release asset by replacing the path. An
700
+ attestation confirms build provenance; it is not an ISO barcode-conformance certificate, a patent
701
+ clearance, or a guarantee that the implementation is vulnerability-free.
702
+
703
+ Release automation validates the package version against the selected Git tag; it does not invent
704
+ or increment versions by itself. The current release is `1.5.11`.
705
+
706
+ ---
707
+
659
708
  ## Licence
660
709
 
661
710
  MIT © 2026 Sythos (https://www.sythos.net). Every source file carries the header.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Sythos Barcode Suite v1.5.9
2
+ * Sythos Barcode Suite v1.5.11
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
- const { data, width, height } = image;
344
- if (!data || !width || !height) {
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
- if (data.length < width * height * 4) {
348
- throw new NotFoundError(`Image data too short: ${data.length} bytes for ${width}x${height} RGBA ` +
349
- `(expected ${width * height * 4})`);
350
- }
351
- const grey = new Uint8Array(width * height);
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
- if (grey.length < width * height) {
377
- throw new NotFoundError('Greyscale buffer shorter than width * height');
378
- }
379
- return new LuminanceSource(grey, width, height);
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.9';
17599
+ const VERSION = '1.5.11';
17558
17600
 
17559
17601
  __exports.listFormats = listFormats;
17560
17602
  __exports.encode = encode;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Sythos Barcode Suite v1.5.9
2
+ * Sythos Barcode Suite v1.5.11
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
- const { data, width, height } = image;
345
- if (!data || !width || !height) {
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
- if (data.length < width * height * 4) {
349
- throw new NotFoundError(`Image data too short: ${data.length} bytes for ${width}x${height} RGBA ` +
350
- `(expected ${width * height * 4})`);
351
- }
352
- const grey = new Uint8Array(width * height);
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
- if (grey.length < width * height) {
378
- throw new NotFoundError('Greyscale buffer shorter than width * height');
379
- }
380
- return new LuminanceSource(grey, width, height);
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.9';
17600
+ const VERSION = '1.5.11';
17559
17601
 
17560
17602
  __exports.listFormats = listFormats;
17561
17603
  __exports.encode = encode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sythos/js_barcode_universal",
3
- "version": "1.5.9",
3
+ "version": "1.5.11",
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",
package/src/index.d.ts CHANGED
@@ -360,4 +360,4 @@ export declare function decodeStrict(image: {
360
360
  height: number;
361
361
  }, options?: object): DecodeResult;
362
362
  /** Library version, matching package.json. */
363
- export declare const VERSION = "1.5.9";
363
+ export declare const VERSION = "1.5.11";
package/src/index.js CHANGED
@@ -779,4 +779,4 @@ export function decodeStrict(image, options) {
779
779
  return results[0];
780
780
  }
781
781
  /** Library version, matching package.json. */
782
- export const VERSION = '1.5.9';
782
+ export const VERSION = '1.5.11';
@@ -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
- const { data, width, height } = image;
71
- if (!data || !width || !height) {
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 grey = new Uint8Array(width * height);
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
- if (grey.length < width * height) {
104
- throw new NotFoundError('Greyscale buffer shorter than width * height');
105
- }
106
- return new LuminanceSource(grey, width, height);
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;
@@ -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
- const { data, width, height } = image;
76
- if (!data || !width || !height) {
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(width * height);
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
- if (grey.length < width * height) {
113
- throw new NotFoundError('Greyscale buffer shorter than width * height');
114
- }
115
- return new LuminanceSource(grey, width, height);
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
@@ -360,4 +360,4 @@ export declare function decodeStrict(image: {
360
360
  height: number;
361
361
  }, options?: object): DecodeResult;
362
362
  /** Library version, matching package.json. */
363
- export declare const VERSION = "1.5.9";
363
+ export declare const VERSION = "1.5.11";
package/src/ts/index.ts CHANGED
@@ -787,4 +787,4 @@ export function decodeStrict(image, options) {
787
787
  }
788
788
 
789
789
  /** Library version, matching package.json. */
790
- export const VERSION = '1.5.9';
790
+ export const VERSION = '1.5.11';