# 图片适配模式技术设计 ## 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),不向用户暴露配置选项。 **理由:** 演示文稿场景下图片质量优先于处理速度,使用最高质量算法避免用户困惑。