UNPKG

exif

Version:

A node.js library to extract Exif metadata from images.

1,065 lines (927 loc) 35.5 kB
/*jslint node: true */ "use strict"; var assert = require('assert'); var fs = require('fs'); var util = require('util'); var BufferExtender = require('./Buffer'); // bad idea var debug = require('debug')('exif'); var DEFAULT_MAX_ENTRIES=128; /** * Represents an image with Exif information. When instantiating it you have to * provide an image and a callback function which is called once all metadata * is extracted from the image. * * Available options are: * - image The image to get Exif data from can be either a filesystem path or * a Buffer. * - tiffOffsets (boolean) an object named "offsets" is added to exifData * and contains lot of offsets needed to get thumbnail and other things. * - fixThumbnailOffset: node-exif corrects the thumbnail offset in order to have an offset from the start of the buffer/file. * - maxEntries: Specifies the maximum entries to be parsed * - ifd0MaxEntries * - ifd1MaxEntries * - maxGpsEntries * - maxInteroperabilityEntries * - agfaMaxEntries * - epsonMaxEntries * - fujifilmMaxEntries * - olympusMaxEntries * - panasonicMaxEntries * - sanyoMaxEntries * - noPadding * * If you don't set the image field, you might call exifImage.loadImage(image, callback) to get exif datas. * * @param options Configuration options as described above * @param callback Function to call when data is extracted or an error occurred * @return Nothing of importance, calls the specified callback function instead */ function ExifImage (options, callback) { if (!(this instanceof ExifImage)) { if (typeof(options)==="string") { options = { image: options } } assert(typeof(options)==="object", "Invalid options object"); var exifImage = new ExifImage(options, function(error, data) { if (error) { return callback(error); } callback(null, data, options.image); }); return exifImage; } if (typeof(options)==="string") { options= { image: options } } else if (options instanceof Buffer) { options= { image: options } } var ops={}; if (options) { for(var k in options) { ops[k]=options[k]; } } this.options=ops; // Default option values ["ifd0MaxEntries", "ifd1MaxEntries", "maxGpsEntries", "maxInteroperabilityEntries", "agfaMaxEntries", "epsonMaxEntries", "fujifilmMaxEntries", "olympusMaxEntries", "panasonicMaxEntries", "sanyoMaxEntries"].forEach(function(p) { if (ops[p]===undefined) { ops[p]=DEFAULT_MAX_ENTRIES; } }); this.exifData = { image : {}, // Information about the main image thumbnail : {}, // Information about the thumbnail exif : {}, // Exif information gps : {}, // GPS information interoperability: {}, // Exif Interoperability information makernote : {} // Makernote information }; this.offsets={}; if (ops.tiffOffsets) { exifData.offsets=offsets; } debug("New ExifImage options=",options); if (!ops.image) { // If options image is not specified, the developper must call loadImage() to parse the image. // callback(new Error('You have to provide an image, it is pretty hard to extract Exif data from nothing...')); return; } if (typeof callback !== 'function') { throw new Error('You have to provide a callback function.'); } var self=this; setImmediate(function() { self.loadImage(ops.image, function (error, exifData) { if (error) { return callback(error); } callback(null, exifData, ops.image); }); }); } ExifImage.ExifImage=ExifImage; module.exports = ExifImage; /** * Load image and parse exifDatas * * @param [String|Buffer] image the image * @param callback a callback which is called when exif datas are parsed. * @return Nothing */ ExifImage.prototype.loadImage = function (image, callback) { assert(typeof(callback)==="function", "Callback must be a function"); var self = this; debug("loadImage image=", image); if (image.constructor.name === 'Buffer') { this.processImage("Buffer", image, callback); return; } if (image.constructor.name === 'String') { fs.readFile(image, function (error, data) { if (error) { callback(new Error('Encountered the following error while trying to read given image: '+error)); return; } self.processImage("File: "+image, data, callback); }); return; } callback(new Error('Given image is neither a buffer nor a file, please provide one of these.')); }; ExifImage.prototype.processImage = function (source, data, callback) { assert(typeof(source)==="string", "Source must be a string"); assert(typeof(callback)==="function", "Callback must be a function"); var offset = 0; if (data[offset++] != 0xFF || data[offset++] != 0xD8) { var e=new Error('The given image is not a JPEG and thus unsupported right now.'); e.source=source; e.code="NOT_A_JPEG"; callback(e); return; } this.imageType = 'JPEG'; while (offset < data.length) { if (data[offset++] != 0xFF) { break; } if (data[offset++] == 0xE1) { try { this.extractExifData(data, offset + 2, data.getShort(offset, true) - 2); } catch (error) { error.code="PARSING_ERROR"; error.source=source; debug("Extract exif data error source=", source, "offset=", offset, "error=",error); callback(error); return; } debug("Extract exif data success source=", source, "exifData=",this.exifData); callback(null, this.exifData); return; } offset += data.getShort(offset, true); } var e2=new Error('No Exif segment found in the given image.'); e2.source=source; e2.code="NO_EXIF_SEGMENT"; callback(e2); }; ExifImage.prototype.extractExifData = function (data, start, length) { var exifData=this.exifData; var tiffOffset = start + 6; var ifdOffset, numberOfEntries; var noPadding = (this.options.noPadding!==false); this.offsets.tiff=tiffOffset; // Exif data always starts with Exif\0\0 if (data.toString('utf8', start, tiffOffset) != 'Exif\0\0') { throw new Error('The Exif data is not valid.'); } // After the Exif start we either have 0x4949 if the following data is // stored in big endian or 0x4D4D if it is stored in little endian if (data.getShort(tiffOffset) == 0x4949) { this.isBigEndian = false; } else if (data.getShort(tiffOffset) == 0x4D4D) { this.isBigEndian = true; } else { throw new Error('Invalid TIFF data! Expected 0x4949 or 0x4D4D at offset '+(tiffOffset)+' but found 0x'+data[tiffOffset].toString(16).toUpperCase()+data[tiffOffset + 1].toString(16).toUpperCase()+"."); } debug("BigEndian=",this.isBigEndian); // Valid TIFF headers always have 0x002A here if (data.getShort(tiffOffset + 2, this.isBigEndian) != 0x002A) { var expected = (this.isBigEndian) ? '0x002A' : '0x2A00'; throw new Error('Invalid TIFF data! Expected '+expected+' at offset '+(tiffOffset + 2)+' but found 0x'+data[tiffOffset + 2].toString(16).toUpperCase()+data[tiffOffset + 3].toString(16).toUpperCase()+"."); } /********************************* IFD0 **********************************/ // Offset to IFD0 which is always followed by two bytes with the amount of // entries in this IFD ifdOffset = tiffOffset + data.getLong(tiffOffset + 4, this.isBigEndian); this.offsets.ifd0=ifdOffset; numberOfEntries = data.getShort(ifdOffset, this.isBigEndian); if (this.options.ifd0MaxEntries) { numberOfEntries=Math.min(numberOfEntries, this.options.ifd0MaxEntries); } debug("IFD0 ifdOffset=",ifdOffset, "numberOfEntries=", numberOfEntries); // Each IFD entry consists of 12 bytes which we loop through and extract // the data from for (var i = 0; i < numberOfEntries; i++) { var exifEntry = this.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif); if (!exifEntry) { continue; } if (exifEntry.tagId===0xEA1C && noPadding) { continue; } exifData.image[exifEntry.tagName] = exifEntry.value; } debug("IFD0 parsed", exifData.image); /********************************* IFD1 **********************************/ // Check if there is an offset for IFD1. If so it is always followed by two // bytes with the amount of entries in this IFD, if not there is no IFD1 var nextIfdOffset = data.getLong(ifdOffset + 2 + (numberOfEntries * 12), this.isBigEndian) if (nextIfdOffset != 0x00000000) { ifdOffset = tiffOffset + nextIfdOffset; this.offsets.ifd1=ifdOffset; numberOfEntries = data.getShort(ifdOffset, this.isBigEndian); if (this.options.ifd1MaxEntries) { numberOfEntries=Math.min(numberOfEntries, this.options.ifd1MaxEntries); } debug("IFD1 ifdOffset=",ifdOffset, "numberOfEntries=", numberOfEntries); // Each IFD entry consists of 12 bytes which we loop through and extract // the data from for (var i = 0; i < numberOfEntries; i++) { var exifEntry = this.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif); if (!exifEntry) { continue; } if (exifEntry.tagId===0xEA1C && noPadding) { continue; } exifData.thumbnail[exifEntry.tagName] = exifEntry.value; } if (this.options.fixThumbnailOffset) { var thumbnailOffset=exifData.thumbnail[ExifImage.TAGS.exif[0x0201]]; if (thumbnailOffset) { debug("IFD1 fix thumbnail offset, add=",this.offsets.tiff); exifData.thumbnail[ExifImage.TAGS.exif[0x0201]]+=this.offsets.tiff; } } debug("IFD1 parsed", exifData.thumbnail); } /******************************* EXIF IFD ********************************/ // Look for a pointer to the Exif IFD in IFD0 and extract information from // it if available if (exifData.image[ExifImage.TAGS.exif[0x8769]]) { ifdOffset = tiffOffset + exifData.image[ExifImage.TAGS.exif[0x8769]]; this.offsets.tags=ifdOffset; numberOfEntries = data.getShort(ifdOffset, this.isBigEndian); if (this.options.maxEntries) { numberOfEntries=Math.min(numberOfEntries, this.options.maxEntries); } debug("EXIF IFD ifdOffset=",ifdOffset, "numberOfEntries=", numberOfEntries); // Each IFD entry consists of 12 bytes which we loop through and extract // the data from for (var i = 0; i < numberOfEntries; i++) { var exifEntry = this.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif); if (!exifEntry) { continue; } if (exifEntry.tagId===0xEA1C && noPadding) { continue; } exifData.exif[exifEntry.tagName] = exifEntry.value; } debug("EXIF IFD parsed",exifData.exif); } /******************************** GPS IFD ********************************/ // Look for a pointer to the GPS IFD in IFD0 and extract information from // it if available if (exifData.image[ExifImage.TAGS.exif[0x8825]]) { ifdOffset = tiffOffset + exifData.image[ExifImage.TAGS.exif[0x8825]]; this.offsets.gps=ifdOffset; numberOfEntries = data.getShort(ifdOffset, this.isBigEndian); if (this.options.maxGpsEntries) { numberOfEntries=Math.min(numberOfEntries, this.options.maxGpsEntries); } debug("GPS IFD ifdOffset=", ifdOffset, "numberOfEntries=", numberOfEntries); // Each IFD entry consists of 12 bytes which we loop through and extract // the data from for (var i = 0; i < numberOfEntries; i++) { var exifEntry = this.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.gps); if (!exifEntry) { continue; } if (exifEntry.tagId===0xEA1C && noPadding) { continue; } exifData.gps[exifEntry.tagName] = exifEntry.value; } debug("GPS IFD parsed",exifData.gps); } /************************* Interoperability IFD **************************/ // Look for a pointer to the interoperatbility IFD in the Exif IFD and // extract information from it if available if (exifData.exif[ExifImage.TAGS.exif[0xA005]]) { ifdOffset = tiffOffset + exifData.exif[ExifImage.TAGS.exif[0xA005]]; this.offsets.interoperability=ifdOffset; numberOfEntries = data.getShort(ifdOffset, this.isBigEndian); if (this.options.maxInteroperabilityEntries) { numberOfEntries=Math.min(numberOfEntries, this.options.maxInteroperabilityEntries); } debug("Interoperability IFD ifdOffset=", ifdOffset, "numberOfEntries=", numberOfEntries); // Each IFD entry consists of 12 bytes which we loop through and extract // the data from for (var i = 0; i < numberOfEntries; i++) { var exifEntry = this.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif); if (!exifEntry) { break; } if (exifEntry.tagId===0xEA1C && noPadding) { continue; } exifData.interoperability[exifEntry.tagName] = exifEntry.value; } debug("Interoperability IFD parsed",exifData.gps); } /***************************** Makernote IFD *****************************/ // Look for Makernote data in the Exif IFD, check which type of proprietary // Makernotes the image contains, load the respective functionality and // start the extraction if (typeof exifData.exif[ExifImage.TAGS.exif[0x927C]] != "undefined") { var type; // Check the header to see what kind of Makernote we are dealing with if (exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 7) === "OLYMP\x00\x01" || exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 7) === "OLYMP\x00\x02") { type="olympus" } else if (exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 7) === "AGFA \x00\x01") { type="agfa"; } else if (exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 8) === "EPSON\x00\x01\x00") { type="epson"; } else if (exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 8) === "FUJIFILM") { type="fujifilm"; } else if (exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 9) === "Panasonic") { type="panasonic"; } else if (exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 5) === "SANYO") { type="sanyo"; } debug("Makernote IFD ifdOffset=", ifdOffset, "type=", type); if (type) { var extractMakernotes = require('./makernotes/'+type).extractMakernotes; exifData.makernote = extractMakernotes.call(this, data, this.makernoteOffset, tiffOffset); } else { // Makernotes are available but the format is not recognized so // an error message is pushed instead, this ain't the best // solution but should do for now exifData.makernote['error'] = 'Unable to extract Makernote information as it is in an unsupported or unrecognized format.'; } debug("Makernote IFD parsed",exifData.makernote); } }; ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset, isBigEndian, tags) { var entry = { tag : data.slice(entryOffset, entryOffset + 2), tagId : null, tagName : null, format : data.getShort(entryOffset + 2, isBigEndian), components : data.getLong(entryOffset + 4, isBigEndian), valueOffset: null, value : [] } entry.tagId = entry.tag.getShort(0, isBigEndian); // The tagId may correspond to more then one tagName so check which if (tags && tags[entry.tagId] && typeof tags[entry.tagId] == "function") { entry.tagName = tags[entry.tagId].call(this, entry); if (!entry.tagName) { return false; } // The tagId corresponds to exactly one tagName } else if (tags && tags[entry.tagId]) { entry.tagName = tags[entry.tagId]; if (entry.tagName===undefined) { return false; } // The tagId is not recognized } else { return false; } switch (entry.format) { case 0x0001: // unsigned byte, 1 byte per component entry.valueOffset = (entry.components <= 4) ? entryOffset + 8 : data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; for (var i = 0; i < entry.components; i++) entry.value.push(data.getByte(entry.valueOffset + i)); break; case 0x0002: // ascii strings, 1 byte per component entry.valueOffset = (entry.components <= 4) ? entryOffset + 8 : data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; entry.value = data.getString(entry.valueOffset, entry.components); if (entry.value[entry.value.length - 1] === "\u0000") // Trim null terminated strings entry.value = entry.value.substring(0, entry.value.length - 1); break; case 0x0003: // unsigned short, 2 byte per component entry.valueOffset = (entry.components <= 2) ? entryOffset + 8 : data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; for (var i = 0; i < entry.components; i++) entry.value.push(data.getShort(entry.valueOffset + i * 2, isBigEndian)); break; case 0x0004: // unsigned long, 4 byte per component entry.valueOffset = (entry.components == 1) ? entryOffset + 8 : data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; for (var i = 0; i < entry.components; i++) entry.value.push(data.getLong(entry.valueOffset + i * 4, isBigEndian)); break; case 0x0005: // unsigned rational, 8 byte per component (4 byte numerator and 4 byte denominator) entry.valueOffset = data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; for (var i = 0; i < entry.components; i++) entry.value.push(data.getLong(entry.valueOffset + i * 8, isBigEndian) / data.getLong(entry.valueOffset + i * 8 + 4, isBigEndian)); break; case 0x0006: // signed byte, 1 byte per component entry.valueOffset = (entry.components <= 4) ? entryOffset + 8 : data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; for (var i = 0; i < entry.components; i++) entry.value.push(data.getSignedByte(entry.valueOffset + i)); break; case 0x0007: // undefined, 1 byte per component entry.valueOffset = (entry.components <= 4) ? entryOffset + 8 : data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; entry.value.push(data.slice(entry.valueOffset, entry.valueOffset + entry.components)); break; case 0x0008: // signed short, 2 byte per component entry.valueOffset = (entry.components <= 2) ? entryOffset + 8 : data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; for (var i = 0; i < entry.components; i++) entry.value.push(data.getSignedShort(entry.valueOffset + i * 2, isBigEndian)); break; case 0x0009: // signed long, 4 byte per component entry.valueOffset = (entry.components == 1) ? entryOffset + 8 : data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; for (var i = 0; i < entry.components; i++) entry.value.push(data.getSignedLong(entry.valueOffset + i * 4, isBigEndian)); break; case 0x000A: // signed rational, 8 byte per component (4 byte numerator and 4 byte denominator) entry.valueOffset = data.getLong(entryOffset + 8, isBigEndian) + tiffOffset; for (var i = 0; i < entry.components; i++) entry.value.push(data.getSignedLong(entry.valueOffset + i * 8, isBigEndian) / data.getSignedLong(entry.valueOffset + i * 8 + 4, isBigEndian)); break; default: return false; } // If this is the Makernote tag save its offset for later use if (entry.tagName === "MakerNote") { this.offsets.makernoteOffset = entry.valueOffset; } // If the value array has only one element we don't need an array if (entry.value.length == 1) { entry.value = entry.value[0]; } return entry; }; /** * Comprehensive list of TIFF and Exif tags found on * http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/EXIF.html */ ExifImage.TAGS = { // Exif tags exif : { 0x0001 : "InteropIndex", 0x0002 : "InteropVersion", 0x000B : "ProcessingSoftware", 0x00FE : "SubfileType", 0x00FF : "OldSubfileType", 0x0100 : "ImageWidth", 0x0101 : "ImageHeight", 0x0102 : "BitsPerSample", 0x0103 : "Compression", 0x0106 : "PhotometricInterpretation", 0x0107 : "Thresholding", 0x0108 : "CellWidth", 0x0109 : "CellLength", 0x010A : "FillOrder", 0x010D : "DocumentName", 0x010E : "ImageDescription", 0x010F : "Make", 0x0110 : "Model", 0x0111 : "StripOffsets", 0x0112 : "Orientation", 0x0115 : "SamplesPerPixel", 0x0116 : "RowsPerStrip", 0x0117 : "StripByteCounts", 0x0118 : "MinSampleValue", 0x0119 : "MaxSampleValue", 0x011A : "XResolution", 0x011B : "YResolution", 0x011C : "PlanarConfiguration", 0x011D : "PageName", 0x011E : "XPosition", 0x011F : "YPosition", 0x0120 : "FreeOffsets", 0x0121 : "FreeByteCounts", 0x0122 : "GrayResponseUnit", 0x0123 : "GrayResponseCurve", 0x0124 : "T4Options", 0x0125 : "T6Options", 0x0128 : "ResolutionUnit", 0x0129 : "PageNumber", 0x012C : "ColorResponseUnit", 0x012D : "TransferFunction", 0x0131 : "Software", 0x0132 : "ModifyDate", 0x013B : "Artist", 0x013C : "HostComputer", 0x013D : "Predictor", 0x013E : "WhitePoint", 0x013F : "PrimaryChromaticities", 0x0140 : "ColorMap", 0x0141 : "HalftoneHints", 0x0142 : "TileWidth", 0x0143 : "TileLength", 0x0144 : "TileOffsets", 0x0145 : "TileByteCounts", 0x0146 : "BadFaxLines", 0x0147 : "CleanFaxData", 0x0148 : "ConsecutiveBadFaxLines", 0x014A : "SubIFD", 0x014C : "InkSet", 0x014D : "InkNames", 0x014E : "NumberofInks", 0x0150 : "DotRange", 0x0151 : "TargetPrinter", 0x0152 : "ExtraSamples", 0x0153 : "SampleFormat", 0x0154 : "SMinSampleValue", 0x0155 : "SMaxSampleValue", 0x0156 : "TransferRange", 0x0157 : "ClipPath", 0x0158 : "XClipPathUnits", 0x0159 : "YClipPathUnits", 0x015A : "Indexed", 0x015B : "JPEGTables", 0x015F : "OPIProxy", 0x0190 : "GlobalParametersIFD", 0x0191 : "ProfileType", 0x0192 : "FaxProfile", 0x0193 : "CodingMethods", 0x0194 : "VersionYear", 0x0195 : "ModeNumber", 0x01B1 : "Decode", 0x01B2 : "DefaultImageColor", 0x01B3 : "T82Options", 0x01B5 : "JPEGTables", 0x0200 : "JPEGProc", 0x0201 : "ThumbnailOffset", 0x0202 : "ThumbnailLength", 0x0203 : "JPEGRestartInterval", 0x0205 : "JPEGLosslessPredictors", 0x0206 : "JPEGPointTransforms", 0x0207 : "JPEGQTables", 0x0208 : "JPEGDCTables", 0x0209 : "JPEGACTables", 0x0211 : "YCbCrCoefficients", 0x0212 : "YCbCrSubSampling", 0x0213 : "YCbCrPositioning", 0x0214 : "ReferenceBlackWhite", 0x022F : "StripRowCounts", 0x02BC : "ApplicationNotes", 0x03E7 : "USPTOMiscellaneous", 0x1000 : "RelatedImageFileFormat", 0x1001 : "RelatedImageWidth", 0x1002 : "RelatedImageHeight", 0x4746 : "Rating", 0x4747 : "XP_DIP_XML", 0x4748 : "StitchInfo", 0x4749 : "RatingPercent", 0x800D : "ImageID", 0x80A3 : "WangTag1", 0x80A4 : "WangAnnotation", 0x80A5 : "WangTag3", 0x80A6 : "WangTag4", 0x80E3 : "Matteing", 0x80E4 : "DataType", 0x80E5 : "ImageDepth", 0x80E6 : "TileDepth", 0x827D : "Model2", 0x828D : "CFARepeatPatternDim", 0x828E : "CFAPattern2", 0x828F : "BatteryLevel", 0x8290 : "KodakIFD", 0x8298 : "Copyright", 0x829A : "ExposureTime", 0x829D : "FNumber", 0x82A5 : "MDFileTag", 0x82A6 : "MDScalePixel", 0x82A7 : "MDColorTable", 0x82A8 : "MDLabName", 0x82A9 : "MDSampleInfo", 0x82AA : "MDPrepDate", 0x82AB : "MDPrepTime", 0x82AC : "MDFileUnits", 0x830E : "PixelScale", 0x8335 : "AdventScale", 0x8336 : "AdventRevision", 0x835C : "UIC1Tag", 0x835D : "UIC2Tag", 0x835E : "UIC3Tag", 0x835F : "UIC4Tag", 0x83BB : "IPTC-NAA", 0x847E : "IntergraphPacketData", 0x847F : "IntergraphFlagRegisters", 0x8480 : "IntergraphMatrix", 0x8481 : "INGRReserved", 0x8482 : "ModelTiePoint", 0x84E0 : "Site", 0x84E1 : "ColorSequence", 0x84E2 : "IT8Header", 0x84E3 : "RasterPadding", 0x84E4 : "BitsPerRunLength", 0x84E5 : "BitsPerExtendedRunLength", 0x84E6 : "ColorTable", 0x84E7 : "ImageColorIndicator", 0x84E8 : "BackgroundColorIndicator", 0x84E9 : "ImageColorValue", 0x84EA : "BackgroundColorValue", 0x84EB : "PixelIntensityRange", 0x84EC : "TransparencyIndicator", 0x84ED : "ColorCharacterization", 0x84EE : "HCUsage", 0x84EF : "TrapIndicator", 0x84F0 : "CMYKEquivalent", 0x8546 : "SEMInfo", 0x8568 : "AFCP_IPTC", 0x85B8 : "PixelMagicJBIGOptions", 0x85D8 : "ModelTransform", 0x8602 : "WB_GRGBLevels", 0x8606 : "LeafData", 0x8649 : "PhotoshopSettings", 0x8769 : "ExifOffset", 0x8773 : "ICC_Profile", 0x877F : "TIFF_FXExtensions", 0x8780 : "MultiProfiles", 0x8781 : "SharedData", 0x8782 : "T88Options", 0x87AC : "ImageLayer", 0x87AF : "GeoTiffDirectory", 0x87B0 : "GeoTiffDoubleParams", 0x87B1 : "GeoTiffAsciiParams", 0x8822 : "ExposureProgram", 0x8824 : "SpectralSensitivity", 0x8825 : "GPSInfo", 0x8827 : "ISO", 0x8828 : "Opto-ElectricConvFactor", 0x8829 : "Interlace", 0x882A : "TimeZoneOffset", 0x882B : "SelfTimerMode", 0x8830 : "SensitivityType", 0x8831 : "StandardOutputSensitivity", 0x8832 : "RecommendedExposureIndex", 0x8833 : "ISOSpeed", 0x8834 : "ISOSpeedLatitudeyyy", 0x8835 : "ISOSpeedLatitudezzz", 0x885C : "FaxRecvParams", 0x885D : "FaxSubAddress", 0x885E : "FaxRecvTime", 0x888A : "LeafSubIFD", 0x9000 : "ExifVersion", 0x9003 : "DateTimeOriginal", 0x9004 : "CreateDate", 0x9101 : "ComponentsConfiguration", 0x9102 : "CompressedBitsPerPixel", 0x9201 : "ShutterSpeedValue", 0x9202 : "ApertureValue", 0x9203 : "BrightnessValue", 0x9204 : "ExposureCompensation", 0x9205 : "MaxApertureValue", 0x9206 : "SubjectDistance", 0x9207 : "MeteringMode", 0x9208 : "LightSource", 0x9209 : "Flash", 0x920A : "FocalLength", 0x920B : "FlashEnergy", 0x920C : "SpatialFrequencyResponse", 0x920D : "Noise", 0x920E : "FocalPlaneXResolution", 0x920F : "FocalPlaneYResolution", 0x9210 : "FocalPlaneResolutionUnit", 0x9211 : "ImageNumber", 0x9212 : "SecurityClassification", 0x9213 : "ImageHistory", 0x9214 : "SubjectArea", 0x9215 : "ExposureIndex", 0x9216 : "TIFF-EPStandardID", 0x9217 : "SensingMethod", 0x923A : "CIP3DataFile", 0x923B : "CIP3Sheet", 0x923C : "CIP3Side", 0x923F : "StoNits", 0x927C : "MakerNote", 0x9286 : "UserComment", 0x9290 : "SubSecTime", 0x9291 : "SubSecTimeOriginal", 0x9292 : "SubSecTimeDigitized", 0x932F : "MSDocumentText", 0x9330 : "MSPropertySetStorage", 0x9331 : "MSDocumentTextPosition", 0x935C : "ImageSourceData", 0x9C9B : "XPTitle", 0x9C9C : "XPComment", 0x9C9D : "XPAuthor", 0x9C9E : "XPKeywords", 0x9C9F : "XPSubject", 0xA000 : "FlashpixVersion", 0xA001 : "ColorSpace", 0xA002 : "ExifImageWidth", 0xA003 : "ExifImageHeight", 0xA004 : "RelatedSoundFile", 0xA005 : "InteropOffset", 0xA20B : "FlashEnergy", 0xA20C : "SpatialFrequencyResponse", 0xA20D : "Noise", 0xA20E : "FocalPlaneXResolution", 0xA20F : "FocalPlaneYResolution", 0xA210 : "FocalPlaneResolutionUnit", 0xA211 : "ImageNumber", 0xA212 : "SecurityClassification", 0xA213 : "ImageHistory", 0xA214 : "SubjectLocation", 0xA215 : "ExposureIndex", 0xA216 : "TIFF-EPStandardID", 0xA217 : "SensingMethod", 0xA300 : "FileSource", 0xA301 : "SceneType", 0xA302 : "CFAPattern", 0xA401 : "CustomRendered", 0xA402 : "ExposureMode", 0xA403 : "WhiteBalance", 0xA404 : "DigitalZoomRatio", 0xA405 : "FocalLengthIn35mmFormat", 0xA406 : "SceneCaptureType", 0xA407 : "GainControl", 0xA408 : "Contrast", 0xA409 : "Saturation", 0xA40A : "Sharpness", 0xA40B : "DeviceSettingDescription", 0xA40C : "SubjectDistanceRange", 0xA420 : "ImageUniqueID", 0xA430 : "OwnerName", 0xA431 : "SerialNumber", 0xA432 : "LensInfo", 0xA433 : "LensMake", 0xA434 : "LensModel", 0xA435 : "LensSerialNumber", 0xA480 : "GDALMetadata", 0xA481 : "GDALNoData", 0xA500 : "Gamma", 0xAFC0 : "ExpandSoftware", 0xAFC1 : "ExpandLens", 0xAFC2 : "ExpandFilm", 0xAFC3 : "ExpandFilterLens", 0xAFC4 : "ExpandScanner", 0xAFC5 : "ExpandFlashLamp", 0xBC01 : "PixelFormat", 0xBC02 : "Transformation", 0xBC03 : "Uncompressed", 0xBC04 : "ImageType", 0xBC80 : "ImageWidth", 0xBC81 : "ImageHeight", 0xBC82 : "WidthResolution", 0xBC83 : "HeightResolution", 0xBCC0 : "ImageOffset", 0xBCC1 : "ImageByteCount", 0xBCC2 : "AlphaOffset", 0xBCC3 : "AlphaByteCount", 0xBCC4 : "ImageDataDiscard", 0xBCC5 : "AlphaDataDiscard", 0xC427 : "OceScanjobDesc", 0xC428 : "OceApplicationSelector", 0xC429 : "OceIDNumber", 0xC42A : "OceImageLogic", 0xC44F : "Annotations", 0xC4A5 : "PrintIM", 0xC580 : "USPTOOriginalContentType", 0xC612 : "DNGVersion", 0xC613 : "DNGBackwardVersion", 0xC614 : "UniqueCameraModel", 0xC615 : "LocalizedCameraModel", 0xC616 : "CFAPlaneColor", 0xC617 : "CFALayout", 0xC618 : "LinearizationTable", 0xC619 : "BlackLevelRepeatDim", 0xC61A : "BlackLevel", 0xC61B : "BlackLevelDeltaH", 0xC61C : "BlackLevelDeltaV", 0xC61D : "WhiteLevel", 0xC61E : "DefaultScale", 0xC61F : "DefaultCropOrigin", 0xC620 : "DefaultCropSize", 0xC621 : "ColorMatrix1", 0xC622 : "ColorMatrix2", 0xC623 : "CameraCalibration1", 0xC624 : "CameraCalibration2", 0xC625 : "ReductionMatrix1", 0xC626 : "ReductionMatrix2", 0xC627 : "AnalogBalance", 0xC628 : "AsShotNeutral", 0xC629 : "AsShotWhiteXY", 0xC62A : "BaselineExposure", 0xC62B : "BaselineNoise", 0xC62C : "BaselineSharpness", 0xC62D : "BayerGreenSplit", 0xC62E : "LinearResponseLimit", 0xC62F : "CameraSerialNumber", 0xC630 : "DNGLensInfo", 0xC631 : "ChromaBlurRadius", 0xC632 : "AntiAliasStrength", 0xC633 : "ShadowScale", 0xC634 : "DNGPrivateData", 0xC635 : "MakerNoteSafety", 0xC640 : "RawImageSegmentation", 0xC65A : "CalibrationIlluminant1", 0xC65B : "CalibrationIlluminant2", 0xC65C : "BestQualityScale", 0xC65D : "RawDataUniqueID", 0xC660 : "AliasLayerMetadata", 0xC68B : "OriginalRawFileName", 0xC68C : "OriginalRawFileData", 0xC68D : "ActiveArea", 0xC68E : "MaskedAreas", 0xC68F : "AsShotICCProfile", 0xC690 : "AsShotPreProfileMatrix", 0xC691 : "CurrentICCProfile", 0xC692 : "CurrentPreProfileMatrix", 0xC6BF : "ColorimetricReference", 0xC6D2 : "PanasonicTitle", 0xC6D3 : "PanasonicTitle2", 0xC6F3 : "CameraCalibrationSig", 0xC6F4 : "ProfileCalibrationSig", 0xC6F5 : "ProfileIFD", 0xC6F6 : "AsShotProfileName", 0xC6F7 : "NoiseReductionApplied", 0xC6F8 : "ProfileName", 0xC6F9 : "ProfileHueSatMapDims", 0xC6FA : "ProfileHueSatMapData1", 0xC6FB : "ProfileHueSatMapData2", 0xC6FC : "ProfileToneCurve", 0xC6FD : "ProfileEmbedPolicy", 0xC6FE : "ProfileCopyright", 0xC714 : "ForwardMatrix1", 0xC715 : "ForwardMatrix2", 0xC716 : "PreviewApplicationName", 0xC717 : "PreviewApplicationVersion", 0xC718 : "PreviewSettingsName", 0xC719 : "PreviewSettingsDigest", 0xC71A : "PreviewColorSpace", 0xC71B : "PreviewDateTime", 0xC71C : "RawImageDigest", 0xC71D : "OriginalRawFileDigest", 0xC71E : "SubTileBlockSize", 0xC71F : "RowInterleaveFactor", 0xC725 : "ProfileLookTableDims", 0xC726 : "ProfileLookTableData", 0xC740 : "OpcodeList1", 0xC741 : "OpcodeList2", 0xC74E : "OpcodeList3", 0xC761 : "NoiseProfile", 0xC763 : "TimeCodes", 0xC764 : "FrameRate", 0xC772 : "TStop", 0xC789 : "ReelName", 0xC791 : "OriginalDefaultFinalSize", 0xC792 : "OriginalBestQualitySize", 0xC793 : "OriginalDefaultCropSize", 0xC7A1 : "CameraLabel", 0xC7A3 : "ProfileHueSatMapEncoding", 0xC7A4 : "ProfileLookTableEncoding", 0xC7A5 : "BaselineExposureOffset", 0xC7A6 : "DefaultBlackRender", 0xC7A7 : "NewRawImageDigest", 0xC7A8 : "RawToPreviewGain", 0xC7B5 : "DefaultUserCrop", 0xEA1C : "Padding", 0xEA1D : "OffsetSchema", 0xFDE8 : "OwnerName", 0xFDE9 : "SerialNumber", 0xFDEA : "Lens", 0xFE00 : "KDC_IFD", 0xFE4C : "RawFile", 0xFE4D : "Converter", 0xFE4E : "WhiteBalance", 0xFE51 : "Exposure", 0xFE52 : "Shadows", 0xFE53 : "Brightness", 0xFE54 : "Contrast", 0xFE55 : "Saturation", 0xFE56 : "Sharpness", 0xFE57 : "Smoothness", 0xFE58 : "MoireFilter" }, // GPS Tags gps : { 0x0000 : 'GPSVersionID', 0x0001 : 'GPSLatitudeRef', 0x0002 : 'GPSLatitude', 0x0003 : 'GPSLongitudeRef', 0x0004 : 'GPSLongitude', 0x0005 : 'GPSAltitudeRef', 0x0006 : 'GPSAltitude', 0x0007 : 'GPSTimeStamp', 0x0008 : 'GPSSatellites', 0x0009 : 'GPSStatus', 0x000A : 'GPSMeasureMode', 0x000B : 'GPSDOP', 0x000C : 'GPSSpeedRef', 0x000D : 'GPSSpeed', 0x000E : 'GPSTrackRef', 0x000F : 'GPSTrack', 0x0010 : 'GPSImgDirectionRef', 0x0011 : 'GPSImgDirection', 0x0012 : 'GPSMapDatum', 0x0013 : 'GPSDestLatitudeRef', 0x0014 : 'GPSDestLatitude', 0x0015 : 'GPSDestLongitudeRef', 0x0016 : 'GPSDestLongitude', 0x0017 : 'GPSDestBearingRef', 0x0018 : 'GPSDestBearing', 0x0019 : 'GPSDestDistanceRef', 0x001A : 'GPSDestDistance', 0x001B : 'GPSProcessingMethod', 0x001C : 'GPSAreaInformation', 0x001D : 'GPSDateStamp', 0x001E : 'GPSDifferential', 0x001F : 'GPSHPositioningError' } };