<commit_before>var fs = require('fs'),
    util = require('util'),
    BufferExtender = require('./Buffer');

/**
 * 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.
 * 
 * @param options Configuration options as described above
 * @param callback Function to call when data is extracted or an error occured 
 * @return Nothing of importance, calls the specified callback function instead
 */
function ExifImage (options, callback) {
	
	var self = this;
	
	if (!options) 
		var options = {};
	
	this.image;
	this.imageType;
	this.isBigEndian;
	
	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
	};
	
	if (!options.image) {
		throw new Error('You have to provide an image, it is pretty hard to extract Exif data from nothing...');
	} else if (typeof callback !== 'function') {
		throw new Error('You have to provide a callback function.');
	} else {
		this.loadImage(options.image, function (error, image) {
			if (error) 
				callback(error);
			else
				callback(false, image);
		});
	}
	
}

module.exports = ExifImage;

ExifImage.prototype.loadImage = function (image, callback) {

	var self = this;
	
	if (image.constructor.name === 'Buffer') {
		
		this.processImage(image, callback);
		
	} else 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));
			else
				self.processImage(data, callback);
		});
		
	} else {
		
		callback(new Error('Given image is neither a buffer nor a file, please provide one of these.'));

	}

};

ExifImage.prototype.processImage = function (data, callback) {

	var self = this;
	var offset = 0;
	
	if (data[offset++] == 0xFF && data[offset++] == 0xD8) {
		self.imageType = 'JPEG';
	} else {
		callback(new Error('The given image is not a JPEG and thus unsupported right now.'));
		return;
	}

	try {
	
		while (offset < data.length) {
			
			if (data[offset++] != 0xFF) {
				callback(new Error('Invalid marker found at offset '+(--offset)+'. Expected 0xFF but found 0x'+data[offset].toString(16).toUpperCase()+"."));
				return;
			}
			
			if (data[offset++] == 0xE1) {
				var exifData = self.extractExifData(data, offset + 2, data.getShort(offset, true) - 2);
				callback(false, exifData);
				return;
			} else {
				offset += data.getShort(offset, true);
			}
			
		}

	} catch (error) {
		callback(error);
	}
	
	callback(new Error('No Exif segment found in the given image.'));
	
};

ExifImage.prototype.extractExifData = function (data, start, length) {

	var self = this;
	var tiffOffset = start + 6;
	var ifdOffset, numberOfEntries;
	
	// Exif data always starts with Exif\0\0
	if (data.toString('utf8', start, tiffOffset) != 'Exif\0\0') {
		throw new Error('The Exif data ist 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()+".");
	}
	
	// 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);
	numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);
	
	// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif);
		if (exifEntry && exifEntry.tagName !== null) this.exifData.image[exifEntry.tagName] = exifEntry.value;
	}

	/********************************* 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;
		numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);
		
		// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif);
			if (exifEntry && exifEntry.tagName !== null) this.exifData.thumbnail[exifEntry.tagName] = exifEntry.value;
		}
	}
	
	/******************************* EXIF IFD ********************************/
	
	// Look for a pointer to the Exif IFD in IFD0 and extract information from
	// it if available
	if (typeof this.exifData.image[ExifImage.TAGS.exif[0x8769]] != "undefined") {

		ifdOffset = tiffOffset + this.exifData.image[ExifImage.TAGS.exif[0x8769]];
		numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);

		// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif);
			if (exifEntry && exifEntry.tagName !== null) this.exifData.exif[exifEntry.tagName] = exifEntry.value;
		}
			
	}
	
	/******************************** GPS IFD ********************************/
	
	// Look for a pointer to the GPS IFD in IFD0 and extract information from
	// it if available
	if (typeof this.exifData.image[ExifImage.TAGS.exif[0x8825]] != "undefined") {

		ifdOffset = tiffOffset + this.exifData.image[ExifImage.TAGS.exif[0x8825]];
		numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);
		
		// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.gps);
			if (exifEntry && exifEntry.tagName !== null) this.exifData.gps[exifEntry.tagName] = exifEntry.value;
		}
			
	}
	
	/************************* Interoperability IFD **************************/
	
	// Look for a pointer to the interoperatbility IFD in the Exif IFD and 
	// extract information from it if available
	if (typeof this.exifData.exif[ExifImage.TAGS.exif[0xA005]] != "undefined") {

		ifdOffset = tiffOffset + this.exifData.exif[ExifImage.TAGS.exif[0xA005]];
		numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);
		
		// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian);
			if (exifEntry && exifEntry.tagName !== null) this.exifData.interoperability[exifEntry.tagName] = exifEntry.value;
		}
			
	}
	
	/***************************** 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 this.exifData.exif[ExifImage.TAGS.exif[0x927C]] != "undefined") {

		// Check the header to see what kind of Makernote we are dealing with
		if (this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 7) === "OLYMP\x00\x01" || this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 7) === "OLYMP\x00\x02") {
			this.extractMakernotes = require('./makernotes/olympus').extractMakernotes;
		} else if (this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 7) === "AGFA \x00\x01") {
			this.extractMakernotes = require('./makernotes/agfa').extractMakernotes;
		} else if (this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 8) === "EPSON\x00\x01\x00") {
			this.extractMakernotes = require('./makernotes/epson').extractMakernotes;
		} else if (this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 8) === "FUJIFILM") {
			this.extractMakernotes = require('./makernotes/fujifilm').extractMakernotes;
		} 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
			this.exifData.makernote['error'] = 'Unable to extract Makernote information as it is in an unsupported or unrecognized format.';
		}
		
		this.exifData.makernote = this.extractMakernotes(data, this.exifData.exif[ExifImage.TAGS.exif[0x927C]].valueOffset, tiffOffset);
			
	}
	
	return this.exifData;
	
};

ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset, isBigEndian, tags) {

	var entry = { 
		tag : data.slice(entryOffset, entryOffset + 2),
		tagName : null,
		format : data.getShort(entryOffset + 2, isBigEndian),
		components : data.getLong(entryOffset + 4, isBigEndian),
		valueOffset: null,
		value : []
	}

	entry.tagName = (tags && tags[entry.tag.getShort(0, isBigEndian)]) ? tags[entry.tag.getShort(0, isBigEndian)] : null;

	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 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",<commit_msg>Fixed extraction of Makernotes<commit_after>var fs = require('fs'),
    util = require('util'),
    BufferExtender = require('./Buffer');

/**
 * 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.
 * 
 * @param options Configuration options as described above
 * @param callback Function to call when data is extracted or an error occured 
 * @return Nothing of importance, calls the specified callback function instead
 */
function ExifImage (options, callback) {
	
	var self = this;
	
	if (!options) 
		var options = {};
	
	this.image;
	this.imageType;
	this.isBigEndian;
	this.makernoteOffset;
	
	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
	};

	if (!options.image) {
		throw new Error('You have to provide an image, it is pretty hard to extract Exif data from nothing...');
	} else if (typeof callback !== 'function') {
		throw new Error('You have to provide a callback function.');
	} else {
		this.loadImage(options.image, function (error, image) {
			if (error) 
				callback(error);
			else
				callback(false, image);
		});
	}
	
}

module.exports = ExifImage;

ExifImage.prototype.loadImage = function (image, callback) {

	var self = this;
	
	if (image.constructor.name === 'Buffer') {
		
		this.processImage(image, callback);
		
	} else 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));
			else
				self.processImage(data, callback);
		});
		
	} else {
		
		callback(new Error('Given image is neither a buffer nor a file, please provide one of these.'));

	}

};

ExifImage.prototype.processImage = function (data, callback) {

	var self = this;
	var offset = 0;
	
	if (data[offset++] == 0xFF && data[offset++] == 0xD8) {
		self.imageType = 'JPEG';
	} else {
		callback(new Error('The given image is not a JPEG and thus unsupported right now.'));
		return;
	}

	try {
	
		while (offset < data.length) {
			
			if (data[offset++] != 0xFF) {
				callback(new Error('Invalid marker found at offset '+(--offset)+'. Expected 0xFF but found 0x'+data[offset].toString(16).toUpperCase()+"."));
				return;
			}
			
			if (data[offset++] == 0xE1) {
				var exifData = self.extractExifData(data, offset + 2, data.getShort(offset, true) - 2);
				callback(false, exifData);
				return;
			} else {
				offset += data.getShort(offset, true);
			}
			
		}

	} catch (error) {
		callback(error);
	}
	
	callback(new Error('No Exif segment found in the given image.'));
	
};

ExifImage.prototype.extractExifData = function (data, start, length) {

	var self = this;
	var tiffOffset = start + 6;
	var ifdOffset, numberOfEntries;
	
	// Exif data always starts with Exif\0\0
	if (data.toString('utf8', start, tiffOffset) != 'Exif\0\0') {
		throw new Error('The Exif data ist 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()+".");
	}
	
	// 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);
	numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);
	
	// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif);
		if (exifEntry && exifEntry.tagName !== null) this.exifData.image[exifEntry.tagName] = exifEntry.value;
	}

	/********************************* 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;
		numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);
		
		// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif);
			if (exifEntry && exifEntry.tagName !== null) this.exifData.thumbnail[exifEntry.tagName] = exifEntry.value;
		}
	}
	
	/******************************* EXIF IFD ********************************/
	
	// Look for a pointer to the Exif IFD in IFD0 and extract information from
	// it if available
	if (typeof this.exifData.image[ExifImage.TAGS.exif[0x8769]] != "undefined") {

		ifdOffset = tiffOffset + this.exifData.image[ExifImage.TAGS.exif[0x8769]];
		numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);

		// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.exif);
			if (exifEntry && exifEntry.tagName !== null) this.exifData.exif[exifEntry.tagName] = exifEntry.value;
		}
			
	}
	
	/******************************** GPS IFD ********************************/
	
	// Look for a pointer to the GPS IFD in IFD0 and extract information from
	// it if available
	if (typeof this.exifData.image[ExifImage.TAGS.exif[0x8825]] != "undefined") {

		ifdOffset = tiffOffset + this.exifData.image[ExifImage.TAGS.exif[0x8825]];
		numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);
		
		// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian, ExifImage.TAGS.gps);
			if (exifEntry && exifEntry.tagName !== null) this.exifData.gps[exifEntry.tagName] = exifEntry.value;
		}
			
	}
	
	/************************* Interoperability IFD **************************/
	
	// Look for a pointer to the interoperatbility IFD in the Exif IFD and 
	// extract information from it if available
	if (typeof this.exifData.exif[ExifImage.TAGS.exif[0xA005]] != "undefined") {

		ifdOffset = tiffOffset + this.exifData.exif[ExifImage.TAGS.exif[0xA005]];
		numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);
		
		// 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 = self.extractExifEntry(data, (ifdOffset + 2 + (i * 12)), tiffOffset, this.isBigEndian);
			if (exifEntry && exifEntry.tagName !== null) this.exifData.interoperability[exifEntry.tagName] = exifEntry.value;
		}
			
	}
	
	/***************************** 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 this.exifData.exif[ExifImage.TAGS.exif[0x927C]] != "undefined") {

		// Check the header to see what kind of Makernote we are dealing with
		if (this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 7) === "OLYMP\x00\x01" || this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 7) === "OLYMP\x00\x02") {
			this.extractMakernotes = require('./makernotes/olympus').extractMakernotes;
		} else if (this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 7) === "AGFA \x00\x01") {
			this.extractMakernotes = require('./makernotes/agfa').extractMakernotes;
		} else if (this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 8) === "EPSON\x00\x01\x00") {
			this.extractMakernotes = require('./makernotes/epson').extractMakernotes;
		} else if (this.exifData.exif[ExifImage.TAGS.exif[0x927C]].getString(0, 8) === "FUJIFILM") {
			this.extractMakernotes = require('./makernotes/fujifilm').extractMakernotes;
		} 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
			this.exifData.makernote['error'] = 'Unable to extract Makernote information as it is in an unsupported or unrecognized format.';
		}
		
		this.exifData.makernote = this.extractMakernotes(data, self.makernoteOffset, tiffOffset);
			
	}
	
	return this.exifData;
	
};

ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset, isBigEndian, tags) {

	var self = this;

	var entry = { 
		tag : data.slice(entryOffset, entryOffset + 2),
		tagName : null,
		format : data.getShort(entryOffset + 2, isBigEndian),
		components : data.getLong(entryOffset + 4, isBigEndian),
		valueOffset: null,
		value : []
	}

	entry.tagName = (tags && tags[entry.tag.getShort(0, isBigEndian)]) ? tags[entry.tag.getShort(0, isBigEndian)] : null;

	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") self.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",
{'fraction_non_alphanumeric': 0.1515750812261619, 'fraction_numerical': 0.024085322785704195, 'mean_word_length': 3.797052346264611, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 3, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1266774', 'n_tokens_mistral': 10744, 'n_tokens_neox': 9289, 'n_words': 2593}