compressorjs-next 1.1.2 → 2.0.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,11 +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 (as of 1.1.0—follow [the changelog](https://github.com/j9t/compressorjs-next/blob/main/CHANGELOG.md) from there):
25
+ The API is otherwise the same, with these exceptions (as of 2.0.0—follow [the changelog](https://github.com/j9t/compressorjs-next/blob/main/CHANGELOG.md) from there):
26
26
 
27
27
  * ESM is now the default module format (CommonJS is still supported)
28
- * The `noConflict()` method has been removed
28
+ * The `checkOrientation` option has been removed, as all supported browsers now handle EXIF orientation natively
29
29
  * The default for `convertTypes` has changed from `['image/png']` to `[]`
30
+ * The `noConflict()` method has been removed
30
31
  * Internet Explorer is no longer supported
31
32
 
32
33
  ## Main files
@@ -85,7 +86,7 @@ document.getElementById('file').addEventListener('change', (e) => {
85
86
  return;
86
87
  }
87
88
 
88
- new Compressor(file, {
89
+ const compressor = new Compressor(file, {
89
90
  quality: 0.6,
90
91
 
91
92
  // The compression process is asynchronous,
@@ -130,19 +131,6 @@ Indicates whether to output the original image instead of the compressed one whe
130
131
  * The `maxWidth` option is set and its value is less than the natural width of the image.
131
132
  * The `maxHeight` option is set and its value is less than the natural height of the image.
132
133
 
133
- ### `checkOrientation`
134
-
135
- * Type: `boolean`
136
- * Default: `true`
137
-
138
- Indicates whether to read the image’s Exif Orientation value (JPEG image only), and then rotate or flip the image automatically with the value.
139
-
140
- **Notes:**
141
-
142
- * Don’t trust this all the time as some JPEG images have incorrect (not standard) `Orientation` values.
143
- * If the size of the target image is too large (e.g., greater than 10 MB), you should disable this option to avoid an out-of-memory crash.
144
- * The image’s Exif information will be removed after compressed, so if you need the Exif information, you may need to upload the original image as well.
145
-
146
134
  ### `retainExif`
147
135
 
148
136
  * Type: `boolean`
@@ -309,7 +297,7 @@ new Compressor(file, {
309
297
  * Type: `Function`
310
298
  * Default: `null`
311
299
  * Parameters:
312
- - `result`: The compressed image (a `File` (**read only**) or `Blob` object).
300
+ - `result`: The compressed image (a [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) object, which is also a `Blob`).
313
301
 
314
302
  The hook function to execute when successful to compress the image.
315
303
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Compressor.js Next v1.1.2
2
+ * Compressor.js Next v2.0.1
3
3
  * https://github.com/j9t/compressorjs-next
4
4
  *
5
5
  * Copyright 2018–2024 Chen Fengyuan
@@ -16,12 +16,6 @@ var DEFAULTS = {
16
16
  * @type {boolean}
17
17
  */
18
18
  strict: true,
19
- /**
20
- * Indicates if read the image’s Exif Orientation information,
21
- * and then rotate or flip the image automatically.
22
- * @type {boolean}
23
- */
24
- checkOrientation: true,
25
19
  /**
26
20
  * Indicates if retain the image’s Exif information after compressed.
27
21
  * @type {boolean}
@@ -187,40 +181,13 @@ function getStringFromCharCode(dataView, start, length) {
187
181
  }
188
182
  return str;
189
183
  }
190
- const {
191
- btoa
192
- } = WINDOW;
193
-
194
- /**
195
- * Transform array buffer to Data URL.
196
- * @param {ArrayBuffer} arrayBuffer - The array buffer to transform.
197
- * @param {string} mimeType - The mime type of the Data URL.
198
- * @returns {string} The result Data URL.
199
- */
200
- function arrayBufferToDataURL(arrayBuffer, mimeType) {
201
- const uint8 = new Uint8Array(arrayBuffer);
202
- const {
203
- length
204
- } = uint8;
205
- const chunkSize = 8192;
206
- let binary = '';
207
- for (let i = 0; i < length; i += chunkSize) {
208
- const end = Math.min(i + chunkSize, length);
209
- let chunk = '';
210
- for (let j = i; j < end; j += 1) {
211
- chunk += fromCharCode(uint8[j]);
212
- }
213
- binary += chunk;
214
- }
215
- return `data:${mimeType};base64,${btoa(binary)}`;
216
- }
217
184
 
218
185
  /**
219
186
  * Get orientation value from given array buffer.
220
187
  * @param {ArrayBuffer} arrayBuffer - The array buffer to read.
221
188
  * @returns {number} The read orientation value.
222
189
  */
223
- function resetAndGetOrientation(arrayBuffer) {
190
+ function resetOrientation(arrayBuffer) {
224
191
  const dataView = new DataView(arrayBuffer);
225
192
  let orientation;
226
193
 
@@ -282,60 +249,6 @@ function resetAndGetOrientation(arrayBuffer) {
282
249
  }
283
250
  return orientation;
284
251
  }
285
-
286
- /**
287
- * Parse Exif Orientation value.
288
- * @param {number} orientation - The orientation to parse.
289
- * @returns {Object} The parsed result.
290
- */
291
- function parseOrientation(orientation) {
292
- let rotate = 0;
293
- let scaleX = 1;
294
- let scaleY = 1;
295
- switch (orientation) {
296
- // Flip horizontal
297
- case 2:
298
- scaleX = -1;
299
- break;
300
-
301
- // Rotate left 180°
302
- case 3:
303
- rotate = -180;
304
- break;
305
-
306
- // Flip vertical
307
- case 4:
308
- scaleY = -1;
309
- break;
310
-
311
- // Flip vertical and rotate right 90°
312
- case 5:
313
- rotate = 90;
314
- scaleY = -1;
315
- break;
316
-
317
- // Rotate right 90°
318
- case 6:
319
- rotate = 90;
320
- break;
321
-
322
- // Flip horizontal and rotate right 90°
323
- case 7:
324
- rotate = 90;
325
- scaleX = -1;
326
- break;
327
-
328
- // Rotate left 90°
329
- case 8:
330
- rotate = -90;
331
- break;
332
- }
333
- return {
334
- rotate,
335
- scaleX,
336
- scaleY
337
- };
338
- }
339
252
  let cachedCanvasReliable;
340
253
 
341
254
  /**
@@ -609,7 +522,6 @@ class Compressor {
609
522
  return;
610
523
  }
611
524
  if (!ArrayBuffer) {
612
- options.checkOrientation = false;
613
525
  options.retainExif = false;
614
526
  }
615
527
  if (!isCanvasReliable()) {
@@ -625,17 +537,18 @@ class Compressor {
625
537
  target
626
538
  }) => {
627
539
  if (this.aborted) return;
628
- let result;
540
+ let blob;
629
541
  try {
630
- const stripped = stripExif(target.result);
631
- result = uint8ArrayToBlob(stripped, mimeType);
542
+ blob = uint8ArrayToBlob(stripExif(target.result), mimeType);
632
543
  } catch {
633
544
  this.fail(new Error('Failed to process the image data.'));
634
545
  return;
635
546
  }
636
547
  const date = new Date();
637
- result.name = file.name;
638
- result.lastModified = date.getTime();
548
+ const result = new File([blob], file.name || '', {
549
+ type: mimeType,
550
+ lastModified: date.getTime()
551
+ });
639
552
  this.result = result;
640
553
  if (options.success) {
641
554
  options.success.call(this, result);
@@ -665,9 +578,8 @@ class Compressor {
665
578
  return;
666
579
  }
667
580
  const isJPEGImage = mimeType === 'image/jpeg';
668
- const checkOrientation = isJPEGImage && options.checkOrientation;
669
581
  const retainExif = isJPEGImage && options.retainExif;
670
- if (URL && !checkOrientation && !retainExif) {
582
+ if (!retainExif) {
671
583
  this.url = URL.createObjectURL(file);
672
584
  this.load({
673
585
  url: this.url
@@ -678,36 +590,16 @@ class Compressor {
678
590
  reader.onload = ({
679
591
  target
680
592
  }) => {
681
- const {
682
- result
683
- } = target;
684
- const data = {};
685
- let orientation = 1;
686
- if (checkOrientation) {
687
- // Reset the orientation value to its default value (1)
688
- // as some iOS browsers will render image with its orientation
689
- orientation = resetAndGetOrientation(result);
690
- if (orientation > 1) {
691
- Object.assign(data, parseOrientation(orientation));
692
- }
693
- }
694
- if (retainExif) {
695
- this.exif = getExif(result);
696
- }
697
- if (checkOrientation || retainExif) {
698
- if (!URL
699
-
700
- // Generate a new URL with the default orientation value (1)
701
- || orientation > 1) {
702
- data.url = arrayBufferToDataURL(result, mimeType);
703
- } else {
704
- this.url = URL.createObjectURL(file);
705
- data.url = this.url;
706
- }
707
- } else {
708
- data.url = result;
709
- }
710
- this.load(data);
593
+ if (this.aborted) return;
594
+
595
+ // Normalize EXIF orientation to 1 before extracting, since the browser
596
+ // handles rotation natively via `image-orientation: from-image`
597
+ resetOrientation(target.result);
598
+ this.exif = getExif(target.result);
599
+ this.url = URL.createObjectURL(file);
600
+ this.load({
601
+ url: this.url
602
+ });
711
603
  };
712
604
  reader.onabort = () => {
713
605
  this.fail(new Error('Aborted to read the image with FileReader.'));
@@ -718,11 +610,7 @@ class Compressor {
718
610
  reader.onloadend = () => {
719
611
  this.reader = null;
720
612
  };
721
- if (checkOrientation || retainExif) {
722
- reader.readAsArrayBuffer(file);
723
- } else {
724
- reader.readAsDataURL(file);
725
- }
613
+ reader.readAsArrayBuffer(file);
726
614
  }
727
615
  }
728
616
  load(data) {
@@ -755,10 +643,7 @@ class Compressor {
755
643
  }
756
644
  draw({
757
645
  naturalWidth,
758
- naturalHeight,
759
- rotate = 0,
760
- scaleX = 1,
761
- scaleY = 1
646
+ naturalHeight
762
647
  }) {
763
648
  const {
764
649
  file,
@@ -767,7 +652,6 @@ class Compressor {
767
652
  } = this;
768
653
  const canvas = document.createElement('canvas');
769
654
  const context = canvas.getContext('2d');
770
- const is90DegreesRotated = Math.abs(rotate) % 180 === 90;
771
655
  const resizable = (options.resize === 'contain' || options.resize === 'cover') && isPositiveNumber(options.width) && isPositiveNumber(options.height);
772
656
  let maxWidth = Math.max(options.maxWidth, 0) || Infinity;
773
657
  let maxHeight = Math.max(options.maxHeight, 0) || Infinity;
@@ -778,11 +662,6 @@ class Compressor {
778
662
  width,
779
663
  height
780
664
  } = options;
781
- if (is90DegreesRotated) {
782
- [maxWidth, maxHeight] = [maxHeight, maxWidth];
783
- [minWidth, minHeight] = [minHeight, minWidth];
784
- [width, height] = [height, width];
785
- }
786
665
  if (resizable) {
787
666
  aspectRatio = width / height;
788
667
  }
@@ -823,10 +702,6 @@ class Compressor {
823
702
  }
824
703
  width = Math.floor(normalizeDecimalNumber(Math.min(Math.max(width, minWidth), maxWidth)));
825
704
  height = Math.floor(normalizeDecimalNumber(Math.min(Math.max(height, minHeight), maxHeight)));
826
- const destX = -width / 2;
827
- const destY = -height / 2;
828
- const destWidth = width;
829
- const destHeight = height;
830
705
  const params = [];
831
706
  if (resizable) {
832
707
  const {
@@ -844,10 +719,7 @@ class Compressor {
844
719
  const srcY = (naturalHeight - srcHeight) / 2;
845
720
  params.push(srcX, srcY, srcWidth, srcHeight);
846
721
  }
847
- params.push(destX, destY, destWidth, destHeight);
848
- if (is90DegreesRotated) {
849
- [width, height] = [height, width];
850
- }
722
+ params.push(0, 0, width, height);
851
723
  canvas.width = width;
852
724
  canvas.height = height;
853
725
  if (!isImageType(options.mimeType)) {
@@ -874,12 +746,7 @@ class Compressor {
874
746
  if (this.aborted) {
875
747
  return;
876
748
  }
877
- context.save();
878
- context.translate(width / 2, height / 2);
879
- context.rotate(rotate * Math.PI / 180);
880
- context.scale(scaleX, scaleY);
881
749
  context.drawImage(image, ...params);
882
- context.restore();
883
750
  if (options.drew) {
884
751
  options.drew.call(this, context, canvas);
885
752
  }
@@ -895,12 +762,46 @@ class Compressor {
895
762
  });
896
763
  if (blob && isJPEGImage && options.retainExif && this.exif && this.exif.length > 0) {
897
764
  const next = arrayBuffer => {
765
+ if (this.aborted) return;
898
766
  const withExif = insertExif(arrayBuffer, this.exif);
899
767
  done(uint8ArrayToBlob(withExif, options.mimeType));
900
768
  };
901
769
  if (blob.arrayBuffer) {
902
770
  blob.arrayBuffer().then(next).catch(() => {
903
- this.fail(new Error('Failed to read the compressed image with Blob.arrayBuffer().'));
771
+ if (this.aborted) return;
772
+ this.fail(new Error('Failed to read the compressed image with `Blob.arrayBuffer()`.'));
773
+ });
774
+ } else {
775
+ const reader = new FileReader();
776
+ this.reader = reader;
777
+ reader.onload = ({
778
+ target
779
+ }) => {
780
+ next(target.result);
781
+ };
782
+ reader.onabort = () => {
783
+ this.fail(new Error('Aborted to read the compressed image with FileReader.'));
784
+ };
785
+ reader.onerror = () => {
786
+ this.fail(new Error('Failed to read the compressed image with FileReader.'));
787
+ };
788
+ reader.onloadend = () => {
789
+ this.reader = null;
790
+ };
791
+ reader.readAsArrayBuffer(blob);
792
+ }
793
+ } else if (blob && isJPEGImage && !options.retainExif) {
794
+ // Strip any EXIF that may be present in the canvas output
795
+ // (most browsers strip it automatically, but WebKit preserves the
796
+ // source EXIF—this ensures consistent, privacy-safe output)
797
+ const next = arrayBuffer => {
798
+ if (this.aborted) return;
799
+ done(uint8ArrayToBlob(stripExif(arrayBuffer), options.mimeType));
800
+ };
801
+ if (blob.arrayBuffer) {
802
+ blob.arrayBuffer().then(next).catch(() => {
803
+ if (this.aborted) return;
804
+ this.fail(new Error('Failed to read the compressed image with `Blob.arrayBuffer()`.'));
904
805
  });
905
806
  } else {
906
807
  const reader = new FileReader();
@@ -938,25 +839,89 @@ class Compressor {
938
839
  options
939
840
  } = this;
940
841
  this.revokeUrl();
842
+ let strictFallback = false;
941
843
  if (result) {
942
844
  // Returns original file if the result is greater than it and without size-related options
943
845
  if (options.strict && !options.retainExif && result.size > file.size && options.mimeType === file.type && !(options.width > naturalWidth || options.height > naturalHeight || options.minWidth > naturalWidth || options.minHeight > naturalHeight || options.maxWidth < naturalWidth || options.maxHeight < naturalHeight)) {
944
846
  result = file;
847
+ strictFallback = true;
945
848
  } else {
946
849
  const date = new Date();
947
- result.lastModified = date.getTime();
948
- result.name = file.name;
850
+ let name = file.name || '';
949
851
 
950
852
  // Convert the extension to match its type
951
- if (result.name && result.type !== file.type) {
952
- result.name = result.name.replace(REGEXP_EXTENSION, imageTypeToExtension(result.type));
853
+ if (name && result.type !== file.type) {
854
+ name = name.replace(REGEXP_EXTENSION, imageTypeToExtension(result.type));
953
855
  }
856
+ result = new File([result], name, {
857
+ type: result.type,
858
+ lastModified: date.getTime()
859
+ });
954
860
  }
955
861
  } else {
956
862
  // Returns original file if the result is null in some cases
957
863
  console.warn('Compressor.js Next: Canvas produced no output—returning the original image');
958
864
  result = file;
959
865
  }
866
+
867
+ // When strict returns the original file, it may still contain EXIF—strip it
868
+ // asynchronously so the output is consistently EXIF-free across all browsers
869
+ if (strictFallback && file.type === 'image/jpeg') {
870
+ if (file.arrayBuffer) {
871
+ file.arrayBuffer().then(arrayBuffer => {
872
+ if (this.aborted) return;
873
+ const strippedBlob = uint8ArrayToBlob(stripExif(arrayBuffer), file.type);
874
+ const stripped = new File([strippedBlob], file.name || '', {
875
+ type: file.type,
876
+ lastModified: file.lastModified || Date.now()
877
+ });
878
+ this.result = stripped;
879
+ if (options.success) {
880
+ options.success.call(this, stripped);
881
+ }
882
+ }).catch(err => {
883
+ if (this.aborted) return;
884
+ console.warn(`Compressor.js Next: Failed to strip EXIF from original file—returning original with EXIF intact${file.name ? ` [${file.name}]` : ''}${err?.message ? `: ${err.message}` : ''}`);
885
+ this.result = file;
886
+ if (options.success) {
887
+ options.success.call(this, file);
888
+ }
889
+ });
890
+ } else {
891
+ const reader = new FileReader();
892
+ this.reader = reader;
893
+ reader.onload = ({
894
+ target
895
+ }) => {
896
+ if (this.aborted) return;
897
+ const strippedBlob = uint8ArrayToBlob(stripExif(target.result), file.type);
898
+ const stripped = new File([strippedBlob], file.name || '', {
899
+ type: file.type,
900
+ lastModified: file.lastModified || Date.now()
901
+ });
902
+ this.result = stripped;
903
+ if (options.success) {
904
+ options.success.call(this, stripped);
905
+ }
906
+ };
907
+ reader.onabort = () => {
908
+ this.fail(new Error('Aborted to read the original file with FileReader.'));
909
+ };
910
+ reader.onerror = () => {
911
+ if (this.aborted) return;
912
+ console.warn(`Compressor.js Next: Failed to strip EXIF from original file—returning original with EXIF intact${file.name ? ` [${file.name}]` : ''}`);
913
+ this.result = file;
914
+ if (options.success) {
915
+ options.success.call(this, file);
916
+ }
917
+ };
918
+ reader.onloadend = () => {
919
+ this.reader = null;
920
+ };
921
+ reader.readAsArrayBuffer(file);
922
+ }
923
+ return;
924
+ }
960
925
  this.result = result;
961
926
  if (options.success) {
962
927
  options.success.call(this, result);