Skip to content

Instantly share code, notes, and snippets.

@Hongyu-Zhuo
Last active February 18, 2021 06:34
Show Gist options
  • Save Hongyu-Zhuo/d9f2aa32f8082e2052fae115127b9121 to your computer and use it in GitHub Desktop.
Save Hongyu-Zhuo/d9f2aa32f8082e2052fae115127b9121 to your computer and use it in GitHub Desktop.
Image orientation
export class ImageUtil {
constructor() {
}
init() {
window.URL = window.URL || webkitURL;
}
// https://www.media.mit.edu/pia/Research/deepview/exif.html
// https://exiftool.org/TagNames/EXIF.html
static async getExifOrientation(file: Blob): Promise<number> {
return new Promise((resolve, reject) => {
file = file.slice(0, 131072);
const reader = new FileReader();
reader.onload = function(e) {
const dataView = new DataView(e.target?.result as ArrayBuffer);
// 是否是 jpeg
if (dataView.getUint16(0, false) !== 0xffd8) {
resolve(-2);
return;
}
const length = dataView.byteLength;
let offset = 2;
while (offset < length) {
const marker = dataView.getUint16(offset, false);
offset += 2;
// 是否是 EXIF(jpeg)
if (marker === 0xffe1) {
// 是否存在 EXIF metadata,`0x45786966` 是 EXIF 头的起始 ASCII 码;
if (dataView.getUint16((offset += 2), false) !== 0x45786966) {
resolve(-1);
return;
}
// 是否是小字节序(little-endian);
const little = dataView.getUint16((offset += 6), false) === 0x4949;
offset += dataView.getUint32(offset + 4, little);
const tags = dataView.getUint16(offset, little);
offset += 2;
for (let i = 0; i < tags; i++) {
if (dataView.getUint16(offset + i * 12, little) == 0x0112) {
resolve(dataView.getUint16(offset + i * 12 + 8, little));
return;
}
}
} else if ((marker & 0xff00) !== 0xff00) {
break;
} else {
offset += dataView.getUint16(offset, false);
}
resolve(-1);
}
reader.readAsArrayBuffer(file);
}
})
}
// Derived from https://stackoverflow.com/a/40867559, cc by-sa
private static imgToCanvasWithOrientation(img: HTMLImageElement, rawWidth: number, rawHeight: number, orientation: number) {
const canvas = document.createElement("canvas");
// https://www.impulseadventure.com/photo/exif-orientation.html
if (orientation > 4) {
canvas.width = rawHeight;
canvas.height = rawWidth;
} else {
canvas.width = rawWidth;
canvas.height = rawHeight;
}
if (orientation > 1) {
console.log("EXIF orientation = " + orientation + ", rotating picture");
}
const context = canvas.getContext('2d');
switch (orientation) {
case 2:
context?.transform(-1, 0, 0, 1, rawWidth, 0);
break;
case 3:
context?.transform(-1, 0, 0, -1, rawWidth, rawHeight);
break;
case 4:
context?.transform(1, 0, 0, -1, 0, rawHeight);
break;
case 5:
context?.transform(0, 1, 1, 0, 0, 0);
break;
case 6:
context?.transform(0, 1, -1, 0, rawHeight, 0);
break;
case 7:
context?.transform(0, -1, -1, 0, rawHeight, rawWidth);
break;
case 8:
context?.transform(0, -1, 1, 0, 0, rawWidth);
break;
}
context?.drawImage(img, 0, 0, rawWidth, rawHeight);
return canvas;
}
static async reduceFileSize(file: File, acceptFileSize: number, maxWidth: 'auto', maxHeight: 'auto', quality: number): Promise<Blob>;
static async reduceFileSize(file: File, acceptFileSize: number, maxWidth: number, maxHeight: number, quality: number): Promise<Blob>;
/**
* @function reduceFileSize
* @param file 原始文件
* @param acceptFileSize 最大文件大小,传入的文件尺寸如果大于这个值,就会执行压缩
* @param maxWidth 压缩后的图片最大宽度
* @param maxHeight 最大高度
* @param quality 压缩质量,(0, 1]
* @description
*
*/
static async reduceFileSize(file: File, acceptFileSize: number, maxWidth: number | 'auto', maxHeight: number | 'auto', quality: number): Promise<Blob> {
return new Promise((resolve, reject) =>{
if (file.size < acceptFileSize) {
resolve(file);
return;
}
const image = new Image();
image.onerror = function() {
URL.revokeObjectURL(this.src);
resolve(file);
return;
}
image.onload = async function() {
URL.revokeObjectURL((this as HTMLImageElement).src);
const orientation = await ImageUtil.getExifOrientation(file);
let w = image.width;
let h = image.height;
let scale = 1;
if ( maxWidth !== 'auto' && maxHeight !== 'auto') {
scale = orientation > 4
? Math.min(maxHeight / w, maxWidth / h, 1)
: Math.min(maxWidth / w, maxHeight / h, 1);
}
h = Math.round(h * scale);
w = Math.round(w * scale);
const canvas = ImageUtil.imgToCanvasWithOrientation(image, w, h, orientation);
canvas.toBlob((blob: Blob | null) => {
console.log(
"Resized image to " + w + "x" + h + ", " + ((blob as Blob).size >> 10) + "kB"
);
resolve(blob as Blob);
return;
}, "image/jpeg", quality);
}
image.src = URL.createObjectURL(file);
});
}
}
//from: https://stackoverflow.com/questions/7584794/accessing-jpeg-exif-rotation-data-in-javascript-on-the-client-side/32490603#32490603
/**
* Based on StackOverflow answer: https://stackoverflow.com/a/32490603
*
* @param imageFile The image file to inspect
* @param onRotationFound callback when the rotation is discovered. Will return 0 if if it fails, otherwise 0, 90, 180, or 270
* @example
* ```ts
* import { getOrientation } from './ImageUtils';
* onDrop = (pics: any) => {
* getOrientation(pics[0], rotationInDegrees => {
* this.setState({ image: pics[0], rotate: rotationInDegrees });
* });
* };
* ```
*/
export function getOrientation(imageFile: File, onRotationFound: (rotationInDegrees: number) => void) {
const reader = new FileReader();
reader.onload = (event: ProgressEvent) => {
if (!event.target) {
return;
}
const innerFile = event.target as FileReader;
const view = new DataView(innerFile.result as ArrayBuffer);
if (view.getUint16(0, false) !== 0xffd8) {
return onRotationFound(convertRotationToDegrees(-2));
}
const length = view.byteLength;
let offset = 2;
while (offset < length) {
if (view.getUint16(offset + 2, false) <= 8) {
return onRotationFound(convertRotationToDegrees(-1));
}
const marker = view.getUint16(offset, false);
offset += 2;
if (marker === 0xffe1) {
if (view.getUint32((offset += 2), false) !== 0x45786966) {
return onRotationFound(convertRotationToDegrees(-1));
}
const little = view.getUint16((offset += 6), 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 onRotationFound(convertRotationToDegrees(view.getUint16(offset + i * 12 + 8, little)));
}
}
// tslint:disable-next-line:no-bitwise
} else if ((marker & 0xff00) !== 0xff00) {
break;
} else {
offset += view.getUint16(offset, false);
}
}
return onRotationFound(convertRotationToDegrees(-1));
};
reader.readAsArrayBuffer(imageFile);
}
/**
* Based off snippet here: https://github.com/mosch/react-avatar-editor/issues/123#issuecomment-354896008
* @param rotation converts the int into a degrees rotation.
*/
function convertRotationToDegrees(rotation: number): number {
let rotationInDegrees = 0;
switch (rotation) {
case 8:
rotationInDegrees = 270;
break;
case 6:
rotationInDegrees = 90;
break;
case 3:
rotationInDegrees = 180;
break;
default:
rotationInDegrees = 0;
}
return rotationInDegrees;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment