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,10 @@
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
+ !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).Compressor=t()}(this,function(){"use strict";var e={strict:!0,checkOrientation:!0,retainExif:!1,maxWidth:1/0,maxHeight:1/0,minWidth:0,minHeight:0,width:void 0,height:void 0,resize:"none",quality:.8,mimeType:"auto",convertTypes:["image/png"],convertSize:5e6,beforeDraw:null,drew:null,success:null,error:null};const t="undefined"!=typeof window&&void 0!==window.document?window:{},i=e=>e>0&&e<1/0,r=/^image\/.+$/;function n(e){return r.test(e)}const{fromCharCode:a}=String;const{btoa:o}=t;function s(e){const t=new DataView(e);let i;try{let e,r,n;if(255===t.getUint8(0)&&216===t.getUint8(1)){const e=t.byteLength;let i=2;for(;i+1<e;){if(255===t.getUint8(i)&&225===t.getUint8(i+1)){r=i;break}i+=1}}if(r){const i=r+10;if("Exif"===function(e,t,i){let r,n="";for(i+=t,r=t;r<i;r+=1)n+=a(e.getUint8(r));return n}(t,r+4,4)){const r=t.getUint16(i);if(e=18761===r,(e||19789===r)&&42===t.getUint16(i+2,e)){const r=t.getUint32(i+4,e);r>=8&&(n=i+r)}}}if(n){const r=t.getUint16(n,e);let a,o;for(o=0;o<r;o+=1)if(a=n+12*o+2,274===t.getUint16(a,e)){a+=8,i=t.getUint16(a,e),t.setUint16(a,1,e);break}}}catch{i=1}return i}const h=/\.\d*(?:0|9){12}\d*$/;function l(e,t=1e11){return h.test(e)?Math.round(e*t)/t:e}function c({aspectRatio:e,height:t,width:r},n="none"){const a=i(r),o=i(t);if(a&&o){const i=t*e;("contain"===n||"none"===n)&&i>r||"cover"===n&&i<r?t=r/e:r=t*e}else a?t=r/e:o&&(r=t*e);return{width:r,height:t}}const{ArrayBuffer:d,FileReader:f}=t,u=t.URL||t.webkitURL,g=/\.\w+$/;return class{constructor(t,i){this.file=t,this.exif=[],this.image=new Image,this.options={...e,...i},this.aborted=!1,this.result=null,this.url=null,this.init()}init(){const{file:e,options:t}=this;if(!(e instanceof Blob))return void this.fail(new Error("The first argument must be a File or Blob object."));const i=e.type;if(!n(i))return void this.fail(new Error("The first argument must be an image File or Blob object."));if(!u||!f)return void this.fail(new Error("The current browser does not support image compression."));d||(t.checkOrientation=!1,t.retainExif=!1);const r="image/jpeg"===i,h=r&&t.checkOrientation,l=r&&t.retainExif;if(!u||h||l){const t=new f;this.reader=t,t.onload=({target:t})=>{const{result:r}=t,n={};let c=1;h&&(c=s(r),c>1&&Object.assign(n,function(e){let t=0,i=1,r=1;switch(e){case 2:i=-1;break;case 3:t=-180;break;case 4:r=-1;break;case 5:t=90,r=-1;break;case 6:t=90;break;case 7:t=90,i=-1;break;case 8:t=-90}return{rotate:t,scaleX:i,scaleY:r}}(c))),l&&(this.exif=function(e){const t=new DataView(e),{byteLength:i}=t,r=[];let n=0;for(;n+3<i;){const e=t.getUint8(n),a=t.getUint8(n+1);if(255===e&&218===a)break;if(255===e&&216===a)n+=2;else{const o=n+t.getUint16(n+2)+2;if(255===e&&225===a)for(let e=n;e<o&&e<i;e+=1)r.push(t.getUint8(e));n=o}}return r}(r)),h||l?!u||c>1?n.url=function(e,t){const i=new Uint8Array(e),{length:r}=i;let n="";for(let e=0;e<r;e+=8192){const t=Math.min(e+8192,r);let o="";for(let r=e;r<t;r+=1)o+=a(i[r]);n+=o}return`data:${t};base64,${o(n)}`}(r,i):(this.url=u.createObjectURL(e),n.url=this.url):n.url=r,this.load(n)},t.onabort=()=>{this.fail(new Error("Aborted to read the image with FileReader."))},t.onerror=()=>{this.fail(new Error("Failed to read the image with FileReader."))},t.onloadend=()=>{this.reader=null},h||l?t.readAsArrayBuffer(e):t.readAsDataURL(e)}else this.url=u.createObjectURL(e),this.load({url:this.url})}load(e){const{file:i,image:r}=this;r.onload=()=>{this.draw({...e,naturalWidth:r.naturalWidth,naturalHeight:r.naturalHeight})},r.onabort=()=>{this.fail(new Error("Aborted to load the image."))},r.onerror=()=>{this.fail(new Error("Failed to load the image."))},t.navigator&&/(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(t.navigator.userAgent)&&(r.crossOrigin="anonymous"),r.alt=i.name,r.src=e.url}draw({naturalWidth:e,naturalHeight:t,rotate:r=0,scaleX:a=1,scaleY:o=1}){const{file:s,image:h,options:d}=this,u=document.createElement("canvas"),g=u.getContext("2d"),m=Math.abs(r)%180==90,w=("contain"===d.resize||"cover"===d.resize)&&i(d.width)&&i(d.height);let p=Math.max(d.maxWidth,0)||1/0,b=Math.max(d.maxHeight,0)||1/0,y=Math.max(d.minWidth,0)||0,U=Math.max(d.minHeight,0)||0,x=e/t,{width:v,height:E}=d;m&&([p,b]=[b,p],[y,U]=[U,y],[v,E]=[E,v]),w&&(x=v/E),({width:p,height:b}=c({aspectRatio:x,width:p,height:b},"contain")),({width:y,height:U}=c({aspectRatio:x,width:y,height:U},"cover")),w?({width:v,height:E}=c({aspectRatio:x,width:v,height:E},d.resize)):({width:v=e,height:E=t}=c({aspectRatio:x,width:v,height:E})),v=Math.floor(l(Math.min(Math.max(v,y),p))),E=Math.floor(l(Math.min(Math.max(E,U),b)));const R=-v/2,T=-E/2,k=v,M=E,A=[];if(w){let i=0,r=0,n=e,a=t;({width:n,height:a}=c({aspectRatio:x,width:e,height:t},{contain:"cover",cover:"contain"}[d.resize])),i=(e-n)/2,r=(t-a)/2,A.push(i,r,n,a)}A.push(R,T,k,M),m&&([v,E]=[E,v]),u.width=v,u.height=E,n(d.mimeType)||(d.mimeType=s.type);let j="transparent";s.size>d.convertSize&&d.convertTypes.indexOf(d.mimeType)>=0&&(d.mimeType="image/jpeg");const B="image/jpeg"===d.mimeType;if(B&&(j="#fff"),g.fillStyle=j,g.fillRect(0,0,v,E),d.beforeDraw&&d.beforeDraw.call(this,g,u),this.aborted)return;if(g.save(),g.translate(v/2,E/2),g.rotate(r*Math.PI/180),g.scale(a,o),g.drawImage(h,...A),g.restore(),d.drew&&d.drew.call(this,g,u),this.aborted)return;u.toBlob(i=>{if(!this.aborted){const r=i=>this.done({naturalWidth:e,naturalHeight:t,result:i});if(i&&B&&d.retainExif&&this.exif&&this.exif.length>0){const e=e=>{const t=function(e,t){const i=new DataView(e),r=new Uint8Array(e);if(255!==i.getUint8(2)||224!==i.getUint8(3))return r;const n=4+i.getUint16(4),a=r.byteLength-n,o=new Uint8Array(2+t.length+a);o[0]=255,o[1]=216;for(let e=0;e<t.length;e+=1)o[2+e]=t[e];return o.set(r.subarray(n),2+t.length),o}(e,this.exif);var i,n;r((i=t,n=d.mimeType,new Blob([i],{type:n})))};if(i.arrayBuffer)i.arrayBuffer().then(e).catch(()=>{this.fail(new Error("Failed to read the compressed image with Blob.arrayBuffer()."))});else{const t=new f;this.reader=t,t.onload=({target:t})=>{e(t.result)},t.onabort=()=>{this.fail(new Error("Aborted to read the compressed image with FileReader."))},t.onerror=()=>{this.fail(new Error("Failed to read the compressed image with FileReader."))},t.onloadend=()=>{this.reader=null},t.readAsArrayBuffer(i)}}else r(i)}},d.mimeType,d.quality)}done({naturalWidth:e,naturalHeight:t,result:i}){const{file:r,options:a}=this;if(this.revokeUrl(),i)if(a.strict&&!a.retainExif&&i.size>r.size&&a.mimeType===r.type&&!(a.width>e||a.height>t||a.minWidth>e||a.minHeight>t||a.maxWidth<e||a.maxHeight<t))i=r;else{const e=new Date;i.lastModified=e.getTime(),i.lastModifiedDate=e,i.name=r.name,i.name&&i.type!==r.type&&(i.name=i.name.replace(g,function(e){let t=n(e)?e.slice(6):"";return"jpeg"===t&&(t="jpg"),`.${t}`}(i.type)))}else i=r;this.result=i,a.success&&a.success.call(this,i)}fail(e){const{options:t}=this;if(this.revokeUrl(),!t.error)throw e;t.error.call(this,e)}revokeUrl(){u&&this.url&&(u.revokeObjectURL(this.url),this.url=null)}abort(){this.aborted||(this.aborted=!0,this.reader?this.reader.abort():this.image.complete?this.fail(new Error("The compression process has been aborted.")):(this.image.onload=null,this.image.onerror=null,this.image.onabort=null,this.fail(new Error("Aborted to load the image."))))}static setDefaults(t){Object.assign(e,t)}}});
package/package.json ADDED
@@ -0,0 +1,94 @@
1
+ {
2
+ "author": {
3
+ "name": "Jens Oliver Meiert",
4
+ "url": "https://meiert.com/"
5
+ },
6
+ "browser": "dist/compressor.js",
7
+ "bugs": {
8
+ "url": "https://github.com/j9t/compressorjs-next/issues"
9
+ },
10
+ "contributors": [
11
+ {
12
+ "name": "Chen Fengyuan",
13
+ "url": "https://chenfengyuan.com/"
14
+ }
15
+ ],
16
+ "description": "JavaScript image compressor.",
17
+ "devDependencies": {
18
+ "@babel/core": "^7.29.0",
19
+ "@babel/preset-env": "^7.29.0",
20
+ "@eslint/js": "^9.39.2",
21
+ "@rollup/plugin-babel": "^6.1.0",
22
+ "@rollup/plugin-commonjs": "^29.0.0",
23
+ "@rollup/plugin-node-resolve": "^16.0.3",
24
+ "@vitest/browser-playwright": "^4.0.18",
25
+ "@vitest/coverage-istanbul": "^4.0.18",
26
+ "del-cli": "^7.0.0",
27
+ "eslint": "^9.39.2",
28
+ "husky": "^9.1.7",
29
+ "playwright": "^1.58.2",
30
+ "rollup": "^4.57.1",
31
+ "stylelint": "^17.1.1",
32
+ "stylelint-config-standard": "^40.0.0",
33
+ "stylelint-order": "^7.0.1",
34
+ "terser": "^5.46.0",
35
+ "vitest": "^4.0.18"
36
+ },
37
+ "exports": {
38
+ ".": {
39
+ "import": "./dist/compressor.esm.js",
40
+ "require": "./dist/compressor.common.js"
41
+ }
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "src",
46
+ "types"
47
+ ],
48
+ "homepage": "https://github.com/j9t/compressorjs-next",
49
+ "keywords": [
50
+ "blob",
51
+ "browser",
52
+ "canvas",
53
+ "client-side",
54
+ "compress",
55
+ "compressor",
56
+ "image",
57
+ "image-compression",
58
+ "image-processing",
59
+ "image-upload",
60
+ "javascript",
61
+ "jpeg",
62
+ "jpg",
63
+ "lossy-compression",
64
+ "png",
65
+ "webp"
66
+ ],
67
+ "license": "MIT",
68
+ "main": "dist/compressor.esm.js",
69
+ "name": "compressorjs-next",
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "git+https://github.com/j9t/compressorjs-next.git"
73
+ },
74
+ "scripts": {
75
+ "build": "rollup -c",
76
+ "clean": "del-cli dist",
77
+ "compress": "terser dist/compressor.js -o dist/compressor.min.js -c -m --comments /^!/",
78
+ "dev": "rollup -c -m -w",
79
+ "lint": "npm run lint:js && npm run lint:css",
80
+ "lint:css": "stylelint docs/**/*.css --fix",
81
+ "lint:js": "eslint src test",
82
+ "prepare": "husky",
83
+ "pretest": "playwright install chromium",
84
+ "release": "npm run clean && npm run lint && npm run build && npm run compress && npm test",
85
+ "start": "npm run dev",
86
+ "test": "vitest run",
87
+ "test:coverage": "vitest run --coverage",
88
+ "test:watch": "vitest"
89
+ },
90
+ "sideEffects": false,
91
+ "type": "module",
92
+ "types": "types/index.d.ts",
93
+ "version": "1.0.0"
94
+ }
@@ -0,0 +1,3 @@
1
+ export const WINDOW = (typeof window !== 'undefined' && typeof window.document !== 'undefined')
2
+ ? window
3
+ : {};
@@ -0,0 +1,142 @@
1
+ export default {
2
+ /**
3
+ * Indicates if output the original image instead of the compressed one
4
+ * when the size of the compressed image is greater than the original one’s
5
+ * @type {boolean}
6
+ */
7
+ strict: true,
8
+
9
+ /**
10
+ * Indicates if read the image’s Exif Orientation information,
11
+ * and then rotate or flip the image automatically.
12
+ * @type {boolean}
13
+ */
14
+ checkOrientation: true,
15
+
16
+ /**
17
+ * Indicates if retain the image’s Exif information after compressed.
18
+ * @type {boolean}
19
+ */
20
+ retainExif: false,
21
+
22
+ /**
23
+ * The max width of the output image.
24
+ * @type {number}
25
+ */
26
+ maxWidth: Infinity,
27
+
28
+ /**
29
+ * The max height of the output image.
30
+ * @type {number}
31
+ */
32
+ maxHeight: Infinity,
33
+
34
+ /**
35
+ * The min width of the output image.
36
+ * @type {number}
37
+ */
38
+ minWidth: 0,
39
+
40
+ /**
41
+ * The min height of the output image.
42
+ * @type {number}
43
+ */
44
+ minHeight: 0,
45
+
46
+ /**
47
+ * The width of the output image.
48
+ * If not specified, the natural width of the source image will be used.
49
+ * @type {number}
50
+ */
51
+ width: undefined,
52
+
53
+ /**
54
+ * The height of the output image.
55
+ * If not specified, the natural height of the source image will be used.
56
+ * @type {number}
57
+ */
58
+ height: undefined,
59
+
60
+ /**
61
+ * Sets how the size of the image should be resized to the container
62
+ * specified by the `width` and `height` options.
63
+ * @type {string}
64
+ */
65
+ resize: 'none',
66
+
67
+ /**
68
+ * The quality of the output image.
69
+ * It must be a number between `0` and `1`,
70
+ * and only available for `image/jpeg` and `image/webp` images.
71
+ * Check out {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob canvas.toBlob}.
72
+ * @type {number}
73
+ */
74
+ quality: 0.8,
75
+
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
+ /**
84
+ * Files whose file type is included in this list,
85
+ * and whose file size exceeds the `convertSize` value will be converted to JPEGs.
86
+ * @type {string|Array}
87
+ */
88
+ convertTypes: ['image/png'],
89
+
90
+ /**
91
+ * PNG files over this size (5 MB by default) will be converted to JPEGs.
92
+ * To disable this, just set the value to `Infinity`.
93
+ * @type {number}
94
+ */
95
+ convertSize: 5000000,
96
+
97
+ /**
98
+ * The hook function to execute before draw the image into the canvas for compression.
99
+ * @type {Function}
100
+ * @param {CanvasRenderingContext2D} context - The 2d rendering context of the canvas.
101
+ * @param {HTMLCanvasElement} canvas - The canvas for compression.
102
+ * @example
103
+ * function (context, canvas) {
104
+ * context.fillStyle = '#fff';
105
+ * }
106
+ */
107
+ beforeDraw: null,
108
+
109
+ /**
110
+ * The hook function to execute after drew the image into the canvas for compression.
111
+ * @type {Function}
112
+ * @param {CanvasRenderingContext2D} context - The 2d rendering context of the canvas.
113
+ * @param {HTMLCanvasElement} canvas - The canvas for compression.
114
+ * @example
115
+ * function (context, canvas) {
116
+ * context.filter = 'grayscale(100%)';
117
+ * }
118
+ */
119
+ drew: null,
120
+
121
+ /**
122
+ * The hook function to execute when success to compress the image.
123
+ * @type {Function}
124
+ * @param {File} file - The compressed image File object.
125
+ * @example
126
+ * function (file) {
127
+ * console.log(file);
128
+ * }
129
+ */
130
+ success: null,
131
+
132
+ /**
133
+ * The hook function to execute when fail to compress the image.
134
+ * @type {Function}
135
+ * @param {Error} err - An Error object.
136
+ * @example
137
+ * function (err) {
138
+ * console.log(err.message);
139
+ * }
140
+ */
141
+ error: null,
142
+ };
package/src/index.js ADDED
@@ -0,0 +1,445 @@
1
+ import DEFAULTS from './defaults';
2
+ import {
3
+ WINDOW,
4
+ } from './constants';
5
+ import {
6
+ getAdjustedSizes,
7
+ imageTypeToExtension,
8
+ isImageType,
9
+ isPositiveNumber,
10
+ normalizeDecimalNumber,
11
+ parseOrientation,
12
+ resetAndGetOrientation,
13
+ arrayBufferToDataURL,
14
+ getExif,
15
+ insertExif,
16
+ uint8ArrayToBlob,
17
+ } from './utilities';
18
+
19
+ const { ArrayBuffer, FileReader } = WINDOW;
20
+ const URL = WINDOW.URL || WINDOW.webkitURL;
21
+ const REGEXP_EXTENSION = /\.\w+$/;
22
+
23
+ /**
24
+ * Creates a new image compressor.
25
+ * @class
26
+ */
27
+ export default class Compressor {
28
+ /**
29
+ * The constructor of Compressor.
30
+ * @param {File|Blob} file - The target image file for compressing.
31
+ * @param {Object} [options] - The options for compressing.
32
+ */
33
+ constructor(file, options) {
34
+ this.file = file;
35
+ this.exif = [];
36
+ this.image = new Image();
37
+ this.options = {
38
+ ...DEFAULTS,
39
+ ...options,
40
+ };
41
+ this.aborted = false;
42
+ this.result = null;
43
+ this.url = null;
44
+ this.init();
45
+ }
46
+
47
+ init() {
48
+ const { file, options } = this;
49
+
50
+ if (!(file instanceof Blob)) {
51
+ this.fail(new Error('The first argument must be a File or Blob object.'));
52
+ return;
53
+ }
54
+
55
+ const mimeType = file.type;
56
+
57
+ if (!isImageType(mimeType)) {
58
+ this.fail(new Error('The first argument must be an image File or Blob object.'));
59
+ return;
60
+ }
61
+
62
+ if (!URL || !FileReader) {
63
+ this.fail(new Error('The current browser does not support image compression.'));
64
+ return;
65
+ }
66
+
67
+ if (!ArrayBuffer) {
68
+ options.checkOrientation = false;
69
+ options.retainExif = false;
70
+ }
71
+
72
+ const isJPEGImage = mimeType === 'image/jpeg';
73
+ const checkOrientation = isJPEGImage && options.checkOrientation;
74
+ const retainExif = isJPEGImage && options.retainExif;
75
+
76
+ if (URL && !checkOrientation && !retainExif) {
77
+ this.url = URL.createObjectURL(file);
78
+ this.load({
79
+ url: this.url,
80
+ });
81
+ } else {
82
+ const reader = new FileReader();
83
+
84
+ this.reader = reader;
85
+ reader.onload = ({ target }) => {
86
+ const { result } = target;
87
+ const data = {};
88
+ let orientation = 1;
89
+
90
+ if (checkOrientation) {
91
+ // Reset the orientation value to its default value (1)
92
+ // as some iOS browsers will render image with its orientation
93
+ orientation = resetAndGetOrientation(result);
94
+
95
+ if (orientation > 1) {
96
+ Object.assign(data, parseOrientation(orientation));
97
+ }
98
+ }
99
+
100
+ if (retainExif) {
101
+ this.exif = getExif(result);
102
+ }
103
+
104
+ if (checkOrientation || retainExif) {
105
+ if (
106
+ !URL
107
+
108
+ // Generate a new URL with the default orientation value (1)
109
+ || orientation > 1
110
+ ) {
111
+ data.url = arrayBufferToDataURL(result, mimeType);
112
+ } else {
113
+ this.url = URL.createObjectURL(file);
114
+ data.url = this.url;
115
+ }
116
+ } else {
117
+ data.url = result;
118
+ }
119
+
120
+ this.load(data);
121
+ };
122
+ reader.onabort = () => {
123
+ this.fail(new Error('Aborted to read the image with FileReader.'));
124
+ };
125
+ reader.onerror = () => {
126
+ this.fail(new Error('Failed to read the image with FileReader.'));
127
+ };
128
+ reader.onloadend = () => {
129
+ this.reader = null;
130
+ };
131
+
132
+ if (checkOrientation || retainExif) {
133
+ reader.readAsArrayBuffer(file);
134
+ } else {
135
+ reader.readAsDataURL(file);
136
+ }
137
+ }
138
+ }
139
+
140
+ load(data) {
141
+ const { file, image } = this;
142
+
143
+ image.onload = () => {
144
+ this.draw({
145
+ ...data,
146
+ naturalWidth: image.naturalWidth,
147
+ naturalHeight: image.naturalHeight,
148
+ });
149
+ };
150
+ image.onabort = () => {
151
+ this.fail(new Error('Aborted to load the image.'));
152
+ };
153
+ image.onerror = () => {
154
+ this.fail(new Error('Failed to load the image.'));
155
+ };
156
+
157
+ // Match all browsers that use WebKit as the layout engine in iOS devices,
158
+ // such as Safari for iOS, Chrome for iOS, and in-app browsers
159
+ if (WINDOW.navigator && /(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(WINDOW.navigator.userAgent)) {
160
+ // Fix the `The operation is insecure` error (#57)
161
+ image.crossOrigin = 'anonymous';
162
+ }
163
+
164
+ image.alt = file.name;
165
+ image.src = data.url;
166
+ }
167
+
168
+ draw({
169
+ naturalWidth,
170
+ naturalHeight,
171
+ rotate = 0,
172
+ scaleX = 1,
173
+ scaleY = 1,
174
+ }) {
175
+ const { file, image, options } = this;
176
+ const canvas = document.createElement('canvas');
177
+ const context = canvas.getContext('2d');
178
+ const is90DegreesRotated = Math.abs(rotate) % 180 === 90;
179
+ const resizable = (options.resize === 'contain' || options.resize === 'cover') && isPositiveNumber(options.width) && isPositiveNumber(options.height);
180
+ let maxWidth = Math.max(options.maxWidth, 0) || Infinity;
181
+ let maxHeight = Math.max(options.maxHeight, 0) || Infinity;
182
+ let minWidth = Math.max(options.minWidth, 0) || 0;
183
+ let minHeight = Math.max(options.minHeight, 0) || 0;
184
+ let aspectRatio = naturalWidth / naturalHeight;
185
+ let { width, height } = options;
186
+
187
+ if (is90DegreesRotated) {
188
+ [maxWidth, maxHeight] = [maxHeight, maxWidth];
189
+ [minWidth, minHeight] = [minHeight, minWidth];
190
+ [width, height] = [height, width];
191
+ }
192
+
193
+ if (resizable) {
194
+ aspectRatio = width / height;
195
+ }
196
+
197
+ ({ width: maxWidth, height: maxHeight } = getAdjustedSizes({
198
+ aspectRatio,
199
+ width: maxWidth,
200
+ height: maxHeight,
201
+ }, 'contain'));
202
+ ({ width: minWidth, height: minHeight } = getAdjustedSizes({
203
+ aspectRatio,
204
+ width: minWidth,
205
+ height: minHeight,
206
+ }, 'cover'));
207
+
208
+ if (resizable) {
209
+ ({ width, height } = getAdjustedSizes({
210
+ aspectRatio,
211
+ width,
212
+ height,
213
+ }, options.resize));
214
+ } else {
215
+ ({ width = naturalWidth, height = naturalHeight } = getAdjustedSizes({
216
+ aspectRatio,
217
+ width,
218
+ height,
219
+ }));
220
+ }
221
+
222
+ width = Math.floor(normalizeDecimalNumber(Math.min(Math.max(width, minWidth), maxWidth)));
223
+ height = Math.floor(normalizeDecimalNumber(Math.min(Math.max(height, minHeight), maxHeight)));
224
+
225
+ const destX = -width / 2;
226
+ const destY = -height / 2;
227
+ const destWidth = width;
228
+ const destHeight = height;
229
+ const params = [];
230
+
231
+ if (resizable) {
232
+ let srcX = 0;
233
+ let srcY = 0;
234
+ let srcWidth = naturalWidth;
235
+ let srcHeight = naturalHeight;
236
+
237
+ ({ width: srcWidth, height: srcHeight } = getAdjustedSizes({
238
+ aspectRatio,
239
+ width: naturalWidth,
240
+ height: naturalHeight,
241
+ }, {
242
+ contain: 'cover',
243
+ cover: 'contain',
244
+ }[options.resize]));
245
+ srcX = (naturalWidth - srcWidth) / 2;
246
+ srcY = (naturalHeight - srcHeight) / 2;
247
+
248
+ params.push(srcX, srcY, srcWidth, srcHeight);
249
+ }
250
+
251
+ params.push(destX, destY, destWidth, destHeight);
252
+
253
+ if (is90DegreesRotated) {
254
+ [width, height] = [height, width];
255
+ }
256
+
257
+ canvas.width = width;
258
+ canvas.height = height;
259
+
260
+ if (!isImageType(options.mimeType)) {
261
+ options.mimeType = file.type;
262
+ }
263
+
264
+ let fillStyle = 'transparent';
265
+
266
+ // Converts PNG files over the `convertSize` to JPEGs.
267
+ if (file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) {
268
+ options.mimeType = 'image/jpeg';
269
+ }
270
+
271
+ const isJPEGImage = options.mimeType === 'image/jpeg';
272
+
273
+ if (isJPEGImage) {
274
+ fillStyle = '#fff';
275
+ }
276
+
277
+ // Override the default fill color (`#000`, black)
278
+ context.fillStyle = fillStyle;
279
+ context.fillRect(0, 0, width, height);
280
+
281
+ if (options.beforeDraw) {
282
+ options.beforeDraw.call(this, context, canvas);
283
+ }
284
+
285
+ if (this.aborted) {
286
+ return;
287
+ }
288
+
289
+ context.save();
290
+ context.translate(width / 2, height / 2);
291
+ context.rotate((rotate * Math.PI) / 180);
292
+ context.scale(scaleX, scaleY);
293
+ context.drawImage(image, ...params);
294
+ context.restore();
295
+
296
+ if (options.drew) {
297
+ options.drew.call(this, context, canvas);
298
+ }
299
+
300
+ if (this.aborted) {
301
+ return;
302
+ }
303
+
304
+ const callback = (blob) => {
305
+ if (!this.aborted) {
306
+ const done = (result) => this.done({
307
+ naturalWidth,
308
+ naturalHeight,
309
+ result,
310
+ });
311
+
312
+ if (blob && isJPEGImage && options.retainExif && this.exif && this.exif.length > 0) {
313
+ const next = (arrayBuffer) => {
314
+ const withExif = insertExif(arrayBuffer, this.exif);
315
+ done(uint8ArrayToBlob(withExif, options.mimeType));
316
+ };
317
+
318
+ if (blob.arrayBuffer) {
319
+ blob.arrayBuffer().then(next).catch(() => {
320
+ this.fail(new Error('Failed to read the compressed image with Blob.arrayBuffer().'));
321
+ });
322
+ } else {
323
+ const reader = new FileReader();
324
+
325
+ this.reader = reader;
326
+ reader.onload = ({ target }) => {
327
+ next(target.result);
328
+ };
329
+ reader.onabort = () => {
330
+ this.fail(new Error('Aborted to read the compressed image with FileReader.'));
331
+ };
332
+ reader.onerror = () => {
333
+ this.fail(new Error('Failed to read the compressed image with FileReader.'));
334
+ };
335
+ reader.onloadend = () => {
336
+ this.reader = null;
337
+ };
338
+ reader.readAsArrayBuffer(blob);
339
+ }
340
+ } else {
341
+ done(blob);
342
+ }
343
+ }
344
+ };
345
+
346
+ canvas.toBlob(callback, options.mimeType, options.quality);
347
+ }
348
+
349
+ done({
350
+ naturalWidth,
351
+ naturalHeight,
352
+ result,
353
+ }) {
354
+ const { file, options } = this;
355
+
356
+ this.revokeUrl();
357
+
358
+ if (result) {
359
+ // Returns original file if the result is greater than it and without size-related options
360
+ if (
361
+ options.strict
362
+ && !options.retainExif
363
+ && result.size > file.size
364
+ && options.mimeType === file.type
365
+ && !(
366
+ options.width > naturalWidth
367
+ || options.height > naturalHeight
368
+ || options.minWidth > naturalWidth
369
+ || options.minHeight > naturalHeight
370
+ || options.maxWidth < naturalWidth
371
+ || options.maxHeight < naturalHeight
372
+ )
373
+ ) {
374
+ result = file;
375
+ } else {
376
+ const date = new Date();
377
+
378
+ result.lastModified = date.getTime();
379
+ result.lastModifiedDate = date;
380
+ result.name = file.name;
381
+
382
+ // Convert the extension to match its type
383
+ if (result.name && result.type !== file.type) {
384
+ result.name = result.name.replace(
385
+ REGEXP_EXTENSION,
386
+ imageTypeToExtension(result.type),
387
+ );
388
+ }
389
+ }
390
+ } else {
391
+ // Returns original file if the result is null in some cases
392
+ result = file;
393
+ }
394
+
395
+ this.result = result;
396
+
397
+ if (options.success) {
398
+ options.success.call(this, result);
399
+ }
400
+ }
401
+
402
+ fail(err) {
403
+ const { options } = this;
404
+
405
+ this.revokeUrl();
406
+
407
+ if (options.error) {
408
+ options.error.call(this, err);
409
+ } else {
410
+ throw err;
411
+ }
412
+ }
413
+
414
+ revokeUrl() {
415
+ if (URL && this.url) {
416
+ URL.revokeObjectURL(this.url);
417
+ this.url = null;
418
+ }
419
+ }
420
+
421
+ abort() {
422
+ if (!this.aborted) {
423
+ this.aborted = true;
424
+
425
+ if (this.reader) {
426
+ this.reader.abort();
427
+ } else if (!this.image.complete) {
428
+ this.image.onload = null;
429
+ this.image.onerror = null;
430
+ this.image.onabort = null;
431
+ this.fail(new Error('Aborted to load the image.'));
432
+ } else {
433
+ this.fail(new Error('The compression process has been aborted.'));
434
+ }
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Change the default options.
440
+ * @param {Object} options - The new default options.
441
+ */
442
+ static setDefaults(options) {
443
+ Object.assign(DEFAULTS, options);
444
+ }
445
+ }