From 396a2ca319c256fcefb154bd5129684c5e4dac58 Mon Sep 17 00:00:00 2001 From: Ndibe Raymond Olisaemeka Date: Fri, 22 Jan 2021 04:39:06 +0100 Subject: [PATCH] fixed issue #34: Increased upload image size, added image compression and functionality to remove image metadata (#79) --- zubhub_frontend/zubhub/package-lock.json | 27 ++++++ zubhub_frontend/zubhub/package.json | 2 + .../zubhub/src/assets/js/Compress.js | 39 ++++++++ .../src/assets/js/removeMetaDataWorker.js | 62 ++++++++++++ .../views/create_project/CreateProject.jsx | 94 ++++++++++++------- 5 files changed, 192 insertions(+), 32 deletions(-) create mode 100644 zubhub_frontend/zubhub/src/assets/js/Compress.js create mode 100644 zubhub_frontend/zubhub/src/assets/js/removeMetaDataWorker.js diff --git a/zubhub_frontend/zubhub/package-lock.json b/zubhub_frontend/zubhub/package-lock.json index b1c50af10..20d60cf5d 100644 --- a/zubhub_frontend/zubhub/package-lock.json +++ b/zubhub_frontend/zubhub/package-lock.json @@ -3424,6 +3424,11 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, + "blueimp-canvas-to-blob": { + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/blueimp-canvas-to-blob/-/blueimp-canvas-to-blob-3.28.0.tgz", + "integrity": "sha512-5q+YHzgGsuHQ01iouGgJaPJXod2AzTxJXmVv90PpGrRxU7G7IqgPqWXz+PBmt3520jKKi6irWbNV87DicEa7wg==" + }, "bn.js": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", @@ -4175,6 +4180,15 @@ } } }, + "compressorjs": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/compressorjs/-/compressorjs-1.0.7.tgz", + "integrity": "sha512-ca+H8CGrn0LG103//VQmXBbNdvzvHiW26LGdWncp4RmLNbNQjaaFWIUxMN9++hbhGobLtofkHoxzzXGisNyD3w==", + "requires": { + "blueimp-canvas-to-blob": "^3.28.0", + "is-blob": "^2.1.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7709,6 +7723,11 @@ "binary-extensions": "^2.0.0" } }, + "is-blob": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-blob/-/is-blob-2.1.0.tgz", + "integrity": "sha512-SZ/fTft5eUhQM6oF/ZaASFDEdbFVe89Imltn9uZr03wdKMcWNVYSMjQPFtg05QuNkt5l5c135ElvXEQG0rk4tw==" + }, "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", @@ -16428,6 +16447,14 @@ "microevent.ts": "~0.1.1" } }, + "workerize-loader": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/workerize-loader/-/workerize-loader-1.3.0.tgz", + "integrity": "sha512-utWDc8K6embcICmRBUUkzanPgKBb8yM1OHfh6siZfiMsswE8wLCa9CWS+L7AARz0+Th4KH4ZySrqer/OJ9WuWw==", + "requires": { + "loader-utils": "^2.0.0" + } + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/zubhub_frontend/zubhub/package.json b/zubhub_frontend/zubhub/package.json index c24a548ff..28db15799 100644 --- a/zubhub_frontend/zubhub/package.json +++ b/zubhub_frontend/zubhub/package.json @@ -12,6 +12,7 @@ "@testing-library/user-event": "^12.2.2", "aws-sdk": "^2.813.0", "classnames": "^2.2.6", + "compressorjs": "^1.0.7", "date-fns": "^2.16.1", "formik": "^2.2.5", "nanoid": "^3.1.20", @@ -27,6 +28,7 @@ "redux-thunk": "^2.3.0", "slick-carousel": "^1.8.1", "web-vitals": "^0.2.4", + "workerize-loader": "^1.3.0", "yup": "^0.29.3" }, "scripts": { diff --git a/zubhub_frontend/zubhub/src/assets/js/Compress.js b/zubhub_frontend/zubhub/src/assets/js/Compress.js new file mode 100644 index 000000000..f4aec897c --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/Compress.js @@ -0,0 +1,39 @@ +import Compressor from 'compressorjs'; + +const Compress = (images, state, handleSetState) => { + let compressed = []; + + for (let index = 0; index < images.length; index += 1) { + let image = images[index]; + + if (image && image.type.split('/')[1] !== 'gif') { + new Compressor(image, { + quality: 0.6, + convertSize: 100000, + success: result => { + compressed.push(result); + shouldSetImages(compressed, images, state, handleSetState); + }, + error: error => { + console.warn(error.message); + compressed.push(image); + shouldSetImages(compressed, images, state, handleSetState); + }, + }); + } else { + compressed.push(image); + shouldSetImages(compressed, images, state, handleSetState); + } + } +}; + +const shouldSetImages = (compressed, images, state, handleSetState) => { + if (compressed.length === images.length) { + const { image_upload } = state; + image_upload.images_to_upload = compressed; + + handleSetState(image_upload); + } +}; + +export default Compress; diff --git a/zubhub_frontend/zubhub/src/assets/js/removeMetaDataWorker.js b/zubhub_frontend/zubhub/src/assets/js/removeMetaDataWorker.js new file mode 100644 index 000000000..166abf3f2 --- /dev/null +++ b/zubhub_frontend/zubhub/src/assets/js/removeMetaDataWorker.js @@ -0,0 +1,62 @@ +export const removeMetaData = imageArr => { + let newImageArr = []; + + for (let index = 0; index < imageArr.length; index++) { + let fr = new FileReader(); + fr.onload = process; + fr.mainFile = imageArr[index]; + fr.readAsArrayBuffer(imageArr[index]); + } + + function process() { + let dv = new DataView(this.result); + let offset = 0, + recess = 0; + let pieces = []; + let i = 0; + if (dv.getUint16(offset) === 0xffd8) { + offset += 2; + let app1 = dv.getUint16(offset); + offset += 2; + while (offset < dv.byteLength) { + if (app1 === 0xffe1) { + pieces[i] = { recess: recess, offset: offset - 2 }; + recess = offset + dv.getUint16(offset); + i++; + } else if (app1 === 0xffda) { + break; + } + offset += dv.getUint16(offset); + app1 = dv.getUint16(offset); + offset += 2; + } + + if (pieces.length > 0) { + let newPieces = []; + pieces.forEach(function (v) { + newPieces.push(this.result.slice(v.recess, v.offset)); + }, this); + newPieces.push(this.result.slice(recess)); + newImageArr.push( + new Blob(newPieces, { type: imageArr[newImageArr.length].type }), + ); + + if (newImageArr.length === imageArr.length) { + postMessage(newImageArr); + } + } else { + newImageArr.push(this.mainFile); + + if (newImageArr.length === imageArr.length) { + postMessage(newImageArr); + } + } + } else { + newImageArr.push(this.mainFile); + + if (newImageArr.length === imageArr.length) { + postMessage(newImageArr); + } + } + } +}; diff --git a/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx b/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx index 84864e0c7..539e629bf 100644 --- a/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx +++ b/zubhub_frontend/zubhub/src/views/create_project/CreateProject.jsx @@ -33,6 +33,8 @@ import { import * as ProjectActions from '../../store/actions/projectActions'; import ErrorPage from '../error/ErrorPage'; import DO, { doConfig } from '../../assets/js/DO'; +import worker from 'workerize-loader!../../assets/js/removeMetaDataWorker'; // eslint-disable-line import/no-webpack-loader-syntax +import Compress from '../../assets/js/Compress'; import { useStateUpdateCallback } from '../../assets/js/customHooks'; import CustomButton from '../../components/button/Button'; import styles from '../../assets/js/styles/views/create_project/createProjectStyles'; @@ -42,10 +44,23 @@ const useStyles = makeStyles(styles); let image_field_touched = false; let video_field_touched = false; -const handleImageFieldChange = (e, refs, props) => { - props.setFieldValue(e.currentTarget.name, refs.imageEl.current); +const handleImageFieldChange = (refs, props, state, handleSetState) => { refs.imageCountEl.current.innerText = refs.imageEl.current.files.length; refs.imageCountEl.current.style.fontSize = '0.8rem'; + + props.setFieldValue('project_images', refs.imageEl.current).then(errors => { + if (!errors['project_images']) { + removeMetaData(refs.imageEl.current.files, state, handleSetState); + } + }); +}; + +const removeMetaData = (images, state, handleSetState) => { + const newWorker = worker(); + newWorker.removeMetaData(images); + newWorker.addEventListener('message', e => { + Compress(e.data, state, handleSetState); + }); }; const handleImageButtonClick = (e, props, refs) => { @@ -117,7 +132,7 @@ function CreateProject(props) { useStateUpdateCallback(() => { if ( - state.image_upload.images_to_upload === + state.image_upload.images_to_upload.length === state.image_upload.successful_uploads ) { handleSetState(upload_project()); @@ -252,35 +267,30 @@ function CreateProject(props) { props.setFieldTouched('project_images'); props.setFieldTouched('video'); props.setFieldTouched('materials_used'); - props.validateField('title'); - props.validateField('description'); - props.validateField('project_images'); - props.validateField('video'); - props.validateField('materials_used'); - - if ( - props.errors['title'] || - props.errors['description'] || - props.errors['project_images'] || - props.errors['video'] || - props.errors['materials_used'] - ) { - return; - } else if (refs.imageEl.current.files.length === 0) { - handleSetState(upload_project()); - } else { - const project_images = refs.imageEl.current.files; - const { image_upload } = state; - image_upload.images_to_upload = project_images.length; - image_upload.upload_dialog = true; - image_upload.upload_percent = 0; - handleSetState({ image_upload }); + image_field_touched = true; + video_field_touched = true; - for (let index = 0; index < project_images.length; index++) { - upload(project_images[index]); + props.validateForm().then(errors => { + if (Object.keys(errors).length > 0) { + return; + } else if (refs.imageEl.current.files.length === 0) { + handleSetState(upload_project()); + } else { + const { image_upload } = state; + image_upload.upload_dialog = true; + image_upload.upload_percent = 0; + handleSetState({ image_upload }); + + for ( + let index = 0; + index < image_upload.images_to_upload.length; + index++ + ) { + upload(image_upload.images_to_upload[index]); + } } - } + }); } }; @@ -446,7 +456,14 @@ function CreateProject(props) { id="project_images" name="project_images" multiple - onChange={e => handleImageFieldChange(e, refs, props)} + onChange={e => + handleImageFieldChange( + refs, + props, + state, + handleSetState, + ) + } onBlur={props.handleBlur} /> @@ -689,6 +706,19 @@ export default connect( : true; }, ) + .test('not_an_image', 'only images are allowed', value => { + if (value) { + let not_an_image = false; + for (let index = 0; index < value.files.length; index++) { + if (value.files[index].type.split('/')[0] !== 'image') { + not_an_image = true; + } + } + return not_an_image ? false : true; + } else { + return true; + } + }) .test('too_many_images', 'too many images uploaded', value => { if (value) { return value.files.length > 10 ? false : true; @@ -698,12 +728,12 @@ export default connect( }) .test( 'image_size_too_large', - 'one or more of your image is greater than 3mb', + 'one or more of your image is greater than 10mb', value => { if (value) { let image_size_too_large = false; for (let index = 0; index < value.files.length; index++) { - if (value.files[index].size / 1000 > 3072) { + if (value.files[index].size / 1000 > 10240) { image_size_too_large = true; } }