Skip to content

Instantly share code, notes, and snippets.

@scf37
Last active September 5, 2023 16:43
Show Gist options
  • Save scf37/6b4bf47dce4d78be92216323b12f2d21 to your computer and use it in GitHub Desktop.
Save scf37/6b4bf47dce4d78be92216323b12f2d21 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
// Based on: https://gist.github.com/mindplay-dk/72f47c1a570e870a375bd3dbcb9328fb
/**
* 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 function getImageUrl(file: File, maxWidth: number|undefined): Promise<string> {
return readOrientation(file).then(orientation => {
if (browserSupportsAutoRotation) orientation = undefined;
return applyRotation(file, orientation || 1, maxWidth || 999999)
});
}
let browserSupportsAutoRotation = null;
(function () {
// black 2x1 JPEG, with the following meta information set:
// - EXIF Orientation: 6 (Rotated 90° CCW)
var testImageURL =
'data:image/jpeg;base64,/9j/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAA' +
'AAAD/2wCEAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBA' +
'QEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE' +
'BAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAf/AABEIAAEAAgMBEQACEQEDEQH/x' +
'ABKAAEAAAAAAAAAAAAAAAAAAAALEAEAAAAAAAAAAAAAAAAAAAAAAQEAAAAAAAAAAAAAAAA' +
'AAAAAEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwA/8H//2Q=='
var img = document.createElement('img')
img.onload = function () {
// Check if browser supports automatic image orientation:
browserSupportsAutoRotation = img.width === 1 && img.height === 2
}
img.src = testImageURL
})()
/**
* @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(/** @type {ArrayBuffer} */ (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 (let 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 = Math.floor(width * scale);
height = Math.floor(height * scale);
// to rotate rectangular image, we need enough space so square canvas is used
const wh = Math.max(width, height);
// set proper canvas dimensions before transform & export
canvas.width = wh;
canvas.height = wh;
// for some transformations output image will be aligned to the right of square canvas
let rightAligned = false;
// transform context before drawing image
switch (orientation) {
case 2: context.transform(-1, 0, 0, 1, wh, 0); rightAligned = true; break;
case 3: context.transform(-1, 0, 0, -1, wh, wh); rightAligned = true; break;
case 4: context.transform(1, 0, 0, -1, 0, wh); break;
case 5: context.transform(0, 1, 1, 0, 0, 0); break;
case 6: context.transform(0, 1, -1, 0, wh, 0); rightAligned = true; break;
case 7: context.transform(0, -1, -1, 0, wh, wh); rightAligned = true; break;
case 8: context.transform(0, -1, 1, 0, 0, wh); break;
default: break;
}
// draw image
context.drawImage(image, 0, 0, width, height);
// copy rotated image to output dimensions and export it
const canvas2 = document.createElement("canvas");
canvas2.width = Math.floor(outputWidth * scale);
canvas2.height = Math.floor(outputHeight * scale);
const ctx2 = canvas2.getContext("2d");
const sx = rightAligned ? canvas.width - canvas2.width : 0;
ctx2.drawImage(canvas, sx, 0, canvas2.width, canvas2.height, 0, 0, canvas2.width, canvas2.height);
// export base64
resolve(canvas2.toDataURL("image/jpeg"));
};
image.src = url;
};
reader.readAsDataURL(file);
});
@sandstrom
Copy link

@scf37 Sorry for the constant pings on this! 🙃

But if you find some space, I'm eager to hear your thinking on this.

@SaliZumberi
Copy link

Very clean code! I like it.
Unfortunately it doesnot work if I upload an image from my Iphone :S Did I miss something?

@scf37
Copy link
Author

scf37 commented Mar 3, 2020

@sandstrom fixed!

Thank you for pointing out that bug :-)

@scf37
Copy link
Author

scf37 commented Mar 3, 2020

@SaliZumberi can't really help you without details - javascript error (if any) and screenshot would be nice.

@sandstrom
Copy link

@scf37 Awesome! 🥇

Glad I could help 😄

@sandstrom
Copy link

@SaliZumberi I've used the code for iOS uploads and it works really well. Perhaps there is something wrong with your implementation of the code?

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