compressorjs-next 1.0.0

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.
@@ -0,0 +1,866 @@
1
+ /*!
2
+ * Compressor.js v1.0.0
3
+ * https://github.com/j9t/compressorjs-next
4
+ *
5
+ * Copyright 2018–2024 Chen Fengyuan
6
+ * Copyright 2026 Jens Oliver Meiert
7
+ *
8
+ * Released under the MIT license.
9
+ */
10
+ 'use strict';
11
+
12
+ var DEFAULTS = {
13
+ /**
14
+ * Indicates if output the original image instead of the compressed one
15
+ * when the size of the compressed image is greater than the original one’s
16
+ * @type {boolean}
17
+ */
18
+ strict: true,
19
+ /**
20
+ * Indicates if read the image’s Exif Orientation information,
21
+ * and then rotate or flip the image automatically.
22
+ * @type {boolean}
23
+ */
24
+ checkOrientation: true,
25
+ /**
26
+ * Indicates if retain the image’s Exif information after compressed.
27
+ * @type {boolean}
28
+ */
29
+ retainExif: false,
30
+ /**
31
+ * The max width of the output image.
32
+ * @type {number}
33
+ */
34
+ maxWidth: Infinity,
35
+ /**
36
+ * The max height of the output image.
37
+ * @type {number}
38
+ */
39
+ maxHeight: Infinity,
40
+ /**
41
+ * The min width of the output image.
42
+ * @type {number}
43
+ */
44
+ minWidth: 0,
45
+ /**
46
+ * The min height of the output image.
47
+ * @type {number}
48
+ */
49
+ minHeight: 0,
50
+ /**
51
+ * The width of the output image.
52
+ * If not specified, the natural width of the source image will be used.
53
+ * @type {number}
54
+ */
55
+ width: undefined,
56
+ /**
57
+ * The height of the output image.
58
+ * If not specified, the natural height of the source image will be used.
59
+ * @type {number}
60
+ */
61
+ height: undefined,
62
+ /**
63
+ * Sets how the size of the image should be resized to the container
64
+ * specified by the `width` and `height` options.
65
+ * @type {string}
66
+ */
67
+ resize: 'none',
68
+ /**
69
+ * The quality of the output image.
70
+ * It must be a number between `0` and `1`,
71
+ * and only available for `image/jpeg` and `image/webp` images.
72
+ * Check out {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob canvas.toBlob}.
73
+ * @type {number}
74
+ */
75
+ quality: 0.8,
76
+ /**
77
+ * The mime type of the output image.
78
+ * By default, the original mime type of the source image file will be used.
79
+ * @type {string}
80
+ */
81
+ mimeType: 'auto',
82
+ /**
83
+ * Files whose file type is included in this list,
84
+ * and whose file size exceeds the `convertSize` value will be converted to JPEGs.
85
+ * @type {string|Array}
86
+ */
87
+ convertTypes: ['image/png'],
88
+ /**
89
+ * PNG files over this size (5 MB by default) will be converted to JPEGs.
90
+ * To disable this, just set the value to `Infinity`.
91
+ * @type {number}
92
+ */
93
+ convertSize: 5000000,
94
+ /**
95
+ * The hook function to execute before draw the image into the canvas for compression.
96
+ * @type {Function}
97
+ * @param {CanvasRenderingContext2D} context - The 2d rendering context of the canvas.
98
+ * @param {HTMLCanvasElement} canvas - The canvas for compression.
99
+ * @example
100
+ * function (context, canvas) {
101
+ * context.fillStyle = '#fff';
102
+ * }
103
+ */
104
+ beforeDraw: null,
105
+ /**
106
+ * The hook function to execute after drew the image into the canvas for compression.
107
+ * @type {Function}
108
+ * @param {CanvasRenderingContext2D} context - The 2d rendering context of the canvas.
109
+ * @param {HTMLCanvasElement} canvas - The canvas for compression.
110
+ * @example
111
+ * function (context, canvas) {
112
+ * context.filter = 'grayscale(100%)';
113
+ * }
114
+ */
115
+ drew: null,
116
+ /**
117
+ * The hook function to execute when success to compress the image.
118
+ * @type {Function}
119
+ * @param {File} file - The compressed image File object.
120
+ * @example
121
+ * function (file) {
122
+ * console.log(file);
123
+ * }
124
+ */
125
+ success: null,
126
+ /**
127
+ * The hook function to execute when fail to compress the image.
128
+ * @type {Function}
129
+ * @param {Error} err - An Error object.
130
+ * @example
131
+ * function (err) {
132
+ * console.log(err.message);
133
+ * }
134
+ */
135
+ error: null
136
+ };
137
+
138
+ const WINDOW = typeof window !== 'undefined' && typeof window.document !== 'undefined' ? window : {};
139
+
140
+ /**
141
+ * Check if the given value is a positive number.
142
+ * @param {*} value - The value to check.
143
+ * @returns {boolean} Returns `true` if the given value is a positive number, else `false`.
144
+ */
145
+ const isPositiveNumber = value => value > 0 && value < Infinity;
146
+ const REGEXP_IMAGE_TYPE = /^image\/.+$/;
147
+
148
+ /**
149
+ * Check if the given value is a mime type of image.
150
+ * @param {*} value - The value to check.
151
+ * @returns {boolean} Returns `true` if the given is a mime type of image, else `false`.
152
+ */
153
+ function isImageType(value) {
154
+ return REGEXP_IMAGE_TYPE.test(value);
155
+ }
156
+
157
+ /**
158
+ * Convert image type to extension.
159
+ * @param {string} value - The image type to convert.
160
+ * @returns {boolean} Returns the image extension.
161
+ */
162
+ function imageTypeToExtension(value) {
163
+ let extension = isImageType(value) ? value.slice(6) : '';
164
+ if (extension === 'jpeg') {
165
+ extension = 'jpg';
166
+ }
167
+ return `.${extension}`;
168
+ }
169
+ const {
170
+ fromCharCode
171
+ } = String;
172
+
173
+ /**
174
+ * Get string from char code in data view.
175
+ * @param {DataView} dataView - The data view for read.
176
+ * @param {number} start - The start index.
177
+ * @param {number} length - The read length.
178
+ * @returns {string} The read result.
179
+ */
180
+ function getStringFromCharCode(dataView, start, length) {
181
+ let str = '';
182
+ let i;
183
+ length += start;
184
+ for (i = start; i < length; i += 1) {
185
+ str += fromCharCode(dataView.getUint8(i));
186
+ }
187
+ return str;
188
+ }
189
+ const {
190
+ btoa
191
+ } = WINDOW;
192
+
193
+ /**
194
+ * Transform array buffer to Data URL.
195
+ * @param {ArrayBuffer} arrayBuffer - The array buffer to transform.
196
+ * @param {string} mimeType - The mime type of the Data URL.
197
+ * @returns {string} The result Data URL.
198
+ */
199
+ function arrayBufferToDataURL(arrayBuffer, mimeType) {
200
+ const uint8 = new Uint8Array(arrayBuffer);
201
+ const {
202
+ length
203
+ } = uint8;
204
+ const chunkSize = 8192;
205
+ let binary = '';
206
+ for (let i = 0; i < length; i += chunkSize) {
207
+ const end = Math.min(i + chunkSize, length);
208
+ let chunk = '';
209
+ for (let j = i; j < end; j += 1) {
210
+ chunk += fromCharCode(uint8[j]);
211
+ }
212
+ binary += chunk;
213
+ }
214
+ return `data:${mimeType};base64,${btoa(binary)}`;
215
+ }
216
+
217
+ /**
218
+ * Get orientation value from given array buffer.
219
+ * @param {ArrayBuffer} arrayBuffer - The array buffer to read.
220
+ * @returns {number} The read orientation value.
221
+ */
222
+ function resetAndGetOrientation(arrayBuffer) {
223
+ const dataView = new DataView(arrayBuffer);
224
+ let orientation;
225
+
226
+ // Ignores range error when the image does not have correct Exif information
227
+ try {
228
+ let littleEndian;
229
+ let app1Start;
230
+ let ifdStart;
231
+
232
+ // Only handle JPEG image (start by 0xFFD8)
233
+ if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {
234
+ const length = dataView.byteLength;
235
+ let offset = 2;
236
+ while (offset + 1 < length) {
237
+ if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {
238
+ app1Start = offset;
239
+ break;
240
+ }
241
+ offset += 1;
242
+ }
243
+ }
244
+ if (app1Start) {
245
+ const exifIDCode = app1Start + 4;
246
+ const tiffOffset = app1Start + 10;
247
+ if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') {
248
+ const endianness = dataView.getUint16(tiffOffset);
249
+ littleEndian = endianness === 0x4949;
250
+ if (littleEndian || endianness === 0x4D4D /* bigEndian */) {
251
+ if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {
252
+ const firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);
253
+ if (firstIFDOffset >= 0x00000008) {
254
+ ifdStart = tiffOffset + firstIFDOffset;
255
+ }
256
+ }
257
+ }
258
+ }
259
+ }
260
+ if (ifdStart) {
261
+ const length = dataView.getUint16(ifdStart, littleEndian);
262
+ let offset;
263
+ let i;
264
+ for (i = 0; i < length; i += 1) {
265
+ offset = ifdStart + i * 12 + 2;
266
+ if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) {
267
+ // 8 is the offset of the current tag's value
268
+ offset += 8;
269
+
270
+ // Get the original orientation value
271
+ orientation = dataView.getUint16(offset, littleEndian);
272
+
273
+ // Override the orientation with its default value
274
+ dataView.setUint16(offset, 1, littleEndian);
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ } catch {
280
+ orientation = 1;
281
+ }
282
+ return orientation;
283
+ }
284
+
285
+ /**
286
+ * Parse Exif Orientation value.
287
+ * @param {number} orientation - The orientation to parse.
288
+ * @returns {Object} The parsed result.
289
+ */
290
+ function parseOrientation(orientation) {
291
+ let rotate = 0;
292
+ let scaleX = 1;
293
+ let scaleY = 1;
294
+ switch (orientation) {
295
+ // Flip horizontal
296
+ case 2:
297
+ scaleX = -1;
298
+ break;
299
+
300
+ // Rotate left 180°
301
+ case 3:
302
+ rotate = -180;
303
+ break;
304
+
305
+ // Flip vertical
306
+ case 4:
307
+ scaleY = -1;
308
+ break;
309
+
310
+ // Flip vertical and rotate right 90°
311
+ case 5:
312
+ rotate = 90;
313
+ scaleY = -1;
314
+ break;
315
+
316
+ // Rotate right 90°
317
+ case 6:
318
+ rotate = 90;
319
+ break;
320
+
321
+ // Flip horizontal and rotate right 90°
322
+ case 7:
323
+ rotate = 90;
324
+ scaleX = -1;
325
+ break;
326
+
327
+ // Rotate left 90°
328
+ case 8:
329
+ rotate = -90;
330
+ break;
331
+ }
332
+ return {
333
+ rotate,
334
+ scaleX,
335
+ scaleY
336
+ };
337
+ }
338
+ const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/;
339
+
340
+ /**
341
+ * Normalize decimal number.
342
+ * Check out {@link https://0.30000000000000004.com/}
343
+ * @param {number} value - The value to normalize.
344
+ * @param {number} [times=100000000000] - The times for normalizing.
345
+ * @returns {number} Returns the normalized number.
346
+ */
347
+ function normalizeDecimalNumber(value, times = 100000000000) {
348
+ return REGEXP_DECIMALS.test(value) ? Math.round(value * times) / times : value;
349
+ }
350
+
351
+ /**
352
+ * Get the max sizes in a rectangle under the given aspect ratio.
353
+ * @param {Object} data - The original sizes.
354
+ * @param {string} [type='contain'] - The adjust type.
355
+ * @returns {Object} The result sizes.
356
+ */
357
+ function getAdjustedSizes({
358
+ aspectRatio,
359
+ height,
360
+ width
361
+ },
362
+ // `none` | `contain` | `cover`
363
+ type = 'none') {
364
+ const isValidWidth = isPositiveNumber(width);
365
+ const isValidHeight = isPositiveNumber(height);
366
+ if (isValidWidth && isValidHeight) {
367
+ const adjustedWidth = height * aspectRatio;
368
+ if ((type === 'contain' || type === 'none') && adjustedWidth > width || type === 'cover' && adjustedWidth < width) {
369
+ height = width / aspectRatio;
370
+ } else {
371
+ width = height * aspectRatio;
372
+ }
373
+ } else if (isValidWidth) {
374
+ height = width / aspectRatio;
375
+ } else if (isValidHeight) {
376
+ width = height * aspectRatio;
377
+ }
378
+ return {
379
+ width,
380
+ height
381
+ };
382
+ }
383
+
384
+ /**
385
+ * Get Exif information from the given array buffer.
386
+ * @param {ArrayBuffer} arrayBuffer - The array buffer to read.
387
+ * @returns {Array} The read Exif information.
388
+ */
389
+ function getExif(arrayBuffer) {
390
+ const dataView = new DataView(arrayBuffer);
391
+ const {
392
+ byteLength
393
+ } = dataView;
394
+ const exifArray = [];
395
+ let start = 0;
396
+ while (start + 3 < byteLength) {
397
+ const value = dataView.getUint8(start);
398
+ const next = dataView.getUint8(start + 1);
399
+
400
+ // SOS (Start of Scan)
401
+ if (value === 0xFF && next === 0xDA) {
402
+ break;
403
+ }
404
+
405
+ // SOI (Start of Image)
406
+ if (value === 0xFF && next === 0xD8) {
407
+ start += 2;
408
+ } else {
409
+ const segmentLength = dataView.getUint16(start + 2);
410
+ const end = start + segmentLength + 2;
411
+
412
+ // APP1 marker (EXIF)
413
+ if (value === 0xFF && next === 0xE1) {
414
+ for (let i = start; i < end && i < byteLength; i += 1) {
415
+ exifArray.push(dataView.getUint8(i));
416
+ }
417
+ }
418
+ start = end;
419
+ }
420
+ }
421
+ return exifArray;
422
+ }
423
+
424
+ /**
425
+ * Insert Exif information into the given array buffer.
426
+ * @param {ArrayBuffer} arrayBuffer - The array buffer to transform.
427
+ * @param {Array} exifArray - The Exif information to insert.
428
+ * @returns {Uint8Array} The transformed array as Uint8Array.
429
+ */
430
+ function insertExif(arrayBuffer, exifArray) {
431
+ const dataView = new DataView(arrayBuffer);
432
+ const uint8 = new Uint8Array(arrayBuffer);
433
+
434
+ // Check for APP0 marker (JFIF)
435
+ if (dataView.getUint8(2) !== 0xFF || dataView.getUint8(3) !== 0xE0) {
436
+ return uint8;
437
+ }
438
+ const app0Length = dataView.getUint16(4);
439
+ const restStart = 4 + app0Length;
440
+ const restLength = uint8.byteLength - restStart;
441
+
442
+ // Create new buffer: SOI (2) + EXIF + rest of image
443
+ const result = new Uint8Array(2 + exifArray.length + restLength);
444
+
445
+ // SOI marker
446
+ result[0] = 0xFF;
447
+ result[1] = 0xD8;
448
+
449
+ // EXIF data
450
+ for (let i = 0; i < exifArray.length; i += 1) {
451
+ result[2 + i] = exifArray[i];
452
+ }
453
+
454
+ // Rest of image (skip SOI and APP0)
455
+ result.set(uint8.subarray(restStart), 2 + exifArray.length);
456
+ return result;
457
+ }
458
+
459
+ /**
460
+ * Convert a Uint8Array to a Blob.
461
+ * @param {Uint8Array} uint8Array - The Uint8Array to convert.
462
+ * @param {string} mimeType - The mime type of the Blob.
463
+ * @returns {Blob} The resulting Blob.
464
+ */
465
+ function uint8ArrayToBlob(uint8Array, mimeType) {
466
+ return new Blob([uint8Array], {
467
+ type: mimeType
468
+ });
469
+ }
470
+
471
+ const {
472
+ ArrayBuffer,
473
+ FileReader
474
+ } = WINDOW;
475
+ const URL = WINDOW.URL || WINDOW.webkitURL;
476
+ const REGEXP_EXTENSION = /\.\w+$/;
477
+
478
+ /**
479
+ * Creates a new image compressor.
480
+ * @class
481
+ */
482
+ class Compressor {
483
+ /**
484
+ * The constructor of Compressor.
485
+ * @param {File|Blob} file - The target image file for compressing.
486
+ * @param {Object} [options] - The options for compressing.
487
+ */
488
+ constructor(file, options) {
489
+ this.file = file;
490
+ this.exif = [];
491
+ this.image = new Image();
492
+ this.options = {
493
+ ...DEFAULTS,
494
+ ...options
495
+ };
496
+ this.aborted = false;
497
+ this.result = null;
498
+ this.url = null;
499
+ this.init();
500
+ }
501
+ init() {
502
+ const {
503
+ file,
504
+ options
505
+ } = this;
506
+ if (!(file instanceof Blob)) {
507
+ this.fail(new Error('The first argument must be a File or Blob object.'));
508
+ return;
509
+ }
510
+ const mimeType = file.type;
511
+ if (!isImageType(mimeType)) {
512
+ this.fail(new Error('The first argument must be an image File or Blob object.'));
513
+ return;
514
+ }
515
+ if (!URL || !FileReader) {
516
+ this.fail(new Error('The current browser does not support image compression.'));
517
+ return;
518
+ }
519
+ if (!ArrayBuffer) {
520
+ options.checkOrientation = false;
521
+ options.retainExif = false;
522
+ }
523
+ const isJPEGImage = mimeType === 'image/jpeg';
524
+ const checkOrientation = isJPEGImage && options.checkOrientation;
525
+ const retainExif = isJPEGImage && options.retainExif;
526
+ if (URL && !checkOrientation && !retainExif) {
527
+ this.url = URL.createObjectURL(file);
528
+ this.load({
529
+ url: this.url
530
+ });
531
+ } else {
532
+ const reader = new FileReader();
533
+ this.reader = reader;
534
+ reader.onload = ({
535
+ target
536
+ }) => {
537
+ const {
538
+ result
539
+ } = target;
540
+ const data = {};
541
+ let orientation = 1;
542
+ if (checkOrientation) {
543
+ // Reset the orientation value to its default value (1)
544
+ // as some iOS browsers will render image with its orientation
545
+ orientation = resetAndGetOrientation(result);
546
+ if (orientation > 1) {
547
+ Object.assign(data, parseOrientation(orientation));
548
+ }
549
+ }
550
+ if (retainExif) {
551
+ this.exif = getExif(result);
552
+ }
553
+ if (checkOrientation || retainExif) {
554
+ if (!URL
555
+
556
+ // Generate a new URL with the default orientation value (1)
557
+ || orientation > 1) {
558
+ data.url = arrayBufferToDataURL(result, mimeType);
559
+ } else {
560
+ this.url = URL.createObjectURL(file);
561
+ data.url = this.url;
562
+ }
563
+ } else {
564
+ data.url = result;
565
+ }
566
+ this.load(data);
567
+ };
568
+ reader.onabort = () => {
569
+ this.fail(new Error('Aborted to read the image with FileReader.'));
570
+ };
571
+ reader.onerror = () => {
572
+ this.fail(new Error('Failed to read the image with FileReader.'));
573
+ };
574
+ reader.onloadend = () => {
575
+ this.reader = null;
576
+ };
577
+ if (checkOrientation || retainExif) {
578
+ reader.readAsArrayBuffer(file);
579
+ } else {
580
+ reader.readAsDataURL(file);
581
+ }
582
+ }
583
+ }
584
+ load(data) {
585
+ const {
586
+ file,
587
+ image
588
+ } = this;
589
+ image.onload = () => {
590
+ this.draw({
591
+ ...data,
592
+ naturalWidth: image.naturalWidth,
593
+ naturalHeight: image.naturalHeight
594
+ });
595
+ };
596
+ image.onabort = () => {
597
+ this.fail(new Error('Aborted to load the image.'));
598
+ };
599
+ image.onerror = () => {
600
+ this.fail(new Error('Failed to load the image.'));
601
+ };
602
+
603
+ // Match all browsers that use WebKit as the layout engine in iOS devices,
604
+ // such as Safari for iOS, Chrome for iOS, and in-app browsers
605
+ if (WINDOW.navigator && /(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(WINDOW.navigator.userAgent)) {
606
+ // Fix the `The operation is insecure` error (#57)
607
+ image.crossOrigin = 'anonymous';
608
+ }
609
+ image.alt = file.name;
610
+ image.src = data.url;
611
+ }
612
+ draw({
613
+ naturalWidth,
614
+ naturalHeight,
615
+ rotate = 0,
616
+ scaleX = 1,
617
+ scaleY = 1
618
+ }) {
619
+ const {
620
+ file,
621
+ image,
622
+ options
623
+ } = this;
624
+ const canvas = document.createElement('canvas');
625
+ const context = canvas.getContext('2d');
626
+ const is90DegreesRotated = Math.abs(rotate) % 180 === 90;
627
+ const resizable = (options.resize === 'contain' || options.resize === 'cover') && isPositiveNumber(options.width) && isPositiveNumber(options.height);
628
+ let maxWidth = Math.max(options.maxWidth, 0) || Infinity;
629
+ let maxHeight = Math.max(options.maxHeight, 0) || Infinity;
630
+ let minWidth = Math.max(options.minWidth, 0) || 0;
631
+ let minHeight = Math.max(options.minHeight, 0) || 0;
632
+ let aspectRatio = naturalWidth / naturalHeight;
633
+ let {
634
+ width,
635
+ height
636
+ } = options;
637
+ if (is90DegreesRotated) {
638
+ [maxWidth, maxHeight] = [maxHeight, maxWidth];
639
+ [minWidth, minHeight] = [minHeight, minWidth];
640
+ [width, height] = [height, width];
641
+ }
642
+ if (resizable) {
643
+ aspectRatio = width / height;
644
+ }
645
+ ({
646
+ width: maxWidth,
647
+ height: maxHeight
648
+ } = getAdjustedSizes({
649
+ aspectRatio,
650
+ width: maxWidth,
651
+ height: maxHeight
652
+ }, 'contain'));
653
+ ({
654
+ width: minWidth,
655
+ height: minHeight
656
+ } = getAdjustedSizes({
657
+ aspectRatio,
658
+ width: minWidth,
659
+ height: minHeight
660
+ }, 'cover'));
661
+ if (resizable) {
662
+ ({
663
+ width,
664
+ height
665
+ } = getAdjustedSizes({
666
+ aspectRatio,
667
+ width,
668
+ height
669
+ }, options.resize));
670
+ } else {
671
+ ({
672
+ width = naturalWidth,
673
+ height = naturalHeight
674
+ } = getAdjustedSizes({
675
+ aspectRatio,
676
+ width,
677
+ height
678
+ }));
679
+ }
680
+ width = Math.floor(normalizeDecimalNumber(Math.min(Math.max(width, minWidth), maxWidth)));
681
+ height = Math.floor(normalizeDecimalNumber(Math.min(Math.max(height, minHeight), maxHeight)));
682
+ const destX = -width / 2;
683
+ const destY = -height / 2;
684
+ const destWidth = width;
685
+ const destHeight = height;
686
+ const params = [];
687
+ if (resizable) {
688
+ let srcX = 0;
689
+ let srcY = 0;
690
+ let srcWidth = naturalWidth;
691
+ let srcHeight = naturalHeight;
692
+ ({
693
+ width: srcWidth,
694
+ height: srcHeight
695
+ } = getAdjustedSizes({
696
+ aspectRatio,
697
+ width: naturalWidth,
698
+ height: naturalHeight
699
+ }, {
700
+ contain: 'cover',
701
+ cover: 'contain'
702
+ }[options.resize]));
703
+ srcX = (naturalWidth - srcWidth) / 2;
704
+ srcY = (naturalHeight - srcHeight) / 2;
705
+ params.push(srcX, srcY, srcWidth, srcHeight);
706
+ }
707
+ params.push(destX, destY, destWidth, destHeight);
708
+ if (is90DegreesRotated) {
709
+ [width, height] = [height, width];
710
+ }
711
+ canvas.width = width;
712
+ canvas.height = height;
713
+ if (!isImageType(options.mimeType)) {
714
+ options.mimeType = file.type;
715
+ }
716
+ let fillStyle = 'transparent';
717
+
718
+ // Converts PNG files over the `convertSize` to JPEGs.
719
+ if (file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) {
720
+ options.mimeType = 'image/jpeg';
721
+ }
722
+ const isJPEGImage = options.mimeType === 'image/jpeg';
723
+ if (isJPEGImage) {
724
+ fillStyle = '#fff';
725
+ }
726
+
727
+ // Override the default fill color (`#000`, black)
728
+ context.fillStyle = fillStyle;
729
+ context.fillRect(0, 0, width, height);
730
+ if (options.beforeDraw) {
731
+ options.beforeDraw.call(this, context, canvas);
732
+ }
733
+ if (this.aborted) {
734
+ return;
735
+ }
736
+ context.save();
737
+ context.translate(width / 2, height / 2);
738
+ context.rotate(rotate * Math.PI / 180);
739
+ context.scale(scaleX, scaleY);
740
+ context.drawImage(image, ...params);
741
+ context.restore();
742
+ if (options.drew) {
743
+ options.drew.call(this, context, canvas);
744
+ }
745
+ if (this.aborted) {
746
+ return;
747
+ }
748
+ const callback = blob => {
749
+ if (!this.aborted) {
750
+ const done = result => this.done({
751
+ naturalWidth,
752
+ naturalHeight,
753
+ result
754
+ });
755
+ if (blob && isJPEGImage && options.retainExif && this.exif && this.exif.length > 0) {
756
+ const next = arrayBuffer => {
757
+ const withExif = insertExif(arrayBuffer, this.exif);
758
+ done(uint8ArrayToBlob(withExif, options.mimeType));
759
+ };
760
+ if (blob.arrayBuffer) {
761
+ blob.arrayBuffer().then(next).catch(() => {
762
+ this.fail(new Error('Failed to read the compressed image with Blob.arrayBuffer().'));
763
+ });
764
+ } else {
765
+ const reader = new FileReader();
766
+ this.reader = reader;
767
+ reader.onload = ({
768
+ target
769
+ }) => {
770
+ next(target.result);
771
+ };
772
+ reader.onabort = () => {
773
+ this.fail(new Error('Aborted to read the compressed image with FileReader.'));
774
+ };
775
+ reader.onerror = () => {
776
+ this.fail(new Error('Failed to read the compressed image with FileReader.'));
777
+ };
778
+ reader.onloadend = () => {
779
+ this.reader = null;
780
+ };
781
+ reader.readAsArrayBuffer(blob);
782
+ }
783
+ } else {
784
+ done(blob);
785
+ }
786
+ }
787
+ };
788
+ canvas.toBlob(callback, options.mimeType, options.quality);
789
+ }
790
+ done({
791
+ naturalWidth,
792
+ naturalHeight,
793
+ result
794
+ }) {
795
+ const {
796
+ file,
797
+ options
798
+ } = this;
799
+ this.revokeUrl();
800
+ if (result) {
801
+ // Returns original file if the result is greater than it and without size-related options
802
+ if (options.strict && !options.retainExif && result.size > file.size && options.mimeType === file.type && !(options.width > naturalWidth || options.height > naturalHeight || options.minWidth > naturalWidth || options.minHeight > naturalHeight || options.maxWidth < naturalWidth || options.maxHeight < naturalHeight)) {
803
+ result = file;
804
+ } else {
805
+ const date = new Date();
806
+ result.lastModified = date.getTime();
807
+ result.lastModifiedDate = date;
808
+ result.name = file.name;
809
+
810
+ // Convert the extension to match its type
811
+ if (result.name && result.type !== file.type) {
812
+ result.name = result.name.replace(REGEXP_EXTENSION, imageTypeToExtension(result.type));
813
+ }
814
+ }
815
+ } else {
816
+ // Returns original file if the result is null in some cases
817
+ result = file;
818
+ }
819
+ this.result = result;
820
+ if (options.success) {
821
+ options.success.call(this, result);
822
+ }
823
+ }
824
+ fail(err) {
825
+ const {
826
+ options
827
+ } = this;
828
+ this.revokeUrl();
829
+ if (options.error) {
830
+ options.error.call(this, err);
831
+ } else {
832
+ throw err;
833
+ }
834
+ }
835
+ revokeUrl() {
836
+ if (URL && this.url) {
837
+ URL.revokeObjectURL(this.url);
838
+ this.url = null;
839
+ }
840
+ }
841
+ abort() {
842
+ if (!this.aborted) {
843
+ this.aborted = true;
844
+ if (this.reader) {
845
+ this.reader.abort();
846
+ } else if (!this.image.complete) {
847
+ this.image.onload = null;
848
+ this.image.onerror = null;
849
+ this.image.onabort = null;
850
+ this.fail(new Error('Aborted to load the image.'));
851
+ } else {
852
+ this.fail(new Error('The compression process has been aborted.'));
853
+ }
854
+ }
855
+ }
856
+
857
+ /**
858
+ * Change the default options.
859
+ * @param {Object} options - The new default options.
860
+ */
861
+ static setDefaults(options) {
862
+ Object.assign(DEFAULTS, options);
863
+ }
864
+ }
865
+
866
+ module.exports = Compressor;