/* * JavaScript Load Image Meta * https://github.com/blueimp/JavaScript-Load-Image * * Copyright 2013, Sebastian Tschan * https://blueimp.net * * Image metadata handling implementation * based on the help and contribution of * Achim Stöhr. * * Licensed under the MIT license: * https://opensource.org/licenses/MIT */ /* global define, module, require, Promise, DataView, Uint8Array, ArrayBuffer */ ;(function (factory) { 'use strict' if (typeof define === 'function' && define.amd) { // Register as an anonymous AMD module: define(['./load-image'], factory) } else if (typeof module === 'object' && module.exports) { factory(require('./load-image')) } else { // Browser globals: factory(window.loadImage) } })(function (loadImage) { 'use strict' var global = loadImage.global var originalTransform = loadImage.transform var blobSlice = global.Blob && (Blob.prototype.slice || Blob.prototype.webkitSlice || Blob.prototype.mozSlice) var bufferSlice = (global.ArrayBuffer && ArrayBuffer.prototype.slice) || function (begin, end) { // Polyfill for IE10, which does not support ArrayBuffer.slice // eslint-disable-next-line no-param-reassign end = end || this.byteLength - begin var arr1 = new Uint8Array(this, begin, end) var arr2 = new Uint8Array(end) arr2.set(arr1) return arr2.buffer } var metaDataParsers = { jpeg: { 0xffe1: [], // APP1 marker 0xffed: [] // APP13 marker } } /** * Parses image metadata and calls the callback with an object argument * with the following property: * - imageHead: The complete image head as ArrayBuffer * The options argument accepts an object and supports the following * properties: * - maxMetaDataSize: Defines the maximum number of bytes to parse. * - disableImageHead: Disables creating the imageHead property. * * @param {Blob} file Blob object * @param {Function} [callback] Callback function * @param {object} [options] Parsing options * @param {object} [data] Result data object * @returns {Promise|undefined} Returns Promise if no callback given. */ function parseMetaData(file, callback, options, data) { var that = this /** * Promise executor * * @param {Function} resolve Resolution function * @param {Function} reject Rejection function * @returns {undefined} Undefined */ function executor(resolve, reject) { if ( !( global.DataView && blobSlice && file && file.size >= 12 && file.type === 'image/jpeg' ) ) { // Nothing to parse return resolve(data) } // 256 KiB should contain all EXIF/ICC/IPTC segments: var maxMetaDataSize = options.maxMetaDataSize || 262144 if ( !loadImage.readFile( blobSlice.call(file, 0, maxMetaDataSize), function (buffer) { // Note on endianness: // Since the marker and length bytes in JPEG files are always // stored in big endian order, we can leave the endian parameter // of the DataView methods undefined, defaulting to big endian. var dataView = new DataView(buffer) // Check for the JPEG marker (0xffd8): if (dataView.getUint16(0) !== 0xffd8) { return reject( new Error('Invalid JPEG file: Missing JPEG marker.') ) } var offset = 2 var maxOffset = dataView.byteLength - 4 var headLength = offset var markerBytes var markerLength var parsers var i while (offset < maxOffset) { markerBytes = dataView.getUint16(offset) // Search for APPn (0xffeN) and COM (0xfffe) markers, // which contain application-specific metadata like // Exif, ICC and IPTC data and text comments: if ( (markerBytes >= 0xffe0 && markerBytes <= 0xffef) || markerBytes === 0xfffe ) { // The marker bytes (2) are always followed by // the length bytes (2), indicating the length of the // marker segment, which includes the length bytes, // but not the marker bytes, so we add 2: markerLength = dataView.getUint16(offset + 2) + 2 if (offset + markerLength > dataView.byteLength) { // eslint-disable-next-line no-console console.log('Invalid JPEG metadata: Invalid segment size.') break } parsers = metaDataParsers.jpeg[markerBytes] if (parsers && !options.disableMetaDataParsers) { for (i = 0; i < parsers.length; i += 1) { parsers[i].call( that, dataView, offset, markerLength, data, options ) } } offset += markerLength headLength = offset } else { // Not an APPn or COM marker, probably safe to // assume that this is the end of the metadata break } } // Meta length must be longer than JPEG marker (2) // plus APPn marker (2), followed by length bytes (2): if (!options.disableImageHead && headLength > 6) { data.imageHead = bufferSlice.call(buffer, 0, headLength) } resolve(data) }, reject, 'readAsArrayBuffer' ) ) { // No support for the FileReader interface, nothing to parse resolve(data) } } options = options || {} // eslint-disable-line no-param-reassign if (global.Promise && typeof callback !== 'function') { options = callback || {} // eslint-disable-line no-param-reassign data = options // eslint-disable-line no-param-reassign return new Promise(executor) } data = data || {} // eslint-disable-line no-param-reassign return executor(callback, callback) } /** * Replaces the head of a JPEG Blob * * @param {Blob} blob Blob object * @param {ArrayBuffer} oldHead Old JPEG head * @param {ArrayBuffer} newHead New JPEG head * @returns {Blob} Combined Blob */ function replaceJPEGHead(blob, oldHead, newHead) { if (!blob || !oldHead || !newHead) return null return new Blob([newHead, blobSlice.call(blob, oldHead.byteLength)], { type: 'image/jpeg' }) } /** * Replaces the image head of a JPEG blob with the given one. * Returns a Promise or calls the callback with the new Blob. * * @param {Blob} blob Blob object * @param {ArrayBuffer} head New JPEG head * @param {Function} [callback] Callback function * @returns {Promise|undefined} Combined Blob */ function replaceHead(blob, head, callback) { var options = { maxMetaDataSize: 256, disableMetaDataParsers: true } if (!callback && global.Promise) { return parseMetaData(blob, options).then(function (data) { return replaceJPEGHead(blob, data.imageHead, head) }) } parseMetaData( blob, function (data) { callback(replaceJPEGHead(blob, data.imageHead, head)) }, options ) } loadImage.transform = function (img, options, callback, file, data) { if (loadImage.requiresMetaData(options)) { data = data || {} // eslint-disable-line no-param-reassign parseMetaData( file, function (result) { if (result !== data) { // eslint-disable-next-line no-console if (global.console) console.log(result) result = data // eslint-disable-line no-param-reassign } originalTransform.call( loadImage, img, options, callback, file, result ) }, options, data ) } else { originalTransform.apply(loadImage, arguments) } } loadImage.blobSlice = blobSlice loadImage.bufferSlice = bufferSlice loadImage.replaceHead = replaceHead loadImage.parseMetaData = parseMetaData loadImage.metaDataParsers = metaDataParsers })