PageRenderTime 64ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/src/javascript/plupload.html5.js

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