file-converter-development
npx skills add https://github.com/rytass/utils --skill file-converter-development
Agent 安装分布
Skill 文档
File Converter Adapter Development Guide (æªæ¡è½æ Adapter éç¼æå)
Overview
æ¬æå說æå¦ä½åºæ¼ @rytass/file-converter åºç¤å¥ä»¶éç¼æ°çæªæ¡è½æé©é
å¨ã
Base Package Architecture
@rytass/file-converter (Base)
âââ ConvertableFile # 輸å
¥é¡å (Readable | Buffer)
âââ FileConverter<O> # è½æå¨ä»é¢
âââ ConverterManager # 管éå¼è½æç®¡çå¨
Core Interfaces
FileConverter Interface
import { Readable } from 'stream';
type ConvertableFile = Readable | Buffer;
interface FileConverter<O = Record<string, unknown>> {
convert<Buffer>(file: ConvertableFile): Promise<Buffer>;
convert<Readable>(file: ConvertableFile): Promise<Readable>;
}
ConverterManager
class ConverterManager {
constructor(converters: FileConverter[]);
convert<ConvertableFileFormat extends ConvertableFile>(
file: ConvertableFile
): Promise<ConvertableFileFormat>;
}
注æ: ConverterManager 䏿ä¾
pipe()æ¹æ³ãææè½æå¨å¿ é å¨å»ºæ§æééé£åå³å ¥ã
Existing Adapters Reference
| Adapter | åè½ | è¼¸å ¥ | è¼¸åº |
|---|---|---|---|
image-resizer |
åçç¸®æ¾ | Buffer / Readable | Buffer / Readable |
image-transcoder |
æ ¼å¼è½æ | Buffer / Readable | Buffer / Readable |
image-watermark |
浮水å°çå | Buffer / Readable | Buffer / Readable |
Implementing a New Adapter
Step 1: Define Configuration
// my-converter/src/typings.ts
export interface MyConverterOptions {
someOption?: string;
concurrency?: number;
// ... other options
}
Step 2: Implement FileConverter
// my-converter/src/my-converter.ts
import { FileConverter, ConvertableFile } from '@rytass/file-converter';
import sharp from 'sharp';
import { Readable } from 'stream';
import { MyConverterOptions } from './typings';
sharp.cache(false);
export class MyConverter implements FileConverter<MyConverterOptions> {
private readonly options: MyConverterOptions;
constructor(options: MyConverterOptions) {
this.options = options;
// è¨å® Sharp 並è¡èçæ¸é
sharp.concurrency(options.concurrency ?? 1);
}
async convert<Output extends ConvertableFile>(file: ConvertableFile): Promise<Output> {
let converter;
if (file instanceof Buffer) {
converter = sharp(file);
} else {
converter = sharp();
}
// å¥ç¨è½æè¨å®
// converter.resize(...) ç
// Stream 輸å
¥éè¦ pipe
if (file instanceof Readable) {
file.pipe(converter);
}
// æ ¹æè¼¸å
¥é¡ååå³å°æè¼¸åº
if (file instanceof Buffer) {
return converter.toBuffer() as Promise<Output>;
}
return converter as Readable as Output;
}
}
Step 3: Export Package
// my-converter/src/index.ts
export * from './typings';
export * from './my-converter';
Actual Adapter Implementations
ImageResizer (image-resizer)
實éé¸é ä»é¢ï¼
export interface ImageResizerOptions {
maxWidth?: number; // æå¤§å¯¬åº¦ï¼å¿
é æä¾ maxWidth æ maxHeight è³å°ä¸åï¼
maxHeight?: number; // æå¤§é«åº¦ï¼å¿
é æä¾ maxWidth æ maxHeight è³å°ä¸åï¼
keepAspectRatio?: boolean; // ä¿ææ¯ä¾ï¼é è¨ trueï¼ä½¿ç¨ fit: 'inside'ï¼
concurrency?: number; // Sharp ä¸¦è¡æ¸ï¼é è¨ 1ï¼
}
使ç¨ç¯ä¾ï¼
import { ImageResizer } from '@rytass/file-converter-adapter-image-resizer';
// å¿
é æä¾è³å°ä¸å maxWidth æ maxHeight
const resizer = new ImageResizer({
maxWidth: 800,
maxHeight: 600,
keepAspectRatio: true, // é è¨ true
concurrency: 1,
});
// è½æ - æ¯æ´ Buffer æ Readable 輸å
¥
const result = await resizer.convert<Buffer>(inputBuffer);
// æ
const resultStream = await resizer.convert<Readable>(inputStream);
注æ: ä½¿ç¨ withoutEnlargement: trueï¼ä¸ææ¾å¤§å°æ¼ç®æ¨å°ºå¯¸çåçã
ImageTranscoder (image-transcoder)
實éé¸é ä»é¢ï¼ä½¿ç¨ Sharp çæ ¼å¼ç¹å®é¸é ï¼ï¼
import type { AvifOptions, GifOptions, HeifOptions, JpegOptions, PngOptions, TiffOptions, WebpOptions } from 'sharp';
// æ ¹æç®æ¨æ ¼å¼ä½¿ç¨å°æçé¸é
é¡å
type ImageTranscoderOptions =
| ({ targetFormat: 'avif' } & AvifOptions)
| ({ targetFormat: 'heif' } & HeifOptions)
| ({ targetFormat: 'gif' } & GifOptions)
| ({ targetFormat: 'tif' | 'tiff' } & TiffOptions)
| ({ targetFormat: 'png' } & PngOptions)
| ({ targetFormat: 'webp' } & WebpOptions)
| ({ targetFormat: 'jpg' | 'jpeg' } & JpegOptions);
// constructor 坦鿥åçé¡åï¼é¡å¤å
å« concurrencyï¼
type ImageTranscoderConstructorOptions = ImageTranscoderOptions & { concurrency?: number };
æ¯æ´ç便ºæ ¼å¼ï¼['jpg', 'png', 'webp', 'gif', 'avif', 'tif', 'svg']
使ç¨ç¯ä¾ï¼
import { ImageTranscoder } from '@rytass/file-converter-adapter-image-transcoder';
// è½æçº WebP
const transcoder = new ImageTranscoder({
targetFormat: 'webp',
quality: 80, // WebpOptions çé¸é
lossless: false, // WebpOptions çé¸é
concurrency: 1, // Sharp ä¸¦è¡æ¸ï¼é è¨ 1ï¼
});
// è½æçº JPEG
const jpegTranscoder = new ImageTranscoder({
targetFormat: 'jpeg',
quality: 85,
progressive: true, // JpegOptions çé¸é
});
// è½æçº AVIF
const avifTranscoder = new ImageTranscoder({
targetFormat: 'avif',
quality: 50,
effort: 4, // AvifOptions çé¸é
});
const result = await transcoder.convert<Buffer>(inputBuffer);
注æ: 䏿¯æ´ç便ºæ ¼å¼ææåº UnsupportedSource é¯èª¤ã
ImageWatermark (image-watermark)
實éé¸é ä»é¢ï¼
import type { Gravity } from 'sharp';
type FilePath = string;
interface Watermark {
image: FilePath | Buffer; // 浮水å°åçï¼æªæ¡è·¯å¾æ Bufferï¼
gravity?: Gravity; // Sharp gravityï¼é è¨ southeast å³ä¸è§ï¼
}
export interface ImageWatermarkOptions {
watermarks: Watermark[]; // 浮水å°é£åï¼æ¯æ´å¤å浮水å°ï¼
concurrency?: number; // Sharp ä¸¦è¡æ¸ï¼é è¨ 1ï¼
}
Sharp Gravity å¼ï¼
import { gravity } from 'sharp';
// å¯ç¨ç gravity å¼ï¼
// gravity.north, gravity.northeast, gravity.east, gravity.southeast,
// gravity.south, gravity.southwest, gravity.west, gravity.northwest,
// gravity.center (æ gravity.centre)
使ç¨ç¯ä¾ï¼
import { ImageWatermark } from '@rytass/file-converter-adapter-image-watermark';
import { gravity } from 'sharp';
// å®ä¸æµ®æ°´å°
const watermark = new ImageWatermark({
watermarks: [
{
image: watermarkBuffer, // æ '/path/to/watermark.png'
gravity: gravity.southeast, // å³ä¸è§ï¼é è¨ï¼
},
],
});
// å¤å浮水å°
const multiWatermark = new ImageWatermark({
watermarks: [
{ image: logoBuffer, gravity: gravity.northwest }, // å·¦ä¸è§
{ image: copyrightBuffer, gravity: gravity.south }, // 䏿¹ç½®ä¸
],
concurrency: 2,
});
const result = await watermark.convert<Buffer>(inputBuffer);
Pipeline Usage
Using ConverterManager
ConverterManager å 許串æ¥å¤åè½æå¨ï¼æé åºå·è¡è½æã
import { ConverterManager } from '@rytass/file-converter';
import { ImageResizer } from '@rytass/file-converter-adapter-image-resizer';
import { ImageWatermark } from '@rytass/file-converter-adapter-image-watermark';
import { ImageTranscoder } from '@rytass/file-converter-adapter-image-transcoder';
import { gravity } from 'sharp';
// 建ç«è½æç®¡ç·ï¼ç¸®æ¾ â æµ®æ°´å° â æ ¼å¼è½æ
const manager = new ConverterManager([
new ImageResizer({
maxWidth: 800,
maxHeight: 600,
keepAspectRatio: true,
}),
new ImageWatermark({
watermarks: [
{ image: watermarkBuffer, gravity: gravity.southeast },
],
}),
new ImageTranscoder({
targetFormat: 'webp',
quality: 85,
}),
]);
// å·è¡è½æ (æ¯æ´ Buffer æ Readable 輸å
¥)
const result = await manager.convert<Buffer>(inputBuffer);
注æ: ææè½æå¨å¿ é å¨å»ºæ§ ConverterManager æééé£åå³å ¥ï¼ä¸æ¯æ´åæ æ·»å è½æå¨ã
Error Handling
UnsupportedSource Error
ImageTranscoder æå¨ä¸æ¯æ´ç便ºæ ¼å¼ææåºæ¤é¯èª¤ï¼
import { ImageTranscoder } from '@rytass/file-converter-adapter-image-transcoder';
try {
const transcoder = new ImageTranscoder({ targetFormat: 'webp' });
await transcoder.convert(unsupportedFormatBuffer);
} catch (error) {
// UnsupportedSource é¡å¥æªå¾å¥ä»¶å°åºï¼ééé message 夿·
if (error instanceof Error && error.message === 'UnsupportedSource') {
console.error('䏿¯æ´çåçæ ¼å¼');
}
}
注æ:
UnsupportedSourceé¯èª¤é¡å¥åSupportSources常æ¸ç®åæªå¾å¥ä»¶å°åºï¼å ä¾å §é¨ä½¿ç¨ã
Testing Guidelines
// __tests__/my-converter.spec.ts
import { MyConverter } from '../src';
import * as fs from 'fs';
import * as path from 'path';
describe('MyConverter', () => {
const testImage = fs.readFileSync(path.join(__dirname, 'fixtures/test.jpg'));
it('should convert image', async () => {
const converter = new MyConverter({ someOption: 'value' });
const result = await converter.convert<Buffer>(testImage);
expect(result).toBeInstanceOf(Buffer);
expect(result.length).toBeGreaterThan(0);
});
});
Package Structure
my-converter/
âââ src/
â âââ index.ts
â âââ typings.ts
â âââ my-converter.ts
âââ __tests__/
â âââ fixtures/
â â âââ test.jpg
â âââ my-converter.spec.ts
âââ package.json
âââ tsconfig.build.json
Publishing Checklist
- 實ç¾
FileConverter<O>ä»é¢ - å®ç¾©æ¸ æ¥çé¸é ä»é¢ (Options type)
- æ¯æ´
ConvertableFile(Buffer æ Readable) è¼¸å ¥ - æ¯æ´ Buffer å Readable 輸åº
- è¨å®
sharp.cache(false)é¿å è¨æ¶é«åé¡ - æ¯æ´
concurrencyé¸é æ§å¶ Sharp ä¸¦è¡æ¸ - è ConverterManager ç¸å®¹
- æ°å¯«å®å 測試ï¼å«æ¸¬è©¦åçï¼
- æ´æ° README å«ä½¿ç¨ç¯ä¾
- éµå¾ª
@rytass/file-converter-adapter-*å½åè¦ç¯