Skip to content

Instantly share code, notes, and snippets.

@redaktor
Last active August 29, 2015 14:01
Show Gist options
  • Select an option

  • Save redaktor/c3683f49cab545e91729 to your computer and use it in GitHub Desktop.

Select an option

Save redaktor/c3683f49cab545e91729 to your computer and use it in GitHub Desktop.

Revisions

  1. redaktor revised this gist May 7, 2014. No changes.
  2. redaktor revised this gist May 7, 2014. 1 changed file with 114 additions and 12 deletions.
    126 changes: 114 additions & 12 deletions EXIF node-exif
    Original file line number Diff line number Diff line change
    @@ -316,6 +316,20 @@ ExifImage.prototype.tidyString = function(str) {
    if (str.toLowerCase() == 'undefined' || str.toLowerCase() == 'unknown') str = '';
    return str.trim();
    }
    ExifImage.prototype.pad = function(input, chr, len) {
    var returnString = input;
    while (returnString.length < len) {
    returnString = chr + returnString;
    }
    return returnString;
    }
    ExifImage.prototype.intArrayToHexString = function(arrayOfInts) {
    var response = '';
    for ( var i in arrayOfInts) {
    response += ExifImage.prototype.pad(arrayOfInts[i].toString(16), '0', 2);
    }
    return response;
    }

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

    @@ -434,12 +448,16 @@ ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset,
    /* composite values */
    var pair = null;
    switch(entry.tagName){
    case 'SerialNumber':
    pair = self.exifData.image.Make;
    break;
    case 'GPSLatitude':
    pair = self.exifData.gps.GPSLatitudeRef.value;
    break;
    case 'GPSLongitude':
    pair = self.exifData.gps.GPSLongitudeRef.value;
    break;
    break;

    }
    entry.value = (pair) ? ref(entry.value, pair) : ref(entry.value);
    } else if (entry.value in ref){
    @@ -935,15 +953,15 @@ ExifImage.TAGS = {
    ref : {

    /* helper functions - TODO might go in helper module */
    arrToDeg: function(vArr, lRef){
    var deg = parseFloat(vArr[0]), m = parseFloat(vArr[1]), s = parseFloat(vArr[2]);
    arrToDeg: function(nArr, lRef){
    var deg = parseFloat(nArr[0]), m = parseFloat(nArr[1]), s = parseFloat(nArr[2]);
    if(s==0 && m>0){
    var _m = Math.floor(m);
    s = (m-_m)*60;
    m = _m;
    _m = null;
    }
    if(typeof deg !== 'number' || typeof m !== 'number' || typeof s !== 'number') return vArr;
    if(typeof deg !== 'number' || typeof m !== 'number' || typeof s !== 'number') return nArr;
    if (lRef === 'S' || lRef === 'N' || lRef === 'E' || lRef === 'W') {
    var lInt = (lRef == 'S' || lRef == 'W') ? -1 : 1;
    var v = (deg+(m/60)+(s/3600)) * lInt;
    @@ -955,7 +973,14 @@ ExifImage.TAGS = {
    return [deg, m, s];
    },
    decToFrac: function(d){

    /* TODO - NOTE : some vendors handle infinite values incorrect or have "finetuned" values
    // needs fix
    // e.g. Leica exposureCompensation: '-85/256 EV', value: -0.33203125 - preparse toFixed(2) ???
    */

    if(typeof d == 'number'){
    if(d===0) return 0;
    var pref = (d>0) ? '+' : '-';
    var df = 1, top = 1, bot = 1;
    var limit = 1e5;
    @@ -980,16 +1005,17 @@ ExifImage.TAGS = {
    var v = parseInt(vStr);
    return (typeof v === 'number' && v!=0) ? (v/100).toString() : vStr;
    },
    expoTime : function(_t){
    if (typeof _t === 'number' && _t < 0.25001 && _t > 0) {
    return '1/'.concat(Math.floor(0.5 + 1/_t));
    expoTime : function(t){
    if (typeof t === 'number' && t < 0.25001 && t > 0) {
    return '1/'.concat(Math.floor(0.5 + 1/t));
    }
    return (typeof _t === 'number') ? _t.toFixed(1).replace(/\.0$/, '') : _t.replace(/\.0$/, '');
    return (typeof t === 'number') ? t.toFixed(1).replace(/\.0$/, '') : t.replace(/\.0$/, '');
    },
    aperture : function(data){
    var v = Math.pow(2, (data / 2));
    return (typeof v == 'number') ? {description:v, value:data} : data;
    aperture : function(d){
    var v = Math.pow(2, (d / 2));
    return (typeof v == 'number') ? {description:v, value:d} : d;
    },

    /* helper end */

    ExifVersion : function(data){ return ExifImage.TAGS.ref.versions(data); },
    @@ -1013,6 +1039,82 @@ ExifImage.TAGS = {
    }
    }
    */
    SerialNumber : function(serial, make) {

    var returnSerial = ExifImage.prototype.tidyString(serial);

    switch (make) {
    case "Canon":
    if (returnSerial.length > 6) {
    returnSerial = ExifImage.prototype.pad(returnSerial, "0", 10);
    } else {
    returnSerial = ExifImage.prototype.pad(returnSerial, "0", 6);
    }
    break;
    case "FUJIFILM":
    var startOf12CharBlock = returnSerial.lastIndexOf(" ") + 1;
    if (startOf12CharBlock == -1) {
    returnSerial + "";
    break;
    }
    var iDateIndex = startOf12CharBlock + 12;
    var year = returnSerial.substr(iDateIndex, 2);
    if (year > 80) {
    year = "19" + year;
    } else {
    year = "20" + year;
    }
    var month = returnSerial.substr(iDateIndex + 2, 2);
    var date = returnSerial.substr(iDateIndex + 4, 2);
    var lastChunk = returnSerial.substr(iDateIndex + 6, 12);
    var returnSerial = returnSerial.substr(0, iDateIndex) + " "
    + year + ":" + month + ":" + date + " " + lastChunk;
    if (lastChunk.length < 12) {
    returnSerial = "";
    }

    break;
    case "Panasonic":
    var year = String.fromCharCode(serial[3])
    + String.fromCharCode(serial[4]);
    var month = String.fromCharCode(serial[5])
    + String.fromCharCode(serial[6]);
    var date = String.fromCharCode(serial[7])
    + String.fromCharCode(serial[8]);

    var iYear = parseInt(year, 10);
    var iMonth = parseInt(month, 10);
    var iDate = parseInt(date, 10);

    returnSerial = "";

    if (isNaN(iYear) || isNaN(iMonth) || isNaN(iDate) || iYear < 0
    || iYear > 99 || iMonth < 1 || iMonth > 12 || iDate < 1
    || iDate > 31) {
    // error
    } else {
    returnSerial = "(" + String.fromCharCode(serial[0])
    + String.fromCharCode(serial[1])
    + String.fromCharCode(serial[2]) + ")";
    returnSerial += " 20" + year; // year
    returnSerial += ":" + month; // month
    returnSerial += ":" + date; // date
    returnSerial += " no. " + String.fromCharCode(serial[9])
    + String.fromCharCode(serial[10])
    + String.fromCharCode(serial[11])
    + String.fromCharCode(serial[12]); // id
    }
    break;
    case "Pentax":
    if (returnSerial.length != 7) {
    returnSerial = "";
    }
    break;

    }

    return returnSerial.trim();
    },
    Thresholding : {
    1: 'No dithering or halftoning',
    2: 'Ordered dither or halftone',
    @@ -1456,7 +1558,7 @@ ExifImage.TAGS = {
    2: 'Auto bracket',
    3: 'Samsung EX/NX specific'
    },
    /* TODO DNG
    /* TODO DNG - own namespace ? - needs fix - DNG reader for headers
    0xc612: {
    Name: 'DNGVersion',
    Notes: 'tags 0xc612-0xc7b5 are used in DNG images unless otherwise noted',
  3. redaktor revised this gist May 7, 2014. No changes.
  4. redaktor revised this gist May 7, 2014. No changes.
  5. redaktor revised this gist May 7, 2014. 1 changed file with 2 additions and 18 deletions.
    20 changes: 2 additions & 18 deletions EXIF node-exif
    Original file line number Diff line number Diff line change
    @@ -7,30 +7,14 @@ var fs = require('fs'),
    # This allows conditional replacement with 'exiftool -TAG-= -TAG=VALUE'.
    # - also removed are any other trailing whitespace characters
    ---
    SubIFD
    SubIFD for RAWs
    ---
    ThumbnailOffset
    ---
    ISO
    0x8827: {
    Name: 'ISO',
    Notes: q{
    called ISOSpeedRatings by EXIF 2.2, then PhotographicSensitivity by the EXIF
    2.3 spec.
    },
    PrintConv: '$val=~s/\s+/, /g; $val',
    },
    and
    0x8833
    ISO 0x8827 / 0x8833 - is it a difference?
    ---
    # handle maker notes as a conditional list
    0x927c: \@Image::ExifTool::MakerNotes::Main,
    0x9286: {
    Name: 'UserComment',
    # I have seen other applications write it incorrectly as 'string' or 'int8u' #ph
    Format: 'undef',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    },

    0xa432: { #24
    Name: 'LensInfo',
  6. redaktor revised this gist May 6, 2014. 1 changed file with 65 additions and 56 deletions.
    121 changes: 65 additions & 56 deletions EXIF node-exif
    Original file line number Diff line number Diff line change
    @@ -277,50 +277,62 @@ ExifImage.prototype.extractExifData = function (data, start, length) {
    // 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

    // check explicitly for the getString method in case somehow this isn't
    // a buffer. Found this in an image in the wild
    var makerNoteValue = this.exifData.exif[ExifImage.TAGS.exif[0x927C]];

    // check explicitly for the getString method in case somehow this isn't
    // a buffer. Found this in an image in the wild
    var makerNoteValue = this.exifData.exif[ExifImage.TAGS.exif[0x927C]];
    var delMakerNoteBuffer = false;
    if (typeof makerNoteValue != 'undefined') {

    if (typeof makerNoteValue.getString == 'undefined' && typeof makerNoteValue.length != 'undefined') {
    // assume we can convert to buffer (we can do arrays and strings)
    makerNoteValue = new Buffer(makerNoteValue);
    }

    if (typeof makerNoteValue.getString != 'undefined') {
    // Check the header to see what kind of Makernote we are dealing with
    if (makerNoteValue.getString(0, 7) === 'OLYMP\x00\x01' || makerNoteValue.getString(0, 7) === 'OLYMP\x00\x02') {
    this.extractMakernotes = require('./makernotes/olympus').extractMakernotes;
    } else if (makerNoteValue.getString(0, 7) === 'AGFA \x00\x01') {
    this.extractMakernotes = require('./makernotes/agfa').extractMakernotes;
    } else if (makerNoteValue.getString(0, 8) === 'EPSON\x00\x01\x00') {
    this.extractMakernotes = require('./makernotes/epson').extractMakernotes;
    } else if (makerNoteValue.getString(0, 8) === 'FUJIFILM') {
    this.extractMakernotes = require('./makernotes/fujifilm').extractMakernotes;
    } else if (makerNoteValue.getString(0, 5) === 'SANYO') {
    this.extractMakernotes = require('./makernotes/sanyo').extractMakernotes;
    } else if (makerNoteValue.getString(0, 5) === 'Nikon') {
    this.extractMakernotes = require('./makernotes/nikon').extractMakernotes;
    } else if (makerNoteValue.getString(0, 4) === '%\u0000\u0001\u0000') {
    this.extractMakernotes = require('./makernotes/canon').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'] = makerNoteValue.getString(0, 5).concat('...: Unable to extract Makernote information as it is in an unsupported or unrecognized format.');

    if (typeof makerNoteValue.getString == 'undefined' && typeof makerNoteValue.length != 'undefined') {
    // assume we can convert to buffer (we can do arrays and strings)
    makerNoteValue = new Buffer(makerNoteValue);
    }

    if (typeof this.exifData.makernote['error'] == 'undefined') {
    this.exifData.makernote = this.extractMakernotes(data, self.makernoteOffset, tiffOffset);
    }
    }
    }

    return this.exifData;

    if (typeof makerNoteValue.getString != 'undefined') {
    // Check the header to see what kind of Makernote we are dealing with
    if (makerNoteValue.getString(0, 7) === 'OLYMP\x00\x01' || makerNoteValue.getString(0, 7) === 'OLYMP\x00\x02') {
    this.extractMakernotes = require('./makernotes/olympus').extractMakernotes;
    } else if (makerNoteValue.getString(0, 7) === 'AGFA \x00\x01') {
    this.extractMakernotes = require('./makernotes/agfa').extractMakernotes;
    } else if (makerNoteValue.getString(0, 8) === 'EPSON\x00\x01\x00') {
    this.extractMakernotes = require('./makernotes/epson').extractMakernotes;
    } else if (makerNoteValue.getString(0, 8) === 'FUJIFILM') {
    this.extractMakernotes = require('./makernotes/fujifilm').extractMakernotes;
    } else if (makerNoteValue.getString(0, 5) === 'SANYO') {
    this.extractMakernotes = require('./makernotes/sanyo').extractMakernotes;
    } else if (makerNoteValue.getString(0, 5) === 'Nikon') {
    this.extractMakernotes = require('./makernotes/nikon').extractMakernotes;
    } else if (makerNoteValue.getString(0, 4) === '%\u0000\u0001\u0000') {
    this.extractMakernotes = require('./makernotes/canon').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'] = makerNoteValue.getString(0, 5).concat('...: Unable to extract Makernote information as it is in an unsupported or unrecognized format.');
    }

    if (typeof this.exifData.makernote['error'] == 'undefined') {
    this.exifData.makernote = this.extractMakernotes(data, self.makernoteOffset, tiffOffset);
    delMakerNoteBuffer = true;
    }
    }
    }
    // TODO - lang.exists
    if(delMakerNoteBuffer === true && 'MakerNote' in this.exifData.exif && Buffer.isBuffer(this.exifData.exif.MakerNote)) delete this.exifData.exif.MakerNote;
    return this.exifData;

    };

    ExifImage.prototype.tidyString = function(str) {
    if (typeof str === 'undefined') str = '';
    str = str + '';
    str = str.replace(/[^a-z0-9 \-\/\.\(\)\:\;\,\©\@\\]/gi, '');
    str = str.replace(/^\s+|\s+$/g, ''); // trim
    if (str.toLowerCase() == 'undefined' || str.toLowerCase() == 'unknown') str = '';
    return str.trim();
    }

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


    @@ -337,15 +349,6 @@ ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset,
    value : []
    }

    var tidyString = function(str) {
    if (typeof str === 'undefined') str = '';
    str = str + '';
    str = str.replace(/[^a-z0-9 \-\/\.\(\)\:\;\,\©\@\\]/gi, '');
    str = str.replace(/^\s+|\s+$/g, ''); // trim
    if (str.toLowerCase() == 'undefined' || str.toLowerCase() == 'unknown') str = '';
    return str.trim();
    }

    entry.tagId = entry.tag.getShort(0, isBigEndian);

    // The tagId may correspond to more then one tagName so check which
    @@ -379,7 +382,7 @@ ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset,
    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 = tidyString(entry.value.substring(0, entry.value.length - 1));
    entry.value = this.tidyString(entry.value.substring(0, entry.value.length - 1));
    break;

    case 0x0003: // unsigned short, 2 byte per component
    @@ -440,7 +443,7 @@ ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset,
    // If the value array has only one element we don't need an array
    if (entry.value.length == 1) entry.value = entry.value[0];

    // Is there a string match - todo lang.exists
    // Is there a string match
    if (entry.tagName in ExifImage.TAGS.ref){
    var ref = ExifImage.TAGS.ref[entry.tagName];
    if (typeof ref === 'function'){
    @@ -969,20 +972,22 @@ ExifImage.TAGS = {
    },
    decToFrac: function(d){
    if(typeof d == 'number'){
    var pref = (d>0) ? '+' : '-';
    var df = 1, top = 1, bot = 1;
    var limit = 1e5;

    while (df != d && limit-- > 0) {
    if (df < d) {
    var _d = Math.abs(d);
    while (df != _d && limit-- > 0) {
    if (df < _d) {
    top += 1;
    }
    else {
    bot += 1;
    top = parseInt(d * bot, 10);
    top = parseInt(_d * bot, 10);
    }
    df = top / bot;
    }
    return {description:top.concat('/', bot, ' EV'), value:d};
    _d = null;
    return {description:pref+top.toString().concat('/', bot, ' EV'), value:d};
    }
    return d;
    },
    @@ -1059,7 +1064,7 @@ ExifImage.TAGS = {
    1: 'None',
    2: 'Horizontal differencing'
    },
    /* TODO ?
    /*
    WhitePoint',
    Groups: { 2: 'Camera' }
    */
    @@ -1225,6 +1230,10 @@ ExifImage.TAGS = {
    7: 'Trilinear',
    8: 'Color sequential linear',
    },
    UserComment : function(data){
    if(Buffer.isBuffer(data)) data = data.toString('utf8');
    return ExifImage.prototype.tidyString(data);
    },
    ColorSpace : {
    1 : 'sRGB'
    },
  7. redaktor revised this gist May 6, 2014. No changes.
  8. redaktor revised this gist May 6, 2014. 1 changed file with 65 additions and 74 deletions.
    139 changes: 65 additions & 74 deletions EXIF node-exif
    Original file line number Diff line number Diff line change
    @@ -6,11 +6,11 @@ var fs = require('fs'),
    # may be 'unknown' (filled with spaces) according to the EXIF spec.
    # This allows conditional replacement with 'exiftool -TAG-= -TAG=VALUE'.
    # - also removed are any other trailing whitespace characters

    ---
    SubIFD

    ---
    ThumbnailOffset

    ---
    ISO
    0x8827: {
    Name: 'ISO',
    @@ -22,6 +22,25 @@ var fs = require('fs'),
    },
    and
    0x8833
    ---
    # handle maker notes as a conditional list
    0x927c: \@Image::ExifTool::MakerNotes::Main,
    0x9286: {
    Name: 'UserComment',
    # I have seen other applications write it incorrectly as 'string' or 'int8u' #ph
    Format: 'undef',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    },

    0xa432: { #24
    Name: 'LensInfo',
    Notes: q{
    4 rational values giving focal and aperture ranges, called LensSpecification
    by the EXIF spec.
    },
    # convert to the form "12-20mm f/3.8-4.5" or "50mm f/1.4"
    PrintConv: \&Image::ExifTool::Exif::PrintLensInfo,
    },
    */

    /**
    @@ -948,13 +967,40 @@ ExifImage.TAGS = {
    }
    return [deg, m, s];
    },
    versions : function(data){
    var v = data.toString('utf8').trim().replace(/\0+$/, '');
    console.log( v );
    data = (Buffer.isBuffer(data)) ? data.toJSON().join('.') : data.join('.');
    return data.replace(/\0+$/, '');
    decToFrac: function(d){
    if(typeof d == 'number'){
    var df = 1, top = 1, bot = 1;
    var limit = 1e5;

    while (df != d && limit-- > 0) {
    if (df < d) {
    top += 1;
    }
    else {
    bot += 1;
    top = parseInt(d * bot, 10);
    }
    df = top / bot;
    }
    return {description:top.concat('/', bot, ' EV'), value:d};
    }
    return d;
    },
    versions : function(data){
    var vStr = data.toString('utf8').trim().replace(/^0/, '').replace(/\0+$/, '');
    var v = parseInt(vStr);
    return (typeof v === 'number' && v!=0) ? (v/100).toString() : vStr;
    },
    expoTime : function(_t){
    if (typeof _t === 'number' && _t < 0.25001 && _t > 0) {
    return '1/'.concat(Math.floor(0.5 + 1/_t));
    }
    return (typeof _t === 'number') ? _t.toFixed(1).replace(/\.0$/, '') : _t.replace(/\.0$/, '');
    },
    aperture : function(data){
    var v = Math.pow(2, (data / 2));
    return (typeof v == 'number') ? {description:v, value:data} : data;
    },

    /* helper end */

    ExifVersion : function(data){ return ExifImage.TAGS.ref.versions(data); },
    @@ -1145,66 +1191,16 @@ ExifImage.TAGS = {
    }
    return data
    },
    /* TODO
    0x9201: {
    Name: 'ShutterSpeedValue',
    Notes: 'displayed in seconds, but stored as an APEX value',
    Format: 'rational64s', # Leica M8 patch (incorrectly written as rational64u)
    ValueConv: 'abs($val)<100 ? 2**(-$val) : 0',
    PrintConv: 'Image::ExifTool::Exif::PrintExposureTime($val)',
    },
    0x9202: {
    Name: 'ApertureValue',
    Notes: 'displayed as an F number, but stored as an APEX value',
    ValueConv: '2 ** ($val / 2)',
    PrintConv: 'sprintf("%.1f",$val)',
    },
    # Wikipedia: BrightnessValue = Bv = Av + Tv - Sv
    # ExifTool: LightValue = LV = Av + Tv - Sv + 5 (5 is the Sv for ISO 100 in Exif usage)
    0x9203: 'BrightnessValue',
    0x9204: {
    Name: 'ExposureCompensation',
    Format: 'rational64s', # Leica M8 patch (incorrectly written as rational64u)
    Notes: 'called ExposureBiasValue by the EXIF spec.',
    PrintConv: 'Image::ExifTool::Exif::PrintFraction($val)',
    },
    0x9205: {
    Name: 'MaxApertureValue',
    Notes: 'displayed as an F number, but stored as an APEX value',
    Groups: { 2: 'Camera' },
    ValueConv: '2 ** ($val / 2)',
    PrintConv: 'sprintf("%.1f",$val)',
    },
    0x9206: {
    Name: 'SubjectDistance',
    Groups: { 2: 'Camera' },
    PrintConv: '$val =~ /^(inf|undef)$/ ? $val : "${val} m"',
    },
    0x920a: {
    Name: 'FocalLength',
    Groups: { 2: 'Camera' },
    PrintConv: 'sprintf("%.1f mm",$val)',
    },

    # handle maker notes as a conditional list
    0x927c: \@Image::ExifTool::MakerNotes::Main,
    0x9286: {
    Name: 'UserComment',
    # I have seen other applications write it incorrectly as 'string' or 'int8u'
    Format: 'undef',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    },

    0xa432: { #24
    Name: 'LensInfo',
    Notes: q{
    4 rational values giving focal and aperture ranges, called LensSpecification
    by the EXIF spec.
    },
    # convert to the form "12-20mm f/3.8-4.5" or "50mm f/1.4"
    PrintConv: \&Image::ExifTool::Exif::PrintLensInfo,
    ShutterSpeedValue : function(data){ return ExifImage.TAGS.ref.expoTime(data); },
    ApertureValue : function(data){ return ExifImage.TAGS.ref.aperture(data); },
    MaxApertureValue : function(data){ return ExifImage.TAGS.ref.aperture(data); },
    ExposureCompensation : function(data){ return ExifImage.TAGS.ref.decToFrac(data); },
    SubjectDistance : function(data){
    return (data.match(/^(inf|undef)$/)) ? data : {description:data.toString().concat(' m'), value:data};
    },
    FocalLength : function(data){
    return (typeof data !== 'number') ? data : {description:data.toString().concat(' mm'), value:data};
    },
    */
    FocalPlaneResolutionUnit : {
    1: 'None',
    2: 'inches',
    @@ -1336,12 +1332,7 @@ ExifImage.TAGS = {
    2 : 'Close view',
    3 : 'Distant view'
    },
    ExposureTime : function(_t){
    if (typeof _t === 'number' && _t < 0.25001 && _t > 0) {
    return '1/'.concat(Math.floor(0.5 + 1/_t));
    }
    return (typeof _t === 'number') ? _t.toFixed(1).replace(/\.0$/, '') : _t.replace(/\.0$/, '');
    },
    ExposureTime : function(data){ return ExifImage.TAGS.ref.expoTime(data); },
    FileSource : function(data){
    return (Buffer.isBuffer(data) && data.toJSON()[0]===3) ? { description:'Digital Still Camera', value:3 } : (parseInt(data)||data);
    },
  9. redaktor revised this gist May 6, 2014. 1 changed file with 1487 additions and 590 deletions.
    2,077 changes: 1,487 additions & 590 deletions EXIF node-exif
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,28 @@
    var fs = require('fs'),
    util = require('util'),
    BufferExtender = require('./Buffer');
    /* TODO s
    # NOTE: trailing 'blanks' (spaces) are removed from all EXIF tags which
    # may be 'unknown' (filled with spaces) according to the EXIF spec.
    # This allows conditional replacement with 'exiftool -TAG-= -TAG=VALUE'.
    # - also removed are any other trailing whitespace characters

    SubIFD

    ThumbnailOffset

    ISO
    0x8827: {
    Name: 'ISO',
    Notes: q{
    called ISOSpeedRatings by EXIF 2.2, then PhotographicSensitivity by the EXIF
    2.3 spec.
    },
    PrintConv: '$val=~s/\s+/, /g; $val',
    },
    and
    0x8833
    */

    /**
    * Represents an image with Exif information. When instantiating it you have to
    @@ -95,7 +117,7 @@ ExifImage.prototype.processImage = function (data, callback) {
    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()+"."));
    callback(new Error('Invalid marker found at offset '+(--offset)+'. Expected 0xFF but found 0x'+data[offset].toString(16).toUpperCase()+'.'));
    return;
    }

    @@ -135,13 +157,13 @@ ExifImage.prototype.extractExifData = function (data, start, length) {
    } 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()+".");
    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()+".");
    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 **********************************/
    @@ -180,7 +202,7 @@ ExifImage.prototype.extractExifData = function (data, start, length) {

    // 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") {
    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);
    @@ -199,7 +221,7 @@ ExifImage.prototype.extractExifData = function (data, start, length) {
    // Look for a pointer to the GPS IFD in IFD0 and extract information from
    // it if available
    var gpsifdOffset = this.exifData.image[ExifImage.TAGS.exif[0x8825]];
    if (typeof gpsifdOffset != "undefined" && gpsifdOffset > 0) {
    if (typeof gpsifdOffset != 'undefined' && gpsifdOffset > 0) {

    ifdOffset = tiffOffset + this.exifData.image[ExifImage.TAGS.exif[0x8825]];
    numberOfEntries = data.getShort(ifdOffset, this.isBigEndian);
    @@ -217,7 +239,7 @@ ExifImage.prototype.extractExifData = function (data, start, length) {

    // 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") {
    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);
    @@ -240,28 +262,28 @@ ExifImage.prototype.extractExifData = function (data, start, length) {
    // check explicitly for the getString method in case somehow this isn't
    // a buffer. Found this in an image in the wild
    var makerNoteValue = this.exifData.exif[ExifImage.TAGS.exif[0x927C]];
    if (typeof makerNoteValue != "undefined") {
    if (typeof makerNoteValue != 'undefined') {

    if (typeof makerNoteValue.getString == "undefined" && typeof makerNoteValue.length != "undefined") {
    if (typeof makerNoteValue.getString == 'undefined' && typeof makerNoteValue.length != 'undefined') {
    // assume we can convert to buffer (we can do arrays and strings)
    makerNoteValue = new Buffer(makerNoteValue);
    }

    if (typeof makerNoteValue.getString != "undefined") {
    if (typeof makerNoteValue.getString != 'undefined') {
    // Check the header to see what kind of Makernote we are dealing with
    if (makerNoteValue.getString(0, 7) === "OLYMP\x00\x01" || makerNoteValue.getString(0, 7) === "OLYMP\x00\x02") {
    if (makerNoteValue.getString(0, 7) === 'OLYMP\x00\x01' || makerNoteValue.getString(0, 7) === 'OLYMP\x00\x02') {
    this.extractMakernotes = require('./makernotes/olympus').extractMakernotes;
    } else if (makerNoteValue.getString(0, 7) === "AGFA \x00\x01") {
    } else if (makerNoteValue.getString(0, 7) === 'AGFA \x00\x01') {
    this.extractMakernotes = require('./makernotes/agfa').extractMakernotes;
    } else if (makerNoteValue.getString(0, 8) === "EPSON\x00\x01\x00") {
    } else if (makerNoteValue.getString(0, 8) === 'EPSON\x00\x01\x00') {
    this.extractMakernotes = require('./makernotes/epson').extractMakernotes;
    } else if (makerNoteValue.getString(0, 8) === "FUJIFILM") {
    } else if (makerNoteValue.getString(0, 8) === 'FUJIFILM') {
    this.extractMakernotes = require('./makernotes/fujifilm').extractMakernotes;
    } else if (makerNoteValue.getString(0, 5) === "SANYO") {
    } else if (makerNoteValue.getString(0, 5) === 'SANYO') {
    this.extractMakernotes = require('./makernotes/sanyo').extractMakernotes;
    } else if (makerNoteValue.getString(0, 5) === "Nikon") {
    } else if (makerNoteValue.getString(0, 5) === 'Nikon') {
    this.extractMakernotes = require('./makernotes/nikon').extractMakernotes;
    } else if (makerNoteValue.getString(0, 4) === "%\u0000\u0001\u0000") {
    } else if (makerNoteValue.getString(0, 4) === '%\u0000\u0001\u0000') {
    this.extractMakernotes = require('./makernotes/canon').extractMakernotes;
    } else {
    // Makernotes are available but the format is not recognized so
    @@ -270,7 +292,7 @@ ExifImage.prototype.extractExifData = function (data, start, length) {
    this.exifData.makernote['error'] = makerNoteValue.getString(0, 5).concat('...: Unable to extract Makernote information as it is in an unsupported or unrecognized format.');
    }

    if (typeof this.exifData.makernote['error'] == "undefined") {
    if (typeof this.exifData.makernote['error'] == 'undefined') {
    this.exifData.makernote = this.extractMakernotes(data, self.makernoteOffset, tiffOffset);
    }
    }
    @@ -297,18 +319,18 @@ ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset,
    }

    var tidyString = function(str) {
    if (typeof str === "undefined") str = "";
    str = str + "";
    if (typeof str === 'undefined') str = '';
    str = str + '';
    str = str.replace(/[^a-z0-9 \-\/\.\(\)\:\;\,\©\@\\]/gi, '');
    str = str.replace(/^\s+|\s+$/g, ''); // trim
    if (str.toLowerCase() == "undefined" || str.toLowerCase() == "unknown") str = "";
    if (str.toLowerCase() == 'undefined' || str.toLowerCase() == 'unknown') str = '';
    return str.trim();
    }

    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") {
    if (tags && tags[entry.tagId] && typeof tags[entry.tagId] == 'function') {
    if (!(entry.tagName = tags[entry.tagId](entry))) {
    return false;
    }
    @@ -337,7 +359,7 @@ ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset,
    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
    if (entry.value[entry.value.length - 1] === '\u0000') // Trim null terminated strings
    entry.value = tidyString(entry.value.substring(0, entry.value.length - 1));
    break;

    @@ -394,7 +416,7 @@ ExifImage.prototype.extractExifEntry = function (data, entryOffset, tiffOffset,
    }

    // If this is the Makernote tag save its offset for later use
    if (entry.tagName === "MakerNote") self.makernoteOffset = entry.valueOffset;
    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];
    @@ -432,435 +454,437 @@ 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 : "FocalLengthIn35mmFilm",
    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"
    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',
    // TODO 0x9009 - undef[44] written by Google Plus uploader - PH
    0x9009 : 'GooglePlus',
    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 : 'FocalLengthIn35mmFilm',
    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'

    },

    @@ -915,140 +939,402 @@ ExifImage.TAGS = {
    }
    if(typeof deg !== 'number' || typeof m !== 'number' || typeof s !== 'number') return vArr;
    if (lRef === 'S' || lRef === 'N' || lRef === 'E' || lRef === 'W') {
    var lInt = (lRef == "S" || lRef == "W") ? -1 : 1;
    var lInt = (lRef == 'S' || lRef == 'W') ? -1 : 1;
    var v = (deg+(m/60)+(s/3600)) * lInt;
    return {
    description: deg.toString().concat(" deg ", m, "' ", s.toFixed(4), "'' ", lRef),
    description: deg.toString().concat('° ', m, '\' ', s.toFixed(4), '" ', lRef),
    value: (typeof v === 'number') ? v : [deg, m, s]
    };
    }
    return [deg, m, s];
    },

    versions : function(data){
    var v = data.toString('utf8').trim().replace(/\0+$/, '');
    console.log( v );
    data = (Buffer.isBuffer(data)) ? data.toJSON().join('.') : data.join('.');
    return data.replace(/\0+$/, '');
    },

    /* helper end */

    ExifVersion : function(data){ return ExifImage.TAGS.ref.versions(data); },
    InteropVersion : function(data){ return ExifImage.TAGS.ref.versions(data); },
    FlashpixVersion : function(data){ return ExifImage.TAGS.ref.versions(data); },

    ColorSpace : {
    1 : "sRGB"
    OldSubfileType : {
    1: 'Full-resolution image',
    2: 'Reduced-resolution image',
    3: 'Single page of multi-page image'
    },
    /* TODO : ExifImage.js has no file type while redaktor.meta.js has
    'Compression'
    sub IdentifyRawFile($$){
    my ($et, $comp) = @_;
    if ($$et{FILE_TYPE} eq 'TIFF' and not $$et{IdentifiedRawFile}) {
    if ($compression{$comp} and $compression{$comp} =~ /^\w+ ([A-Z]{3}) Compressed$/) {
    $et->OverrideFileType($$et{TIFF_TYPE} = $1);
    $$et{IdentifiedRawFile} = 1;
    }
    }
    }
    */
    Thresholding : {
    1: 'No dithering or halftoning',
    2: 'Ordered dither or halftone',
    3: 'Randomized dither'
    },
    FillOrder : {
    1: 'Normal',
    2: 'Reversed'
    },
    PlanarConfiguration : {
    1: 'Chunky',
    2: 'Planar'
    },
    GrayResponseUnit : {
    1: 0.1,
    2: 0.001,
    3: 0.0001,
    4: 0.00001,
    5: 0.000001
    },
    T4Options : {
    0: '2-Dimensional encoding',
    1: 'Uncompressed',
    2: 'Fill bits added'
    },
    T6Options : { 1: 'Uncompressed' },
    ResolutionUnit : {
    1: 'unknown',
    2: 'inches',
    3: 'cm'
    },
    Predictor : {
    1: 'None',
    2: 'Horizontal differencing'
    },
    /* TODO ?
    WhitePoint',
    Groups: { 2: 'Camera' }
    */
    CleanFaxData : {
    0: 'Clean',
    1: 'Regenerated',
    2: 'Unclean'
    },
    ExtraSamples : {
    0: 'Unspecified',
    1: 'Associated Alpha',
    2: 'Unassociated Alpha'
    },
    Indexed : { 0: 'Not indexed', 1: 'Indexed' },
    OPIProxy : {
    0: 'Higher resolution image does not exist',
    1: 'Higher resolution image exists'
    },
    ProfileType: { 0: 'Unspecified', 1: 'Group 3 FAX' },
    FaxProfile : {
    0: 'Unknown',
    1: 'Minimal B&W lossless, S',
    2: 'Extended B&W lossless, F',
    3: 'Lossless JBIG B&W, J',
    4: 'Lossy color and grayscale, C',
    5: 'Lossless color and grayscale, L',
    6: 'Mixed raster content, M',
    7: 'Profile T',
    255: 'Multi Profiles'
    },
    CodingMethods : {
    0: 'Unspecified compression',
    1: 'Modified Huffman',
    2: 'Modified Read',
    3: 'Modified MR',
    4: 'JBIG',
    5: 'Baseline JPEG',
    6: 'JBIG color'
    },
    JPEGProc : {
    1: 'Baseline',
    14: 'Lossless'
    },
    Copyright : function(data){
    data = data.replace(/ *\0/, String.fromCharCode(10));
    data = data.replace(/ *\0[\s\S]*/, '');
    return data.replace(/\n$/, '');
    },
    ModelTiePoint : { 2: 'Location' },
    RasterPadding : {
    0: 'Byte',
    1: 'Word',
    2: 'Long Word',
    9: 'Sector',
    10: 'Long Sector'
    },
    ImageColorIndicator : {
    0: 'Unspecified Image Color',
    1: 'Specified Image Color'
    },
    BackgroundColorIndicator : {
    0: 'Unspecified Background Color',
    1: 'Specified Background Color'
    },
    HCUsage : {
    0: 'CT',
    1: 'Line Art',
    2: 'Trap'
    },
    TIFF_FXExtensions : {
    /* BITMASK */
    0: 'Resolution/Image Width',
    1: 'N Layer Profile M',
    2: 'Shared Data',
    3: 'B&W JBIG2',
    4: 'JBIG2 Profile M'
    },
    MultiProfiles : {
    0: 'Profile S',
    1: 'Profile F',
    2: 'Profile J',
    3: 'Profile C',
    4: 'Profile L',
    5: 'Profile M',
    6: 'Profile T',
    7: 'Resolution/Image Width',
    8: 'N Layer Profile M',
    9: 'Shared Data',
    10: 'JBIG2 Profile M'
    },
    /* TODO
    'GeoTiffDirectory', 'GeoTiffDoubleParams',
    RawConv: '$val . GetByteOrder()', # save byte order
    */
    ExposureProgram : {
    0 : "Not defined",
    1 : "Manual",
    2 : "Normal program",
    3 : "Aperture priority",
    4 : "Shutter priority",
    5 : "Creative program",
    6 : "Action program",
    7 : "Portrait mode",
    8 : "Landscape mode"
    0: 'Not Defined',
    1: 'Manual',
    2: 'Program AE',
    3: 'Aperture-priority AE',
    4: 'Shutter speed priority AE',
    5: 'Creative (Slow speed)',
    6: 'Action (High speed)',
    7: 'Portrait',
    8: 'Landscape',
    9: 'Bulb'
    },
    SpectralSensitivity : { 2: 'Camera' },
    SensitivityType : {
    0: 'Unknown',
    1: 'Standard Output Sensitivity',
    2: 'Recommended Exposure Index',
    3: 'ISO Speed',
    4: 'Standard Output Sensitivity and Recommended Exposure Index',
    5: 'Standard Output Sensitivity and ISO Speed',
    6: 'Recommended Exposure Index and ISO Speed',
    7: 'Standard Output Sensitivity, Recommended Exposure Index and ISO Speed',
    },
    ComponentsConfiguration : function(data){
    if (Buffer.isBuffer(data)) data = data.toJSON();
    var c = ['', 'Y', 'Cb', 'Cr', 'R', 'G', 'B'];
    var cStr = '';
    if(data instanceof Array){
    if(data.join().trim() == '4,5,6,0') return {description:'RGB uncompressed', value:data};
    if(data.join().trim() == '1,2,3,0') return {description:'Y, Cb, Cr', value:data};
    data.forEach(function(index){
    cStr.concat(c[index]);
    });
    return {description:cStr, value:data};
    }
    return data
    },
    /* TODO
    0x9201: {
    Name: 'ShutterSpeedValue',
    Notes: 'displayed in seconds, but stored as an APEX value',
    Format: 'rational64s', # Leica M8 patch (incorrectly written as rational64u)
    ValueConv: 'abs($val)<100 ? 2**(-$val) : 0',
    PrintConv: 'Image::ExifTool::Exif::PrintExposureTime($val)',
    },
    0x9202: {
    Name: 'ApertureValue',
    Notes: 'displayed as an F number, but stored as an APEX value',
    ValueConv: '2 ** ($val / 2)',
    PrintConv: 'sprintf("%.1f",$val)',
    },
    # Wikipedia: BrightnessValue = Bv = Av + Tv - Sv
    # ExifTool: LightValue = LV = Av + Tv - Sv + 5 (5 is the Sv for ISO 100 in Exif usage)
    0x9203: 'BrightnessValue',
    0x9204: {
    Name: 'ExposureCompensation',
    Format: 'rational64s', # Leica M8 patch (incorrectly written as rational64u)
    Notes: 'called ExposureBiasValue by the EXIF spec.',
    PrintConv: 'Image::ExifTool::Exif::PrintFraction($val)',
    },
    0x9205: {
    Name: 'MaxApertureValue',
    Notes: 'displayed as an F number, but stored as an APEX value',
    Groups: { 2: 'Camera' },
    ValueConv: '2 ** ($val / 2)',
    PrintConv: 'sprintf("%.1f",$val)',
    },
    0x9206: {
    Name: 'SubjectDistance',
    Groups: { 2: 'Camera' },
    PrintConv: '$val =~ /^(inf|undef)$/ ? $val : "${val} m"',
    },
    0x920a: {
    Name: 'FocalLength',
    Groups: { 2: 'Camera' },
    PrintConv: 'sprintf("%.1f mm",$val)',
    },

    # handle maker notes as a conditional list
    0x927c: \@Image::ExifTool::MakerNotes::Main,
    0x9286: {
    Name: 'UserComment',
    # I have seen other applications write it incorrectly as 'string' or 'int8u'
    Format: 'undef',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    },

    0xa432: { #24
    Name: 'LensInfo',
    Notes: q{
    4 rational values giving focal and aperture ranges, called LensSpecification
    by the EXIF spec.
    },
    # convert to the form "12-20mm f/3.8-4.5" or "50mm f/1.4"
    PrintConv: \&Image::ExifTool::Exif::PrintLensInfo,
    },
    */
    FocalPlaneResolutionUnit : {
    1: 'None',
    2: 'inches',
    3: 'cm',
    4: 'mm',
    5: 'um',
    },
    SecurityClassification : {
    T: 'Top Secret',
    S: 'Secret',
    C: 'Confidential',
    R: 'Restricted',
    U: 'Unclassified',
    },
    SensingMethod : {
    1: 'Monochrome area',
    2: 'One-chip color area',
    3: 'Two-chip color area',
    4: 'Three-chip color area',
    5: 'Color sequential area',
    6: 'Monochrome linear',
    7: 'Trilinear',
    8: 'Color sequential linear',
    },
    ColorSpace : {
    1 : 'sRGB'
    },
    MeteringMode : {
    0 : "Unknown",
    1 : "Average",
    2 : "CenterWeightedAverage",
    3 : "Spot",
    4 : "MultiSpot",
    5 : "Pattern",
    6 : "Partial",
    255 : "Other"
    0 : 'Unknown',
    1 : 'Average',
    2 : 'CenterWeightedAverage',
    3 : 'Spot',
    4 : 'MultiSpot',
    5 : 'Pattern',
    6 : 'Partial',
    255 : 'Other'
    },
    LightSource : {
    0 : "Unknown",
    1 : "Daylight",
    2 : "Fluorescent",
    3 : "Tungsten (incandescent light)",
    4 : "Flash",
    9 : "Fine weather",
    10 : "Cloudy weather",
    11 : "Shade",
    12 : "Daylight fluorescent (D 5700 - 7100K)",
    13 : "Day white fluorescent (N 4600 - 5400K)",
    14 : "Cool white fluorescent (W 3900 - 4500K)",
    15 : "White fluorescent (WW 3200 - 3700K)",
    17 : "Standard light A",
    18 : "Standard light B",
    19 : "Standard light C",
    20 : "D55",
    21 : "D65",
    22 : "D75",
    23 : "D50",
    24 : "ISO studio tungsten",
    255 : "Other"
    0 : 'Unknown',
    1 : 'Daylight',
    2 : 'Fluorescent',
    3 : 'Tungsten (incandescent light)',
    4 : 'Flash',
    9 : 'Fine weather',
    10 : 'Cloudy weather',
    11 : 'Shade',
    12 : 'Daylight fluorescent (D 5700 - 7100K)',
    13 : 'Day white fluorescent (N 4600 - 5400K)',
    14 : 'Cool white fluorescent (W 3900 - 4500K)',
    15 : 'White fluorescent (WW 3200 - 3700K)',
    17 : 'Standard light A',
    18 : 'Standard light B',
    19 : 'Standard light C',
    20 : 'D55',
    21 : 'D65',
    22 : 'D75',
    23 : 'D50',
    24 : 'ISO studio tungsten',
    255 : 'Other'
    },
    Flash : {
    0x0000 : "No Flash",
    0x0001 : "Flash fired",
    0x0005 : "Strobe return light not detected",
    0x0007 : "Strobe return light detected",
    0x0009 : "Flash fired, compulsory flash mode",
    0x000D : "Flash fired, compulsory flash mode, return light not detected",
    0x000F : "Flash fired, compulsory flash mode, return light detected",
    0x0010 : "Flash did not fire, compulsory flash mode",
    0x0018 : "Flash did not fire, auto mode",
    0x0019 : "Flash fired, auto mode",
    0x001D : "Flash fired, auto mode, return light not detected",
    0x001F : "Flash fired, auto mode, return light detected",
    0x0020 : "No flash function",
    0x0041 : "Flash fired, red-eye reduction mode",
    0x0045 : "Flash fired, red-eye reduction mode, return light not detected",
    0x0047 : "Flash fired, red-eye reduction mode, return light detected",
    0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode",
    0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",
    0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",
    0x0059 : "Flash fired, auto mode, red-eye reduction mode",
    0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode",
    0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode"
    0x0000 : 'No Flash',
    0x0001 : 'Flash fired',
    0x0005 : 'Strobe return light not detected',
    0x0007 : 'Strobe return light detected',
    0x0009 : 'Flash fired, compulsory flash mode',
    0x000D : 'Flash fired, compulsory flash mode, return light not detected',
    0x000F : 'Flash fired, compulsory flash mode, return light detected',
    0x0010 : 'Flash did not fire, compulsory flash mode',
    0x0018 : 'Flash did not fire, auto mode',
    0x0019 : 'Flash fired, auto mode',
    0x001D : 'Flash fired, auto mode, return light not detected',
    0x001F : 'Flash fired, auto mode, return light detected',
    0x0020 : 'No flash function',
    0x0041 : 'Flash fired, red-eye reduction mode',
    0x0045 : 'Flash fired, red-eye reduction mode, return light not detected',
    0x0047 : 'Flash fired, red-eye reduction mode, return light detected',
    0x0049 : 'Flash fired, compulsory flash mode, red-eye reduction mode',
    0x004D : 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
    0x004F : 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
    0x0059 : 'Flash fired, auto mode, red-eye reduction mode',
    0x005D : 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
    0x005F : 'Flash fired, auto mode, return light detected, red-eye reduction mode'
    },
    SensingMethod : {
    1 : "Not defined",
    2 : "One-chip color area sensor",
    3 : "Two-chip color area sensor",
    4 : "Three-chip color area sensor",
    5 : "Color sequential area sensor",
    7 : "Trilinear sensor",
    8 : "Color sequential linear sensor"
    1 : 'Not defined',
    2 : 'One-chip color area sensor',
    3 : 'Two-chip color area sensor',
    4 : 'Three-chip color area sensor',
    5 : 'Color sequential area sensor',
    7 : 'Trilinear sensor',
    8 : 'Color sequential linear sensor'
    },
    SceneCaptureType : {
    0 : "Standard",
    1 : "Landscape",
    2 : "Portrait",
    3 : "Night scene"
    },
    CustomRendered : {
    0 : "Normal",
    1 : "Custom"
    0 : 'Standard',
    1 : 'Landscape',
    2 : 'Portrait',
    3 : 'Night scene'
    },
    WhiteBalance : {
    0 : "Auto",
    1 : "Manual"
    0 : 'Auto',
    1 : 'Manual'
    },
    GainControl : {
    0 : "None",
    1 : "Low gain up",
    2 : "High gain up",
    3 : "Low gain down",
    4 : "High gain down"
    0 : 'None',
    1 : 'Low gain up',
    2 : 'High gain up',
    3 : 'Low gain down',
    4 : 'High gain down'
    },
    Contrast : {
    0 : "Normal",
    1 : "Soft",
    2 : "Hard"
    0 : 'Normal',
    1 : 'Soft',
    2 : 'Hard'
    },
    Saturation : {
    0 : "Normal",
    1 : "Low saturation",
    2 : "High saturation"
    0 : 'Normal',
    1 : 'Low saturation',
    2 : 'High saturation'
    },
    Sharpness : {
    0 : "Normal",
    1 : "Soft",
    2 : "Hard"
    0 : 'Normal',
    1 : 'Soft',
    2 : 'Hard'
    },
    SubjectDistanceRange : {
    0 : "Unknown",
    1 : "Macro",
    2 : "Close view",
    3 : "Distant view"
    0 : 'Unknown',
    1 : 'Macro',
    2 : 'Close view',
    3 : 'Distant view'
    },
    ExposureTime : function(_t){
    if (typeof _t === 'number' && _t < 0.25001 && _t > 0) {
    @@ -1057,11 +1343,97 @@ ExifImage.TAGS = {
    return (typeof _t === 'number') ? _t.toFixed(1).replace(/\.0$/, '') : _t.replace(/\.0$/, '');
    },
    FileSource : function(data){
    return (Buffer.isBuffer(data) && data.toJSON()[0]===3) ? { description:"Digital Still Camera", value:3 } : (parseInt(data)||data);
    return (Buffer.isBuffer(data) && data.toJSON()[0]===3) ? { description:'Digital Still Camera', value:3 } : (parseInt(data)||data);
    },
    SceneType : function(data){
    return (Buffer.isBuffer(data) && data.toJSON()[0]===1) ? { description:"Directly photographed", value:1 } : (parseInt(data)||data);
    return (Buffer.isBuffer(data) && data.toJSON()[0]===1) ? { description:'Directly photographed', value:1 } : (parseInt(data)||data);
    },
    PixelFormat : {
    0x0d: '24-bit RGB',
    0x0c: '24-bit BGR',
    0x0e: '32-bit BGR',
    0x15: '48-bit RGB',
    0x12: '48-bit RGB Fixed Point',
    0x3b: '48-bit RGB Half',
    0x18: '96-bit RGB Fixed Point',
    0x1b: '128-bit RGB Float',
    0x0f: '32-bit BGRA',
    0x16: '64-bit RGBA',
    0x1d: '64-bit RGBA Fixed Point',
    0x3a: '64-bit RGBA Half',
    0x1e: '128-bit RGBA Fixed Point',
    0x19: '128-bit RGBA Float',
    0x10: '32-bit PBGRA',
    0x17: '64-bit PRGBA',
    0x1a: '128-bit PRGBA Float',
    0x1c: '32-bit CMYK',
    0x2c: '40-bit CMYK Alpha',
    0x1f: '64-bit CMYK',
    0x2d: '80-bit CMYK Alpha',
    0x20: '24-bit 3 Channels',
    0x21: '32-bit 4 Channels',
    0x22: '40-bit 5 Channels',
    0x23: '48-bit 6 Channels',
    0x24: '56-bit 7 Channels',
    0x25: '64-bit 8 Channels',
    0x2e: '32-bit 3 Channels Alpha',
    0x2f: '40-bit 4 Channels Alpha',
    0x30: '48-bit 5 Channels Alpha',
    0x31: '56-bit 6 Channels Alpha',
    0x32: '64-bit 7 Channels Alpha',
    0x33: '72-bit 8 Channels Alpha',
    0x26: '48-bit 3 Channels',
    0x27: '64-bit 4 Channels',
    0x28: '80-bit 5 Channels',
    0x29: '96-bit 6 Channels',
    0x2a: '112-bit 7 Channels',
    0x2b: '128-bit 8 Channels',
    0x34: '64-bit 3 Channels Alpha',
    0x35: '80-bit 4 Channels Alpha',
    0x36: '96-bit 5 Channels Alpha',
    0x37: '112-bit 6 Channels Alpha',
    0x38: '128-bit 7 Channels Alpha',
    0x39: '144-bit 8 Channels Alpha',
    0x08: '8-bit Gray',
    0x0b: '16-bit Gray',
    0x13: '16-bit Gray Fixed Point',
    0x3e: '16-bit Gray Half',
    0x3f: '32-bit Gray Fixed Point',
    0x11: '32-bit Gray Float',
    0x05: 'Black & White',
    0x09: '16-bit BGR555',
    0x0a: '16-bit BGR565',
    0x13: '32-bit BGR101010',
    0x3d: '32-bit RGBE',
    },
    Transformation : {
    0: 'Horizontal (normal)',
    1: 'Mirror vertical',
    2: 'Mirror horizontal',
    3: 'Rotate 180',
    4: 'Rotate 90 CW',
    5: 'Mirror horizontal and rotate 90 CW',
    6: 'Mirror horizontal and rotate 270 CW',
    7: 'Rotate 270 CW',
    },
    Uncompressed : { 0: 'No', 1: 'Yes' },
    ImageDataDiscard : {
    0: 'Full Resolution',
    1: 'Flexbits Discarded',
    2: 'HighPass Frequency Data Discarded',
    3: 'Highpass and LowPass Frequency Data Discarded',
    },
    AlphaDataDiscard : {
    0: 'Full Resolution',
    1: 'Flexbits Discarded',
    2: 'HighPass Frequency Data Discarded',
    3: 'Highpass and LowPass Frequency Data Discarded',
    },
    USPTOOriginalContentType : {
    0: 'Text or Drawing',
    1: 'Grayscale',
    2: 'Color',
    },
    CFAPattern : function(data){
    /* The value consists of:
    - Two short, being the grid width and height of the repeated pattern.
    @@ -1088,11 +1460,408 @@ ExifImage.TAGS = {
    console.log( rtn );
    return (rtn.indexOf('0')==-1) ? rtn.slice(0,-1) : arr;
    },
    ComponentsConfiguration : function(data){
    var c = ["-", "Y", "Cb", "Cr", "R", "G", "B"];
    /* TODO */
    return data.toJSON();
    CustomRendered : {
    0 : 'Normal',
    1 : 'Custom',
    4 : 'Apple iPhone5c horizontal orientation',
    6 : 'Apple iPhone5c panorama'
    },
    ExposureMode : {
    0: 'Auto',
    1: 'Manual',
    2: 'Auto bracket',
    3: 'Samsung EX/NX specific'
    },
    /* TODO DNG
    0xc612: {
    Name: 'DNGVersion',
    Notes: 'tags 0xc612-0xc7b5 are used in DNG images unless otherwise noted',
    DataMember: 'DNGVersion',
    RawConv: '$$self{DNGVersion} = $val',
    PrintConv: '$val =~ tr/ /./; $val',
    },
    0xc613: {
    Name: 'DNGBackwardVersion',
    PrintConv: '$val =~ tr/ /./; $val',
    },
    0xc614: 'UniqueCameraModel',
    0xc615: {
    Name: 'LocalizedCameraModel',
    Format: 'string',
    PrintConv: '$self->Printable($val, 0)',
    },
    0xc616: {
    Name: 'CFAPlaneColor',
    PrintConv: q{
    my @cols = qw(Red Green Blue Cyan Magenta Yellow White);
    my @vals = map { $cols[$_] || "Unknown($_)" } split(' ', $val);
    return join(',', @vals);
    },
    },
    0xc617: {
    Name: 'CFALayout : {1: 'Rectangular',
    2: 'Even columns offset down 1/2 row',
    3: 'Even columns offset up 1/2 row',
    4: 'Even rows offset right 1/2 column',
    5: 'Even rows offset left 1/2 column',
    # the following are new for DNG 1.3:
    6: 'Even rows offset up by 1/2 row, even columns offset left by 1/2 column',
    7: 'Even rows offset up by 1/2 row, even columns offset right by 1/2 column',
    8: 'Even rows offset down by 1/2 row, even columns offset left by 1/2 column',
    9: 'Even rows offset down by 1/2 row, even columns offset right by 1/2 column',
    },
    },
    0xc634: [
    {
    Condition: '$$self{TIFF_TYPE} =~ /^(ARW|SR2)$/',
    Name: 'SR2Private',
    Groups: { 1: 'SR2' },
    Flags: 'SubIFD',
    Format: 'int32u',
    # some utilites have problems unless this is int8u format:
    # - Adobe Camera Raw 5.3 gives an error
    # - Apple Preview 10.5.8 gets the wrong white balance
    FixFormat: 'int8u', # (stupid Sony)
    SubDirectory: {
    DirName: 'SR2Private',
    TagTable: 'Image::ExifTool::Sony::SR2Private',
    Start: '$val',
    },
    },
    {
    Condition: '$$valPt =~ /^Adobe\0/',
    Name: 'DNGAdobeData',
    Flags: [ 'Binary', 'Protected' ],
    Writable: 'undef', # (writable directory!) (to make it possible to delete this mess)
    WriteGroup: 'IFD0',
    NestedHtmlDump: 1,
    SubDirectory: { TagTable: 'Image::ExifTool::DNG::AdobeData' },
    Format: 'undef', # written incorrectly as int8u (change to undef for speed)
    },
    {
    # Pentax/Samsung models that write AOC maker notes in JPG images:
    # K-5,K-7,K-m,K-x,K-r,K10D,K20D,K100D,K110D,K200D,K2000,GX10,GX20
    # (Note: the following expression also appears in WriteExif.pl)
    Condition: q{
    $$valPt =~ /^(PENTAX |SAMSUNG)\0/ and
    $$self{Model} =~ /\b(K(-[57mrx]|(10|20|100|110|200)D|2000)|GX(10|20))\b/
    },
    Name: 'MakerNotePentax',
    MakerNotes: 1, # (causes "MakerNotes header" to be identified in HtmlDump output)
    Binary: 1,
    # Note: Don't make this block-writable for a few reasons:
    # 1) It would be dangerous (possibly confusing Pentax software)
    # 2) It is a different format from the JPEG version of MakerNotePentax
    # 3) It is converted to JPEG format by RebuildMakerNotes() when copying
    SubDirectory: {
    TagTable: 'Image::ExifTool::Pentax::Main',
    Start: '$valuePtr + 10',
    Base: '$start - 10',
    ByteOrder: 'Unknown', # easier to do this than read byteorder word
    },
    Format: 'undef', # written incorrectly as int8u (change to undef for speed)
    },
    {
    # must duplicate the above tag with a different name for more recent
    # Pentax models which use the "PENTAX" instead of the "AOC" maker notes
    # in JPG images (needed when copying maker notes from DNG to JPG)
    Condition: '$$valPt =~ /^(PENTAX |SAMSUNG)\0/',
    Name: 'MakerNotePentax5',
    MakerNotes: 1,
    Binary: 1,
    SubDirectory: {
    TagTable: 'Image::ExifTool::Pentax::Main',
    Start: '$valuePtr + 10',
    Base: '$start - 10',
    ByteOrder: 'Unknown',
    },
    Format: 'undef',
    },
    {
    Name: 'DNGPrivateData',
    Flags: [ 'Binary', 'Protected' ],
    Format: 'undef',
    Writable: 'undef',
    WriteGroup: 'IFD0',
    },
    ],
    0xc635: {
    Name: 'MakerNoteSafety : {0: 'Unsafe',
    1: 'Safe',
    },
    },
    0xc640: { #15
    Name: 'RawImageSegmentation',
    # (int16u[3], not writable)
    Notes: q{
    used in segmented Canon CR2 images. 3 numbers: 1. Number of segments minus
    one; 2. Pixel width of segments except last; 3. Pixel width of last segment
    },
    },
    0xc65a: {
    Name: 'CalibrationIlluminant1',
    SeparateTable: 'LightSource',
    PrintConv: \%lightSource,
    },
    0xc65b: {
    Name: 'CalibrationIlluminant2',
    SeparateTable: 'LightSource',
    PrintConv: \%lightSource,
    },
    0xc65c: 'BestQualityScale',
    0xc65d: {
    Name: 'RawDataUniqueID',
    Format: 'undef',
    ValueConv: 'uc(unpack("H*",$val))',
    },
    0xc660: { #3
    Name: 'AliasLayerMetadata',
    Notes: 'used by Alias Sketchbook Pro',
    },
    0xc68b: {
    Name: 'OriginalRawFileName',
    Format: 'string', # sometimes written as int8u
    },
    0xc68c: {
    Name: 'OriginalRawFileData', # (writable directory!)
    Writable: 'undef', # must be defined here so tag will be extracted if specified
    WriteGroup: 'IFD0',
    Flags: [ 'Binary', 'Protected' ],
    SubDirectory: {
    TagTable: 'Image::ExifTool::DNG::OriginalRaw',
    },
    },
    0xc68d: 'ActiveArea',
    0xc68e: 'MaskedAreas',
    0xc68f: {
    Name: 'AsShotICCProfile',
    Binary: 1,
    Writable: 'undef', # must be defined here so tag will be extracted if specified
    SubDirectory: {
    DirName: 'AsShotICCProfile',
    TagTable: 'Image::ExifTool::ICC_Profile::Main',
    },
    },
    0xc690: 'AsShotPreProfileMatrix',
    0xc691: {
    Name: 'CurrentICCProfile',
    Binary: 1,
    Writable: 'undef', # must be defined here so tag will be extracted if specified
    SubDirectory: {
    DirName: 'CurrentICCProfile',
    TagTable: 'Image::ExifTool::ICC_Profile::Main',
    },
    },
    0xc692: 'CurrentPreProfileMatrix',
    0xc6bf: 'ColorimetricReference',
    0xc6d2: { #JD (Panasonic DMC-TZ5)
    # this text is UTF-8 encoded (hooray!) - PH (TZ5)
    Name: 'PanasonicTitle',
    Format: 'string', # written incorrectly as 'undef'
    Notes: 'proprietary Panasonic tag used for baby/pet name, etc',
    # panasonic always records this tag (64 zero bytes),
    # so ignore it unless it contains valid information
    RawConv: 'length($val) ? $val : undef',
    ValueConv: '$self->Decode($val, "UTF8")',
    },
    0xc6d3: { #PH (Panasonic DMC-FS7)
    Name: 'PanasonicTitle2',
    Format: 'string', # written incorrectly as 'undef'
    Notes: 'proprietary Panasonic tag used for baby/pet name with age',
    # panasonic always records this tag (128 zero bytes),
    # so ignore it unless it contains valid information
    RawConv: 'length($val) ? $val : undef',
    ValueConv: '$self->Decode($val, "UTF8")',
    },
    0xc6f3: 'CameraCalibrationSig',
    0xc6f4: 'ProfileCalibrationSig',
    0xc6f5: {
    Name: 'ProfileIFD', # (ExtraCameraProfiles)
    Groups: { 1: 'ProfileIFD' },
    Flags: 'SubIFD',
    SubDirectory: {
    ProcessProc: \&ProcessTiffIFD,
    WriteProc: \&ProcessTiffIFD,
    DirName: 'ProfileIFD',
    Start: '$val',
    Base: '$start', # offsets relative to start of TIFF-like header
    MaxSubdirs: 10,
    Magic: 0x4352, # magic number for TIFF-like header
    },
    },
    0xc6f6: 'AsShotProfileName',
    0xc6f7: 'NoiseReductionApplied',
    0xc6f8: 'ProfileName',
    0xc6f9: 'ProfileHueSatMapDims',
    0xc6fa: { Name: 'ProfileHueSatMapData1', %longBin },
    0xc6fb: { Name: 'ProfileHueSatMapData2', %longBin },
    0xc6fc: {
    Name: 'ProfileToneCurve',
    Binary: 1,
    },
    0xc6fd: {
    Name: 'ProfileEmbedPolicy : {0: 'Allow Copying',
    1: 'Embed if Used',
    2: 'Never Embed',
    3: 'No Restrictions',
    },
    },
    0xc6fe: 'ProfileCopyright',
    0xc714: 'ForwardMatrix1',
    0xc715: 'ForwardMatrix2',
    0xc716: 'PreviewApplicationName',
    0xc717: 'PreviewApplicationVersion',
    0xc718: 'PreviewSettingsName',
    0xc719: {
    Name: 'PreviewSettingsDigest',
    Format: 'undef',
    ValueConv: 'unpack("H*", $val)',
    },
    0xc71a: 'PreviewColorSpace',
    0xc71b: {
    Name: 'PreviewDateTime',
    Groups: { 2: 'Time' },
    ValueConv: q{
    require Image::ExifTool::XMP;
    return Image::ExifTool::XMP::ConvertXMPDate($val);
    },
    },
    0xc71c: {
    Name: 'RawImageDigest',
    Format: 'undef',
    ValueConv: 'unpack("H*", $val)',
    },
    0xc71d: {
    Name: 'OriginalRawFileDigest',
    Format: 'undef',
    ValueConv: 'unpack("H*", $val)',
    },
    0xc71e: 'SubTileBlockSize',
    0xc71f: 'RowInterleaveFactor',
    0xc725: 'ProfileLookTableDims',
    0xc726: {
    Name: 'ProfileLookTableData',
    Binary: 1,
    },
    0xc740: { # DNG 1.3
    Name: 'OpcodeList1',
    Binary: 1,
    # opcodes:
    # 1: 'WarpRectilinear',
    # 2: 'WarpFisheye',
    # 3: 'FixVignetteRadial',
    # 4: 'FixBadPixelsConstant',
    # 5: 'FixBadPixelsList',
    # 6: 'TrimBounds',
    # 7: 'MapTable',
    # 8: 'MapPolynomial',
    # 9: 'GainMap',
    # 10: 'DeltaPerRow',
    # 11: 'DeltaPerColumn',
    # 12: 'ScalePerRow',
    # 13: 'ScalePerColumn',
    },
    0xc741: { # DNG 1.3
    Name: 'OpcodeList2',
    Binary: 1,
    },
    0xc74e: { # DNG 1.3
    Name: 'OpcodeList3',
    Binary: 1,
    },
    0xc761: 'NoiseProfile', # DNG 1.3
    0xc763: { #28
    Name: 'TimeCodes',
    ValueConv: q{
    my @a = split ' ', $val;
    my @v;
    push @v, join('.', map { sprintf('%.2x',$_) } splice(@a,0,8)) while @a >= 8;
    join ' ', @v;
    },
    # Note: Currently ignore the flags:
    # byte 0 0x80 - color frame
    # byte 0 0x40 - drop frame
    # byte 1 0x80 - field phase
    PrintConv: q{
    my @a = map hex, split /[. ]+/, $val;
    my @v;
    while (@a >= 8) {
    my $str = sprintf("%.2x:%.2x:%.2x.%.2x", $a[3]&0x3f,
    $a[2]&0x7f, $a[1]&0x7f, $a[0]&0x3f);
    if ($a[3] & 0x80) { # date+timezone exist if BGF2 is set
    my $tz = $a[7] & 0x3f;
    my $bz = sprintf('%.2x', $tz);
    $bz = 100 if $bz =~ /[a-f]/i; # not BCD
    if ($bz < 26) {
    $tz = ($bz < 13 ? 0 : 26) - $bz;
    } elsif ($bz == 32) {
    $tz = 12.75;
    } elsif ($bz >= 28 and $bz <= 31) {
    $tz = 0; # UTC
    } elsif ($bz < 100) {
    undef $tz; # undefined or user-defined
    } elsif ($tz < 0x20) {
    $tz = (($tz < 0x10 ? 10 : 20) - $tz) - 0.5;
    } else {
    $tz = (($tz < 0x30 ? 53 : 63) - $tz) + 0.5;
    }
    if ($a[7] & 0x80) { # MJD format (/w UTC time)
    my ($h,$m,$s,$f) = split /[:.]/, $str;
    my $jday = sprintf('%x%.2x%.2x', reverse @a[4..6]);
    $str = ConvertUnixTime(($jday - 40587) * 24 * 3600
    + ((($h+$tz) * 60) + $m) * 60 + $s) . ".$f";
    $str =~ s/^(\d+):(\d+):(\d+) /$1-$2-${3}T/;
    } else { # YYMMDD (Note: CinemaDNG 1.1 example seems wrong)
    my $yr = sprintf('%.2x',$a[6]) + 1900;
    $yr += 100 if $yr < 1970;
    $str = sprintf('%d-%.2x-%.2xT%s',$yr,$a[5],$a[4],$str);
    }
    $str .= TimeZoneString($tz*60) if defined $tz;
    }
    push @v, $str;
    splice @a, 0, 8;
    }
    join ' ', @v;
    },
    },
    0xc764: { #28
    Name: 'FrameRate',
    PrintConv: 'int($val * 1000 + 0.5) / 1000',
    },
    0xc772: { #28
    Name: 'TStop',
    PrintConv: 'join("-", map { sprintf("%.2f",$_) } split " ", $val)',
    },
    0xc789: 'ReelName', #28
    0xc791: 'OriginalDefaultFinalSize', # DNG 1.4
    0xc792: { # DNG 1.4
    Name: 'OriginalBestQualitySize',
    Notes: 'called OriginalBestQualityFinalSize by the DNG spec',
    },
    0xc793: 'OriginalDefaultCropSize', # DNG 1.4
    0xc7a1: 'CameraLabel', #28
    0xc7a3: { # DNG 1.4
    Name: 'ProfileHueSatMapEncoding : {0: 'Linear',
    1: 'sRGB',
    },
    },
    0xc7a4: { # DNG 1.4
    Name: 'ProfileLookTableEncoding : {0: 'Linear',
    1: 'sRGB',
    },
    },
    0xc7a5: 'BaselineExposureOffset', # DNG 1.4
    0xc7a6: { # DNG 1.4
    Name: 'DefaultBlackRender : {0: 'Auto',
    1: 'None',
    },
    },
    0xc7a7: { # DNG 1.4
    Name: 'NewRawImageDigest',
    Format: 'undef',
    ValueConv: 'unpack("H*", $val)',
    },
    */


    GPSVersionID: function(data){
    @@ -1144,23 +1913,34 @@ ExifImage.TAGS = {
    GPSDestDistanceRef: { K: 'Kilometers', M: 'Miles', N: 'Nautical Miles' },
    GPSDifferential: { 0: 'No Correction', 1: 'Differential Corrected' }

    /* TODO - might be important for writing...
    /* TODO - following might be important for WRITING...
    sub ConvertExifText($$;$)


    0x001b: {
    Name: 'GPSProcessingMethod',
    Writable: 'undef',
    Notes: 'values of "GPS", "CELLID", "WLAN" or "MANUAL" by the EXIF spec.',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    RawConvInv: 'Image::ExifTool::Exif::EncodeExifText($self,$val)',
    },
    0x001c: {
    Name: 'GPSAreaInformation',
    Writable: 'undef',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    RawConvInv: 'Image::ExifTool::Exif::EncodeExifText($self,$val)',
    },
    0x2bc: {
    Name: 'ApplicationNotes', # (writable directory!)
    Writable: 'int8u',
    Format: 'undef',
    Flags: [ 'Binary', 'Protected' ],
    # this could be an XMP block
    SubDirectory: {
    DirName: 'XMP',
    TagTable: 'Image::ExifTool::XMP::Main',
    },
    },

    0x001b: {
    Name: 'GPSProcessingMethod',
    Writable: 'undef',
    Notes: 'values of 'GPS', 'CELLID', 'WLAN' or 'MANUAL' by the EXIF spec.',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    RawConvInv: 'Image::ExifTool::Exif::EncodeExifText($self,$val)',
    },
    0x001c: {
    Name: 'GPSAreaInformation',
    Writable: 'undef',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    RawConvInv: 'Image::ExifTool::Exif::EncodeExifText($self,$val)',
    },
    GPSTimeStamp: {
    Notes => q{
    when writing, date is stripped off if present, and time is adjusted to UTC
    @@ -1188,25 +1968,142 @@ ExifImage.TAGS = {
    while ($a[-2] < 0) { $a[-2] += 60; --$a[-3] }
    $a[-3] = ($a[-3] + 24) % 24;
    }
    return "$a[-3]:$a[-2]:$a[-1]";
    return '$a[-3]:$a[-2]:$a[-1]';
    }
    },
    0x001d: {
    Name: 'GPSDateStamp',
    Groups: { 2: 'Time' },
    Writable: 'string',
    Format: 'undef', # (Casio EX-H20G uses "\0" instead of ":" as a separator)
    Count: 11,
    Shift: 'Time',
    Notes: q{ when writing, time is stripped off if present, after adjusting date/time to UTC if time includes a timezone. Format is YYYY:mm:dd
    },
    ValueConv: 'Image::ExifTool::Exif::ExifDate($val)',
    ValueConvInv: '$val',
    # pull date out of any format date/time string
    # (and adjust to UTC if this is a full date/time/timezone value)
    PrintConvInv: q{ my $secs; if ($val =~ /[-+]/ and ($secs = Image::ExifTool::GetUnixTime($val, 1))) { $val = Image::ExifTool::ConvertUnixTime($secs); } return $val =~ /(\d{4}).*?(\d{2}).*?(\d{2})/ ? "$1:$2:$3" : undef;
    },
    }
    0x001d: {
    Name: 'GPSDateStamp',
    Groups: { 2: 'Time' },
    Writable: 'string',
    Format: 'undef', # (Casio EX-H20G uses '\0' instead of ':' as a separator)
    Count: 11,
    Shift: 'Time',
    Notes: q{ when writing, time is stripped off if present, after adjusting date/time to UTC if time includes a timezone. Format is YYYY:mm:dd
    },
    ValueConv: 'Image::ExifTool::Exif::ExifDate($val)',
    ValueConvInv: '$val',
    # pull date out of any format date/time string
    # (and adjust to UTC if this is a full date/time/timezone value)
    PrintConvInv: q{ my $secs; if ($val =~ /[-+]/ and ($secs = Image::ExifTool::GetUnixTime($val, 1))) { $val = Image::ExifTool::ConvertUnixTime($secs); } return $val =~ /(\d{4}).*?(\d{2}).*?(\d{2})/ ? '$1:$2:$3' : undef;
    },
    }

    0x111: [
    {
    Condition: q[
    $$self{TIFF_TYPE} eq 'MRW' and $$self{DIR_NAME} eq 'IFD0' and
    $$self{Model} =~ /^DiMAGE A200/
    ],
    Name: 'StripOffsets',
    IsOffset: 1,
    OffsetPair: 0x117, # point to associated byte counts
    # A200 stores this information in the wrong byte order!!
    ValueConv: '$val=join(' ',unpack('N*',pack('V*',split(' ',$val))));\$val',
    ByteOrder: 'LittleEndian',
    },
    {
    Condition: q[
    ($$self{TIFF_TYPE} ne 'CR2' or $$self{DIR_NAME} ne 'IFD0') and
    ($$self{TIFF_TYPE} ne 'DNG' or $$self{DIR_NAME} !~ /^SubIFD[12]$/)
    ],
    Name: 'StripOffsets',
    IsOffset: 1,
    OffsetPair: 0x117, # point to associated byte counts
    ValueConv: 'length($val) > 32 ? \$val : $val',
    },
    {
    Condition: '$$self{DIR_NAME} eq 'IFD0'',
    Name: 'PreviewImageStart',
    IsOffset: 1,
    OffsetPair: 0x117,
    Notes: q{
    PreviewImageStart in IFD0 of CR2 images and SubIFD1 of DNG images, and
    JpgFromRawStart in SubIFD2 of DNG images
    },
    DataTag: 'PreviewImage',
    Writable: 'int32u',
    WriteGroup: 'IFD0',
    WriteCondition: '$$self{TIFF_TYPE} eq 'CR2'',
    Protected: 2,
    },
    {
    Condition: '$$self{DIR_NAME} eq 'SubIFD1'',
    Name: 'PreviewImageStart',
    IsOffset: 1,
    OffsetPair: 0x117,
    DataTag: 'PreviewImage',
    Writable: 'int32u',
    WriteGroup: 'SubIFD1',
    WriteCondition: '$$self{TIFF_TYPE} eq 'DNG'',
    Protected: 2,
    },
    {
    Name: 'JpgFromRawStart',
    IsOffset: 1,
    OffsetPair: 0x117,
    DataTag: 'JpgFromRaw',
    Writable: 'int32u',
    WriteGroup: 'SubIFD2',
    WriteCondition: '$$self{TIFF_TYPE} eq 'DNG'',
    Protected: 2,
    },
    ],

    0x117: [
    {
    Condition: q[
    $$self{TIFF_TYPE} eq 'MRW' and $$self{DIR_NAME} eq 'IFD0' and
    $$self{Model} =~ /^DiMAGE A200/
    ],
    Name: 'StripByteCounts',
    OffsetPair: 0x111, # point to associated offset
    # A200 stores this information in the wrong byte order!!
    ValueConv: '$val=join(' ',unpack('N*',pack('V*',split(' ',$val))));\$val',
    ByteOrder: 'LittleEndian',
    },
    {
    Condition: q[
    ($$self{TIFF_TYPE} ne 'CR2' or $$self{DIR_NAME} ne 'IFD0') and
    ($$self{TIFF_TYPE} ne 'DNG' or $$self{DIR_NAME} !~ /^SubIFD[12]$/)
    ],
    Name: 'StripByteCounts',
    OffsetPair: 0x111, # point to associated offset
    ValueConv: 'length($val) > 32 ? \$val : $val',
    },
    {
    Condition: '$$self{DIR_NAME} eq 'IFD0'',
    Name: 'PreviewImageLength',
    OffsetPair: 0x111,
    Notes: q{
    PreviewImageLength in IFD0 of CR2 images and SubIFD1 of DNG images, and
    JpgFromRawLength in SubIFD2 of DNG images
    },
    DataTag: 'PreviewImage',
    Writable: 'int32u',
    WriteGroup: 'IFD0',
    WriteCondition: '$$self{TIFF_TYPE} eq 'CR2'',
    Protected: 2,
    },
    {
    Condition: '$$self{DIR_NAME} eq 'SubIFD1'',
    Name: 'PreviewImageLength',
    OffsetPair: 0x111,
    DataTag: 'PreviewImage',
    Writable: 'int32u',
    WriteGroup: 'SubIFD1',
    WriteCondition: '$$self{TIFF_TYPE} eq 'DNG'',
    Protected: 2,
    },
    {
    Name: 'JpgFromRawLength',
    OffsetPair: 0x111,
    DataTag: 'JpgFromRaw',
    Writable: 'int32u',
    WriteGroup: 'SubIFD2',
    WriteCondition: '$$self{TIFF_TYPE} eq 'DNG'',
    Protected: 2,
    },
    ],
    */

    }
  10. redaktor renamed this gist May 6, 2014. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  11. redaktor created this gist May 6, 2014.
    1,214 changes: 1,214 additions & 0 deletions WIP exiftool like
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,1214 @@
    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.
    * - exif_buffer An exif_buffer to directly parse.
    *
    * @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 && !options.exif_buffer) {
    throw new Error('You have to provide an image or exif_buffer, 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 {
    if (options.image) {
    this.loadImage(options.image, function (error, image) {
    if (error)
    callback(error);
    else
    callback(false, image);
    });
    } else {
    process.nextTick(function(){
    self.extractExifData(options.exif_buffer, 0, options.exif_buffer.length);
    callback(null, self.exifData);
    });
    }
    }
    }

    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++) {
    //console.log( data );
    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
    var gpsifdOffset = this.exifData.image[ExifImage.TAGS.exif[0x8825]];
    if (typeof gpsifdOffset != "undefined" && gpsifdOffset > 0) {

    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, ExifImage.TAGS.exif);
    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

    // check explicitly for the getString method in case somehow this isn't
    // a buffer. Found this in an image in the wild
    var makerNoteValue = this.exifData.exif[ExifImage.TAGS.exif[0x927C]];
    if (typeof makerNoteValue != "undefined") {

    if (typeof makerNoteValue.getString == "undefined" && typeof makerNoteValue.length != "undefined") {
    // assume we can convert to buffer (we can do arrays and strings)
    makerNoteValue = new Buffer(makerNoteValue);
    }

    if (typeof makerNoteValue.getString != "undefined") {
    // Check the header to see what kind of Makernote we are dealing with
    if (makerNoteValue.getString(0, 7) === "OLYMP\x00\x01" || makerNoteValue.getString(0, 7) === "OLYMP\x00\x02") {
    this.extractMakernotes = require('./makernotes/olympus').extractMakernotes;
    } else if (makerNoteValue.getString(0, 7) === "AGFA \x00\x01") {
    this.extractMakernotes = require('./makernotes/agfa').extractMakernotes;
    } else if (makerNoteValue.getString(0, 8) === "EPSON\x00\x01\x00") {
    this.extractMakernotes = require('./makernotes/epson').extractMakernotes;
    } else if (makerNoteValue.getString(0, 8) === "FUJIFILM") {
    this.extractMakernotes = require('./makernotes/fujifilm').extractMakernotes;
    } else if (makerNoteValue.getString(0, 5) === "SANYO") {
    this.extractMakernotes = require('./makernotes/sanyo').extractMakernotes;
    } else if (makerNoteValue.getString(0, 5) === "Nikon") {
    this.extractMakernotes = require('./makernotes/nikon').extractMakernotes;
    } else if (makerNoteValue.getString(0, 4) === "%\u0000\u0001\u0000") {
    this.extractMakernotes = require('./makernotes/canon').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'] = makerNoteValue.getString(0, 5).concat('...: Unable to extract Makernote information as it is in an unsupported or unrecognized format.');
    }

    if (typeof this.exifData.makernote['error'] == "undefined") {
    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 tagName;

    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 : []
    }

    var tidyString = function(str) {
    if (typeof str === "undefined") str = "";
    str = str + "";
    str = str.replace(/[^a-z0-9 \-\/\.\(\)\:\;\,\©\@\\]/gi, '');
    str = str.replace(/^\s+|\s+$/g, ''); // trim
    if (str.toLowerCase() == "undefined" || str.toLowerCase() == "unknown") str = "";
    return str.trim();
    }

    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") {
    if (!(entry.tagName = tags[entry.tagId](entry))) {
    return false;
    }
    // The tagId corresponds to exactly one tagName
    } else if (tags && tags[entry.tagId]) {
    entry.tagName = tags[entry.tagId];
    // The tagId is not recognized
    } else {
    return false;
    }

    if (entry.components > data.length) {
    entry.components = 0;
    return entry;
    }


    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 = tidyString(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];

    // Is there a string match - todo lang.exists
    if (entry.tagName in ExifImage.TAGS.ref){
    var ref = ExifImage.TAGS.ref[entry.tagName];
    if (typeof ref === 'function'){
    /* composite values */
    var pair = null;
    switch(entry.tagName){
    case 'GPSLatitude':
    pair = self.exifData.gps.GPSLatitudeRef.value;
    break;
    case 'GPSLongitude':
    pair = self.exifData.gps.GPSLongitudeRef.value;
    break;
    }
    entry.value = (pair) ? ref(entry.value, pair) : ref(entry.value);
    } else if (entry.value in ref){
    entry.value = { description: ref[entry.value], value:entry.value };
    }
    ref = null;
    }
    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 : "FocalLengthIn35mmFilm",
    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'

    },

    ref : {

    /* helper functions - TODO might go in helper module */
    arrToDeg: function(vArr, lRef){
    var deg = parseFloat(vArr[0]), m = parseFloat(vArr[1]), s = parseFloat(vArr[2]);
    if(s==0 && m>0){
    var _m = Math.floor(m);
    s = (m-_m)*60;
    m = _m;
    _m = null;
    }
    if(typeof deg !== 'number' || typeof m !== 'number' || typeof s !== 'number') return vArr;
    if (lRef === 'S' || lRef === 'N' || lRef === 'E' || lRef === 'W') {
    var lInt = (lRef == "S" || lRef == "W") ? -1 : 1;
    var v = (deg+(m/60)+(s/3600)) * lInt;
    return {
    description: deg.toString().concat(" deg ", m, "' ", s.toFixed(4), "'' ", lRef),
    value: (typeof v === 'number') ? v : [deg, m, s]
    };
    }
    return [deg, m, s];
    },

    /* helper end */


    ColorSpace : {
    1 : "sRGB"
    },
    ExposureProgram : {
    0 : "Not defined",
    1 : "Manual",
    2 : "Normal program",
    3 : "Aperture priority",
    4 : "Shutter priority",
    5 : "Creative program",
    6 : "Action program",
    7 : "Portrait mode",
    8 : "Landscape mode"
    },
    MeteringMode : {
    0 : "Unknown",
    1 : "Average",
    2 : "CenterWeightedAverage",
    3 : "Spot",
    4 : "MultiSpot",
    5 : "Pattern",
    6 : "Partial",
    255 : "Other"
    },
    LightSource : {
    0 : "Unknown",
    1 : "Daylight",
    2 : "Fluorescent",
    3 : "Tungsten (incandescent light)",
    4 : "Flash",
    9 : "Fine weather",
    10 : "Cloudy weather",
    11 : "Shade",
    12 : "Daylight fluorescent (D 5700 - 7100K)",
    13 : "Day white fluorescent (N 4600 - 5400K)",
    14 : "Cool white fluorescent (W 3900 - 4500K)",
    15 : "White fluorescent (WW 3200 - 3700K)",
    17 : "Standard light A",
    18 : "Standard light B",
    19 : "Standard light C",
    20 : "D55",
    21 : "D65",
    22 : "D75",
    23 : "D50",
    24 : "ISO studio tungsten",
    255 : "Other"
    },
    Flash : {
    0x0000 : "No Flash",
    0x0001 : "Flash fired",
    0x0005 : "Strobe return light not detected",
    0x0007 : "Strobe return light detected",
    0x0009 : "Flash fired, compulsory flash mode",
    0x000D : "Flash fired, compulsory flash mode, return light not detected",
    0x000F : "Flash fired, compulsory flash mode, return light detected",
    0x0010 : "Flash did not fire, compulsory flash mode",
    0x0018 : "Flash did not fire, auto mode",
    0x0019 : "Flash fired, auto mode",
    0x001D : "Flash fired, auto mode, return light not detected",
    0x001F : "Flash fired, auto mode, return light detected",
    0x0020 : "No flash function",
    0x0041 : "Flash fired, red-eye reduction mode",
    0x0045 : "Flash fired, red-eye reduction mode, return light not detected",
    0x0047 : "Flash fired, red-eye reduction mode, return light detected",
    0x0049 : "Flash fired, compulsory flash mode, red-eye reduction mode",
    0x004D : "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",
    0x004F : "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",
    0x0059 : "Flash fired, auto mode, red-eye reduction mode",
    0x005D : "Flash fired, auto mode, return light not detected, red-eye reduction mode",
    0x005F : "Flash fired, auto mode, return light detected, red-eye reduction mode"
    },
    SensingMethod : {
    1 : "Not defined",
    2 : "One-chip color area sensor",
    3 : "Two-chip color area sensor",
    4 : "Three-chip color area sensor",
    5 : "Color sequential area sensor",
    7 : "Trilinear sensor",
    8 : "Color sequential linear sensor"
    },
    SceneCaptureType : {
    0 : "Standard",
    1 : "Landscape",
    2 : "Portrait",
    3 : "Night scene"
    },
    CustomRendered : {
    0 : "Normal",
    1 : "Custom"
    },
    WhiteBalance : {
    0 : "Auto",
    1 : "Manual"
    },
    GainControl : {
    0 : "None",
    1 : "Low gain up",
    2 : "High gain up",
    3 : "Low gain down",
    4 : "High gain down"
    },
    Contrast : {
    0 : "Normal",
    1 : "Soft",
    2 : "Hard"
    },
    Saturation : {
    0 : "Normal",
    1 : "Low saturation",
    2 : "High saturation"
    },
    Sharpness : {
    0 : "Normal",
    1 : "Soft",
    2 : "Hard"
    },
    SubjectDistanceRange : {
    0 : "Unknown",
    1 : "Macro",
    2 : "Close view",
    3 : "Distant view"
    },
    ExposureTime : function(_t){
    if (typeof _t === 'number' && _t < 0.25001 && _t > 0) {
    return '1/'.concat(Math.floor(0.5 + 1/_t));
    }
    return (typeof _t === 'number') ? _t.toFixed(1).replace(/\.0$/, '') : _t.replace(/\.0$/, '');
    },
    FileSource : function(data){
    return (Buffer.isBuffer(data) && data.toJSON()[0]===3) ? { description:"Digital Still Camera", value:3 } : (parseInt(data)||data);
    },
    SceneType : function(data){
    return (Buffer.isBuffer(data) && data.toJSON()[0]===1) ? { description:"Directly photographed", value:1 } : (parseInt(data)||data);
    },
    CFAPattern : function(data){
    /* The value consists of:
    - Two short, being the grid width and height of the repeated pattern.
    - Next, for every pixel in that pattern, an identification code.
    */
    var arr = data.toJSON();
    if ( (arr.reduce(function(a, b){return a + b;})) > 36 ) return '<truncated data>';
    if ( arr.length < 2 ) return '<zero pattern size>';
    var w = arr[1];
    var h = arr[3];
    if ( (4 + w * h) !== arr.length ) return '<truncated data>';
    var cfaColor = ['Red', 'Green', 'Blue', 'Cyan', 'Magenta', 'Yellow', 'White'];
    var rtn = '[';
    var r = 0;
    arr.forEach(function(index,i){
    if(i>3){
    r++;
    var color = (index < cfaColor.length) ? cfaColor[index] : 0;
    rtn = rtn.concat( color );
    color = null;
    if(r==2){ rtn = rtn.concat(']['); r = 0; } else { rtn = rtn.concat(','); }
    }
    });
    console.log( rtn );
    return (rtn.indexOf('0')==-1) ? rtn.slice(0,-1) : arr;
    },
    ComponentsConfiguration : function(data){
    var c = ["-", "Y", "Cb", "Cr", "R", "G", "B"];
    /* TODO */
    return data.toJSON();
    },


    GPSVersionID: function(data){
    return (Buffer.isBuffer(data)) ? data.toJSON().join('.') : data.join('.');
    },
    GPSLatitudeRef: function(data){
    var known = {N: {description:'North', value:data}, S: {description:'South', value:data}};
    if(typeof data === 'string' && data in known){
    return known[data];
    } else if ( typeof parseInt(data) === 'number' ){
    return (parseInt(data)<0) ? known.S : known.N;
    }
    return (Buffer.isBuffer(data)) ? data.toJSON() : data;
    },
    GPSLatitude: function(data, lRef){
    var values = (Buffer.isBuffer(data)) ? data.toJSON() : data;
    if( values instanceof Array && values.length == 3 ) return ExifImage.TAGS.ref.arrToDeg(values, lRef);
    return values;
    },
    GPSLongitudeRef: function(data){
    var known = {E: {description:'East', value:data}, W: {description:'West', value:data}};
    if(typeof data === 'string' && data in known){
    return known[data];
    } else if ( typeof parseInt(data) === 'number' ){
    return (parseInt(data)<0) ? known.W : known.E;
    }
    return (Buffer.isBuffer(data)) ? data.toJSON() : data;
    },
    GPSLongitude: function(data, lRef){
    var values = (Buffer.isBuffer(data)) ? data.toJSON() : data;
    if( values instanceof Array && values.length == 3 ) return ExifImage.TAGS.ref.arrToDeg(values, lRef);
    return values;
    },
    GPSAltitudeRef: { 0: 'Above Sea Level', 1: 'Below Sea Level' },
    GPSAltitude: function(data){
    var v = (typeof data === 'string') ? parseInt(data.replace(/\s*m$/, '')) : data;
    if(typeof v === 'number') return {description: v.toString().trim().concat(' m'), value: data};
    return data;
    },
    GPSTimeStamp: function(data){ return (Buffer.isBuffer(data)) ? data.toJSON().join(':') : data.join(':'); },
    GPSMeasureMode: { 2: '2-Dimensional Measurement', 3: '3-Dimensional Measurement' },
    GPSStatus: { A: 'Measurement Active', V: 'Measurement Void' },
    GPSSpeedRef: { K: 'km/h', M: 'mph', N: 'knots' },
    GPSTrackRef: { M: 'Magnetic North', T: 'True North' },
    GPSImgDirectionRef: { M: 'Magnetic North', T: 'True North' },
    GPSDestLatitudeRef: { N: 'North', S: 'South' },
    GPSDestLongitudeRef: { E: 'East', W: 'West' },
    GPSDestBearingRef: { M: 'Magnetic North', T: 'True North' },
    GPSDestDistanceRef: { K: 'Kilometers', M: 'Miles', N: 'Nautical Miles' },
    GPSDifferential: { 0: 'No Correction', 1: 'Differential Corrected' }

    /* TODO - might be important for writing...
    sub ConvertExifText($$;$)


    0x001b: {
    Name: 'GPSProcessingMethod',
    Writable: 'undef',
    Notes: 'values of "GPS", "CELLID", "WLAN" or "MANUAL" by the EXIF spec.',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    RawConvInv: 'Image::ExifTool::Exif::EncodeExifText($self,$val)',
    },
    0x001c: {
    Name: 'GPSAreaInformation',
    Writable: 'undef',
    RawConv: 'Image::ExifTool::Exif::ConvertExifText($self,$val,1)',
    RawConvInv: 'Image::ExifTool::Exif::EncodeExifText($self,$val)',
    },
    GPSTimeStamp: {
    Notes => q{
    when writing, date is stripped off if present, and time is adjusted to UTC
    if it includes a timezone
    },
    ValueConv => 'Image::ExifTool::GPS::ConvertTimeStamp($val)',
    ValueConvInv => '$val=~tr/:/ /;$val',
    # pull time out of any format date/time string
    # (converting to UTC if a timezone is given)
    PrintConvInv => sub {
    my $v = shift;
    my @tz;
    if ($v =~ s/([-+])(.*)//s) { # remove timezone
    my $s = $1 eq '-' ? 1 : -1; # opposite sign to convert back to UTC
    my $t = $2;
    @tz = ($s*$1, $s*$2) if $t =~ /^(\d{2}):?(\d{2})\s*$/;
    }
    my @a = ($v =~ /((?=\d|\.\d)\d*(?:\.\d*)?)/g);
    push @a, '00' while @a < 3;
    if (@tz) {
    # adjust to UTC
    $a[-2] += $tz[1];
    $a[-3] += $tz[0];
    while ($a[-2] >= 60) { $a[-2] -= 60; ++$a[-3] }
    while ($a[-2] < 0) { $a[-2] += 60; --$a[-3] }
    $a[-3] = ($a[-3] + 24) % 24;
    }
    return "$a[-3]:$a[-2]:$a[-1]";
    }
    },
    0x001d: {
    Name: 'GPSDateStamp',
    Groups: { 2: 'Time' },
    Writable: 'string',
    Format: 'undef', # (Casio EX-H20G uses "\0" instead of ":" as a separator)
    Count: 11,
    Shift: 'Time',
    Notes: q{ when writing, time is stripped off if present, after adjusting date/time to UTC if time includes a timezone. Format is YYYY:mm:dd
    },
    ValueConv: 'Image::ExifTool::Exif::ExifDate($val)',
    ValueConvInv: '$val',
    # pull date out of any format date/time string
    # (and adjust to UTC if this is a full date/time/timezone value)
    PrintConvInv: q{ my $secs; if ($val =~ /[-+]/ and ($secs = Image::ExifTool::GetUnixTime($val, 1))) { $val = Image::ExifTool::ConvertUnixTime($secs); } return $val =~ /(\d{4}).*?(\d{2}).*?(\d{2})/ ? "$1:$2:$3" : undef;
    },
    }
    */

    }

    };