Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk mindplay-dk/image-url.ts
Last active Jan 9, 2020

Embed
What would you like to do?
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);
});
@aquaductape

This comment has been minimized.

Copy link

aquaductape commented Mar 17, 2019

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

@scf37

This comment has been minimized.

Copy link

scf37 commented Apr 15, 2019

Just tested for my SM-G900H - oriented images draw correctly.

@scf37

This comment has been minimized.

Copy link

scf37 commented Apr 15, 2019

Still, I wonder is it possible to use css-transform instead of canvas to fix orientation...

@scf37

This comment has been minimized.

Copy link

scf37 commented Apr 15, 2019

Apparently I was wrong, this one is buggy.
Here is fixed version: https://gist.github.com/scf37/6b4bf47dce4d78be92216323b12f2d21
Now it does not crop images when rotating.
Tested on this set of all 8 orientations: http://www.galloway.me.uk/2012/01/uiimageorientation-exif-orientation-sample-images/

@patotoma

This comment has been minimized.

Copy link

patotoma commented May 22, 2019

thank you @mindplay-dk, just a little mistake in case 8
it should be: case 8: context.transform(0, -1, 1, 0, 0, width); break;
fixed indentation and updated case 8 here: https://gist.github.com/patotoma/59dc40554a935ab98764c1dcdce5856a

@mindplay-dk

This comment has been minimized.

Copy link
Owner Author

mindplay-dk commented May 22, 2019

Here is fixed version

@scf37 what did you fix? a diff would be handy.

thank you @mindplay-dk, just a little mistake in case 8

@patotoma thanks for catching this :-)

@scf37

This comment has been minimized.

Copy link

scf37 commented May 22, 2019

@mindplay-dk Root problem is it is not possible to rotate image inside rectangular canvas "in place" - long side of the image will be cut.
My solution is to copy source image to large enough square canvas, rotate square and extract rotated image.

@severo

This comment has been minimized.

Copy link

severo commented Dec 18, 2019

The line

https://gist.github.com/mindplay-dk/72f47c1a570e870a375bd3dbcb9328fb#file-image-url-ts-L80

generates a SyntaxError: unexpected token: '!'. I imagine the ! character should be deleted.

@mindplay-dk

This comment has been minimized.

Copy link
Owner Author

mindplay-dk commented Dec 22, 2019

The line [... ]generates a SyntaxError: unexpected token: '!'. I imagine the ! character should be deleted.

That's the non-null assertion operator, which was added in TypeScript 2.0 - you're probably using a very old version of TypeScript?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.