- 支持四种图片适配模式: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 模式
9.8 KiB
图片适配模式技术设计
Context
当前状态
当前系统图片渲染逻辑非常简单:直接使用 python-pptx 的 add_picture() 方法,传入 box 的宽高参数。python-pptx 会自动将图片拉伸到指定尺寸,无法保持宽高比,也无法进行居中或裁剪处理。
# 当前实现 (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: fillcontain→object-fit: containcover→object-fit: covercenter→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
部署步骤
-
代码变更
- 添加 Pillow 依赖到 pyproject.toml
- 创建
utils/image_utils.py - 创建
validators/image_config.py - 更新
core/elements.py的 ImageElement - 更新
renderers/pptx_renderer.py - 更新
renderers/html_renderer.py
-
测试
- 单元测试:image_utils 的各个函数
- 单元测试:validators 的图片配置验证
- 集成测试:四种 fit 模式的 PPTX 渲染
- 集成测试:四种 fit 模式的 HTML 渲染
- 端到端测试:完整 YAML 转换流程
-
文档更新
- 更新 README.md,添加图片适配模式说明
- 更新 README_DEV.md,添加架构说明
回滚策略
- 如果发现严重问题,可以回退到之前版本
- 向后兼容设计确保未指定 fit 参数的 YAML 文件仍能正常工作
- 回滚后用户只需删除 fit 和 background 参数即可
Open Questions
Q1: 是否需要在图片处理失败时提供降级方案?
决策: 图片处理失败时抛出 ERROR 级别错误,让用户修复图片问题,不提供降级方案。
理由: 保持简单明确,图片问题应该由用户在源头解决,而不是掩盖问题。
Q2: background 参数是否支持渐变色?
决策: background 参数仅支持纯色,不支持渐变色。
理由: 简化实现,满足绝大多数使用场景。如有后续需求,可以作为独立功能添加。
Q3: 是否需要支持图片质量设置?
决策: 使用 Pillow 的最高质量重采样算法(LANCZOS),不向用户暴露配置选项。
理由: 演示文稿场景下图片质量优先于处理速度,使用最高质量算法避免用户困惑。