Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk
Last active September 28, 2023 15:25
Show Gist options
  • Save mindplay-dk/72f47c1a570e870a375bd3dbcb9328fb to your computer and use it in GitHub Desktop.
Save mindplay-dk/72f47c1a570e870a375bd3dbcb9328fb to your computer and use it in GitHub Desktop.
Rotate image preview to compensate for EXIF orientation (Javascript / Typescript)
// Based on: https://stackoverflow.com/a/46814952/283851
/**
* Create a Base64 Image URL, with rotation applied to compensate for EXIF orientation, if needed.
*
* Optionally resize to a smaller maximum width - to improve performance for larger image thumbnails.
*/
export async function getImageUrl(file: File, maxWidth: number|undefined) {
return readOrientation(file).then(orientation => applyRotation(file, orientation || 1, maxWidth || 999999));
}
/**
* @returns EXIF orientation value (or undefined)
*/
const readOrientation = (file: File) => new Promise<number|undefined>(resolve => {
const reader = new FileReader();
reader.onload = () => resolve((() => {
const view = new DataView(reader.result as ArrayBuffer);
if (view.getUint16(0, false) != 0xFFD8) {
return;
}
const length = view.byteLength;
let offset = 2;
while (offset < length) {
const marker = view.getUint16(offset, false);
offset += 2;
if (marker == 0xFFE1) {
offset += 2;
if (view.getUint32(offset, false) != 0x45786966) {
return;
}
offset += 6;
const little = view.getUint16(offset, false) == 0x4949;
offset += view.getUint32(offset + 4, little);
const tags = view.getUint16(offset, little);
offset += 2;
for (var i = 0; i < tags; i++) {
if (view.getUint16(offset + (i * 12), little) == 0x0112) {
return view.getUint16(offset + (i * 12) + 8, little);
}
}
} else if ((marker & 0xFF00) != 0xFF00) {
break;
} else {
offset += view.getUint16(offset, false);
}
}
})());
reader.readAsArrayBuffer(file.slice(0, 64 * 1024));
});
/**
* @returns Base64 Image URL (with rotation applied to compensate for orientation, if any)
*/
const applyRotation = (file: File, orientation: number, maxWidth: number) => new Promise<string>(resolve => {
const reader = new FileReader();
reader.onload = () => {
const url = reader.result as string;
const image = new Image();
image.onload = () => {
const canvas = document.createElement("canvas");
const context = canvas.getContext("2d")!;
let { width, height } = image;
const [outputWidth, outputHeight] = orientation >= 5 && orientation <= 8
? [height, width]
: [width, height];
const scale = outputWidth > maxWidth ? maxWidth / outputWidth : 1;
width = width * scale;
height = height * scale;
// set proper canvas dimensions before transform & export
canvas.width = outputWidth * scale;
canvas.height = outputHeight * scale;
// transform context before drawing image
switch (orientation) {
case 2: context.transform(-1, 0, 0, 1, width, 0); break;
case 3: context.transform(-1, 0, 0, -1, width, height); break;
case 4: context.transform(1, 0, 0, -1, 0, height); break;
case 5: context.transform(0, 1, 1, 0, 0, 0); break;
case 6: context.transform(0, 1, -1, 0, height, 0); break;
case 7: context.transform(0, -1, -1, 0, height, width); break;
case 8: context.transform(0, -1, 1, 0, 0, width); break;
default: break;
}
// draw image
context.drawImage(image, 0, 0, width, height);
// export base64
resolve(canvas.toDataURL("image/jpeg"));
};
image.src = url;
}
reader.readAsDataURL(file);
});
@ivanotOrozco
Copy link

ivanotOrozco commented Apr 8, 2020

Thanks for the awesome tool but for some reason when you input a picture taken from your selfie camera, there's a thick black bottom border that's rendered into the img/canvas. Pictures taken on front camera renders correctly. Here's a fiddle and try it on your mobile phone https://jsfiddle.net/Lishipu/3feg7v98/. The original code unoptimized from the the link in your gist, works correctly when rendering from selfie camera https://jsfiddle.net/Lishipu/4kav750y/.

thanks guy @aquaductape

@mindplay-dk
Copy link
Author

I've tried to get the code working on these example images, but cannot get all of them to render correctly. Do these images render correctly on your machine after being parsed by this code?

@sandstrom there's no quick way to answer that question. Someone probably should port this code to a proper package with a test suite for those cases - but I'm not working on that project anymore, sorry. But feel free. 🙂

@sandstrom
Copy link

thanks @mindplay-dk!

In case anyone else find this gist, I ended up using the code from here instead for the rotation part:
https://gist.github.com/scf37/6b4bf47dce4d78be92216323b12f2d21

It worked really well!

@dbrunonascimento
Copy link

Thanks

@aquaductape
Copy link

aquaductape commented Feb 12, 2021

Did recent browsers natively fix this issue? Because I keep getting the correct orientation now when updating img.src from original file input. But using this gist results in a wrong orientation.

Here's a demo link that compares the original file from input and the "fixed orientation". https://codesandbox.io/s/competent-oskar-qs0k3?file=/src/App.tsx

I tested with these two images that have different orientations
https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_5.jpg
https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_6.jpg

I also uploaded images from taking pictures with front and back cameras from Galaxy s10e and iPhone 6s (iOS 14.3).

All of them successfully rendered the correct orientation straight from the file input, but after converting it with this gist, it results in a wrong orientation.

@sandstrom
Copy link

sandstrom commented Feb 12, 2021

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