mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-03-01 08:10:25 +00:00
Add TIFF parser and buffer-based image size APIs
Introduce a TIFF parser and integrate it into the image detection/size parsing pipeline. Add buffer-based APIs (detectImageTypeFromBuffer, imageSizeFromBuffer, imageSizeFromBufferFallBack) and a helper to convert Buffer to a Readable stream; refactor parser registry into a type-to-parser map and a first-byte fast-path map for quicker detection. Harden WebP parsing with safer length checks. Add sample image resources and a comprehensive Vitest test suite (packages/napcat-test) with updated package dependency and resolve aliases. pnpm-lock updated to link the new package.
This commit is contained in:
@@ -2,9 +2,11 @@ import { BmpParser } from '@/napcat-image-size/src/parser/BmpParser';
|
||||
import { GifParser } from '@/napcat-image-size/src/parser/GifParser';
|
||||
import { JpegParser } from '@/napcat-image-size/src/parser/JpegParser';
|
||||
import { PngParser } from '@/napcat-image-size/src/parser/PngParser';
|
||||
import { TiffParser } from '@/napcat-image-size/src/parser/TiffParser';
|
||||
import { WebpParser } from '@/napcat-image-size/src/parser/WebpParser';
|
||||
import * as fs from 'fs';
|
||||
import { ReadStream } from 'fs';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
export interface ImageSize {
|
||||
width: number;
|
||||
@@ -17,6 +19,7 @@ export enum ImageType {
|
||||
BMP = 'bmp',
|
||||
GIF = 'gif',
|
||||
WEBP = 'webp',
|
||||
TIFF = 'tiff',
|
||||
UNKNOWN = 'unknown',
|
||||
}
|
||||
|
||||
@@ -40,13 +43,39 @@ export function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolea
|
||||
return true;
|
||||
}
|
||||
|
||||
const parsers: ReadonlyArray<ImageParser> = [
|
||||
new PngParser(),
|
||||
new JpegParser(),
|
||||
new BmpParser(),
|
||||
new GifParser(),
|
||||
new WebpParser(),
|
||||
];
|
||||
// 所有解析器实例
|
||||
const parserInstances = {
|
||||
png: new PngParser(),
|
||||
jpeg: new JpegParser(),
|
||||
bmp: new BmpParser(),
|
||||
gif: new GifParser(),
|
||||
webp: new WebpParser(),
|
||||
tiff: new TiffParser(),
|
||||
};
|
||||
|
||||
// 首字节到可能的图片类型映射,用于快速筛选
|
||||
const firstByteMap = new Map<number, ImageType[]>([
|
||||
[0x42, [ImageType.BMP]], // 'B' - BMP
|
||||
[0x47, [ImageType.GIF]], // 'G' - GIF
|
||||
[0x49, [ImageType.TIFF]], // 'I' - TIFF (II - little endian)
|
||||
[0x4D, [ImageType.TIFF]], // 'M' - TIFF (MM - big endian)
|
||||
[0x52, [ImageType.WEBP]], // 'R' - RIFF (WebP)
|
||||
[0x89, [ImageType.PNG]], // PNG signature
|
||||
[0xFF, [ImageType.JPEG]], // JPEG SOI
|
||||
]);
|
||||
|
||||
// 类型到解析器的映射
|
||||
const typeToParser = new Map<ImageType, ImageParser>([
|
||||
[ImageType.PNG, parserInstances.png],
|
||||
[ImageType.JPEG, parserInstances.jpeg],
|
||||
[ImageType.BMP, parserInstances.bmp],
|
||||
[ImageType.GIF, parserInstances.gif],
|
||||
[ImageType.WEBP, parserInstances.webp],
|
||||
[ImageType.TIFF, parserInstances.tiff],
|
||||
]);
|
||||
|
||||
// 所有解析器列表(用于回退)
|
||||
const parsers: ReadonlyArray<ImageParser> = Object.values(parserInstances);
|
||||
|
||||
export async function detectImageType (filePath: string): Promise<ImageType> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -56,18 +85,22 @@ export async function detectImageType (filePath: string): Promise<ImageType> {
|
||||
end: 63,
|
||||
});
|
||||
|
||||
let buffer: Buffer | null = null;
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
stream.once('error', (err) => {
|
||||
stream.on('error', (err) => {
|
||||
stream.destroy();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
stream.once('readable', () => {
|
||||
buffer = stream.read(64) as Buffer;
|
||||
stream.destroy();
|
||||
stream.on('data', (chunk: Buffer | string) => {
|
||||
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
chunks.push(chunkBuffer);
|
||||
});
|
||||
|
||||
if (!buffer) {
|
||||
stream.on('end', () => {
|
||||
const buffer = Buffer.concat(chunks);
|
||||
|
||||
if (buffer.length === 0) {
|
||||
return resolve(ImageType.UNKNOWN);
|
||||
}
|
||||
|
||||
@@ -79,12 +112,6 @@ export async function detectImageType (filePath: string): Promise<ImageType> {
|
||||
|
||||
resolve(ImageType.UNKNOWN);
|
||||
});
|
||||
|
||||
stream.once('end', () => {
|
||||
if (!buffer) {
|
||||
resolve(ImageType.UNKNOWN);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -92,7 +119,7 @@ export async function imageSizeFromFile (filePath: string): Promise<ImageSize |
|
||||
try {
|
||||
// 先检测类型
|
||||
const type = await detectImageType(filePath);
|
||||
const parser = parsers.find(p => p.type === type);
|
||||
const parser = typeToParser.get(type);
|
||||
if (!parser) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -124,3 +151,71 @@ export async function imageSizeFallBack (
|
||||
): Promise<ImageSize> {
|
||||
return await imageSizeFromFile(filePath) ?? fallback;
|
||||
}
|
||||
|
||||
// 从 Buffer 创建可读流
|
||||
function bufferToReadStream (buffer: Buffer): ReadStream {
|
||||
const readable = new Readable({
|
||||
read () {
|
||||
this.push(buffer);
|
||||
this.push(null);
|
||||
}
|
||||
});
|
||||
return readable as unknown as ReadStream;
|
||||
}
|
||||
|
||||
// 从 Buffer 检测图片类型(使用首字节快速筛选)
|
||||
export function detectImageTypeFromBuffer (buffer: Buffer): ImageType {
|
||||
if (buffer.length === 0) {
|
||||
return ImageType.UNKNOWN;
|
||||
}
|
||||
|
||||
const firstByte = buffer[0]!;
|
||||
const possibleTypes = firstByteMap.get(firstByte);
|
||||
|
||||
if (possibleTypes) {
|
||||
// 根据首字节快速筛选可能的类型
|
||||
for (const type of possibleTypes) {
|
||||
const parser = typeToParser.get(type);
|
||||
if (parser && parser.canParse(buffer)) {
|
||||
return parser.type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 回退:遍历所有解析器
|
||||
for (const parser of parsers) {
|
||||
if (parser.canParse(buffer)) {
|
||||
return parser.type;
|
||||
}
|
||||
}
|
||||
|
||||
return ImageType.UNKNOWN;
|
||||
}
|
||||
|
||||
// 从 Buffer 解析图片尺寸
|
||||
export async function imageSizeFromBuffer (buffer: Buffer): Promise<ImageSize | undefined> {
|
||||
const type = detectImageTypeFromBuffer(buffer);
|
||||
const parser = typeToParser.get(type);
|
||||
if (!parser) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const stream = bufferToReadStream(buffer);
|
||||
return await parser.parseSize(stream);
|
||||
} catch (err) {
|
||||
console.error(`解析图片尺寸出错: ${err}`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 从 Buffer 解析图片尺寸,带回退值
|
||||
export async function imageSizeFromBufferFallBack (
|
||||
buffer: Buffer,
|
||||
fallback: ImageSize = {
|
||||
width: 1024,
|
||||
height: 1024,
|
||||
}
|
||||
): Promise<ImageSize> {
|
||||
return await imageSizeFromBuffer(buffer) ?? fallback;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user