PageRenderTime 60ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 1ms

/src/javascript/plupload.html5.js

https://github.com/bluker/plupload
JavaScript | 1175 lines | 810 code | 220 blank | 145 comment | 139 complexity | 0f7acf7870d402879f74cbb7dc866660 MD5 | raw file
Possible License(s): GPL-2.0
  1. /**
  2. * plupload.html5.js
  3. *
  4. * Copyright 2009, Moxiecode Systems AB
  5. * Released under GPL License.
  6. *
  7. * License: http://www.plupload.com/license
  8. * Contributing: http://www.plupload.com/contributing
  9. */
  10. // JSLint defined globals
  11. /*global plupload:false, File:false, window:false, atob:false, FormData:false, FileReader:false, ArrayBuffer:false, Uint8Array:false, BlobBuilder:false, unescape:false */
  12. (function(window, document, plupload, undef) {
  13. var fakeSafariDragDrop, ExifParser;
  14. function readFile(file, callback) {
  15. var reader;
  16. // Use FileReader if it's available
  17. if ("FileReader" in window) {
  18. reader = new FileReader();
  19. reader.readAsDataURL(file);
  20. reader.onload = function() {
  21. callback(reader.result);
  22. };
  23. } else {
  24. return callback(file.getAsDataURL());
  25. }
  26. }
  27. function scaleImage(image_file, max_width, max_height, mime, callback) {
  28. var canvas, context, img, scale;
  29. readFile(image_file, function(data) {
  30. // Setup canvas and context
  31. canvas = document.createElement("canvas");
  32. canvas.style.display = 'none';
  33. document.body.appendChild(canvas);
  34. context = canvas.getContext('2d');
  35. // Load image
  36. img = new Image();
  37. img.onload = function() {
  38. var width, height, percentage, APP1, parser;
  39. scale = Math.min(max_width / img.width, max_height / img.height);
  40. if (scale < 1) {
  41. width = Math.round(img.width * scale);
  42. height = Math.round(img.height * scale);
  43. // Scale image and canvas
  44. canvas.width = width;
  45. canvas.height = height;
  46. context.drawImage(img, 0, 0, width, height);
  47. // Get original EXIF info
  48. parser = new ExifParser();
  49. parser.init(atob(data.substring(data.indexOf('base64,') + 7)));
  50. APP1 = parser.APP1({width: width, height: height});
  51. // Remove data prefix information and grab the base64 encoded data and decode it
  52. data = canvas.toDataURL(mime);
  53. data = data.substring(data.indexOf('base64,') + 7);
  54. data = atob(data);
  55. // Restore EXIF info to scaled image
  56. if (APP1) {
  57. parser.init(data);
  58. parser.setAPP1(APP1);
  59. data = parser.getBinary();
  60. }
  61. // Remove canvas and execute callback with decoded image data
  62. canvas.parentNode.removeChild(canvas);
  63. callback({success : true, data : data});
  64. } else {
  65. // Image does not need to be resized
  66. callback({success : false});
  67. }
  68. };
  69. img.src = data;
  70. });
  71. }
  72. /**
  73. * HMTL5 implementation. This runtime supports these features: dragdrop, jpgresize, pngresize.
  74. *
  75. * @static
  76. * @class plupload.runtimes.Html5
  77. * @extends plupload.Runtime
  78. */
  79. plupload.runtimes.Html5 = plupload.addRuntime("html5", {
  80. /**
  81. * Returns a list of supported features for the runtime.
  82. *
  83. * @return {Object} Name/value object with supported features.
  84. */
  85. getFeatures : function() {
  86. var xhr, hasXhrSupport, hasProgress, dataAccessSupport, sliceSupport, win = window;
  87. hasXhrSupport = hasProgress = dataAccessSupport = sliceSupport = false;
  88. /* Introduce sendAsBinary for cutting edge WebKit builds that have support for BlobBuilder and typed arrays:
  89. credits: http://javascript0.org/wiki/Portable_sendAsBinary,
  90. more info: http://code.google.com/p/chromium/issues/detail?id=35705
  91. */
  92. if (win.Uint8Array && win.ArrayBuffer && !XMLHttpRequest.prototype.sendAsBinary) {
  93. XMLHttpRequest.prototype.sendAsBinary = function(datastr) {
  94. var data, ui8a, bb, blob;
  95. data = new ArrayBuffer(datastr.length);
  96. ui8a = new Uint8Array(data, 0);
  97. for (var i=0; i<datastr.length; i++) {
  98. ui8a[i] = (datastr.charCodeAt(i) & 0xff);
  99. }
  100. bb = new BlobBuilder();
  101. bb.append(data);
  102. blob = bb.getBlob();
  103. this.send(blob);
  104. };
  105. }
  106. if (win.XMLHttpRequest) {
  107. xhr = new XMLHttpRequest();
  108. hasProgress = !!xhr.upload;
  109. hasXhrSupport = !!(xhr.sendAsBinary || xhr.upload);
  110. }
  111. // Check for support for various features
  112. if (hasXhrSupport) {
  113. // Set dataAccessSupport only for Gecko since BlobBuilder and XHR doesn't handle binary data correctly
  114. dataAccessSupport = !!(File && (File.prototype.getAsDataURL || win.FileReader) && xhr.sendAsBinary);
  115. sliceSupport = !!(File && File.prototype.slice);
  116. }
  117. // Sniff for Safari and fake drag/drop
  118. fakeSafariDragDrop = navigator.userAgent.indexOf('Safari') > 0 && navigator.vendor.indexOf('Apple') !== -1;
  119. return {
  120. // Detect drag/drop file support by sniffing, will try to find a better way
  121. html5: hasXhrSupport, // This is a special one that we check inside the init call
  122. dragdrop: win.mozInnerScreenX !== undef || sliceSupport || fakeSafariDragDrop,
  123. jpgresize: dataAccessSupport,
  124. pngresize: dataAccessSupport,
  125. multipart: dataAccessSupport || !!win.FileReader || !!win.FormData,
  126. progress: hasProgress,
  127. chunking: sliceSupport || dataAccessSupport,
  128. /* WebKit let you trigger file dialog programmatically while FF and Opera - do not, so we
  129. sniff for it here... probably not that good idea, but impossibillity of controlling cursor style
  130. on top of add files button obviously feels even worse */
  131. canOpenDialog: navigator.userAgent.indexOf('WebKit') !== -1
  132. };
  133. },
  134. /**
  135. * Initializes the upload runtime.
  136. *
  137. * @method init
  138. * @param {plupload.Uploader} uploader Uploader instance that needs to be initialized.
  139. * @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.
  140. */
  141. init : function(uploader, callback) {
  142. var html5files = {}, features;
  143. function addSelectedFiles(native_files) {
  144. var file, i, files = [], id, fileNames = {};
  145. // Add the selected files to the file queue
  146. for (i = 0; i < native_files.length; i++) {
  147. file = native_files[i];
  148. // Safari on Windows will add first file from dragged set multiple times
  149. // @see: https://bugs.webkit.org/show_bug.cgi?id=37957
  150. if (fileNames[file.name]) {
  151. continue;
  152. }
  153. fileNames[file.name] = true;
  154. // Store away gears blob internally
  155. id = plupload.guid();
  156. html5files[id] = file;
  157. // Expose id, name and size
  158. files.push(new plupload.File(id, file.fileName, file.fileSize));
  159. }
  160. // Trigger FilesAdded event if we added any
  161. if (files.length) {
  162. uploader.trigger("FilesAdded", files);
  163. }
  164. }
  165. // No HTML5 upload support
  166. features = this.getFeatures();
  167. if (!features.html5) {
  168. callback({success : false});
  169. return;
  170. }
  171. uploader.bind("Init", function(up) {
  172. var inputContainer, browseButton, mimes = [], i, y, filters = up.settings.filters, ext, type, container = document.body, inputFile;
  173. // Create input container and insert it at an absolute position within the browse button
  174. inputContainer = document.createElement('div');
  175. inputContainer.id = up.id + '_html5_container';
  176. // Convert extensions to mime types list
  177. for (i = 0; i < filters.length; i++) {
  178. ext = filters[i].extensions.split(/,/);
  179. for (y = 0; y < ext.length; y++) {
  180. type = plupload.mimeTypes[ext[y]];
  181. if (type) {
  182. mimes.push(type);
  183. }
  184. }
  185. }
  186. plupload.extend(inputContainer.style, {
  187. position : 'absolute',
  188. background : uploader.settings.shim_bgcolor || 'transparent',
  189. width : '100px',
  190. height : '100px',
  191. overflow : 'hidden',
  192. zIndex : 99999,
  193. opacity : uploader.settings.shim_bgcolor ? '' : 0 // Force transparent if bgcolor is undefined
  194. });
  195. inputContainer.className = 'plupload html5';
  196. if (uploader.settings.container) {
  197. container = document.getElementById(uploader.settings.container);
  198. container.style.position = 'relative';
  199. }
  200. container.appendChild(inputContainer);
  201. // Insert the input inside the input container
  202. inputContainer.innerHTML = '<input id="' + uploader.id + '_html5" ' +
  203. 'style="width:100%;height:100%;" type="file" accept="' + mimes.join(',') + '" ' +
  204. (uploader.settings.multi_selection ? 'multiple="multiple"' : '') + ' />';
  205. inputFile = document.getElementById(uploader.id + '_html5');
  206. inputFile.onchange = function() {
  207. // Add the selected files from file input
  208. addSelectedFiles(this.files);
  209. // Clearing the value enables the user to select the same file again if they want to
  210. this.value = '';
  211. };
  212. /* Since we have to place input[type=file] on top of the browse_button for some browsers (FF, Opera),
  213. browse_button loses interactivity, here we try to neutralize this issue highlighting browse_button
  214. with a special class
  215. TODO: needs to be revised as things will change */
  216. browseButton = document.getElementById(up.settings.browse_button);
  217. if (browseButton) {
  218. var hoverClass = up.settings.browse_button_hover,
  219. activeClass = up.settings.browse_button_active,
  220. topElement = up.features.canOpenDialog ? browseButton : inputContainer;
  221. if (hoverClass) {
  222. plupload.addEvent(topElement, 'mouseover', function() {
  223. plupload.addClass(browseButton, hoverClass);
  224. }, up.id);
  225. plupload.addEvent(topElement, 'mouseout', function() {
  226. plupload.removeClass(browseButton, hoverClass);
  227. }, up.id);
  228. }
  229. if (activeClass) {
  230. plupload.addEvent(topElement, 'mousedown', function() {
  231. plupload.addClass(browseButton, activeClass);
  232. }, up.id);
  233. plupload.addEvent(document.body, 'mouseup', function() {
  234. plupload.removeClass(browseButton, activeClass);
  235. }, up.id);
  236. }
  237. // Route click event to the input[type=file] element for supporting browsers
  238. if (up.features.canOpenDialog) {
  239. plupload.addEvent(browseButton, 'click', function(e) {
  240. document.getElementById(up.id + '_html5').click();
  241. e.preventDefault();
  242. }, up.id);
  243. }
  244. }
  245. });
  246. // Add drop handler
  247. uploader.bind("PostInit", function() {
  248. var dropElm = document.getElementById(uploader.settings.drop_element);
  249. if (dropElm) {
  250. // 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
  251. // TODO: Remove this logic once Safari has official drag/drop support
  252. if (fakeSafariDragDrop) {
  253. plupload.addEvent(dropElm, 'dragenter', function(e) {
  254. var dropInputElm, dropPos, dropSize;
  255. // Get or create drop zone
  256. dropInputElm = document.getElementById(uploader.id + "_drop");
  257. if (!dropInputElm) {
  258. dropInputElm = document.createElement("input");
  259. dropInputElm.setAttribute('type', "file");
  260. dropInputElm.setAttribute('id', uploader.id + "_drop");
  261. dropInputElm.setAttribute('multiple', 'multiple');
  262. plupload.addEvent(dropInputElm, 'change', function() {
  263. // Add the selected files from file input
  264. addSelectedFiles(this.files);
  265. // Remove input element
  266. plupload.removeEvent(dropInputElm, 'change', uploader.id);
  267. dropInputElm.parentNode.removeChild(dropInputElm);
  268. }, uploader.id);
  269. dropElm.appendChild(dropInputElm);
  270. }
  271. dropPos = plupload.getPos(dropElm, document.getElementById(uploader.settings.container));
  272. dropSize = plupload.getSize(dropElm);
  273. plupload.extend(dropElm.style, {
  274. position : 'relative'
  275. });
  276. plupload.extend(dropInputElm.style, {
  277. position : 'absolute',
  278. display : 'block',
  279. top : 0,
  280. left : 0,
  281. width : dropSize.w + 'px',
  282. height : dropSize.h + 'px',
  283. opacity : 0
  284. });
  285. }, uploader.id);
  286. return;
  287. }
  288. // Block browser default drag over
  289. plupload.addEvent(dropElm, 'dragover', function(e) {
  290. e.preventDefault();
  291. }, uploader.id);
  292. // Attach drop handler and grab files
  293. plupload.addEvent(dropElm, 'drop', function(e) {
  294. var dataTransfer = e.dataTransfer;
  295. // Add dropped files
  296. if (dataTransfer && dataTransfer.files) {
  297. addSelectedFiles(dataTransfer.files);
  298. }
  299. e.preventDefault();
  300. }, uploader.id);
  301. }
  302. });
  303. uploader.bind("Refresh", function(up) {
  304. var browseButton, browsePos, browseSize, inputContainer, pzIndex;
  305. browseButton = document.getElementById(uploader.settings.browse_button);
  306. if (browseButton) {
  307. browsePos = plupload.getPos(browseButton, document.getElementById(up.settings.container));
  308. browseSize = plupload.getSize(browseButton);
  309. inputContainer = document.getElementById(uploader.id + '_html5_container');
  310. plupload.extend(inputContainer.style, {
  311. top : browsePos.y + 'px',
  312. left : browsePos.x + 'px',
  313. width : browseSize.w + 'px',
  314. height : browseSize.h + 'px'
  315. });
  316. // for IE and WebKit place input element underneath the browse button and route onclick event
  317. // TODO: revise when browser support for this feature will change
  318. if (uploader.features.canOpenDialog) {
  319. pzIndex = parseInt(browseButton.parentNode.style.zIndex, 10);
  320. if (isNaN(pzIndex)) {
  321. pzIndex = 0;
  322. }
  323. plupload.extend(browseButton.style, {
  324. position : 'relative',
  325. zIndex : pzIndex
  326. });
  327. plupload.extend(inputContainer.style, {
  328. zIndex : pzIndex - 1
  329. });
  330. }
  331. }
  332. });
  333. uploader.bind("UploadFile", function(up, file) {
  334. var settings = up.settings, nativeFile, resize;
  335. function sendBinaryBlob(blob) {
  336. var chunk = 0, loaded = 0;
  337. function uploadNextChunk() {
  338. var chunkBlob = blob, xhr, upload, chunks, args, multipartDeltaSize = 0,
  339. boundary = '----pluploadboundary' + plupload.guid(), chunkSize, curChunkSize, formData,
  340. dashdash = '--', crlf = '\r\n', multipartBlob = '', mimeType, url = up.settings.url;
  341. // File upload finished
  342. if (file.status == plupload.DONE || file.status == plupload.FAILED || up.state == plupload.STOPPED) {
  343. return;
  344. }
  345. // Standard arguments
  346. args = {name : file.target_name || file.name};
  347. // Only add chunking args if needed
  348. if (settings.chunk_size && features.chunking) {
  349. chunkSize = settings.chunk_size;
  350. chunks = Math.ceil(file.size / chunkSize);
  351. curChunkSize = Math.min(chunkSize, file.size - (chunk * chunkSize));
  352. // Blob is string so we need to fake chunking, this is not
  353. // ideal since the whole file is loaded into memory
  354. if (typeof(blob) == 'string') {
  355. chunkBlob = blob.substring(chunk * chunkSize, chunk * chunkSize + curChunkSize);
  356. } else {
  357. // Slice the chunk
  358. chunkBlob = blob.slice(chunk * chunkSize, curChunkSize);
  359. }
  360. // Setup query string arguments
  361. args.chunk = chunk;
  362. args.chunks = chunks;
  363. } else {
  364. curChunkSize = file.size;
  365. }
  366. // Setup XHR object
  367. xhr = new XMLHttpRequest();
  368. upload = xhr.upload;
  369. // Do we have upload progress support
  370. if (upload) {
  371. upload.onprogress = function(e) {
  372. file.loaded = Math.min(file.size, loaded + e.loaded - multipartDeltaSize); // Loaded can be larger than file size due to multipart encoding
  373. up.trigger('UploadProgress', file);
  374. };
  375. }
  376. // Add name, chunk and chunks to query string on direct streaming
  377. if (!up.settings.multipart || !features.multipart) {
  378. url = plupload.buildUrl(up.settings.url, args);
  379. } else {
  380. args.name = file.target_name || file.name;
  381. }
  382. xhr.open("post", url, true);
  383. xhr.onreadystatechange = function() {
  384. var httpStatus, chunkArgs;
  385. if (xhr.readyState == 4) {
  386. // Getting the HTTP status might fail on some Gecko versions
  387. try {
  388. httpStatus = xhr.status;
  389. } catch (ex) {
  390. httpStatus = 0;
  391. }
  392. // Is error status
  393. if (httpStatus >= 400) {
  394. up.trigger('Error', {
  395. code : plupload.HTTP_ERROR,
  396. message : plupload.translate('HTTP Error.'),
  397. file : file,
  398. status : httpStatus
  399. });
  400. } else {
  401. // Handle chunk response
  402. if (chunks) {
  403. chunkArgs = {
  404. chunk : chunk,
  405. chunks : chunks,
  406. response : xhr.responseText,
  407. status : httpStatus
  408. };
  409. up.trigger('ChunkUploaded', file, chunkArgs);
  410. loaded += curChunkSize;
  411. // Stop upload
  412. if (chunkArgs.cancelled) {
  413. file.status = plupload.FAILED;
  414. return;
  415. }
  416. file.loaded = Math.min(file.size, (chunk + 1) * chunkSize);
  417. } else {
  418. file.loaded = file.size;
  419. }
  420. up.trigger('UploadProgress', file);
  421. // Check if file is uploaded
  422. if (!chunks || ++chunk >= chunks) {
  423. file.status = plupload.DONE;
  424. up.trigger('FileUploaded', file, {
  425. response : xhr.responseText,
  426. status : httpStatus
  427. });
  428. nativeFile = blob = html5files[file.id] = null; // Free memory
  429. } else {
  430. // Still chunks left
  431. uploadNextChunk();
  432. }
  433. }
  434. xhr = chunkBlob = formData = multipartBlob = null; // Free memory
  435. }
  436. };
  437. // Set custom headers
  438. plupload.each(up.settings.headers, function(value, name) {
  439. xhr.setRequestHeader(name, value);
  440. });
  441. // Build multipart request
  442. if (up.settings.multipart && features.multipart) {
  443. // Has FormData support like Chrome 6+, Safari 5+, Firefox 4
  444. if (!xhr.sendAsBinary) {
  445. formData = new FormData();
  446. // Add multipart params
  447. plupload.each(plupload.extend(args, up.settings.multipart_params), function(value, name) {
  448. formData.append(name, value);
  449. });
  450. // Add file and send it
  451. formData.append(up.settings.file_data_name, chunkBlob);
  452. xhr.send(formData);
  453. return;
  454. }
  455. // Gecko multipart request
  456. xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
  457. // Append multipart parameters
  458. plupload.each(plupload.extend(args, up.settings.multipart_params), function(value, name) {
  459. multipartBlob += dashdash + boundary + crlf +
  460. 'Content-Disposition: form-data; name="' + name + '"' + crlf + crlf;
  461. multipartBlob += unescape(encodeURIComponent(value)) + crlf;
  462. });
  463. mimeType = plupload.mimeTypes[file.name.replace(/^.+\.([^.]+)/, '$1')] || 'application/octet-stream';
  464. // Build RFC2388 blob
  465. multipartBlob += dashdash + boundary + crlf +
  466. 'Content-Disposition: form-data; name="' + up.settings.file_data_name + '"; filename="' + unescape(encodeURIComponent(file.name)) + '"' + crlf +
  467. 'Content-Type: ' + mimeType + crlf + crlf +
  468. chunkBlob + crlf +
  469. dashdash + boundary + dashdash + crlf;
  470. multipartDeltaSize = multipartBlob.length - chunkBlob.length;
  471. chunkBlob = multipartBlob;
  472. } else {
  473. // Binary stream header
  474. xhr.setRequestHeader('Content-Type', 'application/octet-stream');
  475. }
  476. if (xhr.sendAsBinary) {
  477. xhr.sendAsBinary(chunkBlob); // Gecko
  478. } else {
  479. xhr.send(chunkBlob); // WebKit
  480. }
  481. }
  482. // Start uploading chunks
  483. uploadNextChunk();
  484. }
  485. nativeFile = html5files[file.id];
  486. resize = up.settings.resize;
  487. if (features.jpgresize) {
  488. // Resize image if it's a supported format and resize is enabled
  489. if (resize && /\.(png|jpg|jpeg)$/i.test(file.name)) {
  490. scaleImage(nativeFile, resize.width, resize.height, /\.png$/i.test(file.name) ? 'image/png' : 'image/jpeg', function(res) {
  491. // If it was scaled send the scaled image if it failed then
  492. // send the raw image and let the server do the scaling
  493. if (res.success) {
  494. file.size = res.data.length;
  495. sendBinaryBlob(res.data);
  496. } else {
  497. sendBinaryBlob(nativeFile.getAsBinary());
  498. }
  499. });
  500. } else {
  501. sendBinaryBlob(nativeFile.getAsBinary());
  502. }
  503. } else {
  504. sendBinaryBlob(nativeFile);
  505. }
  506. });
  507. uploader.bind('Destroy', function(up) {
  508. var name, element, container = document.body,
  509. elements = {
  510. inputContainer: up.id + '_html5_container',
  511. inputFile: up.id + '_html5',
  512. browseButton: up.settings.browse_button,
  513. dropElm: up.settings.drop_element
  514. };
  515. // Unbind event handlers
  516. for (name in elements) {
  517. element = document.getElementById(elements[name]);
  518. if (element) {
  519. plupload.removeAllEvents(element, up.id);
  520. }
  521. }
  522. plupload.removeAllEvents(document.body, up.id);
  523. if (up.settings.container) {
  524. container = document.getElementById(up.settings.container);
  525. }
  526. // Remove mark-up
  527. container.removeChild(document.getElementById(elements.inputContainer));
  528. });
  529. callback({success : true});
  530. }
  531. });
  532. ExifParser = function() {
  533. // Private ExifParser fields
  534. var Tiff, Exif, GPS, app0, app0_offset, app0_length, app1, app1_offset, data,
  535. app1_length, exifIFD_offset, gpsIFD_offset, IFD0_offset, TIFFHeader_offset, undef,
  536. tiffTags, exifTags, gpsTags, tagDescs;
  537. /**
  538. * @constructor
  539. */
  540. function BinaryReader() {
  541. var II = false, bin;
  542. // Private functions
  543. function read(idx, size) {
  544. var mv = II ? 0 : -8 * (size - 1), sum = 0, i;
  545. for (i = 0; i < size; i++) {
  546. sum |= (bin.charCodeAt(idx + i) << Math.abs(mv + i*8));
  547. }
  548. return sum;
  549. }
  550. function putstr(idx, segment, replace) {
  551. bin = bin.substr(0, idx) + segment + bin.substr((replace === true ? segment.length : 0) + idx);
  552. }
  553. function write(idx, num, size) {
  554. var str = '', mv = II ? 0 : -8 * (size - 1), i;
  555. for (i = 0; i < size; i++) {
  556. str += String.fromCharCode((num >> Math.abs(mv + i*8)) & 255);
  557. }
  558. putstr(idx, str, true);
  559. }
  560. // Public functions
  561. return {
  562. II: function(order) {
  563. if (order === undef) {
  564. return II;
  565. } else {
  566. II = order;
  567. }
  568. },
  569. init: function(binData) {
  570. bin = binData;
  571. },
  572. SEGMENT: function(idx, segment, replace) {
  573. if (!arguments.length) {
  574. return bin;
  575. }
  576. if (typeof segment == 'number') {
  577. return bin.substr(parseInt(idx, 10), segment);
  578. }
  579. putstr(idx, segment, replace);
  580. },
  581. BYTE: function(idx) {
  582. return read(idx, 1);
  583. },
  584. SHORT: function(idx) {
  585. return read(idx, 2);
  586. },
  587. LONG: function(idx, num) {
  588. if (num === undef) {
  589. return read(idx, 4);
  590. } else {
  591. write(idx, num, 4);
  592. }
  593. },
  594. SLONG: function(idx) { // 2's complement notation
  595. var num = read(idx, 4);
  596. return (num > 2147483647 ? num - 4294967296 : num);
  597. },
  598. STRING: function(idx, size) {
  599. var str = '';
  600. for (size += idx; idx < size; idx++) {
  601. str += String.fromCharCode(read(idx, 1));
  602. }
  603. return str;
  604. }
  605. };
  606. }
  607. data = new BinaryReader();
  608. tiffTags = {
  609. /*
  610. The image orientation viewed in terms of rows and columns.
  611. 1 - The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
  612. 2 - The 0th row is at the visual top of the image, and the 0th column is the visual left-hand side.
  613. 3 - The 0th row is at the visual top of the image, and the 0th column is the visual right-hand side.
  614. 4 - The 0th row is at the visual bottom of the image, and the 0th column is the visual right-hand side.
  615. 5 - The 0th row is at the visual bottom of the image, and the 0th column is the visual left-hand side.
  616. 6 - The 0th row is the visual left-hand side of the image, and the 0th column is the visual top.
  617. 7 - The 0th row is the visual right-hand side of the image, and the 0th column is the visual top.
  618. 8 - The 0th row is the visual right-hand side of the image, and the 0th column is the visual bottom.
  619. 9 - The 0th row is the visual left-hand side of the image, and the 0th column is the visual bottom.
  620. */
  621. 0x0112: 'Orientation',
  622. 0x8769: 'ExifIFDPointer',
  623. 0x8825: 'GPSInfoIFDPointer'
  624. };
  625. exifTags = {
  626. 0x9000: 'ExifVersion',
  627. 0xA001: 'ColorSpace',
  628. 0xA002: 'PixelXDimension',
  629. 0xA003: 'PixelYDimension',
  630. 0x9003: 'DateTimeOriginal',
  631. 0x829A: 'ExposureTime',
  632. 0x829D: 'FNumber',
  633. 0x8827: 'ISOSpeedRatings',
  634. 0x9201: 'ShutterSpeedValue',
  635. 0x9202: 'ApertureValue' ,
  636. 0x9207: 'MeteringMode',
  637. 0x9208: 'LightSource',
  638. 0x9209: 'Flash',
  639. 0xA402: 'ExposureMode',
  640. 0xA403: 'WhiteBalance',
  641. 0xA406: 'SceneCaptureType',
  642. 0xA404: 'DigitalZoomRatio',
  643. 0xA408: 'Contrast',
  644. 0xA409: 'Saturation',
  645. 0xA40A: 'Sharpness'
  646. };
  647. gpsTags = {
  648. 0x0000: 'GPSVersionID',
  649. 0x0001: 'GPSLatitudeRef',
  650. 0x0002: 'GPSLatitude',
  651. 0x0003: 'GPSLongitudeRef',
  652. 0x0004: 'GPSLongitude'
  653. };
  654. tagDescs = {
  655. 'ColorSpace': {
  656. 1: 'sRGB',
  657. 0: 'Uncalibrated'
  658. },
  659. 'MeteringMode': {
  660. 0: 'Unknown',
  661. 1: 'Average',
  662. 2: 'CenterWeightedAverage',
  663. 3: 'Spot',
  664. 4: 'MultiSpot',
  665. 5: 'Pattern',
  666. 6: 'Partial',
  667. 255: 'Other'
  668. },
  669. 'LightSource': {
  670. 1: 'Daylight',
  671. 2: 'Fliorescent',
  672. 3: 'Tungsten',
  673. 4: 'Flash',
  674. 9: 'Fine weather',
  675. 10: 'Cloudy weather',
  676. 11: 'Shade',
  677. 12: 'Daylight fluorescent (D 5700 - 7100K)',
  678. 13: 'Day white fluorescent (N 4600 -5400K)',
  679. 14: 'Cool white fluorescent (W 3900 - 4500K)',
  680. 15: 'White fluorescent (WW 3200 - 3700K)',
  681. 17: 'Standard light A',
  682. 18: 'Standard light B',
  683. 19: 'Standard light C',
  684. 20: 'D55',
  685. 21: 'D65',
  686. 22: 'D75',
  687. 23: 'D50',
  688. 24: 'ISO studio tungsten',
  689. 255: 'Other'
  690. },
  691. 'Flash': {
  692. 0x0000: 'Flash did not fire.',
  693. 0x0001: 'Flash fired.',
  694. 0x0005: 'Strobe return light not detected.',
  695. 0x0007: 'Strobe return light detected.',
  696. 0x0009: 'Flash fired, compulsory flash mode',
  697. 0x000D: 'Flash fired, compulsory flash mode, return light not detected',
  698. 0x000F: 'Flash fired, compulsory flash mode, return light detected',
  699. 0x0010: 'Flash did not fire, compulsory flash mode',
  700. 0x0018: 'Flash did not fire, auto mode',
  701. 0x0019: 'Flash fired, auto mode',
  702. 0x001D: 'Flash fired, auto mode, return light not detected',
  703. 0x001F: 'Flash fired, auto mode, return light detected',
  704. 0x0020: 'No flash function',
  705. 0x0041: 'Flash fired, red-eye reduction mode',
  706. 0x0045: 'Flash fired, red-eye reduction mode, return light not detected',
  707. 0x0047: 'Flash fired, red-eye reduction mode, return light detected',
  708. 0x0049: 'Flash fired, compulsory flash mode, red-eye reduction mode',
  709. 0x004D: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
  710. 0x004F: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
  711. 0x0059: 'Flash fired, auto mode, red-eye reduction mode',
  712. 0x005D: 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
  713. 0x005F: 'Flash fired, auto mode, return light detected, red-eye reduction mode'
  714. },
  715. 'ExposureMode': {
  716. 0: 'Auto exposure',
  717. 1: 'Manual exposure',
  718. 2: 'Auto bracket'
  719. },
  720. 'WhiteBalance': {
  721. 0: 'Auto white balance',
  722. 1: 'Manual white balance'
  723. },
  724. 'SceneCaptureType': {
  725. 0: 'Standard',
  726. 1: 'Landscape',
  727. 2: 'Portrait',
  728. 3: 'Night scene'
  729. },
  730. 'Contrast': {
  731. 0: 'Normal',
  732. 1: 'Soft',
  733. 2: 'Hard'
  734. },
  735. 'Saturation': {
  736. 0: 'Normal',
  737. 1: 'Low saturation',
  738. 2: 'High saturation'
  739. },
  740. 'Sharpness': {
  741. 0: 'Normal',
  742. 1: 'Soft',
  743. 2: 'Hard'
  744. },
  745. // GPS related
  746. 'GPSLatitudeRef': {
  747. N: 'North latitude',
  748. S: 'South latitude'
  749. },
  750. 'GPSLongitudeRef': {
  751. E: 'East longitude',
  752. W: 'West longitude'
  753. }
  754. };
  755. function extractTags(IFD_offset, tags2extract) {
  756. var length = data.SHORT(IFD_offset), i, ii,
  757. tag, type, count, tagOffset, offset, value, values = [], tags = {};
  758. for (i = 0; i < length; i++) {
  759. // Set binary reader pointer to beginning of the next tag
  760. offset = tagOffset = IFD_offset + 12 * i + 2;
  761. tag = tags2extract[data.SHORT(offset)];
  762. if (tag === undef) {
  763. continue; // Not the tag we requested
  764. }
  765. type = data.SHORT(offset+=2);
  766. count = data.LONG(offset+=2);
  767. offset += 4;
  768. values = [];
  769. switch (type) {
  770. case 1: // BYTE
  771. case 7: // UNDEFINED
  772. if (count > 4) {
  773. offset = data.LONG(offset) + TIFFHeader_offset;
  774. }
  775. for (ii = 0; ii < count; ii++) {
  776. values[ii] = data.BYTE(offset + ii);
  777. }
  778. break;
  779. case 2: // STRING
  780. if (count > 4) {
  781. offset = data.LONG(offset) + TIFFHeader_offset;
  782. }
  783. tags[tag] = data.STRING(offset, count - 1);
  784. continue;
  785. case 3: // SHORT
  786. if (count > 2) {
  787. offset = data.LONG(offset) + TIFFHeader_offset;
  788. }
  789. for (ii = 0; ii < count; ii++) {
  790. values[ii] = data.SHORT(offset + ii*2);
  791. }
  792. break;
  793. case 4: // LONG
  794. if (count > 1) {
  795. offset = data.LONG(offset) + TIFFHeader_offset;
  796. }
  797. for (ii = 0; ii < count; ii++) {
  798. values[ii] = data.LONG(offset + ii*4);
  799. }
  800. break;
  801. case 5: // RATIONAL
  802. offset = data.LONG(offset) + TIFFHeader_offset;
  803. for (ii = 0; ii < count; ii++) {
  804. values[ii] = data.LONG(offset + ii*4) / data.LONG(offset + ii*4 + 4);
  805. }
  806. break;
  807. case 9: // SLONG
  808. offset = data.LONG(offset) + TIFFHeader_offset;
  809. for (ii = 0; ii < count; ii++) {
  810. values[ii] = data.SLONG(offset + ii*4);
  811. }
  812. break;
  813. case 10: // SRATIONAL
  814. offset = data.LONG(offset) + TIFFHeader_offset;
  815. for (ii = 0; ii < count; ii++) {
  816. values[ii] = data.SLONG(offset + ii*4) / data.SLONG(offset + ii*4 + 4);
  817. }
  818. break;
  819. default:
  820. continue;
  821. }
  822. value = (count == 1 ? values[0] : values);
  823. if (tagDescs.hasOwnProperty(tag) && typeof value != 'object') {
  824. tags[tag] = tagDescs[tag][value];
  825. } else {
  826. tags[tag] = value;
  827. }
  828. }
  829. return tags;
  830. }
  831. function getIFDOffsets() {
  832. var idx = app1_offset + 4;
  833. // Fix TIFF header offset
  834. TIFFHeader_offset += app1_offset;
  835. // Check if that's EXIF we are reading
  836. if (data.STRING(idx, 4).toUpperCase() !== 'EXIF' || data.SHORT(idx+=4) !== 0) {
  837. return;
  838. }
  839. // Set read order of multi-byte data
  840. data.II(data.SHORT(idx+=2) == 0x4949);
  841. // Check if always present bytes are indeed present
  842. if (data.SHORT(idx+=2) !== 0x002A) {
  843. return;
  844. }
  845. IFD0_offset = TIFFHeader_offset + data.LONG(idx += 2);
  846. Tiff = extractTags(IFD0_offset, tiffTags);
  847. exifIFD_offset = ('ExifIFDPointer' in Tiff ? TIFFHeader_offset + Tiff.ExifIFDPointer : undef);
  848. gpsIFD_offset = ('GPSInfoIFDPointer' in Tiff ? TIFFHeader_offset + Tiff.GPSInfoIFDPointer : undef);
  849. return true;
  850. }
  851. function findTagValueOffset(data_app1, tegHex, offset) {
  852. var length = data_app1.SHORT(offset), tagOffset, i;
  853. for (i = 0; i < length; i++) {
  854. tagOffset = offset + 12 * i + 2;
  855. if (data_app1.SHORT(tagOffset) == tegHex) {
  856. return tagOffset + 8;
  857. }
  858. }
  859. }
  860. function setNewWxH(width, height) {
  861. var w_offset, h_offset,
  862. offset = exifIFD_offset != undef ? exifIFD_offset - app1_offset : undef,
  863. data_app1 = new BinaryReader();
  864. data_app1.init(app1);
  865. data_app1.II(data.II());
  866. if (offset === undef) {
  867. return;
  868. }
  869. // Find offset for PixelXDimension tag
  870. w_offset = findTagValueOffset(data_app1, 0xA002, offset);
  871. if (w_offset !== undef) {
  872. data_app1.LONG(w_offset, width);
  873. }
  874. // Find offset for PixelYDimension tag
  875. h_offset = findTagValueOffset(data_app1, 0xA003, offset);
  876. if (h_offset !== undef) {
  877. data_app1.LONG(h_offset, height);
  878. }
  879. app1 = data_app1.SEGMENT();
  880. }
  881. // Public functions
  882. return {
  883. init: function(jpegData) {
  884. // Reset internal data
  885. TIFFHeader_offset = 10;
  886. Tiff = Exif = GPS = app0 = app0_offset = app0_length = app1 = app1_offset = app1_length = undef;
  887. data.init(jpegData);
  888. // Check if data is jpeg
  889. if (data.SHORT(0) !== 0xFFD8) {
  890. return false;
  891. }
  892. switch (data.SHORT(2)) {
  893. // app0
  894. case 0xFFE0:
  895. app0_offset = 2;
  896. app0_length = data.SHORT(4) + 2;
  897. // check if app1 follows
  898. if (data.SHORT(app0_length) == 0xFFE1) {
  899. app1_offset = app0_length;
  900. app1_length = data.SHORT(app0_length + 2) + 2;
  901. }
  902. break;
  903. // app1
  904. case 0xFFE1:
  905. app1_offset = 2;
  906. app1_length = data.SHORT(4) + 2;
  907. break;
  908. default:
  909. return false;
  910. }
  911. if (app1_length !== undef) {
  912. getIFDOffsets();
  913. }
  914. },
  915. APP1: function(args) {
  916. if (app1_offset === undef && app1_length === undef) {
  917. return;
  918. }
  919. app1 = app1 || (app1 = data.SEGMENT(app1_offset, app1_length));
  920. // If requested alter width/height tags in app1
  921. if (args !== undef && 'width' in args && 'height' in args) {
  922. setNewWxH(args.width, args.height);
  923. }
  924. return app1;
  925. },
  926. EXIF: function() {
  927. // Populate EXIF hash
  928. Exif = extractTags(exifIFD_offset, exifTags);
  929. // Fix formatting of some tags
  930. Exif.ExifVersion = String.fromCharCode(
  931. Exif.ExifVersion[0],
  932. Exif.ExifVersion[1],
  933. Exif.ExifVersion[2],
  934. Exif.ExifVersion[3]
  935. );
  936. return Exif;
  937. },
  938. GPS: function() {
  939. GPS = extractTags(gpsIFD_offset, gpsTags);
  940. GPS.GPSVersionID = GPS.GPSVersionID.join('.');
  941. return GPS;
  942. },
  943. setAPP1: function(data_app1) {
  944. if (app1_offset !== undef) {
  945. return false;
  946. }
  947. data.SEGMENT((app0_offset ? app0_offset + app0_length : 2), data_app1);
  948. },
  949. getBinary: function() {
  950. return data.SEGMENT();
  951. }
  952. };
  953. };
  954. })(window, document, plupload);