Something went wrong. Try again.
A fully featured CLI used as an alternative to AppView applications (Bsky.app/mu.social)
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158// Reading just enough of an image or a video to describe it: what type it is// (from its bytes, not its name) and how big, which posts carry as an aspect// ratio so clients can lay them out before loading them.
export interface MediaInfo { mimeType: string; width?: number | undefined; height?: number | undefined;}
export const IMAGE_MAX_BYTES = 2_000_000;export const VIDEO_MAX_BYTES = 300_000_000;
export function sniff(bytes: Uint8Array): MediaInfo | undefined { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const ascii = (at: number, length: number) => String.fromCharCode(...bytes.subarray(at, at + length));
// PNG: IHDR is always first. if (bytes.length > 24 && bytes[0] === 0x89 && ascii(1, 3) === 'PNG') { return { mimeType: 'image/png', width: view.getUint32(16), height: view.getUint32(20) }; } // GIF if (bytes.length > 10 && ascii(0, 4) === 'GIF8') { return { mimeType: 'image/gif', width: view.getUint16(6, true), height: view.getUint16(8, true) }; } // WebP: lossy, lossless, extended. if (bytes.length > 30 && ascii(0, 4) === 'RIFF' && ascii(8, 4) === 'WEBP') { const kind = ascii(12, 4); if (kind === 'VP8 ') return { mimeType: 'image/webp', width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff }; if (kind === 'VP8L') { const bits = view.getUint32(21, true); return { mimeType: 'image/webp', width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 }; } if (kind === 'VP8X') { const width = 1 + (bytes[24]! | (bytes[25]! << 8) | (bytes[26]! << 16)); const height = 1 + (bytes[27]! | (bytes[28]! << 8) | (bytes[29]! << 16)); return { mimeType: 'image/webp', width, height }; } return { mimeType: 'image/webp' }; } // JPEG: walk the segments to the first start-of-frame. if (bytes.length > 4 && bytes[0] === 0xff && bytes[1] === 0xd8) { let at = 2; while (at + 9 < bytes.length) { if (bytes[at] !== 0xff) { at++; continue; } const marker = bytes[at + 1]!; if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) { at += 2; continue; } const length = view.getUint16(at + 2); if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) { const height = view.getUint16(at + 5); const width = view.getUint16(at + 7); return { mimeType: 'image/jpeg', ...orient(bytes, width, height) }; } at += 2 + length; } return { mimeType: 'image/jpeg' }; } // AVIF / HEIC and MP4 / MOV share the ISO base media file format. if (bytes.length > 12 && ascii(4, 4) === 'ftyp') { const brand = ascii(8, 4); if (brand === 'avif' || brand === 'avis') return { mimeType: 'image/avif', ...ispe(bytes) }; if (/^(heic|heix|mif1|msf1)$/.test(brand)) return { mimeType: 'image/heic', ...ispe(bytes) }; const mimeType = brand === 'qt ' ? 'video/quicktime' : 'video/mp4'; return { mimeType, ...trackSize(bytes) }; } if (bytes.length > 4 && view.getUint32(0) === 0x1a45dfa3) return { mimeType: 'video/webm' }; return undefined;}
/** A JPEG whose EXIF says it is rotated a quarter turn is shown with its sides swapped. */function orient(bytes: Uint8Array, width: number, height: number): { width: number; height: number } { const exif = findBytes(bytes, [0x45, 0x78, 0x69, 0x66, 0x00, 0x00], 0, Math.min(bytes.length, 65536)); if (exif < 0) return { width, height }; const tiff = exif + 6; const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); if (tiff + 8 > bytes.length) return { width, height }; const little = bytes[tiff] === 0x49; const ifd = tiff + view.getUint32(tiff + 4, little); if (ifd + 2 > bytes.length) return { width, height }; const count = view.getUint16(ifd, little); for (let i = 0; i < count; i++) { const entry = ifd + 2 + i * 12; if (entry + 12 > bytes.length) break; if (view.getUint16(entry, little) === 0x0112) { const orientation = view.getUint16(entry + 8, little); return orientation >= 5 && orientation <= 8 ? { width: height, height: width } : { width, height }; } } return { width, height };}
function findBytes(bytes: Uint8Array, needle: number[], from: number, to: number): number { outer: for (let at = from; at <= to - needle.length; at++) { for (let j = 0; j < needle.length; j++) if (bytes[at + j] !== needle[j]) continue outer; return at; } return -1;}
/** The `ispe` (image spatial extent) property of an AVIF or HEIC. */function ispe(bytes: Uint8Array): { width?: number; height?: number } { const at = findBytes(bytes, [0x69, 0x73, 0x70, 0x65], 0, Math.min(bytes.length, 1 << 20)); if (at < 0 || at + 16 > bytes.length) return {}; const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); return { width: view.getUint32(at + 8), height: view.getUint32(at + 12) };}
/** The size of the first video track: `moov/trak/tkhd`, whose width and height are 16.16 fixed point. */function trackSize(bytes: Uint8Array): { width?: number; height?: number } { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const walk = (start: number, end: number, path: string[]): { width?: number; height?: number } | undefined => { let at = start; while (at + 8 <= end) { let size = view.getUint32(at); const type = String.fromCharCode(...bytes.subarray(at + 4, at + 8)); let header = 8; if (size === 1 && at + 16 <= end) { size = Number(view.getBigUint64(at + 8)); header = 16; } else if (size === 0) { size = end - at; } if (size < header || at + size > end) return undefined; if (type === path[0]) { if (path.length === 1) { const version = bytes[at + header]!; const offset = at + header + (version === 1 ? 88 : 76); if (offset + 8 > end) return undefined; const width = view.getUint32(offset) / 65536; const height = view.getUint32(offset + 4) / 65536; if (width > 0 && height > 0) return { width: Math.round(width), height: Math.round(height) }; } else { const found = walk(at + header, at + size, path.slice(1)); if (found) return found; if (type === 'trak') { at += size; continue; } } } at += size; } return undefined; }; return walk(0, bytes.length, ['moov', 'trak', 'tkhd']) ?? {};}
/** An aspect ratio as posts carry it: whole numbers, reduced. */export function aspectRatio(info: MediaInfo): { width: number; height: number } | undefined { if (!info.width || !info.height) return undefined; const gcd = (a: number, b: number): number => (b ? gcd(b, a % b) : a); const divisor = gcd(info.width, info.height); return { width: info.width / divisor, height: info.height / divisor };}
export function formatBytes(size: number): string { if (size < 1024) return `${size} B`; if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} kB`; return `${(size / 1024 / 1024).toFixed(1)} MB`;}