1
0

feat: 移除图片适配模式功能

移除图片 fit 和 background 参数支持,简化图片渲染逻辑。系统恢复到直接使用 python-pptx 原生图片添加功能,图片将被拉伸到指定尺寸。

变更内容:
- 移除 ImageElement 的 fit 和 background 字段
- 移除 metadata.dpi 配置
- 删除 utils/image_utils.py 图片处理工具模块
- 删除 validators/image_config.py 验证器
- 简化 PPTX 和 HTML 渲染器的图片处理逻辑
- HTML 渲染器使用硬编码 DPI=96(Web 标准)
- 删除相关测试文件(单元测试、集成测试、e2e 测试)
- 更新规格文档和用户文档
- 保留 Pillow 依赖用于未来可能的图片处理需求

影响:
- 删除 11 个文件
- 修改 10 个文件
- 净减少 1558 行代码
- 所有 402 个测试通过

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 14:23:12 +08:00
parent 2fd8bc1b4a
commit f34405be36
31 changed files with 494 additions and 1556 deletions

View File

@@ -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" # 白色背景
```
### 形状元素

View File

@@ -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` - 协调所有子验证器
- 集成元素级验证、几何验证、资源验证

View File

@@ -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

View File

@@ -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") # 可选的描述字段
# 验证尺寸值

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-04

View File

@@ -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
无。移除操作明确清晰,无未决问题。

View File

@@ -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=96Web 标准,无需用户配置)
## 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`: 移除图片处理架构说明

View File

@@ -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** 系统基于演示文稿文件所在目录解析相对路径

View File

@@ -0,0 +1,86 @@
# HTML Rendering Delta Spec
## REMOVED Requirements
### Requirement: 系统必须渲染图片元素 (部分移除)
**Reason**: 移除图片适配模式功能fit 和 background 参数),简化 HTML 图片渲染
**Migration**: 移除图片元素的 `fit``background` 参数定义HTML 渲染器将输出简单的 `<img>` 标签
#### 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 `<img>` 标签,使用固定 DPI 进行尺寸转换。
#### Scenario: 渲染本地图片
- **WHEN** 元素定义为 `{type: image, src: "images/logo.png", box: [2, 3, 4, 3]}`
- **THEN** 系统生成 `<img>` 标签src 为图片的文件路径,位置为 (192px, 288px),尺寸为 384x288 像素
#### Scenario: 处理相对路径
- **WHEN** 图片 src 使用相对路径 `"assets/logo.png"`
- **THEN** 系统基于 YAML 文件所在目录解析相对路径
#### Scenario: 图片不存在时显示占位符
- **WHEN** 图片文件不存在
- **THEN** 系统显示占位符或错误提示,而不是崩溃

View File

@@ -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` 测试预览功能

View File

@@ -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` 参数,否则验证失败。

View File

@@ -133,45 +133,12 @@ HTML Rendering 系统负责将 YAML 中定义的各类元素(文本、图片
### Requirement: 系统必须渲染图片元素
系统 SHALL 将 YAML 中的图片元素转换为 HTML `<img>` 标签,支持 `fit``background` 参数,使用 CSS object-fit 实现适配模式
系统 SHALL 将 YAML 中的图片元素转换为 HTML `<img>` 标签,使用固定 DPI 进行尺寸转换
#### Scenario: 渲染本地图片
- **WHEN** 元素定义为 `{type: image, src: "images/logo.png", box: [2, 3, 4, 3]}`
- **THEN** 系统生成 `<img>` 标签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` 参数,否则验证失败。

View File

@@ -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** 裁剪操作不损失图片质量

View File

@@ -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', [])):

View File

@@ -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'''<div class="element image-container" style="{container_style}">
<img class="image-element" src="file://{img_path.absolute()}" style="{img_style}">
</div>'''
else:
return f'<img class="element image-element" src="file://{img_path.absolute()}" style="{style}">'
return f'<img class="element image-element" src="file://{img_path.absolute()}" style="{style}">'

View File

@@ -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):
"""

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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}")

View File

@@ -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 '<img' in html or 'background-image' in html
def test_html_background_color(self, yaml_dir, test_image_path):
"""测试 HTML 渲染器的背景色支持"""
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))
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 '#f0f0f0' in html or 'background-color' in html

View File

@@ -89,19 +89,6 @@ class TestImageElement:
assert elem.type == 'image'
assert elem.src == "test.png"
assert elem.box == [1, 1, 4, 3]
assert elem.fit is None
assert elem.background is None
def test_create_with_fit_and_background(self):
"""测试创建带 fit 和 background 的 ImageElement"""
elem = ImageElement(
src="test.png",
box=[1, 1, 4, 3],
fit="contain",
background="#ffffff"
)
assert elem.fit == "contain"
assert elem.background == "#ffffff"
def test_empty_src_raises_error(self):
"""测试空 src 会引发错误"""

View File

@@ -1,120 +0,0 @@
"""
单元测试:图片处理工具函数
测试 utils/image_utils.py 中的图片处理函数。
"""
import pytest
from PIL import Image
from utils.image_utils import (
inches_to_pixels,
pixels_to_inches,
apply_fit_mode,
create_canvas_with_background,
)
class TestUnitConversion:
"""测试单位转换函数"""
def test_inches_to_pixels_default_dpi(self):
"""测试英寸转像素(默认 96 DPI"""
assert inches_to_pixels(1) == 96
assert inches_to_pixels(2) == 192
assert inches_to_pixels(0.5) == 48
def test_inches_to_pixels_custom_dpi(self):
"""测试英寸转像素(自定义 DPI"""
assert inches_to_pixels(1, dpi=72) == 72
assert inches_to_pixels(2, dpi=150) == 300
assert inches_to_pixels(0.5, dpi=300) == 150
def test_pixels_to_inches_default_dpi(self):
"""测试像素转英寸(默认 96 DPI"""
assert pixels_to_inches(96) == 1.0
assert pixels_to_inches(192) == 2.0
assert pixels_to_inches(48) == 0.5
def test_pixels_to_inches_custom_dpi(self):
"""测试像素转英寸(自定义 DPI"""
assert pixels_to_inches(72, dpi=72) == 1.0
assert pixels_to_inches(300, dpi=150) == 2.0
class TestApplyFitMode:
"""测试图片适配模式函数"""
@pytest.fixture
def test_image(self):
"""创建测试图片100x100"""
return Image.new("RGB", (100, 100), color="red")
def test_stretch_mode(self, test_image):
"""测试 stretch 模式"""
result = apply_fit_mode(test_image, (200, 150), "stretch")
assert result.size == (200, 150)
def test_contain_mode_wider_box(self, test_image):
"""测试 contain 模式box 更宽)"""
result = apply_fit_mode(test_image, (200, 100), "contain")
# 图片应该适配到高度,宽度保持比例
assert result.size == (100, 100)
def test_contain_mode_taller_box(self, test_image):
"""测试 contain 模式box 更高)"""
result = apply_fit_mode(test_image, (100, 200), "contain")
# 图片应该适配到宽度,高度保持比例
assert result.size == (100, 100)
def test_cover_mode_wider_box(self, test_image):
"""测试 cover 模式box 更宽)"""
result = apply_fit_mode(test_image, (200, 100), "cover")
# 图片应该覆盖整个 box
assert result.size == (200, 100)
def test_cover_mode_taller_box(self, test_image):
"""测试 cover 模式box 更高)"""
result = apply_fit_mode(test_image, (100, 200), "cover")
# 图片应该覆盖整个 box
assert result.size == (100, 200)
def test_center_mode(self, test_image):
"""测试 center 模式"""
result = apply_fit_mode(test_image, (200, 150), "center")
# center 模式保持原始尺寸
assert result.size == (100, 100)
def test_invalid_mode(self, test_image):
"""测试无效的 fit 模式"""
with pytest.raises(ValueError, match="不支持的 fit 模式"):
apply_fit_mode(test_image, (200, 150), "invalid")
class TestCreateCanvas:
"""测试画布创建函数"""
def test_create_canvas_with_white_background(self):
"""测试创建白色背景画布"""
canvas = create_canvas_with_background((200, 150), "#ffffff")
assert canvas.size == (200, 150)
assert canvas.mode == "RGB"
# 检查中心像素是白色
assert canvas.getpixel((100, 75)) == (255, 255, 255)
def test_create_canvas_with_colored_background(self):
"""测试创建彩色背景画布"""
canvas = create_canvas_with_background((200, 150), "#ff0000")
assert canvas.size == (200, 150)
# 检查中心像素是红色
assert canvas.getpixel((100, 75)) == (255, 0, 0)
def test_create_canvas_with_short_hex(self):
"""测试创建画布(短格式十六进制颜色)"""
canvas = create_canvas_with_background((100, 100), "#f00")
# 检查中心像素是红色
assert canvas.getpixel((50, 50)) == (255, 0, 0)
def test_create_canvas_with_invalid_color(self):
"""测试无效颜色格式"""
with pytest.raises(ValueError, match="无效的颜色格式"):
create_canvas_with_background((100, 100), "invalid")

View File

@@ -6,21 +6,18 @@ HTML 渲染器单元测试
import pytest
from pathlib import Path
from renderers.html_renderer import HtmlRenderer, DPI
from renderers.html_renderer import HtmlRenderer
from core.elements import TextElement, ImageElement, ShapeElement, TableElement
class TestHtmlRenderer:
"""HtmlRenderer 测试类"""
def test_renderer_has_dpi_constant(self):
"""测试渲染器有 DPI 常量"""
assert DPI == 96
def test_init_renderer(self):
"""测试创建渲染器"""
renderer = HtmlRenderer()
assert renderer is not None
assert renderer.dpi == 96 # 硬编码的 DPI
class TestRenderText:

View File

@@ -5,7 +5,7 @@
"""
import pytest
from utils import hex_to_rgb, validate_color
from utils import hex_to_rgb
class TestHexToRgb:
@@ -52,53 +52,3 @@ class TestHexToRgb:
"""测试大小写混合"""
assert hex_to_rgb("#AbCdEf") == (171, 205, 239)
assert hex_to_rgb("#aBcDeF") == (171, 205, 239)
class TestValidateColor:
"""validate_color 函数测试类"""
def test_valid_full_hex_colors(self):
"""测试有效的完整十六进制颜色"""
assert validate_color("#ffffff") is True
assert validate_color("#000000") is True
assert validate_color("#4a90e2") is True
assert validate_color("#FF0000") is True
assert validate_color("#ABCDEF") is True
def test_valid_short_hex_colors(self):
"""测试有效的短格式十六进制颜色"""
assert validate_color("#fff") is True
assert validate_color("#000") is True
assert validate_color("#abc") is True
assert validate_color("#ABC") is True
def test_without_hash_sign(self):
"""测试没有 # 号"""
assert validate_color("ffffff") is False
assert validate_color("fff") is False
def test_invalid_length(self):
"""测试无效长度"""
assert validate_color("#ff") is False
assert validate_color("#ffff") is False
assert validate_color("#fffff") is False
assert validate_color("#fffffff") is False
def test_invalid_characters(self):
"""测试无效字符"""
assert validate_color("#gggggg") is False
assert validate_color("#xyz") is False
assert validate_color("red") is False
assert validate_color("rgb(255,0,0)") is False
def test_non_string_input(self):
"""测试非字符串输入"""
assert validate_color(123) is False
assert validate_color(None) is False
assert validate_color([]) is False
assert validate_color({"color": "#fff"}) is False
def test_empty_string(self):
"""测试空字符串"""
assert validate_color("") is False
assert validate_color("#") is False

View File

@@ -1,136 +0,0 @@
"""
单元测试:图片配置验证器
测试 validators/image_config.py 中的验证函数。
"""
import pytest
from validators.image_config import (
validate_fit_value,
validate_background_color,
validate_dpi_value,
)
class TestValidateFitValue:
"""测试 fit 值验证"""
def test_valid_fit_values(self):
"""测试有效的 fit 值"""
assert validate_fit_value("stretch") == []
assert validate_fit_value("contain") == []
assert validate_fit_value("cover") == []
assert validate_fit_value("center") == []
def test_none_fit_value(self):
"""测试 None 值(默认)"""
# None 是无效的,应该返回错误
issues = validate_fit_value(None)
assert len(issues) > 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

View File

@@ -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}")

View File

@@ -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

View File

@@ -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(

View File

@@ -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', [])