Files
dev/third_party/pi/packages/durable/src/images/exif.ts
alexion 6081e3e325 chore(vendor): import pi 1.3.0
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
2026-10-15 17:58:48 -04:00

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;
}