src/services/imageUtil.ts
export function isImageFile (file: File | null): boolean {
return Boolean(file && file.type.startsWith('image/'));
}
export function isJpegImage (file: File | null): boolean {
return isImageFile(file) && file!.type === 'image/jpeg';
}
export enum ExifOrientation {
original = 1,
deg90 = 6,
deg180 = 3,
deg270 = 8,
flipped = 2,
deg90Flipped = 5,
deg180Flipped = 4,
deg270Flipped = 7,
unknown = -1,
}
function sleep (ms: number) {
return new Promise(done => setTimeout(done, ms));
}
/**
* @see http://www.cipa.jp/std/documents/j/DC-008-2012_J.pdf
*/
export async function getOrientation (file: File): Promise<ExifOrientation> {
// TODO check why this is called twice
const jpeg = 0xffd8;
const exifMarker = 0xffe1;
const exifId = 0x45786966; // "E", "X", "I", "F"
const orderLittleEndian = 0x4949;
const endianAssertion = 0x002a;
const orientationTag = 0x0112;
// tslint:disable:object-literal-sort-keys
const offsets = {
firstMarker: 2,
segment: {
marker: 0,
length: 2,
exifId: 4,
},
tiffHeader: {
fromSegment: 10,
byteOrder: 0,
endianAssertion: 2,
ifdOffset: 4,
},
ifd: {
fromTiffHeader: -1,
tag: 0,
type: 2,
count: 4,
value: 8,
},
};
// tslint:enable:object-literal-sort-keys
const buffer = await readFileAsArrayBuffer(file);
const arr = new Uint8Array(buffer);
const view = new DataView(arr.buffer);
if (view.getUint16(0, false) !== jpeg) {
throw new Error('Invalid JPEG format: first 2 bytes');
}
// APPx/Exif p.18, 19
// - marker (short) `0xffe1` = APP1
// - length (short) of segment
// - padding (short) `0x0000` if exif
// - "EXIF" (char[4]) if exif
// - content
// (The doc describe APP1 have to lay next to the SOI,
// however, Photoshop renders a JPEG file that SOI is followed by APP0.)
let segmentPosition = offsets.firstMarker;
while (true) {
// just in case
await sleep(1);
const marker = view.getUint16(segmentPosition + offsets.segment.marker, false);
if (marker === exifMarker) {
const id = view.getUint32(segmentPosition + offsets.segment.exifId, false);
if (id === exifId) {
// found, yay!
break;
} else {
console.warn('APP1 is not exif format', `0x${marker.toString(16)}, 0x${id.toString(16)}`);
return -1;
}
}
const offsetLength = offsets.segment.length;
const length = offsetLength + view.getUint16(segmentPosition + offsetLength, false);
segmentPosition += length;
if (segmentPosition > view.byteLength) {
console.warn('APP1 not found');
return -1;
}
}
const tiffHeaderOffset = segmentPosition + offsets.tiffHeader.fromSegment;
// TIFF Header p.17
// - byte order (short). `0x4949` = little, `0x4d4d` = big
// - 42 (0x002a) (short)
// - offset of IFD (long). Minimum is `0x00000008` (8).
const littleEndian = view.getUint16(tiffHeaderOffset + offsets.tiffHeader.byteOrder, false) === orderLittleEndian;
const endianAssertionValue = view.getUint16(tiffHeaderOffset + offsets.tiffHeader.endianAssertion, littleEndian);
if (endianAssertionValue !== endianAssertion) {
throw new Error(`Invalid JPEG format: littleEndian ${littleEndian}, assertion: 0x${endianAssertionValue}`);
}
const idfDistance = view.getUint32(tiffHeaderOffset + offsets.tiffHeader.ifdOffset, littleEndian);
const idfPosition = tiffHeaderOffset + idfDistance;
// IFD p.23
// - num of IFD fields (short)
// - IFD:
// - tag (short)
// - type (short)
// - count (long)
// - value offset (long)
// - IFD...
const numOfIdfFields = view.getUint16(idfPosition, littleEndian);
const idfValuesPosition = idfPosition + 2;
const fieldLength = 12;
for (let i = 0; i < numOfIdfFields; i++) {
const currentOffset = i * fieldLength;
const tag = view.getUint16(idfValuesPosition + currentOffset, littleEndian);
if (tag === orientationTag) {
const valueOffset = currentOffset + offsets.ifd.value;
const orientation = view.getUint16(idfValuesPosition + valueOffset, littleEndian);
return orientation;
}
}
// not found
console.warn('Rotation information was not found');
return -1;
}
function readFileAsArrayBuffer (file: File): Promise<ArrayBuffer> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as ArrayBuffer);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
export async function readImage (file: File): Promise<HTMLImageElement | null> {
if (!file || !file.type.startsWith('image/')) {
return null;
}
const image = await readFileAsImage(file);
if (file.type === 'image/jpeg') {
const orientation = await getOrientation(file);
const modifiedImage = await applyImageOrientation(image, orientation);
return modifiedImage;
} else {
return image;
}
}
function readFileAsImage (file: File): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const image = document.createElement('img');
image.src = reader.result as string;
// Firefox sometimes doesn't render immediately
setTimeout(() => resolve(image), 1);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
function applyImageOrientation (image: HTMLImageElement, orientation: ExifOrientation): Promise<HTMLImageElement> {
const width = image.naturalWidth;
const height = image.naturalHeight;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) { throw new Error('Failed to get canvas context'); }
const deg90 = orientation === ExifOrientation.deg90 ||
orientation === ExifOrientation.deg90Flipped;
const deg180 = orientation === ExifOrientation.deg180 ||
orientation === ExifOrientation.deg180Flipped;
const deg270 = orientation === ExifOrientation.deg270 ||
orientation === ExifOrientation.deg270Flipped;
const deg0 = !(deg90 || deg180 || deg270);
const flipped = orientation === ExifOrientation.flipped ||
orientation === ExifOrientation.deg90Flipped ||
orientation === ExifOrientation.deg180Flipped ||
orientation === ExifOrientation.deg270Flipped;
const canvasWidth = deg0 || deg180 ? width : height;
const canvasHeight = deg0 || deg180 ? height : width;
const x0 = deg0 || deg90 ? 0 : -width;
const y0 = deg0 || deg270 ? 0 : -height;
let degree = 0;
if (deg0) {
degree = 0;
} else if (deg90) {
degree = 90;
} else if (deg180) {
degree = 180;
} else if (deg270) {
degree = 270;
}
else {
throw new Error(`Unknown orientation type: ${orientation}`);
}
canvas.width = canvasWidth;
canvas.height = canvasHeight;
if (flipped) {
ctx.translate(canvasWidth, 0);
ctx.scale(-1, 1);
}
ctx.rotate(degree / 360 * 2 * Math.PI);
ctx.translate(x0, y0);
ctx.drawImage(image, 0, 0, width, height);
return new Promise((resolve, reject) => {
const modified = document.createElement('img');
// Firefox sometimes doesn't render immediately so wait a msec
modified.onload = () => setTimeout(() => resolve(modified), 1);
modified.onerror = reject;
modified.src = canvas.toDataURL('image/jpeg');
});
}