const decoder = new TextDecoder(); const toUTF8String = (input, start = 0, end = input.length) => decoder.decode(input.slice(start, end)); const toHexString = (input, start = 0, end = input.length) => input.slice(start, end).reduce((memo, i) => memo + ("0" + i.toString(16)).slice(-2), ""); const readInt16LE = (input, offset = 0) => { const val = input[offset] + input[offset + 1] * 2 ** 8; return val | (val & 2 ** 15) * 131070; }; const readUInt16BE = (input, offset = 0) => input[offset] * 2 ** 8 + input[offset + 1]; const readUInt16LE = (input, offset = 0) => input[offset] + input[offset + 1] * 2 ** 8; const readUInt24LE = (input, offset = 0) => input[offset] + input[offset + 1] * 2 ** 8 + input[offset + 2] * 2 ** 16; const readInt32LE = (input, offset = 0) => input[offset] + input[offset + 1] * 2 ** 8 + input[offset + 2] * 2 ** 16 + (input[offset + 3] << 24); const readUInt32BE = (input, offset = 0) => input[offset] * 2 ** 24 + input[offset + 1] * 2 ** 16 + input[offset + 2] * 2 ** 8 + input[offset + 3]; const readUInt32LE = (input, offset = 0) => input[offset] + input[offset + 1] * 2 ** 8 + input[offset + 2] * 2 ** 16 + input[offset + 3] * 2 ** 24; const methods = { readUInt16BE, readUInt16LE, readUInt32BE, readUInt32LE }; function readUInt(input, bits, offset, isBigEndian) { offset = offset || 0; const endian = isBigEndian ? "BE" : "LE"; const methodName = "readUInt" + bits + endian; return methods[methodName](input, offset); } const BMP = { validate: (input) => toUTF8String(input, 0, 2) === "BM", calculate: (input) => ({ height: Math.abs(readInt32LE(input, 22)), width: readUInt32LE(input, 18) }) }; const TYPE_ICON = 1; const SIZE_HEADER$1 = 2 + 2 + 2; const SIZE_IMAGE_ENTRY = 1 + 1 + 1 + 1 + 2 + 2 + 4 + 4; function getSizeFromOffset(input, offset) { const value = input[offset]; return value === 0 ? 256 : value; } function getImageSize$1(input, imageIndex) { const offset = SIZE_HEADER$1 + imageIndex * SIZE_IMAGE_ENTRY; return { height: getSizeFromOffset(input, offset + 1), width: getSizeFromOffset(input, offset) }; } const ICO = { validate(input) { const reserved = readUInt16LE(input, 0); const imageCount = readUInt16LE(input, 4); if (reserved !== 0 || imageCount === 0) { return false; } const imageType = readUInt16LE(input, 2); return imageType === TYPE_ICON; }, calculate(input) { const nbImages = readUInt16LE(input, 4); const imageSize = getImageSize$1(input, 0); if (nbImages === 1) { return imageSize; } const imgs = [imageSize]; for (let imageIndex = 1; imageIndex < nbImages; imageIndex += 1) { imgs.push(getImageSize$1(input, imageIndex)); } return { height: imageSize.height, images: imgs, width: imageSize.width }; } }; const TYPE_CURSOR = 2; const CUR = { validate(input) { const reserved = readUInt16LE(input, 0); const imageCount = readUInt16LE(input, 4); if (reserved !== 0 || imageCount === 0) { return false; } const imageType = readUInt16LE(input, 2); return imageType === TYPE_CURSOR; }, calculate: (input) => ICO.calculate(input) }; const DDS = { validate: (input) => readUInt32LE(input, 0) === 542327876, calculate: (input) => ({ height: readUInt32LE(input, 12), width: readUInt32LE(input, 16) }) }; const gifRegexp = /^GIF8[79]a/; const GIF = { validate: (input) => gifRegexp.test(toUTF8String(input, 0, 6)), calculate: (input) => ({ height: readUInt16LE(input, 8), width: readUInt16LE(input, 6) }) }; const SIZE_HEADER = 4 + 4; const FILE_LENGTH_OFFSET = 4; const ENTRY_LENGTH_OFFSET = 4; const ICON_TYPE_SIZE = { ICON: 32, "ICN#": 32, // m => 16 x 16 "icm#": 16, icm4: 16, icm8: 16, // s => 16 x 16 "ics#": 16, ics4: 16, ics8: 16, is32: 16, s8mk: 16, icp4: 16, // l => 32 x 32 icl4: 32, icl8: 32, il32: 32, l8mk: 32, icp5: 32, ic11: 32, // h => 48 x 48 ich4: 48, ich8: 48, ih32: 48, h8mk: 48, // . => 64 x 64 icp6: 64, ic12: 32, // t => 128 x 128 it32: 128, t8mk: 128, ic07: 128, // . => 256 x 256 ic08: 256, ic13: 256, // . => 512 x 512 ic09: 512, ic14: 512, // . => 1024 x 1024 ic10: 1024 }; function readImageHeader(input, imageOffset) { const imageLengthOffset = imageOffset + ENTRY_LENGTH_OFFSET; return [ toUTF8String(input, imageOffset, imageLengthOffset), readUInt32BE(input, imageLengthOffset) ]; } function getImageSize(type) { const size = ICON_TYPE_SIZE[type]; return { width: size, height: size, type }; } const ICNS = { validate: (input) => toUTF8String(input, 0, 4) === "icns", calculate(input) { const inputLength = input.length; const fileLength = readUInt32BE(input, FILE_LENGTH_OFFSET); let imageOffset = SIZE_HEADER; let imageHeader = readImageHeader(input, imageOffset); let imageSize = getImageSize(imageHeader[0]); imageOffset += imageHeader[1]; if (imageOffset === fileLength) { return imageSize; } const result = { height: imageSize.height, images: [imageSize], width: imageSize.width }; while (imageOffset < fileLength && imageOffset < inputLength) { imageHeader = readImageHeader(input, imageOffset); imageSize = getImageSize(imageHeader[0]); imageOffset += imageHeader[1]; result.images.push(imageSize); } return result; } }; const J2C = { // TODO: this doesn't seem right. SIZ marker doesn't have to be right after the SOC validate: (input) => toHexString(input, 0, 4) === "ff4fff51", calculate: (input) => ({ height: readUInt32BE(input, 12), width: readUInt32BE(input, 8) }) }; const BoxTypes = { ftyp: "66747970", ihdr: "69686472", jp2h: "6a703268", jp__: "6a502020", rreq: "72726571", xml_: "786d6c20" }; const calculateRREQLength = (box) => { const unit = box[0]; let offset = 1 + 2 * unit; const numStdFlags = readUInt16BE(box, offset); const flagsLength = numStdFlags * (2 + unit); offset = offset + 2 + flagsLength; const numVendorFeatures = readUInt16BE(box, offset); const featuresLength = numVendorFeatures * (16 + unit); return offset + 2 + featuresLength; }; const parseIHDR = (box) => { return { height: readUInt32BE(box, 4), width: readUInt32BE(box, 8) }; }; const JP2 = { validate(input) { const signature = toHexString(input, 4, 8); const signatureLength = readUInt32BE(input, 0); if (signature !== BoxTypes.jp__ || signatureLength < 1) { return false; } const ftypeBoxStart = signatureLength + 4; const ftypBoxLength = readUInt32BE(input, signatureLength); const ftypBox = input.slice(ftypeBoxStart, ftypeBoxStart + ftypBoxLength); return toHexString(ftypBox, 0, 4) === BoxTypes.ftyp; }, calculate(input) { const signatureLength = readUInt32BE(input, 0); const ftypBoxLength = readUInt16BE(input, signatureLength + 2); let offset = signatureLength + 4 + ftypBoxLength; const nextBoxType = toHexString(input, offset, offset + 4); switch (nextBoxType) { case BoxTypes.rreq: { const MAGIC = 4; offset = offset + 4 + MAGIC + calculateRREQLength(input.slice(offset + 4)); return parseIHDR(input.slice(offset + 8, offset + 24)); } case BoxTypes.jp2h: { return parseIHDR(input.slice(offset + 8, offset + 24)); } default: { throw new TypeError( "Unsupported header found: " + toUTF8String(input, offset, offset + 4) ); } } } }; const EXIF_MARKER = "45786966"; const APP1_DATA_SIZE_BYTES = 2; const EXIF_HEADER_BYTES = 6; const TIFF_BYTE_ALIGN_BYTES = 2; const BIG_ENDIAN_BYTE_ALIGN = "4d4d"; const LITTLE_ENDIAN_BYTE_ALIGN = "4949"; const IDF_ENTRY_BYTES = 12; const NUM_DIRECTORY_ENTRIES_BYTES = 2; function isEXIF(input) { return toHexString(input, 2, 6) === EXIF_MARKER; } function extractSize(input, index) { return { height: readUInt16BE(input, index), width: readUInt16BE(input, index + 2) }; } function extractOrientation(exifBlock, isBigEndian) { const idfOffset = 8; const offset = EXIF_HEADER_BYTES + idfOffset; const idfDirectoryEntries = readUInt(exifBlock, 16, offset, isBigEndian); for (let directoryEntryNumber = 0; directoryEntryNumber < idfDirectoryEntries; directoryEntryNumber++) { const start = offset + NUM_DIRECTORY_ENTRIES_BYTES + directoryEntryNumber * IDF_ENTRY_BYTES; const end = start + IDF_ENTRY_BYTES; if (start > exifBlock.length) { return; } const block = exifBlock.slice(start, end); const tagNumber = readUInt(block, 16, 0, isBigEndian); if (tagNumber === 274) { const dataFormat = readUInt(block, 16, 2, isBigEndian); if (dataFormat !== 3) { return; } const numberOfComponents = readUInt(block, 32, 4, isBigEndian); if (numberOfComponents !== 1) { return; } return readUInt(block, 16, 8, isBigEndian); } } } function validateExifBlock(input, index) { const exifBlock = input.slice(APP1_DATA_SIZE_BYTES, index); const byteAlign = toHexString( exifBlock, EXIF_HEADER_BYTES, EXIF_HEADER_BYTES + TIFF_BYTE_ALIGN_BYTES ); const isBigEndian = byteAlign === BIG_ENDIAN_BYTE_ALIGN; const isLittleEndian = byteAlign === LITTLE_ENDIAN_BYTE_ALIGN; if (isBigEndian || isLittleEndian) { return extractOrientation(exifBlock, isBigEndian); } } function validateInput(input, index) { if (index > input.length) { throw new TypeError("Corrupt JPG, exceeded buffer limits"); } if (input[index] !== 255) { throw new TypeError("Invalid JPG, marker table corrupted"); } } const JPG = { validate: (input) => toHexString(input, 0, 2) === "ffd8", calculate(input) { input = input.slice(4); let orientation; let next; while (input.length > 0) { const i = readUInt16BE(input, 0); if (isEXIF(input)) { orientation = validateExifBlock(input, i); } validateInput(input, i); next = input[i + 1]; if (next === 192 || next === 193 || next === 194) { const size = extractSize(input, i + 5); if (!orientation) { return size; } return { height: size.height, orientation, width: size.width }; } input = input.slice(i + 2); } throw new TypeError("Invalid JPG, no size found"); } }; const KTX = { validate: (input) => toUTF8String(input, 1, 7) === "KTX 11", calculate: (input) => ({ height: readUInt32LE(input, 40), width: readUInt32LE(input, 36) }) }; const pngSignature = "PNG\r\n\n"; const pngImageHeaderChunkName = "IHDR"; const pngFriedChunkName = "CgBI"; const PNG = { validate(input) { if (pngSignature === toUTF8String(input, 1, 8)) { let chunkName = toUTF8String(input, 12, 16); if (chunkName === pngFriedChunkName) { chunkName = toUTF8String(input, 28, 32); } if (chunkName !== pngImageHeaderChunkName) { throw new TypeError("Invalid PNG"); } return true; } return false; }, calculate(input) { if (toUTF8String(input, 12, 16) === pngFriedChunkName) { return { height: readUInt32BE(input, 36), width: readUInt32BE(input, 32) }; } return { height: readUInt32BE(input, 20), width: readUInt32BE(input, 16) }; } }; const PNMTypes = { P1: "pbm/ascii", P2: "pgm/ascii", P3: "ppm/ascii", P4: "pbm", P5: "pgm", P6: "ppm", P7: "pam", PF: "pfm" }; const handlers = { default: (lines) => { let dimensions = []; while (lines.length > 0) { const line = lines.shift(); if (line[0] === "#") { continue; } dimensions = line.split(" "); break; } if (dimensions.length === 2) { return { height: Number.parseInt(dimensions[1], 10), width: Number.parseInt(dimensions[0], 10) }; } else { throw new TypeError("Invalid PNM"); } }, pam: (lines) => { const size = {}; while (lines.length > 0) { const line = lines.shift(); if (line.length > 16 || (line.codePointAt(0) || 0) > 128) { continue; } const [key, value] = line.split(" "); if (key && value) { size[key.toLowerCase()] = Number.parseInt(value, 10); } if (size.height && size.width) { break; } } if (size.height && size.width) { return { height: size.height, width: size.width }; } else { throw new TypeError("Invalid PAM"); } } }; const PNM = { validate: (input) => toUTF8String(input, 0, 2) in PNMTypes, calculate(input) { const signature = toUTF8String(input, 0, 2); const type = PNMTypes[signature]; const lines = toUTF8String(input, 3).split(/[\n\r]+/); const handler = handlers[type] || handlers.default; return handler(lines); } }; const PSD = { validate: (input) => toUTF8String(input, 0, 4) === "8BPS", calculate: (input) => ({ height: readUInt32BE(input, 14), width: readUInt32BE(input, 18) }) }; const svgReg = /]|"[^"]*"|'[^']*')*>/; const extractorRegExps = { height: /\sheight=(["'])([^%]+?)\1/, root: svgReg, viewbox: /\sviewbox=(["'])(.+?)\1/i, width: /\swidth=(["'])([^%]+?)\1/ }; const INCH_CM = 2.54; const units = { in: 96, cm: 96 / INCH_CM, em: 16, ex: 8, m: 96 / INCH_CM * 100, mm: 96 / INCH_CM / 10, pc: 96 / 72 / 12, pt: 96 / 72, px: 1 }; const unitsReg = new RegExp( `^([0-9.]+(?:e\\d+)?)(${Object.keys(units).join("|")})?$` ); function parseLength(len) { const m = unitsReg.exec(len); if (!m) { return void 0; } return Math.round(Number(m[1]) * (units[m[2]] || 1)); } function parseViewbox(viewbox) { const bounds = viewbox.split(" "); return { height: parseLength(bounds[3]), width: parseLength(bounds[2]) }; } function parseAttributes(root) { const width = root.match(extractorRegExps.width); const height = root.match(extractorRegExps.height); const viewbox = root.match(extractorRegExps.viewbox); return { height: height && parseLength(height[2]), viewbox: viewbox && parseViewbox(viewbox[2]), width: width && parseLength(width[2]) }; } function calculateByDimensions(attrs) { return { height: attrs.height, width: attrs.width }; } function calculateByViewbox(attrs, viewbox) { const ratio = viewbox.width / viewbox.height; if (attrs.width) { return { height: Math.floor(attrs.width / ratio), width: attrs.width }; } if (attrs.height) { return { height: attrs.height, width: Math.floor(attrs.height * ratio) }; } return { height: viewbox.height, width: viewbox.width }; } const SVG = { // Scan only the first kilo-byte to speed up the check on larger files validate: (input) => svgReg.test(toUTF8String(input, 0, 1e3)), calculate(input) { const root = toUTF8String(input).match(extractorRegExps.root); if (root) { const attrs = parseAttributes(root[0]); if (attrs.width && attrs.height) { return calculateByDimensions(attrs); } if (attrs.viewbox) { return calculateByViewbox(attrs, attrs.viewbox); } } throw new TypeError("Invalid SVG"); } }; const TGA = { validate(input) { return readUInt16LE(input, 0) === 0 && readUInt16LE(input, 4) === 0; }, calculate(input) { return { height: readUInt16LE(input, 14), width: readUInt16LE(input, 12) }; } }; function readIFD(buffer, isBigEndian) { const ifdOffset = readUInt(buffer, 32, 4, isBigEndian); let bufferSize = 1024; const fileSize = buffer.length; if (ifdOffset + bufferSize > fileSize) { bufferSize = fileSize - ifdOffset - 10; } return buffer.slice(ifdOffset + 2, ifdOffset + 2 + bufferSize); } function readValue(buffer, isBigEndian) { const low = readUInt(buffer, 16, 8, isBigEndian); const high = readUInt(buffer, 16, 10, isBigEndian); return (high << 16) + low; } function nextTag(buffer) { if (buffer.length > 24) { return buffer.slice(12); } } function extractTags(buffer, isBigEndian) { const tags = {}; let temp = buffer; while (temp && temp.length > 0) { const code = readUInt(temp, 16, 0, isBigEndian); const type = readUInt(temp, 16, 2, isBigEndian); const length = readUInt(temp, 32, 4, isBigEndian); if (code === 0) { break; } else { if (length === 1 && (type === 3 || type === 4)) { tags[code] = readValue(temp, isBigEndian); } temp = nextTag(temp); } } return tags; } function determineEndianness(input) { const signature = toUTF8String(input, 0, 2); if (signature === "II") { return "LE"; } else if (signature === "MM") { return "BE"; } } const signatures = /* @__PURE__ */ new Set([ // '492049', // currently not supported "49492a00", // Little endian "4d4d002a" // Big Endian // '4d4d002a', // BigTIFF > 4GB. currently not supported ]); const TIFF = { validate: (input) => signatures.has(toHexString(input, 0, 4)), calculate(input) { const isBigEndian = determineEndianness(input) === "BE"; const ifdBuffer = readIFD(input, isBigEndian); const tags = extractTags(ifdBuffer, isBigEndian); const width = tags[256]; const height = tags[257]; if (!width || !height) { throw new TypeError("Invalid Tiff. Missing tags"); } return { height, width }; } }; function calculateExtended(input) { return { height: 1 + readUInt24LE(input, 7), width: 1 + readUInt24LE(input, 4) }; } function calculateLossless(input) { return { height: 1 + ((input[4] & 15) << 10 | input[3] << 2 | (input[2] & 192) >> 6), width: 1 + ((input[2] & 63) << 8 | input[1]) }; } function calculateLossy(input) { return { height: readInt16LE(input, 8) & 16383, width: readInt16LE(input, 6) & 16383 }; } const WEBP = { validate(input) { const riffHeader = toUTF8String(input, 0, 4) === "RIFF"; const webpHeader = toUTF8String(input, 8, 12) === "WEBP"; const vp8Header = toUTF8String(input, 12, 15) === "VP8"; return riffHeader && webpHeader && vp8Header; }, calculate(input) { const chunkHeader = toUTF8String(input, 12, 16); input = input.slice(20, 30); if (chunkHeader === "VP8X") { const extendedHeader = input[0]; const validStart = (extendedHeader & 192) === 0; const validEnd = (extendedHeader & 1) === 0; if (validStart && validEnd) { return calculateExtended(input); } else { throw new TypeError("Invalid WebP"); } } if (chunkHeader === "VP8 " && input[0] !== 47) { return calculateLossy(input); } const signature = toHexString(input, 3, 6); if (chunkHeader === "VP8L" && signature !== "9d012a") { return calculateLossless(input); } throw new TypeError("Invalid WebP"); } }; const AVIF = { validate: (input) => toUTF8String(input, 8, 12) === "avif", calculate: (input) => { const metaBox = findBox(input, "meta"); const iprpBox = findBox( input, "iprp", metaBox.offset + 12, metaBox.offset + metaBox.size ); const ipcoBox = findBox( input, "ipco", iprpBox.offset + 8, iprpBox.offset + iprpBox.size ); const ispeBox = findBox( input, "ispe", ipcoBox.offset + 8, ipcoBox.offset + ipcoBox.size ); const width = readUInt32BE(input, ispeBox.offset + 12); const height = readUInt32BE(input, ispeBox.offset + 16); return { width, height }; } }; function findBox(input, type, startOffset = 0, endOffset = input.length) { for (let offset = startOffset; offset < endOffset; ) { const size = readUInt32BE(input, offset); const boxType = toUTF8String(input, offset + 4, offset + 8); if (boxType === type) { return { offset, size }; } if (size <= 0 || offset + size > endOffset) { break; } offset += size; } throw new Error(`${type} box not found`); } const typeHandlers = { bmp: BMP, cur: CUR, dds: DDS, gif: GIF, icns: ICNS, ico: ICO, j2c: J2C, jp2: JP2, jpg: JPG, ktx: KTX, png: PNG, pnm: PNM, psd: PSD, svg: SVG, tga: TGA, tiff: TIFF, webp: WEBP, avif: AVIF }; const keys = Object.keys(typeHandlers); const firstBytes = { 56: "psd", 66: "bmp", 68: "dds", 71: "gif", 73: "tiff", 77: "tiff", 82: "webp", 105: "icns", 137: "png", 255: "jpg" }; function detector(input) { const byte = input[0]; if (byte in firstBytes) { const type = firstBytes[byte]; if (type && typeHandlers[type].validate(input)) { return type; } } return keys.find((key) => typeHandlers[key].validate(input)); } function imageMeta(input) { if (!(input instanceof Uint8Array)) { throw new TypeError("Input should be a Uint8Array"); } const type = detector(input); if (type !== void 0 && type in typeHandlers) { const size = typeHandlers[type].calculate(input); if (size !== void 0) { size.type = type; return size; } } throw new TypeError(`Unsupported file type: ${type}`); } export { imageMeta };