mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-11 23:40:24 +00:00
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.
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src';
|
||
import { ReadStream } from 'fs';
|
||
|
||
// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式
|
||
export class WebpParser implements ImageParser {
|
||
readonly type = ImageType.WEBP;
|
||
// WEBP RIFF 头:52 49 46 46 (RIFF)
|
||
private readonly RIFF_SIGNATURE = [0x52, 0x49, 0x46, 0x46];
|
||
// WEBP 魔术头:57 45 42 50 (WEBP)
|
||
private readonly WEBP_SIGNATURE = [0x57, 0x45, 0x42, 0x50];
|
||
|
||
// WEBP 块头
|
||
private readonly CHUNK_VP8 = [0x56, 0x50, 0x38, 0x20]; // "VP8 "
|
||
private readonly CHUNK_VP8L = [0x56, 0x50, 0x38, 0x4C]; // "VP8L"
|
||
private readonly CHUNK_VP8X = [0x56, 0x50, 0x38, 0x58]; // "VP8X"
|
||
|
||
canParse (buffer: Buffer): boolean {
|
||
return (
|
||
buffer.length >= 12 &&
|
||
matchMagic(buffer, this.RIFF_SIGNATURE, 0) &&
|
||
matchMagic(buffer, this.WEBP_SIGNATURE, 8)
|
||
);
|
||
}
|
||
|
||
isChunkType (buffer: Buffer, offset: number, chunkType: number[]): boolean {
|
||
return buffer.length >= offset + 4 && matchMagic(buffer, chunkType, offset);
|
||
}
|
||
|
||
async parseSize (stream: ReadStream): Promise<ImageSize | undefined> {
|
||
return new Promise((resolve, reject) => {
|
||
// 需要读取足够的字节来检测所有三种格式
|
||
const MAX_HEADER_SIZE = 32;
|
||
let totalBytes = 0;
|
||
let buffer = Buffer.alloc(0);
|
||
|
||
stream.on('error', reject);
|
||
|
||
stream.on('data', (chunk: Buffer | string) => {
|
||
const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||
buffer = Buffer.concat([buffer, chunkBuffer]);
|
||
totalBytes += chunk.length;
|
||
|
||
// 检查是否有足够的字节进行格式检测
|
||
if (totalBytes >= MAX_HEADER_SIZE) {
|
||
stream.destroy();
|
||
|
||
// 检查基本的WEBP签名
|
||
if (!this.canParse(buffer)) {
|
||
return resolve(undefined);
|
||
}
|
||
|
||
// 检查chunk头部,位于字节12-15
|
||
if (this.isChunkType(buffer, 12, this.CHUNK_VP8)) {
|
||
// VP8格式 - 标准WebP
|
||
// 宽度和高度在帧头中
|
||
const width = buffer.readUInt16LE(26) & 0x3FFF;
|
||
const height = buffer.readUInt16LE(28) & 0x3FFF;
|
||
return resolve({ width, height });
|
||
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8L)) {
|
||
// VP8L格式 - 无损WebP
|
||
// 1字节标记后是14位宽度和14位高度
|
||
const bits = buffer.readUInt32LE(21);
|
||
const width = 1 + (bits & 0x3FFF);
|
||
const height = 1 + ((bits >> 14) & 0x3FFF);
|
||
return resolve({ width, height });
|
||
} else if (this.isChunkType(buffer, 12, this.CHUNK_VP8X)) {
|
||
// VP8X格式 - 扩展WebP
|
||
// 24位宽度和高度(减去1)
|
||
if (buffer.length < 30) {
|
||
return resolve(undefined);
|
||
}
|
||
const width = 1 + ((buffer[24]! | (buffer[25]! << 8) | (buffer[26]! << 16)) & 0xFFFFFF);
|
||
const height = 1 + ((buffer[27]! | (buffer[28]! << 8) | (buffer[29]! << 16)) & 0xFFFFFF);
|
||
return resolve({ width, height });
|
||
} else {
|
||
// 未知的WebP子格式
|
||
return resolve(undefined);
|
||
}
|
||
}
|
||
});
|
||
|
||
stream.on('end', () => {
|
||
// 如果没有读到足够的字节
|
||
if (totalBytes < MAX_HEADER_SIZE) {
|
||
resolve(undefined);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|