| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- // 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']);
|