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