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-* 命名規範