Browse Source

Refactor resizeImage method (#7236)

- Use URL.createObjectURL (replace from FileReader)
- Use HTMLCanvasElement.prototype.toBlob
  (replace from HTMLCanvasElement.prototype.toDataURL)
- Use Promise (replace callback interface)
Yamagishi Kazutoshi 6 years ago
parent
commit
0758b00bfd

+ 7 - 85
app/javascript/mastodon/actions/compose.js

@@ -4,6 +4,7 @@ import { throttle } from 'lodash';
 import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
 import { tagHistory } from '../settings';
 import { useEmoji } from './emojis';
+import resizeImage from '../utils/resize_image';
 import { importFetchedAccounts } from './importer';
 import { updateTimeline } from './timelines';
 import { showAlertForError } from './alerts';
@@ -174,79 +175,6 @@ export function submitComposeFail(error) {
   };
 };
 
-const MAX_IMAGE_DIMENSION = 1280;
-
-const dataURLtoBlob = dataURL => {
-  const BASE64_MARKER = ';base64,';
-
-  if (dataURL.indexOf(BASE64_MARKER) === -1) {
-    const parts       = dataURL.split(',');
-    const contentType = parts[0].split(':')[1];
-    const raw         = parts[1];
-
-    return new Blob([raw], { type: contentType });
-  }
-
-  const parts       = dataURL.split(BASE64_MARKER);
-  const contentType = parts[0].split(':')[1];
-  const raw         = window.atob(parts[1]);
-  const rawLength   = raw.length;
-
-  const uInt8Array = new Uint8Array(rawLength);
-
-  for (let i = 0; i < rawLength; ++i) {
-    uInt8Array[i] = raw.charCodeAt(i);
-  }
-
-  return new Blob([uInt8Array], { type: contentType });
-};
-
-const resizeImage = (inputFile, callback) => {
-  if (inputFile.type.match(/image.*/) && inputFile.type !== 'image/gif') {
-    const reader = new FileReader();
-
-    reader.onload = e => {
-      const img = new Image();
-
-      img.onload = () => {
-        const canvas = document.createElement('canvas');
-        const { width, height } = img;
-
-        let newWidth, newHeight;
-
-        if (width < MAX_IMAGE_DIMENSION && height < MAX_IMAGE_DIMENSION) {
-          callback(inputFile);
-          return;
-        }
-
-        if (width > height) {
-          newHeight = height * MAX_IMAGE_DIMENSION / width;
-          newWidth  = MAX_IMAGE_DIMENSION;
-        } else if (height > width) {
-          newWidth  = width * MAX_IMAGE_DIMENSION / height;
-          newHeight = MAX_IMAGE_DIMENSION;
-        } else {
-          newWidth  = MAX_IMAGE_DIMENSION;
-          newHeight = MAX_IMAGE_DIMENSION;
-        }
-
-        canvas.width  = newWidth;
-        canvas.height = newHeight;
-
-        canvas.getContext('2d').drawImage(img, 0, 0, newWidth, newHeight);
-
-        callback(dataURLtoBlob(canvas.toDataURL(inputFile.type)));
-      };
-
-      img.src = e.target.result;
-    };
-
-    reader.readAsDataURL(inputFile);
-  } else {
-    callback(inputFile);
-  }
-};
-
 export function uploadCompose(files) {
   return function (dispatch, getState) {
     if (getState().getIn(['compose', 'media_attachments']).size > 3) {
@@ -255,20 +183,14 @@ export function uploadCompose(files) {
 
     dispatch(uploadComposeRequest());
 
-    resizeImage(files[0], file => {
-      let data = new FormData();
+    resizeImage(files[0]).then(file => {
+      const data = new FormData();
       data.append('file', file);
 
-      api(getState).post('/api/v1/media', data, {
-        onUploadProgress: function (e) {
-          dispatch(uploadComposeProgress(e.loaded, e.total));
-        },
-      }).then(function (response) {
-        dispatch(uploadComposeSuccess(response.data));
-      }).catch(function (error) {
-        dispatch(uploadComposeFail(error));
-      });
-    });
+      return api(getState).post('/api/v1/media', data, {
+        onUploadProgress: ({ loaded, total }) => dispatch(uploadComposeProgress(loaded, total)),
+      }).then(({ data }) => dispatch(uploadComposeSuccess(data)));
+    }).catch(error => dispatch(uploadComposeFail(error)));
   };
 };
 

+ 2 - 7
app/javascript/mastodon/actions/push_notifications/registerer.js

@@ -1,4 +1,5 @@
 import api from '../../api';
+import { decode as decodeBase64 } from '../../utils/base64';
 import { pushNotificationsSetting } from '../../settings';
 import { setBrowserSupport, setSubscription, clearSubscription } from './setter';
 import { me } from '../../initial_state';
@@ -10,13 +11,7 @@ const urlBase64ToUint8Array = (base64String) => {
     .replace(/\-/g, '+')
     .replace(/_/g, '/');
 
-  const rawData = window.atob(base64);
-  const outputArray = new Uint8Array(rawData.length);
-
-  for (let i = 0; i < rawData.length; ++i) {
-    outputArray[i] = rawData.charCodeAt(i);
-  }
-  return outputArray;
+  return decodeBase64(base64);
 };
 
 const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');

+ 21 - 0
app/javascript/mastodon/base_polyfills.js

@@ -5,6 +5,7 @@ import includes from 'array-includes';
 import assign from 'object-assign';
 import values from 'object.values';
 import isNaN from 'is-nan';
+import { decode as decodeBase64 } from './utils/base64';
 
 if (!Array.prototype.includes) {
   includes.shim();
@@ -21,3 +22,23 @@ if (!Object.values) {
 if (!Number.isNaN) {
   Number.isNaN = isNaN;
 }
+
+if (!HTMLCanvasElement.prototype.toBlob) {
+  const BASE64_MARKER = ';base64,';
+
+  Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
+    value(callback, type = 'image/png', quality) {
+      const dataURL = this.toDataURL(type, quality);
+      let data;
+
+      if (dataURL.indexOf(BASE64_MARKER) >= 0) {
+        const [, base64] = dataURL.split(BASE64_MARKER);
+        data = decodeBase64(base64);
+      } else {
+        [, data] = dataURL.split(',');
+      }
+
+      callback(new Blob([data], { type }));
+    },
+  });
+}

+ 4 - 3
app/javascript/mastodon/load_polyfills.js

@@ -12,12 +12,13 @@ function importExtraPolyfills() {
 
 function loadPolyfills() {
   const needsBasePolyfills = !(
+    Array.prototype.includes &&
+    HTMLCanvasElement.prototype.toBlob &&
     window.Intl &&
+    Number.isNaN &&
     Object.assign &&
     Object.values &&
-    Number.isNaN &&
-    window.Symbol &&
-    Array.prototype.includes
+    window.Symbol
   );
 
   // Latest version of Firefox and Safari do not have IntersectionObserver.

+ 10 - 0
app/javascript/mastodon/utils/__tests__/base64-test.js

@@ -0,0 +1,10 @@
+import * as base64 from '../base64';
+
+describe('base64', () => {
+  describe('decode', () => {
+    it('returns a uint8 array', () => {
+      const arr = base64.decode('dGVzdA==');
+      expect(arr).toEqual(new Uint8Array([116, 101, 115, 116]));
+    });
+  });
+});

+ 10 - 0
app/javascript/mastodon/utils/base64.js

@@ -0,0 +1,10 @@
+export const decode = base64 => {
+  const rawData = window.atob(base64);
+  const outputArray = new Uint8Array(rawData.length);
+
+  for (let i = 0; i < rawData.length; ++i) {
+    outputArray[i] = rawData.charCodeAt(i);
+  }
+
+  return outputArray;
+};

+ 66 - 0
app/javascript/mastodon/utils/resize_image.js

@@ -0,0 +1,66 @@
+const MAX_IMAGE_DIMENSION = 1280;
+
+const getImageUrl = inputFile => new Promise((resolve, reject) => {
+  if (window.URL && URL.createObjectURL) {
+    try {
+      resolve(URL.createObjectURL(inputFile));
+    } catch (error) {
+      reject(error);
+    }
+    return;
+  }
+
+  const reader = new FileReader();
+  reader.onerror = (...args) => reject(...args);
+  reader.onload  = ({ target }) => resolve(target.result);
+
+  reader.readAsDataURL(inputFile);
+});
+
+const loadImage = inputFile => new Promise((resolve, reject) => {
+  getImageUrl(inputFile).then(url => {
+    const img = new Image();
+
+    img.onerror = (...args) => reject(...args);
+    img.onload  = () => resolve(img);
+
+    img.src = url;
+  }).catch(reject);
+});
+
+export default inputFile => new Promise((resolve, reject) => {
+  if (!inputFile.type.match(/image.*/) || inputFile.type === 'image/gif') {
+    resolve(inputFile);
+    return;
+  }
+
+  loadImage(inputFile).then(img => {
+    const canvas = document.createElement('canvas');
+    const { width, height } = img;
+
+    let newWidth, newHeight;
+
+    if (width < MAX_IMAGE_DIMENSION && height < MAX_IMAGE_DIMENSION) {
+      resolve(inputFile);
+      return;
+    }
+
+    if (width > height) {
+      newHeight = height * MAX_IMAGE_DIMENSION / width;
+      newWidth  = MAX_IMAGE_DIMENSION;
+    } else if (height > width) {
+      newWidth  = width * MAX_IMAGE_DIMENSION / height;
+      newHeight = MAX_IMAGE_DIMENSION;
+    } else {
+      newWidth  = MAX_IMAGE_DIMENSION;
+      newHeight = MAX_IMAGE_DIMENSION;
+    }
+
+    canvas.width  = newWidth;
+    canvas.height = newHeight;
+
+    canvas.getContext('2d').drawImage(img, 0, 0, newWidth, newHeight);
+
+    canvas.toBlob(resolve, inputFile.type);
+  }).catch(reject);
+});