1
0
Files
PPTX/openspec/changes/archive/2026-03-04-add-image-fit-modes/design.md
lanyuanxiaoyao 19d6661381 feat: 添加图片适配模式支持
- 支持四种图片适配模式: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 模式
2026-03-04 10:29:21 +08:00

9.8 KiB
Raw Blame History

图片适配模式技术设计

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 属性
  • stretchobject-fit: fill
  • containobject-fit: contain
  • coverobject-fit: cover
  • centerobject-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不向用户暴露配置选项。

理由: 演示文稿场景下图片质量优先于处理速度,使用最高质量算法避免用户困惑。