// Image size from the file header, without decoding: PNG, JPEG (with EXIF orientation) and WebP. // Anything else (GIF, BMP, HEIC, AVIF, …) returns null and is refused before any decoding. // Returns { width, height, format, rotated } with width/height as displayed (after EXIF rotation), or null. const HEAD_BYTES = 512 * 1024; export async function readImageInfo(file) { const buf = new Uint8Array(await file.slice(0, HEAD_BYTES).arrayBuffer()); return png(buf) ?? jpeg(buf) ?? webp(buf); } const u16be = (b, i) => (b[i] << 8) | b[i + 1]; const u16le = (b, i) => b[i] | (b[i + 1] << 8); const u32be = (b, i) => ((b[i] << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]) >>> 0; const u32le = (b, i) => (b[i] | (b[i + 1] << 8) | (b[i + 2] << 16) | (b[i + 3] << 24)) >>> 0; const ascii = (b, i, n) => String.fromCharCode(...b.subarray(i, i + n)); function result(width, height, format, orientation = 1) { if (!width || !height) return null; const rotated = orientation >= 5 && orientation <= 8; return { width: rotated ? height : width, height: rotated ? width : height, format, rotated }; } function png(b) { if (b.length < 24 || u32be(b, 0) !== 0x89504e47 || ascii(b, 12, 4) !== "IHDR") return null; let at = 8; while (at + 8 <= b.length) { const len = u32be(b, at); const type = ascii(b, at + 4, 4); if (type === "acTL") return { ...result(u32be(b, 16), u32be(b, 20), "png"), animated: true }; if (type === "IDAT") return result(u32be(b, 16), u32be(b, 20), "png"); at += 12 + len; } return null; } function exifOrientation(b, start, len) { if (ascii(b, start, 4) !== "Exif") return 1; const tiff = start + 6; if (tiff + 8 > b.length) return 1; const little = ascii(b, tiff, 2) === "II"; const r16 = (i) => (little ? u16le(b, i) : u16be(b, i)); const r32 = (i) => (little ? u32le(b, i) : u32be(b, i)); const ifd = tiff + r32(tiff + 4); if (ifd + 2 > Math.min(b.length, start + len)) return 1; const count = r16(ifd); for (let k = 0; k < count; k++) { const e = ifd + 2 + k * 12; if (e + 12 > b.length) break; if (r16(e) === 0x0112) return r16(e + 8) || 1; } return 1; } function jpeg(b) { if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null; let at = 2; let orientation = 1; while (at + 4 <= b.length) { if (b[at] !== 0xff) return null; const marker = b[at + 1]; if (marker === 0xd8 || (marker >= 0xd0 && marker <= 0xd7) || marker === 0x01) { at += 2; continue; } const len = u16be(b, at + 2); if (marker === 0xe1) orientation = exifOrientation(b, at + 4, len - 2); const isSof = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc; if (isSof && at + 9 <= b.length) return result(u16be(b, at + 7), u16be(b, at + 5), "jpeg", orientation); at += 2 + len; } return null; } function webp(b) { if (b.length < 30 || ascii(b, 0, 4) !== "RIFF" || ascii(b, 8, 4) !== "WEBP") return null; const kind = ascii(b, 12, 4); if (kind === "VP8 ") return result(u16le(b, 26) & 0x3fff, u16le(b, 28) & 0x3fff, "webp"); if (kind === "VP8L") { const bits = u32le(b, 21); return result((bits & 0x3fff) + 1, ((bits >> 14) & 0x3fff) + 1, "webp"); } if (kind === "VP8X") { const animated = (b[20] & 0x02) !== 0; const w = 1 + (b[24] | (b[25] << 8) | (b[26] << 16)); const h = 1 + (b[27] | (b[28] << 8) | (b[29] << 16)); return { ...result(w, h, "webp"), animated }; } return null; }