file-converter-development
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-*命名規範
More from rytass/utils
wms-module
|
24logistics-development
|
13logistics-adapters
|
12invoice-adapters
Taiwan e-invoice integration (台灣電子發票整合). Use when working with ECPay (綠界), EZPay (藍新), BankPro (金財通), or Amego (光貿) invoice services. Covers issuing invoices (開立發票), voiding (作廢), allowances (折讓), and querying invoice data.
8payment-adapters
Taiwan payment integration (台灣支付整合). Use when working with ECPay (綠界), NewebPay (藍新), HwaNan Bank (華南銀行), CTBC (中信), iCash Pay, or Happy Card payment services. Covers credit card (信用卡), virtual account (虛擬帳號), ATM, CVS payment (超商代碼/條碼), card binding (卡片綁定), installments (分期付款), recurring payments (訂閱付款), and NestJS integration.
7member-module
|
7