text: /**
   * plupload.html5.js
   *
   * Copyright 2009, Moxiecode Systems AB
   * Released under GPL License.
   *
   * License: http://www.plupload.com/license
   * Contributing: http://www.plupload.com/contributing
   */
  
  // JSLint defined globals
  /*global plupload:false, File:false, window:false, atob:false, FormData:false, FileReader:false, ArrayBuffer:false, Uint8Array:false, BlobBuilder:false, unescape:false */
  
  (function(window, document, plupload, undef) {
    var html5files = {}, // queue of original File objects
      fakeSafariDragDrop;
  
    function readFileAsDataURL(file, callback) {
      var reader;
  
      // Use FileReader if it's available
      if ("FileReader" in window) {
        reader = new FileReader();
        reader.readAsDataURL(file);
        reader.onload = function() {
          callback(reader.result);
        };
      } else {
        return callback(file.getAsDataURL());
      }
    }
  
    function readFileAsBinary(file, callback) {
      var reader;
  
      // Use FileReader if it's available
      if ("FileReader" in window) {
        reader = new FileReader();
        reader.readAsBinaryString(file);
        reader.onload = function() {
          callback(reader.result);
        };
      } else {
        return callback(file.getAsBinary());
      }
    }
  
    function scaleImage(file, resize, mime, callback) {
      var canvas, context, img, scale,
        up = this;
  
      readFileAsDataURL(html5files[file.id], function(data) {
        // Setup canvas and context
        canvas = document.createElement("canvas");
        canvas.style.display = 'none';
        document.body.appendChild(canvas);
        context = canvas.getContext('2d');
  
        // Load image
        img = new Image();
        img.onerror = img.onabort = function() {
          // Failed to load, the image may be invalid
          callback({success : false});
        };
        img.onload = function() {
          var width, height, percentage, jpegHeaders, exifParser;
  
          if (!resize['width']) {
            resize['width'] = img.width;
          }
  
          if (!resize['height']) {
            resize['height'] = img.height;
          }
  
          scale = Math.min(resize.width / img.width, resize.height / img.height);
  
          if (scale < 1 || (scale === 1 && mime === 'image/jpeg')) {
            width = Math.round(img.width * scale);
            height = Math.round(img.height * scale);
  
            // Scale image and canvas
            canvas.width = width;
            canvas.height = height;
            context.drawImage(img, 0, 0, width, height);
  
            // Preserve JPEG headers
            if (mime === 'image/jpeg') {
              jpegHeaders = new JPEG_Headers(atob(data.substring(data.indexOf('base64,') + 7)));
              if (jpegHeaders['headers'] && jpegHeaders['headers'].length) {
                exifParser = new ExifParser();
  
                if (exifParser.init(jpegHeaders.get('exif')[0])) {
                  // Set new width and height
                  exifParser.setExif('PixelXDimension', width);
                  exifParser.setExif('PixelYDimension', height);
  
                  // Update EXIF header
                  jpegHeaders.set('exif', exifParser.getBinary());
  
                  // trigger Exif events only if someone listens to them
                  if (up.hasEventListener('ExifData')) {
                    up.trigger('ExifData', file, exifParser.EXIF());
                  }
  
                  if (up.hasEventListener('GpsData')) {
                    up.trigger('GpsData', file, exifParser.GPS());
                  }
                }
              }
  
              if (resize['quality']) {
                // Try quality property first
                try {
                  data = canvas.toDataURL(mime, resize['quality'] / 100);
                } catch (e) {
                  data = canvas.toDataURL(mime);
                }
              }
            } else {
              data = canvas.toDataURL(mime);
            }
  
            // Remove data prefix information and grab the base64 encoded data and decode it
            data = data.substring(data.indexOf('base64,') + 7);
            data = atob(data);
  
            // Restore JPEG headers if applicable
            if (jpegHeaders && jpegHeaders['headers'] && jpegHeaders['headers'].length) {
              data = jpegHeaders.restore(data);
              jpegHeaders.purge(); // free memory
            }
  
            // Remove canvas and execute callback with decoded image data
            canvas.parentNode.removeChild(canvas);
            callback({success : true, data : data});
          } else {
            // Image does not need to be resized
            callback({success : false});
          }
        };
  
        img.src = data;
      });
    }
  
    /**
     * HMTL5 implementation. This runtime supports these features: dragdrop, jpgresize, pngresize.
     *
     * @static
     * @class plupload.runtimes.Html5
     * @extends plupload.Runtime
     */
    plupload.runtimes.Html5 = plupload.addRuntime("html5", {
      /**
       * Returns a list of supported features for the runtime.
       *
       * @return {Object} Name/value object with supported features.
       */
      getFeatures : function() {
        var xhr, hasXhrSupport, hasProgress, canSendBinary, dataAccessSupport, sliceSupport;
  
        hasXhrSupport = hasProgress = dataAccessSupport = sliceSupport = false;
  
        if (window.XMLHttpRequest) {
          xhr = new XMLHttpRequest();
          hasProgress = !!xhr.upload;
          hasXhrSupport = !!(xhr.sendAsBinary || xhr.upload);
        }
  
        // Check for support for various features
        if (hasXhrSupport) {
          canSendBinary = !!(xhr.sendAsBinary || (window.Uint8Array && window.ArrayBuffer));
  
          // Set dataAccessSupport only for Gecko since BlobBuilder and XHR doesn't handle binary data correctly
          dataAccessSupport = !!(File && (File.prototype.getAsDataURL || window.FileReader) && canSendBinary);
          sliceSupport = !!(File && (File.prototype.mozSlice || File.prototype.webkitSlice || File.prototype.slice));
        }
  
        // sniff out Safari for Windows and fake drag/drop
        fakeSafariDragDrop = plupload.ua.safari && plupload.ua.windows;
  
        return {
          html5: hasXhrSupport, // This is a special one that we check inside the init call
          dragdrop: (function() {
            // this comes directly from Modernizr: http://www.modernizr.com/
            var div = document.createElement('div');
            return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
          }()),
          jpgresize: dataAccessSupport,
          pngresize: dataAccessSupport,
          multipart: dataAccessSupport || !!window.FileReader || !!window.FormData,
          canSendBinary: canSendBinary,
          // gecko 2/5/6 can't send blob with FormData: https://bugzilla.mozilla.org/show_bug.cgi?id=649150
          cantSendBlobInFormData: !!(plupload.ua.gecko && window.FormData && window.FileReader && !FileReader.prototype.readAsArrayBuffer),
          progress: hasProgress,
          chunks: sliceSupport,
          // Safari on Windows has problems when selecting multiple files
          multi_selection: !(plupload.ua.safari && plupload.ua.windows),
          // WebKit and Gecko 2+ can trigger file dialog progrmmatically
          triggerDialog: (plupload.ua.gecko && window.FormData || plupload.ua.webkit)
        };
      },
  
      /**
       * Initializes the upload runtime.
       *
       * @method init
       * @param {plupload.Uploader} uploader Uploader instance that needs to be initialized.
       * @param {function} callback Callback to execute when the runtime initializes or fails to initialize. If it succeeds an object with a parameter name success will be set to true.
       */
      init : function(uploader, callback) {
        var features, xhr;
  
        function addSelectedFiles(native_files) {
          var file, i, files = [], id, fileNames = {};
  
          // Add the selected files to the file queue
          for (i = 0; i < native_files.length; i++) {
            file = native_files[i];
  
            // Safari on Windows will add first file from dragged set multiple times
            // @see: https://bugs.webkit.org/show_bug.cgi?id=37957
            if (fileNames[file.name]) {
              continue;
            }
            fileNames[file.name] = true;
  
            // Store away gears blob internally
            id = plupload.guid();
            html5files[id] = file;
  
            // Expose id, name and size
            files.push(new plupload.File(id, file.fileName || file.name, file.fileSize || file.size)); // fileName / fileSize depricated
          }
  
          // Trigger FilesAdded event if we added any
          if (files.length) {
            uploader.trigger("FilesAdded", files);
          }
        }
  
        // No HTML5 upload support
        features = this.getFeatures();
        if (!features.html5) {
          callback({success : false});
          return;
        }
  
        uploader.bind("Init", function(up) {
          var inputContainer, browseButton, mimes = [], i, y, filters = up.settings.filters, ext, type, container = document.body, inputFile;
  
          // Create input container and insert it at an absolute position within the browse button
          inputContainer = document.createElement('div');
          inputContainer.id = up.id + '_html5_container';
  
          plupload.extend(inputContainer.style, {
            position : 'absolute',
            background : uploader.settings.shim_bgcolor || 'transparent',
            width : '100px',
            height : '100px',
            overflow : 'hidden',
            zIndex : 99999,
            opacity : uploader.settings.shim_bgcolor ? '' : 0 // Force transparent if bgcolor is undefined
          });
          inputContainer.className = 'plupload html5';
  
          if (uploader.settings.container) {
            container = document.getElementById(uploader.settings.container);
            if (plupload.getStyle(container, 'position') === 'static') {
              container.style.position = 'relative';
            }
          }
  
          container.appendChild(inputContainer);
  
          // Convert extensions to mime types list
          no_type_restriction:
          for (i = 0; i < filters.length; i++) {
            ext = filters[i].extensions.split(/,/);
  
            for (y = 0; y < ext.length; y++) {
  
              // If there's an asterisk in the list, then accept attribute is not required
              if (ext[y] === '*') {
                mimes = [];
                break no_type_restriction;
              }
  
              type = plupload.mimeTypes[ext[y]];
  
              if (type && plupload.inArray(type, mimes) === -1) {
                mimes.push(type);
              }
            }
          }
  
  
          // Insert the input inside the input container
          inputContainer.innerHTML = '<input id="' + uploader.id + '_html5" ' + ' style="font-size:999px"' +
                        ' type="file" accept="' + mimes.join(',') + '" ' +
                        (uploader.settings.multi_selection && uploader.features.multi_selection ? 'multiple="multiple"' : '') + ' />';
  
          inputContainer.scrollTop = 100;
          inputFile = document.getElementById(uploader.id + '_html5');
  
          if (up.features.triggerDialog) {
            plupload.extend(inputFile.style, {
              position: 'absolute',
              width: '100%',
              height: '100%'
            });
          } else {
            // shows arrow cursor instead of the text one, bit more logical
            plupload.extend(inputFile.style, {
              cssFloat: 'right',
              styleFloat: 'right'
            });
          }
  
          inputFile.onchange = function() {
            // Add the selected files from file input
            addSelectedFiles(this.files);
  
            // Clearing the value enables the user to select the same file again if they want to
            this.value = '';
          };
  
          /* Since we have to place input[type=file] on top of the browse_button for some browsers (FF, Opera),
          browse_button loses interactivity, here we try to neutralize this issue highlighting browse_button
          with a special classes
          TODO: needs to be revised as things will change */
          browseButton = document.getElementById(up.settings.browse_button);
          if (browseButton) {
            var hoverClass = up.settings.browse_button_hover,
              activeClass = up.settings.browse_button_active,
              topElement = up.features.triggerDialog ? browseButton : inputContainer;
  
            if (hoverClass) {
              plupload.addEvent(topElement, 'mouseover', function() {
                plupload.addClass(browseButton, hoverClass);
              }, up.id);
              plupload.addEvent(topElement, 'mouseout', function() {
                plupload.removeClass(browseButton, hoverClass);
              }, up.id);
            }
  
            if (activeClass) {
              plupload.addEvent(topElement, 'mousedown', function() {
                plupload.addClass(browseButton, activeClass);
              }, up.id);
              plupload.addEvent(document.body, 'mouseup', function() {
                plupload.removeClass(browseButton, activeClass);
              }, up.id);
            }
  
            // Route click event to the input[type=file] element for supporting browsers
            if (up.features.triggerDialog) {
              plupload.addEvent(browseButton, 'click', function(e) {
                var input = document.getElementById(up.id + '_html5');
                if (input && !input.disabled) { // for some reason FF (up to 8.0.1 so far) lets to click disabled input[type=file]
                  input.click();
                }
                e.preventDefault();
              }, up.id);
            }
          }
        });
  
        // Add drop handler
        uploader.bind("PostInit", function() {
          var dropElm = document.getElementById(uploader.settings.drop_element);
  
          if (dropElm) {
            // Lets fake drag/drop on Safari by moving a input type file in front of the mouse pointer when we drag into the drop zone
            // TODO: Remove this logic once Safari has official drag/drop support
            if (fakeSafariDragDrop) {
              plupload.addEvent(dropElm, 'dragenter', function(e) {
                var dropInputElm, dropPos, dropSize;
  
                // Get or create drop zone
                dropInputElm = document.getElementById(uploader.id + "_drop");
                if (!dropInputElm) {
                  dropInputElm = document.createElement("input");
                  dropInputElm.setAttribute('type', "file");
                  dropInputElm.setAttribute('id', uploader.id + "_drop");
                  dropInputElm.setAttribute('multiple', 'multiple');
  
                  plupload.addEvent(dropInputElm, 'change', function() {
                    // Add the selected files from file input
                    addSelectedFiles(this.files);
  
                    // Remove input element
                    plupload.removeEvent(dropInputElm, 'change', uploader.id);
                    dropInputElm.parentNode.removeChild(dropInputElm);
                  }, uploader.id);
  
                  dropElm.appendChild(dropInputElm);
                }
  
                dropPos = plupload.getPos(dropElm, document.getElementById(uploader.settings.container));
                dropSize = plupload.getSize(dropElm);
  
                if (plupload.getStyle(dropElm, 'position') === 'static') {
                  plupload.extend(dropElm.style, {
                    position : 'relative'
                  });
                }
  
                plupload.extend(dropInputElm.style, {
                  position : 'absolute',
                  display : 'block',
                  top : 0,
                  left : 0,
                  width : dropSize.w + 'px',
                  height : dropSize.h + 'px',
                  opacity : 0
                });
              }, uploader.id);
  
              return;
            }
  
            // Block browser default drag over
            plupload.addEvent(dropElm, 'dragover', function(e) {
              e.preventDefault();
            }, uploader.id);
  
            // Attach drop handler and grab files
            plupload.addEvent(dropElm, 'drop', function(e) {
              var dataTransfer = e.dataTransfer;
  
              // Add dropped files
              if (dataTransfer && dataTransfer.files) {
                addSelectedFiles(dataTransfer.files);
              }
  
              e.preventDefault();
            }, uploader.id);
          }
        });
  
        uploader.bind("Refresh", function(up) {
          var browseButton, browsePos, browseSize, inputContainer, zIndex;
  
          browseButton = document.getElementById(uploader.settings.browse_button);
          if (browseButton) {
            browsePos = plupload.getPos(browseButton, document.getElementById(up.settings.container));
            browseSize = plupload.getSize(browseButton);
            inputContainer = document.getElementById(uploader.id + '_html5_container');
  
            plupload.extend(inputContainer.style, {
              top : browsePos.y + 'px',
              left : browsePos.x + 'px',
              width : browseSize.w + 'px',
              height : browseSize.h + 'px'
            });
  
            // for WebKit place input element underneath the browse button and route onclick event
            // TODO: revise when browser support for this feature will change
            if (uploader.features.triggerDialog) {
              if (plupload.getStyle(browseButton, 'position') === 'static') {
                plupload.extend(browseButton.style, {
                  position : 'relative'
                });
              }
  
              zIndex = parseInt(plupload.getStyle(browseButton, 'z-index'), 10);
              if (isNaN(zIndex)) {
                zIndex = 0;
              }
  
              plupload.extend(browseButton.style, {
                zIndex : zIndex
              });
  
              plupload.extend(inputContainer.style, {
                zIndex : zIndex - 1
              });
            }
          }
        });
  
        uploader.bind("DisableBrowse", function(up, disabled) {
          var input = document.getElementById(up.id + '_html5');
          if (input) {
            input.disabled = disabled;
          }
        });
  
        uploader.bind("CancelUpload", function() {
          if (xhr && xhr.abort) {
            xhr.abort();
          }
        });
  
        uploader.bind("UploadFile", function(up, file) {
          var settings = up.settings, nativeFile, resize;
  
          function w3cBlobSlice(blob, start, end) {
            var blobSlice;
  
            if (File.prototype.slice) {
              try {
                blob.slice(); // depricated version will throw WRONG_ARGUMENTS_ERR exception
                return blob.slice(start, end);
              } catch (e) {
                // depricated slice method
                return blob.slice(start, end - start);
              }
            // slice method got prefixed: https://bugzilla.mozilla.org/show_bug.cgi?id=649672
            } else if (blobSlice = File.prototype.webkitSlice || File.prototype.mozSlice) {
              return blobSlice.call(blob, start, end);
            } else {
              return null; // or throw some exception
            }
          }
  
          function sendBinaryBlob(blob) {
            var chunk = 0, loaded = 0,
              fr = ("FileReader" in window) ? new FileReader : null;
  
  
            function uploadNextChunk() {
              var chunkBlob, br, chunks, args, chunkSize, curChunkSize, mimeType, url = up.settings.url;
  
  
              function prepareAndSend(bin) {
                var multipartDeltaSize = 0,
                  boundary = '----pluploadboundary' + plupload.guid(), formData, dashdash = '--', crlf = '\r\n', multipartBlob = '';
  
                xhr = new XMLHttpRequest;
  
                // Do we have upload progress support
                if (xhr.upload) {
                  xhr.upload.onprogress = function(e) {
                    file.loaded = Math.min(file.size, loaded + e.loaded - multipartDeltaSize); // Loaded can be larger than file size due to multipart encoding
                    up.trigger('UploadProgress', file);
                  };
                }
  
                xhr.onreadystatechange = function() {
                  var httpStatus, chunkArgs;
  
                  if (xhr.readyState == 4 && up.state !== plupload.STOPPED) {
                    // Getting the HTTP status might fail on some Gecko versions
                    try {
                      httpStatus = xhr.status;
                    } catch (ex) {
                      httpStatus = 0;
                    }
  
                    // Is error status
                    if (httpStatus >= 400) {
                      up.trigger('Error', {
                        code : plupload.HTTP_ERROR,
                        message : plupload.translate('HTTP Error.'),
                        file : file,
                        status : httpStatus
                      });
                    } else {
                      // Handle chunk response
                      if (chunks) {
                        chunkArgs = {
                          chunk : chunk,
                          chunks : chunks,
                          response : xhr.responseText,
                          status : httpStatus
                        };
  
                        up.trigger('ChunkUploaded', file, chunkArgs);
                        loaded += curChunkSize;
  
                        // Stop upload
                        if (chunkArgs.cancelled) {
                          file.status = plupload.FAILED;
                          return;
                        }
  
                        file.loaded = Math.min(file.size, (chunk + 1) * chunkSize);
                      } else {
                        file.loaded = file.size;
                      }
  
                      up.trigger('UploadProgress', file);
  
                      bin = chunkBlob = formData = multipartBlob = null; // Free memory
  
                      // Check if file is uploaded
                      if (!chunks || ++chunk >= chunks) {
                        file.status = plupload.DONE;
  
                        up.trigger('FileUploaded', file, {
                          response : xhr.responseText,
                          status : httpStatus
                        });
                      } else {
                        // Still chunks left
                        uploadNextChunk();
                      }
                    }
                  }
                };
  
  
                // Build multipart request
                if (up.settings.multipart && features.multipart) {
  
                  args.name = file.target_name || file.name;
  
                  xhr.open("post", url, true);
  
                  // Set custom headers
                  plupload.each(up.settings.headers, function(value, name) {
                    xhr.setRequestHeader(name, value);
                  });
  
  
                  // if has FormData support like Chrome 6+, Safari 5+, Firefox 4, use it
                  if (typeof(bin) !== 'string' && !!window.FormData) {
                    formData = new FormData();
  
                    // Add multipart params
                    plupload.each(plupload.extend(args, up.settings.multipart_params), function(value, name) {
                      formData.append(name, value);
                    });
  
                    // Add file and send it
                    formData.append(up.settings.file_data_name, bin);
                    xhr.send(formData);
  
                    return;
                  }  // if no FormData we can still try to send it directly as last resort (see below)
  
  
                  if (typeof(bin) === 'string') {
                    // Trying to send the whole thing as binary...
  
                    // multipart request
                    xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
  
                    // append multipart parameters
                    plupload.each(plupload.extend(args, up.settings.multipart_params), function(value, name) {
                      multipartBlob += dashdash + boundary + crlf +
                        'Content-Disposition: form-data; name="' + name + '"' + crlf + crlf;
  
                      multipartBlob += unescape(encodeURIComponent(value)) + crlf;
                    });
  
                    mimeType = plupload.mimeTypes[file.name.replace(/^.+\.([^.]+)/, '$1').toLowerCase()] || 'application/octet-stream';
  
                    // Build RFC2388 blob
                    multipartBlob += dashdash + boundary + crlf +
                      'Content-Disposition: form-data; name="' + up.settings.file_data_name + '"; filename="' + unescape(encodeURIComponent(file.name)) + '"' + crlf +
                      'Content-Type: ' + mimeType + crlf + crlf +
                      bin + crlf +
                      dashdash + boundary + dashdash + crlf;
  
                    multipartDeltaSize = multipartBlob.length - bin.length;
                    bin = multipartBlob;
  
  
                    if (xhr.sendAsBinary) { // Gecko
                      xhr.sendAsBinary(bin);
                    } else if (features.canSendBinary) { // WebKit with typed arrays support
                      var ui8a = new Uint8Array(bin.length);
                      for (var i = 0; i < bin.length; i++) {
                        ui8a[i] = (bin.charCodeAt(i) & 0xff);
                      }
                      xhr.send(ui8a.buffer);
                    }
                    return; // will return from here only if shouldn't send binary
                  }
                }
  
                // if no multipart, or last resort, send as binary stream
                url = plupload.buildUrl(up.settings.url, plupload.extend(args, up.settings.multipart_params));
  
                xhr.open("post", url, true);
  
                xhr.setRequestHeader('Content-Type', 'application/octet-stream'); // Binary stream header
  
                // Set custom headers
                plupload.each(up.settings.headers, function(value, name) {
                  xhr.setRequestHeader(name, value);
                });
  
                xhr.send(bin);
              } // prepareAndSend
  
  
              // File upload finished
              if (file.status == plupload.DONE || file.status == plupload.FAILED || up.state == plupload.STOPPED) {
                return;
              }
  
              // Standard arguments
              args = {name : file.target_name || file.name};
  
              // Only add chunking args if needed
              if (settings.chunk_size && file.size > settings.chunk_size && (features.chunks || typeof(blob) == 'string')) { // blob will be of type string if it was loaded in memory
                chunkSize = settings.chunk_size;
                chunks = Math.ceil(file.size / chunkSize);
                curChunkSize = Math.min(chunkSize, file.size - (chunk * chunkSize));
  
                // Blob is string so we need to fake chunking, this is not
                // ideal since the whole file is loaded into memory
                if (typeof(blob) == 'string') {
                  chunkBlob = blob.substring(chunk * chunkSize, chunk * chunkSize + curChunkSize);
                } else {
                  // Slice the chunk
                  chunkBlob = w3cBlobSlice(blob, chunk * chunkSize, chunk * chunkSize + curChunkSize);
                }
  
                // Setup query string arguments
                args.chunk = chunk;
                args.chunks = chunks;
              } else {
                curChunkSize = file.size;
                chunkBlob = blob;
              }
  
              // workaround Gecko 2,5,6 FormData+Blob bug: https://bugzilla.mozilla.org/show_bug.cgi?id=649150
              if (up.settings.multipart && features.multipart && typeof(chunkBlob) !== 'string' && fr && features.cantSendBlobInFormData && features.chunks && up.settings.chunk_size) { // Gecko 2,5,6
                fr.onload = function() {
                  prepareAndSend(fr.result);
                }
                fr.readAsBinaryString(chunkBlob);
              } else {
                prepareAndSend(chunkBlob);
              }
  
            }
  
            // Start uploading chunks
            uploadNextChunk();
          }
  
          nativeFile = html5files[file.id];
  
          // Resize image if it's a supported format and resize is enabled
          if (features.jpgresize && up.settings.resize && /\.(png|jpg|jpeg)$/i.test(file.name)) {
            scaleImage.call(up, file, up.settings.resize, /\.png$/i.test(file.name) ? 'image/png' : 'image/jpeg', function(res) {
              // If it was scaled send the scaled image if it failed then
              // send the raw image and let the server do the scaling
              if (res.success) {
                file.size = res.data.length;
                sendBinaryBlob(res.data);
              } else if (features.chunks) {
                sendBinaryBlob(nativeFile);
              } else {
                readFileAsBinary(nativeFile, sendBinaryBlob); // for browsers not supporting File.slice (e.g. FF3.6)
              }
            });
          // if there's no way to slice file without preloading it in memory, preload it
          } else if (!features.chunks && features.jpgresize) {
            readFileAsBinary(nativeFile, sendBinaryBlob);
          } else {
            sendBinaryBlob(nativeFile);
          }
        });
  
  
        uploader.bind('Destroy', function(up) {
          var name, element, container = document.body,
            elements = {
              inputContainer: up.id + '_html5_container',
              inputFile: up.id + '_html5',
              browseButton: up.settings.browse_button,
              dropElm: up.settings.drop_element
            };
  
          // Unbind event handlers
          for (name in elements) {
            element = document.getElementById(elements[name]);
            if (element) {
              plupload.removeAllEvents(element, up.id);
            }
          }
          plupload.removeAllEvents(document.body, up.id);
  
          if (up.settings.container) {
            container = document.getElementById(up.settings.container);
          }
  
          // Remove mark-up
          container.removeChild(document.getElementById(elements.inputContainer));
        });
  
        callback({success : true});
      }
    });
  
    function BinaryReader() {
      var II = false, bin;
  
      // Private functions
      function read(idx, size) {
        var mv = II ? 0 : -8 * (size - 1), sum = 0, i;
  
        for (i = 0; i < size; i++) {
          sum |= (bin.charCodeAt(idx + i) << Math.abs(mv + i*8));
        }
  
        return sum;
      }
  
      function putstr(segment, idx, length) {
        var length = arguments.length === 3 ? length : bin.length - idx - 1;
  
        bin = bin.substr(0, idx) + segment + bin.substr(length + idx);
      }
  
      function write(idx, num, size) {
        var str = '', mv = II ? 0 : -8 * (size - 1), i;
  
        for (i = 0; i < size; i++) {
          str += String.fromCharCode((num >> Math.abs(mv + i*8)) & 255);
        }
  
        putstr(str, idx, size);
      }
  
      // Public functions
      return {
        II: function(order) {
          if (order === undef) {
            return II;
          } else {
            II = order;
          }
        },
  
        init: function(binData) {
          II = false;
          bin = binData;
        },
  
        SEGMENT: function(idx, length, segment) {
          switch (arguments.length) {
            case 1:
              return bin.substr(idx, bin.length - idx - 1);
            case 2:
              return bin.substr(idx, length);
            case 3:
              putstr(segment, idx, length);
              break;
            default: return bin;
          }
        },
  
        BYTE: function(idx) {
          return read(idx, 1);
        },
  
        SHORT: function(idx) {
          return read(idx, 2);
        },
  
        LONG: function(idx, num) {
          if (num === undef) {
            return read(idx, 4);
          } else {
            write(idx, num, 4);
          }
        },
  
        SLONG: function(idx) { // 2's complement notation
          var num = read(idx, 4);
  
          return (num > 2147483647 ? num - 4294967296 : num);
        },
  
        STRING: function(idx, size) {
          var str = '';
  
          for (size += idx; idx < size; idx++) {
            str += String.fromCharCode(read(idx, 1));
          }
  
          return str;
        }
      };
    }
  
    function JPEG_Headers(data) {
  
      var markers = {
          0xFFE1: {
            app: 'EXIF',
            name: 'APP1',
            signature: "Exif\0"
          },
          0xFFE2: {
            app: 'ICC',
            name: 'APP2',
            signature: "ICC_PROFILE\0"
          },
          0xFFED: {
            app: 'IPTC',
            name: 'APP13',
            signature: "Photoshop 3.0\0"
          }
        },
        headers = [], read, idx, marker = undef, length = 0, limit;
  
  
      read = new BinaryReader();
      read.init(data);
  
      // Check if data is jpeg
      if (read.SHORT(0) !== 0xFFD8) {
        return;
      }
  
      idx = 2;
      limit = Math.min(1048576, data.length);
  
      while (idx <= limit) {
        marker = read.SHORT(idx);
  
        // omit RST (restart) markers
        if (marker >= 0xFFD0 && marker <= 0xFFD7) {
          idx += 2;
          continue;
        }
  
        // no headers allowed after SOS marker
        if (marker === 0xFFDA || marker === 0xFFD9) {
          break;
        }
  
        length = read.SHORT(idx + 2) + 2;
  
        if (markers[marker] &&
          read.STRING(idx + 4, markers[marker].signature.length) === markers[marker].signature) {
          headers.push({
            hex: marker,
            app: markers[marker].app.toUpperCase(),
            name: markers[marker].name.toUpperCase(),
            start: idx,
            length: length,
            segment: read.SEGMENT(idx, length)
          });
        }
        idx += length;
      }
  
      read.init(null); // free memory
  
      return {
  
        headers: headers,
  
        restore: function(data) {
          read.init(data);
  
          // Check if data is jpeg
          var jpegHeaders = new JPEG_Headers(data);
  
          if (!jpegHeaders['headers']) {
            return false;
          }
  
          // Delete any existing headers that need to be replaced
          for (var i = jpegHeaders['headers'].length; i > 0; i--) {
            var hdr = jpegHeaders['headers'][i - 1];
            read.SEGMENT(hdr.start, hdr.length, '')
          }
          jpegHeaders.purge();
  
          idx = read.SHORT(2) == 0xFFE0 ? 4 + read.SHORT(4) : 2;
  
          for (var i = 0, max = headers.length; i < max; i++) {
            read.SEGMENT(idx, 0, headers[i].segment);
            idx += headers[i].length;
          }
  
          return read.SEGMENT();
        },
  
        get: function(app) {
          var array = [];
  
          for (var i = 0, max = headers.length; i < max; i++) {
            if (headers[i].app === app.toUpperCase()) {
              array.push(headers[i].segment);
            }
          }
          return array;
        },
  
        set: function(app, segment) {
          var array = [];
  
          if (typeof(segment) === 'string') {
            array.push(segment);
          } else {
            array = segment;
          }
  
          for (var i = ii = 0, max = headers.length; i < max; i++) {
            if (headers[i].app === app.toUpperCase()) {
              headers[i].segment = array[ii];
              headers[i].length = array[ii].length;
              ii++;
            }
            if (ii >= array.length) break;
          }
        },
  
        purge: function() {
          headers = [];
          read.init(null);
        }
      };
    }
  
  
    function ExifParser() {
      // Private ExifParser fields
      var data, tags, offsets = {}, tagDescs;
  
      data = new BinaryReader();
  
      tags = {
        tiff : {
          /*
          The image orientation viewed in terms of rows and columns.
  
          1 - The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
          2 - The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
          3 - The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
          4 - The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
          5 - The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
          6 - The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
          7 - The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
          8 - The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
          9 - The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
          */
          0x0112: 'Orientation',
          0x8769: 'ExifIFDPointer',
          0x8825: 'GPSInfoIFDPointer'
        },
        exif : {
          0x9000: 'ExifVersion',
          0xA001: 'ColorSpace',
          0xA002: 'PixelXDimension',
          0xA003: 'PixelYDimension',
          0x9003: 'DateTimeOriginal',
          0x829A: 'ExposureTime',
          0x829D: 'FNumber',
          0x8827: 'ISOSpeedRatings',
          0x9201: 'ShutterSpeedValue',
          0x9202: 'ApertureValue' ,
          0x9207: 'MeteringMode',
          0x9208: 'LightSource',
          0x9209: 'Flash',
          0xA402: 'ExposureMode',
          0xA403: 'WhiteBalance',
          0xA406: 'SceneCaptureType',
          0xA404: 'DigitalZoomRatio',
          0xA408: 'Contrast',
          0xA409: 'Saturation',
          0xA40A: 'Sharpness'
        },
        gps : {
          0x0000: 'GPSVersionID',
          0x0001: 'GPSLatitudeRef',
          0x0002: 'GPSLatitude',
          0x0003: 'GPSLongitudeRef',
          0x0004: 'GPSLongitude'
        }
      };
  
      tagDescs = {
        'ColorSpace': {
          1: 'sRGB',
          0: 'Uncalibrated'
        },
  
        'MeteringMode': {
          0: 'Unknown',
          1: 'Average',
          2: 'CenterWeightedAverage',
          3: 'Spot',
          4: 'MultiSpot',
          5: 'Pattern',
          6: 'Partial',
          255: 'Other'
        },
  
        'LightSource': {
          1: 'Daylight',
          2: 'Fliorescent',
          3: 'Tungsten',
          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'
        },
  
        'ExposureMode': {
          0: 'Auto exposure',
          1: 'Manual exposure',
          2: 'Auto bracket'
        },
  
        'WhiteBalance': {
          0: 'Auto white balance',
          1: 'Manual white balance'
        },
  
        'SceneCaptureType': {
          0: 'Standard',
          1: 'Landscape',
          2: 'Portrait',
          3: 'Night scene'
        },
  
        'Contrast': {
          0: 'Normal',
          1: 'Soft',
          2: 'Hard'
        },
  
        'Saturation': {
          0: 'Normal',
          1: 'Low saturation',
          2: 'High saturation'
        },
  
        'Sharpness': {
          0: 'Normal',
          1: 'Soft',
          2: 'Hard'
        },
  
        // GPS related
        'GPSLatitudeRef': {
          N: 'North latitude',
          S: 'South latitude'
        },
  
        'GPSLongitudeRef': {
          E: 'East longitude',
          W: 'West longitude'
        }
      };
  
      function extractTags(IFD_offset, tags2extract) {
        var length = data.SHORT(IFD_offset), i, ii,
          tag, type, count, tagOffset, offset, value, values = [], hash = {};
  
        for (i = 0; i < length; i++) {
          // Set binary reader pointer to beginning of the next tag
          offset = tagOffset = IFD_offset + 12 * i + 2;
  
          tag = tags2extract[data.SHORT(offset)];
  
          if (tag === undef) {
            continue; // Not the tag we requested
          }
  
          type = data.SHORT(offset+=2);
          count = data.LONG(offset+=2);
  
          offset += 4;
          values = [];
  
          switch (type) {
            case 1: // BYTE
            case 7: // UNDEFINED
              if (count > 4) {
                offset = data.LONG(offset) + offsets.tiffHeader;
              }
  
              for (ii = 0; ii < count; ii++) {
                values[ii] = data.BYTE(offset + ii);
              }
  
              break;
  
            case 2: // STRING
              if (count > 4) {
                offset = data.LONG(offset) + offsets.tiffHeader;
              }
  
              hash[tag] = data.STRING(offset, count - 1);
  
              continue;
  
            case 3: // SHORT
              if (count > 2) {
                offset = data.LONG(offset) + offsets.tiffHeader;
              }
  
              for (ii = 0; ii < count; ii++) {
                values[ii] = data.SHORT(offset + ii*2);
              }
  
              break;
  
            case 4: // LONG
              if (count > 1) {
                offset = data.LONG(offset) + offsets.tiffHeader;
              }
  
              for (ii = 0; ii < count; ii++) {
                values[ii] = data.LONG(offset + ii*4);
              }
  
              break;
  
            case 5: // RATIONAL
              offset = data.LONG(offset) + offsets.tiffHeader;
  
              for (ii = 0; ii < count; ii++) {
                values[ii] = data.LONG(offset + ii*4) / data.LONG(offset + ii*4 + 4);
              }
  
              break;
  
            case 9: // SLONG
              offset = data.LONG(offset) + offsets.tiffHeader;
  
              for (ii = 0; ii < count; ii++) {
                values[ii] = data.SLONG(offset + ii*4);
              }
  
              break;
  
            case 10: // SRATIONAL
              offset = data.LONG(offset) + offsets.tiffHeader;
  
              for (ii = 0; ii < count; ii++) {
                values[ii] = data.SLONG(offset + ii*4) / data.SLONG(offset + ii*4 + 4);
              }
  
              break;
  
            default:
              continue;
          }
  
          value = (count == 1 ? values[0] : values);
  
          if (tagDescs.hasOwnProperty(tag) && typeof value != 'object') {
            hash[tag] = tagDescs[tag][value];
          } else {
            hash[tag] = value;
          }
        }
  
        return hash;
      }
  
      function getIFDOffsets() {
        var Tiff = undef, idx = offsets.tiffHeader;
  
        // Set read order of multi-byte data
        data.II(data.SHORT(idx) == 0x4949);
  
        // Check if always present bytes are indeed present
        if (data.SHORT(idx+=2) !== 0x002A) {
          return false;
        }
  
        offsets['IFD0'] = offsets.tiffHeader + data.LONG(idx += 2);
        Tiff = extractTags(offsets['IFD0'], tags.tiff);
  
        offsets['exifIFD'] = ('ExifIFDPointer' in Tiff ? offsets.tiffHeader + Tiff.ExifIFDPointer : undef);
        offsets['gpsIFD'] = ('GPSInfoIFDPointer' in Tiff ? offsets.tiffHeader + Tiff.GPSInfoIFDPointer : undef);
  
        return true;
      }
  
      // At the moment only setting of simple (LONG) values, that do not require offset recalculation, is supported
      function setTag(ifd, tag, value) {
        var offset, length, tagOffset, valueOffset = 0;
  
        // If tag name passed translate into hex key
        if (typeof(tag) === 'string') {
          var tmpTags = tags[ifd.toLowerCase()];
          for (hex in tmpTags) {
            if (tmpTags[hex] === tag) {
              tag = hex;
              break;
            }
          }
        }
        offset = offsets[ifd.toLowerCase() + 'IFD'];
        length = data.SHORT(offset);
  
        for (i = 0; i < length; i++) {
          tagOffset = offset + 12 * i + 2;
  
          if (data.SHORT(tagOffset) == tag) {
            valueOffset = tagOffset + 8;
            break;
          }
        }
  
        if (!valueOffset) return false;
  
  
        data.LONG(valueOffset, value);
        return true;
      }
  
  
      // Public functions
      return {
        init: function(segment) {
          // Reset internal data
          offsets = {
            tiffHeader: 10
          };
  
          if (segment === undef || !segment.length) {
            return false;
          }
  
          data.init(segment);
  
          // Check if that's APP1 and that it has EXIF
          if (data.SHORT(0) === 0xFFE1 && data.STRING(4, 5).toUpperCase() === "EXIF\0") {
            return getIFDOffsets();
          }
          return false;
        },
  
        EXIF: function() {
          var Exif;
  
          // Populate EXIF hash
          Exif = extractTags(offsets.exifIFD, tags.exif);
  
          // Fix formatting of some tags
          if (Exif.ExifVersion && plupload.typeOf(Exif.ExifVersion) === 'array') {
            for (var i = 0, exifVersion = ''; i < Exif.ExifVersion.length; i++) {
              exifVersion += String.fromCharCode(Exif.ExifVersion[i]);
            }
            Exif.ExifVersion = exifVersion;
          }
  
          return Exif;
        },
  
        GPS: function() {
          var GPS;
  
          GPS = extractTags(offsets.gpsIFD, tags.gps);
  
          // iOS devices (and probably some others) do not put in GPSVersionID tag (why?..)
          if (GPS.GPSVersionID) {
            GPS.GPSVersionID = GPS.GPSVersionID.join('.');
          }
  
          return GPS;
        },
  
        setExif: function(tag, value) {
          // Right now only setting of width/height is possible
          if (tag !== 'PixelXDimension' && tag !== 'PixelYDimension') return false;
  
          return setTag('exif', tag, value);
        },
  
  
        getBinary: function() {
          return data.SEGMENT();
        }
      };
    };
  })(window, document, plupload);
  