UNPKG

@taro-hooks/compressorjs

Version:
898 lines (859 loc) 27.7 kB
/*! * Compressor.js v2.2.0 * https://fengyuanchen.github.io/compressorjs * * Copyright 2018-present Chen Fengyuan * Released under the MIT license * * Date: 2025-04-06T12:41:00.917Z */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Compressor = factory()); })(this, (function () { 'use strict'; function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } var canvasToBlob = {exports: {}}; /* * JavaScript Canvas to Blob * https://github.com/blueimp/JavaScript-Canvas-to-Blob * * Copyright 2012, Sebastian Tschan * https://blueimp.net * * Licensed under the MIT license: * https://opensource.org/licenses/MIT * * Based on stackoverflow user Stoive's code snippet: * http://stackoverflow.com/q/4998908 */ (function (module) { if (typeof window === 'undefined') { return; } if (typeof window === 'undefined') { return; } (function (window) { var CanvasPrototype = window.HTMLCanvasElement && window.HTMLCanvasElement.prototype; var hasCanvasPrototype = !!CanvasPrototype; var hasBlobConstructor = window.Blob && function () { try { return Boolean(new Blob()); } catch (e) { return false; } }(); var hasArrayBufferViewSupport = hasBlobConstructor && window.Uint8Array && function () { try { return new Blob([new Uint8Array(100)]).size === 100; } catch (e) { return false; } }(); var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder; var dataURIPattern = /^data:((.*?)(;charset=.*?)?)(;base64)?,/; var dataURLtoBlob = (hasBlobConstructor || BlobBuilder) && window.atob && window.ArrayBuffer && window.Uint8Array && function (dataURI) { var matches, mediaType, isBase64, dataString, byteString, arrayBuffer, intArray, i, bb; // Parse the dataURI components as per RFC 2397 matches = dataURI.match(dataURIPattern); if (!matches) { throw new Error('invalid data URI'); } // Default to text/plain;charset=US-ASCII mediaType = matches[2] ? matches[1] : 'text/plain' + (matches[3] || ';charset=US-ASCII'); isBase64 = !!matches[4]; dataString = dataURI.slice(matches[0].length); if (isBase64) { // Convert base64 to raw binary data held in a string: byteString = atob(dataString); } else { // Convert base64/URLEncoded data component to raw binary: byteString = decodeURIComponent(dataString); } // Write the bytes of the string to an ArrayBuffer: arrayBuffer = new ArrayBuffer(byteString.length); intArray = new Uint8Array(arrayBuffer); for (i = 0; i < byteString.length; i += 1) { intArray[i] = byteString.charCodeAt(i); } // Write the ArrayBuffer (or ArrayBufferView) to a blob: if (hasBlobConstructor) { return new Blob([hasArrayBufferViewSupport ? intArray : arrayBuffer], { type: mediaType }); } bb = new BlobBuilder(); bb.append(arrayBuffer); return bb.getBlob(mediaType); }; if (hasCanvasPrototype) { if (window.HTMLCanvasElement && !CanvasPrototype.toBlob) { if (CanvasPrototype.mozGetAsFile) { CanvasPrototype.toBlob = function (callback, type, quality) { var self = this; setTimeout(function () { if (quality && CanvasPrototype.toDataURL && dataURLtoBlob) { callback(dataURLtoBlob(self.toDataURL(type, quality))); } else { callback(self.mozGetAsFile('blob', type)); } }); }; } else if (CanvasPrototype.toDataURL && dataURLtoBlob) { if (CanvasPrototype.msToBlob) { CanvasPrototype.toBlob = function (callback, type, quality) { var self = this; setTimeout(function () { if ((type && type !== 'image/png' || quality) && CanvasPrototype.toDataURL && dataURLtoBlob) { callback(dataURLtoBlob(self.toDataURL(type, quality))); } else { callback(self.msToBlob(type)); } }); }; } else { CanvasPrototype.toBlob = function (callback, type, quality) { var self = this; setTimeout(function () { callback(dataURLtoBlob(self.toDataURL(type, quality))); }); }; } } } } if (module.exports) { module.exports = dataURLtoBlob; } else { window.dataURLtoBlob = dataURLtoBlob; } })(globalThis || window); // shims globalThis to miniprogram })(canvasToBlob); var toBlob = canvasToBlob.exports; var isBlob = value => { if (typeof Blob === 'undefined') { return false; } return value instanceof Blob || Object.prototype.toString.call(value) === '[object Blob]'; }; var DEFAULTS = { /** * Indicates if output the original image instead of the compressed one * when the size of the compressed image is greater than the original one's * @type {boolean} */ strict: true, /** * Indicates if read the image's Exif Orientation information, * and then rotate or flip the image automatically. * @type {boolean} */ checkOrientation: true, /** * The max width of the output image. * @type {number} */ maxWidth: Infinity, /** * The max height of the output image. * @type {number} */ maxHeight: Infinity, /** * The min width of the output image. * @type {number} */ minWidth: 0, /** * The min height of the output image. * @type {number} */ minHeight: 0, /** * The width of the output image. * If not specified, the natural width of the source image will be used. * @type {number} */ width: undefined, /** * The height of the output image. * If not specified, the natural height of the source image will be used. * @type {number} */ height: undefined, /** * Sets how the size of the image should be resized to the container * specified by the `width` and `height` options. * @type {string} */ resize: 'none', /** * The quality of the output image. * It must be a number between `0` and `1`, * and only available for `image/jpeg` and `image/webp` images. * Check out {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob canvas.toBlob}. * @type {number} */ quality: 0.8, /** * The mime type of the output image. * By default, the original mime type of the source image file will be used. * @type {string} */ mimeType: 'auto', /** * Files whose file type is included in this list, * and whose file size exceeds the `convertSize` value will be converted to JPEGs. * @type {string|Array} */ convertTypes: ['image/png'], /** * PNG files over this size (5 MB by default) will be converted to JPEGs. * To disable this, just set the value to `Infinity`. * @type {number} */ convertSize: 5000000, /** * The hook function to execute before draw the image into the canvas for compression. * @type {Function} * @param {CanvasRenderingContext2D} context - The 2d rendering context of the canvas. * @param {HTMLCanvasElement} canvas - The canvas for compression. * @example * function (context, canvas) { * context.fillStyle = '#fff'; * } */ beforeDraw: null, /** * The hook function to execute after drew the image into the canvas for compression. * @type {Function} * @param {CanvasRenderingContext2D} context - The 2d rendering context of the canvas. * @param {HTMLCanvasElement} canvas - The canvas for compression. * @example * function (context, canvas) { * context.filter = 'grayscale(100%)'; * } */ drew: null, /** * The hook function to execute when success to compress the image. * @type {Function} * @param {File} file - The compressed image File object. * @example * function (file) { * console.log(file); * } */ success: null, /** * The hook function to execute when fail to compress the image. * @type {Function} * @param {Error} err - An Error object. * @example * function (err) { * console.log(err.message); * } */ error: null }; const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined'; const WINDOW = IS_BROWSER ? window : {}; /** * Check if the given value is a positive number. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given value is a positive number, else `false`. */ const isPositiveNumber = value => value > 0 && value < Infinity; const { slice } = Array.prototype; /** * Convert array-like or iterable object to an array. * @param {*} value - The value to convert. * @returns {Array} Returns a new array. */ function toArray(value) { return Array.from ? Array.from(value) : slice.call(value); } const REGEXP_IMAGE_TYPE = /^image\/.+$/; /** * Check if the given value is a mime type of image. * @param {*} value - The value to check. * @returns {boolean} Returns `true` if the given is a mime type of image, else `false`. */ function isImageType(value) { return REGEXP_IMAGE_TYPE.test(value); } /** * Convert image type to extension. * @param {string} value - The image type to convert. * @returns {boolean} Returns the image extension. */ function imageTypeToExtension(value) { let extension = isImageType(value) ? value.substr(6) : ''; if (extension === 'jpeg') { extension = 'jpg'; } return `.${extension}`; } const { fromCharCode } = String; /** * Get string from char code in data view. * @param {DataView} dataView - The data view for read. * @param {number} start - The start index. * @param {number} length - The read length. * @returns {string} The read result. */ function getStringFromCharCode(dataView, start, length) { let str = ''; let i; length += start; for (i = start; i < length; i += 1) { str += fromCharCode(dataView.getUint8(i)); } return str; } const { btoa } = WINDOW; /** * Transform array buffer to Data URL. * @param {ArrayBuffer} arrayBuffer - The array buffer to transform. * @param {string} mimeType - The mime type of the Data URL. * @returns {string} The result Data URL. */ function arrayBufferToDataURL(arrayBuffer, mimeType) { const chunks = []; const chunkSize = 8192; let uint8 = new Uint8Array(arrayBuffer); while (uint8.length > 0) { // XXX: Babel's `toConsumableArray` helper will throw error in IE or Safari 9 // eslint-disable-next-line prefer-spread chunks.push(fromCharCode.apply(null, toArray(uint8.subarray(0, chunkSize)))); uint8 = uint8.subarray(chunkSize); } return `data:${mimeType};base64,${btoa(chunks.join(''))}`; } /** * Get orientation value from given array buffer. * @param {ArrayBuffer} arrayBuffer - The array buffer to read. * @returns {number} The read orientation value. */ function resetAndGetOrientation(arrayBuffer) { const dataView = new DataView(arrayBuffer); let orientation; // Ignores range error when the image does not have correct Exif information try { let littleEndian; let app1Start; let ifdStart; // Only handle JPEG image (start by 0xFFD8) if (dataView.getUint8(0) === 0xff && dataView.getUint8(1) === 0xd8) { const length = dataView.byteLength; let offset = 2; while (offset + 1 < length) { if (dataView.getUint8(offset) === 0xff && dataView.getUint8(offset + 1) === 0xe1) { app1Start = offset; break; } offset += 1; } } if (app1Start) { const exifIDCode = app1Start + 4; const tiffOffset = app1Start + 10; if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') { const endianness = dataView.getUint16(tiffOffset); littleEndian = endianness === 0x4949; if (littleEndian || endianness === 0x4d4d /* bigEndian */) { if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002a) { const firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian); if (firstIFDOffset >= 0x00000008) { ifdStart = tiffOffset + firstIFDOffset; } } } } } if (ifdStart) { const length = dataView.getUint16(ifdStart, littleEndian); let offset; let i; for (i = 0; i < length; i += 1) { offset = ifdStart + i * 12 + 2; if (dataView.getUint16(offset, littleEndian) === 0x0112 /* Orientation */) { // 8 is the offset of the current tag's value offset += 8; // Get the original orientation value orientation = dataView.getUint16(offset, littleEndian); // Override the orientation with its default value dataView.setUint16(offset, 1, littleEndian); break; } } } } catch (e) { orientation = 1; } return orientation; } /** * Parse Exif Orientation value. * @param {number} orientation - The orientation to parse. * @returns {Object} The parsed result. */ function parseOrientation(orientation) { let rotate = 0; let scaleX = 1; let scaleY = 1; switch (orientation) { // Flip horizontal case 2: scaleX = -1; break; // Rotate left 180° case 3: rotate = -180; break; // Flip vertical case 4: scaleY = -1; break; // Flip vertical and rotate right 90° case 5: rotate = 90; scaleY = -1; break; // Rotate right 90° case 6: rotate = 90; break; // Flip horizontal and rotate right 90° case 7: rotate = 90; scaleX = -1; break; // Rotate left 90° case 8: rotate = -90; break; } return { rotate, scaleX, scaleY }; } const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/; /** * Normalize decimal number. * Check out {@link https://0.30000000000000004.com/} * @param {number} value - The value to normalize. * @param {number} [times=100000000000] - The times for normalizing. * @returns {number} Returns the normalized number. */ function normalizeDecimalNumber(value) { let times = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100000000000; return REGEXP_DECIMALS.test(value) ? Math.round(value * times) / times : value; } /** * Get the max sizes in a rectangle under the given aspect ratio. * @param {Object} data - The original sizes. * @param {string} [type='contain'] - The adjust type. * @returns {Object} The result sizes. */ function getAdjustedSizes(_ref) { let { aspectRatio, height, width } = _ref; let type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'none'; const isValidWidth = isPositiveNumber(width); const isValidHeight = isPositiveNumber(height); if (isValidWidth && isValidHeight) { const adjustedWidth = height * aspectRatio; if ((type === 'contain' || type === 'none') && adjustedWidth > width || type === 'cover' && adjustedWidth < width) { height = width / aspectRatio; } else { width = height * aspectRatio; } } else if (isValidWidth) { height = width / aspectRatio; } else if (isValidHeight) { width = height * aspectRatio; } return { width, height }; } const { ArrayBuffer: ArrayBuffer$1, FileReader } = WINDOW; const URL = WINDOW.URL || WINDOW.webkitURL; const REGEXP_EXTENSION = /\.\w+$/; const AnotherCompressor = WINDOW.Compressor; /** * Creates a new image compressor. * @class */ class Compressor { /** * The constructor of Compressor. * @param {File|Blob} file - The target image file for compressing. * @param {Object} [options] - The options for compressing. */ constructor(file, options) { this.file = file; this.image = new Image(); this.options = { ...DEFAULTS, ...options }; this.aborted = false; this.result = null; this.init(); } init() { const { file, options } = this; if (!isBlob(file)) { this.fail(new Error('The first argument must be a File or Blob object.')); return; } const mimeType = file.type; if (!isImageType(mimeType)) { this.fail(new Error('The first argument must be an image File or Blob object.')); return; } if (!URL || !FileReader) { this.fail(new Error('The current browser does not support image compression.')); return; } if (!ArrayBuffer$1) { options.checkOrientation = false; } if (URL && !options.checkOrientation) { this.load({ url: URL.createObjectURL(file) }); } else { const reader = new FileReader(); const checkOrientation = options.checkOrientation && mimeType === 'image/jpeg'; this.reader = reader; reader.onload = _ref => { let { target } = _ref; const { result } = target; const data = {}; if (checkOrientation) { // Reset the orientation value to its default value 1 // as some iOS browsers will render image with its orientation const orientation = resetAndGetOrientation(result); if (orientation > 1 || !URL) { // Generate a new URL which has the default orientation value data.url = arrayBufferToDataURL(result, mimeType); if (orientation > 1) { _extends(data, parseOrientation(orientation)); } } else { data.url = URL.createObjectURL(file); } } else { data.url = result; } this.load(data); }; reader.onabort = () => { this.fail(new Error('Aborted to read the image with FileReader.')); }; reader.onerror = () => { this.fail(new Error('Failed to read the image with FileReader.')); }; reader.onloadend = () => { this.reader = null; }; if (checkOrientation) { reader.readAsArrayBuffer(file); } else { reader.readAsDataURL(file); } } } load(data) { const { file, image } = this; image.onload = () => { this.draw({ ...data, naturalWidth: image.naturalWidth, naturalHeight: image.naturalHeight }); }; image.onabort = () => { this.fail(new Error('Aborted to load the image.')); }; image.onerror = () => { this.fail(new Error('Failed to load the image.')); }; // Match all browsers that use WebKit as the layout engine in iOS devices, // such as Safari for iOS, Chrome for iOS, and in-app browsers. if (WINDOW.navigator && /(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(WINDOW.navigator.userAgent)) { // Fix the `The operation is insecure` error (#57) image.crossOrigin = 'anonymous'; } image.alt = file.name; image.src = data.url; } draw(_ref2) { let { naturalWidth, naturalHeight, rotate = 0, scaleX = 1, scaleY = 1 } = _ref2; const { file, image, options } = this; const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const is90DegreesRotated = Math.abs(rotate) % 180 === 90; const resizable = (options.resize === 'contain' || options.resize === 'cover') && isPositiveNumber(options.width) && isPositiveNumber(options.height); let maxWidth = Math.max(options.maxWidth, 0) || Infinity; let maxHeight = Math.max(options.maxHeight, 0) || Infinity; let minWidth = Math.max(options.minWidth, 0) || 0; let minHeight = Math.max(options.minHeight, 0) || 0; let aspectRatio = naturalWidth / naturalHeight; let { width, height } = options; if (is90DegreesRotated) { [maxWidth, maxHeight] = [maxHeight, maxWidth]; [minWidth, minHeight] = [minHeight, minWidth]; [width, height] = [height, width]; } if (resizable) { aspectRatio = width / height; } ({ width: maxWidth, height: maxHeight } = getAdjustedSizes({ aspectRatio, width: maxWidth, height: maxHeight }, 'contain')); ({ width: minWidth, height: minHeight } = getAdjustedSizes({ aspectRatio, width: minWidth, height: minHeight }, 'cover')); if (resizable) { ({ width, height } = getAdjustedSizes({ aspectRatio, width, height }, options.resize)); } else { ({ width = naturalWidth, height = naturalHeight } = getAdjustedSizes({ aspectRatio, width, height })); } width = Math.floor(normalizeDecimalNumber(Math.min(Math.max(width, minWidth), maxWidth))); height = Math.floor(normalizeDecimalNumber(Math.min(Math.max(height, minHeight), maxHeight))); const destX = -width / 2; const destY = -height / 2; const destWidth = width; const destHeight = height; const params = []; if (resizable) { let srcX = 0; let srcY = 0; let srcWidth = naturalWidth; let srcHeight = naturalHeight; ({ width: srcWidth, height: srcHeight } = getAdjustedSizes({ aspectRatio, width: naturalWidth, height: naturalHeight }, { contain: 'cover', cover: 'contain' }[options.resize])); srcX = (naturalWidth - srcWidth) / 2; srcY = (naturalHeight - srcHeight) / 2; params.push(srcX, srcY, srcWidth, srcHeight); } params.push(destX, destY, destWidth, destHeight); if (is90DegreesRotated) { [width, height] = [height, width]; } canvas.width = width; canvas.height = height; if (!isImageType(options.mimeType)) { options.mimeType = file.type; } let fillStyle = 'transparent'; // Converts PNG files over the `convertSize` to JPEGs. if (file.size > options.convertSize && options.convertTypes.indexOf(options.mimeType) >= 0) { options.mimeType = 'image/jpeg'; } if (options.mimeType === 'image/jpeg') { fillStyle = '#fff'; } // Override the default fill color (#000, black) context.fillStyle = fillStyle; context.fillRect(0, 0, width, height); if (options.beforeDraw) { options.beforeDraw.call(this, context, canvas); } if (this.aborted) { return; } context.save(); context.translate(width / 2, height / 2); context.rotate(rotate * Math.PI / 180); context.scale(scaleX, scaleY); context.drawImage(image, ...params); context.restore(); if (options.drew) { options.drew.call(this, context, canvas); } if (this.aborted) { return; } const done = result => { if (!this.aborted) { this.done({ naturalWidth, naturalHeight, result }); } }; if (canvas.toBlob) { canvas.toBlob(done, options.mimeType, options.quality); } else { done(toBlob(canvas.toDataURL(options.mimeType, options.quality))); } } done(_ref3) { let { naturalWidth, naturalHeight, result } = _ref3; const { file, image, options } = this; if (URL && !options.checkOrientation) { URL.revokeObjectURL(image.src); } if (result) { // Returns original file if the result is greater than it and without size related options if (options.strict && 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)) { result = file; } else { const date = new Date(); result.lastModified = date.getTime(); result.lastModifiedDate = date; result.name = file.name; // Convert the extension to match its type if (result.name && result.type !== file.type) { result.name = result.name.replace(REGEXP_EXTENSION, imageTypeToExtension(result.type)); } } } else { // Returns original file if the result is null in some cases. result = file; } this.result = result; if (options.success) { options.success.call(this, result); } } fail(err) { const { options } = this; if (options.error) { options.error.call(this, err); } else { throw err; } } abort() { if (!this.aborted) { this.aborted = true; if (this.reader) { this.reader.abort(); } else if (!this.image.complete) { this.image.onload = null; this.image.onabort(); } else { this.fail(new Error('The compression process has been aborted.')); } } } /** * Get the no conflict compressor class. * @returns {Compressor} The compressor class. */ static noConflict() { window.Compressor = AnotherCompressor; return Compressor; } /** * Change the default options. * @param {Object} options - The new default options. */ static setDefaults(options) { _extends(DEFAULTS, options); } } return Compressor; }));