- 支持四种图片适配模式:stretch、contain、cover、center - 支持背景色填充功能(contain 和 center 模式) - 支持文档级 DPI 配置(metadata.dpi) - PPTX 渲染器集成 Pillow 实现高质量图片处理 - HTML 渲染器使用 CSS object-fit 实现相同效果 - 添加完整的单元测试、集成测试和端到端测试 - 更新 README 文档和架构文档 - 模块化设计:utils/image_utils.py 图片处理工具模块 - 添加图片配置验证器:validators/image_config.py - 向后兼容:未指定 fit 时默认使用 stretch 模式
261 lines
9.8 KiB
Markdown
261 lines
9.8 KiB
Markdown
# 图片适配模式技术设计
|
||
|
||
## Context
|
||
|
||
### 当前状态
|
||
|
||
当前系统图片渲染逻辑非常简单:直接使用 python-pptx 的 `add_picture()` 方法,传入 box 的宽高参数。python-pptx 会自动将图片拉伸到指定尺寸,无法保持宽高比,也无法进行居中或裁剪处理。
|
||
|
||
```python
|
||
# 当前实现 (pptx_renderer.py)
|
||
def _render_image(self, slide, elem: ImageElement, base_path):
|
||
x, y, w, h = [Inches(v) for v in elem.box]
|
||
slide.shapes.add_picture(str(img_path), x, y, width=w, height=h)
|
||
```
|
||
|
||
### 约束条件
|
||
|
||
- 必须保持向后兼容:未指定 `fit` 参数时,行为与当前一致
|
||
- PPTX 渲染使用英寸单位,图片处理需要像素单位
|
||
- HTML 预览需要与 PPTX 渲染保持一致的视觉效果
|
||
- DPI 配置需要同时影响两个渲染器
|
||
|
||
### 利益相关者
|
||
|
||
- 最终用户:需要多样化的图片适配选项
|
||
- HTML 预览用户:期望预览效果与最终 PPTX 一致
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
|
||
- 支持四种图片适配模式(stretch、contain、cover、center)
|
||
- 支持留白区域的背景色填充
|
||
- 支持可配置的 DPI 转换
|
||
- HTML 预览与 PPTX 渲染效果一致
|
||
- 向后兼容现有 YAML 文件
|
||
|
||
**Non-Goals:**
|
||
|
||
- 不支持图片的旋转、翻转等变换
|
||
- 不支持图片滤镜、水印等高级效果
|
||
- 不支持 fill(平铺)模式
|
||
- 不支持自适应 box 尺寸(box 必需,不是可选)
|
||
|
||
## Decisions
|
||
|
||
### 决策 1: 使用 Pillow 作为图片处理库
|
||
|
||
**选择理由:**
|
||
|
||
- Pillow 是 Python 生态中最成熟的图片处理库,从 PIL 发展而来
|
||
- 内置 `ImageOps` 模块,直接提供 `contain()` 和 `cover()` 方法,与我们的需求完全匹配
|
||
- API 简洁直观,社区活跃,文档完善
|
||
- 轻量级,专注图片处理,不像 OpenCV 那样重量级
|
||
|
||
**替代方案考虑:**
|
||
|
||
| 方案 | 优势 | 劣势 | 结论 |
|
||
|------|------|------|------|
|
||
| Pillow | API 简洁,ImageOps 直接匹配需求 | 需要额外依赖 | ✅ 选择 |
|
||
| OpenCV | 功能强大,性能优异 | API 复杂,BGR 颜色顺序需转换,过度设计 | ❌ |
|
||
| Wand | 功能丰富 | 依赖 ImageMagick,安装复杂,跨平台问题多 | ❌ |
|
||
| 原生实现 | 无额外依赖 | 需要手动实现所有算法,容易出错 | ❌ |
|
||
|
||
### 决策 2: DPI 作为文档级配置
|
||
|
||
**选择:** 在 `metadata.dpi` 中配置,默认值 96
|
||
|
||
**理由:**
|
||
|
||
- DPI 影响整个文档的所有图片,不应在元素级别配置
|
||
- 96 是 Web 标准 DPI,与当前 HTML 渲染器一致
|
||
- 简洁的配置方式,符合 YAML 声明式风格
|
||
|
||
**替代方案:** `metadata.image.dpi`(更结构化,但过于复杂)
|
||
|
||
### 决策 3: PPTX 和 HTML 使用不同的实现方式
|
||
|
||
**PPTX 实现:**
|
||
|
||
- 使用 Pillow 处理图片(缩放、裁剪)
|
||
- 计算居中位置
|
||
- 添加背景色画布(如有需要)
|
||
- 使用 python-pptx 添加处理后的图片
|
||
|
||
**HTML 实现:**
|
||
|
||
- 使用 CSS `object-fit` 属性
|
||
- `stretch` → `object-fit: fill`
|
||
- `contain` → `object-fit: contain`
|
||
- `cover` → `object-fit: cover`
|
||
- `center` → `object-fit: none` + `object-position: center`
|
||
- 背景色使用 CSS `background-color`
|
||
|
||
**理由:** 两个平台的原生能力不同,使用各自的最佳实践,但确保视觉效果一致
|
||
|
||
### 决策 4: 图片适配模式算法设计
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────┐
|
||
│ 图片适配算法流程 │
|
||
└─────────────────────────────────────────────────────────────────────┘
|
||
|
||
输入: img (PIL.Image), box_size (width, height), fit, background
|
||
|
||
1. stretch 模式
|
||
─────────────────────────────────────────────────────────────────
|
||
直接使用 img.resize(box_size),不考虑宽高比
|
||
|
||
2. contain 模式
|
||
─────────────────────────────────────────────────────────────────
|
||
result = ImageOps.contain(img, box_size)
|
||
# result.size <= box_size,保持宽高比
|
||
if background:
|
||
canvas = 创建 box_size 画布,填充背景色
|
||
canvas.paste(result, 居中位置)
|
||
result = canvas
|
||
|
||
3. cover 模式
|
||
─────────────────────────────────────────────────────────────────
|
||
result = ImageOps.cover(img, box_size)
|
||
# result.size == box_size,保持宽高比,裁剪超出部分
|
||
|
||
4. center 模式
|
||
─────────────────────────────────────────────────────────────────
|
||
result = img # 不缩放
|
||
if img.width > box_size.width or img.height > box_size.height:
|
||
result = 裁剪到 box_size(居中裁剪)
|
||
if background:
|
||
canvas = 创建 box_size 画布,填充背景色
|
||
canvas.paste(result, 居中位置)
|
||
result = canvas
|
||
|
||
输出: result (PIL.Image), display_size, display_position
|
||
```
|
||
|
||
### 决策 6: 模块化设计
|
||
|
||
```
|
||
utils/image_utils.py
|
||
├── inches_to_pixels(inches, dpi) -> float
|
||
├── pixels_to_inches(pixels, dpi) -> float
|
||
├── calculate_contain_size(img_size, box_size) -> tuple
|
||
├── calculate_cover_size(img_size, box_size) -> tuple
|
||
├── calculate_center_offset(img_size, box_size) -> tuple
|
||
└── apply_fit_mode(img, box_size, fit, background) -> PIL.Image
|
||
|
||
validators/image_config.py (新建)
|
||
├── validate_fit_value(fit) -> List[ValidationIssue]
|
||
└── validate_fit_box_dependency(elem) -> List[ValidationIssue]
|
||
```
|
||
|
||
**理由:** 单一职责原则,图片处理逻辑与渲染逻辑分离,便于测试和复用
|
||
|
||
### 决策 5: 模块化设计
|
||
|
||
```
|
||
utils/image_utils.py
|
||
├── inches_to_pixels(inches, dpi) -> float
|
||
├── pixels_to_inches(pixels, dpi) -> float
|
||
├── calculate_contain_size(img_size, box_size) -> tuple
|
||
├── calculate_cover_size(img_size, box_size) -> tuple
|
||
├── calculate_center_offset(img_size, box_size) -> tuple
|
||
└── apply_fit_mode(img, box_size, fit, background) -> PIL.Image
|
||
|
||
validators/image_config.py (新建)
|
||
├── validate_fit_value(fit) -> List[ValidationIssue]
|
||
└── validate_background_color(color) -> List[ValidationIssue]
|
||
```
|
||
|
||
**理由:** 单一职责原则,图片处理逻辑与渲染逻辑分离,便于测试和复用
|
||
|
||
**注意:** 由于 box 参数为必填,不存在"没有指定 box"的情况,因此 `validate_fit_box_dependency` 验证器已移除。
|
||
|
||
## Risks / Trade-offs
|
||
|
||
### 风险 1: Pillow 依赖增加
|
||
|
||
**风险:** 新增外部依赖可能增加安装复杂度
|
||
|
||
**缓解措施:**
|
||
- Pillow 是 Python 生态标准库,安装简单(`pip install pillow`)
|
||
- 在 pyproject.toml 中不指定版本号,使用最新稳定版
|
||
- 在 README 中明确说明依赖变更
|
||
|
||
### 风险 2: PPTX 和 HTML 渲染效果不完全一致
|
||
|
||
**风险:** 两个平台实现方式不同,可能在边缘情况下效果有差异
|
||
|
||
**缓解措施:**
|
||
- 核心算法(尺寸计算)使用相同的 Python 函数
|
||
- 编写集成测试,对比两种渲染器的输出
|
||
- 在文档中说明已知差异(如抗锯齿算法不同)
|
||
|
||
### 风险 3: 大图片处理性能问题
|
||
|
||
**风险:** Pillow 处理大图片可能较慢,占用内存
|
||
|
||
**缓解措施:**
|
||
- 仅在需要时才处理图片(contain/cover/center 模式)
|
||
- stretch 模式直接使用 python-pptx 的原生处理,不经过 Pillow
|
||
- 文档中建议用户使用适当尺寸的图片
|
||
|
||
### 风险 4: DPI 配置不当导致尺寸错误
|
||
|
||
**风险:** 用户设置的 DPI 与实际使用场景不符,导致图片尺寸不符合预期
|
||
|
||
**缓解措施:**
|
||
- 默认值 96 适用于大多数场景
|
||
- 在 README 中说明 DPI 的含义和影响
|
||
- 验证器检查 DPI 值是否合理(如 72-300 之间)
|
||
|
||
## Migration Plan
|
||
|
||
### 部署步骤
|
||
|
||
1. **代码变更**
|
||
- 添加 Pillow 依赖到 pyproject.toml
|
||
- 创建 `utils/image_utils.py`
|
||
- 创建 `validators/image_config.py`
|
||
- 更新 `core/elements.py` 的 ImageElement
|
||
- 更新 `renderers/pptx_renderer.py`
|
||
- 更新 `renderers/html_renderer.py`
|
||
|
||
2. **测试**
|
||
- 单元测试:image_utils 的各个函数
|
||
- 单元测试:validators 的图片配置验证
|
||
- 集成测试:四种 fit 模式的 PPTX 渲染
|
||
- 集成测试:四种 fit 模式的 HTML 渲染
|
||
- 端到端测试:完整 YAML 转换流程
|
||
|
||
3. **文档更新**
|
||
- 更新 README.md,添加图片适配模式说明
|
||
- 更新 README_DEV.md,添加架构说明
|
||
|
||
### 回滚策略
|
||
|
||
- 如果发现严重问题,可以回退到之前版本
|
||
- 向后兼容设计确保未指定 fit 参数的 YAML 文件仍能正常工作
|
||
- 回滚后用户只需删除 fit 和 background 参数即可
|
||
|
||
## Open Questions
|
||
|
||
### Q1: 是否需要在图片处理失败时提供降级方案?
|
||
|
||
**决策:** 图片处理失败时抛出 ERROR 级别错误,让用户修复图片问题,不提供降级方案。
|
||
|
||
**理由:** 保持简单明确,图片问题应该由用户在源头解决,而不是掩盖问题。
|
||
|
||
### Q2: background 参数是否支持渐变色?
|
||
|
||
**决策:** background 参数仅支持纯色,不支持渐变色。
|
||
|
||
**理由:** 简化实现,满足绝大多数使用场景。如有后续需求,可以作为独立功能添加。
|
||
|
||
### Q3: 是否需要支持图片质量设置?
|
||
|
||
**决策:** 使用 Pillow 的最高质量重采样算法(LANCZOS),不向用户暴露配置选项。
|
||
|
||
**理由:** 演示文稿场景下图片质量优先于处理速度,使用最高质量算法避免用户困惑。
|