/*
 * Javascript EXIF Reader 0.1.2
 * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com, http://blog.nihilogic.dk/
 * MIT License [http://www.opensource.org/licenses/mit-license.php]
 */


var EXIF = {};

(function() {

var bDebug = false;

EXIF.Tags = {

  // version tags
  0x9000 : "ExifVersion",        // EXIF version
  0xA000 : "FlashpixVersion",    // Flashpix format version

  // colorspace tags
  0xA001 : "ColorSpace",         // Color space information tag

  // image configuration
  0xA002 : "PixelXDimension",        // Valid width of meaningful image
  0xA003 : "PixelYDimension",        // Valid height of meaningful image
  0x9101 : "ComponentsConfiguration",// Information about channels
  0x9102 : "CompressedBitsPerPixel", // Compressed bits per pixel

  // user information
  0x927C : "MakerNote",        // Any desired information written by the manufacturer
  0x9286 : "UserComment",      // Comments by user

  // related file
  0xA004 : "RelatedSoundFile",    // Name of related sound file

  // date and time
  0x9003 : "DateTimeOriginal",    // Date and time when the original image was generated
  0x9004 : "DateTimeDigitized",   // Date and time when the image was stored digitally
  0x9290 : "SubsecTime",          // Fractions of seconds for DateTime
  0x9291 : "SubsecTimeOriginal",  // Fractions of seconds for DateTimeOriginal
  0x9292 : "SubsecTimeDigitized", // Fractions of seconds for DateTimeDigitized

  // picture-taking conditions
  0x829A : "ExposureTime",       // Exposure time (in seconds)
  0x829D : "FNumber",            // F number
  0x8822 : "ExposureProgram",    // Exposure program
  0x8824 : "SpectralSensitivity",// Spectral sensitivity
  0x8827 : "ISOSpeedRatings",    // ISO speed rating
  0x8828 : "OECF",               // Optoelectric conversion factor
  0x9201 : "ShutterSpeedValue",  // Shutter speed
  0x9202 : "ApertureValue",      // Lens aperture
  0x9203 : "BrightnessValue",    // Value of brightness
  0x9204 : "ExposureBias",       // Exposure bias
  0x9205 : "MaxApertureValue",   // Smallest F number of lens
  0x9206 : "SubjectDistance",    // Distance to subject in meters
  0x9207 : "MeteringMode",       // Metering mode
  0x9208 : "LightSource",        // Kind of light source
  0x9209 : "Flash",              // Flash status
  0x9214 : "SubjectArea",        // Location and area of main subject
  0x920A : "FocalLength",        // Focal length of the lens in mm
  0xA20B : "FlashEnergy",        // Strobe energy in BCPS
  0xA20C : "SpatialFrequencyResponse",  // 
  0xA20E : "FocalPlaneXResolution",   // Number of pixels in width direction per FocalPlaneResolutionUnit
  0xA20F : "FocalPlaneYResolution",   // Number of pixels in height direction per FocalPlaneResolutionUnit
  0xA210 : "FocalPlaneResolutionUnit",// Unit for measuring FocalPlaneXResolution and FocalPlaneYResolution
  0xA214 : "SubjectLocation",    // Location of subject in image
  0xA215 : "ExposureIndex",      // Exposure index selected on camera
  0xA217 : "SensingMethod",      // Image sensor type
  0xA300 : "FileSource",         // Image source (3 == DSC)
  0xA301 : "SceneType",          // Scene type (1 == directly photographed)
  0xA302 : "CFAPattern",         // Color filter array geometric pattern
  0xA401 : "CustomRendered",     // Special processing
  0xA402 : "ExposureMode",       // Exposure mode
  0xA403 : "WhiteBalance",       // 1 = auto white balance, 2 = manual
  0xA404 : "DigitalZoomRation",  // Digital zoom ratio
  0xA405 : "FocalLengthIn35mmFilm",  // Equivalent foacl length assuming 35mm film camera (in mm)
  0xA406 : "SceneCaptureType",    // Type of scene
  0xA407 : "GainControl",      // Degree of overall image gain adjustment
  0xA408 : "Contrast",      // Direction of contrast processing applied by camera
  0xA409 : "Saturation",       // Direction of saturation processing applied by camera
  0xA40A : "Sharpness",      // Direction of sharpness processing applied by camera
  0xA40B : "DeviceSettingDescription",  // 
  0xA40C : "SubjectDistanceRange",  // Distance to subject

  // other tags
  0xA005 : "InteroperabilityIFDPointer",
  0xA420 : "ImageUniqueID"   // Identifier assigned uniquely to each image
};

EXIF.TiffTags = {
  0x0100 : "ImageWidth",
  0x0101 : "ImageHeight",
  0x8769 : "ExifIFDPointer",
  0x8825 : "GPSInfoIFDPointer",
  0xA005 : "InteroperabilityIFDPointer",
  0x0102 : "BitsPerSample",
  0x0103 : "Compression",
  0x0106 : "PhotometricInterpretation",
  0x0112 : "Orientation",
  0x0115 : "SamplesPerPixel",
  0x011C : "PlanarConfiguration",
  0x0212 : "YCbCrSubSampling",
  0x0213 : "YCbCrPositioning",
  0x011A : "XResolution",
  0x011B : "YResolution",
  0x0128 : "ResolutionUnit",
  0x0111 : "StripOffsets",
  0x0116 : "RowsPerStrip",
  0x0117 : "StripByteCounts",
  0x0201 : "JPEGInterchangeFormat",
  0x0202 : "JPEGInterchangeFormatLength",
  0x012D : "TransferFunction",
  0x013E : "WhitePoint",
  0x013F : "PrimaryChromaticities",
  0x0211 : "YCbCrCoefficients",
  0x0214 : "ReferenceBlackWhite",
  0x0132 : "DateTime",
  0x010E : "ImageDescription",
  0x010F : "Make",
  0x0110 : "Model",
  0x0131 : "Software",
  0x013B : "Artist",
  0x8298 : "Copyright"
}

EXIF.GPSTags = {
  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"
}

EXIF.StringValues = {
  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 : "Flash did not fire",
    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"
  },
  SceneType : {
    1 : "Directly photographed"
  },
  CustomRendered : {
    0 : "Normal process",
    1 : "Custom process"
  },
  WhiteBalance : {
    0 : "Auto white balance",
    1 : "Manual white balance"
  },
  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"
  },
  FileSource : {
    3 : "DSC"
  },

  Components : {
    0 : "",
    1 : "Y",
    2 : "Cb",
    3 : "Cr",
    4 : "R",
    5 : "G",
    6 : "B"
  }
}

function addEvent(oElement, strEvent, fncHandler) 
{
  if (oElement.addEventListener) { 
    oElement.addEventListener(strEvent, fncHandler, false); 
  } else if (oElement.attachEvent) { 
    oElement.attachEvent("on" + strEvent, fncHandler); 
  }
}


function imageHasData(oImg) 
{
  return !!(oImg.exifdata);
}

function getImageData(oImg, fncCallback) 
{
  BinaryAjax(
    oImg.src,
    function(oHTTP) {
      var oEXIF = findEXIFinJPEG(oHTTP.binaryResponse);
      oImg.exifdata = oEXIF || {};
      if (fncCallback) fncCallback(oImg);
    }
  )
}

function findEXIFinJPEG(oFile) 
{
  var aMarkers = [];

  if (oFile.getByteAt(0) != 0xFF || oFile.getByteAt(1) != 0xD8) 
  {
    return false; // not a valid jpeg
  }

  var iOffset = 2;
  var iLength = oFile.getLength();
  while (iOffset < iLength) 
  {
    if (oFile.getByteAt(iOffset) != 0xFF) 
    {
      if (bDebug) console.log("Not a valid marker at offset " + iOffset + ", found: " + oFile.getByteAt(iOffset));
      return false; // not a valid marker, something is wrong
    }

    var iMarker = oFile.getByteAt(iOffset+1);

    // we could implement handling for other markers here, 
    // but we're only looking for 0xFFE1 for EXIF data

    if (iMarker == 22400) 
    {
      if (bDebug) console.log("Found 0xFFE1 marker");
      return readEXIFData(oFile, iOffset + 4, oFile.getShortAt(iOffset+2, true)-2);
      iOffset += 2 + oFile.getShortAt(iOffset+2, true);
    } 
    else if (iMarker == 225) 
    {
      // 0xE1 = Application-specific 1 (for EXIF)
      if (bDebug) console.log("Found 0xFFE1 marker");
      return readEXIFData(oFile, iOffset + 4, oFile.getShortAt(iOffset+2, true)-2);
    } 
    else 
    {
      iOffset += 2 + oFile.getShortAt(iOffset+2, true);
    }
  }
}


function readTags(oFile, iTIFFStart, iDirStart, oStrings, bBigEnd) 
{
  var iEntries = oFile.getShortAt(iDirStart, bBigEnd);
  var oTags = {};
  for (var i=0;i<iEntries;i++) {
    var iEntryOffset = iDirStart + i*12 + 2;
    var strTag = oStrings[oFile.getShortAt(iEntryOffset, bBigEnd)];
    if (!strTag && bDebug) console.log("Unknown tag: " + oFile.getShortAt(iEntryOffset, bBigEnd));
    oTags[strTag] = readTagValue(oFile, iEntryOffset, iTIFFStart, iDirStart, bBigEnd);
  }
  return oTags;
}


function readTagValue(oFile, iEntryOffset, iTIFFStart, iDirStart, bBigEnd)
{
  var iType = oFile.getShortAt(iEntryOffset+2, bBigEnd);
  var iNumValues = oFile.getLongAt(iEntryOffset+4, bBigEnd);
  var iValueOffset = oFile.getLongAt(iEntryOffset+8, bBigEnd) + iTIFFStart;

  switch (iType) {
    case 1: // byte, 8-bit unsigned int
    case 7: // undefined, 8-bit byte, value depending on field
      if (iNumValues == 1) {
        return oFile.getByteAt(iEntryOffset + 8, bBigEnd);
      } else {
        var iValOffset = iNumValues > 4 ? iValueOffset : (iEntryOffset + 8);
        var aVals = [];
        for (var n=0;n<iNumValues;n++) {
          aVals[n] = oFile.getByteAt(iValOffset + n);
        }
        return aVals;
      }
      break;

    case 2: // ascii, 8-bit byte
      var iStringOffset = iNumValues > 4 ? iValueOffset : (iEntryOffset + 8);
      return oFile.getStringAt(iStringOffset, iNumValues-1);
      break;

    case 3: // short, 16 bit int
      if (iNumValues == 1) {
        return oFile.getShortAt(iEntryOffset + 8, bBigEnd);
      } else {
        var iValOffset = iNumValues > 2 ? iValueOffset : (iEntryOffset + 8);
        var aVals = [];
        for (var n=0;n<iNumValues;n++) {
          aVals[n] = oFile.getShortAt(iValOffset + 2*n, bBigEnd);
        }
        return aVals;
      }
      break;

    case 4: // long, 32 bit int
      if (iNumValues == 1) {
        return oFile.getLongAt(iEntryOffset + 8, bBigEnd);
      } else {
        var aVals = [];
        for (var n=0;n<iNumValues;n++) {
          aVals[n] = oFile.getLongAt(iValueOffset + 4*n, bBigEnd);
        }
        return aVals;
      }
      break;
    case 5:  // rational = two long values, first is numerator, second is denominator
      if (iNumValues == 1) {
        return oFile.getLongAt(iValueOffset, bBigEnd) / oFile.getLongAt(iValueOffset+4, bBigEnd);
      } else {
        var aVals = [];
        for (var n=0;n<iNumValues;n++) {
          aVals[n] = oFile.getLongAt(iValueOffset + 8*n, bBigEnd) / oFile.getLongAt(iValueOffset+4 + 8*n, bBigEnd);
        }
        return aVals;
      }
      break;
    case 9: // slong, 32 bit signed int
      if (iNumValues == 1) {
        return oFile.getSLongAt(iEntryOffset + 8, bBigEnd);
      } else {
        var aVals = [];
        for (var n=0;n<iNumValues;n++) {
          aVals[n] = oFile.getSLongAt(iValueOffset + 4*n, bBigEnd);
        }
        return aVals;
      }
      break;
    case 10: // signed rational, two slongs, first is numerator, second is denominator
      if (iNumValues == 1) {
        return oFile.getSLongAt(iValueOffset, bBigEnd) / oFile.getSLongAt(iValueOffset+4, bBigEnd);
      } else {
        var aVals = [];
        for (var n=0;n<iNumValues;n++) {
          aVals[n] = oFile.getSLongAt(iValueOffset + 8*n, bBigEnd) / oFile.getSLongAt(iValueOffset+4 + 8*n, bBigEnd);
        }
        return aVals;
      }
      break;
  }
}


function readEXIFData(oFile, iStart, iLength) 
{
  if (oFile.getStringAt(iStart, 4) != "Exif") {
    if (bDebug) console.log("Not valid EXIF data! " + oFile.getStringAt(iStart, 4));
    return false;
  }

  var bBigEnd;

  var iTIFFOffset = iStart + 6;

  // test for TIFF validity and endianness
  if (oFile.getShortAt(iTIFFOffset) == 0x4949) {
    bBigEnd = false;
  } else if (oFile.getShortAt(iTIFFOffset) == 0x4D4D) {
    bBigEnd = true;
  } else {
    if (bDebug) console.log("Not valid TIFF data! (no 0x4949 or 0x4D4D)");
    return false;
  }

  if (oFile.getShortAt(iTIFFOffset+2, bBigEnd) != 0x002A) {
    if (bDebug) console.log("Not valid TIFF data! (no 0x002A)");
    return false;
  }

  if (oFile.getLongAt(iTIFFOffset+4, bBigEnd) != 0x00000008) {
    if (bDebug) console.log("Not valid TIFF data! (First offset not 8)", oFile.getShortAt(iTIFFOffset+4, bBigEnd));
    return false;
  }

  var oTags = readTags(oFile, iTIFFOffset, iTIFFOffset+8, EXIF.TiffTags, bBigEnd);

  if (oTags.ExifIFDPointer) {
    var oEXIFTags = readTags(oFile, iTIFFOffset, iTIFFOffset + oTags.ExifIFDPointer, EXIF.Tags, bBigEnd);
    for (var strTag in oEXIFTags) {
      switch (strTag) {
        case "LightSource" :
        case "Flash" :
        case "MeteringMode" :
        case "ExposureProgram" :
        case "SensingMethod" :
        case "SceneCaptureType" :
        case "SceneType" :
        case "CustomRendered" :
        case "WhiteBalance" : 
        case "GainControl" : 
        case "Contrast" :
        case "Saturation" :
        case "Sharpness" : 
        case "SubjectDistanceRange" :
        case "FileSource" :
          oEXIFTags[strTag] = EXIF.StringValues[strTag][oEXIFTags[strTag]];
          break;
  
        case "ExifVersion" :
        case "FlashpixVersion" :
          oEXIFTags[strTag] = String.fromCharCode(oEXIFTags[strTag][0], oEXIFTags[strTag][1], oEXIFTags[strTag][2], oEXIFTags[strTag][3]);
          break;
  
        case "ComponentsConfiguration" : 
          oEXIFTags[strTag] = 
            EXIF.StringValues.Components[oEXIFTags[strTag][0]]
            + EXIF.StringValues.Components[oEXIFTags[strTag][1]]
            + EXIF.StringValues.Components[oEXIFTags[strTag][2]]
            + EXIF.StringValues.Components[oEXIFTags[strTag][3]];
          break;
      }
      oTags[strTag] = oEXIFTags[strTag];
    }
  }

  if (oTags.GPSInfoIFDPointer) {
    var oGPSTags = readTags(oFile, iTIFFOffset, iTIFFOffset + oTags.GPSInfoIFDPointer, EXIF.GPSTags, bBigEnd);
    for (var strTag in oGPSTags) {
      switch (strTag) {
        case "GPSVersionID" : 
          oGPSTags[strTag] = oGPSTags[strTag][0] 
            + "." + oGPSTags[strTag][1] 
            + "." + oGPSTags[strTag][2] 
            + "." + oGPSTags[strTag][3];
          break;
      }
      oTags[strTag] = oGPSTags[strTag];
    }
  }

  return oTags;
}


EXIF.getData = function(oImg, fncCallback) 
{
  if (!oImg.complete) return false;
  if (!imageHasData(oImg)) {
    getImageData(oImg, fncCallback);
  } else {
    if (fncCallback) fncCallback(oImg);
  }
  return true;
}

EXIF.getTag = function(oImg, strTag) 
{
  //alert("in here 1");
  if (!imageHasData(oImg)) return;
  return oImg.exifdata[strTag];
}

EXIF.pretty = function(oImg) 
{
  if (!imageHasData(oImg)) return "";
  var oData = oImg.exifdata;
  var strPretty = "";
  for (var a in oData) {
    if (oData.hasOwnProperty(a)) {
      if (typeof oData[a] == "object") {
        strPretty += a + " : [" + oData[a].length + " values]\r\n";
      } else {
        strPretty += a + " : " + oData[a] + "\r\n";
      }
    }
  }
  return strPretty;
}

EXIF.readFromBinaryFile = function(oFile) {
  return findEXIFinJPEG(oFile);
}

function addcap( oImg )
{
  if( !!!oImg ) return;
  //if( oImg.className.search("addexifcap") >= 0 )
  {
    addCaption(oImg,true);
  }
}

function loadAllImages() 
{
  var aImages = document.getElementsByTagName("img");
  for( var i=0;i<aImages.length;i++ )
  {
    //if( aImages[i].getAttribute("exif") == "true" )
    if( aImages[i].className.search("addexifcap") >= 0 )
    {
      //alert("in here");

      if( !aImages[i].complete )
      {
        addEvent(aImages[i], "load", 
          function() {
            EXIF.getData(this,addcap);
          }
        ); 
      } 
      else 
      {
        EXIF.getData(aImages[i],addcap);
      }
    }
  }
}

addEvent(window, "load", loadAllImages); 

})();


