Last active
September 7, 2024 04:39
-
-
Save SagiMedina/f00a57de4e211456225d3114fd10b0d0 to your computer and use it in GitHub Desktop.
Resize and crop images in the Browser with orientation fix using exif
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 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)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I notice this calls
URL.createObjectURL
but doesn't ever release the url. The documentation suggests usingURL.revokeObjectURL
for every call tocreateObjectUrl
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.