mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-03 22:21:13 +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:
parent
2c3a304440
commit
f971c312b9
BIN
packages/napcat-image-size/resource/test-20x20.jpg
Normal file
BIN
packages/napcat-image-size/resource/test-20x20.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
BIN
packages/napcat-image-size/resource/test-20x20.png
Normal file
BIN
packages/napcat-image-size/resource/test-20x20.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
packages/napcat-image-size/resource/test-20x20.tiff
Normal file
BIN
packages/napcat-image-size/resource/test-20x20.tiff
Normal file
Binary file not shown.
BIN
packages/napcat-image-size/resource/test-20x20.webp
Normal file
BIN
packages/napcat-image-size/resource/test-20x20.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 962 B |
BIN
packages/napcat-image-size/resource/test-490x498.gif
Normal file
BIN
packages/napcat-image-size/resource/test-490x498.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@ -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;
|
||||
}
|
||||
|
||||
124
packages/napcat-image-size/src/parser/TiffParser.ts
Normal file
124
packages/napcat-image-size/src/parser/TiffParser.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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子格式
|
||||
|
||||
346
packages/napcat-test/imageSize.test.ts
Normal file
346
packages/napcat-test/imageSize.test.ts
Normal 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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -11,6 +11,7 @@
|
||||
"vitest": "^4.0.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"napcat-core": "workspace:*"
|
||||
"napcat-core": "workspace:*",
|
||||
"napcat-image-size": "workspace:*"
|
||||
}
|
||||
}
|
||||
@ -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'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user