PageRenderTime 66ms CodeModel.GetById 31ms RepoModel.GetById 0ms app.codeStats 0ms

/src/javascript/plupload.html5.js

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