// Copyright (c) 未来飞马 // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. // // Trademark Notice: // The MPL-2.0 license grants copyright permissions for source code only. // It does NOT grant any rights to use trademarks including "未来飞马", // "Harness Loop", "RSI", and associated slogan "让AI进化提前发生,让AI落地快人一步". // Any use of these trademarks requires separate written permission. /** * 零依赖图片尺寸/类型探测(分组要用「尺寸」信号,不值得为此引第三方库)。 * 支持 PNG / JPEG / GIF / WebP(VP8/VP8L/VP8X)/ BMP。读不出返回 null。 */ import fs from 'node:fs'; const SIGS = [ { ext: 'png', mime: 'image/png', test: (b) => b.length > 8 && b.readUInt32BE(0) === 0x89504e47 }, { ext: 'jpg', mime: 'image/jpeg', test: (b) => b.length > 3 && b[0] === 0xff && b[1] === 0xd8 }, { ext: 'gif', mime: 'image/gif', test: (b) => b.length > 6 && b.slice(0, 3).toString('ascii') === 'GIF' }, { ext: 'webp', mime: 'image/webp', test: (b) => b.length > 12 && b.slice(0, 4).toString('ascii') === 'RIFF' && b.slice(8, 12).toString('ascii') === 'WEBP' }, { ext: 'bmp', mime: 'image/bmp', test: (b) => b.length > 2 && b[0] === 0x42 && b[1] === 0x4d }, ]; function detectFormat(buffer) { for (const sig of SIGS) { try { if (sig.test(buffer)) return sig; } catch { /* 太短,继续 */ } } return null; } function pngSize(buffer) { if (buffer.length < 24) return null; return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) }; } function gifSize(buffer) { if (buffer.length < 10) return null; return { width: buffer.readUInt16LE(6), height: buffer.readUInt16LE(8) }; } function bmpSize(buffer) { if (buffer.length < 26) return null; return { width: Math.abs(buffer.readInt32LE(18)), height: Math.abs(buffer.readInt32LE(22)) }; } function jpegSize(buffer) { let offset = 2; while (offset + 9 < buffer.length) { if (buffer[offset] !== 0xff) { offset += 1; continue; } const marker = buffer[offset + 1]; // SOF0..SOF15,排除 DHT(c4) / JPG(c8) / DAC(cc) if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker)) { return { height: buffer.readUInt16BE(offset + 5), width: buffer.readUInt16BE(offset + 7) }; } const segmentLength = buffer.readUInt16BE(offset + 2); if (segmentLength <= 0) return null; offset += 2 + segmentLength; } return null; } function webpSize(buffer) { const chunk = buffer.slice(12, 16).toString('ascii'); if (chunk === 'VP8 ' && buffer.length > 30) { return { width: buffer.readUInt16LE(26) & 0x3fff, height: buffer.readUInt16LE(28) & 0x3fff }; } if (chunk === 'VP8L' && buffer.length > 25) { const bits = buffer.readUInt32LE(21); return { width: (bits & 0x3fff) + 1, height: ((bits >> 14) & 0x3fff) + 1 }; } if (chunk === 'VP8X' && buffer.length > 30) { const width = 1 + (buffer[24] | (buffer[25] << 8) | (buffer[26] << 16)); const height = 1 + (buffer[27] | (buffer[28] << 8) | (buffer[29] << 16)); return { width, height }; } return null; } /** * 读取图片信息。 * @returns {{ok:boolean, ext:string, mime:string, width:number|null, height:number|null, byteSize:number, error:string|null}} */ export function readImageInfo(filePath) { const base = { ok: false, ext: '', mime: '', width: null, height: null, byteSize: 0, error: null }; let fd; try { const stat = fs.statSync(filePath); base.byteSize = stat.size; const head = Buffer.alloc(Math.min(64 * 1024, Math.max(16, stat.size))); fd = fs.openSync(filePath, 'r'); fs.readSync(fd, head, 0, head.length, 0); const format = detectFormat(head); if (!format) return { ...base, error: 'UNKNOWN_IMAGE_FORMAT' }; let size = null; if (format.ext === 'png') size = pngSize(head); else if (format.ext === 'gif') size = gifSize(head); else if (format.ext === 'bmp') size = bmpSize(head); else if (format.ext === 'jpg') size = jpegSize(head); else if (format.ext === 'webp') size = webpSize(head); return { ...base, ok: true, ext: format.ext, mime: format.mime, width: size ? size.width : null, height: size ? size.height : null, mtimeMs: stat.mtimeMs, error: size ? null : 'SIZE_NOT_FOUND', }; } catch (error) { return { ...base, error: `READ_FAILED: ${error.message}` }; } finally { if (fd !== undefined) { try { fs.closeSync(fd); } catch { /* ignore */ } } } } export const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tif', '.tiff', '.heic']); export const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.m4v', '.avi', '.webm', '.mkv', '.wmv']); export const DOC_EXTENSIONS = new Set(['.docx', '.docm']); export const DECK_EXTENSIONS = new Set(['.pptx', '.pptm']); export const TEXT_EXTENSIONS = new Set(['.md', '.txt', '.markdown', '.csv']);