diff --git a/packages/napcat-image-size/resource/test-20x20.jpg b/packages/napcat-image-size/resource/test-20x20.jpg new file mode 100644 index 00000000..214ae247 Binary files /dev/null and b/packages/napcat-image-size/resource/test-20x20.jpg differ diff --git a/packages/napcat-image-size/resource/test-20x20.png b/packages/napcat-image-size/resource/test-20x20.png new file mode 100644 index 00000000..3cef9a87 Binary files /dev/null and b/packages/napcat-image-size/resource/test-20x20.png differ diff --git a/packages/napcat-image-size/resource/test-20x20.tiff b/packages/napcat-image-size/resource/test-20x20.tiff new file mode 100644 index 00000000..33711d0b Binary files /dev/null and b/packages/napcat-image-size/resource/test-20x20.tiff differ diff --git a/packages/napcat-image-size/resource/test-20x20.webp b/packages/napcat-image-size/resource/test-20x20.webp new file mode 100644 index 00000000..60e96f0a Binary files /dev/null and b/packages/napcat-image-size/resource/test-20x20.webp differ diff --git a/packages/napcat-image-size/resource/test-490x498.gif b/packages/napcat-image-size/resource/test-490x498.gif new file mode 100644 index 00000000..5dcae1be Binary files /dev/null and b/packages/napcat-image-size/resource/test-490x498.gif differ diff --git a/packages/napcat-image-size/src/index.ts b/packages/napcat-image-size/src/index.ts index 5e4f52be..8527dd05 100644 --- a/packages/napcat-image-size/src/index.ts +++ b/packages/napcat-image-size/src/index.ts @@ -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 = [ - 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([ + [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.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 = Object.values(parserInstances); export async function detectImageType (filePath: string): Promise { return new Promise((resolve, reject) => { @@ -56,18 +85,22 @@ export async function detectImageType (filePath: string): Promise { 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 { resolve(ImageType.UNKNOWN); }); - - stream.once('end', () => { - if (!buffer) { - resolve(ImageType.UNKNOWN); - } - }); }); } @@ -92,7 +119,7 @@ export async function imageSizeFromFile (filePath: string): Promise p.type === type); + const parser = typeToParser.get(type); if (!parser) { return undefined; } @@ -124,3 +151,71 @@ export async function imageSizeFallBack ( ): Promise { 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 { + 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 { + return await imageSizeFromBuffer(buffer) ?? fallback; +} diff --git a/packages/napcat-image-size/src/parser/TiffParser.ts b/packages/napcat-image-size/src/parser/TiffParser.ts new file mode 100644 index 00000000..243ebe31 --- /dev/null +++ b/packages/napcat-image-size/src/parser/TiffParser.ts @@ -0,0 +1,124 @@ +import { ImageParser, ImageType, matchMagic, ImageSize } from '@/napcat-image-size/src'; +import { ReadStream } from 'fs'; + +// TIFF解析器 +export class TiffParser implements ImageParser { + readonly type = ImageType.TIFF; + // TIFF Little Endian 魔术头:49 49 2A 00 (II) + private readonly TIFF_LE_SIGNATURE = [0x49, 0x49, 0x2A, 0x00]; + // TIFF Big Endian 魔术头:4D 4D 00 2A (MM) + private readonly TIFF_BE_SIGNATURE = [0x4D, 0x4D, 0x00, 0x2A]; + + canParse (buffer: Buffer): boolean { + return ( + matchMagic(buffer, this.TIFF_LE_SIGNATURE) || + matchMagic(buffer, this.TIFF_BE_SIGNATURE) + ); + } + + async parseSize (stream: ReadStream): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + let totalBytes = 0; + const MAX_BYTES = 64 * 1024; // 最多读取 64KB + + stream.on('error', reject); + + stream.on('data', (chunk: Buffer | string) => { + const chunkBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + chunks.push(chunkBuffer); + totalBytes += chunkBuffer.length; + + if (totalBytes >= MAX_BYTES) { + stream.destroy(); + } + }); + + stream.on('end', () => { + const buffer = Buffer.concat(chunks); + const size = this.parseTiffSize(buffer); + resolve(size); + }); + + stream.on('close', () => { + if (chunks.length > 0) { + const buffer = Buffer.concat(chunks); + const size = this.parseTiffSize(buffer); + resolve(size); + } + }); + }); + } + + private parseTiffSize (buffer: Buffer): ImageSize | undefined { + if (buffer.length < 8) { + return undefined; + } + + // 判断字节序 + const isLittleEndian = buffer[0] === 0x49; // 'I' + + const readUInt16 = isLittleEndian + ? (offset: number) => buffer.readUInt16LE(offset) + : (offset: number) => buffer.readUInt16BE(offset); + + const readUInt32 = isLittleEndian + ? (offset: number) => buffer.readUInt32LE(offset) + : (offset: number) => buffer.readUInt32BE(offset); + + // 获取第一个 IFD 的偏移量 + const ifdOffset = readUInt32(4); + if (ifdOffset + 2 > buffer.length) { + return undefined; + } + + // 读取 IFD 条目数量 + const numEntries = readUInt16(ifdOffset); + let width: number | undefined; + let height: number | undefined; + + // TIFF 标签 + const TAG_IMAGE_WIDTH = 0x0100; + const TAG_IMAGE_HEIGHT = 0x0101; + + // 遍历 IFD 条目 + for (let i = 0; i < numEntries; i++) { + const entryOffset = ifdOffset + 2 + i * 12; + if (entryOffset + 12 > buffer.length) { + break; + } + + const tag = readUInt16(entryOffset); + const type = readUInt16(entryOffset + 2); + // const count = readUInt32(entryOffset + 4); + + // 根据类型读取值 + let value: number; + if (type === 3) { + // SHORT (2 bytes) + value = readUInt16(entryOffset + 8); + } else if (type === 4) { + // LONG (4 bytes) + value = readUInt32(entryOffset + 8); + } else { + continue; + } + + if (tag === TAG_IMAGE_WIDTH) { + width = value; + } else if (tag === TAG_IMAGE_HEIGHT) { + height = value; + } + + if (width !== undefined && height !== undefined) { + return { width, height }; + } + } + + if (width !== undefined && height !== undefined) { + return { width, height }; + } + + return undefined; + } +} diff --git a/packages/napcat-image-size/src/parser/WebpParser.ts b/packages/napcat-image-size/src/parser/WebpParser.ts index 47ae52b0..cb58f07a 100644 --- a/packages/napcat-image-size/src/parser/WebpParser.ts +++ b/packages/napcat-image-size/src/parser/WebpParser.ts @@ -66,11 +66,11 @@ export class WebpParser implements ImageParser { } 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]) { + 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); + 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子格式 diff --git a/packages/napcat-test/imageSize.test.ts b/packages/napcat-test/imageSize.test.ts new file mode 100644 index 00000000..f9306816 --- /dev/null +++ b/packages/napcat-test/imageSize.test.ts @@ -0,0 +1,346 @@ +import { describe, it, expect } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { + detectImageTypeFromBuffer, + imageSizeFromBuffer, + imageSizeFromBufferFallBack, + imageSizeFromFile, + matchMagic, + ImageType, +} from '@/napcat-image-size/src'; + +// resource 目录路径 +const resourceDir = path.resolve(__dirname, '../napcat-image-size/resource'); + +// 测试用的 Buffer 数据 +const testBuffers = { + // PNG 测试图片 (100x200) + png: Buffer.from([ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52, + 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xC8, + 0x08, 0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]), + + // JPEG 测试图片 (320x240) + jpeg: Buffer.from([ + 0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, + 0x4A, 0x46, 0x49, 0x46, 0x00, + 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, + 0xFF, 0xC0, 0x00, 0x0B, 0x08, + 0x00, 0xF0, 0x01, 0x40, 0x03, 0x01, 0x22, 0x00, + ]), + + // BMP 测试图片 (640x480) + bmp: (() => { + const buf = Buffer.alloc(54); + buf.write('BM', 0); + buf.writeUInt32LE(54, 2); + buf.writeUInt32LE(0, 6); + buf.writeUInt32LE(54, 10); + buf.writeUInt32LE(40, 14); + buf.writeUInt32LE(640, 18); + buf.writeUInt32LE(480, 22); + buf.writeUInt16LE(1, 26); + buf.writeUInt16LE(24, 28); + return buf; + })(), + + // GIF87a 测试图片 (800x600) + gif87a: Buffer.from([ + 0x47, 0x49, 0x46, 0x38, 0x37, 0x61, + 0x20, 0x03, 0x58, 0x02, 0x00, 0x00, 0x00, + ]), + + // GIF89a 测试图片 (1024x768) + gif89a: Buffer.from([ + 0x47, 0x49, 0x46, 0x38, 0x39, 0x61, + 0x00, 0x04, 0x00, 0x03, 0x00, 0x00, 0x00, + ]), + + // WebP VP8 测试图片 (1920x1080) + webpVP8: (() => { + const buf = Buffer.alloc(32); + buf.write('RIFF', 0); + buf.writeUInt32LE(24, 4); + buf.write('WEBP', 8); + buf.write('VP8 ', 12); + buf.writeUInt32LE(14, 16); + buf.writeUInt8(0x9D, 20); + buf.writeUInt8(0x01, 21); + buf.writeUInt8(0x2A, 22); + buf.writeUInt16LE(1920 & 0x3FFF, 26); + buf.writeUInt16LE(1080 & 0x3FFF, 28); + return buf; + })(), + + // WebP VP8L 测试图片 (256x128) + webpVP8L: (() => { + const buf = Buffer.alloc(32); + buf.write('RIFF', 0); + buf.writeUInt32LE(24, 4); + buf.write('WEBP', 8); + buf.write('VP8L', 12); + buf.writeUInt32LE(10, 16); + buf.writeUInt8(0x2F, 20); + const vp8lBits = (256 - 1) | ((128 - 1) << 14); + buf.writeUInt32LE(vp8lBits, 21); + return buf; + })(), + + // WebP VP8X 测试图片 (512x384) + webpVP8X: (() => { + const buf = Buffer.alloc(32); + buf.write('RIFF', 0); + buf.writeUInt32LE(24, 4); + buf.write('WEBP', 8); + buf.write('VP8X', 12); + buf.writeUInt32LE(10, 16); + buf.writeUInt8((512 - 1) & 0xFF, 24); + buf.writeUInt8(((512 - 1) >> 8) & 0xFF, 25); + buf.writeUInt8(((512 - 1) >> 16) & 0xFF, 26); + buf.writeUInt8((384 - 1) & 0xFF, 27); + buf.writeUInt8(((384 - 1) >> 8) & 0xFF, 28); + buf.writeUInt8(((384 - 1) >> 16) & 0xFF, 29); + return buf; + })(), + + // TIFF Little Endian 测试图片 + tiffLE: Buffer.from([ + 0x49, 0x49, 0x2A, 0x00, // II + magic + 0x08, 0x00, 0x00, 0x00, // IFD offset = 8 + 0x02, 0x00, // 2 entries + // Entry 1: ImageWidth = 100 + 0x00, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, + // Entry 2: ImageHeight = 200 + 0x01, 0x01, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00, 0xC8, 0x00, 0x00, 0x00, + ]), + + // TIFF Big Endian 测试图片 + tiffBE: Buffer.from([ + 0x4D, 0x4D, 0x00, 0x2A, // MM + magic + 0x00, 0x00, 0x00, 0x08, // IFD offset = 8 + 0x00, 0x02, // 2 entries + // Entry 1: ImageWidth = 100 + 0x01, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0x64, 0x00, 0x00, + // Entry 2: ImageHeight = 200 + 0x01, 0x01, 0x00, 0x03, 0x00, 0x00, 0x00, 0x01, 0x00, 0xC8, 0x00, 0x00, + ]), + + invalid: Buffer.from('This is not an image file'), + empty: Buffer.alloc(0), +}; + +describe('napcat-image-size', () => { + describe('matchMagic', () => { + it('should match magic bytes at the beginning', () => { + const buffer = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]); + expect(matchMagic(buffer, [0x89, 0x50, 0x4E, 0x47])).toBe(true); + }); + + it('should match magic bytes at offset', () => { + const buffer = Buffer.from([0x00, 0x00, 0x89, 0x50, 0x4E, 0x47]); + expect(matchMagic(buffer, [0x89, 0x50, 0x4E, 0x47], 2)).toBe(true); + }); + + it('should return false for non-matching magic', () => { + const buffer = Buffer.from([0x00, 0x00, 0x00, 0x00]); + expect(matchMagic(buffer, [0x89, 0x50, 0x4E, 0x47])).toBe(false); + }); + + it('should return false for buffer too short', () => { + const buffer = Buffer.from([0x89, 0x50]); + expect(matchMagic(buffer, [0x89, 0x50, 0x4E, 0x47])).toBe(false); + }); + + it('should return false for offset beyond buffer', () => { + const buffer = Buffer.from([0x89, 0x50, 0x4E, 0x47]); + expect(matchMagic(buffer, [0x89, 0x50], 10)).toBe(false); + }); + }); + + describe('detectImageTypeFromBuffer', () => { + it('should detect PNG image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.png)).toBe(ImageType.PNG); + }); + + it('should detect JPEG image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.jpeg)).toBe(ImageType.JPEG); + }); + + it('should detect BMP image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.bmp)).toBe(ImageType.BMP); + }); + + it('should detect GIF87a image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.gif87a)).toBe(ImageType.GIF); + }); + + it('should detect GIF89a image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.gif89a)).toBe(ImageType.GIF); + }); + + it('should detect WebP VP8 image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.webpVP8)).toBe(ImageType.WEBP); + }); + + it('should detect WebP VP8L image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.webpVP8L)).toBe(ImageType.WEBP); + }); + + it('should detect WebP VP8X image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.webpVP8X)).toBe(ImageType.WEBP); + }); + + it('should detect TIFF Little Endian image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.tiffLE)).toBe(ImageType.TIFF); + }); + + it('should detect TIFF Big Endian image type', () => { + expect(detectImageTypeFromBuffer(testBuffers.tiffBE)).toBe(ImageType.TIFF); + }); + + it('should return UNKNOWN for invalid data', () => { + expect(detectImageTypeFromBuffer(testBuffers.invalid)).toBe(ImageType.UNKNOWN); + }); + + it('should return UNKNOWN for empty buffer', () => { + expect(detectImageTypeFromBuffer(testBuffers.empty)).toBe(ImageType.UNKNOWN); + }); + }); + + describe('imageSizeFromBuffer', () => { + it('should parse PNG image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.png)).toEqual({ width: 100, height: 200 }); + }); + + it('should parse JPEG image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.jpeg)).toEqual({ width: 320, height: 240 }); + }); + + it('should parse BMP image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.bmp)).toEqual({ width: 640, height: 480 }); + }); + + it('should parse GIF87a image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.gif87a)).toEqual({ width: 800, height: 600 }); + }); + + it('should parse GIF89a image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.gif89a)).toEqual({ width: 1024, height: 768 }); + }); + + it('should parse WebP VP8 image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.webpVP8)).toEqual({ width: 1920, height: 1080 }); + }); + + it('should parse WebP VP8L image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.webpVP8L)).toEqual({ width: 256, height: 128 }); + }); + + it('should parse WebP VP8X image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.webpVP8X)).toEqual({ width: 512, height: 384 }); + }); + + it('should parse TIFF Little Endian image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.tiffLE)).toEqual({ width: 100, height: 200 }); + }); + + it('should parse TIFF Big Endian image size correctly', async () => { + expect(await imageSizeFromBuffer(testBuffers.tiffBE)).toEqual({ width: 100, height: 200 }); + }); + + it('should return undefined for invalid data', async () => { + expect(await imageSizeFromBuffer(testBuffers.invalid)).toBeUndefined(); + }); + + it('should return undefined for empty buffer', async () => { + expect(await imageSizeFromBuffer(testBuffers.empty)).toBeUndefined(); + }); + }); + + describe('imageSizeFromBufferFallBack', () => { + it('should return actual size for valid image', async () => { + expect(await imageSizeFromBufferFallBack(testBuffers.png)).toEqual({ width: 100, height: 200 }); + }); + + it('should return default fallback for invalid data', async () => { + expect(await imageSizeFromBufferFallBack(testBuffers.invalid)).toEqual({ width: 1024, height: 1024 }); + }); + + it('should return custom fallback for invalid data', async () => { + expect(await imageSizeFromBufferFallBack(testBuffers.invalid, { width: 500, height: 300 })).toEqual({ width: 500, height: 300 }); + }); + + it('should return default fallback for empty buffer', async () => { + expect(await imageSizeFromBufferFallBack(testBuffers.empty)).toEqual({ width: 1024, height: 1024 }); + }); + + it('should return custom fallback for empty buffer', async () => { + expect(await imageSizeFromBufferFallBack(testBuffers.empty, { width: 800, height: 600 })).toEqual({ width: 800, height: 600 }); + }); + }); + + describe('ImageType enum', () => { + it('should have correct enum values', () => { + expect(ImageType.JPEG).toBe('jpeg'); + expect(ImageType.PNG).toBe('png'); + expect(ImageType.BMP).toBe('bmp'); + expect(ImageType.GIF).toBe('gif'); + expect(ImageType.WEBP).toBe('webp'); + expect(ImageType.TIFF).toBe('tiff'); + expect(ImageType.UNKNOWN).toBe('unknown'); + }); + }); + + describe('Real image files from resource directory', () => { + it('should detect and parse test-20x20.jpg', async () => { + const filePath = path.join(resourceDir, 'test-20x20.jpg'); + const buffer = fs.readFileSync(filePath); + expect(detectImageTypeFromBuffer(buffer)).toBe(ImageType.JPEG); + const size = await imageSizeFromBuffer(buffer); + expect(size).toEqual({ width: 20, height: 20 }); + }); + + it('should detect and parse test-20x20.png', async () => { + const filePath = path.join(resourceDir, 'test-20x20.png'); + const buffer = fs.readFileSync(filePath); + expect(detectImageTypeFromBuffer(buffer)).toBe(ImageType.PNG); + const size = await imageSizeFromBuffer(buffer); + expect(size).toEqual({ width: 20, height: 20 }); + }); + + it('should detect and parse test-20x20.tiff', async () => { + const filePath = path.join(resourceDir, 'test-20x20.tiff'); + const buffer = fs.readFileSync(filePath); + expect(detectImageTypeFromBuffer(buffer)).toBe(ImageType.TIFF); + const size = await imageSizeFromBuffer(buffer); + expect(size).toEqual({ width: 20, height: 20 }); + }); + + it('should detect and parse test-20x20.webp', async () => { + const filePath = path.join(resourceDir, 'test-20x20.webp'); + const buffer = fs.readFileSync(filePath); + expect(detectImageTypeFromBuffer(buffer)).toBe(ImageType.WEBP); + const size = await imageSizeFromBuffer(buffer); + expect(size).toEqual({ width: 20, height: 20 }); + }); + + it('should detect and parse test-490x498.gif', async () => { + const filePath = path.join(resourceDir, 'test-490x498.gif'); + const buffer = fs.readFileSync(filePath); + expect(detectImageTypeFromBuffer(buffer)).toBe(ImageType.GIF); + const size = await imageSizeFromBuffer(buffer); + expect(size).toEqual({ width: 490, height: 498 }); + }); + + it('should parse real images using imageSizeFromFile', async () => { + expect(await imageSizeFromFile(path.join(resourceDir, 'test-20x20.jpg'))).toEqual({ width: 20, height: 20 }); + expect(await imageSizeFromFile(path.join(resourceDir, 'test-20x20.png'))).toEqual({ width: 20, height: 20 }); + expect(await imageSizeFromFile(path.join(resourceDir, 'test-20x20.tiff'))).toEqual({ width: 20, height: 20 }); + expect(await imageSizeFromFile(path.join(resourceDir, 'test-20x20.webp'))).toEqual({ width: 20, height: 20 }); + expect(await imageSizeFromFile(path.join(resourceDir, 'test-490x498.gif'))).toEqual({ width: 490, height: 498 }); + }); + }); +}); diff --git a/packages/napcat-test/package.json b/packages/napcat-test/package.json index 6c51d86a..812c57bb 100644 --- a/packages/napcat-test/package.json +++ b/packages/napcat-test/package.json @@ -11,6 +11,7 @@ "vitest": "^4.0.9" }, "dependencies": { - "napcat-core": "workspace:*" + "napcat-core": "workspace:*", + "napcat-image-size": "workspace:*" } } \ No newline at end of file diff --git a/packages/napcat-test/vitest.config.ts b/packages/napcat-test/vitest.config.ts index 2ec5edf6..3451aa60 100644 --- a/packages/napcat-test/vitest.config.ts +++ b/packages/napcat-test/vitest.config.ts @@ -8,7 +8,10 @@ export default defineConfig({ }, resolve: { alias: { - '@': resolve(__dirname, '../../'), + '@/napcat-image-size': resolve(__dirname, '../napcat-image-size'), + '@/napcat-test': resolve(__dirname, '.'), + '@/napcat-common': resolve(__dirname, '../napcat-common'), + '@/napcat-core': resolve(__dirname, '../napcat-core'), }, }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 03c82e2d..a5dc338b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -354,6 +354,9 @@ importers: napcat-core: specifier: workspace:* version: link:../napcat-core + napcat-image-size: + specifier: workspace:* + version: link:../napcat-image-size devDependencies: vitest: specifier: ^4.0.9