compressorjs-next 1.1.0 → 1.1.2

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
@@ -245,16 +245,16 @@ The [MIME type](https://webglossary.info/terms/mime-type/) of the output image.
245
245
  - `["image/png", "image/webp"]`
246
246
  - `"image/png,image/webp"`
247
247
 
248
- Files whose file type is included in this list, and whose file size exceeds the `convertSize` value will be converted to JPEG.
248
+ Files whose file type is included in this list and whose file size exceeds the `convertSize` value will be converted to JPEG.
249
249
 
250
250
  For image file type support, see the [Image file type and format guide](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types).
251
251
 
252
252
  ### `convertSize`
253
253
 
254
254
  * Type: `number`
255
- * Default: `5000000` (5 MB)
255
+ * Default: `5000000` (5 MB)
256
256
 
257
- Files whose file type is included in the `convertTypes` list, and whose file size exceeds this value will be converted to JPEG. To disable this, just set the value to `Infinity`.
257
+ Files whose file type is included in the `convertTypes` list and whose file size exceeds this value will be converted to JPEG. Can also be disabled through the value `Infinity`.
258
258
 
259
259
  **Examples:**
260
260
 
@@ -338,3 +338,5 @@ compressor.abort();
338
338
  ## Browser support
339
339
 
340
340
  Supports [browserslist `defaults`](https://browsersl.ist/#q=defaults).
341
+
342
+ **Note:** When the browser’s canvas produces unreliable pixel data—as with Firefox’s `privacy.resistFingerprinting` setting or privacy-focused forks like LibreWolf—, compression, resizing, and format conversion are not possible. In this case, the library falls back to returning the original image with EXIF data stripped (JPEG) or unchanged (other formats).
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Compressor.js Next v1.1.0
2
+ * Compressor.js Next v1.1.2
3
3
  * https://github.com/j9t/compressorjs-next
4
4
  *
5
5
  * Copyright 2018–2024 Chen Fengyuan
@@ -80,15 +80,15 @@ var DEFAULTS = {
80
80
  */
81
81
  mimeType: 'auto',
82
82
  /**
83
- * Files whose file type is included in this list,
84
- * and whose file size exceeds the `convertSize` value will be converted to JPEG.
83
+ * Files whose file type is included in this list and
84
+ * whose file size exceeds the `convertSize` value
85
+ * will be converted to JPEG.
85
86
  * @type {string|Array}
86
87
  */
87
88
  convertTypes: [],
88
89
  /**
89
90
  * Files over this size (5 MB by default) whose type is in `convertTypes`
90
91
  * will be converted to JPEG.
91
- * To disable this, set `convertTypes` to `[]` or the value to `Infinity`.
92
92
  * @type {number}
93
93
  */
94
94
  convertSize: 5000000,
@@ -336,6 +336,95 @@ function parseOrientation(orientation) {
336
336
  scaleY
337
337
  };
338
338
  }
339
+ let cachedCanvasReliable;
340
+
341
+ /**
342
+ * Check if the browser’s canvas produces reliable pixel data.
343
+ * Returns `false` when anti-fingerprinting measures (e.g., Firefox’s
344
+ * `privacy.resistFingerprinting`) add noise to canvas output.
345
+ * The result is cached after the first call.
346
+ * @returns {boolean} Returns `true` if canvas data is reliable.
347
+ */
348
+ function isCanvasReliable() {
349
+ if (cachedCanvasReliable !== undefined) return cachedCanvasReliable;
350
+ try {
351
+ const canvas = document.createElement('canvas');
352
+ canvas.width = 4;
353
+ canvas.height = 4;
354
+ const ctx = canvas.getContext('2d');
355
+ const imageData = ctx.createImageData(canvas.width, canvas.height);
356
+ for (let i = 0; i < imageData.data.length; i += 4) {
357
+ imageData.data[i] = i;
358
+ imageData.data[i + 1] = 1;
359
+ imageData.data[i + 2] = 2;
360
+ imageData.data[i + 3] = 255;
361
+ }
362
+ ctx.putImageData(imageData, 0, 0);
363
+ const result = ctx.getImageData(0, 0, canvas.width, canvas.height);
364
+ cachedCanvasReliable = result.data.every((value, index) => {
365
+ const channel = index % 4;
366
+ if (channel === 0) return value === (index & 0xFF);
367
+ if (channel === 1) return value === 1;
368
+ if (channel === 2) return value === 2;
369
+ return value === 255;
370
+ });
371
+ } catch {
372
+ cachedCanvasReliable = false;
373
+ }
374
+ return cachedCanvasReliable;
375
+ }
376
+
377
+ /**
378
+ * Strip all APP1 (EXIF) segments from a JPEG array buffer.
379
+ * @param {ArrayBuffer} arrayBuffer - The JPEG data to strip.
380
+ * @returns {Uint8Array} The JPEG data without EXIF segments.
381
+ */
382
+ function stripExif(arrayBuffer) {
383
+ const dataView = new DataView(arrayBuffer);
384
+ const {
385
+ byteLength
386
+ } = dataView;
387
+ const pieces = [];
388
+
389
+ // Only handle JPEG data (starts with SOI marker FF D8)
390
+ if (byteLength < 4 || dataView.getUint8(0) !== 0xFF || dataView.getUint8(1) !== 0xD8) {
391
+ return new Uint8Array(arrayBuffer);
392
+ }
393
+
394
+ // Keep SOI marker
395
+ pieces.push(new Uint8Array(arrayBuffer, 0, 2));
396
+ let start = 2;
397
+ while (start + 3 < byteLength) {
398
+ const marker = dataView.getUint8(start);
399
+ const type = dataView.getUint8(start + 1);
400
+ if (marker !== 0xFF) break;
401
+
402
+ // SOS (Start of Scan)—the rest is image data, keep it all
403
+ if (type === 0xDA) {
404
+ pieces.push(new Uint8Array(arrayBuffer, start));
405
+ break;
406
+ }
407
+ if (start + 3 >= byteLength) break;
408
+ const segmentLength = dataView.getUint16(start + 2);
409
+ if (segmentLength < 2) break;
410
+ const segmentEnd = start + 2 + segmentLength;
411
+ if (segmentEnd > byteLength) break;
412
+
413
+ // Skip APP1 (EXIF) segments, keep everything else
414
+ if (type !== 0xE1) {
415
+ pieces.push(new Uint8Array(arrayBuffer, start, segmentEnd - start));
416
+ }
417
+ start = segmentEnd;
418
+ }
419
+ const totalLength = pieces.reduce((sum, piece) => sum + piece.length, 0);
420
+ const result = new Uint8Array(totalLength);
421
+ let offset = 0;
422
+ for (const piece of pieces) {
423
+ result.set(piece, offset);
424
+ offset += piece.length;
425
+ }
426
+ return result;
427
+ }
339
428
  const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
340
429
 
341
430
  /**
@@ -496,6 +585,7 @@ class Compressor {
496
585
  };
497
586
  this.mimeTypeSet = options && options.mimeType && isImageType(options.mimeType);
498
587
  this.aborted = false;
588
+ this.canvasFallback = false;
499
589
  this.result = null;
500
590
  this.url = null;
501
591
  this.init();
@@ -522,6 +612,58 @@ class Compressor {
522
612
  options.checkOrientation = false;
523
613
  options.retainExif = false;
524
614
  }
615
+ if (!isCanvasReliable()) {
616
+ // Canvas is unreliable (e.g., Firefox fingerprinting resistance)—
617
+ // bypass canvas to avoid corrupted output
618
+ console.warn('Compressor.js Next: Canvas data is unreliable (e.g., due to browser fingerprinting resistance)—compression, resizing, and format conversion are unavailable');
619
+ this.canvasFallback = true;
620
+ if (mimeType === 'image/jpeg' && !options.retainExif) {
621
+ // Strip EXIF data directly from the binary to preserve privacy
622
+ const reader = new FileReader();
623
+ this.reader = reader;
624
+ reader.onload = ({
625
+ target
626
+ }) => {
627
+ if (this.aborted) return;
628
+ let result;
629
+ try {
630
+ const stripped = stripExif(target.result);
631
+ result = uint8ArrayToBlob(stripped, mimeType);
632
+ } catch {
633
+ this.fail(new Error('Failed to process the image data.'));
634
+ return;
635
+ }
636
+ const date = new Date();
637
+ result.name = file.name;
638
+ result.lastModified = date.getTime();
639
+ this.result = result;
640
+ if (options.success) {
641
+ options.success.call(this, result);
642
+ }
643
+ };
644
+ reader.onabort = () => {
645
+ this.fail(new Error('Aborted to read the image with FileReader.'));
646
+ };
647
+ reader.onerror = () => {
648
+ this.fail(new Error('Failed to read the image with FileReader.'));
649
+ };
650
+ reader.onloadend = () => {
651
+ this.reader = null;
652
+ };
653
+ reader.readAsArrayBuffer(file);
654
+ } else {
655
+ // Non-JPEG: No EXIF to strip, return as-is
656
+ // Defer callback to match the normal async flow
657
+ Promise.resolve().then(() => {
658
+ if (this.aborted) return;
659
+ this.result = file;
660
+ if (options.success) {
661
+ options.success.call(this, file);
662
+ }
663
+ });
664
+ }
665
+ return;
666
+ }
525
667
  const isJPEGImage = mimeType === 'image/jpeg';
526
668
  const checkOrientation = isJPEGImage && options.checkOrientation;
527
669
  const retainExif = isJPEGImage && options.retainExif;
@@ -687,11 +829,7 @@ class Compressor {
687
829
  const destHeight = height;
688
830
  const params = [];
689
831
  if (resizable) {
690
- let srcX = 0;
691
- let srcY = 0;
692
- let srcWidth = naturalWidth;
693
- let srcHeight = naturalHeight;
694
- ({
832
+ const {
695
833
  width: srcWidth,
696
834
  height: srcHeight
697
835
  } = getAdjustedSizes({
@@ -701,9 +839,9 @@ class Compressor {
701
839
  }, {
702
840
  contain: 'cover',
703
841
  cover: 'contain'
704
- }[options.resize]));
705
- srcX = (naturalWidth - srcWidth) / 2;
706
- srcY = (naturalHeight - srcHeight) / 2;
842
+ }[options.resize]);
843
+ const srcX = (naturalWidth - srcWidth) / 2;
844
+ const srcY = (naturalHeight - srcHeight) / 2;
707
845
  params.push(srcX, srcY, srcWidth, srcHeight);
708
846
  }
709
847
  params.push(destX, destY, destWidth, destHeight);
@@ -807,7 +945,6 @@ class Compressor {
807
945
  } else {
808
946
  const date = new Date();
809
947
  result.lastModified = date.getTime();
810
- result.lastModifiedDate = date;
811
948
  result.name = file.name;
812
949
 
813
950
  // Convert the extension to match its type
@@ -817,6 +954,7 @@ class Compressor {
817
954
  }
818
955
  } else {
819
956
  // Returns original file if the result is null in some cases
957
+ console.warn('Compressor.js Next: Canvas produced no output—returning the original image');
820
958
  result = file;
821
959
  }
822
960
  this.result = result;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Compressor.js Next v1.1.0
2
+ * Compressor.js Next v1.1.2
3
3
  * https://github.com/j9t/compressorjs-next
4
4
  *
5
5
  * Copyright 2018–2024 Chen Fengyuan
@@ -78,15 +78,15 @@ var DEFAULTS = {
78
78
  */
79
79
  mimeType: 'auto',
80
80
  /**
81
- * Files whose file type is included in this list,
82
- * and whose file size exceeds the `convertSize` value will be converted to JPEG.
81
+ * Files whose file type is included in this list and
82
+ * whose file size exceeds the `convertSize` value
83
+ * will be converted to JPEG.
83
84
  * @type {string|Array}
84
85
  */
85
86
  convertTypes: [],
86
87
  /**
87
88
  * Files over this size (5 MB by default) whose type is in `convertTypes`
88
89
  * will be converted to JPEG.
89
- * To disable this, set `convertTypes` to `[]` or the value to `Infinity`.
90
90
  * @type {number}
91
91
  */
92
92
  convertSize: 5000000,
@@ -334,6 +334,95 @@ function parseOrientation(orientation) {
334
334
  scaleY
335
335
  };
336
336
  }
337
+ let cachedCanvasReliable;
338
+
339
+ /**
340
+ * Check if the browser’s canvas produces reliable pixel data.
341
+ * Returns `false` when anti-fingerprinting measures (e.g., Firefox’s
342
+ * `privacy.resistFingerprinting`) add noise to canvas output.
343
+ * The result is cached after the first call.
344
+ * @returns {boolean} Returns `true` if canvas data is reliable.
345
+ */
346
+ function isCanvasReliable() {
347
+ if (cachedCanvasReliable !== undefined) return cachedCanvasReliable;
348
+ try {
349
+ const canvas = document.createElement('canvas');
350
+ canvas.width = 4;
351
+ canvas.height = 4;
352
+ const ctx = canvas.getContext('2d');
353
+ const imageData = ctx.createImageData(canvas.width, canvas.height);
354
+ for (let i = 0; i < imageData.data.length; i += 4) {
355
+ imageData.data[i] = i;
356
+ imageData.data[i + 1] = 1;
357
+ imageData.data[i + 2] = 2;
358
+ imageData.data[i + 3] = 255;
359
+ }
360
+ ctx.putImageData(imageData, 0, 0);
361
+ const result = ctx.getImageData(0, 0, canvas.width, canvas.height);
362
+ cachedCanvasReliable = result.data.every((value, index) => {
363
+ const channel = index % 4;
364
+ if (channel === 0) return value === (index & 0xFF);
365
+ if (channel === 1) return value === 1;
366
+ if (channel === 2) return value === 2;
367
+ return value === 255;
368
+ });
369
+ } catch {
370
+ cachedCanvasReliable = false;
371
+ }
372
+ return cachedCanvasReliable;
373
+ }
374
+
375
+ /**
376
+ * Strip all APP1 (EXIF) segments from a JPEG array buffer.
377
+ * @param {ArrayBuffer} arrayBuffer - The JPEG data to strip.
378
+ * @returns {Uint8Array} The JPEG data without EXIF segments.
379
+ */
380
+ function stripExif(arrayBuffer) {
381
+ const dataView = new DataView(arrayBuffer);
382
+ const {
383
+ byteLength
384
+ } = dataView;
385
+ const pieces = [];
386
+
387
+ // Only handle JPEG data (starts with SOI marker FF D8)
388
+ if (byteLength < 4 || dataView.getUint8(0) !== 0xFF || dataView.getUint8(1) !== 0xD8) {
389
+ return new Uint8Array(arrayBuffer);
390
+ }
391
+
392
+ // Keep SOI marker
393
+ pieces.push(new Uint8Array(arrayBuffer, 0, 2));
394
+ let start = 2;
395
+ while (start + 3 < byteLength) {
396
+ const marker = dataView.getUint8(start);
397
+ const type = dataView.getUint8(start + 1);
398
+ if (marker !== 0xFF) break;
399
+
400
+ // SOS (Start of Scan)—the rest is image data, keep it all
401
+ if (type === 0xDA) {
402
+ pieces.push(new Uint8Array(arrayBuffer, start));
403
+ break;
404
+ }
405
+ if (start + 3 >= byteLength) break;
406
+ const segmentLength = dataView.getUint16(start + 2);
407
+ if (segmentLength < 2) break;
408
+ const segmentEnd = start + 2 + segmentLength;
409
+ if (segmentEnd > byteLength) break;
410
+
411
+ // Skip APP1 (EXIF) segments, keep everything else
412
+ if (type !== 0xE1) {
413
+ pieces.push(new Uint8Array(arrayBuffer, start, segmentEnd - start));
414
+ }
415
+ start = segmentEnd;
416
+ }
417
+ const totalLength = pieces.reduce((sum, piece) => sum + piece.length, 0);
418
+ const result = new Uint8Array(totalLength);
419
+ let offset = 0;
420
+ for (const piece of pieces) {
421
+ result.set(piece, offset);
422
+ offset += piece.length;
423
+ }
424
+ return result;
425
+ }
337
426
  const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
338
427
 
339
428
  /**
@@ -494,6 +583,7 @@ class Compressor {
494
583
  };
495
584
  this.mimeTypeSet = options && options.mimeType && isImageType(options.mimeType);
496
585
  this.aborted = false;
586
+ this.canvasFallback = false;
497
587
  this.result = null;
498
588
  this.url = null;
499
589
  this.init();
@@ -520,6 +610,58 @@ class Compressor {
520
610
  options.checkOrientation = false;
521
611
  options.retainExif = false;
522
612
  }
613
+ if (!isCanvasReliable()) {
614
+ // Canvas is unreliable (e.g., Firefox fingerprinting resistance)—
615
+ // bypass canvas to avoid corrupted output
616
+ console.warn('Compressor.js Next: Canvas data is unreliable (e.g., due to browser fingerprinting resistance)—compression, resizing, and format conversion are unavailable');
617
+ this.canvasFallback = true;
618
+ if (mimeType === 'image/jpeg' && !options.retainExif) {
619
+ // Strip EXIF data directly from the binary to preserve privacy
620
+ const reader = new FileReader();
621
+ this.reader = reader;
622
+ reader.onload = ({
623
+ target
624
+ }) => {
625
+ if (this.aborted) return;
626
+ let result;
627
+ try {
628
+ const stripped = stripExif(target.result);
629
+ result = uint8ArrayToBlob(stripped, mimeType);
630
+ } catch {
631
+ this.fail(new Error('Failed to process the image data.'));
632
+ return;
633
+ }
634
+ const date = new Date();
635
+ result.name = file.name;
636
+ result.lastModified = date.getTime();
637
+ this.result = result;
638
+ if (options.success) {
639
+ options.success.call(this, result);
640
+ }
641
+ };
642
+ reader.onabort = () => {
643
+ this.fail(new Error('Aborted to read the image with FileReader.'));
644
+ };
645
+ reader.onerror = () => {
646
+ this.fail(new Error('Failed to read the image with FileReader.'));
647
+ };
648
+ reader.onloadend = () => {
649
+ this.reader = null;
650
+ };
651
+ reader.readAsArrayBuffer(file);
652
+ } else {
653
+ // Non-JPEG: No EXIF to strip, return as-is
654
+ // Defer callback to match the normal async flow
655
+ Promise.resolve().then(() => {
656
+ if (this.aborted) return;
657
+ this.result = file;
658
+ if (options.success) {
659
+ options.success.call(this, file);
660
+ }
661
+ });
662
+ }
663
+ return;
664
+ }
523
665
  const isJPEGImage = mimeType === 'image/jpeg';
524
666
  const checkOrientation = isJPEGImage && options.checkOrientation;
525
667
  const retainExif = isJPEGImage && options.retainExif;
@@ -685,11 +827,7 @@ class Compressor {
685
827
  const destHeight = height;
686
828
  const params = [];
687
829
  if (resizable) {
688
- let srcX = 0;
689
- let srcY = 0;
690
- let srcWidth = naturalWidth;
691
- let srcHeight = naturalHeight;
692
- ({
830
+ const {
693
831
  width: srcWidth,
694
832
  height: srcHeight
695
833
  } = getAdjustedSizes({
@@ -699,9 +837,9 @@ class Compressor {
699
837
  }, {
700
838
  contain: 'cover',
701
839
  cover: 'contain'
702
- }[options.resize]));
703
- srcX = (naturalWidth - srcWidth) / 2;
704
- srcY = (naturalHeight - srcHeight) / 2;
840
+ }[options.resize]);
841
+ const srcX = (naturalWidth - srcWidth) / 2;
842
+ const srcY = (naturalHeight - srcHeight) / 2;
705
843
  params.push(srcX, srcY, srcWidth, srcHeight);
706
844
  }
707
845
  params.push(destX, destY, destWidth, destHeight);
@@ -805,7 +943,6 @@ class Compressor {
805
943
  } else {
806
944
  const date = new Date();
807
945
  result.lastModified = date.getTime();
808
- result.lastModifiedDate = date;
809
946
  result.name = file.name;
810
947
 
811
948
  // Convert the extension to match its type
@@ -815,6 +952,7 @@ class Compressor {
815
952
  }
816
953
  } else {
817
954
  // Returns original file if the result is null in some cases
955
+ console.warn('Compressor.js Next: Canvas produced no output—returning the original image');
818
956
  result = file;
819
957
  }
820
958
  this.result = result;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Compressor.js Next v1.1.0
2
+ * Compressor.js Next v1.1.2
3
3
  * https://github.com/j9t/compressorjs-next
4
4
  *
5
5
  * Copyright 2018–2024 Chen Fengyuan
@@ -84,15 +84,15 @@
84
84
  */
85
85
  mimeType: 'auto',
86
86
  /**
87
- * Files whose file type is included in this list,
88
- * and whose file size exceeds the `convertSize` value will be converted to JPEG.
87
+ * Files whose file type is included in this list and
88
+ * whose file size exceeds the `convertSize` value
89
+ * will be converted to JPEG.
89
90
  * @type {string|Array}
90
91
  */
91
92
  convertTypes: [],
92
93
  /**
93
94
  * Files over this size (5 MB by default) whose type is in `convertTypes`
94
95
  * will be converted to JPEG.
95
- * To disable this, set `convertTypes` to `[]` or the value to `Infinity`.
96
96
  * @type {number}
97
97
  */
98
98
  convertSize: 5000000,
@@ -340,6 +340,95 @@
340
340
  scaleY
341
341
  };
342
342
  }
343
+ let cachedCanvasReliable;
344
+
345
+ /**
346
+ * Check if the browser’s canvas produces reliable pixel data.
347
+ * Returns `false` when anti-fingerprinting measures (e.g., Firefox’s
348
+ * `privacy.resistFingerprinting`) add noise to canvas output.
349
+ * The result is cached after the first call.
350
+ * @returns {boolean} Returns `true` if canvas data is reliable.
351
+ */
352
+ function isCanvasReliable() {
353
+ if (cachedCanvasReliable !== undefined) return cachedCanvasReliable;
354
+ try {
355
+ const canvas = document.createElement('canvas');
356
+ canvas.width = 4;
357
+ canvas.height = 4;
358
+ const ctx = canvas.getContext('2d');
359
+ const imageData = ctx.createImageData(canvas.width, canvas.height);
360
+ for (let i = 0; i < imageData.data.length; i += 4) {
361
+ imageData.data[i] = i;
362
+ imageData.data[i + 1] = 1;
363
+ imageData.data[i + 2] = 2;
364
+ imageData.data[i + 3] = 255;
365
+ }
366
+ ctx.putImageData(imageData, 0, 0);
367
+ const result = ctx.getImageData(0, 0, canvas.width, canvas.height);
368
+ cachedCanvasReliable = result.data.every((value, index) => {
369
+ const channel = index % 4;
370
+ if (channel === 0) return value === (index & 0xFF);
371
+ if (channel === 1) return value === 1;
372
+ if (channel === 2) return value === 2;
373
+ return value === 255;
374
+ });
375
+ } catch {
376
+ cachedCanvasReliable = false;
377
+ }
378
+ return cachedCanvasReliable;
379
+ }
380
+
381
+ /**
382
+ * Strip all APP1 (EXIF) segments from a JPEG array buffer.
383
+ * @param {ArrayBuffer} arrayBuffer - The JPEG data to strip.
384
+ * @returns {Uint8Array} The JPEG data without EXIF segments.
385
+ */
386
+ function stripExif(arrayBuffer) {
387
+ const dataView = new DataView(arrayBuffer);
388
+ const {
389
+ byteLength
390
+ } = dataView;
391
+ const pieces = [];
392
+
393
+ // Only handle JPEG data (starts with SOI marker FF D8)
394
+ if (byteLength < 4 || dataView.getUint8(0) !== 0xFF || dataView.getUint8(1) !== 0xD8) {
395
+ return new Uint8Array(arrayBuffer);
396
+ }
397
+
398
+ // Keep SOI marker
399
+ pieces.push(new Uint8Array(arrayBuffer, 0, 2));
400
+ let start = 2;
401
+ while (start + 3 < byteLength) {
402
+ const marker = dataView.getUint8(start);
403
+ const type = dataView.getUint8(start + 1);
404
+ if (marker !== 0xFF) break;
405
+
406
+ // SOS (Start of Scan)—the rest is image data, keep it all
407
+ if (type === 0xDA) {
408
+ pieces.push(new Uint8Array(arrayBuffer, start));
409
+ break;
410
+ }
411
+ if (start + 3 >= byteLength) break;
412
+ const segmentLength = dataView.getUint16(start + 2);
413
+ if (segmentLength < 2) break;
414
+ const segmentEnd = start + 2 + segmentLength;
415
+ if (segmentEnd > byteLength) break;
416
+
417
+ // Skip APP1 (EXIF) segments, keep everything else
418
+ if (type !== 0xE1) {
419
+ pieces.push(new Uint8Array(arrayBuffer, start, segmentEnd - start));
420
+ }
421
+ start = segmentEnd;
422
+ }
423
+ const totalLength = pieces.reduce((sum, piece) => sum + piece.length, 0);
424
+ const result = new Uint8Array(totalLength);
425
+ let offset = 0;
426
+ for (const piece of pieces) {
427
+ result.set(piece, offset);
428
+ offset += piece.length;
429
+ }
430
+ return result;
431
+ }
343
432
  const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
344
433
 
345
434
  /**
@@ -500,6 +589,7 @@
500
589
  };
501
590
  this.mimeTypeSet = options && options.mimeType && isImageType(options.mimeType);
502
591
  this.aborted = false;
592
+ this.canvasFallback = false;
503
593
  this.result = null;
504
594
  this.url = null;
505
595
  this.init();
@@ -526,6 +616,58 @@
526
616
  options.checkOrientation = false;
527
617
  options.retainExif = false;
528
618
  }
619
+ if (!isCanvasReliable()) {
620
+ // Canvas is unreliable (e.g., Firefox fingerprinting resistance)—
621
+ // bypass canvas to avoid corrupted output
622
+ console.warn('Compressor.js Next: Canvas data is unreliable (e.g., due to browser fingerprinting resistance)—compression, resizing, and format conversion are unavailable');
623
+ this.canvasFallback = true;
624
+ if (mimeType === 'image/jpeg' && !options.retainExif) {
625
+ // Strip EXIF data directly from the binary to preserve privacy
626
+ const reader = new FileReader();
627
+ this.reader = reader;
628
+ reader.onload = ({
629
+ target
630
+ }) => {
631
+ if (this.aborted) return;
632
+ let result;
633
+ try {
634
+ const stripped = stripExif(target.result);
635
+ result = uint8ArrayToBlob(stripped, mimeType);
636
+ } catch {
637
+ this.fail(new Error('Failed to process the image data.'));
638
+ return;
639
+ }
640
+ const date = new Date();
641
+ result.name = file.name;
642
+ result.lastModified = date.getTime();
643
+ this.result = result;
644
+ if (options.success) {
645
+ options.success.call(this, result);
646
+ }
647
+ };
648
+ reader.onabort = () => {
649
+ this.fail(new Error('Aborted to read the image with FileReader.'));
650
+ };
651
+ reader.onerror = () => {
652
+ this.fail(new Error('Failed to read the image with FileReader.'));
653
+ };
654
+ reader.onloadend = () => {
655
+ this.reader = null;
656
+ };
657
+ reader.readAsArrayBuffer(file);
658
+ } else {
659
+ // Non-JPEG: No EXIF to strip, return as-is
660
+ // Defer callback to match the normal async flow
661
+ Promise.resolve().then(() => {
662
+ if (this.aborted) return;
663
+ this.result = file;
664
+ if (options.success) {
665
+ options.success.call(this, file);
666
+ }
667
+ });
668
+ }
669
+ return;
670
+ }
529
671
  const isJPEGImage = mimeType === 'image/jpeg';
530
672
  const checkOrientation = isJPEGImage && options.checkOrientation;
531
673
  const retainExif = isJPEGImage && options.retainExif;
@@ -691,11 +833,7 @@
691
833
  const destHeight = height;
692
834
  const params = [];
693
835
  if (resizable) {
694
- let srcX = 0;
695
- let srcY = 0;
696
- let srcWidth = naturalWidth;
697
- let srcHeight = naturalHeight;
698
- ({
836
+ const {
699
837
  width: srcWidth,
700
838
  height: srcHeight
701
839
  } = getAdjustedSizes({
@@ -705,9 +843,9 @@
705
843
  }, {
706
844
  contain: 'cover',
707
845
  cover: 'contain'
708
- }[options.resize]));
709
- srcX = (naturalWidth - srcWidth) / 2;
710
- srcY = (naturalHeight - srcHeight) / 2;
846
+ }[options.resize]);
847
+ const srcX = (naturalWidth - srcWidth) / 2;
848
+ const srcY = (naturalHeight - srcHeight) / 2;
711
849
  params.push(srcX, srcY, srcWidth, srcHeight);
712
850
  }
713
851
  params.push(destX, destY, destWidth, destHeight);
@@ -811,7 +949,6 @@
811
949
  } else {
812
950
  const date = new Date();
813
951
  result.lastModified = date.getTime();
814
- result.lastModifiedDate = date;
815
952
  result.name = file.name;
816
953
 
817
954
  // Convert the extension to match its type
@@ -821,6 +958,7 @@
821
958
  }
822
959
  } else {
823
960
  // Returns original file if the result is null in some cases
961
+ console.warn('Compressor.js Next: Canvas produced no output—returning the original image');
824
962
  result = file;
825
963
  }
826
964
  this.result = result;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Compressor.js Next v1.1.0
2
+ * Compressor.js Next v1.1.2
3
3
  * https://github.com/j9t/compressorjs-next
4
4
  *
5
5
  * Copyright 2018–2024 Chen Fengyuan
@@ -7,4 +7,4 @@
7
7
  *
8
8
  * Released under the MIT license.
9
9
  */
10
- !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Compressor=t()}(this,function(){"use strict";var e={strict:!0,checkOrientation:!0,retainExif:!1,maxWidth:1/0,maxHeight:1/0,minWidth:0,minHeight:0,width:void 0,height:void 0,resize:"none",quality:.8,mimeType:"auto",convertTypes:[],convertSize:5e6,beforeDraw:null,drew:null,success:null,error:null};const t="undefined"!=typeof window&&void 0!==window.document?window:{},i=e=>e>0&&e<1/0,r=/^image\/.+$/;function n(e){return r.test(e)}const{fromCharCode:a}=String;const{btoa:o}=t;function s(e){const t=new DataView(e);let i;try{let e,r,n;if(255===t.getUint8(0)&&216===t.getUint8(1)){const e=t.byteLength;let i=2;for(;i+1<e;){if(255===t.getUint8(i)&&225===t.getUint8(i+1)){r=i;break}i+=1}}if(r){const i=r+10;if("Exif"===function(e,t,i){let r,n="";for(i+=t,r=t;r<i;r+=1)n+=a(e.getUint8(r));return n}(t,r+4,4)){const r=t.getUint16(i);if(e=18761===r,(e||19789===r)&&42===t.getUint16(i+2,e)){const r=t.getUint32(i+4,e);r>=8&&(n=i+r)}}}if(n){const r=t.getUint16(n,e);let a,o;for(o=0;o<r;o+=1)if(a=n+12*o+2,274===t.getUint16(a,e)){a+=8,i=t.getUint16(a,e),t.setUint16(a,1,e);break}}}catch{i=1}return i}const h=/\.\d*(?:0|9){12}\d*$/;function l(e,t=1e11){return h.test(e)?Math.round(e*t)/t:e}function c({aspectRatio:e,height:t,width:r},n="none"){const a=i(r),o=i(t);if(a&&o){const i=t*e;("contain"===n||"none"===n)&&i>r||"cover"===n&&i<r?t=r/e:r=t*e}else a?t=r/e:o&&(r=t*e);return{width:r,height:t}}const{ArrayBuffer:d,FileReader:f}=t,u=t.URL||t.webkitURL,g=/\.\w+$/;return class{constructor(t,i){this.file=t,this.exif=[],this.image=new Image,this.options={...e,...i},this.mimeTypeSet=i&&i.mimeType&&n(i.mimeType),this.aborted=!1,this.result=null,this.url=null,this.init()}init(){const{file:e,options:t}=this;if(!(e instanceof Blob))return void this.fail(new Error("The first argument must be a File or Blob object."));const i=e.type;if(!n(i))return void this.fail(new Error("The first argument must be an image File or Blob object."));if(!u||!f)return void this.fail(new Error("The current browser does not support image compression."));d||(t.checkOrientation=!1,t.retainExif=!1);const r="image/jpeg"===i,h=r&&t.checkOrientation,l=r&&t.retainExif;if(!u||h||l){const t=new f;this.reader=t,t.onload=({target:t})=>{const{result:r}=t,n={};let c=1;h&&(c=s(r),c>1&&Object.assign(n,function(e){let t=0,i=1,r=1;switch(e){case 2:i=-1;break;case 3:t=-180;break;case 4:r=-1;break;case 5:t=90,r=-1;break;case 6:t=90;break;case 7:t=90,i=-1;break;case 8:t=-90}return{rotate:t,scaleX:i,scaleY:r}}(c))),l&&(this.exif=function(e){const t=new DataView(e),{byteLength:i}=t,r=[];let n=0;for(;n+3<i;){const e=t.getUint8(n),a=t.getUint8(n+1);if(255===e&&218===a)break;if(255===e&&216===a)n+=2;else{const o=n+t.getUint16(n+2)+2;if(255===e&&225===a)for(let e=n;e<o&&e<i;e+=1)r.push(t.getUint8(e));n=o}}return r}(r)),h||l?!u||c>1?n.url=function(e,t){const i=new Uint8Array(e),{length:r}=i;let n="";for(let e=0;e<r;e+=8192){const t=Math.min(e+8192,r);let o="";for(let r=e;r<t;r+=1)o+=a(i[r]);n+=o}return`data:${t};base64,${o(n)}`}(r,i):(this.url=u.createObjectURL(e),n.url=this.url):n.url=r,this.load(n)},t.onabort=()=>{this.fail(new Error("Aborted to read the image with FileReader."))},t.onerror=()=>{this.fail(new Error("Failed to read the image with FileReader."))},t.onloadend=()=>{this.reader=null},h||l?t.readAsArrayBuffer(e):t.readAsDataURL(e)}else this.url=u.createObjectURL(e),this.load({url:this.url})}load(e){const{file:i,image:r}=this;r.onload=()=>{this.draw({...e,naturalWidth:r.naturalWidth,naturalHeight:r.naturalHeight})},r.onabort=()=>{this.fail(new Error("Aborted to load the image."))},r.onerror=()=>{this.fail(new Error("Failed to load the image."))},t.navigator&&/(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(t.navigator.userAgent)&&(r.crossOrigin="anonymous"),r.alt=i.name,r.src=e.url}draw({naturalWidth:e,naturalHeight:t,rotate:r=0,scaleX:a=1,scaleY:o=1}){const{file:s,image:h,options:d}=this,u=document.createElement("canvas"),g=u.getContext("2d"),m=Math.abs(r)%180==90,w=("contain"===d.resize||"cover"===d.resize)&&i(d.width)&&i(d.height);let p=Math.max(d.maxWidth,0)||1/0,b=Math.max(d.maxHeight,0)||1/0,y=Math.max(d.minWidth,0)||0,U=Math.max(d.minHeight,0)||0,x=e/t,{width:v,height:T}=d;m&&([p,b]=[b,p],[y,U]=[U,y],[v,T]=[T,v]),w&&(x=v/T),({width:p,height:b}=c({aspectRatio:x,width:p,height:b},"contain")),({width:y,height:U}=c({aspectRatio:x,width:y,height:U},"cover")),w?({width:v,height:T}=c({aspectRatio:x,width:v,height:T},d.resize)):({width:v=e,height:T=t}=c({aspectRatio:x,width:v,height:T})),v=Math.floor(l(Math.min(Math.max(v,y),p))),T=Math.floor(l(Math.min(Math.max(T,U),b)));const E=-v/2,R=-T/2,k=v,M=T,A=[];if(w){let i=0,r=0,n=e,a=t;({width:n,height:a}=c({aspectRatio:x,width:e,height:t},{contain:"cover",cover:"contain"}[d.resize])),i=(e-n)/2,r=(t-a)/2,A.push(i,r,n,a)}A.push(E,R,k,M),m&&([v,T]=[T,v]),u.width=v,u.height=T,n(d.mimeType)||(d.mimeType=s.type);let j="transparent";!this.mimeTypeSet&&s.size>d.convertSize&&d.convertTypes.indexOf(d.mimeType)>=0&&(d.mimeType="image/jpeg");const B="image/jpeg"===d.mimeType;if(B&&(j="#fff"),g.fillStyle=j,g.fillRect(0,0,v,T),d.beforeDraw&&d.beforeDraw.call(this,g,u),this.aborted)return;if(g.save(),g.translate(v/2,T/2),g.rotate(r*Math.PI/180),g.scale(a,o),g.drawImage(h,...A),g.restore(),d.drew&&d.drew.call(this,g,u),this.aborted)return;u.toBlob(i=>{if(!this.aborted){const r=i=>this.done({naturalWidth:e,naturalHeight:t,result:i});if(i&&B&&d.retainExif&&this.exif&&this.exif.length>0){const e=e=>{const t=function(e,t){const i=new DataView(e),r=new Uint8Array(e);if(255!==i.getUint8(2)||224!==i.getUint8(3))return r;const n=4+i.getUint16(4),a=r.byteLength-n,o=new Uint8Array(2+t.length+a);o[0]=255,o[1]=216;for(let e=0;e<t.length;e+=1)o[2+e]=t[e];return o.set(r.subarray(n),2+t.length),o}(e,this.exif);var i,n;r((i=t,n=d.mimeType,new Blob([i],{type:n})))};if(i.arrayBuffer)i.arrayBuffer().then(e).catch(()=>{this.fail(new Error("Failed to read the compressed image with Blob.arrayBuffer()."))});else{const t=new f;this.reader=t,t.onload=({target:t})=>{e(t.result)},t.onabort=()=>{this.fail(new Error("Aborted to read the compressed image with FileReader."))},t.onerror=()=>{this.fail(new Error("Failed to read the compressed image with FileReader."))},t.onloadend=()=>{this.reader=null},t.readAsArrayBuffer(i)}}else r(i)}},d.mimeType,d.quality)}done({naturalWidth:e,naturalHeight:t,result:i}){const{file:r,options:a}=this;if(this.revokeUrl(),i)if(a.strict&&!a.retainExif&&i.size>r.size&&a.mimeType===r.type&&!(a.width>e||a.height>t||a.minWidth>e||a.minHeight>t||a.maxWidth<e||a.maxHeight<t))i=r;else{const e=new Date;i.lastModified=e.getTime(),i.lastModifiedDate=e,i.name=r.name,i.name&&i.type!==r.type&&(i.name=i.name.replace(g,function(e){let t=n(e)?e.slice(6):"";return"jpeg"===t&&(t="jpg"),`.${t}`}(i.type)))}else i=r;this.result=i,a.success&&a.success.call(this,i)}fail(e){const{options:t}=this;if(this.revokeUrl(),!t.error)throw e;t.error.call(this,e)}revokeUrl(){u&&this.url&&(u.revokeObjectURL(this.url),this.url=null)}abort(){this.aborted||(this.aborted=!0,this.reader?this.reader.abort():this.image.complete?this.fail(new Error("The compression process has been aborted.")):(this.image.onload=null,this.image.onerror=null,this.image.onabort=null,this.fail(new Error("Aborted to load the image."))))}static setDefaults(t){Object.assign(e,t)}}});
10
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Compressor=t()}(this,function(){"use strict";var e={strict:!0,checkOrientation:!0,retainExif:!1,maxWidth:1/0,maxHeight:1/0,minWidth:0,minHeight:0,width:void 0,height:void 0,resize:"none",quality:.8,mimeType:"auto",convertTypes:[],convertSize:5e6,beforeDraw:null,drew:null,success:null,error:null};const t="undefined"!=typeof window&&void 0!==window.document?window:{},i=e=>e>0&&e<1/0,r=/^image\/.+$/;function a(e){return r.test(e)}const{fromCharCode:n}=String;const{btoa:o}=t;function s(e){const t=new DataView(e);let i;try{let e,r,a;if(255===t.getUint8(0)&&216===t.getUint8(1)){const e=t.byteLength;let i=2;for(;i+1<e;){if(255===t.getUint8(i)&&225===t.getUint8(i+1)){r=i;break}i+=1}}if(r){const i=r+10;if("Exif"===function(e,t,i){let r,a="";for(i+=t,r=t;r<i;r+=1)a+=n(e.getUint8(r));return a}(t,r+4,4)){const r=t.getUint16(i);if(e=18761===r,(e||19789===r)&&42===t.getUint16(i+2,e)){const r=t.getUint32(i+4,e);r>=8&&(a=i+r)}}}if(a){const r=t.getUint16(a,e);let n,o;for(o=0;o<r;o+=1)if(n=a+12*o+2,274===t.getUint16(n,e)){n+=8,i=t.getUint16(n,e),t.setUint16(n,1,e);break}}}catch{i=1}return i}let h;const l=/\.\d*(?:0|9){12}\d*$/;function c(e,t=1e11){return l.test(e)?Math.round(e*t)/t:e}function d({aspectRatio:e,height:t,width:r},a="none"){const n=i(r),o=i(t);if(n&&o){const i=t*e;("contain"===a||"none"===a)&&i>r||"cover"===a&&i<r?t=r/e:r=t*e}else n?t=r/e:o&&(r=t*e);return{width:r,height:t}}function f(e,t){return new Blob([e],{type:t})}const{ArrayBuffer:u,FileReader:g}=t,m=t.URL||t.webkitURL,w=/\.\w+$/;return class{constructor(t,i){this.file=t,this.exif=[],this.image=new Image,this.options={...e,...i},this.mimeTypeSet=i&&i.mimeType&&a(i.mimeType),this.aborted=!1,this.canvasFallback=!1,this.result=null,this.url=null,this.init()}init(){const{file:e,options:t}=this;if(!(e instanceof Blob))return void this.fail(new Error("The first argument must be a File or Blob object."));const i=e.type;if(!a(i))return void this.fail(new Error("The first argument must be an image File or Blob object."));if(!m||!g)return void this.fail(new Error("The current browser does not support image compression."));if(u||(t.checkOrientation=!1,t.retainExif=!1),!function(){if(void 0!==h)return h;try{const e=document.createElement("canvas");e.width=4,e.height=4;const t=e.getContext("2d"),i=t.createImageData(e.width,e.height);for(let e=0;e<i.data.length;e+=4)i.data[e]=e,i.data[e+1]=1,i.data[e+2]=2,i.data[e+3]=255;t.putImageData(i,0,0);const r=t.getImageData(0,0,e.width,e.height);h=r.data.every((e,t)=>{const i=t%4;return 0===i?e===(255&t):1===i?1===e:2===i?2===e:255===e})}catch{h=!1}return h}()){if(console.warn("Compressor.js Next: Canvas data is unreliable (e.g., due to browser fingerprinting resistance)—compression, resizing, and format conversion are unavailable"),this.canvasFallback=!0,"image/jpeg"!==i||t.retainExif)Promise.resolve().then(()=>{this.aborted||(this.result=e,t.success&&t.success.call(this,e))});else{const r=new g;this.reader=r,r.onload=({target:r})=>{if(this.aborted)return;let a;try{const e=function(e){const t=new DataView(e),{byteLength:i}=t,r=[];if(i<4||255!==t.getUint8(0)||216!==t.getUint8(1))return new Uint8Array(e);r.push(new Uint8Array(e,0,2));let a=2;for(;a+3<i;){const n=t.getUint8(a),o=t.getUint8(a+1);if(255!==n)break;if(218===o){r.push(new Uint8Array(e,a));break}if(a+3>=i)break;const s=t.getUint16(a+2);if(s<2)break;const h=a+2+s;if(h>i)break;225!==o&&r.push(new Uint8Array(e,a,h-a)),a=h}const n=r.reduce((e,t)=>e+t.length,0),o=new Uint8Array(n);let s=0;for(const e of r)o.set(e,s),s+=e.length;return o}(r.result);a=f(e,i)}catch{return void this.fail(new Error("Failed to process the image data."))}const n=new Date;a.name=e.name,a.lastModified=n.getTime(),this.result=a,t.success&&t.success.call(this,a)},r.onabort=()=>{this.fail(new Error("Aborted to read the image with FileReader."))},r.onerror=()=>{this.fail(new Error("Failed to read the image with FileReader."))},r.onloadend=()=>{this.reader=null},r.readAsArrayBuffer(e)}return}const r="image/jpeg"===i,l=r&&t.checkOrientation,c=r&&t.retainExif;if(!m||l||c){const t=new g;this.reader=t,t.onload=({target:t})=>{const{result:r}=t,a={};let h=1;l&&(h=s(r),h>1&&Object.assign(a,function(e){let t=0,i=1,r=1;switch(e){case 2:i=-1;break;case 3:t=-180;break;case 4:r=-1;break;case 5:t=90,r=-1;break;case 6:t=90;break;case 7:t=90,i=-1;break;case 8:t=-90}return{rotate:t,scaleX:i,scaleY:r}}(h))),c&&(this.exif=function(e){const t=new DataView(e),{byteLength:i}=t,r=[];let a=0;for(;a+3<i;){const e=t.getUint8(a),n=t.getUint8(a+1);if(255===e&&218===n)break;if(255===e&&216===n)a+=2;else{const o=a+t.getUint16(a+2)+2;if(255===e&&225===n)for(let e=a;e<o&&e<i;e+=1)r.push(t.getUint8(e));a=o}}return r}(r)),l||c?!m||h>1?a.url=function(e,t){const i=new Uint8Array(e),{length:r}=i;let a="";for(let e=0;e<r;e+=8192){const t=Math.min(e+8192,r);let o="";for(let r=e;r<t;r+=1)o+=n(i[r]);a+=o}return`data:${t};base64,${o(a)}`}(r,i):(this.url=m.createObjectURL(e),a.url=this.url):a.url=r,this.load(a)},t.onabort=()=>{this.fail(new Error("Aborted to read the image with FileReader."))},t.onerror=()=>{this.fail(new Error("Failed to read the image with FileReader."))},t.onloadend=()=>{this.reader=null},l||c?t.readAsArrayBuffer(e):t.readAsDataURL(e)}else this.url=m.createObjectURL(e),this.load({url:this.url})}load(e){const{file:i,image:r}=this;r.onload=()=>{this.draw({...e,naturalWidth:r.naturalWidth,naturalHeight:r.naturalHeight})},r.onabort=()=>{this.fail(new Error("Aborted to load the image."))},r.onerror=()=>{this.fail(new Error("Failed to load the image."))},t.navigator&&/(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(t.navigator.userAgent)&&(r.crossOrigin="anonymous"),r.alt=i.name,r.src=e.url}draw({naturalWidth:e,naturalHeight:t,rotate:r=0,scaleX:n=1,scaleY:o=1}){const{file:s,image:h,options:l}=this,u=document.createElement("canvas"),m=u.getContext("2d"),w=Math.abs(r)%180==90,p=("contain"===l.resize||"cover"===l.resize)&&i(l.width)&&i(l.height);let b=Math.max(l.maxWidth,0)||1/0,y=Math.max(l.maxHeight,0)||1/0,U=Math.max(l.minWidth,0)||0,v=Math.max(l.minHeight,0)||0,x=e/t,{width:k,height:E}=l;w&&([b,y]=[y,b],[U,v]=[v,U],[k,E]=[E,k]),p&&(x=k/E),({width:b,height:y}=d({aspectRatio:x,width:b,height:y},"contain")),({width:U,height:v}=d({aspectRatio:x,width:U,height:v},"cover")),p?({width:k,height:E}=d({aspectRatio:x,width:k,height:E},l.resize)):({width:k=e,height:E=t}=d({aspectRatio:x,width:k,height:E})),k=Math.floor(c(Math.min(Math.max(k,U),b))),E=Math.floor(c(Math.min(Math.max(E,v),y)));const A=-k/2,T=-E/2,R=k,F=E,j=[];if(p){const{width:i,height:r}=d({aspectRatio:x,width:e,height:t},{contain:"cover",cover:"contain"}[l.resize]),a=(e-i)/2,n=(t-r)/2;j.push(a,n,i,r)}j.push(A,T,R,F),w&&([k,E]=[E,k]),u.width=k,u.height=E,a(l.mimeType)||(l.mimeType=s.type);let M="transparent";!this.mimeTypeSet&&s.size>l.convertSize&&l.convertTypes.indexOf(l.mimeType)>=0&&(l.mimeType="image/jpeg");const D="image/jpeg"===l.mimeType;if(D&&(M="#fff"),m.fillStyle=M,m.fillRect(0,0,k,E),l.beforeDraw&&l.beforeDraw.call(this,m,u),this.aborted)return;if(m.save(),m.translate(k/2,E/2),m.rotate(r*Math.PI/180),m.scale(n,o),m.drawImage(h,...j),m.restore(),l.drew&&l.drew.call(this,m,u),this.aborted)return;u.toBlob(i=>{if(!this.aborted){const r=i=>this.done({naturalWidth:e,naturalHeight:t,result:i});if(i&&D&&l.retainExif&&this.exif&&this.exif.length>0){const e=e=>{const t=function(e,t){const i=new DataView(e),r=new Uint8Array(e);if(255!==i.getUint8(2)||224!==i.getUint8(3))return r;const a=4+i.getUint16(4),n=r.byteLength-a,o=new Uint8Array(2+t.length+n);o[0]=255,o[1]=216;for(let e=0;e<t.length;e+=1)o[2+e]=t[e];return o.set(r.subarray(a),2+t.length),o}(e,this.exif);r(f(t,l.mimeType))};if(i.arrayBuffer)i.arrayBuffer().then(e).catch(()=>{this.fail(new Error("Failed to read the compressed image with Blob.arrayBuffer()."))});else{const t=new g;this.reader=t,t.onload=({target:t})=>{e(t.result)},t.onabort=()=>{this.fail(new Error("Aborted to read the compressed image with FileReader."))},t.onerror=()=>{this.fail(new Error("Failed to read the compressed image with FileReader."))},t.onloadend=()=>{this.reader=null},t.readAsArrayBuffer(i)}}else r(i)}},l.mimeType,l.quality)}done({naturalWidth:e,naturalHeight:t,result:i}){const{file:r,options:n}=this;if(this.revokeUrl(),i)if(n.strict&&!n.retainExif&&i.size>r.size&&n.mimeType===r.type&&!(n.width>e||n.height>t||n.minWidth>e||n.minHeight>t||n.maxWidth<e||n.maxHeight<t))i=r;else{const e=new Date;i.lastModified=e.getTime(),i.name=r.name,i.name&&i.type!==r.type&&(i.name=i.name.replace(w,function(e){let t=a(e)?e.slice(6):"";return"jpeg"===t&&(t="jpg"),`.${t}`}(i.type)))}else console.warn("Compressor.js Next: Canvas produced no output—returning the original image"),i=r;this.result=i,n.success&&n.success.call(this,i)}fail(e){const{options:t}=this;if(this.revokeUrl(),!t.error)throw e;t.error.call(this,e)}revokeUrl(){m&&this.url&&(m.revokeObjectURL(this.url),this.url=null)}abort(){this.aborted||(this.aborted=!0,this.reader?this.reader.abort():this.image.complete?this.fail(new Error("The compression process has been aborted.")):(this.image.onload=null,this.image.onerror=null,this.image.onabort=null,this.fail(new Error("Aborted to load the image."))))}static setDefaults(t){Object.assign(e,t)}}});
package/package.json CHANGED
@@ -17,14 +17,14 @@
17
17
  "devDependencies": {
18
18
  "@babel/core": "^7.29.0",
19
19
  "@babel/preset-env": "^7.29.0",
20
- "@eslint/js": "^9.39.2",
20
+ "@eslint/js": "^10.0.0",
21
21
  "@rollup/plugin-babel": "^6.1.0",
22
22
  "@rollup/plugin-commonjs": "^29.0.0",
23
23
  "@rollup/plugin-node-resolve": "^16.0.3",
24
24
  "@vitest/browser-playwright": "^4.0.18",
25
25
  "@vitest/coverage-istanbul": "^4.0.18",
26
26
  "del-cli": "^7.0.0",
27
- "eslint": "^9.39.2",
27
+ "eslint": "^10.0.0",
28
28
  "husky": "^9.1.7",
29
29
  "playwright": "^1.58.2",
30
30
  "rollup": "^4.57.1",
@@ -36,6 +36,7 @@
36
36
  },
37
37
  "exports": {
38
38
  ".": {
39
+ "types": "./types/index.d.ts",
39
40
  "import": "./dist/compressor.esm.js",
40
41
  "require": "./dist/compressor.common.js"
41
42
  }
@@ -92,5 +93,5 @@
92
93
  "sideEffects": false,
93
94
  "type": "module",
94
95
  "types": "types/index.d.ts",
95
- "version": "1.1.0"
96
+ "version": "1.1.2"
96
97
  }
package/src/defaults.js CHANGED
@@ -81,8 +81,9 @@ export default {
81
81
  mimeType: 'auto',
82
82
 
83
83
  /**
84
- * Files whose file type is included in this list,
85
- * and whose file size exceeds the `convertSize` value will be converted to JPEG.
84
+ * Files whose file type is included in this list and
85
+ * whose file size exceeds the `convertSize` value
86
+ * will be converted to JPEG.
86
87
  * @type {string|Array}
87
88
  */
88
89
  convertTypes: [],
@@ -90,7 +91,6 @@ export default {
90
91
  /**
91
92
  * Files over this size (5 MB by default) whose type is in `convertTypes`
92
93
  * will be converted to JPEG.
93
- * To disable this, set `convertTypes` to `[]` or the value to `Infinity`.
94
94
  * @type {number}
95
95
  */
96
96
  convertSize: 5000000,
package/src/index.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  import {
6
6
  getAdjustedSizes,
7
7
  imageTypeToExtension,
8
+ isCanvasReliable,
8
9
  isImageType,
9
10
  isPositiveNumber,
10
11
  normalizeDecimalNumber,
@@ -13,6 +14,7 @@ import {
13
14
  arrayBufferToDataURL,
14
15
  getExif,
15
16
  insertExif,
17
+ stripExif,
16
18
  uint8ArrayToBlob,
17
19
  } from './utilities';
18
20
 
@@ -40,6 +42,7 @@ export default class Compressor {
40
42
  };
41
43
  this.mimeTypeSet = options && options.mimeType && isImageType(options.mimeType);
42
44
  this.aborted = false;
45
+ this.canvasFallback = false;
43
46
  this.result = null;
44
47
  this.url = null;
45
48
  this.init();
@@ -70,6 +73,68 @@ export default class Compressor {
70
73
  options.retainExif = false;
71
74
  }
72
75
 
76
+ if (!isCanvasReliable()) {
77
+ // Canvas is unreliable (e.g., Firefox fingerprinting resistance)—
78
+ // bypass canvas to avoid corrupted output
79
+ console.warn('Compressor.js Next: Canvas data is unreliable (e.g., due to browser fingerprinting resistance)—compression, resizing, and format conversion are unavailable');
80
+ this.canvasFallback = true;
81
+ if (mimeType === 'image/jpeg' && !options.retainExif) {
82
+ // Strip EXIF data directly from the binary to preserve privacy
83
+ const reader = new FileReader();
84
+
85
+ this.reader = reader;
86
+ reader.onload = ({ target }) => {
87
+ if (this.aborted) return;
88
+
89
+ let result;
90
+
91
+ try {
92
+ const stripped = stripExif(target.result);
93
+
94
+ result = uint8ArrayToBlob(stripped, mimeType);
95
+ } catch {
96
+ this.fail(new Error('Failed to process the image data.'));
97
+ return;
98
+ }
99
+
100
+ const date = new Date();
101
+
102
+ result.name = file.name;
103
+ result.lastModified = date.getTime();
104
+
105
+ this.result = result;
106
+
107
+ if (options.success) {
108
+ options.success.call(this, result);
109
+ }
110
+ };
111
+ reader.onabort = () => {
112
+ this.fail(new Error('Aborted to read the image with FileReader.'));
113
+ };
114
+ reader.onerror = () => {
115
+ this.fail(new Error('Failed to read the image with FileReader.'));
116
+ };
117
+ reader.onloadend = () => {
118
+ this.reader = null;
119
+ };
120
+ reader.readAsArrayBuffer(file);
121
+ } else {
122
+ // Non-JPEG: No EXIF to strip, return as-is
123
+ // Defer callback to match the normal async flow
124
+ Promise.resolve().then(() => {
125
+ if (this.aborted) return;
126
+
127
+ this.result = file;
128
+
129
+ if (options.success) {
130
+ options.success.call(this, file);
131
+ }
132
+ });
133
+ }
134
+
135
+ return;
136
+ }
137
+
73
138
  const isJPEGImage = mimeType === 'image/jpeg';
74
139
  const checkOrientation = isJPEGImage && options.checkOrientation;
75
140
  const retainExif = isJPEGImage && options.retainExif;
@@ -230,21 +295,16 @@ export default class Compressor {
230
295
  const params = [];
231
296
 
232
297
  if (resizable) {
233
- let srcX = 0;
234
- let srcY = 0;
235
- let srcWidth = naturalWidth;
236
- let srcHeight = naturalHeight;
237
-
238
- ({ width: srcWidth, height: srcHeight } = getAdjustedSizes({
298
+ const { width: srcWidth, height: srcHeight } = getAdjustedSizes({
239
299
  aspectRatio,
240
300
  width: naturalWidth,
241
301
  height: naturalHeight,
242
302
  }, {
243
303
  contain: 'cover',
244
304
  cover: 'contain',
245
- }[options.resize]));
246
- srcX = (naturalWidth - srcWidth) / 2;
247
- srcY = (naturalHeight - srcHeight) / 2;
305
+ }[options.resize]);
306
+ const srcX = (naturalWidth - srcWidth) / 2;
307
+ const srcY = (naturalHeight - srcHeight) / 2;
248
308
 
249
309
  params.push(srcX, srcY, srcWidth, srcHeight);
250
310
  }
@@ -379,7 +439,6 @@ export default class Compressor {
379
439
  const date = new Date();
380
440
 
381
441
  result.lastModified = date.getTime();
382
- result.lastModifiedDate = date;
383
442
  result.name = file.name;
384
443
 
385
444
  // Convert the extension to match its type
@@ -392,6 +451,7 @@ export default class Compressor {
392
451
  }
393
452
  } else {
394
453
  // Returns original file if the result is null in some cases
454
+ console.warn('Compressor.js Next: Canvas produced no output—returning the original image');
395
455
  result = file;
396
456
  }
397
457
 
package/src/utilities.js CHANGED
@@ -222,6 +222,124 @@ export function parseOrientation(orientation) {
222
222
  };
223
223
  }
224
224
 
225
+ let cachedCanvasReliable;
226
+
227
+ /**
228
+ * Check if the browser’s canvas produces reliable pixel data.
229
+ * Returns `false` when anti-fingerprinting measures (e.g., Firefox’s
230
+ * `privacy.resistFingerprinting`) add noise to canvas output.
231
+ * The result is cached after the first call.
232
+ * @returns {boolean} Returns `true` if canvas data is reliable.
233
+ */
234
+ export function isCanvasReliable() {
235
+ if (cachedCanvasReliable !== undefined) return cachedCanvasReliable;
236
+
237
+ try {
238
+ const canvas = document.createElement('canvas');
239
+
240
+ canvas.width = 4;
241
+ canvas.height = 4;
242
+
243
+ const ctx = canvas.getContext('2d');
244
+ const imageData = ctx.createImageData(canvas.width, canvas.height);
245
+
246
+ for (let i = 0; i < imageData.data.length; i += 4) {
247
+ imageData.data[i] = i;
248
+ imageData.data[i + 1] = 1;
249
+ imageData.data[i + 2] = 2;
250
+ imageData.data[i + 3] = 255;
251
+ }
252
+
253
+ ctx.putImageData(imageData, 0, 0);
254
+
255
+ const result = ctx.getImageData(0, 0, canvas.width, canvas.height);
256
+
257
+ cachedCanvasReliable = result.data.every((value, index) => {
258
+ const channel = index % 4;
259
+
260
+ if (channel === 0) return value === (index & 0xFF);
261
+ if (channel === 1) return value === 1;
262
+ if (channel === 2) return value === 2;
263
+ return value === 255;
264
+ });
265
+ } catch {
266
+ cachedCanvasReliable = false;
267
+ }
268
+
269
+ return cachedCanvasReliable;
270
+ }
271
+
272
+ /**
273
+ * Reset the cached canvas reliability result.
274
+ * Intended for testing only.
275
+ */
276
+ export function resetCanvasReliableCache() {
277
+ cachedCanvasReliable = undefined;
278
+ }
279
+
280
+ /**
281
+ * Strip all APP1 (EXIF) segments from a JPEG array buffer.
282
+ * @param {ArrayBuffer} arrayBuffer - The JPEG data to strip.
283
+ * @returns {Uint8Array} The JPEG data without EXIF segments.
284
+ */
285
+ export function stripExif(arrayBuffer) {
286
+ const dataView = new DataView(arrayBuffer);
287
+ const { byteLength } = dataView;
288
+ const pieces = [];
289
+
290
+ // Only handle JPEG data (starts with SOI marker FF D8)
291
+ if (byteLength < 4
292
+ || dataView.getUint8(0) !== 0xFF
293
+ || dataView.getUint8(1) !== 0xD8) {
294
+ return new Uint8Array(arrayBuffer);
295
+ }
296
+
297
+ // Keep SOI marker
298
+ pieces.push(new Uint8Array(arrayBuffer, 0, 2));
299
+ let start = 2;
300
+
301
+ while (start + 3 < byteLength) {
302
+ const marker = dataView.getUint8(start);
303
+ const type = dataView.getUint8(start + 1);
304
+
305
+ if (marker !== 0xFF) break;
306
+
307
+ // SOS (Start of Scan)—the rest is image data, keep it all
308
+ if (type === 0xDA) {
309
+ pieces.push(new Uint8Array(arrayBuffer, start));
310
+ break;
311
+ }
312
+
313
+ if (start + 3 >= byteLength) break;
314
+
315
+ const segmentLength = dataView.getUint16(start + 2);
316
+
317
+ if (segmentLength < 2) break;
318
+
319
+ const segmentEnd = start + 2 + segmentLength;
320
+
321
+ if (segmentEnd > byteLength) break;
322
+
323
+ // Skip APP1 (EXIF) segments, keep everything else
324
+ if (type !== 0xE1) {
325
+ pieces.push(new Uint8Array(arrayBuffer, start, segmentEnd - start));
326
+ }
327
+
328
+ start = segmentEnd;
329
+ }
330
+
331
+ const totalLength = pieces.reduce((sum, piece) => sum + piece.length, 0);
332
+ const result = new Uint8Array(totalLength);
333
+ let offset = 0;
334
+
335
+ for (const piece of pieces) {
336
+ result.set(piece, offset);
337
+ offset += piece.length;
338
+ }
339
+
340
+ return result;
341
+ }
342
+
225
343
  const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
226
344
 
227
345
  /**