diff --git a/packages/napcat-image-size/src/index.ts b/packages/napcat-image-size/src/index.ts index 8626a7a7..5e4f52be 100644 --- a/packages/napcat-image-size/src/index.ts +++ b/packages/napcat-image-size/src/index.ts @@ -1,3 +1,8 @@ +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 { WebpParser } from '@/napcat-image-size/src/parser/WebpParser'; import * as fs from 'fs'; import { ReadStream } from 'fs'; @@ -15,14 +20,14 @@ export enum ImageType { UNKNOWN = 'unknown', } -interface ImageParser { +export interface ImageParser { readonly type: ImageType; - canParse(buffer: Buffer): boolean; - parseSize(stream: ReadStream): Promise; + canParse (buffer: Buffer): boolean; + parseSize (stream: ReadStream): Promise; } // 魔术匹配 -function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean { +export function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean { if (buffer.length < offset + magic.length) { return false; } @@ -35,309 +40,6 @@ function matchMagic (buffer: Buffer, magic: number[], offset = 0): boolean { return true; } -// PNG解析器 -class PngParser implements ImageParser { - readonly type = ImageType.PNG; - // PNG 魔术头:89 50 4E 47 0D 0A 1A 0A - private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; - - canParse (buffer: Buffer): boolean { - return matchMagic(buffer, this.PNG_SIGNATURE); - } - - async parseSize (stream: ReadStream): Promise { - return new Promise((resolve, reject) => { - stream.once('error', reject); - stream.once('readable', () => { - const buf = stream.read(24) as Buffer; - if (!buf || buf.length < 24) { - return resolve(undefined); - } - if (this.canParse(buf)) { - const width = buf.readUInt32BE(16); - const height = buf.readUInt32BE(20); - resolve({ width, height }); - } else { - resolve(undefined); - } - }); - }); - } -} - -// JPEG解析器 -class JpegParser implements ImageParser { - readonly type = ImageType.JPEG; - // JPEG 魔术头:FF D8 - private readonly JPEG_SIGNATURE = [0xFF, 0xD8]; - - // JPEG标记常量 - private readonly SOF_MARKERS = { - SOF0: 0xC0, // 基线DCT - SOF1: 0xC1, // 扩展顺序DCT - SOF2: 0xC2, // 渐进式DCT - SOF3: 0xC3, // 无损 - } as const; - - // 非SOF标记 - private readonly NON_SOF_MARKERS: number[] = [ - 0xC4, // DHT - 0xC8, // JPEG扩展 - 0xCC, // DAC - ] as const; - - canParse (buffer: Buffer): boolean { - return matchMagic(buffer, this.JPEG_SIGNATURE); - } - - isSOFMarker (marker: number): boolean { - return ( - marker === this.SOF_MARKERS.SOF0 || - marker === this.SOF_MARKERS.SOF1 || - marker === this.SOF_MARKERS.SOF2 || - marker === this.SOF_MARKERS.SOF3 - ); - } - - isNonSOFMarker (marker: number): boolean { - return this.NON_SOF_MARKERS.includes(marker); - } - - async parseSize (stream: ReadStream): Promise { - return new Promise((resolve, reject) => { - const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整 - let buffer = Buffer.alloc(0); - let offset = 0; - let found = false; - - // 处理错误 - stream.on('error', (err) => { - stream.destroy(); - reject(err); - }); - - // 处理数据块 - stream.on('data', (chunk: Buffer | string) => { - // 追加新数据到缓冲区 - const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); - buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]); - offset = 0; - - // 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配 - const bufferSize = buffer.length; - const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数 - - // 从JPEG头部后开始扫描 - while (offset < bufferSize - MIN_REQUIRED_BYTES) { - // 寻找FF标记 - if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) { - const marker = buffer[offset + 1]; - if (!marker) { - break; - } - // 跳过非SOF标记 - if (this.isNonSOFMarker(marker)) { - offset += 2; - continue; - } - - // 处理SOF标记 (包含尺寸信息) - if (this.isSOFMarker(marker)) { - // 确保缓冲区中有足够数据读取尺寸 - if (offset + 9 < bufferSize) { - // 解析尺寸: FF XX YY YY PP HH HH WW WW ... - // XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽 - const height = buffer.readUInt16BE(offset + 5); - const width = buffer.readUInt16BE(offset + 7); - - found = true; - stream.destroy(); - resolve({ width, height }); - return; - } else { - // 如果缓冲区内数据不够,保留当前位置等待更多数据 - break; - } - } - } - - offset++; - } - - // 缓冲区管理: 如果处理了许多数据但没找到标记, - // 保留最后N字节用于跨块匹配,丢弃之前的数据 - if (offset > BUFFER_SIZE) { - const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况 - if (offset > KEEP_BYTES) { - buffer = buffer.subarray(offset - KEEP_BYTES); - offset = KEEP_BYTES; - } - } - }); - - // 处理流结束 - stream.on('end', () => { - if (!found) { - resolve(undefined); - } - }); - }); - } -} - -// BMP解析器 -class BmpParser implements ImageParser { - readonly type = ImageType.BMP; - // BMP 魔术头:42 4D (BM) - private readonly BMP_SIGNATURE = [0x42, 0x4D]; - - canParse (buffer: Buffer): boolean { - return matchMagic(buffer, this.BMP_SIGNATURE); - } - - async parseSize (stream: ReadStream): Promise { - return new Promise((resolve, reject) => { - stream.once('error', reject); - stream.once('readable', () => { - const buf = stream.read(26) as Buffer; - if (!buf || buf.length < 26) { - return resolve(undefined); - } - if (this.canParse(buf)) { - const width = buf.readUInt32LE(18); - const height = buf.readUInt32LE(22); - resolve({ width, height }); - } else { - resolve(undefined); - } - }); - }); - } -} - -// GIF解析器 -class GifParser implements ImageParser { - readonly type = ImageType.GIF; - // GIF87a 魔术头:47 49 46 38 37 61 - private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]; - // GIF89a 魔术头:47 49 46 38 39 61 - private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; - - canParse (buffer: Buffer): boolean { - return ( - matchMagic(buffer, this.GIF87A_SIGNATURE) || - matchMagic(buffer, this.GIF89A_SIGNATURE) - ); - } - - async parseSize (stream: ReadStream): Promise { - return new Promise((resolve, reject) => { - stream.once('error', reject); - stream.once('readable', () => { - const buf = stream.read(10) as Buffer; - if (!buf || buf.length < 10) { - return resolve(undefined); - } - if (this.canParse(buf)) { - const width = buf.readUInt16LE(6); - const height = buf.readUInt16LE(8); - resolve({ width, height }); - } else { - resolve(undefined); - } - }); - }); - } -} - -// WEBP解析器 - 完整支持VP8, VP8L, VP8X格式 -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 { - 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[24] || !buffer[25] || !buffer[26] || !buffer[27] || !buffer[28] || !buffer[29]) { - 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); - } - }); - }); - } -} - const parsers: ReadonlyArray = [ new PngParser(), new JpegParser(), diff --git a/packages/napcat-image-size/src/parser/BmpParser.ts b/packages/napcat-image-size/src/parser/BmpParser.ts new file mode 100644 index 00000000..8b3128af --- /dev/null +++ b/packages/napcat-image-size/src/parser/BmpParser.ts @@ -0,0 +1,32 @@ +import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src'; +import { ReadStream } from 'fs'; + +// BMP解析器 +export class BmpParser implements ImageParser { + readonly type = ImageType.BMP; + // BMP 魔术头:42 4D (BM) + private readonly BMP_SIGNATURE = [0x42, 0x4D]; + + canParse (buffer: Buffer): boolean { + return matchMagic(buffer, this.BMP_SIGNATURE); + } + + async parseSize (stream: ReadStream): Promise { + return new Promise((resolve, reject) => { + stream.once('error', reject); + stream.once('readable', () => { + const buf = stream.read(26) as Buffer; + if (!buf || buf.length < 26) { + return resolve(undefined); + } + if (this.canParse(buf)) { + const width = buf.readUInt32LE(18); + const height = buf.readUInt32LE(22); + resolve({ width, height }); + } else { + resolve(undefined); + } + }); + }); + } +} diff --git a/packages/napcat-image-size/src/parser/GifParser.ts b/packages/napcat-image-size/src/parser/GifParser.ts new file mode 100644 index 00000000..c82e1a10 --- /dev/null +++ b/packages/napcat-image-size/src/parser/GifParser.ts @@ -0,0 +1,37 @@ +import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src'; +import { ReadStream } from 'fs'; + +// GIF解析器 +export class GifParser implements ImageParser { + readonly type = ImageType.GIF; + // GIF87a 魔术头:47 49 46 38 37 61 + private readonly GIF87A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]; + // GIF89a 魔术头:47 49 46 38 39 61 + private readonly GIF89A_SIGNATURE = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; + + canParse (buffer: Buffer): boolean { + return ( + matchMagic(buffer, this.GIF87A_SIGNATURE) || + matchMagic(buffer, this.GIF89A_SIGNATURE) + ); + } + + async parseSize (stream: ReadStream): Promise { + return new Promise((resolve, reject) => { + stream.once('error', reject); + stream.once('readable', () => { + const buf = stream.read(10) as Buffer; + if (!buf || buf.length < 10) { + return resolve(undefined); + } + if (this.canParse(buf)) { + const width = buf.readUInt16LE(6); + const height = buf.readUInt16LE(8); + resolve({ width, height }); + } else { + resolve(undefined); + } + }); + }); + } +} diff --git a/packages/napcat-image-size/src/parser/JpegParser.ts b/packages/napcat-image-size/src/parser/JpegParser.ts new file mode 100644 index 00000000..042c899b --- /dev/null +++ b/packages/napcat-image-size/src/parser/JpegParser.ts @@ -0,0 +1,123 @@ +import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src'; +import { ReadStream } from 'fs'; + +// JPEG解析器 +export class JpegParser implements ImageParser { + readonly type = ImageType.JPEG; + // JPEG 魔术头:FF D8 + private readonly JPEG_SIGNATURE = [0xFF, 0xD8]; + + // JPEG标记常量 + private readonly SOF_MARKERS = { + SOF0: 0xC0, // 基线DCT + SOF1: 0xC1, // 扩展顺序DCT + SOF2: 0xC2, // 渐进式DCT + SOF3: 0xC3, // 无损 + } as const; + + // 非SOF标记 + private readonly NON_SOF_MARKERS: number[] = [ + 0xC4, // DHT + 0xC8, // JPEG扩展 + 0xCC, // DAC + ] as const; + + canParse (buffer: Buffer): boolean { + return matchMagic(buffer, this.JPEG_SIGNATURE); + } + + isSOFMarker (marker: number): boolean { + return ( + marker === this.SOF_MARKERS.SOF0 || + marker === this.SOF_MARKERS.SOF1 || + marker === this.SOF_MARKERS.SOF2 || + marker === this.SOF_MARKERS.SOF3 + ); + } + + isNonSOFMarker (marker: number): boolean { + return this.NON_SOF_MARKERS.includes(marker); + } + + async parseSize (stream: ReadStream): Promise { + return new Promise((resolve, reject) => { + const BUFFER_SIZE = 1024; // 读取块大小,可以根据需要调整 + let buffer = Buffer.alloc(0); + let offset = 0; + let found = false; + + // 处理错误 + stream.on('error', (err) => { + stream.destroy(); + reject(err); + }); + + // 处理数据块 + stream.on('data', (chunk: Buffer | string) => { + // 追加新数据到缓冲区 + const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + buffer = Buffer.concat([buffer.subarray(offset), chunkBuffer]); + offset = 0; + + // 保持缓冲区在合理大小内,只保留最后的部分用于跨块匹配 + const bufferSize = buffer.length; + const MIN_REQUIRED_BYTES = 10; // SOF段最低字节数 + + + // 从JPEG头部后开始扫描 + while (offset < bufferSize - MIN_REQUIRED_BYTES) { + // 寻找FF标记 + if (buffer[offset] === 0xFF && buffer[offset + 1]! >= 0xC0 && buffer[offset + 1]! <= 0xCF) { + const marker = buffer[offset + 1]; + if (!marker) { + break; + } + // 跳过非SOF标记 + if (this.isNonSOFMarker(marker)) { + offset += 2; + continue; + } + + // 处理SOF标记 (包含尺寸信息) + if (this.isSOFMarker(marker)) { + // 确保缓冲区中有足够数据读取尺寸 + if (offset + 9 < bufferSize) { + // 解析尺寸: FF XX YY YY PP HH HH WW WW ... + // XX = 标记, YY YY = 段长度, PP = 精度, HH HH = 高, WW WW = 宽 + const height = buffer.readUInt16BE(offset + 5); + const width = buffer.readUInt16BE(offset + 7); + + found = true; + stream.destroy(); + resolve({ width, height }); + return; + } else { + // 如果缓冲区内数据不够,保留当前位置等待更多数据 + break; + } + } + } + + offset++; + } + + // 缓冲区管理: 如果处理了许多数据但没找到标记, + // 保留最后N字节用于跨块匹配,丢弃之前的数据 + if (offset > BUFFER_SIZE) { + const KEEP_BYTES = 20; // 保留足够数据以处理跨块边界的情况 + if (offset > KEEP_BYTES) { + buffer = buffer.subarray(offset - KEEP_BYTES); + offset = KEEP_BYTES; + } + } + }); + + // 处理流结束 + stream.on('end', () => { + if (!found) { + resolve(undefined); + } + }); + }); + } +} diff --git a/packages/napcat-image-size/src/parser/PngParser.ts b/packages/napcat-image-size/src/parser/PngParser.ts new file mode 100644 index 00000000..b0be8de0 --- /dev/null +++ b/packages/napcat-image-size/src/parser/PngParser.ts @@ -0,0 +1,32 @@ +import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src'; +import { ReadStream } from 'fs'; + +// PNG解析器 +export class PngParser implements ImageParser { + readonly type = ImageType.PNG; + // PNG 魔术头:89 50 4E 47 0D 0A 1A 0A + private readonly PNG_SIGNATURE = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]; + + canParse (buffer: Buffer): boolean { + return matchMagic(buffer, this.PNG_SIGNATURE); + } + + async parseSize (stream: ReadStream): Promise { + return new Promise((resolve, reject) => { + stream.once('error', reject); + stream.once('readable', () => { + const buf = stream.read(24) as Buffer; + if (!buf || buf.length < 24) { + return resolve(undefined); + } + if (this.canParse(buf)) { + const width = buf.readUInt32BE(16); + const height = buf.readUInt32BE(20); + resolve({ width, height }); + } else { + resolve(undefined); + } + }); + }); + } +} diff --git a/packages/napcat-image-size/src/parser/WebpParser.ts b/packages/napcat-image-size/src/parser/WebpParser.ts new file mode 100644 index 00000000..47ae52b0 --- /dev/null +++ b/packages/napcat-image-size/src/parser/WebpParser.ts @@ -0,0 +1,90 @@ +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 { + 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[24] || !buffer[25] || !buffer[26] || !buffer[27] || !buffer[28] || !buffer[29]) { + 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); + } + }); + }); + } +}