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 +12 -5
- package/dist/compressor.common.js +158 -17
- package/dist/compressor.esm.js +158 -17
- package/dist/compressor.js +158 -17
- package/dist/compressor.min.js +2 -2
- package/package.json +3 -3
- package/src/defaults.js +6 -5
- package/src/index.js +75 -12
- package/src/utilities.js +118 -0
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
|
|
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: `[
|
|
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
|
|
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
|
|
255
|
+
* Default: `5000000` (5 MB)
|
|
251
256
|
|
|
252
|
-
Files whose file type is included in the `convertTypes` list
|
|
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.
|
|
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
|
-
*
|
|
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: [
|
|
88
|
+
convertTypes: [],
|
|
88
89
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
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
|
-
|
|
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
|
|
719
|
-
|
|
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;
|
package/dist/compressor.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Compressor.js Next v1.
|
|
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
|
-
*
|
|
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: [
|
|
86
|
+
convertTypes: [],
|
|
86
87
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
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
|
-
|
|
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
|
|
717
|
-
|
|
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;
|
package/dist/compressor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Compressor.js Next v1.
|
|
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
|
-
*
|
|
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: [
|
|
92
|
+
convertTypes: [],
|
|
92
93
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
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
|
-
|
|
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
|
|
723
|
-
|
|
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;
|
package/dist/compressor.min.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* Compressor.js Next v1.
|
|
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:[
|
|
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": "^
|
|
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": "^
|
|
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.
|
|
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
|
-
*
|
|
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: [
|
|
89
|
+
convertTypes: [],
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
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
|
-
|
|
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
|
|
267
|
-
|
|
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
|
/**
|