# 图片预览与编辑架构文档

对应 Gitea issues #31–#35

---

## 1. 背景与目标

iOS 老项目 (`ImageMessageBubble.swift`, `ImageGridBubble.swift`, `ImageProcessor.swift`) 已实现图片收发、全屏查看和编辑。Flutter 新项目目前 `_MessageBubble` 对所有消息类型均渲染为纯文本，需补齐以下能力：

| 功能 | Issue | iOS 对应 |
|------|-------|---------|
| typ=2 图片气泡展示 | #31 | `ImageMessageBubble.swift` |
| 全屏查看 + 保存 + 分享 | #32 | `ImageFullscreenView.swift` |
| 相册/相机选图 + CDN 上传 | #33 | `ChatView.swift photosPicker` |
| 图片编辑（裁剪/旋转） | #34 | `ImageProcessor.swift` + `TOCropViewController` |
| 消息气泡 BubbleKind 路由 | #35 | `ChatMessageBubble.swift switch` |

---

## 2. 消息格式

### typ=2 单张图片

```json
{"url":"Image/xxx.jpg","width":1024,"height":768}
```

- `url`：CDN 相对路径，经 `CdnUrlResolver.resolve()` 转为完整 URL
- `width` / `height`：原始像素尺寸，用于计算显示比例

---

## 3. 架构层次

```
┌─────────────────────────────────────────────────────────────────┐
│  UI Layer                                                       │
│  ImagePickerSheet → (image_picker + image_cropper)             │
│  ImageMessageBubble → ImageViewerPage (photo_view)             │
│  _MessageBubble routing (typ switch → MediaBubble)            │
├─────────────────────────────────────────────────────────────────┤
│  UseCase Layer                                                  │
│  SendImageUseCase                                               │
│    ├─ readAsBytes() → dart:ui ImageDescriptor (width/height)   │
│    ├─ UploadFileRequest (POST /app/api/upload/file)            │
│    └─ SendMessageUseCase.execute(typ=2, content=JSON)          │
├─────────────────────────────────────────────────────────────────┤
│  Data Layer                                                     │
│  UploadFileRequest → UploadResult.url (CDN path)              │
│  CdnUrlResolver.resolve(path) → full URL                       │
└─────────────────────────────────────────────────────────────────┘
```

---

## 4. 关键设计决策

### 4.1 图片尺寸计算（iOS 对齐）

| 参数 | 值 |
|------|---|
| 最长边上限 | 220 pt |
| 最短边下限 | 80 pt |
| 宽高比 | 保持原比例 |
| 圆角 | 12 pt |

```dart
// 伪码
final longest = max(w, h);
if (longest > 220) { w *= 220/longest; h *= 220/longest; }
final shortest = min(w, h);
if (shortest < 80) { w *= 80/shortest; h *= 80/shortest; }
```

### 4.2 图片压缩策略（iOS ImageProcessor 对齐）

| 参数 | 值 |
|------|---|
| 最大边长 | 1920 px |
| JPEG 质量 | 85 |
| 方式 | `image_picker` 内置 `maxWidth/maxHeight/imageQuality` |

不引入 `flutter_image_compress`——`image_picker` 的内置参数已满足 iOS 等价逻辑。

### 4.3 尺寸解析

上传前使用 `dart:ui.ImageDescriptor.encoded(ImmutableBuffer)` 解析压缩后尺寸，零外部依赖。

### 4.4 CDN 上传（单步 FormData）

沿用现有 `UploadFileRequest`（FormData → `/app/api/upload/file`），返回 `UploadResult.url`（CDN 相对路径）。

> 注意：iOS 使用三步 presign→S3 PUT→finish，Flutter 项目因后端已提供单步上传端点，使用更简单的 FormData 模式。

### 4.5 全屏查看器（iOS ImageFullscreenView 对齐）

| iOS | Flutter |
|-----|---------|
| `UIScrollView` pinch-to-zoom | `photo_view` PhotoView |
| 双击 2.5x | `PhotoView.enableDoubleTapZoom` |
| 底部保存/分享 | `image_gallery_saver_plus` + `share_plus` |
| 多图滑动 | `PhotoViewGallery` |

### 4.6 图片编辑（iOS TOCropViewController 对齐）

`image_cropper` 在 iOS 上也调用 `TOCropViewController`（通过 Plugin Bridge），UI 效果一致。
Flutter 侧无需自建裁剪页，直接调用：
```dart
await ImageCropper().cropImage(sourcePath: ..., compressQuality: 85, ...)
```

---

## 5. 数据流：发送图片

```
用户点击附件图标
  └─ ImagePickerSheet.show()
       ├─ 拍照: ImagePicker.pickImage(camera, maxW=1920, q=85)
       └─ 相册: ImagePicker.pickMultiImage(maxW=1920, q=85, limit=9)
            ├─ (可选) _PreviewTile 点击 → ImageCropper.cropImage()
            └─ "发送" → SendImageUseCase.execute(imageFile, chatId)
                  ├─ readAsBytes() → Uint8List
                  ├─ dart:ui.ImageDescriptor → (width, height)
                  ├─ 写临时文件 → UploadFileRequest → UploadResult.url
                  ├─ jsonEncode({"url":url,"width":w,"height":h})
                  └─ SendMessageUseCase(chatId, content, typ=2)
                        ├─ 乐观写入 MessageRepository (DB Stream → UI)
                        └─ HTTP POST /app/api/chat/send-message
```

## 6. 数据流：接收/查看图片

```
WS / HTTP 历史 → DB → MessageRepository Stream
  └─ messagesByChatIdProvider(chatId)
       └─ ListView → _MessageBubble(message)
            └─ message.typ == 2
                 └─ ImageMessageBubble(rawContent)
                      ├─ parse JSON → url, width, height
                      ├─ CdnUrlResolver.resolve(url) → fullUrl
                      ├─ Image.network(fullUrl, fit: cover)
                      └─ GestureTap → ImageViewerPage(urls: [fullUrl])
                           └─ PhotoView pinch-to-zoom
                                ├─ 保存 → ImageGallerySaverPlus.saveNetworkImage(url)
                                └─ 分享 → Share.share(url)
```

---

## 7. 新增文件清单

| 文件 | 说明 |
|------|------|
| `lib/features/chat/view/widgets/image_message_bubble.dart` | typ=2 气泡（#31） |
| `lib/features/chat/view/image_viewer_page.dart` | 全屏查看（#32） |
| `lib/features/chat/view/widgets/image_picker_sheet.dart` | 选图底部弹窗（#33） |
| `lib/features/chat/usecases/send_image_usecase.dart` | 上传+发送（#33） |

## 8. 修改文件清单

| 文件 | 修改内容 |
|------|---------|
| `pubspec.yaml` | 新增 photo_view / image_picker / image_cropper / image_gallery_saver_plus / share_plus |
| `lib/features/chat/di/chat_service_providers.dart` | 新增 sendImageUseCaseProvider |
| `lib/features/chat/view/chat_detail_page.dart` | 输入栏附件按钮 + _MessageBubble typ 路由（#35） |

---

## 9. 待完成

- 多图网格气泡 `ImageGridBubble`（微信 2-9 图 layout，待 typ 确认）
- 图片 sticker (typ=5) 展示
- GIF 消息 (typ=25) 支持
- 端到端加密图片（`MediaCrypto` decrypt，待 cipher_guard_sdk 接入）
