# 機能設計書：28-画像リサイズ

## 1. 概要

### 1.1 機能の目的

本機能は、PayloadCMSにおける画像のリサイズ・変換処理を提供する。アップロードされた画像に対して、設定された複数のサイズバリエーションを自動生成し、フォーカルポイントに基づいたインテリジェントなクロップ、フォーマット変換、トリム処理を行う。

### 1.2 機能の範囲

- 複数の画像サイズ自動生成
- フォーカルポイントに基づくスマートクロップ
- EXIF情報に基づく自動回転
- 画像フォーマット変換（formatOptions）
- 画像トリム処理（trimOptions）
- アニメーション画像（GIF/WebP/AVIF）対応
- メタデータの選択的付与

### 1.3 関連する画面・機能

| 画面/機能ID | 画面/機能名 | 関連度 |
|------------|------------|-------|
| file-upload | ファイルアップロード | 高 |
| admin-ui | Admin UI | 高 |
| collection-create | コレクション作成 | 中 |

---

## 2. 機能詳細

### 2.1 機能フロー

```
+-------------------+
| オリジナル画像     |
| (Buffer/TempFile) |
+--------+----------+
         |
         v
+--------+----------+
| Sharp インスタンス |
| .rotate() EXIF回転|
+--------+----------+
         |
         v
+--------+----------+
| imageSizes 並列処理|
| Promise.all()     |
+--------+----------+
         |
    +----+----+----+----+
    |    |    |    |    |
    v    v    v    v    v
+--+ +--+ +--+ +--+ +--+
|S1| |S2| |S3| |S4| |S5|
+--+ +--+ +--+ +--+ +--+
    |    |    |    |    |
    +----+----+----+----+
         |
         v
+--------+----------+
| sizeData (FileSizes)|
| sizesToSave (Files)|
+-------------------+
```

### 2.2 入力仕様

#### createImageSizes パラメータ

| パラメータ | 型 | 必須 | 説明 |
|-----------|---|------|------|
| config | SanitizedCollectionConfig | はい | コレクション設定 |
| dimensions | ProbedImageSize | はい | オリジナル画像の寸法 |
| file | PayloadRequest['file'] | はい | アップロードファイル |
| focalPoint | FocalPoint | いいえ | フォーカルポイント座標 |
| mimeType | string | はい | MIMEタイプ |
| req | PayloadRequest | はい | リクエストオブジェクト |
| savedFilename | string | はい | 保存ファイル名 |
| sharp | SharpDependency | いいえ | Sharpインスタンス |
| staticPath | string | はい | 静的ファイルパス |
| withMetadata | WithMetadata | いいえ | メタデータ付与設定 |

#### ImageSize 設定

| パラメータ | 型 | 必須 | デフォルト値 | 説明 |
|-----------|---|------|-------------|------|
| name | string | はい | - | サイズ名 |
| width | number | いいえ | undefined | 目標幅 |
| height | number | いいえ | undefined | 目標高さ |
| fit | string | いいえ | 'cover' | リサイズフィット方式 |
| position | string | いいえ | 'center' | 配置位置 |
| withoutEnlargement | boolean | いいえ | undefined | 拡大禁止 |
| withoutReduction | boolean | いいえ | false | 縮小禁止 |
| formatOptions | ImageUploadFormatOptions | いいえ | undefined | フォーマット変換設定 |
| trimOptions | ImageUploadTrimOptions | いいえ | undefined | トリム設定 |
| generateImageName | GenerateImageName | いいえ | undefined | カスタムファイル名生成 |
| admin | object | いいえ | undefined | Admin UI設定 |

### 2.3 出力仕様

#### ImageSizesResult

| フィールド | 型 | 説明 |
|-----------|---|------|
| sizeData | FileSizes | 各サイズのメタデータ |
| sizesToSave | FileToSave[] | 保存するファイル配列 |

#### FileSize（各サイズのメタデータ）

| フィールド | 型 | 説明 |
|-----------|---|------|
| filename | string \| null | ファイル名 |
| filesize | number \| null | ファイルサイズ（バイト） |
| width | number \| null | 幅（ピクセル） |
| height | number \| null | 高さ（ピクセル） |
| mimeType | string \| null | MIMEタイプ |
| url | string \| null | URL |

### 2.4 処理詳細

#### 2.4.1 createImageSizes メイン処理

```
処理フロー:
1. imageSizes未設定 or Sharp未提供の場合、空結果を返却
2. アニメーション画像判定（gif/webp/avif）
3. Sharpベースインスタンス生成
   - tempFilePathまたはdataから生成
   - rotate()でEXIF自動回転
4. オリジナルメタデータ取得
5. EXIF orientation 5-8の場合、寸法を入れ替え
6. imageSizesを並列処理（Promise.all）
   - sanitizeResizeConfig適用
   - getImageResizeActionで処理方式決定
   - 方式に応じてリサイズ実行
   - formatOptions/trimOptions適用
   - optionallyAppendMetadata実行
   - バッファ生成・ファイル名生成
7. sizeData/sizesToSave返却
```

#### 2.4.2 getImageResizeAction（リサイズアクション決定）

```
戻り値: 'omit' | 'resize' | 'resizeWithFocalPoint'

判定ロジック:
1. withoutEnlargement=undefined && 元画像 < 目標サイズ
   → 'omit'（サイズ生成しない）

2. fit='contain' or 'inside'
   → 'resize'（通常リサイズ）

3. 目標幅/高さが未設定
   → 'resize'

4. アスペクト比が同じ
   → 'resize'

5. withoutEnlargement=true && 元画像が小さい
   → 'resize'

6. withoutReduction=true && 元画像が大きい
   → 'resize'

7. フォーカルポイントあり
   → 'resizeWithFocalPoint'

8. それ以外
   → 'resize'
```

#### 2.4.3 resizeWithFocalPoint（フォーカルポイントリサイズ）

```
処理フロー:
1. 目標幅/高さの計算
   - 片方のみ指定時、アスペクト比から算出
2. リサイズアスペクト比 vs オリジナルアスペクト比の比較
   - prioritizeHeight = リサイズAR < オリジナルAR
3. 第一段階リサイズ
   - fastShrinkOnLoad: false
   - 優先軸のみ指定してリサイズ
4. リサイズ後メタデータ取得（バッファから）
5. フォーカルポイントに基づく抽出位置計算
   - xFocalCenter = 幅 * (focalPoint.x / 100)
   - yFocalCenter = 高さ * (focalPoint.y / 100)
   - 境界調整（画像端の処理）
6. extract()でクロップ実行
```

#### 2.4.4 sanitizeResizeConfig

```
処理ルール:
- withoutReduction=true の場合:
  - fit: resizeConfig.fit || 'contain'
  - position: resizeConfig.position || 'left top'
```

#### 2.4.5 extractHeightFromImage（アニメーション画像対応）

```
処理ルール:
- sharpMetadata.pages存在時（アニメーション）:
  → height / pages（1フレームの高さ）
- それ以外:
  → height（通常の高さ）
```

#### 2.4.6 generateImageSizeFilename

```
ファイル名形式:
{outputImageName}-{width}x{height}.{extension}

例: photo-800x600.jpg
```

---

## 3. ビジネスルール

### 3.1 リサイズ判定ルール

| ルールID | ルール名 | 条件 | 処理 |
|---------|---------|------|------|
| BR-001 | デフォルト拡大禁止 | withoutEnlargement未定義 && 元画像<目標 | サイズ生成をスキップ |
| BR-002 | 拡大許可 | withoutEnlargement=false | 常に目標サイズに拡大 |
| BR-003 | 拡大禁止 | withoutEnlargement=true | 小さい画像はオリジナル保持 |
| BR-004 | 縮小禁止 | withoutReduction=true | fit='contain', position='left top'適用 |

### 3.2 アスペクト比処理ルール

| ルールID | ルール名 | 条件 | 処理 |
|---------|---------|------|------|
| BR-005 | 同一アスペクト比 | 元画像AR == 目標AR | 単純リサイズ |
| BR-006 | 異なるアスペクト比 | 元画像AR != 目標AR && フォーカルポイントあり | フォーカルポイントクロップ |
| BR-007 | contain/inside | fit設定時 | 単純リサイズ（クロップなし） |

### 3.3 アニメーション画像ルール

| ルールID | ルール名 | 条件 | 処理 |
|---------|---------|------|------|
| BR-008 | アニメーション検出 | gif/webp/avif | animated=true設定 |
| BR-009 | 高さ補正 | pages存在時 | height = 全体高さ / ページ数 |

### 3.4 EXIF回転ルール

| ルールID | ルール名 | 条件 | 処理 |
|---------|---------|------|------|
| BR-010 | 自動回転 | 全画像 | rotate()で自動補正 |
| BR-011 | 寸法入替 | orientation 5-8 | width/heightを入れ替え |

---

## 4. データモデル

### 4.1 ImageSize型定義

```typescript
type ImageSize = {
  name: string
  width?: number
  height?: number
  fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'
  position?: string
  withoutEnlargement?: boolean
  withoutReduction?: boolean
  formatOptions?: {
    format: string
    options?: object
  }
  trimOptions?: TrimOptions
  generateImageName?: (args: {
    extension: string
    height: number
    originalName: string
    sizeName: string
    width: number
  }) => string
  admin?: {
    disableGroupBy?: boolean
    disableListColumn?: boolean
    disableListFilter?: boolean
  }
}
```

### 4.2 FocalPoint型定義

```typescript
type FocalPoint = {
  x: number  // 0-100 (パーセンテージ)
  y: number  // 0-100 (パーセンテージ)
}
```

---

## 5. エラー処理

### 5.1 エラー一覧

| エラーコード | エラー名 | 発生条件 | 処理 |
|-------------|---------|---------|------|
| E-001 | SharpError | Sharp処理失敗 | 上位でキャッチ、FileUploadError |
| E-002 | FileExistsError | ファイル削除失敗 | Ignore（無視） |

### 5.2 エラー処理フロー

```
既存ファイル削除時:
try {
  await fs.unlink(imagePath)
} catch {
  // Ignore unlink errors
}
```

---

## 6. セキュリティ考慮事項

### 6.1 入力検証

| 項目 | 実装 | 備考 |
|-----|------|------|
| ファイル名サニタイズ | sanitize-filename | 特殊文字除去 |
| パス生成 | staticPath + filename | ディレクトリトラバーサル防止 |

---

## 7. パフォーマンス考慮事項

### 7.1 最適化ポイント

| 項目 | 実装 | 効果 |
|-----|------|------|
| 並列処理 | Promise.all | 複数サイズの同時生成 |
| クローン使用 | sharpBase.clone() | メモリ効率化 |
| fastShrinkOnLoad | false | フォーカルポイント精度確保 |
| 条件付きスキップ | 'omit'判定 | 不要なリサイズ回避 |

### 7.2 メモリ管理

| 項目 | 実装 |
|-----|------|
| バッファ解決 | toBuffer({ resolveWithObject: true }) |
| 一時ファイル対応 | tempFilePath優先 |
| メタデータキャッシュ | req.payloadUploadSizes |

---

## 8. コードリーディングガイド

### 8.1 読み解く順序

1. **型定義の理解** (`types.ts`)
   - ImageSize, FileSize, FocalPoint型
   - ProbedImageSize型

2. **エントリーポイント** (`createImageSizes.ts`)
   - createImageSizes関数
   - 並列処理ロジック

3. **リサイズアクション決定** (`getImageResizeAction.ts`)
   - withoutEnlargement/withoutReductionの判定
   - フォーカルポイント判定

4. **設定サニタイズ** (`sanitizeResizeConfig.ts`)
   - withoutReduction時のfit/position設定

5. **ファイル名生成** (`generateImageSizeFilename.ts`, `parseFilename.ts`)
   - ファイル名形式の生成
   - 拡張子の抽出

6. **ユーティリティ** (`extractHeightFromImage.ts`, `optionallyAppendMetadata.ts`)
   - アニメーション画像の高さ計算
   - メタデータ付与ロジック

### 8.2 プログラム呼び出し階層図

```
createImageSizes
├── sharp() - Sharpインスタンス生成
│   └── .rotate() - EXIF自動回転
├── .metadata() - メタデータ取得
└── Promise.all(imageSizes.map())
    ├── sanitizeResizeConfig
    ├── getImageResizeAction
    │   └── isNumber
    ├── (resizeWithFocalPoint処理)
    │   ├── .resize({ fastShrinkOnLoad: false })
    │   ├── optionallyAppendMetadata
    │   ├── .toBuffer()
    │   ├── extractHeightFromImage
    │   └── .extract()
    ├── (通常resize処理)
    │   └── .resize(imageResizeConfig)
    ├── .toFormat() - formatOptions適用
    ├── .trim() - trimOptions適用
    ├── optionallyAppendMetadata
    ├── .toBuffer()
    ├── parseFilename
    ├── fileTypeFromBuffer
    ├── generateImageSizeFilename
    ├── fileExists
    ├── fs.unlink
    └── createImageSize
```

### 8.3 データフロー図

```
Original Image (Buffer/TempFile)
        |
        v
+-------+--------+
| Sharp Instance |
| .rotate()      |
+-------+--------+
        |
        v
+-------+--------+
| .metadata()    |
| (EXIF取得)     |
+-------+--------+
        |
        v
+-------+--------+
| Dimension Adj. |
| (orientation)  |
+-------+--------+
        |
        +----------------------+
        |                      |
        v                      v
+-------+--------+    +-------+--------+
| resize Action  |    | resize Action  |
| (size 1)       |    | (size 2)       |
+-------+--------+    +-------+--------+
        |                      |
        v                      v
+-------+--------+    +-------+--------+
| .resize()      |    | .resize()      |
| or .extract()  |    | or .extract()  |
+-------+--------+    +-------+--------+
        |                      |
        v                      v
+-------+--------+    +-------+--------+
| formatOptions  |    | formatOptions  |
| trimOptions    |    | trimOptions    |
+-------+--------+    +-------+--------+
        |                      |
        v                      v
+-------+--------+    +-------+--------+
| .toBuffer()    |    | .toBuffer()    |
+-------+--------+    +-------+--------+
        |                      |
        +----------+-----------+
                   |
                   v
          +--------+--------+
          | sizeData        |
          | sizesToSave     |
          +-----------------+
```

### 8.4 関連ファイル一覧

| ファイルパス | 責務 | 行数目安 |
|------------|------|---------|
| packages/payload/src/uploads/image-resizing/createImageSizes.ts | メインリサイズ処理 | 1-300 |
| packages/payload/src/uploads/image-resizing/createImageSize.ts | FileSizeオブジェクト生成 | 1-41 |
| packages/payload/src/uploads/image-resizing/getImageResizeAction.ts | リサイズアクション決定 | 1-77 |
| packages/payload/src/uploads/image-resizing/sanitizeResizeConfig.ts | リサイズ設定サニタイズ | 1-22 |
| packages/payload/src/uploads/image-resizing/generateImageSizeFilename.ts | ファイル名生成 | 1-28 |
| packages/payload/src/uploads/image-resizing/parseFilename.ts | ファイル名解析 | 1-19 |
| packages/payload/src/uploads/image-resizing/extractHeightFromImage.ts | 高さ抽出（アニメーション対応） | 1-15 |
| packages/payload/src/uploads/optionallyAppendMetadata.ts | メタデータ付与 | 1-32 |
| packages/payload/src/uploads/types.ts | 型定義（ImageSize等） | 72-121 |
| packages/payload/src/uploads/canResizeImage.ts | リサイズ可否判定 | 1-8 |

---

## 9. 付録

### 9.1 リサイズ可能MIMEタイプ

- image/jpeg
- image/png
- image/gif
- image/webp
- image/tiff
- image/avif

### 9.2 アニメーション対応MIMEタイプ

- image/gif
- image/webp
- image/avif

### 9.3 fit オプション値

| 値 | 説明 |
|---|------|
| cover | アスペクト比を維持しつつ目標サイズを満たす（デフォルト） |
| contain | アスペクト比を維持しつつ目標サイズ内に収める |
| fill | 目標サイズに引き伸ばす |
| inside | containと同様、ただし拡大なし |
| outside | coverと同様、ただし縮小なし |

### 9.4 EXIF Orientation値

| 値 | 回転 | 反転 |
|---|------|------|
| 1 | なし | なし |
| 2 | なし | 水平 |
| 3 | 180度 | なし |
| 4 | 180度 | 水平 |
| 5 | 90度時計回り | 水平 |
| 6 | 90度時計回り | なし |
| 7 | 90度反時計回り | 水平 |
| 8 | 90度反時計回り | なし |

※ orientation 5-8では画像の幅と高さが入れ替わる
