-
-
Save SagiMedina/f00a57de4e211456225d3114fd10b0d0 to your computer and use it in GitHub Desktop.
import EXIF from 'exif-js'; | |
const hasBlobConstructor = typeof (Blob) !== 'undefined' && (function checkBlobConstructor() { | |
try { | |
return Boolean(new Blob()); | |
} catch (error) { | |
return false; | |
} | |
}()); | |
const hasArrayBufferViewSupport = hasBlobConstructor && typeof (Uint8Array) !== 'undefined' && (function checkArrayBufferView() { | |
try { | |
return new Blob([new Uint8Array(100)]).size === 100; | |
} catch (error) { | |
return false; | |
} | |
}()); | |
const hasToBlobSupport = (typeof HTMLCanvasElement !== 'undefined' ? HTMLCanvasElement.prototype.toBlob : false); | |
const hasBlobSupport = (hasToBlobSupport || (typeof Uint8Array !== 'undefined' && typeof ArrayBuffer !== 'undefined' && typeof atob !== 'undefined')); | |
const hasReaderSupport = (typeof FileReader !== 'undefined' || typeof URL !== 'undefined'); | |
const hasCanvasSupport = (typeof HTMLCanvasElement !== 'undefined'); | |
export default class ImageTools { | |
constructor() { | |
this.browserSupport = this.isSupportedByBrowser(); | |
} | |
isSupportedByBrowser = () => (hasCanvasSupport && hasBlobSupport && hasReaderSupport); | |
resize = (file, maxDimensions) => new Promise((resolve) => { | |
if (!this.browserSupport || !file.type.match(/image.*/)) return resolve(file); // early exit - not supported | |
if (file.type.match(/image\/gif/)) return resolve(file); // early exit - could be an animated gif | |
const image = document.createElement('img'); | |
image.onload = () => { | |
let width = image.width; | |
let height = image.height; | |
if (width >= height && width > maxDimensions.width) { | |
height *= maxDimensions.width / width; | |
width = maxDimensions.width; | |
} else if (height > maxDimensions.height) { | |
width *= maxDimensions.height / height; | |
height = maxDimensions.height; | |
} else return resolve(file); // early exit; no need to resize | |
EXIF.getData(image, () => { | |
const orientation = EXIF.getTag(image, 'Orientation'); | |
const imageCanvas = this.drawImageToCanvas(image, orientation, 0, 0, width, height, 'contain'); | |
if (hasToBlobSupport) imageCanvas.toBlob(blob => resolve(blob), file.type); | |
else resolve(this.toBlob(imageCanvas, file.type)); | |
}); | |
}; | |
this.loadImage(image, file); | |
return true; | |
}); | |
crop = (file, dimensions) => new Promise((resolve) => { | |
if (!this.browserSupport || !file.type.match(/image.*/)) return resolve(file); // early exit - not supported | |
if (file.type.match(/image\/gif/)) return resolve(file); // early exit - could be an animated gif | |
const image = document.createElement('img'); | |
image.onload = () => { | |
if (dimensions.width > image.width && dimensions.height > image.height) return resolve(file); // early exit - no need to resize | |
const width = Math.min(dimensions.width, image.width); | |
const height = Math.min(dimensions.height, image.height); | |
if (image.width > dimensions.width * 2 || image.height > dimensions.height * 2) { | |
return this.resize(file, { width: dimensions.width * 2, height: dimensions.height * 2 }).then((zoomedOutImage) => { | |
this.crop(zoomedOutImage, { width, height }).then(resolve); | |
}); | |
} | |
EXIF.getData(image, () => { | |
const orientation = EXIF.getTag(image, 'Orientation'); | |
const imageCanvas = this.drawImageToCanvas(image, orientation, 0, 0, width, height, 'crop'); | |
if (hasToBlobSupport) imageCanvas.toBlob(blob => resolve(blob), file.type); | |
else resolve(this.toBlob(imageCanvas, file.type)); | |
}); | |
}; | |
this.loadImage(image, file); | |
return true; | |
}); | |
drawImageToCanvas = (img, orientation = 1, x = 0, y = 0, width = img.width, height = img.height, method = 'contain') => { | |
const canvas = document.createElement('canvas'); | |
const ctx = canvas.getContext('2d'); | |
canvas.width = width; | |
canvas.height = height; | |
ctx.save(); | |
switch (Number(orientation)) { | |
// explained here: https://i.stack.imgur.com/6cJTP.gif | |
case 1: | |
break; | |
case 2: | |
ctx.translate(width, 0); | |
ctx.scale(-1, 1); | |
break; | |
case 3: | |
ctx.translate(width, height); | |
ctx.rotate((180 / 180) * Math.PI); // 180/180 is 1? No shit, but how else will you know its need 180 rotation? | |
break; | |
case 4: | |
ctx.translate(0, height); | |
ctx.scale(1, -1); | |
break; | |
case 5: | |
canvas.width = height; | |
canvas.height = width; | |
ctx.rotate((90 / 180) * Math.PI); | |
ctx.scale(1, -1); | |
break; | |
case 6: | |
canvas.width = height; | |
canvas.height = width; | |
ctx.rotate((90 / 180) * Math.PI); | |
ctx.translate(0, -height); | |
break; | |
case 7: | |
canvas.width = height; | |
canvas.height = width; | |
ctx.rotate((270 / 180) * Math.PI); | |
ctx.translate(-width, height); | |
ctx.scale(1, -1); | |
break; | |
case 8: | |
canvas.width = height; | |
canvas.height = width; | |
ctx.translate(0, width); | |
ctx.rotate((270 / 180) * Math.PI); | |
break; | |
default: | |
break; | |
} | |
if (method === 'crop') ctx.drawImage(img, (img.width / 2) - (width / 2), (img.height / 2) - (height / 2), width, height, 0, 0, width, height); | |
else ctx.drawImage(img, x, y, width, height); | |
ctx.restore(); | |
return canvas; | |
}; | |
toBlob = (canvas, type) => { | |
const dataURI = canvas.toDataURL(type); | |
const dataURIParts = dataURI.split(','); | |
let byteString; | |
if (dataURIParts[0].indexOf('base64') >= 0) { | |
byteString = atob(dataURIParts[1]); | |
} else { | |
byteString = decodeURIComponent(dataURIParts[1]); | |
} | |
const arrayBuffer = new ArrayBuffer(byteString.length); | |
const intArray = new Uint8Array(arrayBuffer); | |
for (let i = 0; i < byteString.length; i += 1) { | |
intArray[i] = byteString.charCodeAt(i); | |
} | |
const mimeString = dataURIParts[0].split(':')[1].split(';')[0]; | |
let blob = null; | |
if (hasBlobConstructor) { | |
blob = new Blob([hasArrayBufferViewSupport ? intArray : arrayBuffer], { type: mimeString }); | |
} else { | |
const bb = new BlobBuilder(); | |
bb.append(arrayBuffer); | |
blob = bb.getBlob(mimeString); | |
} | |
return blob; | |
}; | |
loadImage = (image, file) => { | |
if (typeof (URL) === 'undefined') { | |
const reader = new FileReader(); | |
reader.onload = (event) => { | |
image.src = event.target.result; | |
}; | |
reader.readAsDataURL(file); | |
} else { | |
image.src = URL.createObjectURL(file); | |
} | |
}; | |
} |
Hey @SagiMedina - Thanks for the rapid response. You'll have to excuse the questions, fairly new to the whole web pack thing...
- In replace of "import EXIF from 'exif-js', what do I need to put? I've installed exif-js via npm. I've just added var EXIF = require('exif-js'); so far
- I've imported the imageTools class into my angular component ts file: import ImageTools from '../../../../assets/scripts/ImageTools';
- If I just run the app without the import (just to let the file load on its own) I get: Uncaught SyntaxError: Unexpected token export on the following line from ImageTools "export default class ImageTools {"
- I currently just store the 'imageTools.js' file in an assets folder and link to it via my index.html file. Does that mean it's not going through the web pack compiler. If so, what do I need to do to use this file?
Thanks again. I appreciate the time.
@MatthewLHolden
Hmm ok, so if you are using webpack with ES6 compiler you don't need to change anything in this file.
Just import it to your code like this
import ImageTools from 'PATH/TO/FILE';
And in your code initiate it like this
const imageTools = new ImageTools();
Then you can do
imageTools.resize(file, { width: 1080, height: 1920 });
imageTools.crop(file, { width: 400, height: 400 });
@SagiMedina - Still not working I'm afraid. Here's what I've done so far:
- Save your JS file
- In my component add:
import { ImageTools } from '../../../../assets/scripts/ImageTools.js`
- In my component function add:
const imageTools = new ImageTools(); imageTools.resize(file, { width: 480, height: 320 }).then((blob) => {})
When compiling my app, the terminal throws the following error:
ERROR in ./src/assets/scripts/ImageTools.js
Module parse failed: Unexpected token (33:25)
You may need an appropriate loader to handle this file type.
| }
|
| isSupportedByBrowser = () => (hasCanvasSupport && hasBlobSupport && hasReaderSupport);
|
| resize = (file, maxDimensions) => new Promise((resolve) => {
@MatthewLHolden Try
import ImageTools from '../../../../assets/scripts/ImageTools.js'
@SagiMedina - Same error :( - It seems to be this line: isSupportedByBrowser = () => (hasCanvasSupport && hasBlobSupport && hasReaderSupport);
that it has issues with. Any idea what this could be? This is the final piece of a puzzle I've been working on, so I'm excited to get it implemented :)
@MatthewLHolden Can you share your webpack and bable configuration?
Hey @SagiMedina - I think the only way to get a webpack.config file is to eject the angular application. Which I don't think is too wise as I think adding npm packages after that is a little more tricky. I'm running Angular 6 if that helps at all.
Hey @SagiMedina - this is exactly what i need to fix the orientation issue but i cant figure out how to use this. i was previously using ImageTools.js posted by @dcollien by calling it like <script src ="ImageTools.js" type="text/javascript"></script> and then implemented it as
'document.getElementById('inputfile').onchange = function(evt) {
ImageTools.resize(this.files[0], {
width: 320, // maximum width
height: 240 // maximum height
}, function(blob, didItResize) {
document.getElementById('preview').src = window.URL.createObjectURL(blob);
// upload to server.
});
};`
but to use your code what should i do different .. i am new to web development and i really need it bcz images are getting rotated.. please help me..
Thanks in Advance.
@SagiMedina Thank you for not replying, found a way better alternative(bcz when processing image through yours code using some android phone, image turns black.) than yours. https://github.com/davejm/client-compress
@pranav65 this gist is written in ES2015, but I'm glad you found something that works for you.
@SagiMedina - Any idea on my error at all? I still can't get past this :(
It's angular application. Component below.
Your script is just a JS file in my assets folder.
I have npm'd exif-js.
component.ts
import { ImageTools } from '../../../assets/scripts/imageTools.js';
...
resizeImage(file){
const imageTools = new ImageTools();
imageTools.resize(file, { width: 480, height: 400 }).then((blob) => {
// I upload data here
});
}
Error in the console on load (the function hasn't even been called yet)
./src/assets/scripts/imageTools.js
Module parse failed: Unexpected token (33:25)
You may need an appropriate loader to handle this file type.
| }
|
| isSupportedByBrowser = () => (hasCanvasSupport && hasBlobSupport && hasReaderSupport);
|
| resize = (file, maxDimensions) => new Promise((resolve) => {
The only way I've managed to get it to work is by using Babel's online compiler. However, I do end up with console errors that read:
imageTools.js:3 Uncaught ReferenceError: exports is not defined
on the following code:
Object.defineProperty(exports, "__esModule", {
value: true
})
@MatthewLHolden Are you using any es compiler? And in which stage?
sorry for the delay, for some reason I'm not getting any notification about comments here
AOT I guess? My project was built on Angular Cli (Angular 6) - No worries about the notifications, I'm not getting them either! :)
I expected the resulting image to be no larger than specified size, but this can happen with the current code. Here is my fixed version:
if (width >= height && width > maxDimensions.width) {
height *= maxDimensions.width / width;
width = maxDimensions.width;
if (height > maxDimensions.height) {
width *= maxDimensions.height / height;
height = maxDimensions.height;
}
} else if (height > maxDimensions.height) {
width *= maxDimensions.height / height;
height = maxDimensions.height;
if(width > maxDimensions.width) {
height *= maxDimensions.width / width;
width = maxDimensions.width;
}
} else return resolve(file); // early exit; no need to resize
Basicly, after resizing i.e. by width, check again that the height is within the boundary and resize if not.
i.e. i had an input image with 2048x1536 that should be fit within 240x135. Previous code resulted in an 240x180 pixel image.
@cnyyk that is the point. I think if i rescale 2048x1536 to 240x135, I expect an 180x135 image as result instead of 240x180. That is because 180x135 fits within the requested 240x135 boundaries while keeping the shape.
My usecase was that I wanted to display multiple thumbnails next to each other. Without the fix, the portrait pictures were larger than the horizontal ones, which looked bad.
Well done man. Thanks! 👍
I notice this calls URL.createObjectURL
but doesn't ever release the url. The documentation suggests using URL.revokeObjectURL
for every call to createObjectUrl
to avoid memory leaks. This can be seen in the "Memory Management" section of this document: https://devdocs.io/dom/url/createobjecturl.
I'm not sure where a good place to call that would be here, but I wonder if calling it after the resolve
call would be best.
@SagiMedina Thank you! Works perfectly with Axios and it's fast!
Hello
Reactjs error
./src/Components/ImageTools.js
Line 192: 'BlobBuilder' is not defined no-undef
Search for the keywords to learn more about each error.
@enkhee
I changed:
const bb = new BlobBuilder();
bb.append(arrayBuffer);
blob = bb.getBlob(mimeString);
to:
blob = new Blob([arrayBuffer]);
That's what was recommended here and it seems to be working.
thanks for the helpful code, @SagiMedina !
Hey there, I was using @dcollien solution as well and were facing the same rotation issues. But with your code, my Svelte + Rollup setup fails showing the following issue:
[!] (plugin commonjs) SyntaxError: Unexpected token (33:25) in /Users/username/Git/projectname/src/ImageTools.js
src/ImageTools.js (33:25)
33: isSupportedByBrowser = () => (hasCanvasSupport && hasBlobSupport && hasReaderSupport);
^
Any idea?
Hello I'm trying to implement in svelte project, i've imported exif-js
and im getting:
exif.js:741 Uncaught ReferenceError: n is not defined
at getStringFromDB (exif.js:741:14)
at readEXIFData (exif.js:748:13)
at findEXIFinJPEG (exif.js:449:24)
at handleBinaryFile (exif.js:370:24)
at fileReader.onload (exif.js:391:21)
Hey @MatthewLHolden, are you working with webpack or something similar?
Basically, it should work with Angular React Vue or anything else you just need a JS 'compiler' Like bable to transform ES-2015 to plain JS.
And yes it should work the same plus the orientation fix, if I remember correctly how the original one works...
If it's still unclear LMK and I'll try to elaborate.