Refactor resizeImage method (#7236)
- Use URL.createObjectURL (replace from FileReader) - Use HTMLCanvasElement.prototype.toBlob (replace from HTMLCanvasElement.prototype.toDataURL) - Use Promise (replace callback interface)
This commit is contained in:
		
				
					committed by
					
						 Eugen Rochko
						Eugen Rochko
					
				
			
			
				
	
			
			
			
						parent
						
							660cb058e1
						
					
				
				
					commit
					0758b00bfd
				
			| @@ -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))); | ||||
|   }; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -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'); | ||||
|   | ||||
| @@ -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 })); | ||||
|     }, | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -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
									
								
								app/javascript/mastodon/utils/__tests__/base64-test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/javascript/mastodon/utils/__tests__/base64-test.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								app/javascript/mastodon/utils/base64.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/javascript/mastodon/utils/base64.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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
									
								
								app/javascript/mastodon/utils/resize_image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								app/javascript/mastodon/utils/resize_image.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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); | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user