export interface ImageSize { width: number; height: number; } function base64ToBytes(b64: string): Uint8Array { const bin = atob(b64); const out = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); return out; } const u16 = (b: Uint8Array, i: number) => (b[i] << 8) | b[i + 1]; const u32 = (b: Uint8Array, i: number) => ((b[i] << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]) >>> 0; function pngSize(b: Uint8Array): ImageSize | null { const sig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; if (b.length < 24 || sig.some((v, i) => b[i] !== v)) return null; return { width: u32(b, 16), height: u32(b, 20) }; } function jpegSize(b: Uint8Array): ImageSize | null { if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null; let i = 2; while (i + 3 < b.length) { if (b[i] !== 0xff) { i++; continue; } const marker = b[i + 1]; if (marker === 0xff) { i++; // fill byte continue; } // Standalone markers carry no length. if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd8)) { i += 2; continue; } const isSof = marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc; if (isSof) { if (i + 8 >= b.length) return null; return { height: u16(b, i + 5), width: u16(b, i + 7) }; } i += 2 + u16(b, i + 2); } return null; } /** Pixel size of a PNG or JPEG data URI, or null when it is another format or malformed. */ export function imageSizeFromDataUri(uri: string): ImageSize | null { const comma = uri.indexOf(","); if (!uri.startsWith("data:") || comma < 0) return null; try { const bytes = base64ToBytes(uri.slice(comma + 1)); const size = pngSize(bytes) ?? jpegSize(bytes); return size && size.width > 0 && size.height > 0 ? size : null; } catch { return null; } }