file-converter-development

📁 rytass/utils 📅 Feb 5, 2026
4
总安装量
4
周安装量
#50887
全站排名
安装命令
npx skills add https://github.com/rytass/utils --skill file-converter-development

Agent 安装分布

amp 4
github-copilot 4
replit 4
codex 4
kimi-cli 4
gemini-cli 4

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