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
461 lines (440 loc) • 13.4 kB
JavaScript
/*
* JavaScript Load Image Exif Parser
* https://github.com/blueimp/JavaScript-Load-Image
*
* Copyright 2013, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* https://opensource.org/licenses/MIT
*/
/* global define, module, require, DataView */
/* eslint-disable no-console */
;(function (factory) {
'use strict'
if (typeof define === 'function' && define.amd) {
// Register as an anonymous AMD module:
define(['./load-image', './load-image-meta'], factory)
} else if (typeof module === 'object' && module.exports) {
factory(require('./load-image'), require('./load-image-meta'))
} else {
// Browser globals:
factory(window.loadImage)
}
})(function (loadImage) {
'use strict'
/**
* Exif tag map
*
* @name ExifMap
* @class
* @param {number|string} tagCode IFD tag code
*/
function ExifMap(tagCode) {
if (tagCode) {
Object.defineProperty(this, 'map', {
value: this.ifds[tagCode].map
})
Object.defineProperty(this, 'tags', {
value: (this.tags && this.tags[tagCode]) || {}
})
}
}
ExifMap.prototype.map = {
Orientation: 0x0112,
Thumbnail: 'ifd1',
Blob: 0x0201, // Alias for JPEGInterchangeFormat
Exif: 0x8769,
GPSInfo: 0x8825,
Interoperability: 0xa005
}
ExifMap.prototype.ifds = {
ifd1: { name: 'Thumbnail', map: ExifMap.prototype.map },
0x8769: { name: 'Exif', map: {} },
0x8825: { name: 'GPSInfo', map: {} },
0xa005: { name: 'Interoperability', map: {} }
}
/**
* Retrieves exif tag value
*
* @param {number|string} id Exif tag code or name
* @returns {object} Exif tag value
*/
ExifMap.prototype.get = function (id) {
return this[id] || this[this.map[id]]
}
/**
* Returns the Exif Thumbnail data as Blob.
*
* @param {DataView} dataView Data view interface
* @param {number} offset Thumbnail data offset
* @param {number} length Thumbnail data length
* @returns {undefined|Blob} Returns the Thumbnail Blob or undefined
*/
function getExifThumbnail(dataView, offset, length) {
if (!length) return
if (offset + length > dataView.byteLength) {
console.log('Invalid Exif data: Invalid thumbnail data.')
return
}
return new Blob(
[loadImage.bufferSlice.call(dataView.buffer, offset, offset + length)],
{
type: 'image/jpeg'
}
)
}
var ExifTagTypes = {
// byte, 8-bit unsigned int:
1: {
getValue: function (dataView, dataOffset) {
return dataView.getUint8(dataOffset)
},
size: 1
},
// ascii, 8-bit byte:
2: {
getValue: function (dataView, dataOffset) {
return String.fromCharCode(dataView.getUint8(dataOffset))
},
size: 1,
ascii: true
},
// short, 16 bit int:
3: {
getValue: function (dataView, dataOffset, littleEndian) {
return dataView.getUint16(dataOffset, littleEndian)
},
size: 2
},
// long, 32 bit int:
4: {
getValue: function (dataView, dataOffset, littleEndian) {
return dataView.getUint32(dataOffset, littleEndian)
},
size: 4
},
// rational = two long values, first is numerator, second is denominator:
5: {
getValue: function (dataView, dataOffset, littleEndian) {
return (
dataView.getUint32(dataOffset, littleEndian) /
dataView.getUint32(dataOffset + 4, littleEndian)
)
},
size: 8
},
// slong, 32 bit signed int:
9: {
getValue: function (dataView, dataOffset, littleEndian) {
return dataView.getInt32(dataOffset, littleEndian)
},
size: 4
},
// srational, two slongs, first is numerator, second is denominator:
10: {
getValue: function (dataView, dataOffset, littleEndian) {
return (
dataView.getInt32(dataOffset, littleEndian) /
dataView.getInt32(dataOffset + 4, littleEndian)
)
},
size: 8
}
}
// undefined, 8-bit byte, value depending on field:
ExifTagTypes[7] = ExifTagTypes[1]
/**
* Returns Exif tag value.
*
* @param {DataView} dataView Data view interface
* @param {number} tiffOffset TIFF offset
* @param {number} offset Tag offset
* @param {number} type Tag type
* @param {number} length Tag length
* @param {boolean} littleEndian Little endian encoding
* @returns {object} Tag value
*/
function getExifValue(
dataView,
tiffOffset,
offset,
type,
length,
littleEndian
) {
var tagType = ExifTagTypes[type]
var tagSize
var dataOffset
var values
var i
var str
var c
if (!tagType) {
console.log('Invalid Exif data: Invalid tag type.')
return
}
tagSize = tagType.size * length
// Determine if the value is contained in the dataOffset bytes,
// or if the value at the dataOffset is a pointer to the actual data:
dataOffset =
tagSize > 4
? tiffOffset + dataView.getUint32(offset + 8, littleEndian)
: offset + 8
if (dataOffset + tagSize > dataView.byteLength) {
console.log('Invalid Exif data: Invalid data offset.')
return
}
if (length === 1) {
return tagType.getValue(dataView, dataOffset, littleEndian)
}
values = []
for (i = 0; i < length; i += 1) {
values[i] = tagType.getValue(
dataView,
dataOffset + i * tagType.size,
littleEndian
)
}
if (tagType.ascii) {
str = ''
// Concatenate the chars:
for (i = 0; i < values.length; i += 1) {
c = values[i]
// Ignore the terminating NULL byte(s):
if (c === '\u0000') {
break
}
str += c
}
return str
}
return values
}
/**
* Determines if the given tag should be included.
*
* @param {object} includeTags Map of tags to include
* @param {object} excludeTags Map of tags to exclude
* @param {number|string} tagCode Tag code to check
* @returns {boolean} True if the tag should be included
*/
function shouldIncludeTag(includeTags, excludeTags, tagCode) {
return (
(!includeTags || includeTags[tagCode]) &&
(!excludeTags || excludeTags[tagCode] !== true)
)
}
/**
* Parses Exif tags.
*
* @param {DataView} dataView Data view interface
* @param {number} tiffOffset TIFF offset
* @param {number} dirOffset Directory offset
* @param {boolean} littleEndian Little endian encoding
* @param {ExifMap} tags Map to store parsed exif tags
* @param {ExifMap} tagOffsets Map to store parsed exif tag offsets
* @param {object} includeTags Map of tags to include
* @param {object} excludeTags Map of tags to exclude
* @returns {number} Next directory offset
*/
function parseExifTags(
dataView,
tiffOffset,
dirOffset,
littleEndian,
tags,
tagOffsets,
includeTags,
excludeTags
) {
var tagsNumber, dirEndOffset, i, tagOffset, tagNumber, tagValue
if (dirOffset + 6 > dataView.byteLength) {
console.log('Invalid Exif data: Invalid directory offset.')
return
}
tagsNumber = dataView.getUint16(dirOffset, littleEndian)
dirEndOffset = dirOffset + 2 + 12 * tagsNumber
if (dirEndOffset + 4 > dataView.byteLength) {
console.log('Invalid Exif data: Invalid directory size.')
return
}
for (i = 0; i < tagsNumber; i += 1) {
tagOffset = dirOffset + 2 + 12 * i
tagNumber = dataView.getUint16(tagOffset, littleEndian)
if (!shouldIncludeTag(includeTags, excludeTags, tagNumber)) continue
tagValue = getExifValue(
dataView,
tiffOffset,
tagOffset,
dataView.getUint16(tagOffset + 2, littleEndian), // tag type
dataView.getUint32(tagOffset + 4, littleEndian), // tag length
littleEndian
)
tags[tagNumber] = tagValue
if (tagOffsets) {
tagOffsets[tagNumber] = tagOffset
}
}
// Return the offset to the next directory:
return dataView.getUint32(dirEndOffset, littleEndian)
}
/**
* Parses tags in a given IFD (Image File Directory).
*
* @param {object} data Data object to store exif tags and offsets
* @param {number|string} tagCode IFD tag code
* @param {DataView} dataView Data view interface
* @param {number} tiffOffset TIFF offset
* @param {boolean} littleEndian Little endian encoding
* @param {object} includeTags Map of tags to include
* @param {object} excludeTags Map of tags to exclude
*/
function parseExifIFD(
data,
tagCode,
dataView,
tiffOffset,
littleEndian,
includeTags,
excludeTags
) {
var dirOffset = data.exif[tagCode]
if (dirOffset) {
data.exif[tagCode] = new ExifMap(tagCode)
if (data.exifOffsets) {
data.exifOffsets[tagCode] = new ExifMap(tagCode)
}
parseExifTags(
dataView,
tiffOffset,
tiffOffset + dirOffset,
littleEndian,
data.exif[tagCode],
data.exifOffsets && data.exifOffsets[tagCode],
includeTags && includeTags[tagCode],
excludeTags && excludeTags[tagCode]
)
}
}
loadImage.parseExifData = function (dataView, offset, length, data, options) {
if (options.disableExif) {
return
}
var includeTags = options.includeExifTags
var excludeTags = options.excludeExifTags || {
0x8769: {
// ExifIFDPointer
0x927c: true // MakerNote
}
}
var tiffOffset = offset + 10
var littleEndian
var dirOffset
var thumbnailIFD
// Check for the ASCII code for "Exif" (0x45786966):
if (dataView.getUint32(offset + 4) !== 0x45786966) {
// No Exif data, might be XMP data instead
return
}
if (tiffOffset + 8 > dataView.byteLength) {
console.log('Invalid Exif data: Invalid segment size.')
return
}
// Check for the two null bytes:
if (dataView.getUint16(offset + 8) !== 0x0000) {
console.log('Invalid Exif data: Missing byte alignment offset.')
return
}
// Check the byte alignment:
switch (dataView.getUint16(tiffOffset)) {
case 0x4949:
littleEndian = true
break
case 0x4d4d:
littleEndian = false
break
default:
console.log('Invalid Exif data: Invalid byte alignment marker.')
return
}
// Check for the TIFF tag marker (0x002A):
if (dataView.getUint16(tiffOffset + 2, littleEndian) !== 0x002a) {
console.log('Invalid Exif data: Missing TIFF marker.')
return
}
// Retrieve the directory offset bytes, usually 0x00000008 or 8 decimal:
dirOffset = dataView.getUint32(tiffOffset + 4, littleEndian)
// Create the exif object to store the tags:
data.exif = new ExifMap()
if (!options.disableExifOffsets) {
data.exifOffsets = new ExifMap()
data.exifTiffOffset = tiffOffset
data.exifLittleEndian = littleEndian
}
// Parse the tags of the main image directory (IFD0) and retrieve the
// offset to the next directory (IFD1), usually the thumbnail directory:
dirOffset = parseExifTags(
dataView,
tiffOffset,
tiffOffset + dirOffset,
littleEndian,
data.exif,
data.exifOffsets,
includeTags,
excludeTags
)
if (dirOffset && shouldIncludeTag(includeTags, excludeTags, 'ifd1')) {
data.exif.ifd1 = dirOffset
if (data.exifOffsets) {
data.exifOffsets.ifd1 = tiffOffset + dirOffset
}
}
Object.keys(data.exif.ifds).forEach(function (tagCode) {
parseExifIFD(
data,
tagCode,
dataView,
tiffOffset,
littleEndian,
includeTags,
excludeTags
)
})
thumbnailIFD = data.exif.ifd1
// Check for JPEG Thumbnail offset and data length:
if (thumbnailIFD && thumbnailIFD[0x0201]) {
thumbnailIFD[0x0201] = getExifThumbnail(
dataView,
tiffOffset + thumbnailIFD[0x0201],
thumbnailIFD[0x0202] // Thumbnail data length
)
}
}
// Registers the Exif parser for the APP1 JPEG metadata segment:
loadImage.metaDataParsers.jpeg[0xffe1].push(loadImage.parseExifData)
loadImage.exifWriters = {
// Orientation writer:
0x0112: function (buffer, data, value) {
var orientationOffset = data.exifOffsets[0x0112]
if (!orientationOffset) return buffer
var view = new DataView(buffer, orientationOffset + 8, 2)
view.setUint16(0, value, data.exifLittleEndian)
return buffer
}
}
loadImage.writeExifData = function (buffer, data, id, value) {
return loadImage.exifWriters[data.exif.map[id]](buffer, data, value)
}
loadImage.ExifMap = ExifMap
// Adds the following properties to the parseMetaData callback data:
// - exif: The parsed Exif tags
// - exifOffsets: The parsed Exif tag offsets
// - exifTiffOffset: TIFF header offset (used for offset pointers)
// - exifLittleEndian: little endian order if true, big endian if false
// Adds the following options to the parseMetaData method:
// - disableExif: Disables Exif parsing when true.
// - disableExifOffsets: Disables storing Exif tag offsets when true.
// - includeExifTags: A map of Exif tags to include for parsing.
// - excludeExifTags: A map of Exif tags to exclude from parsing.
})