461 lines
13 KiB
JavaScript
461 lines
13 KiB
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.
|
|
})
|