-
-
Save scf37/6b4bf47dce4d78be92216323b12f2d21 to your computer and use it in GitHub Desktop.
// 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); | |
}); |
Well, it works for me, tested in chrome, opera, ff. Probably wrong js/css on your side? try this:
<html>
<head></head>
<body>
<input id=file type=file />
<img id=img />
<script>
f = document.getElementById("file");
f.addEventListener("change", ev => {
ff = ev.target.files[0];
getImageUrl(ff).then(url => {
img.src = url;
})
});
// compiled ts code below
</script>
</body>
</html>
You are right, 5 and 6 works! They don't get squished now.
But another problem 😄 is that 3/4 and 7/8 from the landscape set gets cut off. It doesn't happen on the portrait set.
I guess we need to adjust the rightAligned
logic to work both for x and y displacement. Right now it's used to set the custom sx
, but we need similar logic for sy
.
@scf37 Does that make sense?
@scf37 If you have time I'd love to hear your thoughts on this, and whether you think it would make sense to introduce something similar to 'right-aligned' (probably called 'bottom-aligned'), to rectify this issue.
@sandstrom I'll look into this on weekend.
@scf37 Awesome, looking forward to hear your thoughts! 😄
@scf37 Just curious, did you have time to look at this? Would love to hear your thoughts
@scf37 Sorry for the constant pings on this! 🙃
But if you find some space, I'm eager to hear your thinking on this.
Very clean code! I like it.
Unfortunately it doesnot work if I upload an image from my Iphone :S Did I miss something?
@sandstrom fixed!
Thank you for pointing out that bug :-)
@SaliZumberi can't really help you without details - javascript error (if any) and screenshot would be nice.
@scf37 Awesome! 🥇
Glad I could help 😄
@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?
@scf37 Sure, basically it's orientation 5,6,7,8 that doesn't work — images gets squeezed. The pattern is the same for all four. Here are examples for 5 and 6.
However, I should note that I remember reading somewhere about a canvas implementation issue where images gets squeezed. So that may also be why this doesn't work as expected (in other words, not necessarily the code above that's broken). I've only tested in Chrome. Will try to test in Firefox too.
https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_5.jpg
https://github.com/recurser/exif-orientation-examples/blob/master/Landscape_6.jpg
Landscape 5, before
Landscape 6, before
Landscape 5, after
Landscape 6, after