UNPKG

blueimp-load-image

Version:

JavaScript Load Image is a library to load images provided as File or Blob objects or via URL. It returns an optionally scaled, cropped or rotated HTML img or canvas element. It also provides methods to parse image metadata to extract IPTC and Exif tags a

260 lines (248 loc) 8.58 kB
/* * 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<object>|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<Blob|null>|undefined} Combined Blob */ function replaceHead(blob, head, callback) { var options = { maxMetaDataSize: 1024, 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 })