aboutsummaryrefslogtreecommitdiffstats
path: root/library/cropperjs/src/js/utilities.js
diff options
context:
space:
mode:
Diffstat (limited to 'library/cropperjs/src/js/utilities.js')
-rw-r--r--library/cropperjs/src/js/utilities.js985
1 files changed, 985 insertions, 0 deletions
diff --git a/library/cropperjs/src/js/utilities.js b/library/cropperjs/src/js/utilities.js
new file mode 100644
index 000000000..50b586a76
--- /dev/null
+++ b/library/cropperjs/src/js/utilities.js
@@ -0,0 +1,985 @@
+import {
+ WINDOW,
+} from './constants';
+
+/**
+ * Check if the given value is not a number.
+ */
+export const isNaN = Number.isNaN || WINDOW.isNaN;
+
+/**
+ * Check if the given value is a number.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is a number, else `false`.
+ */
+export function isNumber(value) {
+ return typeof value === 'number' && !isNaN(value);
+}
+
+/**
+ * Check if the given value is undefined.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is undefined, else `false`.
+ */
+export function isUndefined(value) {
+ return typeof value === 'undefined';
+}
+
+/**
+ * Check if the given value is an object.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is an object, else `false`.
+ */
+export function isObject(value) {
+ return typeof value === 'object' && value !== null;
+}
+
+const { hasOwnProperty } = Object.prototype;
+
+/**
+ * Check if the given value is a plain object.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is a plain object, else `false`.
+ */
+export function isPlainObject(value) {
+ if (!isObject(value)) {
+ return false;
+ }
+
+ try {
+ const { constructor } = value;
+ const { prototype } = constructor;
+
+ return constructor && prototype && hasOwnProperty.call(prototype, 'isPrototypeOf');
+ } catch (e) {
+ return false;
+ }
+}
+
+/**
+ * Check if the given value is a function.
+ * @param {*} value - The value to check.
+ * @returns {boolean} Returns `true` if the given value is a function, else `false`.
+ */
+export function isFunction(value) {
+ return typeof value === 'function';
+}
+
+/**
+ * Iterate the given data.
+ * @param {*} data - The data to iterate.
+ * @param {Function} callback - The process function for each element.
+ * @returns {*} The original data.
+ */
+export function each(data, callback) {
+ if (data && isFunction(callback)) {
+ if (Array.isArray(data) || isNumber(data.length)/* array-like */) {
+ const { length } = data;
+ let i;
+
+ for (i = 0; i < length; i += 1) {
+ if (callback.call(data, data[i], i, data) === false) {
+ break;
+ }
+ }
+ } else if (isObject(data)) {
+ Object.keys(data).forEach((key) => {
+ callback.call(data, data[key], key, data);
+ });
+ }
+ }
+
+ return data;
+}
+
+/**
+ * Extend the given object.
+ * @param {*} obj - The object to be extended.
+ * @param {*} args - The rest objects which will be merged to the first object.
+ * @returns {Object} The extended object.
+ */
+export function extend(obj, ...args) {
+ if (isObject(obj) && args.length > 0) {
+ if (Object.assign) {
+ return Object.assign(obj, ...args);
+ }
+
+ args.forEach((arg) => {
+ if (isObject(arg)) {
+ Object.keys(arg).forEach((key) => {
+ obj[key] = arg[key];
+ });
+ }
+ });
+ }
+
+ return obj;
+}
+
+/**
+ * Takes a function and returns a new one that will always have a particular context.
+ * @param {Function} fn - The target function.
+ * @param {Object} context - The new context for the function.
+ * @returns {boolean} The new function.
+ */
+export function proxy(fn, context, ...args) {
+ return (...args2) => fn.apply(context, args.concat(args2));
+}
+
+const REGEXP_DECIMALS = /\.\d*(?:0|9){12}\d*$/i;
+
+/**
+ * Normalize decimal number.
+ * Check out {@link http://0.30000000000000004.com/ }
+ * @param {number} value - The value to normalize.
+ * @param {number} [times=100000000000] - The times for normalizing.
+ * @returns {number} Returns the normalized number.
+ */
+export function normalizeDecimalNumber(value, times = 100000000000) {
+ return REGEXP_DECIMALS.test(value) ? (Math.round(value * times) / times) : value;
+}
+
+const REGEXP_SUFFIX = /^(?:width|height|left|top|marginLeft|marginTop)$/;
+
+/**
+ * Apply styles to the given element.
+ * @param {Element} element - The target element.
+ * @param {Object} styles - The styles for applying.
+ */
+export function setStyle(element, styles) {
+ const { style } = element;
+
+ each(styles, (value, property) => {
+ if (REGEXP_SUFFIX.test(property) && isNumber(value)) {
+ value += 'px';
+ }
+
+ style[property] = value;
+ });
+}
+
+/**
+ * Check if the given element has a special class.
+ * @param {Element} element - The element to check.
+ * @param {string} value - The class to search.
+ * @returns {boolean} Returns `true` if the special class was found.
+ */
+export function hasClass(element, value) {
+ return element.classList ?
+ element.classList.contains(value) :
+ element.className.indexOf(value) > -1;
+}
+
+/**
+ * Add classes to the given element.
+ * @param {Element} element - The target element.
+ * @param {string} value - The classes to be added.
+ */
+export function addClass(element, value) {
+ if (!value) {
+ return;
+ }
+
+ if (isNumber(element.length)) {
+ each(element, (elem) => {
+ addClass(elem, value);
+ });
+ return;
+ }
+
+ if (element.classList) {
+ element.classList.add(value);
+ return;
+ }
+
+ const className = element.className.trim();
+
+ if (!className) {
+ element.className = value;
+ } else if (className.indexOf(value) < 0) {
+ element.className = `${className} ${value}`;
+ }
+}
+
+/**
+ * Remove classes from the given element.
+ * @param {Element} element - The target element.
+ * @param {string} value - The classes to be removed.
+ */
+export function removeClass(element, value) {
+ if (!value) {
+ return;
+ }
+
+ if (isNumber(element.length)) {
+ each(element, (elem) => {
+ removeClass(elem, value);
+ });
+ return;
+ }
+
+ if (element.classList) {
+ element.classList.remove(value);
+ return;
+ }
+
+ if (element.className.indexOf(value) >= 0) {
+ element.className = element.className.replace(value, '');
+ }
+}
+
+/**
+ * Add or remove classes from the given element.
+ * @param {Element} element - The target element.
+ * @param {string} value - The classes to be toggled.
+ * @param {boolean} added - Add only.
+ */
+export function toggleClass(element, value, added) {
+ if (!value) {
+ return;
+ }
+
+ if (isNumber(element.length)) {
+ each(element, (elem) => {
+ toggleClass(elem, value, added);
+ });
+ return;
+ }
+
+ // IE10-11 doesn't support the second parameter of `classList.toggle`
+ if (added) {
+ addClass(element, value);
+ } else {
+ removeClass(element, value);
+ }
+}
+
+const REGEXP_HYPHENATE = /([a-z\d])([A-Z])/g;
+
+/**
+ * Hyphenate the given value.
+ * @param {string} value - The value to hyphenate.
+ * @returns {string} The hyphenated value.
+ */
+export function hyphenate(value) {
+ return value.replace(REGEXP_HYPHENATE, '$1-$2').toLowerCase();
+}
+
+/**
+ * Get data from the given element.
+ * @param {Element} element - The target element.
+ * @param {string} name - The data key to get.
+ * @returns {string} The data value.
+ */
+export function getData(element, name) {
+ if (isObject(element[name])) {
+ return element[name];
+ } else if (element.dataset) {
+ return element.dataset[name];
+ }
+
+ return element.getAttribute(`data-${hyphenate(name)}`);
+}
+
+/**
+ * Set data to the given element.
+ * @param {Element} element - The target element.
+ * @param {string} name - The data key to set.
+ * @param {string} data - The data value.
+ */
+export function setData(element, name, data) {
+ if (isObject(data)) {
+ element[name] = data;
+ } else if (element.dataset) {
+ element.dataset[name] = data;
+ } else {
+ element.setAttribute(`data-${hyphenate(name)}`, data);
+ }
+}
+
+/**
+ * Remove data from the given element.
+ * @param {Element} element - The target element.
+ * @param {string} name - The data key to remove.
+ */
+export function removeData(element, name) {
+ if (isObject(element[name])) {
+ try {
+ delete element[name];
+ } catch (e) {
+ element[name] = null;
+ }
+ } else if (element.dataset) {
+ // #128 Safari not allows to delete dataset property
+ try {
+ delete element.dataset[name];
+ } catch (e) {
+ element.dataset[name] = null;
+ }
+ } else {
+ element.removeAttribute(`data-${hyphenate(name)}`);
+ }
+}
+
+const REGEXP_SPACES = /\s\s*/;
+
+/**
+ * Remove event listener from the target element.
+ * @param {Element} element - The event target.
+ * @param {string} type - The event type(s).
+ * @param {Function} listener - The event listener.
+ * @param {Object} options - The event options.
+ */
+export function removeListener(element, type, listener, options = {}) {
+ if (!isFunction(listener)) {
+ return;
+ }
+
+ const types = type.trim().split(REGEXP_SPACES);
+
+ if (types.length > 1) {
+ each(types, (t) => {
+ removeListener(element, t, listener, options);
+ });
+ return;
+ }
+
+ if (element.removeEventListener) {
+ element.removeEventListener(type, listener, options);
+ } else if (element.detachEvent) {
+ element.detachEvent(`on${type}`, listener);
+ }
+}
+
+/**
+ * Add event listener to the target element.
+ * @param {Element} element - The event target.
+ * @param {string} type - The event type(s).
+ * @param {Function} listener - The event listener.
+ * @param {Object} options - The event options.
+ */
+export function addListener(element, type, listener, options = {}) {
+ if (!isFunction(listener)) {
+ return;
+ }
+
+ const types = type.trim().split(REGEXP_SPACES);
+
+ if (types.length > 1) {
+ each(types, (t) => {
+ addListener(element, t, listener, options);
+ });
+ return;
+ }
+
+ if (options.once) {
+ const originalListener = listener;
+
+ listener = (...args) => {
+ removeListener(element, type, listener, options);
+ return originalListener.apply(element, args);
+ };
+ }
+
+ if (element.addEventListener) {
+ element.addEventListener(type, listener, options);
+ } else if (element.attachEvent) {
+ element.attachEvent(`on${type}`, listener);
+ }
+}
+
+/**
+ * Dispatch event on the target element.
+ * @param {Element} element - The event target.
+ * @param {string} type - The event type(s).
+ * @param {Object} data - The additional event data.
+ * @returns {boolean} Indicate if the event is default prevented or not.
+ */
+export function dispatchEvent(element, type, data) {
+ if (element.dispatchEvent) {
+ let event;
+
+ // Event and CustomEvent on IE9-11 are global objects, not constructors
+ if (isFunction(Event) && isFunction(CustomEvent)) {
+ if (isUndefined(data)) {
+ event = new Event(type, {
+ bubbles: true,
+ cancelable: true,
+ });
+ } else {
+ event = new CustomEvent(type, {
+ detail: data,
+ bubbles: true,
+ cancelable: true,
+ });
+ }
+ } else if (isUndefined(data)) {
+ event = document.createEvent('Event');
+ event.initEvent(type, true, true);
+ } else {
+ event = document.createEvent('CustomEvent');
+ event.initCustomEvent(type, true, true, data);
+ }
+
+ // IE9+
+ return element.dispatchEvent(event);
+ } else if (element.fireEvent) {
+ // IE6-10 (native events only)
+ return element.fireEvent(`on${type}`);
+ }
+
+ return true;
+}
+
+/**
+ * Get the offset base on the document.
+ * @param {Element} element - The target element.
+ * @returns {Object} The offset data.
+ */
+export function getOffset(element) {
+ const doc = document.documentElement;
+ const box = element.getBoundingClientRect();
+
+ return {
+ left: box.left + (
+ (window.scrollX || (doc && doc.scrollLeft) || 0) - ((doc && doc.clientLeft) || 0)
+ ),
+ top: box.top + (
+ (window.scrollY || (doc && doc.scrollTop) || 0) - ((doc && doc.clientTop) || 0)
+ ),
+ };
+}
+
+/**
+ * Empty an element.
+ * @param {Element} element - The element to empty.
+ */
+export function empty(element) {
+ while (element.firstChild) {
+ element.removeChild(element.firstChild);
+ }
+}
+
+const { location } = WINDOW;
+const REGEXP_ORIGINS = /^(https?:)\/\/([^:/?#]+):?(\d*)/i;
+
+/**
+ * Check if the given URL is a cross origin URL.
+ * @param {string} url - The target URL.
+ * @returns {boolean} Returns `true` if the given URL is a cross origin URL, else `false`.
+ */
+export function isCrossOriginURL(url) {
+ const parts = url.match(REGEXP_ORIGINS);
+
+ return parts && (
+ parts[1] !== location.protocol ||
+ parts[2] !== location.hostname ||
+ parts[3] !== location.port
+ );
+}
+
+/**
+ * Add timestamp to the given URL.
+ * @param {string} url - The target URL.
+ * @returns {string} The result URL.
+ */
+export function addTimestamp(url) {
+ const timestamp = `timestamp=${(new Date()).getTime()}`;
+
+ return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp);
+}
+
+/**
+ * Get transforms base on the given object.
+ * @param {Object} obj - The target object.
+ * @returns {string} A string contains transform values.
+ */
+export function getTransforms({
+ rotate,
+ scaleX,
+ scaleY,
+ translateX,
+ translateY,
+}) {
+ const values = [];
+
+ if (isNumber(translateX) && translateX !== 0) {
+ values.push(`translateX(${translateX}px)`);
+ }
+
+ if (isNumber(translateY) && translateY !== 0) {
+ values.push(`translateY(${translateY}px)`);
+ }
+
+ // Rotate should come first before scale to match orientation transform
+ if (isNumber(rotate) && rotate !== 0) {
+ values.push(`rotate(${rotate}deg)`);
+ }
+
+ if (isNumber(scaleX) && scaleX !== 1) {
+ values.push(`scaleX(${scaleX})`);
+ }
+
+ if (isNumber(scaleY) && scaleY !== 1) {
+ values.push(`scaleY(${scaleY})`);
+ }
+
+ const transform = values.length ? values.join(' ') : 'none';
+
+ return {
+ WebkitTransform: transform,
+ msTransform: transform,
+ transform,
+ };
+}
+
+const { navigator } = WINDOW;
+const IS_SAFARI_OR_UIWEBVIEW = navigator && /(Macintosh|iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent);
+
+/**
+ * Get an image's natural sizes.
+ * @param {string} image - The target image.
+ * @param {Function} callback - The callback function.
+ */
+export function getImageNaturalSizes(image, callback) {
+ // Modern browsers (except Safari)
+ if (image.naturalWidth && !IS_SAFARI_OR_UIWEBVIEW) {
+ callback(image.naturalWidth, image.naturalHeight);
+ return;
+ }
+
+ const newImage = document.createElement('img');
+ const body = document.body || document.documentElement;
+
+ newImage.onload = () => {
+ callback(newImage.width, newImage.height);
+
+ if (!IS_SAFARI_OR_UIWEBVIEW) {
+ body.removeChild(newImage);
+ }
+ };
+
+ newImage.src = image.src;
+
+ // iOS Safari will convert the image automatically
+ // with its orientation once append it into DOM (#279)
+ if (!IS_SAFARI_OR_UIWEBVIEW) {
+ newImage.style.cssText = (
+ 'left:0;' +
+ 'max-height:none!important;' +
+ 'max-width:none!important;' +
+ 'min-height:0!important;' +
+ 'min-width:0!important;' +
+ 'opacity:0;' +
+ 'position:absolute;' +
+ 'top:0;' +
+ 'z-index:-1;'
+ );
+ body.appendChild(newImage);
+ }
+}
+
+/**
+ * Get the max ratio of a group of pointers.
+ * @param {string} pointers - The target pointers.
+ * @returns {number} The result ratio.
+ */
+export function getMaxZoomRatio(pointers) {
+ const pointers2 = extend({}, pointers);
+ const ratios = [];
+
+ each(pointers, (pointer, pointerId) => {
+ delete pointers2[pointerId];
+
+ each(pointers2, (pointer2) => {
+ const x1 = Math.abs(pointer.startX - pointer2.startX);
+ const y1 = Math.abs(pointer.startY - pointer2.startY);
+ const x2 = Math.abs(pointer.endX - pointer2.endX);
+ const y2 = Math.abs(pointer.endY - pointer2.endY);
+ const z1 = Math.sqrt((x1 * x1) + (y1 * y1));
+ const z2 = Math.sqrt((x2 * x2) + (y2 * y2));
+ const ratio = (z2 - z1) / z1;
+
+ ratios.push(ratio);
+ });
+ });
+
+ ratios.sort((a, b) => Math.abs(a) < Math.abs(b));
+
+ return ratios[0];
+}
+
+/**
+ * Get a pointer from an event object.
+ * @param {Object} event - The target event object.
+ * @param {boolean} endOnly - Indicates if only returns the end point coordinate or not.
+ * @returns {Object} The result pointer contains start and/or end point coordinates.
+ */
+export function getPointer({ pageX, pageY }, endOnly) {
+ const end = {
+ endX: pageX,
+ endY: pageY,
+ };
+
+ if (endOnly) {
+ return end;
+ }
+
+ return extend({
+ startX: pageX,
+ startY: pageY,
+ }, end);
+}
+
+/**
+ * Get the center point coordinate of a group of pointers.
+ * @param {Object} pointers - The target pointers.
+ * @returns {Object} The center point coordinate.
+ */
+export function getPointersCenter(pointers) {
+ let pageX = 0;
+ let pageY = 0;
+ let count = 0;
+
+ each(pointers, ({ startX, startY }) => {
+ pageX += startX;
+ pageY += startY;
+ count += 1;
+ });
+
+ pageX /= count;
+ pageY /= count;
+
+ return {
+ pageX,
+ pageY,
+ };
+}
+
+/**
+ * Check if the given value is a finite number.
+ */
+export const isFinite = Number.isFinite || WINDOW.isFinite;
+
+/**
+ * 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.
+ */
+export function getAdjustedSizes(
+ {
+ aspectRatio,
+ height,
+ width,
+ },
+ type = 'contain', // or 'cover'
+) {
+ const isValidNumber = value => isFinite(value) && value > 0;
+
+ if (isValidNumber(width) && isValidNumber(height)) {
+ const adjustedWidth = height * aspectRatio;
+
+ if ((type === 'contain' && adjustedWidth > width) || (type === 'cover' && adjustedWidth < width)) {
+ height = width / aspectRatio;
+ } else {
+ width = height * aspectRatio;
+ }
+ } else if (isValidNumber(width)) {
+ height = width / aspectRatio;
+ } else if (isValidNumber(height)) {
+ width = height * aspectRatio;
+ }
+
+ return {
+ width,
+ height,
+ };
+}
+
+/**
+ * Get the new sizes of a rectangle after rotated.
+ * @param {Object} data - The original sizes.
+ * @returns {Object} The result sizes.
+ */
+export function getRotatedSizes({ width, height, degree }) {
+ degree = Math.abs(degree) % 180;
+
+ if (degree === 90) {
+ return {
+ width: height,
+ height: width,
+ };
+ }
+
+ const arc = ((degree % 90) * Math.PI) / 180;
+ const sinArc = Math.sin(arc);
+ const cosArc = Math.cos(arc);
+ const newWidth = (width * cosArc) + (height * sinArc);
+ const newHeight = (width * sinArc) + (height * cosArc);
+
+ return degree > 90 ? {
+ width: newHeight,
+ height: newWidth,
+ } : {
+ width: newWidth,
+ height: newHeight,
+ };
+}
+
+/**
+ * Get a canvas which drew the given image.
+ * @param {HTMLImageElement} image - The image for drawing.
+ * @param {Object} imageData - The image data.
+ * @param {Object} canvasData - The canvas data.
+ * @param {Object} options - The options.
+ * @returns {HTMLCanvasElement} The result canvas.
+ */
+export function getSourceCanvas(
+ image,
+ {
+ rotate = 0,
+ scaleX = 1,
+ scaleY = 1,
+ },
+ {
+ aspectRatio,
+ naturalWidth,
+ naturalHeight,
+ },
+ {
+ fillColor = 'transparent',
+ imageSmoothingEnabled = true,
+ imageSmoothingQuality = 'low',
+ maxWidth = Infinity,
+ maxHeight = Infinity,
+ minWidth = 0,
+ minHeight = 0,
+ },
+) {
+ const canvas = document.createElement('canvas');
+ const context = canvas.getContext('2d');
+ const maxSizes = getAdjustedSizes({
+ aspectRatio,
+ width: maxWidth,
+ height: maxHeight,
+ });
+ const minSizes = getAdjustedSizes({
+ aspectRatio,
+ width: minWidth,
+ height: minHeight,
+ }, 'cover');
+ const width = Math.min(maxSizes.width, Math.max(minSizes.width, naturalWidth));
+ const height = Math.min(maxSizes.height, Math.max(minSizes.height, naturalHeight));
+ const params = [
+ -width / 2,
+ -height / 2,
+ width,
+ height,
+ ];
+
+ canvas.width = normalizeDecimalNumber(width);
+ canvas.height = normalizeDecimalNumber(height);
+ context.fillStyle = fillColor;
+ context.fillRect(0, 0, width, height);
+ context.save();
+ context.translate(width / 2, height / 2);
+ context.rotate((rotate * Math.PI) / 180);
+ context.scale(scaleX, scaleY);
+ context.imageSmoothingEnabled = imageSmoothingEnabled;
+ context.imageSmoothingQuality = imageSmoothingQuality;
+ context.drawImage(image, ...params.map(param => Math.floor(normalizeDecimalNumber(param))));
+ context.restore();
+ return canvas;
+}
+
+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.
+ */
+export 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 REGEXP_DATA_URL_HEAD = /^data:.*,/;
+
+/**
+ * Transform Data URL to array buffer.
+ * @param {string} dataURL - The Data URL to transform.
+ * @returns {ArrayBuffer} The result array buffer.
+ */
+export function dataURLToArrayBuffer(dataURL) {
+ const base64 = dataURL.replace(REGEXP_DATA_URL_HEAD, '');
+ const binary = atob(base64);
+ const arrayBuffer = new ArrayBuffer(binary.length);
+ const uint8 = new Uint8Array(arrayBuffer);
+
+ each(uint8, (value, i) => {
+ uint8[i] = binary.charCodeAt(i);
+ });
+
+ return arrayBuffer;
+}
+
+/**
+ * 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.
+ */
+export function arrayBufferToDataURL(arrayBuffer, mimeType) {
+ const uint8 = new Uint8Array(arrayBuffer);
+ let data = '';
+
+ // TypedArray.prototype.forEach is not supported in some browsers.
+ each(uint8, (value) => {
+ data += fromCharCode(value);
+ });
+
+ return `data:${mimeType};base64,${btoa(data)}`;
+}
+
+/**
+ * Get orientation value from given array buffer.
+ * @param {ArrayBuffer} arrayBuffer - The array buffer to read.
+ * @returns {number} The read orientation value.
+ */
+export function getOrientation(arrayBuffer) {
+ const dataView = new DataView(arrayBuffer);
+ let orientation;
+ 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 < 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;
+ }
+ }
+ }
+
+ return orientation;
+}
+
+/**
+ * Parse Exif Orientation value.
+ * @param {number} orientation - The orientation to parse.
+ * @returns {Object} The parsed result.
+ */
+export 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;
+
+ default:
+ }
+
+ return {
+ rotate,
+ scaleX,
+ scaleY,
+ };
+}