//
//-----------------------------------------------------------------------------
// exif.js
//
// JavaScript library for reading EXIF image metadata V 2.1.1
//
// https://github.com/exif-js/exif-js
//
// Ergänzungen für IPTC und XMP       peter@danninger.eu       25. 08. 2015
// Modifikationen für EXIF 2.3        peter@danninger.eu       17. 04. 2016
// Modifikationen für EXIF 2.31       peter@danninger.eu       05. 05. 2019
// Modifikationen für EXIF 2.32       peter@danninger.eu       21. 11. 2019
// Version                            peter@danninger.eu       21. 11. 2019
//
// -------------------------- Exif 2.32 spec --------------------------------
// https://www.cipa.jp/std/documents/download_e.html?DC-008-Translation-2019-E
//
//   EXIF.getData (img, callback)
//   EXIF.getExifTag (img, tag)
//   EXIF.getIptcTag (img, tag)
//   EXIF.getXmpHeader (img)
//
// Im Gegensatz zu EXIF und IPTC können beim XMP Header keine einzelnen Tags
// extrahiert werden, sondern nur der komplette XMP-Header.
//
// Ein primitiver nicht validierender XML-Parser als Beispiel:
//        metatags.readXmpTag
//        metatags.getAttr
//
// Beispiel für XMP-Bild-Titel:
//        xmp = EXIF.getXmpHeader (this);
//        XMPtitel = metatags.readXmpTag (xmp, "dc:title.rdf:Alt.rdf:li");
//
//-----------------------------------------------------------------------------
//

(function () {
  var _debug = false;               // debugging
  var _debugAv = false;             // separate debugging Attribute values
  var _console = false;             // false: alert () | true: console.log ()
  var _trace = false;               // trace of all found tags
  var _allEXIF = "";                // all found EXIF-Tags
  var _allIPTC = "";                // all found IPTC-Tags
  var root = this;
  var EXIF = function (obj) {
    if (obj instanceof EXIF)        return (obj);
    if (!(this instanceof EXIF))    return (new EXIF (obj));
    this.EXIFwrapped = obj;
  };
  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = EXIF;
    }
    exports.EXIF = EXIF;
  } else {
    root.EXIF = EXIF;     // this
  }

  // ----------- Exif 2.32 spec page 41-42 -------
  // https://www.cipa.jp/std/documents/download_e.html?DC-008-Translation-2019-E
  //
  var ExifTags = EXIF.Tags = {                      // Exif 2.32 Tags ------------------
    // ------------------------- version tags
    0x9000 : "ExifVersion",                         // undefined (4)
    0xA000 : "FlashpixVersion",                     // undefined (4)
    // ------------------------- image data characteristics
    0xA001 : "ColorSpace",                          // int16u (short) (1)
    0xA500 : "Gamma",                               // rational (1)
    // ------------------------- image configuration
    0x9101 : "ComponentsConfiguration",             // undefined (4)
    0x9102 : "CompressedBitsPerPixel",              // rational (1)
    0xA002 : "PixelXDimension",                     // int32u (int16u) (long or short) (1)
    0xA003 : "PixelYDimension",                     // int32u (int16u) (long or short) (1)
    // ------------------------- user information
    0x927C : "MakerNote",                           // undefined (any)
    0x9286 : "UserComment",                         // undefined (any)
    // ------------------------- related file info
    0xA004 : "RelatedSoundFile",                    // ASCII (13)
    //
    // ------------------------- date and time
    //
    // "DateTimeOriginal" sollte der eigentliche Aufnahmezeitpunkt sein
    // "DateTimeDigitized" sollte der Zeitpunkt des digitalen Bildes sein
    // meistens sind beide identisch, aber ACHTUNG:
    // Bei digitalisierten Bildern, z.B. screenschots, ist nur "DateTimeDigitized"
    // plausibel, ein evtl. "DateTimeOriginal" gibt einen späteren Zeitpunkt an ???
    //
    0x9003 : "DateTimeOriginal",                    // ASCII (20) ==> "DateTimeOriginal"
    0x9004 : "DateTimeDigitized",                   // ASCII (20) ==> "CreateDate"
    //       "DateTime" (siehe other Tags 0x0132)                 ==> "ModifyDate"
    //
    // wird derzeit durch Lightroom nicht unterstützt
    //
    0x9010 : "OffsetTime",                          // ASCII (7)
    0x9011 : "OffsetTimeOriginal",                  // ASCII (7)
    0x9012 : "OffsetTimeDigitized",                 // ASCII (7)
    //
    0x9290 : "SubsecTime",                          // ASCII (any)
    0x9291 : "SubsecTimeOriginal",                  // ASCII (any)
    0x9292 : "SubsecTimeDigitized",                 // ASCII (any)
    //
    // ------------------------- picture-taking conditions (Table 8)
    0x829A : "ExposureTime",                        // rational (1)
    0x829D : "FNumber",                             // rational (1)
    0x8822 : "ExposureProgram",                     // int16u (short) (1)
    0x8824 : "SpectralSensitivity",                 // ASCII (any)
    0x8827 : "PhotographicSensitivity",             // int16u (short) (any)     ISO
    0x8828 : "OECF",                                // undefined (any)
    0x8830 : "SensitivityType",                     // int16u (short)  (1)
    0x8831 : "StandardOutputSensitivity",           // int32u (long)  (1)
    0x8832 : "RecommendedExposureIndex",            // int32u (long)  (1)
    0x8833 : "ISOSpeed",                            // int32u (long)  (1)
    0x8834 : "ISOSpeedLatitudeyyy",                 // int32u (long)  (1)
    0x8835 : "ISOSpeedLatitudezzz",                 // int32u (long)  (1)
    0x9201 : "ShutterSpeedValue",                   // Srational (1)
    0x9202 : "ApertureValue",                       // rational (1)
    0x9203 : "BrightnessValue",                     // Srational (1)
    0x9204 : "ExposureBiasValue",                   // Srational (1)
    0x9205 : "MaxApertureValue",                    // rational (1)
    0x9206 : "SubjectDistance",                     // rational (1)
    0x9207 : "MeteringMode",                        // int16u (short) (1)
    0x9208 : "LightSource",                         // int16u (short) (1)
    0x9209 : "Flash",                               // int16u (short) (1)
    0x920A : "FocalLength",                         // rational (1)
    0x9214 : "SubjectArea",                         // int16u (short) (2 | 3 | 4)
    0xA20B : "FlashEnergy",                         // rational (1)
    0xA20C : "SpatialFrequencyResponse",            // undefined (any)
    0xA20E : "FocalPlaneXResolution",               // rational (1)
    0xA20F : "FocalPlaneYResolution",               // rational (1)
    0xA210 : "FocalPlaneResolutionUnit",            // int16u (short) (1)
    0xA214 : "SubjectLocation",                     // int16u (short) (2)
    0xA215 : "ExposureIndex",                       // rational (1)
    0xA217 : "SensingMethod",                       // int16u (short) (1)
    0xA300 : "FileSource",                          // undefined (1)
    0xA301 : "SceneType",                           // undefined (1)
    0xA302 : "CFAPattern",                          // undefined (any)
    0xA401 : "CustomRendered",                      // int16u (short) (1)
    0xA402 : "ExposureMode",                        // int16u (short) (1)
    0xA403 : "WhiteBalance",                        // int16u (short) (1)
    0xA404 : "DigitalZoomRation",                   // rational (1)
    0xA405 : "FocalLengthIn35mmFilm",               // int16u (short) (1)
    0xA406 : "SceneCaptureType",                    // int16u (short) (1)
    0xA407 : "GainControl",                         // rational (1)
    0xA408 : "Contrast",                            // int16u (short) (1)
    0xA409 : "Saturation",                          // int16u (short) (1)
    0xA40A : "Sharpness",                           // int16u (short) (1)
    0xA40B : "DeviceSettingDescription",            // undefined (any)
    0xA40C : "SubjectDistanceRange",                // int16u (short) (1)
    0xA460 : "CompositeImage",                      // int16u (short) (1)
    0xA461 : "SourceImageNumberOfCompositeImage",   // int16u (short) (2)
    0xA462 : "SourceExposureTimesOfCompositeImage", // undefined (any)
    //
    // ------------------------- Tags relating to shooting situation
    0x9400 : "Temperature",                         // Srational (1)
    0x9401 : "Humidity",                            // rational (1)
    0x9402 : "Pressure",                            // rational (1)
    0x9403 : "WaterDepth",                          // Srational (1)
    0x9404 : "Acceleration",                        // rational (1)
    0x9405 : "CameraElevationAngle",                // Srational (1)
    
    // ----------- exif 2.32 spec page 83 -------
    // https://www.cipa.jp/std/documents/download_e.html?DC-008-Translation-2019-E
    //
    // ------------------------- interoperability tags
    0xA005 : "InteroperabilityIFDPointer",          // int32u (long) ?????
    //
    // ------------------------- other tags
    0xA420 : "ImageUniqueID",                       // ASCII (33)
    0xA430 : "CameraOwnerName",                     // ASCII (any)
    0xA431 : "BodySerialNumber",                    // ASCII (any)
    0xA432 : "LensSpecification",                   // rational (4)
    0xA433 : "LensMake",                            // ASCII (any)
    0xA434 : "LensModel",                           // ASCII (any)
    0xA435 : "LensSerialNumber"                     // ASCII (any)
  };

  // ----------- exif 2.32 spec page 27 -------
  // https://www.cipa.jp/std/documents/download_e.html?DC-008-Translation-2019-E
  //
  var TiffTags = EXIF.TiffTags = {          // TIFF 6.0 Tags -------------------
    // ------------------------- image data structure
    0x0100 : "ImageWidth",                  // int32u (int16u) (long or short) (1)
    0x0101 : "ImageLength",                 // int32u (int16u) (long or short) (1)
    0x0102 : "BitsPerSample",               // int16u (short) (3)
    0x0103 : "Compression",                 // int16u (short) 
    0x0106 : "PhotometricInterpretation",   // int16u (short) 
    0x0112 : "Orientation",                 // int16u (short) 
    0x0115 : "SamplesPerPixel",             // int16u (short) 
    0x011C : "PlanarConfiguration",         // int16u (short) 
    0x0212 : "YCbCrSubSampling",            // int16u (short)  (2)
    0x0213 : "YCbCrPositioning",            // int16u (short) 
    0x011A : "XResolution",                 // rational
    0x011B : "YResolution",                 // rational
    0x0128 : "ResolutionUnit",              // int16u (short) 
    // ------------------------- recording offset
    0x0111 : "StripOffsets",                // int32u (int16u) (long or short) (1)
    0x0116 : "RowsPerStrip",                // int32u (int16u) (long or short) (1)
    0x0117 : "StripByteCounts",             // int32u (int16u) (long or short) (1)
    0x0201 : "JPEGInterchangeFormat",       // int32u (long) 
    0x0202 : "JPEGInterchangeFormatLength", // int32u (long) 
    // ------------------------- image data characteristics
    0x012D : "TransferFunction",            // int16u (short) 
    0x013E : "WhitePoint",                  // rational
    0x013F : "PrimaryChromaticities",       // rational
    0x0211 : "YCbCrCoefficients",           // rational
    0x0214 : "ReferenceBlackWhite",         // rational
    // ------------------------- other tags
    0x010E : "ImageDescription",            // ASCII (any)
    0x010F : "Make",                        // ASCII (any)
    0x0110 : "Model",                       // ASCII (any)
    0x0131 : "Software",                    // ASCII (any)
    0x0132 : "DateTime",                    // ASCII (20) ==> ModifyDate !!!
    0x013B : "Artist",                      // ASCII (any)
    0x8298 : "Copyright",                   // ASCII (any)
    // ------------------------- Exif specific tags
    0x8769 : "ExifIFDPointer",              // int32u (long) 
    0x8825 : "GPSInfoIFDPointer"            // int32u (long) 
  };

  // ----------- exif 2.32 spec page 72 -------
  // https://www.cipa.jp/std/documents/download_e.html?DC-008-Translation-2019-E
  //
  var GPSTags = EXIF.GPSTags = {            // GPS Tags ------------------------
    0x0000 : "GPSVersionID",                // byte (4)
    0x0001 : "GPSLatitudeRef",              // ASCII (2)
    0x0002 : "GPSLatitude",                 // rational (3)
    0x0003 : "GPSLongitudeRef",             // ASCII (2)
    0x0004 : "GPSLongitude",                // rational (3)
    0x0005 : "GPSAltitudeRef",              // byte
    0x0006 : "GPSAltitude",                 // rational
    0x0007 : "GPSTimeStamp",                // rational (3)
    0x0008 : "GPSSatellites",               // ASCII (any)
    0x0009 : "GPSStatus",                   // ASCII (2)
    0x000A : "GPSMeasureMode",              // ASCII (2)
    0x000B : "GPSDOP",                      // rational
    0x000C : "GPSSpeedRef",                 // ASCII (2)
    0x000D : "GPSSpeed",                    // rational
    0x000E : "GPSTrackRef",                 // ASCII (2)
    0x000F : "GPSTrack",                    // rational
    0x0010 : "GPSImgDirectionRef",          // ASCII (2)
    0x0011 : "GPSImgDirection",             // rational
    0x0012 : "GPSMapDatum",                 // ASCII (any)
    0x0013 : "GPSDestLatitudeRef",          // ASCII (2)
    0x0014 : "GPSDestLatitude",             // rational (3)
    0x0015 : "GPSDestLongitudeRef",         // ASCII (2)
    0x0016 : "GPSDestLongitude",            // rational (3)
    0x0017 : "GPSDestBearingRef",           // ASCII (2)
    0x0018 : "GPSDestBearing",              // rational
    0x0019 : "GPSDestDistanceRef",          // ASCII (2)
    0x001A : "GPSDestDistance",             // rational
    0x001B : "GPSProcessingMethod",         // undefined (any)
    0x001C : "GPSAreaInformation",          // undefined (any)
    0x001D : "GPSDateStamp",                // ASCII (11)
    0x001E : "GPSDifferential",             // int16u (short) 
    0x001F : "GPSHPositioningError"         // rational
  };

  // ----------- exif 2.32 spec page 81 -------
  // https://www.cipa.jp/std/documents/download_e.html?DC-008-Translation-2019-E
  //
  var InteropTags = EXIF.InteropTags = {
    0x0001 : "InteroperabilityIndex"        // ASCII (any)
    //0x0002 : "InteroperabilityVersion"      // undefined (4) (fehlt in 2.3.2)
  }

  var StringValues = 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 dec2hex (d, padding) {
    var hex = Number (d).toString (16).toUpperCase ();
    padding = typeof (padding) === "undefined" || padding === null ? padding = 2 : padding;
    while (hex.length < padding)    hex = "0" + hex;
    return (hex);
  }

  function imageHasExif (img) {
    if (typeof img.exifdata == "undefined" || !img.exifdata)      return ("");
    return (img.exifdata);
  }

  function base64ToArrayBuffer (base64, contentType) {
    // e.g. 'data:image/jpeg;base64,...' => 'image/jpeg'
    contentType = contentType || base64.match (/^data\:([^\;]+)\;base64,/mi)[1] || '';
    base64 = base64.replace (/^data\:([^\;]+)\;base64,/gmi, '');
    var binary = atob (base64);
    var len = binary.length;
    var buffer = new ArrayBuffer (len);
    var view = new Uint8Array (buffer);
    for (var i = 0; i < len; i++) {
      view[i] = binary.charCodeAt (i);
    }
    return (buffer);
  }

  function objectURLToBlob (url, callback) {
    var http = new XMLHttpRequest ();
    http.open ("GET", url, true);
    http.responseType = "blob";
    http.onload = function () {
      if (this.status == 200 || this.status === 0) {
        callback (this.response);
      }
    };
    http.onerror = function (e) {
      alert ("XMLHttpRequest-error reading image metadata: " + e.error);
    };
    http.send ();
  }

  function getImageData (img, callback) {
    function handleBinaryFile (binFile) {       // onload
      var exifdata = findEXIFinJPEG (binFile);
      var iptcdata = findIPTCinJPEG (binFile);
      if (typeof (iptcdata) == "undefined") {
        iptcdata = "";
      }
      var xmpdata = findXMPinJPEG (binFile);    // kompletter XMP Header
      if (typeof (xmpdata) == "undefined") {
        xmpdata = "";
      }
      img.exifdata = exifdata || {};
      img.iptcdata = iptcdata || {};
      img.xmpdata = xmpdata || {};              // kompletter XMP Header
      if (callback) {
        callback.call (img);                    // callback
      }
    }
    if (img.src) {
      if (/^data\:/i.test (img.src)) {          // Data URI
        var arrayBuffer = base64ToArrayBuffer (img.src);
        if (_debug)                             debug ("Data URI: img.src: " + img.src);
        handleBinaryFile (arrayBuffer);
      } else if (/^blob\:/i.test (img.src)) {   // Object URL
        if (_debug)                             debug ("Object URL: img.src: " + img.src);
        var fileReader = new FileReader ();
        fileReader.onload = function (e) {
          if (_debug)                           debug ("onload Object URL: Got file of length: " + e.target.result.byteLength);
          handleBinaryFile (e.target.result);
        };
        fileReader.onerror = function () {
          alert ("Could not load image header: img.src: " + img.src);
        };
        objectURLToBlob (img.src, function (blob) {
          fileReader.readAsArrayBuffer (blob);
        });
      } else {                                  // img.src
        var http = new XMLHttpRequest ();
        http.onload = function () {
          if (this.status == 200 || this.status === 0) {
            if (_debug)                         debug ("onload XMLHttpRequest: Got file of length: " + http.response.byteLength);
            handleBinaryFile (http.response);
          } else {
            throw ("onload XMLHttpRequest: Could not load image header:\nimg.src: " + img.src);
          }
          http = null;
        };
        http.onerror = function () {
          alert ("Could not load image header: \n" + img.src +
                  ".\n\n\"cross-origin-request-error\"" +
                  "\nevtl. solved with Browser Plugin ?");
        };
        http.open ("GET", img.src, true);
        http.responseType = "arraybuffer";
        http.send (null);
      }
    } else if (window.FileReader && (img instanceof window.Blob || img instanceof window.File)) {
      var fileReader = new FileReader ();
      fileReader.onload = function (e) {
        if (_debug)                       debug ("onload Blob or File: Got file of length: " + e.target.result.byteLength);
        handleBinaryFile (e.target.result);
      };
      fileReader.onerror = function () {
        alert ("Could not load image header");
      };
      fileReader.readAsArrayBuffer (img);
    }
  }

  // Chrome auf Android hat Problem wenn Datensparmodus aktiviert ist :-(
  // Datensparmodus muss in den Einstellungen deaktiviert werden
  //
  function findEXIFinJPEG (file) {        // EXIF marker 0xFFE1 suchen
    var b1, b2;
    var app0 = false;
    var dataView = new DataView (file);
    if (_debug)                           debug ("EXIF: Got file of length: " + file.byteLength);
    b1 = dataView.getUint8 (0);
    b2 = dataView.getUint8 (1);
    if ((b1 != 0xFF) || (b2 != 0xD8)) {   // FFD8: start of compressed data
      if (_debug)                         debug ("Not a valid JPEG (FFD8): " + dec2hex (b1) + dec2hex (b2));
      return ("");                        // not a valid jpeg
    }
    if (_debug)                           debug ("valid JPEG: FFD8");
    var offset = 2;
    var length = file.byteLength;
    var marker;
    while (offset < length) {
      b1 = dataView.getUint8 (offset);
      if (b1 != 0xFF) {
        if (_debug)                       debug ("Not a valid marker FF at offset: " + offset + ": " + dec2hex (b1));
        break;
      }
      marker = dataView.getUint8 (offset + 1);
      if (marker == 0xE0) {               // APP0: start JFIF attributes
        app0 = true;
      } if (marker == 0xE1) {             // APP1: start EXIF attributes
        if (_debug)                       debug ("APP1: valid marker at offset: " + offset + ": FFE1");
        return (readEXIFData (dataView, offset + 4, dataView.getUint16 (offset + 2) - 2));
        // function only has 2 parameters ??
        // offset += 2 + file.getShortAt (offset+2, true);
      } else {
        if (_debug)                       debug ("Not a valid marker E1 at offset: " + (offset+1) + ": " + dec2hex (marker));
        offset += (2 + dataView.getUint16 (offset + 2));
      }
    }
    if (_debug)                           debug ("no EXIF marker FFE1 found");
    if (app0 && navigator.userAgent.indexOf ("Android") > 0 && navigator.userAgent.indexOf ("Chrome") > 0) {
      if (_debug)                         debug ("Error: deaktivate Data Saver (Datensparmodus) in the browser !!!");
      return ("ChromeErr");
    }
    return ("");                          // kein EXIF marker gefunden
  }

  function findIPTCinJPEG (file) {        // IPTC marker suchen
    var b1, b2;
    var dataView = new DataView (file);
    if (_debug)                           debug ("IPTC: Got file of length " + file.byteLength);
    b1 = dataView.getUint8 (0);
    b2 = dataView.getUint8 (1);
    if ((b1 != 0xFF) || (b2 != 0xD8)) {   // FFD8: start of compressed data
      if (_debug)                         debug ("Not a valid JPEG (FFD8): " + dec2hex (b1) + dec2hex (b2));
      return ("");                        // not a valid jpeg
    }
    if (_debug)                           debug ("valid JPEG: FFD8");
    var offset = 2;
    var length = file.byteLength;
    var isFieldSegmentStart = function (dataView, offset) {
      return (
        dataView.getUint8 (offset) === 0x38 &&
        dataView.getUint8 (offset+1) === 0x42 &&
        dataView.getUint8 (offset+2) === 0x49 &&
        dataView.getUint8 (offset+3) === 0x4D &&
        dataView.getUint8 (offset+4) === 0x04 &&
        dataView.getUint8 (offset+5) === 0x04
      );
    };
    while (offset < length) {
      if (isFieldSegmentStart (dataView, offset)) {
        // Get the length of the name header (which is padded to an even number of bytes)
        var nameHeaderLength = dataView.getUint8 (offset + 7);
        if (nameHeaderLength % 2 !== 0)         nameHeaderLength += 1;
        // Check for pre photoshop 6 format
        if (nameHeaderLength === 0)             nameHeaderLength = 4;       // Always 4
        var startOffset = offset + 8 + nameHeaderLength;
        var sectionLength = dataView.getUint16 (offset + 6 + nameHeaderLength);
        return (readIPTCData (file, startOffset, sectionLength));
      } else {
        offset++;
      }
    }
    if (_debug)                       debug ("no IPTC marker found");
    return ("");                      // kein IPTC marker gefunden
  }

  var IptcFieldMap = {                // https://www.exiv2.org/iptc.html     Iptc.Application2.xxxxx
    0x00 : 'RecordVersion',           // int16u (short)              IPTC Version
    0x03 : 'ObjectType',              // string (3 - 67)
    0x04 : 'ObjectAttribute',         // string (4 - 68)
    0x05 : 'ObjectName',              // string (0 - 64)    Titel
    0x07 : 'EditStatus',              // string (0 - 64)
    0x08 : 'EditorialUpdate',         // string (2)
    0x0A : 'Urgency',                 // string (1)
    0x0C : 'Subject',                 // string (13 - 236)
    0x0F : 'Category',                // string (0 - 3)
    0x14 : 'SuppCategory',            // string (0 - 32)
    0x16 : 'FixtureId',               // string (0 - 32)
    0x19 : 'Keywords',                // string (0 - 64)
    0x1A : 'LocationCode',            // string (3)
    0x1B : 'LocationName',            // string (0 - 64)
    0x1E : 'ReleaseDate',             // string (8)
    0x23 : 'ReleaseTime',             // string (11)
    0x25 : 'ExpirationDate',          // string (8)
    0x26 : 'ExpirationTime',          // string (11)
    0x28 : 'SpecialInstructions',     // string (0 - 256)
    0x2A : 'ActionAdvised',           // string (2)
    0x2D : 'ReferenceService',        // string (0 - 10)
    0x2F : 'ReferenceDate',           // string (8)
    0x32 : 'ReferenceNumber',         // string (8)
    0x37 : 'DateCreated',             // string (8)         Aufnahmedatum
    0x3C : 'TimeCreated',             // string (11)        Aufnahmezeit
    0x3E : 'DigitizationDate',        // string (8)         Digitalisierungsdatum
    0x3F : 'DigitizationTime',        // string (11)        Digitalisierungszeit
    0x41 : 'Program',                 // string (0 - 32)    Foto-Programm
    0x46 : 'ProgramVersion',          // string (0 - 10)    Foto-Programm
    0x4B : 'ObjectCycle',             // string (1)
    0x50 : 'Byline',                  // string (0 - 32)    Author
    0x55 : 'BylineTitle',             // string (0 - 32)    Author
    0x5A : 'City',                    // string (0 - 32)
    0x5C : 'SubLocation',             // string (0 - 32)
    0x5F : 'ProvinceState',           // string (0 - 32)
    0x64 : 'CountryCode',             // string (3)
    0x65 : 'CountryName',             // string (0 - 64)
    0x67 : 'TransmissionReference',   // string (0 - 32)
    0x69 : 'Headline',                // string (0 - 256)   Überschrift
    0x6E : 'Credit',                  // string (0 - 32)
    0x73 : 'Source',                  // string (0 - 32)
    0x74 : 'Copyright',               // string (0 - 128)   Copyright
    0x76 : 'Contact',                 // string (0 - 128)
    0x78 : 'Caption',                 // string (0 - 2000)  Bildbeschreibung (caption, description)
    0x7A : 'Writer',                  // string (0 - 32)
    0x7D : 'RasterizedCaption',       // undefined (7360)
    0x82 : 'ImageType',               // string (2)
    0x83 : 'ImageOrientation',        // string (1)
    0x87 : 'Language',                // string (2 - 3)
    0x96 : 'AudioType',               // string (2)
    0x97 : 'AudioRate',               // string (6)
    0x98 : 'AudioResolution',         // string (2)
    0x99 : 'AudioDuration',           // string (6)
    0x9A : 'AudioOutcue',             // string (0 - 64)
    0xC8 : 'PreviewFormat',           // string (2)
    0xC9 : 'PreviewVersion',          // string (2)
    0xCA : 'Preview'                  // undefined (0 - 256.000)
  };

  // Original hab ich nicht zum Laufen gebracht :-(
  // Daher übergebe ich den kompletten XMP-Header, der kompetente Anwender
  // darf sich selbst rausholen, was er haben will
  // peter@danninger.eu
  //
  function findXMPinJPEG (file) {
    var b1, b2;
    var dataView = new DataView (file);
    if (_debug)                           debug ("XMP: Got file of length " + file.byteLength);
    b1 = dataView.getUint8 (0);
    b2 = dataView.getUint8 (1);
    if ((b1 != 0xFF) || (b2 != 0xD8)) {   // FFD8: start of compressed data
      if (_debug)                         debug ("Not a valid JPEG (FFD8): " + dec2hex (b1) + dec2hex (b2));
      return ("");                        // not a valid jpeg
    }
    var xmpstart = "<x:xmpmeta";          // Start XMP header
    var xmpl = xmpstart.length;
    var xmpstart1 = "<x";                 // wegen Performance
    var offset = 2;
    var length = file.byteLength - xmpl;
    while (offset < length) {
      if (getStringFromDB (dataView, offset, 2) != xmpstart1)         { offset++; continue; }
      if (getStringFromDB (dataView, offset, xmpl) != xmpstart)       { offset++; continue; }
      var startOffset = offset - 1;
      var sectionLength = dataView.getUint16 (offset - 2) - 1;
      var xmpString = getStringFromDB (dataView, startOffset, sectionLength)
      var xmpEndIndex = xmpString.indexOf ('xmpmeta>') + 8;
      xmpString = xmpString.substring (xmpString.indexOf ('<x:xmpmeta'), xmpEndIndex);
      return (xmpString);                   // kompletter XMP Header  peter@danninger.eu
    }
    if (_debug)                       debug ("no XMP marker found");
    return ("");
  }

//
// Bild zum Auswerten übergeben
//
  EXIF.getData = function (img, callback) {
    //if ((img instanceof Image || img instanceof HTMLImageElement) && !img.complete)     return (false);
    if (_debug)                             debug ("EXIF.getData: " + img.src);
    if (imageHasExif (img) == "") {
      getImageData (img, callback);
    } else {
      if (callback) {
        callback.call (img);
      }
    }
    return (true);
  }

  EXIF.imageHasExif = function (img) {
    return (imageHasExif (img));
  }

  function readIPTCData (file, startOffset, sectionLength) {
    var dataView = new DataView (file);
    var data = {};
    var fieldValue, fieldName, dataSize, segmentType, segmentSize;
    var segmentStartPos = startOffset;
    var trace = "";
    if (_trace)     trace += "readIPTCData:   " + startOffset + "   " + sectionLength + "\n";
    while (segmentStartPos < startOffset + sectionLength) {
      if (dataView.getUint8 (segmentStartPos) === 0x1C && dataView.getUint8 (segmentStartPos+1) === 0x02){
        segmentType = dataView.getUint8 (segmentStartPos+2);
        if (segmentType in IptcFieldMap) {
          dataSize = dataView.getInt16 (segmentStartPos+3);
          segmentSize = dataSize + 5;
          fieldName = IptcFieldMap[segmentType];
          if (fieldName == "RecordVersion") {           // short    int16u
            fieldValue = dataView.getUint16 (segmentStartPos+5);
          } else if (fieldName == "RasterizedCaption" || fieldName == "Preview") {    // these binaries we ignore !
            fieldValue = "";
          } else {                                      // all other tags are strings
            fieldValue = getStringFromDB (dataView, segmentStartPos+5, dataSize);
          }
          trace += ("\n " + fieldName + ":   " + fieldValue);
          // Check if we already stored a value with this name
          if (data.hasOwnProperty (fieldName)) {
            // Value already stored with this name, create multivalue field
            if (data[fieldName] instanceof Array) {
              data[fieldName].push (fieldValue);
            }
            else {
              data[fieldName] = [data[fieldName], fieldValue];
            }
          }
          else {
            data[fieldName] = fieldValue;
          }
        }
      }
      segmentStartPos++;
    }
    _allIPTC = trace;
    if (_trace)         alert (trace);
    return (data);
  }

  function readTags (file, tiffStart, dirStart, strings, bigEnd, info) {
    var entries = file.getUint16 (dirStart, !bigEnd);
    var tags = {};
    var entryOffset, tag;
    var i, tv;
    var trace = "readTags  ("+info+") :   "+tiffStart+"   "+dirStart+"   "+strings+"   "+bigEnd+"\n";
    for (i = 0; i < entries; i++) {
      entryOffset = dirStart + i*12 + 2;
      tag = strings[file.getUint16 (entryOffset, !bigEnd)];
      if (!tag && _debug)   debug ("unknown tag:  0x"+file.getUint16 (entryOffset, !bigEnd).toString(16)+
                                    " :   "+readTagValue (file, entryOffset, tiffStart, dirStart, bigEnd, tag));
      tv = readTagValue (file, entryOffset, tiffStart, dirStart, bigEnd, tag);
      tags[tag] = tv[0];
      trace += ("\n " + i + ":  " + tag + "     "  +tags[tag] + "     (" + tv[1] + ")");
    }
    if (_trace)         alert (trace);
    return (tags);
  }

  function readTagValue (file, entryOffset, tiffStart, dirStart, bigEnd, tag) {
    var type = file.getUint16 (entryOffset + 2, !bigEnd);
    var numValues = file.getUint32 (entryOffset + 4, !bigEnd);
    var valueOffset = file.getUint32 (entryOffset + 8, !bigEnd) + tiffStart;
    var offset;
    var vals, val, n;
    var numerator, denominator;
    var rv = new Array ();
    switch (type) {
      case 1:           // byte, 8-bit unsigned int
      case 7:           // undefined, 8-bit byte, value depending on field
        rv[1] = (type == 1) ? "Byte" : "Undefined Byte";
        if (numValues == 1) {
          rv[0] = val = file.getUint8 (entryOffset + 8, !bigEnd);
        } else {
          offset = numValues > 4 ? valueOffset : (entryOffset + 8);
          vals = [];
          for (n = 0; n < numValues; n++) {
            vals[n] = file.getUint8 (offset + n);
          }
          rv[0] = vals;
        }
        break;
      //
      case 2:           // ascii, 8-bit byte
        rv[1] = "ASCII char";
        offset = numValues > 4 ? valueOffset : (entryOffset + 8);
        rv[0] = val = getStringFromDB (file, offset, numValues-1);
        break;
      //
      case 3:           // short, 16 bit unsigned int
        rv[1] = "Short, 16 Bit unsigned Int";
        if (numValues == 1) {
          rv[0] = val = file.getUint16 (entryOffset + 8, !bigEnd);
        } else {
          offset = numValues > 2 ? valueOffset : (entryOffset + 8);
          vals = [];
          for (n = 0; n < numValues; n++) {
            vals[n] = file.getUint16 (offset + 2*n, !bigEnd);
          }
          rv[0] = vals;
        }
        break;
      //
      case 4:           // long, 32 bit unsigned int
        rv[1] = "Long, 32 Bit unsigned Int";
        if (numValues == 1) {
          rv[0] = val = file.getUint32 (entryOffset + 8, !bigEnd);
        } else {
          vals = [];
          for (n = 0; n < numValues; n++) {
            vals[n] = file.getUint32 (valueOffset + 4*n, !bigEnd);
          }
          rv[0] = vals;
        }
        break;
      //
      case 5:           // rational = two long values, first is numerator, second is denominator
        rv[1] = "Rational, 2 * Long";
        if (numValues == 1) {
          numerator = file.getUint32 (valueOffset, !bigEnd);
          denominator = file.getUint32 (valueOffset+4, !bigEnd);
          val = new Number (numerator / denominator);
          val.numerator = numerator;
          val.denominator = denominator;
          rv[0] = val;
        } else {
          vals = [];
          for (n = 0; n < numValues; n++) {
            numerator = file.getUint32 (valueOffset + 8*n, !bigEnd);
            denominator = file.getUint32 (valueOffset + 4 + 8*n, !bigEnd);
            vals[n] = new Number (numerator / denominator);
            vals[n].numerator = numerator;
            vals[n].denominator = denominator;
          }
          rv[0] = vals;
        }
        break;
      //
      case 9:           // slong, 32 bit signed int
        rv[1] = "SLong, 32 Bit signed Int";
        if (numValues == 1) {
          rv[0] = val = file.getInt32 (entryOffset + 8, !bigEnd);
        } else {
          vals = [];
          for (n = 0; n < numValues; n++) {
            vals[n] = file.getInt32 (valueOffset + 4*n, !bigEnd);
          }
          rv[0] = vals;
        }
        break;
      //
      case 10:          // srational, two slongs, first is numerator, second is denominator
        rv[1] = "SRational, 2 * SLong";
        if (numValues == 1) {
          rv[0] = val = file.getInt32 (valueOffset, !bigEnd) / file.getInt32 (valueOffset+4, !bigEnd);
        } else {
          vals = [];
          for (n = 0; n < numValues; n++) {
            vals[n] = file.getInt32 (valueOffset + 8*n, !bigEnd) / file.getInt32 (valueOffset + 4 + 8*n, !bigEnd);
          }
          rv[0] = vals;
        }
        break;
      //
    }
    if (_debugAv)   debug ("readTagValue:   tag: " + tag + "   type: " + rv[1] + "   value: " + rv);
    return (rv);
  }

  function getStringFromDB (buffer, start, length) {
    var outstr = "";
    for (n = start; n < start+length; n++) {
      outstr += String.fromCharCode (buffer.getUint8 (n));
    }
    return (outstr);
  }

  function readEXIFData (file, start) {
    var i, l, s;
    if (getStringFromDB (file, start, 4) != "Exif") {
      if (_debug)                   debug ("Not valid EXIF data! " + getStringFromDB (file, start, 4));
      return (false);
    }
    var bigEnd, tags, tag, exifData, gpsData, interopData;
    var tiffOffset = start + 6;
    // test for TIFF validity and endianness
    if (file.getUint16 (tiffOffset) == 0x4949) {
      bigEnd = false;
    } else if (file.getUint16 (tiffOffset) == 0x4D4D) {
      bigEnd = true;
    } else {
      if (_debug)                   debug ("Not valid TIFF data! (no 0x4949 or 0x4D4D)");
      return (false);
    }
    if (file.getUint16 (tiffOffset+2, !bigEnd) != 0x002A) {
      if (_debug)                   debug ("Not valid TIFF data! (no 0x002A)");
      return (false);
    }
    var firstIFDOffset = file.getUint32 (tiffOffset+4, !bigEnd);
    if (firstIFDOffset < 0x00000008) {
      if (_debug)                   debug ("Not valid TIFF data! (First offset less than 8)", file.getUint32 (tiffOffset+4, !bigEnd));
      return (false);
    }
    tags = readTags (file, tiffOffset, tiffOffset + firstIFDOffset, TiffTags, bigEnd, "TiffTags");
    if (tags.ExifIFDPointer) {
      exifData = readTags (file, tiffOffset, tiffOffset + tags.ExifIFDPointer, ExifTags, bigEnd, "ExifTags");
      var trace = "";
      if (_trace)       trace += "readEXIFData:\n";
      for (tag in exifData) {
        switch (tag) {
          // tags with translation by separate StringValues[] Arrays
          case "ExposureProgram" :
          case "MeteringMode" :
          case "LightSource" :
          case "Flash" :
          case "SensingMethod" :
          case "SceneCaptureType" :
          case "SceneType" :
          case "CustomRendered" :
          case "WhiteBalance" :
          case "GainControl" :
          case "Contrast" :
          case "Saturation" :
          case "Sharpness" :
          case "SubjectDistanceRange" :
          case "FileSource" :
            exifData[tag] = StringValues[tag][exifData[tag]];
            break;
          // 4 entries
          case "ComponentsConfiguration" :
            exifData[tag] = StringValues.Components[exifData[tag][0]] +
                            StringValues.Components[exifData[tag][1]] +
                            StringValues.Components[exifData[tag][2]] +
                            StringValues.Components[exifData[tag][3]];
            break;
          // short or long
          case "PixelXDimension" :            //
          case "PixelYDimension" :            //
            break;
          // rational
          case "SubjectDistance" :            // rational: Numerator: FFFFFFFF.H: infinity / 0: distance unknown
            s = exifData[tag];
            if (s.numerator && s.denominator) {
              if (s.numerator == 0)                   s = "unknown";
              else if (s.numerator == 0xFFFFFFFF)     s = "infinity";
              else                                    s = Math.floor (s.numerator / s.denominator);
            }
            exifData[tag] = s;
            break;
          // more entries
          case "ExifVersion" :
          case "FlashpixVersion" :
          case "UserComment" :
            l = exifData[tag].length;
            s = "";
            i = (tag == "UserComment") ? 8 : 0;
            for ( ; i < l; ++i) {
              s += String.fromCharCode (exifData[tag][i]);
            }
            exifData[tag] = s;
            break;
          // all other tags without special handling
          default:
            break;
        }
        trace += ("\n " + tag + ":   " + exifData[tag]);
        tags[tag] = exifData[tag];
      }
    }
    if (tags.GPSInfoIFDPointer) {
      gpsData = readTags (file, tiffOffset, tiffOffset + tags.GPSInfoIFDPointer, GPSTags, bigEnd, "GPSTags");
      for (tag in gpsData) {
        switch (tag) {
          case "GPSVersionID" :
            gpsData[tag] = gpsData[tag][0] +
              "." + gpsData[tag][1] +
              "." + gpsData[tag][2] +
              "." + gpsData[tag][3];
            break;
        }
        trace += ("\n " + tag + ":   " + gpsData[tag]);
        tags[tag] = gpsData[tag];
      }
    }
    if (tags.InteroperabilityIFDPointer) {
      interopData = readTags (file, tiffOffset, tiffOffset + tags.InteroperabilityIFDPointer, InteropTags, bigEnd, "InteroperabilityTags");
      for (tag in interopData) {
        switch (tag) {
          case "InteroperabilityIndex" :        // ASCII (any)
            l = interopData[tag].length;
            s = "";
            for (i = 0; i < l; ++i) {
              s += interopData[tag][i];
            }
            interopData[tag] = s;
            break;
            /*
          case "InteroperabilityVersion" :      // undefined (4) (fehlt in 2.3.2)
            l = interopData[tag].length;
            s = "";
            for (i = 0; i < l; ++i) {
              s += String.fromCharCode (interopData[tag][i]);
            }
            interopData[tag] = s;
            break;
            */
        }
        trace += ("\n " + tag + ":   " + interopData[tag]);
        tags[tag] = interopData[tag];
      }
    }
    if (_trace)         alert (trace);
    _allEXIF = trace;
    return (tags);
  }

//
// EXIF Tag extrahieren
//
  EXIF.getExifTag = function (img, tag) {
    if (imageHasExif (img) == "")           return ("");
    return (img.exifdata[tag]);
  }

//
// alle EXIF Tags extrahieren
//
  EXIF.getExifAll = function (img) {
    if (imageHasExif (img) == "")           return ("");
    if (imageHasExif (img) == "ChromeErr")  return ("ChromeErr");
    return (_allEXIF);
  }

//
// IPTC Tag extrahieren               ergänzt peter@danninger.eu
//
  EXIF.getIptcTag = function (img, tag) {
    if (img.iptcdata == null || img.iptcdata == "")     return ("");
    return (img.iptcdata[tag]);
  }

//
// alle IPTC Tags extrahieren         ergänzt peter@danninger.eu
//
  EXIF.getIptcAll = function (img) {
    if (img.iptcdata == null || img.iptcdata == "")     return ("");
    return (_allIPTC);
  }

//
// kompletten XMP Header extrahieren         ergänzt peter@danninger.eu
//
  EXIF.getXmpHeader = function (img) {
    if (img.xmpdata == null || img.xmpdata == "")       return ("");
    return (img.xmpdata);
  }

  debug = function (txt) {
    if (_console)       console.log (txt);
    else                alert (txt);
  }

/*
  EXIF.getAllTags = function (img) {
    if (imageHasExif (img) == "")       return {};
    var a;
    var data = img.exifdata;
    var tags = {};
    for (a in data) {
      if (data.hasOwnProperty (a)) {
        tags[a] = data[a];
      }
    }
    return (tags);
  }

  function addEvent (element, event, handler) {
    if (element.addEventListener) {
      element.addEventListener (event, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent ("on" + event, handler);
    }
  }

  EXIF.pretty = function (img) {
    if (imageHasExif (img) == "")     return ("");
    var a;
    var data = img.exifdata;
    var strPretty = "";
    for (a in data) {
      if (data.hasOwnProperty (a)) {
        if (typeof data[a] == "object") {
          if (data[a] instanceof Number) {
            strPretty += a + " : " + data[a] + " [" + data[a].numerator + "/" + data[a].denominator + "]\r\n";
          } else {
            strPretty += a + " : [" + data[a].length + " values]\r\n";
          }
        } else {
          strPretty += a + " : " + data[a] + "\r\n";
        }
      }
    }
    return (strPretty);
  }

  EXIF.readFromBinaryFile = function (file) {
    return (findEXIFinJPEG (file));
  }

  if (typeof define === 'function' && define.amd) {
    define ('exif-js', [], function () {
      return (EXIF);
    });
  }
*/

}.call (this));

//
// the end
//
