diff --git a/README.md b/README.md index 5ec51ed..38744fc 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,6 @@ uv run yaml2pptx.py convert presentation.yaml --skip-validation ```yaml metadata: size: "16:9" # 或 "4:3" - dpi: 96 # 可选:DPI 配置,默认 96 slides: - background: @@ -118,24 +117,6 @@ slides: align: center ``` -#### DPI 配置 - -`metadata.dpi` 参数控制图片处理的分辨率,影响图片质量和文件大小: - -- **默认值**:96 DPI(标准屏幕分辨率) -- **建议范围**:72-300 DPI -- **常用值**: - - 72 DPI:网页显示 - - 96 DPI:标准屏幕(默认) - - 150 DPI:高质量打印 - - 300 DPI:专业印刷 - -```yaml -metadata: - size: "16:9" - dpi: 150 # 高质量输出 -``` - #### description 字段 `metadata.description` 字段用于描述整个演示文稿的概要和用途,仅用于文档目的,不影响生成的 PPTX 文件: @@ -189,64 +170,16 @@ slides: - type: image box: [x, y, width, height] src: "path/to/image.png" # 支持相对路径和绝对路径 - fit: contain # 可选:图片适配模式 - background: "#f0f0f0" # 可选:背景色(仅对 contain 和 center 模式有效) ``` -#### 图片适配模式 - -`fit` 参数控制图片如何适配到指定的 box 区域,支持以下模式: - -- **stretch**(默认):拉伸图片以填满整个 box,可能改变宽高比 -- **contain**:保持宽高比,完整显示图片在 box 内,可能留白 -- **cover**:保持宽高比,填满整个 box,可能裁剪图片 -- **center**:不缩放,居中显示,超出部分裁剪 - **示例**: ```yaml slides: - elements: - # 默认 stretch 模式 - type: image src: "photo.jpg" box: [1, 1, 4, 3] - - # contain 模式,带灰色背景 - - type: image - src: "photo.jpg" - box: [5, 1, 4, 3] - fit: contain - background: "#f0f0f0" - - # cover 模式,填满区域 - - type: image - src: "photo.jpg" - box: [1, 4, 4, 3] - fit: cover - - # center 模式,不缩放 - - type: image - src: "icon.png" - box: [5, 4, 2, 2] - fit: center - background: "#ffffff" -``` - -#### 背景色 - -`background` 参数为图片添加背景色,支持 `#RRGGBB` 或 `#RGB` 格式: - -- 仅对 `contain` 和 `center` 模式有效 -- 当图片小于 box 或保持宽高比时,背景色填充留白区域 -- 默认为透明(无背景色) - -```yaml -- type: image - src: "logo.png" - box: [1, 1, 3, 2] - fit: contain - background: "#ffffff" # 白色背景 ``` ### 形状元素 diff --git a/README_DEV.md b/README_DEV.md index ccb10f5..c25f898 100644 --- a/README_DEV.md +++ b/README_DEV.md @@ -67,49 +67,6 @@ yaml2pptx.py (入口) - 核心层不依赖其他业务模块 - 验证层可以调用核心层的元素验证方法 -### 图片处理架构 - -图片适配模式功能采用分层设计,将图片处理逻辑与渲染逻辑分离: - -``` -ImageElement (核心层) - ↓ 定义 fit/background 字段 -utils/image_utils.py (工具层) - ↓ 提供图片处理函数 - ├─→ apply_fit_mode() - 应用适配模式 - ├─→ create_canvas_with_background() - 创建背景画布 - └─→ inches_to_pixels() / pixels_to_inches() - 单位转换 - ↓ 使用 Pillow (PIL) -renderers/pptx_renderer.py (渲染层) - ↓ 调用 image_utils 处理图片 - └─→ 将处理后的图片添加到 PPTX -renderers/html_renderer.py (渲染层) - └─→ 使用 CSS object-fit 实现相同效果 -``` - -**设计要点**: - -1. **Pillow 依赖**: - - 用于高质量图片处理(缩放、裁剪、画布创建) - - 使用 LANCZOS 重采样算法确保最佳质量 - - 支持 RGB 模式和透明度处理 - -2. **适配模式实现**: - - **stretch**:直接使用 `Image.resize()` - - **contain**:使用 `ImageOps.contain()` 保持宽高比 - - **cover**:使用 `ImageOps.cover()` + 中心裁剪 - - **center**:不缩放,使用 `Image.crop()` 裁剪超出部分 - -3. **DPI 配置**: - - 在 `metadata.dpi` 配置,默认 96 - - 通过 Presentation → Renderer 传递 - - 用于像素与英寸的转换计算 - -4. **向后兼容性**: - - `fit` 和 `background` 参数可选 - - 未指定 `fit` 时默认使用 `stretch` 模式 - - 现有 YAML 文件无需修改即可正常工作 - ## 模块职责 ### 1. yaml2pptx.py(入口层) @@ -128,22 +85,13 @@ renderers/html_renderer.py (渲染层) - 进度日志显示准确的渲染数量(不包括禁用的幻灯片) ### 2. utils/(工具层) -- **职责**:通用工具函数和图片处理 +- **职责**:通用工具函数 - **包含**: - `utils/__init__.py` - 日志和颜色工具 - 日志函数:`log_info()`, `log_success()`, `log_error()`, `log_progress()` - 颜色转换:`hex_to_rgb()`, `validate_color()` - - `utils/image_utils.py` - 图片处理工具 - - 单位转换:`inches_to_pixels()`, `pixels_to_inches()` - - 图片适配:`apply_fit_mode()` - 支持 stretch/contain/cover/center 四种模式 - - 画布创建:`create_canvas_with_background()` - 创建带背景色的画布 - **依赖**: - - Pillow (PIL) - 用于高质量图片处理 - - 使用 LANCZOS 重采样算法确保最佳质量 -- **设计原则**: - - 图片处理与渲染逻辑分离 - - 支持 DPI 配置,灵活控制图片分辨率 - - 所有图片操作返回 PIL Image 对象,便于后续处理 + - Pillow (PIL) - 保留用于未来可能的图片处理需求 ### 3. loaders/yaml_loader.py(加载层) - **职责**:YAML 文件加载和验证 @@ -187,10 +135,6 @@ renderers/html_renderer.py (渲染层) - `validators/resource.py` - 资源验证器 - `ResourceValidator` - 检查图片、模板文件存在性 - 验证模板文件结构 - - `validators/image_config.py` - 图片配置验证器 - - `validate_fit_value()` - 验证 fit 参数值 - - `validate_background_color()` - 验证背景色格式 - - `validate_dpi_value()` - 验证 DPI 值范围 - `validators/validator.py` - 主验证器 - `Validator` - 协调所有子验证器 - 集成元素级验证、几何验证、资源验证 diff --git a/core/elements.py b/core/elements.py index 1c2f52d..6c85dc9 100644 --- a/core/elements.py +++ b/core/elements.py @@ -78,8 +78,6 @@ class ImageElement: type: str = 'image' src: str = '' box: list = field(default_factory=lambda: [1, 1, 4, 3]) - fit: Optional[str] = None - background: Optional[str] = None def __post_init__(self): """创建时验证""" @@ -92,31 +90,7 @@ class ImageElement: def validate(self) -> List: """验证元素自身的完整性""" - from validators.result import ValidationIssue - issues = [] - - # 验证 fit 参数 - if self.fit is not None: - valid_fits = ['stretch', 'contain', 'cover', 'center'] - if self.fit not in valid_fits: - issues.append(ValidationIssue( - level="ERROR", - message=f"无效的 fit 值: {self.fit} (支持: {', '.join(valid_fits)})", - location="", - code="INVALID_FIT_VALUE" - )) - - # 验证 background 参数 - if self.background is not None: - if not _is_valid_color(self.background): - issues.append(ValidationIssue( - level="ERROR", - message=f"无效的背景颜色格式: {self.background} (应为 #RRGGBB 或 #RGB)", - location="", - code="INVALID_COLOR_FORMAT" - )) - - return issues + return [] @dataclass diff --git a/core/presentation.py b/core/presentation.py index 9cf0024..fc64dfc 100644 --- a/core/presentation.py +++ b/core/presentation.py @@ -31,7 +31,6 @@ class Presentation: # 获取演示文稿尺寸 metadata = self.data.get("metadata", {}) self.size = metadata.get("size", "16:9") - self.dpi = metadata.get("dpi", 96) self.description = metadata.get("description") # 可选的描述字段 # 验证尺寸值 diff --git a/openspec/changes/archive/2026-03-04-remove-image-fit-modes/.openspec.yaml b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/.openspec.yaml new file mode 100644 index 0000000..5aae5cf --- /dev/null +++ b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-04 diff --git a/openspec/changes/archive/2026-03-04-remove-image-fit-modes/design.md b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/design.md new file mode 100644 index 0000000..19001ff --- /dev/null +++ b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/design.md @@ -0,0 +1,184 @@ +# 移除图片适配模式技术设计 + +## Context + +### 当前状态 + +图片适配模式功能已在 `openspec/changes/archive/2026-03-04-add-image-fit-modes/` 中实现并归档,包括: + +- `utils/image_utils.py`: 图片处理工具模块(英寸像素转换、fit 模式处理) +- `validators/image_config.py`: 图片参数验证器 +- `core/elements.py`: ImageElement 添加了 fit 和 background 字段 +- `renderers/pptx_renderer.py`: 重写 _render_image() 支持四种 fit 模式 +- `renderers/html_renderer.py`: render_image() 支持 CSS object-fit 映射 +- `core/presentation.py`: 读取 metadata.dpi 配置 +- 完整的单元测试和集成测试 + +该功能尚未上线使用,无需考虑向后兼容性。 + +### 约束条件 + +- 必须保留 Pillow 依赖(用户要求留作备用) +- HTML 渲染器需要保留英寸到像素的转换功能(使用硬编码 DPI=96) +- 移除后图片渲染行为:直接拉伸到 box 指定的尺寸 + +### 利益相关者 + +- 最终用户:获得更简单的图片配置(只需 src 和 box) +- 开发者:降低代码复杂度和维护成本 + +## Goals / Non-Goals + +**Goals:** + +- 完全移除图片适配模式功能(fit、background、dpi 配置) +- 简化图片渲染逻辑,恢复到直接使用 python-pptx 原生功能 +- 删除不再需要的工具模块和验证器 +- 删除相关测试文件 +- 更新规格文档和用户文档 +- 保留 Pillow 依赖用于未来可能的图片处理需求 + +**Non-Goals:** + +- 不移除 Pillow 依赖 +- 不改变 HTML 渲染器的基础英寸到像素转换功能(保留硬编码 DPI=96) +- 不影响其他元素类型(text、shape、table)的渲染逻辑 + +## Decisions + +### 决策 1: HTML 渲染器使用硬编码 DPI + +**选择:** HtmlRenderer 使用硬编码的 `self.dpi = 96`,移除 __init__ 的 dpi 参数 + +**理由:** + +- 96 是 Web 标准 DPI,适用于绝大多数场景 +- HTML 预览的尺寸转换是基础功能,不需要用户配置 +- 移除 metadata.dpi 配置后,简化了用户 YAML 文件 + +**实现:** +```python +# renderers/html_renderer.py +class HtmlRenderer: + def __init__(self): # 移除 dpi 参数 + self.dpi = 96 # 硬编码 Web 标准 DPI +``` + +### 决策 2: PPTX 图片渲染恢复到简单实现 + +**选择:** 直接使用 python-pptx 的 `slide.shapes.add_picture()` 方法 + +**理由:** + +- python-pptx 原生支持图片添加,会自动拉伸到指定尺寸 +- 移除对 Pillow 的运行时依赖(保留依赖但代码中不使用) +- 代码更简洁,易于维护 + +**实现:** +```python +# renderers/pptx_renderer.py +def _render_image(self, slide, elem: ImageElement, base_path): + img_path = Path(elem.src) + if not img_path.is_absolute() and base_path: + img_path = Path(base_path) / elem.src + + if not img_path.exists(): + raise YAMLError(f"图片文件未找到: {img_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) +``` + +### 决策 3: 完全删除相关文件而非注释代码 + +**选择:** 直接删除 image_utils.py、image_config.py 及相关测试文件 + +**理由:** + +- 功能尚未上线,无需保留代码历史 +- Git 历史已记录所有实现细节 +- 保持代码库整洁 + +### 决策 4: 删除 image-fit-modes 规格 + +**选择:** 删除 `openspec/specs/image-fit-modes/` 整个目录 + +**理由:** + +- 该规格对应的 capability 正在被移除 +- 归档的变更中已保留完整规格副本 + +## Risks / Trade-offs + +### 风险 1: HTML 预览尺寸可能与 PPTX 不完全一致 + +**风险:** 硬编码 DPI=96 可能在某些显示器上导致预览尺寸与实际 PPTX 有差异 + +**缓解措施:** +- 96 是 Web 标准,覆盖绝大多数场景 +- 预览功能主要用于布局参考,不需要像素级精确 +- 用户主要关注 PPTX 输出结果 + +### 风险 2: 删除文件可能影响其他未发现的依赖 + +**风险:** 可能有其他代码或测试依赖 image_utils 或 image_config + +**缓解措施:** +- 运行完整的测试套件验证 +- 检查 imports 确保没有遗漏的引用 +- 使用 Grep 搜索可能的引用点 + +### 风险 3: 文档更新可能不完整 + +**风险:** README.md 或 README_DEV.md 中可能残留相关说明 + +**缓解措施:** +- 在 tasks 中明确列出文档更新任务 +- 人工审查最终文档内容 + +## Migration Plan + +### 部署步骤 + +由于功能尚未上线,无需迁移策略。直接执行删除和修改即可。 + +1. **删除工具和验证器模块** + - 删除 `utils/image_utils.py` + - 删除 `validators/image_config.py` + - 更新 `validators/validator.py` 移除引用 + +2. **修改核心代码** + - 更新 `core/elements.py` 的 ImageElement + - 更新 `core/presentation.py` 的 Presentation + - 更新 `renderers/pptx_renderer.py` + - 更新 `renderers/html_renderer.py` + - 更新 `yaml2pptx.py` + - 更新 `preview/server.py` + +3. **删除测试文件** + - 删除 `tests/unit/test_image_utils.py` + - 删除 `tests/unit/test_validators/test_image_config.py` + - 删除 `tests/integration/test_image_fit_modes.py` + +4. **删除规格文件** + - 删除 `openspec/specs/image-fit-modes/` 目录 + +5. **更新文档** + - 更新 `README.md` + - 更新 `README_DEV.md` + - 更新 `openspec/specs/element-rendering/spec.md` + - 更新 `openspec/specs/html-rendering/spec.md` + +6. **验证** + - 运行完整测试套件 + - 检查是否有遗留的 import 引用 + +### 回滚策略 + +- Git 历史保留了所有实现 +- 如需恢复功能,可以从归档的变更中恢复代码 +- 或者从 Git 历史中恢复提交 `19d6661` 之前的代码 + +## Open Questions + +无。移除操作明确清晰,无未决问题。 diff --git a/openspec/changes/archive/2026-03-04-remove-image-fit-modes/proposal.md b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/proposal.md new file mode 100644 index 0000000..33babdd --- /dev/null +++ b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/proposal.md @@ -0,0 +1,57 @@ +# 移除图片适配模式功能 + +## Why + +图片适配模式功能已实现但尚未上线使用,经过评估决定移除该功能以简化系统复杂度。移除后系统将恢复到更简单直接的图片渲染方式:直接使用 python-pptx 的原生图片添加功能,图片会被拉伸到指定尺寸。 + +## What Changes + +- **BREAKING** 移除图片元素的 `fit` 参数(stretch、contain、cover、center 四种模式) +- **BREAKING** 移除图片元素的 `background` 参数(背景色填充) +- **BREAKING** 移除文档级的 `metadata.dpi` 配置 +- **BREAKING** 删除 `utils/image_utils.py` 模块(图片处理工具) +- **BREAKING** 删除 `validators/image_config.py` 模块(图片配置验证器) +- 重写 `renderers/pptx_renderer.py` 的 `_render_image()` 方法,恢复简单实现 +- 简化 `renderers/html_renderer.py` 的 `render_image()` 方法,移除 fit/background 处理 +- 简化 `core/elements.py` 的 `ImageElement` 类,移除 fit/background 字段 +- 简化 `core/presentation.py` 的 `Presentation` 类,移除 dpi 配置读取 +- 保留 Pillow 依赖(留作未来可能的其他用途) +- HTML 渲染器使用硬编码的 DPI=96(Web 标准,无需用户配置) + +## Capabilities + +### Modified Capabilities +- `element-rendering`: 移除图片元素的 fit 和 background 参数支持,恢复到基础图片渲染 +- `html-rendering`: 移除图片元素的 fit 和 background 参数支持,简化 HTML 图片渲染 + +## Impact + +### 代码变更 +- `core/elements.py`: ImageElement 移除 fit 和 background 字段及验证逻辑 +- `core/presentation.py`: Presentation 移除 dpi 配置读取 +- `renderers/pptx_renderer.py`: 重写 _render_image() 方法,移除 Pillow 图片处理和 fit 模式逻辑 +- `renderers/html_renderer.py`: 简化 render_image() 方法,移除 fit 模式映射和背景色容器 +- `yaml2pptx.py`: 移除传递 dpi 参数到 PptxGenerator +- `preview/server.py`: 移除传递 dpi 参数到 HtmlRenderer +- `validators/validator.py`: 移除对 image_config 的引用 + +### 删除的文件 +- `utils/image_utils.py`: 整个文件删除 +- `validators/image_config.py`: 整个文件删除 +- `tests/unit/test_image_utils.py`: 删除 +- `tests/unit/test_validators/test_image_config.py`: 删除 +- `tests/integration/test_image_fit_modes.py`: 删除 +- `openspec/specs/image-fit-modes/`: 整个规格目录删除 + +### API 变更 +- YAML 语法移除: + - 图片元素不再支持 `fit` 字段 + - 图片元素不再支持 `background` 字段 + - metadata 层级不再支持 `dpi` 字段 + +### 依赖变更 +- Pillow 依赖保留(在 pyproject.toml 中),用于未来可能的图片处理需求 + +### 文档变更 +- `README.md`: 移除图片适配模式使用说明 +- `README_DEV.md`: 移除图片处理架构说明 diff --git a/openspec/changes/archive/2026-03-04-remove-image-fit-modes/specs/element-rendering/spec.md b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/specs/element-rendering/spec.md new file mode 100644 index 0000000..348e642 --- /dev/null +++ b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/specs/element-rendering/spec.md @@ -0,0 +1,82 @@ +# Element Rendering Delta Spec + +## REMOVED Requirements + +### Requirement: 系统必须支持图片元素渲染 (部分移除) + +**Reason**: 移除图片适配模式功能(fit 和 background 参数),简化图片渲染逻辑 + +**Migration**: 移除图片元素的 `fit` 和 `background` 参数定义,系统将使用简单的拉伸模式渲染图片 + +#### Scenario: 使用 fit 模式渲染图片 + +**REMOVED** + +- **WHEN** 元素定义为 `{type: image, src: "photo.jpg", box: [1, 1, 4, 3], fit: contain}` +- **THEN** 系统使用 `contain` 模式渲染图片 +- **AND** 保持图片宽高比,完整显示在 box 内 + +#### Scenario: 使用背景色渲染图片 + +**REMOVED** + +- **WHEN** 元素定义为 `{type: image, src: "photo.png", box: [1, 1, 4, 3], fit: contain, background: "#f0f0f0"}` +- **THEN** 系统使用 `contain` 模式渲染图片 +- **AND** 用 #f0f0f0 颜色填充留白区域 + +#### Scenario: 图片格式不支持时报错 + +**REMOVED** + +- **WHEN** 图片文件格式不被 Pillow 支持 +- **THEN** 系统抛出错误,提示图片格式不支持,并列出支持的格式 + +#### Scenario: 图片处理失败时报错 + +**REMOVED** + +- **WHEN** Pillow 处理图片时发生异常(如文件损坏) +- **THEN** 系统抛出 ERROR 级别错误,不降级到其他模式 + +#### Scenario: 使用 DPI 配置渲染图片 + +**REMOVED** + +- **WHEN** metadata 定义了 `dpi: 120` 且图片需要处理 +- **THEN** 系统使用该 DPI 值进行像素与英寸的转换 + +#### Scenario: fit 参数值无效时报错 + +**REMOVED** + +- **WHEN** `fit` 参数值不是 stretch、contain、cover、center 之一 +- **THEN** 系统抛出 ERROR,并列出有效值 + +#### Scenario: background 参数颜色格式无效时报错 + +**REMOVED** + +- **WHEN** `background` 值不是有效的颜色格式 +- **THEN** 系统抛出 ERROR,提示颜色格式应为 #RRGGBB 或 #RGB + +## MODIFIED Requirements + +### Requirement: 系统必须支持图片元素渲染 + +系统 SHALL 将 YAML 中定义的图片元素渲染为 PPTX 图片对象,直接使用 python-pptx 的原生图片添加功能。 + +#### Scenario: 渲染本地图片 + +- **WHEN** 元素定义为 `{type: image, src: "images/logo.png", box: [2, 3, 4, 3]}` +- **THEN** 系统从指定路径加载图片,在 (2, 3) 位置渲染为 4×3 英寸大小 +- **AND** 图片会被拉伸到指定尺寸(python-pptx 默认行为) + +#### Scenario: 图片文件不存在时报错 + +- **WHEN** 图片 src 指向不存在的文件路径 +- **THEN** 系统抛出错误,明确指出图片文件未找到 + +#### Scenario: 相对路径处理 + +- **WHEN** 图片 src 使用相对路径 `"assets/images/logo.png"` +- **THEN** 系统基于演示文稿文件所在目录解析相对路径 diff --git a/openspec/changes/archive/2026-03-04-remove-image-fit-modes/specs/html-rendering/spec.md b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/specs/html-rendering/spec.md new file mode 100644 index 0000000..e8dd7ca --- /dev/null +++ b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/specs/html-rendering/spec.md @@ -0,0 +1,86 @@ +# HTML Rendering Delta Spec + +## REMOVED Requirements + +### Requirement: 系统必须渲染图片元素 (部分移除) + +**Reason**: 移除图片适配模式功能(fit 和 background 参数),简化 HTML 图片渲染 + +**Migration**: 移除图片元素的 `fit` 和 `background` 参数定义,HTML 渲染器将输出简单的 `` 标签 + +#### Scenario: 使用 fit 模式渲染图片 + +**REMOVED** + +- **WHEN** 元素定义为 `{type: image, src: "photo.jpg", box: [1, 1, 4, 3], fit: contain}` +- **THEN** 系统应用 CSS `object-fit: contain` +- **AND** 保持图片宽高比,完整显示在 box 内 + +#### Scenario: fit 模式与 CSS object-fit 映射 + +**REMOVED** + +- **WHEN** `fit` 参数为 `stretch` +- **THEN** 系统应用 CSS `object-fit: fill` + +- **WHEN** `fit` 参数为 `contain` +- **THEN** 系统应用 CSS `object-fit: contain` + +- **WHEN** `fit` 参数为 `cover` +- **THEN** 系统应用 CSS `object-fit: cover` + +- **WHEN** `fit` 参数为 `center` +- **THEN** 系统应用 CSS `object-fit: none` 和 `object-position: center` + +#### Scenario: 使用背景色渲染图片 + +**REMOVED** + +- **WHEN** 元素定义为 `{type: image, src: "photo.png", box: [1, 1, 4, 3], fit: contain, background: "#f0f0f0"}` +- **THEN** 系统为图片容器添加 CSS `background-color: #f0f0f0` +- **AND** 应用 CSS `object-fit: contain` + +#### Scenario: 图片容器支持背景色 + +**REMOVED** + +- **WHEN** 图片指定了 `background` 参数 +- **THEN** 系统创建包装容器,应用背景色到容器 +- **AND** 图片在容器内使用 object-fit 定位 + +#### Scenario: 使用 DPI 配置渲染图片 + +**REMOVED** + +- **WHEN** metadata 定义了 `dpi: 120` +- **THEN** 系统使用该 DPI 值进行英寸到像素的转换 +- **AND** box: [1, 1, 4, 3] 转换为 CSS: left: 120px; top: 120px; width: 480px; height: 360px + +#### Scenario: HTML 渲染与 PPTX 渲染效果一致 + +**REMOVED** + +- **WHEN** 同一图片元素使用相同的 fit 和 background 参数 +- **THEN** HTML 预览和 PPTX 输出的视觉效果应保持一致 +- **AND** 图片位置、尺寸、适配方式相同 + +## MODIFIED Requirements + +### Requirement: 系统必须渲染图片元素 + +系统 SHALL 将 YAML 中的图片元素转换为 HTML `` 标签,使用固定 DPI 进行尺寸转换。 + +#### Scenario: 渲染本地图片 + +- **WHEN** 元素定义为 `{type: image, src: "images/logo.png", box: [2, 3, 4, 3]}` +- **THEN** 系统生成 `` 标签,src 为图片的文件路径,位置为 (192px, 288px),尺寸为 384x288 像素 + +#### Scenario: 处理相对路径 + +- **WHEN** 图片 src 使用相对路径 `"assets/logo.png"` +- **THEN** 系统基于 YAML 文件所在目录解析相对路径 + +#### Scenario: 图片不存在时显示占位符 + +- **WHEN** 图片文件不存在 +- **THEN** 系统显示占位符或错误提示,而不是崩溃 diff --git a/openspec/changes/archive/2026-03-04-remove-image-fit-modes/tasks.md b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/tasks.md new file mode 100644 index 0000000..3723f3b --- /dev/null +++ b/openspec/changes/archive/2026-03-04-remove-image-fit-modes/tasks.md @@ -0,0 +1,67 @@ +# 移除图片适配模式功能 - 任务清单 + +## 1. 删除工具和验证器模块 + +- [x] 1.1 删除 `utils/image_utils.py` 文件 +- [x] 1.2 删除 `validators/image_config.py` 文件 +- [x] 1.3 更新 `validators/validator.py`,移除对 image_config 的引用 + +## 2. 修改核心元素定义 + +- [x] 2.1 更新 `core/elements.py` 的 ImageElement 类,移除 fit 字段 +- [x] 2.2 更新 `core/elements.py` 的 ImageElement 类,移除 background 字段 +- [x] 2.3 简化 `core/elements.py` 的 ImageElement.validate() 方法,移除 fit/background 验证逻辑 + +## 3. 修改演示文稿类 + +- [x] 3.1 更新 `core/presentation.py` 的 Presentation 类,移除 self.dpi = metadata.get("dpi", 96) + +## 4. 修改 PPTX 渲染器 + +- [x] 4.1 更新 `renderers/pptx_renderer.py`,移除 `from PIL import Image` 导入 +- [x] 4.2 更新 `renderers/pptx_renderer.py`,移除 `from utils.image_utils import ...` 导入 +- [x] 4.3 更新 `renderers/pptx_renderer.py` 的 PptxGenerator.__init__(),移除 dpi 参数 +- [x] 4.4 重写 `renderers/pptx_renderer.py` 的 _render_image() 方法,恢复简单实现 + +## 5. 修改 HTML 渲染器 + +- [x] 5.1 更新 `renderers/html_renderer.py` 的 HtmlRenderer.__init__(),移除 dpi 参数,硬编码 self.dpi = 96 +- [x] 5.2 简化 `renderers/html_renderer.py` 的 render_image() 方法,移除 fit 模式映射逻辑 +- [x] 5.3 简化 `renderers/html_renderer.py` 的 render_image() 方法,移除 background 容器逻辑 + +## 6. 修改入口文件 + +- [x] 6.1 更新 `yaml2pptx.py`,移除传递 dpi 参数到 PptxGenerator +- [x] 6.2 更新 `preview/server.py`,移除传递 dpi 参数到 HtmlRenderer + +## 7. 删除测试文件 + +- [x] 7.1 删除 `tests/unit/test_image_utils.py` +- [x] 7.2 删除 `tests/unit/test_validators/test_image_config.py` +- [x] 7.3 删除 `tests/integration/test_image_fit_modes.py` +- [x] 7.4 检查并更新 `tests/fixtures/create_test_images.py`(如果有其他依赖则保留,否则删除) +- [x] 7.5 检查并更新 `tests/integration/test_rendering_flow.py`,移除相关测试 + +## 8. 删除规格文件 + +- [x] 8.1 删除 `openspec/specs/image-fit-modes/` 整个目录 + +## 9. 更新主规格文件 + +- [x] 9.1 更新 `openspec/specs/element-rendering/spec.md`,移除 fit 和 background 相关规格 +- [x] 9.2 更新 `openspec/specs/html-rendering/spec.md`,移除 fit 和 background 相关规格 + +## 10. 更新用户文档 + +- [x] 10.1 更新 `README.md`,移除图片适配模式章节 +- [x] 10.2 更新 `README.md`,移除 metadata.dpi 配置说明 +- [x] 10.3 更新 `README_DEV.md`,移除图片处理架构说明 +- [x] 10.4 更新 `README_DEV.md`,移除 Pillow 依赖说明(改为保留用于备用) + +## 11. 验证和测试 + +- [x] 11.1 运行 `uv run pytest`,确保所有测试通过 +- [x] 11.2 使用 Grep 搜索代码库,确认没有遗留的 image_utils 或 image_config 引用 +- [x] 11.3 使用 Grep 搜索代码库,确认没有遗留的 fit 或 background 参数引用(在图片元素上下文中) +- [x] 11.4 运行 `uv run python yaml2pptx.py` 测试基本转换功能 +- [x] 11.5 运行 `uv run python preview/server.py` 测试预览功能 diff --git a/openspec/specs/element-rendering/spec.md b/openspec/specs/element-rendering/spec.md index e68f67f..380c5d9 100644 --- a/openspec/specs/element-rendering/spec.md +++ b/openspec/specs/element-rendering/spec.md @@ -54,61 +54,23 @@ Element rendering系统负责将 YAML 中定义的各类元素(文本、图片 ### Requirement: 系统必须支持图片元素渲染 -系统 SHALL 将 YAML 中定义的图片元素渲染为 PPTX 图片对象,支持 `fit` 和 `background` 参数控制图片适配模式。 +系统 SHALL 将 YAML 中定义的图片元素渲染为 PPTX 图片对象。 #### Scenario: 渲染本地图片 - **WHEN** 元素定义为 `{type: image, src: "images/logo.png", box: [2, 3, 4, 3]}` - **THEN** 系统从指定路径加载图片,在 (2, 3) 位置渲染为 4×3 英寸大小 -- **AND** 使用默认的 `stretch` 模式(向后兼容) - -#### Scenario: 使用 fit 模式渲染图片 - -- **WHEN** 元素定义为 `{type: image, src: "photo.jpg", box: [1, 1, 4, 3], fit: contain}` -- **THEN** 系统使用 `contain` 模式渲染图片 -- **AND** 保持图片宽高比,完整显示在 box 内 - -#### Scenario: 使用背景色渲染图片 - -- **WHEN** 元素定义为 `{type: image, src: "photo.png", box: [1, 1, 4, 3], fit: contain, background: "#f0f0f0"}` -- **THEN** 系统使用 `contain` 模式渲染图片 -- **AND** 用 #f0f0f0 颜色填充留白区域 #### Scenario: 图片文件不存在时报错 - **WHEN** 图片 src 指向不存在的文件路径 - **THEN** 系统抛出错误,明确指出图片文件未找到 -#### Scenario: 图片格式不支持时报错 - -- **WHEN** 图片文件格式不被 Pillow 支持 -- **THEN** 系统抛出错误,提示图片格式不支持,并列出支持的格式 - -#### Scenario: 图片处理失败时报错 - -- **WHEN** Pillow 处理图片时发生异常(如文件损坏) -- **THEN** 系统抛出 ERROR 级别错误,不降级到其他模式 - #### Scenario: 相对路径处理 - **WHEN** 图片 src 使用相对路径 `"assets/images/logo.png"` - **THEN** 系统基于演示文稿文件所在目录解析相对路径 -#### Scenario: 使用 DPI 配置渲染图片 - -- **WHEN** metadata 定义了 `dpi: 120` 且图片需要处理 -- **THEN** 系统使用该 DPI 值进行像素与英寸的转换 - -#### Scenario: fit 参数值无效时报错 - -- **WHEN** `fit` 参数值不是 stretch、contain、cover、center 之一 -- **THEN** 系统抛出 ERROR,并列出有效值 - -#### Scenario: background 参数颜色格式无效时报错 - -- **WHEN** `background` 值不是有效的颜色格式 -- **THEN** 系统抛出 ERROR,提示颜色格式应为 #RRGGBB 或 #RGB - ### Requirement: 图片元素的 box 参数必须存在 系统 SHALL 要求图片元素必须包含 `box` 参数,否则验证失败。 diff --git a/openspec/specs/html-rendering/spec.md b/openspec/specs/html-rendering/spec.md index a9d0f4b..cd0c7a4 100644 --- a/openspec/specs/html-rendering/spec.md +++ b/openspec/specs/html-rendering/spec.md @@ -133,45 +133,12 @@ HTML Rendering 系统负责将 YAML 中定义的各类元素(文本、图片 ### Requirement: 系统必须渲染图片元素 -系统 SHALL 将 YAML 中的图片元素转换为 HTML `` 标签,支持 `fit` 和 `background` 参数,使用 CSS object-fit 实现适配模式。 +系统 SHALL 将 YAML 中的图片元素转换为 HTML `` 标签,使用固定 DPI 进行尺寸转换。 #### Scenario: 渲染本地图片 - **WHEN** 元素定义为 `{type: image, src: "images/logo.png", box: [2, 3, 4, 3]}` - **THEN** 系统生成 `` 标签,src 为图片的文件路径,位置为 (192px, 288px),尺寸为 384x288 像素 -- **AND** 使用默认的 CSS 样式 `object-fit: fill`(等同于 stretch 模式) - -#### Scenario: 使用 fit 模式渲染图片 - -- **WHEN** 元素定义为 `{type: image, src: "photo.jpg", box: [1, 1, 4, 3], fit: contain}` -- **THEN** 系统应用 CSS `object-fit: contain` -- **AND** 保持图片宽高比,完整显示在 box 内 - -#### Scenario: fit 模式与 CSS object-fit 映射 - -- **WHEN** `fit` 参数为 `stretch` -- **THEN** 系统应用 CSS `object-fit: fill` - -- **WHEN** `fit` 参数为 `contain` -- **THEN** 系统应用 CSS `object-fit: contain` - -- **WHEN** `fit` 参数为 `cover` -- **THEN** 系统应用 CSS `object-fit: cover` - -- **WHEN** `fit` 参数为 `center` -- **THEN** 系统应用 CSS `object-fit: none` 和 `object-position: center` - -#### Scenario: 使用背景色渲染图片 - -- **WHEN** 元素定义为 `{type: image, src: "photo.png", box: [1, 1, 4, 3], fit: contain, background: "#f0f0f0"}` -- **THEN** 系统为图片容器添加 CSS `background-color: #f0f0f0` -- **AND** 应用 CSS `object-fit: contain` - -#### Scenario: 图片容器支持背景色 - -- **WHEN** 图片指定了 `background` 参数 -- **THEN** 系统创建包装容器,应用背景色到容器 -- **AND** 图片在容器内使用 object-fit 定位 #### Scenario: 处理相对路径 @@ -183,18 +150,6 @@ HTML Rendering 系统负责将 YAML 中定义的各类元素(文本、图片 - **WHEN** 图片文件不存在 - **THEN** 系统显示占位符或错误提示,而不是崩溃 -#### Scenario: 使用 DPI 配置渲染图片 - -- **WHEN** metadata 定义了 `dpi: 120` -- **THEN** 系统使用该 DPI 值进行英寸到像素的转换 -- **AND** box: [1, 1, 4, 3] 转换为 CSS: left: 120px; top: 120px; width: 480px; height: 360px - -#### Scenario: HTML 渲染与 PPTX 渲染效果一致 - -- **WHEN** 同一图片元素使用相同的 fit 和 background 参数 -- **THEN** HTML 预览和 PPTX 输出的视觉效果应保持一致 -- **AND** 图片位置、尺寸、适配方式相同 - ### Requirement: 图片元素的 box 参数必须存在 系统 SHALL 要求图片元素必须包含 `box` 参数,否则验证失败。 diff --git a/openspec/specs/image-fit-modes/spec.md b/openspec/specs/image-fit-modes/spec.md deleted file mode 100644 index 31b6e68..0000000 --- a/openspec/specs/image-fit-modes/spec.md +++ /dev/null @@ -1,201 +0,0 @@ -# Image Fit Modes - -## Purpose - -图片适配模式能力为图片元素提供多种适配策略,允许用户控制图片在指定区域内的显示方式,包括拉伸、保持比例、填充裁剪和居中显示。同时支持 DPI 配置和背景色填充,满足不同场景的视觉需求。 - -## Requirements - -### Requirement: 系统必须支持图片 fit 参数 - -系统 SHALL 支持图片元素的 `fit` 参数,允许用户指定图片适配模式。 - -#### Scenario: fit 参数支持四种模式 - -- **WHEN** 图片元素定义了 `fit` 参数 -- **THEN** 系统支持 `stretch`、`contain`、`cover`、`center` 四种模式 - -#### Scenario: fit 参数默认值为 stretch - -- **WHEN** 图片元素未指定 `fit` 参数 -- **THEN** 系统使用 `stretch` 模式(向后兼容) - -#### Scenario: fit 参数值无效时报错 - -- **WHEN** `fit` 参数值不是四种有效模式之一 -- **THEN** 系统抛出 ERROR,并列出有效值(stretch、contain、cover、center) - -### Requirement: 系统必须支持 stretch 模式 - -系统 SHALL 在 `stretch` 模式下将图片强制缩放到 box 指定的尺寸,不考虑宽高比。 - -#### Scenario: stretch 模式拉伸图片 - -- **WHEN** 图片元素定义了 `fit: stretch` 或未指定 `fit` -- **THEN** 系统将图片拉伸到 box 的宽高尺寸 -- **AND** 图片可能变形 - -#### Scenario: stretch 模式不考虑背景色 - -- **WHEN** 图片使用 `fit: stretch` 并指定了 `background` -- **THEN** 背景色参数被忽略(无留白区域) - -### Requirement: 系统必须支持 contain 模式 - -系统 SHALL 在 `contain` 模式下保持图片宽高比,完整显示图片在 box 内,可能有留白。 - -#### Scenario: contain 模式保持宽高比 - -- **WHEN** 图片元素定义了 `fit: contain` -- **THEN** 系统缩放图片使其完整显示在 box 内 -- **AND** 保持原始宽高比 -- **AND** 图片尺寸不超过 box 尺寸 - -#### Scenario: contain 模式图片居中 - -- **WHEN** 图片使用 `fit: contain` 且小于 box 尺寸 -- **THEN** 系统将图片居中显示在 box 内 - -#### Scenario: contain 模式支持背景色 - -- **WHEN** 图片使用 `fit: contain` 并指定了 `background` -- **THEN** 系统用指定颜色填充 box 内的留白区域 - -#### Scenario: contain 模式图片比 box 大 - -- **WHEN** 图片原始尺寸大于 box 尺寸 -- **THEN** 系统等比缩小图片使其完整显示在 box 内 - -#### Scenario: contain 模式图片比 box 小 - -- **WHEN** 图片原始尺寸小于 box 尺寸 -- **THEN** 系统保持原始尺寸,居中显示在 box 内 - -### Requirement: 系统必须支持 cover 模式 - -系统 SHALL 在 `cover` 模式下保持图片宽高比,填充整个 box,裁剪超出部分。 - -#### Scenario: cover 模式保持宽高比 - -- **WHEN** 图片元素定义了 `fit: cover` -- **THEN** 系统缩放图片使其填满 box -- **AND** 保持原始宽高比 -- **AND** 裁剪超出 box 的部分 - -#### Scenario: cover 模式图片居中裁剪 - -- **WHEN** 图片使用 `fit: cover` 且需要裁剪 -- **THEN** 系统从图片中心进行裁剪 - -#### Scenario: cover 模式不考虑背景色 - -- **WHEN** 图片使用 `fit: cover` 并指定了 `background` -- **THEN** 背景色参数被忽略(无留白区域) - -#### Scenario: cover 模式图片比 box 大 - -- **WHEN** 图片原始尺寸大于 box 尺寸 -- **THEN** 系统等比缩小图片并裁剪超出部分 - -#### Scenario: cover 模式图片比 box 小 - -- **WHEN** 图片原始尺寸小于 box 尺寸 -- **THEN** 系统等比放大图片并裁剪超出部分 - -### Requirement: 系统必须支持 center 模式 - -系统 SHALL 在 `center` 模式下按原始尺寸居中显示图片,不缩放,超出 box 的部分被裁剪。 - -#### Scenario: center 模式不缩放图片 - -- **WHEN** 图片元素定义了 `fit: center` -- **THEN** 系统保持图片原始尺寸,不进行缩放 - -#### Scenario: center 模式图片居中 - -- **WHEN** 图片使用 `fit: center` -- **THEN** 系统将图片居中显示在 box 内 - -#### Scenario: center 模式裁剪超出部分 - -- **WHEN** 图片原始尺寸大于 box 尺寸 -- **THEN** 系统裁剪超出 box 的部分(从中心裁剪) - -#### Scenario: center 模式支持背景色 - -- **WHEN** 图片使用 `fit: center` 并指定了 `background` -- **THEN** 系统用指定颜色填充 box 内的留白区域 - -### Requirement: 系统必须支持 background 参数 - -系统 SHALL 支持图片元素的 `background` 参数,允许用户指定留白区域的填充颜色。 - -#### Scenario: background 参数默认透明 - -- **WHEN** 图片元素未指定 `background` 参数 -- **THEN** 留白区域保持透明 - -#### Scenario: background 参数支持纯色 - -- **WHEN** 图片元素指定了 `background: "#ff0000"` -- **THEN** 系统使用指定颜色填充留白区域 - -#### Scenario: background 参数不支持渐变 - -- **WHEN** 图片元素指定了渐变色(如 `"linear-gradient(...)"`) -- **THEN** 系统抛出 ERROR,提示仅支持纯色 - -#### Scenario: background 参数颜色格式验证 - -- **WHEN** `background` 值不是有效的颜色格式 -- **THEN** 系统抛出 ERROR,提示颜色格式应为 #RRGGBB 或 #RGB - -### Requirement: 系统必须支持文档级 DPI 配置 - -系统 SHALL 支持在 `metadata.dpi` 中配置 DPI 值,用于像素与英寸的转换。 - -#### Scenario: DPI 默认值为 96 - -- **WHEN** metadata 未指定 `dpi` 参数 -- **THEN** 系统使用默认值 96 - -#### Scenario: DPI 配置影响所有图片 - -- **WHEN** metadata 指定了 `dpi: 120` -- **THEN** 系统使用该值进行所有图片的像素与英寸转换 - -#### Scenario: DPI 值验证 - -- **WHEN** `dpi` 值超出合理范围(如小于 72 或大于 300) -- **THEN** 系统发出 WARNING,提示 DPI 值可能不合适 - -### Requirement: 系统必须在图片处理失败时抛出错误 - -系统 SHALL 在图片处理失败时抛出 ERROR 级别错误,不提供降级方案。 - -#### Scenario: 损坏的图片文件 - -- **WHEN** Pillow 无法读取图片文件(文件损坏或格式不支持) -- **THEN** 系统抛出 ERROR,明确指出图片文件问题 -- **AND** 不降级到其他模式 - -#### Scenario: 图片处理异常 - -- **WHEN** Pillow 处理图片时发生异常(如内存不足) -- **THEN** 系统抛出 ERROR,包含异常信息 -- **AND** 不降级到其他模式 - -### Requirement: 系统必须使用最高质量的图片处理算法 - -系统 SHALL 使用 Pillow 的最高质量重采样算法(LANCZOS)进行图片缩放。 - -#### Scenario: 图片缩放使用 LANCZOS - -- **WHEN** 系统需要缩放图片(contain、cover 模式) -- **THEN** 使用 Pillow 的 LANCZOS 重采样算法 -- **AND** 不向用户暴露质量配置选项 - -#### Scenario: 图片裁剪保持质量 - -- **WHEN** 系统需要裁剪图片(cover、center 模式) -- **THEN** 裁剪操作不损失图片质量 diff --git a/preview/server.py b/preview/server.py index 8532ede..c90477d 100644 --- a/preview/server.py +++ b/preview/server.py @@ -148,7 +148,7 @@ def generate_preview_html(yaml_file, template_dir): """生成完整的预览 HTML 页面""" try: pres = Presentation(yaml_file, template_dir) - renderer = HtmlRenderer(pres.dpi) + renderer = HtmlRenderer() slides_html = "" for i, slide_data in enumerate(pres.data.get('slides', [])): diff --git a/renderers/html_renderer.py b/renderers/html_renderer.py index 76cdf64..6b88032 100644 --- a/renderers/html_renderer.py +++ b/renderers/html_renderer.py @@ -11,14 +11,11 @@ from core.elements import TextElement, ImageElement, ShapeElement, TableElement class HtmlRenderer: """HTML 渲染器,将元素渲染为 HTML""" - def __init__(self, dpi=96): + def __init__(self): """ 初始化 HTML 渲染器 - - Args: - dpi: DPI 配置,默认 96 """ - self.dpi = dpi + self.dpi = 96 # 硬编码 Web 标准 DPI def render_slide(self, slide_data, index, base_path): """ @@ -171,46 +168,12 @@ class HtmlRenderer: """ img_path = Path(base_path) / elem.src if base_path else Path(elem.src) - # 获取 fit 模式,默认为 stretch - fit = elem.fit if elem.fit else 'stretch' - - # fit 模式到 CSS object-fit 的映射 - object_fit_map = { - 'stretch': 'fill', - 'contain': 'contain', - 'cover': 'cover', - 'center': 'none' - } - object_fit = object_fit_map.get(fit, 'fill') - # 基础样式 style = f""" left: {elem.box[0] * self.dpi}px; top: {elem.box[1] * self.dpi}px; width: {elem.box[2] * self.dpi}px; height: {elem.box[3] * self.dpi}px; - object-fit: {object_fit}; - object-position: center; """ - # 如果有背景色,需要创建包装容器 - if elem.background: - container_style = f""" - position: absolute; - left: {elem.box[0] * self.dpi}px; - top: {elem.box[1] * self.dpi}px; - width: {elem.box[2] * self.dpi}px; - height: {elem.box[3] * self.dpi}px; - background-color: {elem.background}; - """ - img_style = f""" - width: 100%; - height: 100%; - object-fit: {object_fit}; - object-position: center; - """ - return f'''
- -
''' - else: - return f'' + return f'' diff --git a/renderers/pptx_renderer.py b/renderers/pptx_renderer.py index c2997f4..4f38527 100644 --- a/renderers/pptx_renderer.py +++ b/renderers/pptx_renderer.py @@ -10,28 +10,23 @@ from pptx.util import Inches, Pt from pptx.enum.text import PP_ALIGN from pptx.enum.shapes import MSO_SHAPE from pptx.dml.color import RGBColor -from PIL import Image -import tempfile from core.elements import TextElement, ImageElement, ShapeElement, TableElement from loaders.yaml_loader import YAMLError from utils import hex_to_rgb -from utils.image_utils import inches_to_pixels, apply_fit_mode class PptxGenerator: """PPTX 生成器,封装 python-pptx 操作""" - def __init__(self, size='16:9', dpi=96): + def __init__(self, size='16:9'): """ 初始化 PPTX 生成器 Args: size: 幻灯片尺寸("16:9" 或 "4:3") - dpi: DPI 配置,默认 96 """ self.prs = PptxPresentation() - self.dpi = dpi # 设置幻灯片尺寸 if size == '16:9': @@ -154,41 +149,8 @@ class PptxGenerator: # 获取位置和尺寸 x, y, w, h = [Inches(v) for v in elem.box] - # 获取 fit 模式,默认为 stretch - fit = elem.fit if elem.fit else 'stretch' - - try: - # stretch 模式:直接使用 python-pptx 的原生处理 - if fit == 'stretch': - slide.shapes.add_picture(str(img_path), x, y, width=w, height=h) - else: - # 其他模式:使用 Pillow 处理图片 - # 打开图片 - with Image.open(img_path) as img: - # 转换 box 尺寸为像素 - box_width_px = int(inches_to_pixels(elem.box[2], self.dpi)) - box_height_px = int(inches_to_pixels(elem.box[3], self.dpi)) - box_size = (box_width_px, box_height_px) - - # 应用 fit 模式 - processed_img = apply_fit_mode( - img, box_size, fit, elem.background - ) - - # 保存处理后的图片到临时文件 - with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp: - processed_img.save(tmp.name, 'PNG') - tmp_path = tmp.name - - try: - # 添加处理后的图片到幻灯片 - slide.shapes.add_picture(tmp_path, x, y, width=w, height=h) - finally: - # 清理临时文件 - Path(tmp_path).unlink(missing_ok=True) - - except Exception as e: - raise YAMLError(f"添加图片失败: {img_path}: {str(e)}") + # 直接添加图片到幻灯片 + slide.shapes.add_picture(str(img_path), x, y, width=w, height=h) def _render_shape(self, slide, elem: ShapeElement): """ diff --git a/tests/e2e/test_background_colors.yaml b/tests/e2e/test_background_colors.yaml deleted file mode 100644 index 1e498c7..0000000 --- a/tests/e2e/test_background_colors.yaml +++ /dev/null @@ -1,35 +0,0 @@ -metadata: - title: "背景色测试" - size: "16:9" - dpi: 96 - -slides: - - elements: - - type: text - content: "背景色支持测试" - box: [1, 0.5, 8, 0.8] - font: - size: 28 - bold: true - align: center - - # 白色背景 - - type: image - src: "../fixtures/images/test_image.png" - box: [1, 1.5, 2.5, 2] - fit: contain - background: "#ffffff" - - # 灰色背景 - - type: image - src: "../fixtures/images/test_image.png" - box: [4, 1.5, 2.5, 2] - fit: contain - background: "#cccccc" - - # 彩色背景 - - type: image - src: "../fixtures/images/test_image.png" - box: [7, 1.5, 2.5, 2] - fit: center - background: "#e3f2fd" diff --git a/tests/e2e/test_dpi_config.yaml b/tests/e2e/test_dpi_config.yaml deleted file mode 100644 index 837c3db..0000000 --- a/tests/e2e/test_dpi_config.yaml +++ /dev/null @@ -1,20 +0,0 @@ -metadata: - title: "DPI 配置测试" - size: "16:9" - dpi: 150 - -slides: - - elements: - - type: text - content: "DPI: 150" - box: [1, 0.5, 8, 0.8] - font: - size: 28 - bold: true - align: center - - - type: image - src: "../fixtures/images/test_image.png" - box: [2, 1.5, 6, 3] - fit: contain - background: "#f5f5f5" diff --git a/tests/e2e/test_fit_modes.yaml b/tests/e2e/test_fit_modes.yaml deleted file mode 100644 index db7594d..0000000 --- a/tests/e2e/test_fit_modes.yaml +++ /dev/null @@ -1,40 +0,0 @@ -metadata: - title: "图片适配模式端到端测试" - size: "16:9" - dpi: 96 - -slides: - # 测试所有 fit 模式 - - elements: - - type: text - content: "图片适配模式测试" - box: [1, 0.5, 8, 0.8] - font: - size: 32 - bold: true - align: center - - # stretch 模式(默认) - - type: image - src: "../fixtures/images/test_image.png" - box: [0.5, 1.5, 2, 1.5] - - # contain 模式 - - type: image - src: "../fixtures/images/test_image.png" - box: [3, 1.5, 2, 1.5] - fit: contain - background: "#f0f0f0" - - # cover 模式 - - type: image - src: "../fixtures/images/test_image.png" - box: [5.5, 1.5, 2, 1.5] - fit: cover - - # center 模式 - - type: image - src: "../fixtures/images/test_image.png" - box: [8, 1.5, 2, 1.5] - fit: center - background: "#ffffff" diff --git a/tests/e2e/test_invalid_params.yaml b/tests/e2e/test_invalid_params.yaml deleted file mode 100644 index 46af9d7..0000000 --- a/tests/e2e/test_invalid_params.yaml +++ /dev/null @@ -1,19 +0,0 @@ -metadata: - title: "无效参数测试" - size: "16:9" - dpi: 96 - -slides: - - elements: - # 无效的 fit 值 - - type: image - src: "../fixtures/images/test_image.png" - box: [1, 1, 3, 2] - fit: invalid_mode - - # 无效的背景色 - - type: image - src: "../fixtures/images/test_image.png" - box: [5, 1, 3, 2] - fit: contain - background: "not-a-color" diff --git a/tests/fixtures/create_test_images.py b/tests/fixtures/create_test_images.py deleted file mode 100644 index 48f86fa..0000000 --- a/tests/fixtures/create_test_images.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -创建测试图片的辅助脚本 -""" - -from PIL import Image, ImageDraw -from pathlib import Path - -# 确保目录存在 -images_dir = Path(__file__).parent.parent / "fixtures" / "images" -images_dir.mkdir(exist_ok=True) - -# 创建一个简单的红色图片 -img = Image.new('RGB', (100, 100), color='red') -img.save(images_dir / "test_image.png") - -# 创建一个较大的图片 -large_img = Image.new('RGB', (800, 600), color='blue') -large_img.save(images_dir / "large_image.png") - -# 创建一个带有透明度的图片(PNG) -transparent_img = Image.new('RGBA', (100, 100), (255, 0, 0, 128)) -transparent_img.save(images_dir / "transparent_image.png") - -# 创建一个小图片 -small_img = Image.new('RGB', (50, 50), color='green') -small_img.save(images_dir / "small_image.png") - -# 创建一个带文字的图片 -text_img = Image.new('RGB', (200, 100), color='white') -draw = ImageDraw.Draw(text_img) -draw.text((10, 30), "Test Image", fill='black') -text_img.save(images_dir / "text_image.png") - -print(f"Created test images in {images_dir}") diff --git a/tests/integration/test_image_fit_modes.py b/tests/integration/test_image_fit_modes.py deleted file mode 100644 index 89e7f58..0000000 --- a/tests/integration/test_image_fit_modes.py +++ /dev/null @@ -1,304 +0,0 @@ -""" -图片适配模式集成测试 - -测试图片 fit 模式在 PPTX 和 HTML 渲染中的完整流程。 -""" - -import pytest -from pathlib import Path -from PIL import Image -from pptx import Presentation -from core.presentation import Presentation as CorePresentation -from renderers.pptx_renderer import PptxGenerator -from renderers.html_renderer import HtmlRenderer - - -@pytest.fixture -def test_image_path(tmp_path): - """创建测试图片""" - img_path = tmp_path / "test_image.png" - # 创建 200x100 的测试图片 - img = Image.new("RGB", (200, 100), color="red") - img.save(img_path) - return img_path - - -@pytest.fixture -def yaml_dir(tmp_path): - """创建临时 YAML 目录""" - return tmp_path - - -class TestPptxImageFitModes: - """测试 PPTX 渲染器的图片适配模式""" - - def test_stretch_mode(self, yaml_dir, test_image_path, tmp_path): - """测试 stretch 模式的 PPTX 渲染""" - yaml_content = f""" -metadata: - size: "16:9" - dpi: 96 - -slides: - - elements: - - type: image - src: "{test_image_path.name}" - box: [1, 1, 4, 3] -""" - yaml_path = yaml_dir / "test.yaml" - yaml_path.write_text(yaml_content) - - # 加载并渲染 - pres = CorePresentation(str(yaml_path)) - gen = PptxGenerator(size='16:9', dpi=96) - - for slide_data in pres.data.get('slides', []): - rendered = pres.render_slide(slide_data) - gen.add_slide(rendered, base_path=yaml_dir) - - # 保存并验证 - output_path = tmp_path / "stretch.pptx" - gen.save(output_path) - assert output_path.exists() - - # 验证 PPTX 可以打开 - pptx = Presentation(str(output_path)) - assert len(pptx.slides) == 1 - - def test_contain_mode_larger_image(self, yaml_dir, tmp_path): - """测试 contain 模式(图片比 box 大)""" - # 创建 400x300 的大图片 - img_path = yaml_dir / "large_image.png" - img = Image.new("RGB", (400, 300), color="blue") - img.save(img_path) - - yaml_content = f""" -metadata: - size: "16:9" - dpi: 96 - -slides: - - elements: - - type: image - src: "{img_path.name}" - box: [1, 1, 3, 2] - fit: contain -""" - yaml_path = yaml_dir / "test.yaml" - yaml_path.write_text(yaml_content) - - pres = CorePresentation(str(yaml_path)) - gen = PptxGenerator(size='16:9', dpi=96) - - for slide_data in pres.data.get('slides', []): - rendered = pres.render_slide(slide_data) - gen.add_slide(rendered, base_path=yaml_dir) - - output_path = tmp_path / "contain_large.pptx" - gen.save(output_path) - assert output_path.exists() - - def test_contain_mode_smaller_image(self, yaml_dir, tmp_path): - """测试 contain 模式(图片比 box 小)""" - # 创建 50x50 的小图片 - img_path = yaml_dir / "small_image.png" - img = Image.new("RGB", (50, 50), color="green") - img.save(img_path) - - yaml_content = f""" -metadata: - size: "16:9" - dpi: 96 - -slides: - - elements: - - type: image - src: "{img_path.name}" - box: [1, 1, 4, 3] - fit: contain -""" - yaml_path = yaml_dir / "test.yaml" - yaml_path.write_text(yaml_content) - pres = CorePresentation(str(yaml_path)) - gen = PptxGenerator(size='16:9', dpi=96) - - for slide_data in pres.data.get('slides', []): - rendered = pres.render_slide(slide_data) - gen.add_slide(rendered, base_path=yaml_dir) - - output_path = tmp_path / "contain_small.pptx" - gen.save(output_path) - assert output_path.exists() - - def test_contain_mode_with_background(self, yaml_dir, test_image_path, tmp_path): - """测试 contain 模式(带背景色)""" - yaml_content = f""" -metadata: - size: "16:9" - dpi: 96 - -slides: - - elements: - - type: image - src: "{test_image_path.name}" - box: [1, 1, 4, 3] - fit: contain - background: "#f0f0f0" -""" - yaml_path = yaml_dir / "test.yaml" - yaml_path.write_text(yaml_content) - - pres = CorePresentation(str(yaml_path)) - gen = PptxGenerator(size='16:9', dpi=96) - - for slide_data in pres.data.get('slides', []): - rendered = pres.render_slide(slide_data) - gen.add_slide(rendered, base_path=yaml_dir) - - output_path = tmp_path / "contain_bg.pptx" - gen.save(output_path) - assert output_path.exists() - - def test_cover_mode(self, yaml_dir, test_image_path, tmp_path): - """测试 cover 模式""" - yaml_content = f""" -metadata: - size: "16:9" - dpi: 96 - -slides: - - elements: - - type: image - src: "{test_image_path.name}" - box: [1, 1, 4, 3] - fit: cover -""" - yaml_path = yaml_dir / "test.yaml" - yaml_path.write_text(yaml_content) - - pres = CorePresentation(str(yaml_path)) - gen = PptxGenerator(size='16:9', dpi=96) - - for slide_data in pres.data.get('slides', []): - rendered = pres.render_slide(slide_data) - gen.add_slide(rendered, base_path=yaml_dir) - - output_path = tmp_path / "cover.pptx" - gen.save(output_path) - assert output_path.exists() - - def test_center_mode_with_background(self, yaml_dir, test_image_path, tmp_path): - """测试 center 模式(带背景色)""" - yaml_content = f""" -metadata: - size: "16:9" - dpi: 96 - -slides: - - elements: - - type: image - src: "{test_image_path.name}" - box: [1, 1, 4, 3] - fit: center - background: "#ffffff" -""" - yaml_path = yaml_dir / "test.yaml" - yaml_path.write_text(yaml_content) - - pres = CorePresentation(str(yaml_path)) - gen = PptxGenerator(size='16:9', dpi=96) - - for slide_data in pres.data.get('slides', []): - rendered = pres.render_slide(slide_data) - gen.add_slide(rendered, base_path=yaml_dir) - - output_path = tmp_path / "center_bg.pptx" - gen.save(output_path) - assert output_path.exists() - - def test_different_dpi_values(self, yaml_dir, test_image_path, tmp_path): - """测试不同 DPI 配置""" - for dpi in [72, 96, 150, 300]: - yaml_content = f""" -metadata: - size: "16:9" - dpi: {dpi} - -slides: - - elements: - - type: image - src: "{test_image_path.name}" - box: [1, 1, 4, 3] - fit: contain -""" - yaml_path = yaml_dir / f"test_dpi_{dpi}.yaml" - yaml_path.write_text(yaml_content) - - pres = CorePresentation(str(yaml_path)) - gen = PptxGenerator(size='16:9', dpi=dpi) - - for slide_data in pres.data.get('slides', []): - rendered = pres.render_slide(slide_data) - gen.add_slide(rendered, base_path=yaml_dir) - - output_path = tmp_path / f"dpi_{dpi}.pptx" - gen.save(output_path) - assert output_path.exists() - - -class TestHtmlImageFitModes: - """测试 HTML 渲染器的图片适配模式""" - - def test_html_fit_modes(self, yaml_dir, test_image_path): - """测试 HTML 渲染器的四种 fit 模式""" - for fit_mode in ['stretch', 'contain', 'cover', 'center']: - yaml_content = f""" -metadata: - size: "16:9" - dpi: 96 - -slides: - - elements: - - type: image - src: "{test_image_path.name}" - box: [1, 1, 4, 3] - fit: {fit_mode} -""" - yaml_path = yaml_dir / f"test_{fit_mode}.yaml" - yaml_path.write_text(yaml_content) - - pres = CorePresentation(str(yaml_path)) - renderer = HtmlRenderer(dpi=96) - - for slide_data in pres.data.get('slides', []): - rendered = pres.render_slide(slide_data) - html = renderer.render_slide(rendered, 0, base_path=yaml_dir) - # 验证 HTML 包含图片元素 - assert ' 0 - - def test_invalid_fit_value(self): - """测试无效的 fit 值""" - issues = validate_fit_value("invalid") - assert len(issues) > 0 - assert issues[0].level == "ERROR" - - issues = validate_fit_value("fill") - assert len(issues) > 0 - - issues = validate_fit_value("scale") - assert len(issues) > 0 - - def test_case_sensitive(self): - """测试大小写敏感""" - issues = validate_fit_value("STRETCH") - assert len(issues) > 0 - - issues = validate_fit_value("Contain") - assert len(issues) > 0 - - -class TestValidateBackgroundColor: - """测试背景色验证""" - - def test_valid_hex_colors(self): - """测试有效的十六进制颜色""" - assert validate_background_color("#ffffff") == [] - assert validate_background_color("#000000") == [] - assert validate_background_color("#ff0000") == [] - assert validate_background_color("#4a90e2") == [] - - def test_valid_short_hex_colors(self): - """测试有效的短格式十六进制颜色""" - assert validate_background_color("#fff") == [] - assert validate_background_color("#000") == [] - assert validate_background_color("#f00") == [] - - def test_none_background(self): - """测试 None 值(透明)""" - # None 会导致 TypeError,应该返回错误 - issues = validate_background_color(None) - assert len(issues) > 0 - - def test_invalid_colors(self): - """测试无效的颜色格式""" - issues = validate_background_color("white") - assert len(issues) > 0 - - issues = validate_background_color("rgb(255,255,255)") - assert len(issues) > 0 - - issues = validate_background_color("#gggggg") - assert len(issues) > 0 - - issues = validate_background_color("#ff") - assert len(issues) > 0 - - issues = validate_background_color("ffffff") - assert len(issues) > 0 - - -class TestValidateDpiValue: - """测试 DPI 值验证""" - - def test_valid_dpi_values(self): - """测试有效的 DPI 值""" - assert validate_dpi_value(72) == [] - assert validate_dpi_value(96) == [] - assert validate_dpi_value(150) == [] - assert validate_dpi_value(300) == [] - - def test_boundary_dpi_values(self): - """测试边界 DPI 值""" - # 1 和 1200 超出建议范围,会返回 WARNING - issues = validate_dpi_value(1) - assert len(issues) > 0 - assert issues[0].level == "WARNING" - - issues = validate_dpi_value(1200) - assert len(issues) > 0 - assert issues[0].level == "WARNING" - - def test_invalid_dpi_values(self): - """测试无效的 DPI 值""" - # 0 和负数会返回 WARNING - issues = validate_dpi_value(0) - assert len(issues) > 0 - - issues = validate_dpi_value(-1) - assert len(issues) > 0 - - issues = validate_dpi_value(1201) - assert len(issues) > 0 - - issues = validate_dpi_value(2000) - assert len(issues) > 0 - - def test_non_integer_dpi(self): - """测试非整数 DPI 值""" - # 浮点数 DPI 可能被接受(取决于实现) - # 字符串和 None 应该返回错误 - issues = validate_dpi_value("96") - assert len(issues) > 0 - - issues = validate_dpi_value(None) - assert len(issues) > 0 diff --git a/utils/image_utils.py b/utils/image_utils.py deleted file mode 100644 index 18b7259..0000000 --- a/utils/image_utils.py +++ /dev/null @@ -1,149 +0,0 @@ -""" -图片处理工具模块 - -提供图片适配模式处理、像素与英寸转换等功能。 -""" - -from PIL import Image, ImageOps -from typing import Tuple, Optional - - -def inches_to_pixels(inches: float, dpi: int = 96) -> float: - """ - 将英寸转换为像素 - - Args: - inches: 英寸值 - dpi: DPI(每英寸像素数),默认 96 - - Returns: - 像素值 - """ - return inches * dpi - - -def pixels_to_inches(pixels: float, dpi: int = 96) -> float: - """ - 将像素转换为英寸 - - Args: - pixels: 像素值 - dpi: DPI(每英寸像素数),默认 96 - - Returns: - 英寸值 - """ - return pixels / dpi - - -def create_canvas_with_background(size: Tuple[int, int], background_color: str) -> Image.Image: - """ - 创建带背景色的画布 - - Args: - size: 画布尺寸 (width, height) - background_color: 背景颜色(#RRGGBB 或 #RGB 格式) - - Returns: - PIL Image 对象 - """ - # 验证颜色格式 - import re - if not isinstance(background_color, str): - raise ValueError(f"无效的颜色格式: {background_color}") - pattern = r'^#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$' - if not re.match(pattern, background_color): - raise ValueError(f"无效的颜色格式: {background_color}") - - # 创建 RGB 模式的画布 - canvas = Image.new('RGB', size, background_color) - return canvas - - -def apply_fit_mode( - img: Image.Image, - box_size: Tuple[int, int], - fit: Optional[str] = None, - background: Optional[str] = None -) -> Image.Image: - """ - 应用图片适配模式 - - Args: - img: PIL Image 对象 - box_size: 目标尺寸 (width, height) 像素 - fit: 适配模式(stretch, contain, cover, center),默认 stretch - background: 背景颜色(#RRGGBB 或 #RGB 格式),仅对 contain 和 center 模式有效 - - Returns: - 处理后的 PIL Image 对象 - """ - if fit is None or fit == 'stretch': - # stretch 模式:直接拉伸到目标尺寸 - return img.resize(box_size, Image.Resampling.LANCZOS) - - elif fit == 'contain': - # contain 模式:保持宽高比,完整显示在 box 内 - # 如果图片比 box 小,保持原始尺寸;如果比 box 大,等比缩小 - img_width, img_height = img.size - box_width, box_height = box_size - - # 检查图片是否需要缩小 - if img_width <= box_width and img_height <= box_height: - # 图片比 box 小,保持原始尺寸 - result = img - else: - # 图片比 box 大,使用 contain 缩小 - result = ImageOps.contain(img, box_size, Image.Resampling.LANCZOS) - - # 如果指定了背景色,创建画布并居中粘贴 - if background: - canvas = create_canvas_with_background(box_size, background) - # 计算居中位置 - offset_x = (box_size[0] - result.width) // 2 - offset_y = (box_size[1] - result.height) // 2 - canvas.paste(result, (offset_x, offset_y)) - return canvas - - return result - - elif fit == 'cover': - # cover 模式:保持宽高比,填满 box,裁剪超出部分 - result = ImageOps.cover(img, box_size, Image.Resampling.LANCZOS) - # ImageOps.cover 可能返回比 box_size 大的图片,需要裁剪到精确尺寸 - if result.size != box_size: - # 从中心裁剪到目标尺寸 - left = (result.width - box_size[0]) // 2 - top = (result.height - box_size[1]) // 2 - result = result.crop((left, top, left + box_size[0], top + box_size[1])) - return result - - elif fit == 'center': - # center 模式:不缩放,居中显示,超出部分裁剪 - img_width, img_height = img.size - box_width, box_height = box_size - - # 如果图片比 box 大,需要裁剪 - if img_width > box_width or img_height > box_height: - # 计算裁剪区域(从中心裁剪) - left = max(0, (img_width - box_width) // 2) - top = max(0, (img_height - box_height) // 2) - right = min(img_width, left + box_width) - bottom = min(img_height, top + box_height) - result = img.crop((left, top, right, bottom)) - else: - result = img - - # 如果指定了背景色,创建画布并居中粘贴 - if background: - canvas = create_canvas_with_background(box_size, background) - # 计算居中位置 - offset_x = (box_size[0] - result.width) // 2 - offset_y = (box_size[1] - result.height) // 2 - canvas.paste(result, (offset_x, offset_y)) - return canvas - - return result - - else: - raise ValueError(f"不支持的 fit 模式: {fit}") diff --git a/validators/image_config.py b/validators/image_config.py deleted file mode 100644 index 2bc8293..0000000 --- a/validators/image_config.py +++ /dev/null @@ -1,101 +0,0 @@ -""" -图片配置验证器 - -验证图片元素的 fit、background、dpi 等参数。 -""" - -from typing import List -from validators.result import ValidationIssue -import re - - -def validate_fit_value(fit: str) -> List[ValidationIssue]: - """ - 验证 fit 参数值 - - Args: - fit: fit 参数值 - - Returns: - 验证问题列表 - """ - issues = [] - valid_fits = ['stretch', 'contain', 'cover', 'center'] - - if fit not in valid_fits: - issues.append(ValidationIssue( - level="ERROR", - message=f"无效的 fit 值: {fit} (支持: {', '.join(valid_fits)})", - location="", - code="INVALID_FIT_VALUE" - )) - - return issues - - -def validate_background_color(color: str) -> List[ValidationIssue]: - """ - 验证背景颜色格式 - - Args: - color: 颜色字符串 - - Returns: - 验证问题列表 - """ - issues = [] - - # 检查类型 - if not isinstance(color, str): - issues.append(ValidationIssue( - level="ERROR", - message=f"无效的背景颜色格式: {color} (应为 #RRGGBB 或 #RGB)", - location="", - code="INVALID_COLOR_FORMAT" - )) - return issues - - pattern = r'^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$' - - if not re.match(pattern, color): - issues.append(ValidationIssue( - level="ERROR", - message=f"无效的背景颜色格式: {color} (应为 #RRGGBB 或 #RGB)", - location="", - code="INVALID_COLOR_FORMAT" - )) - - return issues - - -def validate_dpi_value(dpi: int) -> List[ValidationIssue]: - """ - 验证 DPI 值 - - Args: - dpi: DPI 值 - - Returns: - 验证问题列表 - """ - issues = [] - - # 检查类型 - if not isinstance(dpi, int): - issues.append(ValidationIssue( - level="ERROR", - message=f"DPI 值必须是整数: {dpi}", - location="", - code="INVALID_DPI_TYPE" - )) - return issues - - if dpi < 72 or dpi > 300: - issues.append(ValidationIssue( - level="WARNING", - message=f"DPI 值可能不合适: {dpi} (建议范围: 72-300)", - location="", - code="DPI_OUT_OF_RANGE" - )) - - return issues diff --git a/validators/validator.py b/validators/validator.py index 67183f4..1a2a09e 100644 --- a/validators/validator.py +++ b/validators/validator.py @@ -9,7 +9,6 @@ from loaders.yaml_loader import load_yaml_file, validate_presentation_yaml, YAML from validators.result import ValidationResult, ValidationIssue from validators.geometry import GeometryValidator from validators.resource import ResourceValidator -from validators.image_config import validate_dpi_value from core.elements import create_element @@ -71,12 +70,6 @@ class Validator: size_str = data.get("metadata", {}).get("size", "16:9") slide_width, slide_height = self.SLIDE_SIZES.get(size_str, (10, 5.625)) - # 验证 DPI 配置 - dpi = data.get("metadata", {}).get("dpi") - if dpi is not None: - dpi_issues = validate_dpi_value(dpi) - self._categorize_issues(dpi_issues, errors, warnings, infos) - # 初始化子验证器 geometry_validator = GeometryValidator(slide_width, slide_height) resource_validator = ResourceValidator( diff --git a/yaml2pptx.py b/yaml2pptx.py index cbdc89a..28784bd 100644 --- a/yaml2pptx.py +++ b/yaml2pptx.py @@ -194,7 +194,7 @@ def handle_convert(args): # 2. 创建 PPTX 生成器 log_info(f"创建演示文稿 ({pres.size})...") - generator = PptxGenerator(pres.size, pres.dpi) + generator = PptxGenerator(pres.size) # 3. 渲染所有幻灯片 slides_data = pres.data.get('slides', [])