compressorjs-next 1.0.2 → 1.1.1

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
@@ -22,7 +22,12 @@ A JavaScript image compressor and converter. Uses the browser’s native [HTMLCa
22
22
 
23
23
  Change the package name from `compressorjs` to `compressorjs-next` in your `package.json` and imports (`import Compressor from 'compressorjs-next'`).
24
24
 
25
- The API is otherwise the same, with these exceptions: ESM is now the default module format (CommonJS is still supported), the `noConflict()` method has been removed, and Internet Explorer is no longer supported.
25
+ The API is otherwise the same, with these exceptions (as of 1.1.0—follow [the changelog](https://github.com/j9t/compressorjs-next/blob/main/CHANGELOG.md) from there):
26
+
27
+ * ESM is now the default module format (CommonJS is still supported)
28
+ * The `noConflict()` method has been removed
29
+ * The default for `convertTypes` has changed from `['image/png']` to `[]`
30
+ * Internet Explorer is no longer supported
26
31
 
27
32
  ## Main files
28
33
 
@@ -235,21 +240,21 @@ The [MIME type](https://webglossary.info/terms/mime-type/) of the output image.
235
240
  ### `convertTypes`
236
241
 
237
242
  * Type: `Array` or `string` (multiple types should be separated by commas)
238
- * Default: `["image/png"]`
243
+ * Default: `[]`
239
244
  * Examples:
240
245
  - `["image/png", "image/webp"]`
241
246
  - `"image/png,image/webp"`
242
247
 
243
- Files whose file type is included in this list, and whose file size exceeds the `convertSize` value will be converted to JPEGs.
248
+ Files whose file type is included in this list and whose file size exceeds the `convertSize` value will be converted to JPEG.
244
249
 
245
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).
246
251
 
247
252
  ### `convertSize`
248
253
 
249
254
  * Type: `number`
250
- * Default: `5000000` (5 MB)
255
+ * Default: `5000000` (5 MB)
251
256
 
252
- Files whose file type is included in the `convertTypes` list, and whose file size exceeds this value will be converted to JPEGs. 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`.
253
258
 
254
259
  **Examples:**
255
260
 
@@ -333,3 +338,5 @@ compressor.abort();
333
338
  ## Browser support
334
339
 
335
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.0.2
2
+ * Compressor.js Next v1.1.1
3
3
  * https://github.com/j9t/compressorjs-next
4
4
  *
5
5
  * Copyright 2018–2024 Chen Fengyuan
@@ -80,14 +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 JPEGs.
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
- convertTypes: ['image/png'],
88
+ convertTypes: [],
88
89
  /**
89
- * PNG files over this size (5 MB by default) will be converted to JPEGs.
90
- * To disable this, just set the value to `Infinity`.
90
+ * Files over this size (5 MB by default) whose type is in `convertTypes`
91
+ * will be converted to JPEG.
91
92
  * @type {number}
92
93
  */
93
94
  convertSize: 5000000,
@@ -335,6 +336,95 @@ function parseOrientation(orientation) {
335
336
  scaleY
336
337
  };
337
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
+ }
338
428
  const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
339
429
 
340
430
  /**
@@ -493,7 +583,9 @@ class Compressor {
493
583
  ...DEFAULTS,
494
584
  ...options
495
585
  };
586
+ this.mimeTypeSet = options && options.mimeType && isImageType(options.mimeType);
496
587
  this.aborted = false;
588
+ this.canvasFallback = false;
497
589
  this.result = null;
498
590
  this.url = null;
499
591
  this.init();
@@ -520,6 +612,58 @@ class Compressor {
520
612
  options.checkOrientation = false;
521
613
  options.retainExif = false;
522
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
+ }
523
667
  const isJPEGImage = mimeType === 'image/jpeg';
524
668
  const checkOrientation = isJPEGImage && options.checkOrientation;
525
669
  const retainExif = isJPEGImage && options.retainExif;
@@ -685,11 +829,7 @@ class Compressor {
685
829
  const destHeight = height;
686
830
  const params = [];
687
831
  if (resizable) {
688
- let srcX = 0;
689
- let srcY = 0;
690
- let srcWidth = naturalWidth;
691
- let srcHeight = naturalHeight;
692
- ({
832
+ const {
693
833
  width: srcWidth,
694
834
  height: srcHeight
695
835
  } = getAdjustedSizes({
@@ -699,9 +839,9 @@ class Compressor {
699
839
  }, {
700
840
  contain: 'cover',
701
841
  cover: 'contain'
702
- }[options.resize]));
703
- srcX = (naturalWidth - srcWidth) / 2;
704
- srcY = (naturalHeight - srcHeight) / 2;
842
+ }[options.resize]);
843
+ const srcX = (naturalWidth - srcWidth) / 2;
844
+ const srcY = (naturalHeight - srcHeight) / 2;
705
845
  params.push(srcX, srcY, srcWidth, srcHeight);
706
846
  }
707
847
  params.push(destX, destY, destWidth, destHeight);
@@ -715,8 +855,9 @@ class Compressor {
715
855
  }
716
856
  let fillStyle = 'transparent';
717
857
 
718
- // Converts PNG files over the `convertSize` to JPEGs.
719
- if (file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) {
858
+ // Converts files over the `convertSize` to JPEG,
859
+ // unless the user explicitly set a `mimeType`.
860
+ if (!this.mimeTypeSet && file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) {
720
861
  options.mimeType = 'image/jpeg';
721
862
  }
722
863
  const isJPEGImage = options.mimeType === 'image/jpeg';
@@ -804,7 +945,6 @@ class Compressor {
804
945
  } else {
805
946
  const date = new Date();
806
947
  result.lastModified = date.getTime();
807
- result.lastModifiedDate = date;
808
948
  result.name = file.name;
809
949
 
810
950
  // Convert the extension to match its type
@@ -814,6 +954,7 @@ class Compressor {
814
954
  }
815
955
  } else {
816
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');
817
958
  result = file;
818
959
  }
819
960
  this.result = result;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Compressor.js Next v1.0.2
2
+ * Compressor.js Next v1.1.1
3
3
  * https://github.com/j9t/compressorjs-next
4
4
  *
5
5
  * Copyright 2018–2024 Chen Fengyuan
@@ -78,14 +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 JPEGs.
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
- convertTypes: ['image/png'],
86
+ convertTypes: [],
86
87
  /**
87
- * PNG files over this size (5 MB by default) will be converted to JPEGs.
88
- * To disable this, just set the value to `Infinity`.
88
+ * Files over this size (5 MB by default) whose type is in `convertTypes`
89
+ * will be converted to JPEG.
89
90
  * @type {number}
90
91
  */
91
92
  convertSize: 5000000,
@@ -333,6 +334,95 @@ function parseOrientation(orientation) {
333
334
  scaleY
334
335
  };
335
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
+ }
336
426
  const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
337
427
 
338
428
  /**
@@ -491,7 +581,9 @@ class Compressor {
491
581
  ...DEFAULTS,
492
582
  ...options
493
583
  };
584
+ this.mimeTypeSet = options && options.mimeType && isImageType(options.mimeType);
494
585
  this.aborted = false;
586
+ this.canvasFallback = false;
495
587
  this.result = null;
496
588
  this.url = null;
497
589
  this.init();
@@ -518,6 +610,58 @@ class Compressor {
518
610
  options.checkOrientation = false;
519
611
  options.retainExif = false;
520
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
+ }
521
665
  const isJPEGImage = mimeType === 'image/jpeg';
522
666
  const checkOrientation = isJPEGImage && options.checkOrientation;
523
667
  const retainExif = isJPEGImage && options.retainExif;
@@ -683,11 +827,7 @@ class Compressor {
683
827
  const destHeight = height;
684
828
  const params = [];
685
829
  if (resizable) {
686
- let srcX = 0;
687
- let srcY = 0;
688
- let srcWidth = naturalWidth;
689
- let srcHeight = naturalHeight;
690
- ({
830
+ const {
691
831
  width: srcWidth,
692
832
  height: srcHeight
693
833
  } = getAdjustedSizes({
@@ -697,9 +837,9 @@ class Compressor {
697
837
  }, {
698
838
  contain: 'cover',
699
839
  cover: 'contain'
700
- }[options.resize]));
701
- srcX = (naturalWidth - srcWidth) / 2;
702
- srcY = (naturalHeight - srcHeight) / 2;
840
+ }[options.resize]);
841
+ const srcX = (naturalWidth - srcWidth) / 2;
842
+ const srcY = (naturalHeight - srcHeight) / 2;
703
843
  params.push(srcX, srcY, srcWidth, srcHeight);
704
844
  }
705
845
  params.push(destX, destY, destWidth, destHeight);
@@ -713,8 +853,9 @@ class Compressor {
713
853
  }
714
854
  let fillStyle = 'transparent';
715
855
 
716
- // Converts PNG files over the `convertSize` to JPEGs.
717
- if (file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) {
856
+ // Converts files over the `convertSize` to JPEG,
857
+ // unless the user explicitly set a `mimeType`.
858
+ if (!this.mimeTypeSet && file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) {
718
859
  options.mimeType = 'image/jpeg';
719
860
  }
720
861
  const isJPEGImage = options.mimeType === 'image/jpeg';
@@ -802,7 +943,6 @@ class Compressor {
802
943
  } else {
803
944
  const date = new Date();
804
945
  result.lastModified = date.getTime();
805
- result.lastModifiedDate = date;
806
946
  result.name = file.name;
807
947
 
808
948
  // Convert the extension to match its type
@@ -812,6 +952,7 @@ class Compressor {
812
952
  }
813
953
  } else {
814
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');
815
956
  result = file;
816
957
  }
817
958
  this.result = result;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Compressor.js Next v1.0.2
2
+ * Compressor.js Next v1.1.1
3
3
  * https://github.com/j9t/compressorjs-next
4
4
  *
5
5
  * Copyright 2018–2024 Chen Fengyuan
@@ -84,14 +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 JPEGs.
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
- convertTypes: ['image/png'],
92
+ convertTypes: [],
92
93
  /**
93
- * PNG files over this size (5 MB by default) will be converted to JPEGs.
94
- * To disable this, just set the value to `Infinity`.
94
+ * Files over this size (5 MB by default) whose type is in `convertTypes`
95
+ * will be converted to JPEG.
95
96
  * @type {number}
96
97
  */
97
98
  convertSize: 5000000,
@@ -339,6 +340,95 @@
339
340
  scaleY
340
341
  };
341
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
+ }
342
432
  const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
343
433
 
344
434
  /**
@@ -497,7 +587,9 @@
497
587
  ...DEFAULTS,
498
588
  ...options
499
589
  };
590
+ this.mimeTypeSet = options && options.mimeType && isImageType(options.mimeType);
500
591
  this.aborted = false;
592
+ this.canvasFallback = false;
501
593
  this.result = null;
502
594
  this.url = null;
503
595
  this.init();
@@ -524,6 +616,58 @@
524
616
  options.checkOrientation = false;
525
617
  options.retainExif = false;
526
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
+ }
527
671
  const isJPEGImage = mimeType === 'image/jpeg';
528
672
  const checkOrientation = isJPEGImage && options.checkOrientation;
529
673
  const retainExif = isJPEGImage && options.retainExif;
@@ -689,11 +833,7 @@
689
833
  const destHeight = height;
690
834
  const params = [];
691
835
  if (resizable) {
692
- let srcX = 0;
693
- let srcY = 0;
694
- let srcWidth = naturalWidth;
695
- let srcHeight = naturalHeight;
696
- ({
836
+ const {
697
837
  width: srcWidth,
698
838
  height: srcHeight
699
839
  } = getAdjustedSizes({
@@ -703,9 +843,9 @@
703
843
  }, {
704
844
  contain: 'cover',
705
845
  cover: 'contain'
706
- }[options.resize]));
707
- srcX = (naturalWidth - srcWidth) / 2;
708
- srcY = (naturalHeight - srcHeight) / 2;
846
+ }[options.resize]);
847
+ const srcX = (naturalWidth - srcWidth) / 2;
848
+ const srcY = (naturalHeight - srcHeight) / 2;
709
849
  params.push(srcX, srcY, srcWidth, srcHeight);
710
850
  }
711
851
  params.push(destX, destY, destWidth, destHeight);
@@ -719,8 +859,9 @@
719
859
  }
720
860
  let fillStyle = 'transparent';
721
861
 
722
- // Converts PNG files over the `convertSize` to JPEGs.
723
- if (file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) {
862
+ // Converts files over the `convertSize` to JPEG,
863
+ // unless the user explicitly set a `mimeType`.
864
+ if (!this.mimeTypeSet && file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) {
724
865
  options.mimeType = 'image/jpeg';
725
866
  }
726
867
  const isJPEGImage = options.mimeType === 'image/jpeg';
@@ -808,7 +949,6 @@
808
949
  } else {
809
950
  const date = new Date();
810
951
  result.lastModified = date.getTime();
811
- result.lastModifiedDate = date;
812
952
  result.name = file.name;
813
953
 
814
954
  // Convert the extension to match its type
@@ -818,6 +958,7 @@
818
958
  }
819
959
  } else {
820
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');
821
962
  result = file;
822
963
  }
823
964
  this.result = result;
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Compressor.js Next v1.0.2
2
+ * Compressor.js Next v1.1.1
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:["image/png"],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.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:E}=d;m&&([p,b]=[b,p],[y,U]=[U,y],[v,E]=[E,v]),w&&(x=v/E),({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:E}=c({aspectRatio:x,width:v,height:E},d.resize)):({width:v=e,height:E=t}=c({aspectRatio:x,width:v,height:E})),v=Math.floor(l(Math.min(Math.max(v,y),p))),E=Math.floor(l(Math.min(Math.max(E,U),b)));const R=-v/2,T=-E/2,k=v,M=E,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(R,T,k,M),m&&([v,E]=[E,v]),u.width=v,u.height=E,n(d.mimeType)||(d.mimeType=s.type);let j="transparent";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,E),d.beforeDraw&&d.beforeDraw.call(this,g,u),this.aborted)return;if(g.save(),g.translate(v/2,E/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",
@@ -92,5 +92,5 @@
92
92
  "sideEffects": false,
93
93
  "type": "module",
94
94
  "types": "types/index.d.ts",
95
- "version": "1.0.2"
95
+ "version": "1.1.1"
96
96
  }
package/src/defaults.js CHANGED
@@ -81,15 +81,16 @@ 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 JPEGs.
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
- convertTypes: ['image/png'],
89
+ convertTypes: [],
89
90
 
90
91
  /**
91
- * PNG files over this size (5 MB by default) will be converted to JPEGs.
92
- * To disable this, just set the value to `Infinity`.
92
+ * Files over this size (5 MB by default) whose type is in `convertTypes`
93
+ * will be converted to JPEG.
93
94
  * @type {number}
94
95
  */
95
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
 
@@ -38,7 +40,9 @@ export default class Compressor {
38
40
  ...DEFAULTS,
39
41
  ...options,
40
42
  };
43
+ this.mimeTypeSet = options && options.mimeType && isImageType(options.mimeType);
41
44
  this.aborted = false;
45
+ this.canvasFallback = false;
42
46
  this.result = null;
43
47
  this.url = null;
44
48
  this.init();
@@ -69,6 +73,68 @@ export default class Compressor {
69
73
  options.retainExif = false;
70
74
  }
71
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
+
72
138
  const isJPEGImage = mimeType === 'image/jpeg';
73
139
  const checkOrientation = isJPEGImage && options.checkOrientation;
74
140
  const retainExif = isJPEGImage && options.retainExif;
@@ -229,21 +295,16 @@ export default class Compressor {
229
295
  const params = [];
230
296
 
231
297
  if (resizable) {
232
- let srcX = 0;
233
- let srcY = 0;
234
- let srcWidth = naturalWidth;
235
- let srcHeight = naturalHeight;
236
-
237
- ({ width: srcWidth, height: srcHeight } = getAdjustedSizes({
298
+ const { width: srcWidth, height: srcHeight } = getAdjustedSizes({
238
299
  aspectRatio,
239
300
  width: naturalWidth,
240
301
  height: naturalHeight,
241
302
  }, {
242
303
  contain: 'cover',
243
304
  cover: 'contain',
244
- }[options.resize]));
245
- srcX = (naturalWidth - srcWidth) / 2;
246
- srcY = (naturalHeight - srcHeight) / 2;
305
+ }[options.resize]);
306
+ const srcX = (naturalWidth - srcWidth) / 2;
307
+ const srcY = (naturalHeight - srcHeight) / 2;
247
308
 
248
309
  params.push(srcX, srcY, srcWidth, srcHeight);
249
310
  }
@@ -263,8 +324,10 @@ export default class Compressor {
263
324
 
264
325
  let fillStyle = 'transparent';
265
326
 
266
- // Converts PNG files over the `convertSize` to JPEGs.
267
- if (file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) {
327
+ // Converts files over the `convertSize` to JPEG,
328
+ // unless the user explicitly set a `mimeType`.
329
+ if (!this.mimeTypeSet && file.size > options.convertSize
330
+ && options.convertTypes.indexOf(options.mimeType) >= 0) {
268
331
  options.mimeType = 'image/jpeg';
269
332
  }
270
333
 
@@ -376,7 +439,6 @@ export default class Compressor {
376
439
  const date = new Date();
377
440
 
378
441
  result.lastModified = date.getTime();
379
- result.lastModifiedDate = date;
380
442
  result.name = file.name;
381
443
 
382
444
  // Convert the extension to match its type
@@ -389,6 +451,7 @@ export default class Compressor {
389
451
  }
390
452
  } else {
391
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');
392
455
  result = file;
393
456
  }
394
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
  /**