src/index.ts
export enum OrientationCode {
original = 1,
deg90 = 6,
deg180 = 3,
deg270 = 8,
flipped = 2,
deg90Flipped = 5,
deg180Flipped = 4,
deg270Flipped = 7,
unknown = -1,
}
export interface IOrientationInfo {
rotation: number;
flipped: boolean;
}
const orientationInfoMap: { [orientation: number]: IOrientationInfo } = {
[OrientationCode.original]: { rotation: 0, flipped: false },
[OrientationCode.deg90]: { rotation: 90, flipped: false },
[OrientationCode.deg180]: { rotation: 180, flipped: false },
[OrientationCode.deg270]: { rotation: 270, flipped: false },
[OrientationCode.flipped]: { rotation: 0, flipped: true },
[OrientationCode.deg90Flipped]: { rotation: 90, flipped: true },
[OrientationCode.deg180Flipped]: { rotation: 180, flipped: true },
[OrientationCode.deg270Flipped]: { rotation: 270, flipped: true },
};
// tslint:disable:object-literal-sort-keys
const statics = {
jpeg: 0xffd8,
app1Marker: 0xffe1,
exifId: [0x45, 0x78, 0x69, 0x66, 0x00, 0x00], // "Exif\0\0"
orderLittleEndian: 0x4949,
endianAssertion: 0x002a,
ifdFieldCountLength: 2,
orientationTag: 0x0112,
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
function sleep (ms: number) {
return new Promise((done) => setTimeout(done, ms));
}
/**
* If the input is not JPEG file with Exif containing orientation information,
* it returns `undefined`.
* @param input JPEG file data.
*/
export async function getOrientation (
input: File | Buffer | ArrayBuffer,
): Promise<IOrientationInfo | undefined> {
const code = await readOrientationCode(input);
const info = getOrientationInfo(code);
return info;
}
/**
* @see http://www.cipa.jp/std/documents/j/DC-008-2012_J.pdf
*/
export async function readOrientationCode (
input: File | Buffer | ArrayBuffer,
): Promise<OrientationCode> {
const view = await prepareDataView(input);
if (!isValidJpeg(view)) {
return OrientationCode.unknown;
}
const segmentOffset = await findExifSegmentOffset(view);
if (segmentOffset < 0) {
return OrientationCode.unknown;
}
const {littleEndian, orientationOffset} = getOrientationOffsetAndLittleEndian(view, segmentOffset);
if (orientationOffset < 0) {
console.warn('Rotation information was not found');
return OrientationCode.unknown;
}
const orientation = readOrientationValueAt(
view,
orientationOffset,
littleEndian,
);
return orientation;
}
export async function updateOrientationCode (
input: File | Buffer | ArrayBuffer,
orientation: OrientationCode,
): Promise<void> {
const view = await prepareDataView(input);
if (!isValidJpeg(view)) {
throw new Error('The File you are trying to update is not a jpeg');
}
const segmentOffset = await findExifSegmentOffset(view);
if (segmentOffset < 0) {
throw new Error('The File you are trying to update has no exif data');
}
const {littleEndian, orientationOffset} = getOrientationOffsetAndLittleEndian(view, segmentOffset);
setOrientationValueAt(
view,
orientationOffset,
orientation,
littleEndian,
);
}
function getOrientationOffsetAndLittleEndian (view: DataView, segmentOffset: number) {
const tiffHeaderOffset = segmentOffset + statics.offsets.tiffHeader.fromSegment;
const littleEndian = isLittleEndian(view, tiffHeaderOffset);
const ifdPosition = findIfdPosition(view, tiffHeaderOffset, littleEndian);
const ifdFieldOffset = ifdPosition + statics.ifdFieldCountLength;
const orientationOffset = findOrientationOffset(
view,
ifdFieldOffset,
littleEndian,
);
return {littleEndian, orientationOffset};
}
async function prepareDataView (
input: File | Buffer | ArrayBuffer,
): Promise<DataView> {
// To run on both browser and Node.js,
// need to check constructors existences before checking instance
let arrayBuffer;
if (typeof File !== 'undefined' && input instanceof File) {
arrayBuffer = await readFile(input);
} else if (typeof Buffer !== 'undefined' && input instanceof Buffer) {
arrayBuffer = input.buffer;
} else {
arrayBuffer = input as ArrayBuffer;
}
const view = new DataView(arrayBuffer);
return view;
}
async function readFile (file: File) {
const arrayBuffer = await new Promise<ArrayBuffer>((resolve) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as ArrayBuffer);
reader.readAsArrayBuffer(file);
});
return arrayBuffer;
}
function isValidJpeg (view: DataView) {
return view.byteLength >= 2 && view.getUint16(0, false) === statics.jpeg;
}
/**
* Returns `-1` if not found.
*/
async function findExifSegmentOffset (view: DataView) {
for await (const segmentPosition of iterateMarkerSegments(view)) {
if (isExifSegment(view, segmentPosition)) {
return segmentPosition;
}
}
// not found
return -1;
}
async function* iterateMarkerSegments (view: DataView) {
// APPx/Exif p.18, 19, 150
// - 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 = statics.offsets.firstMarker;
while (true) {
// just in case
await sleep(1);
yield segmentPosition;
const offsetLength = statics.offsets.segment.length;
const length =
offsetLength + view.getUint16(segmentPosition + offsetLength, false);
segmentPosition += length;
if (segmentPosition > view.byteLength) {
return -1;
}
}
}
function isExifSegment (view: DataView, segmentPosition: number) {
const marker = view.getUint16(
segmentPosition + statics.offsets.segment.marker,
false,
);
if (marker !== statics.app1Marker) {
return false;
}
for (let i = 0; i < statics.exifId.length; i++) {
const c = view.getUint8(
segmentPosition + statics.offsets.segment.exifId + i,
);
if (c !== statics.exifId[i]) {
return false;
}
}
return true;
}
function isLittleEndian (view: DataView, tiffHeaderOffset: number) {
const endian = view.getUint16(
tiffHeaderOffset + statics.offsets.tiffHeader.byteOrder,
false,
);
const littleEndian = endian === statics.orderLittleEndian;
return littleEndian;
}
function findIfdPosition (
view: DataView,
tiffHeaderOffset: number,
littleEndian: boolean | undefined,
) {
// TIFF Header p.17
// - byte order (short). `0x4949` = little, `0x4d4d` = big
// - 42 (0x002a) (short)
// - offset of IFD (long). Minimum is `0x00000008` (8).
const endianAssertionValue = view.getUint16(
tiffHeaderOffset + statics.offsets.tiffHeader.endianAssertion,
littleEndian,
);
if (endianAssertionValue !== statics.endianAssertion) {
throw new Error(
`Invalid JPEG format: littleEndian ${littleEndian}, assertion: 0x${endianAssertionValue}`,
);
}
const ifdDistance = view.getUint32(
tiffHeaderOffset + statics.offsets.tiffHeader.ifdOffset,
littleEndian,
);
const ifdPosition = tiffHeaderOffset + ifdDistance;
return ifdPosition;
}
function findOrientationOffset (
view: DataView,
ifdFieldOffset: number,
littleEndian: boolean,
) {
const fieldIterator = iterateIfdFields(view, ifdFieldOffset, littleEndian);
for (const offset of fieldIterator) {
const index = ifdFieldOffset + offset;
if (index > view.byteLength) {
return -1;
}
const tag = view.getUint16(index, littleEndian);
if (tag === statics.orientationTag) {
const orientationValueOffset = index + statics.offsets.ifd.value;
return orientationValueOffset;
}
}
return -1;
}
function* iterateIfdFields (
view: DataView,
ifdFieldOffset: number,
littleEndian: boolean,
) {
// IFD p.23
// - num of IFD fields (short)
// - IFD:
// - tag (short)
// - type (short)
// - count (long)
// - value offset (long)
// - IFD...
const numOfIfdFields = view.getUint16(ifdFieldOffset, littleEndian);
const fieldLength = 12;
for (let i = 0; i < numOfIfdFields; i++) {
const currentOffset = i * fieldLength;
yield currentOffset;
}
}
function readOrientationValueAt (
view: DataView,
offset: number,
littleEndian: boolean,
) {
const orientation = view.getUint16(offset, littleEndian);
return orientation;
}
function setOrientationValueAt (
view: DataView,
offset: number,
orientation: OrientationCode,
littleEndian: boolean,
) {
view.setUint16(offset, orientation, littleEndian);
}
/**
* Converts orientation code specified in Exif to readable information.
* @param input JPEG file data.
*/
export function getOrientationInfo (
orientation: OrientationCode,
): IOrientationInfo | undefined {
return orientationInfoMap[orientation];
}