-
-
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); | |
}); |
@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 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.