Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
// EXIF orientation of JPEG and WebP files, read from the bytes without decoding the image.
|
|
|
|
/** The EXIF orientation of a JPEG or WebP, 1 to 8; 1 when absent or unreadable. */
|
|
export function exifOrientation(bytes: Uint8Array): number {
|
|
const tiff = tiffStart(bytes);
|
|
if (tiff < 0 || tiff + 8 > bytes.length) return 1;
|
|
const little = bytes[tiff] === 0x49 && bytes[tiff + 1] === 0x49;
|
|
const u16 = (at: number): number =>
|
|
little ? bytes[at]! | (bytes[at + 1]! << 8) : (bytes[at]! << 8) | bytes[at + 1]!;
|
|
const u32 = (at: number): number =>
|
|
little
|
|
? (bytes[at]! | (bytes[at + 1]! << 8) | (bytes[at + 2]! << 16) | (bytes[at + 3]! << 24)) >>> 0
|
|
: ((bytes[at]! << 24) | (bytes[at + 1]! << 16) | (bytes[at + 2]! << 8) | bytes[at + 3]!) >>> 0;
|
|
const directory = tiff + u32(tiff + 4);
|
|
if (directory + 2 > bytes.length) return 1;
|
|
const entries = u16(directory);
|
|
for (let index = 0; index < entries; index++) {
|
|
const entry = directory + 2 + index * 12;
|
|
if (entry + 12 > bytes.length) return 1;
|
|
// Tag 0x0112 is Orientation, a SHORT whose value sits in the entry.
|
|
if (u16(entry) === 0x0112) {
|
|
const value = u16(entry + 8);
|
|
return value >= 1 && value <= 8 ? value : 1;
|
|
}
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/** Where the TIFF header of a JPEG's APP1 or a WebP's EXIF chunk starts; -1 when there is none. */
|
|
function tiffStart(bytes: Uint8Array): number {
|
|
if (bytes[0] === 0xff && bytes[1] === 0xd8) {
|
|
for (let offset = 2; offset + 4 <= bytes.length; ) {
|
|
if (bytes[offset] !== 0xff) return -1;
|
|
const marker = bytes[offset + 1]!;
|
|
if (marker === 0xff) {
|
|
offset++;
|
|
continue;
|
|
}
|
|
if (marker === 0xe1 && isExifHeader(bytes, offset + 4)) return offset + 10;
|
|
offset += 2 + ((bytes[offset + 2]! << 8) | bytes[offset + 3]!);
|
|
}
|
|
return -1;
|
|
}
|
|
if (ascii(bytes, 0, "RIFF") && ascii(bytes, 8, "WEBP")) {
|
|
for (let offset = 12; offset + 8 <= bytes.length; ) {
|
|
const size =
|
|
(bytes[offset + 4]! |
|
|
(bytes[offset + 5]! << 8) |
|
|
(bytes[offset + 6]! << 16) |
|
|
(bytes[offset + 7]! << 24)) >>>
|
|
0;
|
|
// Some WebP files prefix the TIFF header with "Exif\0\0".
|
|
if (ascii(bytes, offset, "EXIF")) return isExifHeader(bytes, offset + 8) ? offset + 14 : offset + 8;
|
|
// RIFF chunks are padded to an even size.
|
|
offset += 8 + size + (size % 2);
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
function isExifHeader(bytes: Uint8Array, offset: number): boolean {
|
|
return ascii(bytes, offset, "Exif") && bytes[offset + 4] === 0 && bytes[offset + 5] === 0;
|
|
}
|
|
|
|
function ascii(bytes: Uint8Array, offset: number, text: string): boolean {
|
|
for (let index = 0; index < text.length; index++) {
|
|
if (bytes[offset + index] !== text.charCodeAt(index)) return false;
|
|
}
|
|
return true;
|
|
}
|