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