Skip to content

Instantly share code, notes, and snippets.

@SagiMedina
Last active March 25, 2024 06:13
Show Gist options
  • Star 36 You must be signed in to star a gist
  • Fork 11 You must be signed in to fork a gist
  • Save SagiMedina/f00a57de4e211456225d3114fd10b0d0 to your computer and use it in GitHub Desktop.
Save SagiMedina/f00a57de4e211456225d3114fd10b0d0 to your computer and use it in GitHub Desktop.
Resize and crop images in the Browser with orientation fix using exif
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);
}
};
}
@SagiMedina
Copy link
Author

@pranav65 this gist is written in ES2015, but I'm glad you found something that works for you.

@MatthewLHolden
Copy link

MatthewLHolden commented Dec 3, 2018

@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
})

@SagiMedina
Copy link
Author

SagiMedina commented Dec 5, 2018

@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

@MatthewLHolden
Copy link

AOT I guess? My project was built on Angular Cli (Angular 6) - No worries about the notifications, I'm not getting them either! :)

@bhelm
Copy link

bhelm commented Jan 18, 2019

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.

@yueim
Copy link

yueim commented Apr 27, 2019

@bhelm

2048x1536 should be 240x180.

if 240x135, image will be out of shape

@bhelm
Copy link

bhelm commented Apr 27, 2019

@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.

@allanesquina
Copy link

Well done man. Thanks! 👍

@marr
Copy link

marr commented Aug 14, 2019

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.

@briangonzalezmia
Copy link

@SagiMedina Thank you! Works perfectly with Axios and it's fast!

@enkhee
Copy link

enkhee commented Nov 29, 2019

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.

@frankalbenesius
Copy link

frankalbenesius commented Nov 30, 2019

@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 !

@kilianso
Copy link

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?

@gzimbron
Copy link

gzimbron commented Aug 4, 2022

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment