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:
手瓜一十雪 2026-02-01 17:42:58 +08:00
parent 2c3a304440
commit f971c312b9
12 changed files with 597 additions and 25 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 962 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -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;
}

View File

@ -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<ImageSize | undefined> {
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;
}
}

View File

@ -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子格式

View File

@ -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 });
});
});
});

View File

@ -11,6 +11,7 @@
"vitest": "^4.0.9"
},
"dependencies": {
"napcat-core": "workspace:*"
"napcat-core": "workspace:*",
"napcat-image-size": "workspace:*"
}
}

View File

@ -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'),
},
},
});

View File

@ -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