/** * 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 fakeSafariDragDrop, ExifParser; 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(image_file, max_width, max_height, mime, callback) { var canvas, context, img, scale; readFileAsDataURL(image_file, 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.onload = function() { var width, height, percentage, APP1, parser; scale = Math.min(max_width / img.width, max_height / img.height); if (scale < 1) { 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); // Get original EXIF info parser = new ExifParser(); parser.init(atob(data.substring(data.indexOf('base64,') + 7))); APP1 = parser.APP1({width: width, height: height}); // Remove data prefix information and grab the base64 encoded data and decode it data = canvas.toDataURL(mime); data = data.substring(data.indexOf('base64,') + 7); data = atob(data); // Restore EXIF info to scaled image if (APP1) { parser.init(data); parser.setAPP1(APP1); data = parser.getBinary(); } // 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, dataAccessSupport, sliceSupport, win = window; hasXhrSupport = hasProgress = dataAccessSupport = sliceSupport = false; /* Introduce sendAsBinary for cutting edge WebKit builds that have support for BlobBuilder and typed arrays: credits: http://javascript0.org/wiki/Portable_sendAsBinary, more info: http://code.google.com/p/chromium/issues/detail?id=35705 */ if (win.Uint8Array && win.ArrayBuffer && !XMLHttpRequest.prototype.sendAsBinary) { XMLHttpRequest.prototype.sendAsBinary = function(datastr) { var data, ui8a, bb, blob; data = new ArrayBuffer(datastr.length); ui8a = new Uint8Array(data, 0); for (var i=0; i 0 && navigator.vendor.indexOf('Apple') !== -1; return { // Detect drag/drop file support by sniffing, will try to find a better way html5: hasXhrSupport, // This is a special one that we check inside the init call dragdrop: win.mozInnerScreenX !== undef || sliceSupport || fakeSafariDragDrop, jpgresize: dataAccessSupport, pngresize: dataAccessSupport, multipart: dataAccessSupport || !!win.FileReader || !!win.FormData, progress: hasProgress, chunking: sliceSupport || dataAccessSupport, /* WebKit let you trigger file dialog programmatically while FF and Opera - do not, so we sniff for it here... probably not that good idea, but impossibillity of controlling cursor style on top of add files button obviously feels even worse */ canOpenDialog: navigator.userAgent.indexOf('WebKit') !== -1 }; }, /** * 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 html5files = {}, features; 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.fileSize || file.size)); // File.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'; // Convert extensions to mime types list for (i = 0; i < filters.length; i++) { ext = filters[i].extensions.split(/,/); for (y = 0; y < ext.length; y++) { type = plupload.mimeTypes[ext[y]]; if (type) { mimes.push(type); } } } 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); container.style.position = 'relative'; } container.appendChild(inputContainer); // Insert the input inside the input container inputContainer.innerHTML = ''; inputFile = document.getElementById(uploader.id + '_html5'); 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 class 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.canOpenDialog ? 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.canOpenDialog) { plupload.addEvent(browseButton, 'click', function(e) { document.getElementById(up.id + '_html5').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); 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, pzIndex; 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 IE and 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.canOpenDialog) { pzIndex = parseInt(browseButton.parentNode.style.zIndex, 10); if (isNaN(pzIndex)) { pzIndex = 0; } plupload.extend(browseButton.style, { position : 'relative', zIndex : pzIndex }); plupload.extend(inputContainer.style, { zIndex : pzIndex - 1 }); } } }); uploader.bind("UploadFile", function(up, file) { var settings = up.settings, nativeFile, resize; function sendBinaryBlob(blob) { var chunk = 0, loaded = 0; function uploadNextChunk() { var chunkBlob = blob, xhr, upload, chunks, args, multipartDeltaSize = 0, boundary = '----pluploadboundary' + plupload.guid(), chunkSize, curChunkSize, formData, dashdash = '--', crlf = '\r\n', multipartBlob = '', mimeType, url = up.settings.url; // 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 && features.chunking) { 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 = blob.slice(chunk * chunkSize, curChunkSize); } // Setup query string arguments args.chunk = chunk; args.chunks = chunks; } else { curChunkSize = file.size; } // Setup XHR object xhr = new XMLHttpRequest(); upload = xhr.upload; // Do we have upload progress support if (upload) { 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); }; } // Add name, chunk and chunks to query string on direct streaming if (!up.settings.multipart || !features.multipart) { url = plupload.buildUrl(up.settings.url, args); } else { args.name = file.target_name || file.name; } xhr.open("post", url, true); xhr.onreadystatechange = function() { var httpStatus, chunkArgs; if (xhr.readyState == 4) { // 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); // Check if file is uploaded if (!chunks || ++chunk >= chunks) { file.status = plupload.DONE; up.trigger('FileUploaded', file, { response : xhr.responseText, status : httpStatus }); nativeFile = blob = html5files[file.id] = null; // Free memory } else { // Still chunks left uploadNextChunk(); } } xhr = chunkBlob = formData = multipartBlob = null; // Free memory } }; // Set custom headers plupload.each(up.settings.headers, function(value, name) { xhr.setRequestHeader(name, value); }); // Build multipart request if (up.settings.multipart && features.multipart) { // Has FormData support like Chrome 6+, Safari 5+, Firefox 4 if (!xhr.sendAsBinary) { 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, chunkBlob); xhr.send(formData); return; } // Gecko 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')] || '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 + chunkBlob + crlf + dashdash + boundary + dashdash + crlf; multipartDeltaSize = multipartBlob.length - chunkBlob.length; chunkBlob = multipartBlob; } else { // Binary stream header xhr.setRequestHeader('Content-Type', 'application/octet-stream'); } if (xhr.sendAsBinary) { xhr.sendAsBinary(chunkBlob); // Gecko } else { xhr.send(chunkBlob); // WebKit } } // Start uploading chunks uploadNextChunk(); } nativeFile = html5files[file.id]; resize = up.settings.resize; if (features.jpgresize) { // Resize image if it's a supported format and resize is enabled if (resize && /\.(png|jpg|jpeg)$/i.test(file.name)) { scaleImage(nativeFile, resize.width, resize.height, /\.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 { readFileAsBinary(nativeFile, sendBinaryBlob); } }); } else { readFileAsBinary(nativeFile, sendBinaryBlob); } } else { sendBinaryBlob(nativeFile); // this works on older WebKits, but fails on fresh ones } }); 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}); } }); ExifParser = function() { // Private ExifParser fields var Tiff, Exif, GPS, app0, app0_offset, app0_length, app1, app1_offset, data, app1_length, exifIFD_offset, gpsIFD_offset, IFD0_offset, TIFFHeader_offset, undef, tiffTags, exifTags, gpsTags, tagDescs; /** * @constructor */ 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(idx, segment, replace) { bin = bin.substr(0, idx) + segment + bin.substr((replace === true ? segment.length : 0) + 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(idx, str, true); } // Public functions return { II: function(order) { if (order === undef) { return II; } else { II = order; } }, init: function(binData) { bin = binData; }, SEGMENT: function(idx, segment, replace) { if (!arguments.length) { return bin; } if (typeof segment == 'number') { return bin.substr(parseInt(idx, 10), segment); } putstr(idx, segment, replace); }, 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; } }; } data = new BinaryReader(); tiffTags = { /* 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' }; exifTags = { 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' }; gpsTags = { 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 = [], tags = {}; 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) + TIFFHeader_offset; } for (ii = 0; ii < count; ii++) { values[ii] = data.BYTE(offset + ii); } break; case 2: // STRING if (count > 4) { offset = data.LONG(offset) + TIFFHeader_offset; } tags[tag] = data.STRING(offset, count - 1); continue; case 3: // SHORT if (count > 2) { offset = data.LONG(offset) + TIFFHeader_offset; } for (ii = 0; ii < count; ii++) { values[ii] = data.SHORT(offset + ii*2); } break; case 4: // LONG if (count > 1) { offset = data.LONG(offset) + TIFFHeader_offset; } for (ii = 0; ii < count; ii++) { values[ii] = data.LONG(offset + ii*4); } break; case 5: // RATIONAL offset = data.LONG(offset) + TIFFHeader_offset; 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) + TIFFHeader_offset; for (ii = 0; ii < count; ii++) { values[ii] = data.SLONG(offset + ii*4); } break; case 10: // SRATIONAL offset = data.LONG(offset) + TIFFHeader_offset; 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') { tags[tag] = tagDescs[tag][value]; } else { tags[tag] = value; } } return tags; } function getIFDOffsets() { var idx = app1_offset + 4; // Fix TIFF header offset TIFFHeader_offset += app1_offset; // Check if that's EXIF we are reading if (data.STRING(idx, 4).toUpperCase() !== 'EXIF' || data.SHORT(idx+=4) !== 0) { return; } // Set read order of multi-byte data data.II(data.SHORT(idx+=2) == 0x4949); // Check if always present bytes are indeed present if (data.SHORT(idx+=2) !== 0x002A) { return; } IFD0_offset = TIFFHeader_offset + data.LONG(idx += 2); Tiff = extractTags(IFD0_offset, tiffTags); exifIFD_offset = ('ExifIFDPointer' in Tiff ? TIFFHeader_offset + Tiff.ExifIFDPointer : undef); gpsIFD_offset = ('GPSInfoIFDPointer' in Tiff ? TIFFHeader_offset + Tiff.GPSInfoIFDPointer : undef); return true; } function findTagValueOffset(data_app1, tegHex, offset) { var length = data_app1.SHORT(offset), tagOffset, i; for (i = 0; i < length; i++) { tagOffset = offset + 12 * i + 2; if (data_app1.SHORT(tagOffset) == tegHex) { return tagOffset + 8; } } } function setNewWxH(width, height) { var w_offset, h_offset, offset = exifIFD_offset != undef ? exifIFD_offset - app1_offset : undef, data_app1 = new BinaryReader(); data_app1.init(app1); data_app1.II(data.II()); if (offset === undef) { return; } // Find offset for PixelXDimension tag w_offset = findTagValueOffset(data_app1, 0xA002, offset); if (w_offset !== undef) { data_app1.LONG(w_offset, width); } // Find offset for PixelYDimension tag h_offset = findTagValueOffset(data_app1, 0xA003, offset); if (h_offset !== undef) { data_app1.LONG(h_offset, height); } app1 = data_app1.SEGMENT(); } // Public functions return { init: function(jpegData) { // Reset internal data TIFFHeader_offset = 10; Tiff = Exif = GPS = app0 = app0_offset = app0_length = app1 = app1_offset = app1_length = undef; data.init(jpegData); // Check if data is jpeg if (data.SHORT(0) !== 0xFFD8) { return false; } switch (data.SHORT(2)) { // app0 case 0xFFE0: app0_offset = 2; app0_length = data.SHORT(4) + 2; // check if app1 follows if (data.SHORT(app0_length) == 0xFFE1) { app1_offset = app0_length; app1_length = data.SHORT(app0_length + 2) + 2; } break; // app1 case 0xFFE1: app1_offset = 2; app1_length = data.SHORT(4) + 2; break; default: return false; } if (app1_length !== undef) { getIFDOffsets(); } }, APP1: function(args) { if (app1_offset === undef && app1_length === undef) { return; } app1 = app1 || (app1 = data.SEGMENT(app1_offset, app1_length)); // If requested alter width/height tags in app1 if (args !== undef && 'width' in args && 'height' in args) { setNewWxH(args.width, args.height); } return app1; }, EXIF: function() { // Populate EXIF hash Exif = extractTags(exifIFD_offset, exifTags); // Fix formatting of some tags Exif.ExifVersion = String.fromCharCode( Exif.ExifVersion[0], Exif.ExifVersion[1], Exif.ExifVersion[2], Exif.ExifVersion[3] ); return Exif; }, GPS: function() { GPS = extractTags(gpsIFD_offset, gpsTags); GPS.GPSVersionID = GPS.GPSVersionID.join('.'); return GPS; }, setAPP1: function(data_app1) { if (app1_offset !== undef) { return false; } data.SEGMENT((app0_offset ? app0_offset + app0_length : 2), data_app1); }, getBinary: function() { return data.SEGMENT(); } }; }; })(window, document, plupload);