src / imageSize.ts
/** Native image-dimension reader for common formats (ported from image_utils.py). */
import { readFile } from "fs/promises";
export async function imageDimensions(path: string): Promise<[number, number]> {
const buf = await readFile(path);
if (buf.length >= 24 && buf.slice(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
if (buf.slice(12, 16).toString("latin1") !== "IHDR") throw new Error("Invalid PNG header.");
return [buf.readUInt32BE(16), buf.readUInt32BE(20)];
}
if (buf.length >= 2 && buf[0] === 0xff && buf[1] === 0xd8) {
return jpegDimensions(buf);
}
const head6 = buf.slice(0, 6).toString("latin1");
if (head6 === "GIF87a" || head6 === "GIF89a") {
if (buf.length < 10) throw new Error("Invalid GIF header.");
return [buf.readUInt16LE(6), buf.readUInt16LE(8)];
}
if (buf.length >= 2 && buf[0] === 0x42 && buf[1] === 0x4d) {
if (buf.length < 26) throw new Error("Invalid BMP header.");
return [Math.abs(buf.readInt32LE(18)), Math.abs(buf.readInt32LE(22))];
}
throw new Error(`Unsupported image format for dimension inference: ${path}`);
}
export async function proportionalDimensions(
path: string,
width: number | null | undefined,
height: number | null | undefined,
): Promise<[number | null, number | null]> {
const w = width ?? null;
const h = height ?? null;
if (w === null && h === null) return [null, null];
if (w !== null && h !== null) return [w, h];
const [nativeWidth, nativeHeight] = await imageDimensions(path);
if (nativeWidth <= 0 || nativeHeight <= 0) throw new Error(`Invalid image dimensions: ${path}`);
if (w !== null) return [w, (w * nativeHeight) / nativeWidth];
return [(h! * nativeWidth) / nativeHeight, h!];
}
const SOF_MARKERS = new Set([0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7, 0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf]);
const NO_LENGTH_MARKERS = new Set<number>([0x01]);
for (let m = 0xd0; m < 0xd8; m++) NO_LENGTH_MARKERS.add(m);
function jpegDimensions(buf: Buffer): [number, number] {
let i = 2; // skip SOI (0xFFD8)
while (i < buf.length) {
if (buf[i] !== 0xff) {
i += 1;
continue;
}
// skip fill bytes
while (i < buf.length && buf[i] === 0xff) i += 1;
if (i >= buf.length) break;
const marker = buf[i];
i += 1;
if (marker === 0xd9) break; // EOI
if (NO_LENGTH_MARKERS.has(marker)) continue;
if (i + 2 > buf.length) break;
const segmentLength = buf.readUInt16BE(i);
if (segmentLength < 2) throw new Error("Invalid JPEG segment length.");
if (SOF_MARKERS.has(marker)) {
if (i + 7 > buf.length) break;
const height = buf.readUInt16BE(i + 3);
const width = buf.readUInt16BE(i + 5);
return [width, height];
}
i += segmentLength;
}
throw new Error("Could not find JPEG dimensions.");
}