From 0758b00bfddf4a2c845d0d611e50717a268fd48a Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Mon, 23 Apr 2018 16:15:51 +0900 Subject: [PATCH] Refactor resizeImage method (#7236) - Use URL.createObjectURL (replace from FileReader) - Use HTMLCanvasElement.prototype.toBlob (replace from HTMLCanvasElement.prototype.toDataURL) - Use Promise (replace callback interface) --- app/javascript/mastodon/actions/compose.js | 92 ++----------------- .../actions/push_notifications/registerer.js | 9 +- app/javascript/mastodon/base_polyfills.js | 21 +++++ app/javascript/mastodon/load_polyfills.js | 7 +- .../mastodon/utils/__tests__/base64-test.js | 10 ++ app/javascript/mastodon/utils/base64.js | 10 ++ app/javascript/mastodon/utils/resize_image.js | 66 +++++++++++++ 7 files changed, 120 insertions(+), 95 deletions(-) create mode 100644 app/javascript/mastodon/utils/__tests__/base64-test.js create mode 100644 app/javascript/mastodon/utils/base64.js create mode 100644 app/javascript/mastodon/utils/resize_image.js diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index c8ea5aaba7..fe3e831d5f 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/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))); }; }; diff --git a/app/javascript/mastodon/actions/push_notifications/registerer.js b/app/javascript/mastodon/actions/push_notifications/registerer.js index 60b215f028..82fe4519a2 100644 --- a/app/javascript/mastodon/actions/push_notifications/registerer.js +++ b/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'); diff --git a/app/javascript/mastodon/base_polyfills.js b/app/javascript/mastodon/base_polyfills.js index 8fbb177858..997813a043 100644 --- a/app/javascript/mastodon/base_polyfills.js +++ b/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 })); + }, + }); +} diff --git a/app/javascript/mastodon/load_polyfills.js b/app/javascript/mastodon/load_polyfills.js index 815e1905b8..8cb81c1a61 100644 --- a/app/javascript/mastodon/load_polyfills.js +++ b/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. diff --git a/app/javascript/mastodon/utils/__tests__/base64-test.js b/app/javascript/mastodon/utils/__tests__/base64-test.js new file mode 100644 index 0000000000..1b3260faaf --- /dev/null +++ b/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])); + }); + }); +}); diff --git a/app/javascript/mastodon/utils/base64.js b/app/javascript/mastodon/utils/base64.js new file mode 100644 index 0000000000..8226e2c54e --- /dev/null +++ b/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; +}; diff --git a/app/javascript/mastodon/utils/resize_image.js b/app/javascript/mastodon/utils/resize_image.js new file mode 100644 index 0000000000..6442eda38c --- /dev/null +++ b/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); +});