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
/*
* 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
})