docs: P10 图片处理文档同步 — iter-22 迭代记录、SKILL.md 第十五章(可复用模式/设计决策/踩坑总结)、project_memory P10 约定;与 8e3ddc5 代码提交一并推送
CI / Lint, Typecheck & Test (push) Has been cancelled
CI / Lint, Typecheck & Test (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
# 迭代 22 — P10 图片处理(imagetool + image_pipeline)
|
||||
|
||||
## 本轮目标
|
||||
|
||||
延续 P9 功能扩展后,补齐 PyFlowX 在图片处理场景的能力,覆盖三个方向:
|
||||
|
||||
1. **P10.1 基础操作子命令**:`imagetool` 工具的 7 个基础操作子命令
|
||||
(resize/crop/rotate/flip/convert/watermark/compress)。
|
||||
2. **P10.2 元数据与信息子命令**:4 个元数据子命令
|
||||
(info/exif/histogram/colors)。
|
||||
3. **P10.3 批量 DAG 编排**:`image_pipeline` 链式 DAG 构造器,
|
||||
将多步图片操作封装为 `Graph`,前一步输出作为后一步输入。
|
||||
|
||||
工具形态:单一 `imagetool` + `office` extra(Pillow 通过 `pyflowx[office]` 安装),
|
||||
不破坏项目零运行时依赖原则。
|
||||
|
||||
## 改动文件清单
|
||||
|
||||
### 新增
|
||||
|
||||
- `src/pyflowx/ops/files/imagetool.py` — 11 个 `@px.tool("imagetool", ...)`
|
||||
子命令 + `_save_image`/`_load_font`/`_resolve_position`/`_print_exif`/
|
||||
`_apply_exif_modifications`/`_apply_single_exif_set`/`_save_exif`/
|
||||
`_print_histogram_buckets` 辅助函数(520 行)。
|
||||
- `src/pyflowx/imaging.py` — `image_pipeline()` 链式 DAG 构造器 +
|
||||
`_make_step_fn` 闭包工厂(130 行)。
|
||||
- `tests/cli/test_imagetool.py` — 25 用例覆盖 11 子命令 + `_require_pil`
|
||||
守卫(316 行)。
|
||||
- `tests/test_imaging.py` — 9 用例覆盖 DAG 拓扑与执行(158 行)。
|
||||
|
||||
### 修改
|
||||
|
||||
- `src/pyflowx/__init__.py` — 导出 `image_pipeline`,加入 `__all__`。
|
||||
- `src/pyflowx/cli/pf.py` — `_TOOL_MODULES` 注册
|
||||
`"imagetool": "pyflowx.ops.files.imagetool"`;`_TOOL_ALIASES` 添加
|
||||
`"image"`/`"imagetool"`/`"img"` 三个别名。
|
||||
|
||||
## 关键决策与依据
|
||||
|
||||
### 1. 单一 `imagetool` + office extra
|
||||
|
||||
不拆分为 `imageops`/`imagemeta` 等多工具,所有图片操作集中在
|
||||
`ops/files/imagetool.py`。Pillow 通过 `pyproject.toml` 的 `[office]`
|
||||
extra 安装(`pillow>=10.4.0`),运行时 try/except 导入 + `HAS_PIL` 标志,
|
||||
未安装时 `_require_pil()` 打印 `pip install pyflowx[office]` 提示而非崩溃。
|
||||
|
||||
依据:项目零运行时依赖原则不可破坏;pdftool 已有相同模式
|
||||
(`pypdf` 通过 `[office]` extra)。
|
||||
|
||||
### 2. `_save_image` 统一保存逻辑
|
||||
|
||||
所有需要保存图片的子命令(resize/crop/rotate/flip/convert/watermark/compress)
|
||||
委托 `_save_image(img, output, fmt, quality)` 统一处理:
|
||||
|
||||
- **JPG → JPEG 规范化**:`save_fmt == "JPG"` 时改写为 `"JPEG"`,
|
||||
否则 Pillow `img.save(format="JPG")` 抛 `KeyError: 'JPG'`。
|
||||
- **JPEG 强制 RGB 转换**:JPEG 不支持 alpha 通道,保存前
|
||||
`img.convert("RGB")` 去 alpha,避免 `cannot write mode RGBA as JPEG`。
|
||||
- **JPEG/WEBP 应用 quality**:其他格式(PNG/BMP/GIF 等)忽略 quality。
|
||||
|
||||
依据:避免每个子命令重复处理格式规范化与 alpha 通道问题。
|
||||
|
||||
### 3. EXIF 用 `exif.tobytes()` 而非 `bytes(exif)`
|
||||
|
||||
Pillow 10+ 的 `Image.Exif` 对象,`bytes(exif)` 调用抛
|
||||
`ValueError: bytes must be in range(0, 256)`。必须用 `exif.tobytes()`
|
||||
序列化为字节串后再传给 `img.save(exif=exif_bytes)`。
|
||||
|
||||
`_save_exif` 辅助函数封装此逻辑:
|
||||
```python
|
||||
exif_bytes = exif.tobytes() if exif else b""
|
||||
img.save(output_path, exif=exif_bytes)
|
||||
```
|
||||
|
||||
### 4. `quantize(colors=N)` 实际色数可能 < N
|
||||
|
||||
`Image.quantize(colors=count)` 对简单图片(如纯色)可能返回少于
|
||||
请求色数的调色板。直接按 `count` 遍历会 `IndexError: list index out of range`。
|
||||
|
||||
修复:用 `actual_count = len(palette) // 3` 动态计算实际色数,
|
||||
`min(count, actual_count)` 防越界:
|
||||
```python
|
||||
actual_count = len(palette) // 3
|
||||
print(f"主色调 (前 {min(count, actual_count)} 色):")
|
||||
for i in range(min(count, actual_count)):
|
||||
...
|
||||
```
|
||||
|
||||
### 5. Pillow 10+ Transpose 枚举迁移
|
||||
|
||||
`Image.FLIP_LEFT_RIGHT`(模块级常量)在 Pillow 10+ 已弃用,
|
||||
pyrefly 无法解析。必须用 `Image.Transpose.FLIP_LEFT_RIGHT`
|
||||
(枚举形式)。
|
||||
|
||||
`image_flip` 中根据 direction 选择枚举:
|
||||
```python
|
||||
method = Image.Transpose.FLIP_LEFT_RIGHT if direction == "horizontal" else Image.Transpose.FLIP_TOP_BOTTOM
|
||||
flipped = img.transpose(method)
|
||||
```
|
||||
|
||||
### 6. `_make_step_fn` 闭包工厂避免循环变量捕获
|
||||
|
||||
`image_pipeline` 中若直接在 for 循环内定义闭包:
|
||||
```python
|
||||
for i, (op_name, params) in enumerate(steps):
|
||||
|
||||
def _run():
|
||||
base_fn(input_path, output_path, **params) # 陷阱!
|
||||
|
||||
...
|
||||
```
|
||||
所有任务的 `_run` 会捕获**最后一次循环**的 `input_path`/`output_path`/`params`,
|
||||
导致所有步骤操作同一文件。
|
||||
|
||||
修复:提取为独立函数 `_make_step_fn(base_fn, input_path, output_path, params)`,
|
||||
利用每次调用的独立作用域正确捕获:
|
||||
```python
|
||||
def _make_step_fn(base_fn, input_path, output_path, params):
|
||||
def _run() -> None:
|
||||
base_fn(input_path, output_path, **params)
|
||||
|
||||
return _run
|
||||
```
|
||||
每次循环调用 `_make_step_fn(...)` 创建新作用域,闭包绑定该次调用的参数。
|
||||
|
||||
### 7. PLR0912 分支数控制
|
||||
|
||||
`image_exif` 函数最初 13 分支(show/clear/set 各分支 + 错误处理)超过
|
||||
ruff `PLR0912` 限制(≤12)。提取 4 个辅助函数降至合规:
|
||||
|
||||
- `_print_exif(exif)` — 打印 EXIF 标签
|
||||
- `_apply_exif_modifications(exif, set_items, clear)` — 应用 clear + set
|
||||
- `_apply_single_exif_set(exif, item)` — 解析单个 KEY=VALUE
|
||||
- `_save_exif(img, exif, output_path)` — 保存图片与 EXIF
|
||||
|
||||
## 验证结果
|
||||
|
||||
```
|
||||
ruff check . → All checks passed!
|
||||
ruff format --check → 131 files already formatted
|
||||
pyrefly check . → 0 errors (68 suppressed)
|
||||
pytest --cov=pyflowx → 1560 passed in 11.72s
|
||||
coverage → 97.15%(branch;门槛 95%)
|
||||
```
|
||||
|
||||
各模块覆盖率(P10 新增部分):
|
||||
- `imagetool.py` 88%(213 stmts, 19 miss — 主要是 ImportError 回退分支
|
||||
与少量错误路径;总覆盖率 97.15% 不受影响)
|
||||
- `imaging.py` 100%(`image_pipeline` 拓扑/执行/convert 扩展名/naming 模板
|
||||
全覆盖)
|
||||
|
||||
CLI 注册验证:
|
||||
- `imagetool` 在 `_TOOL_MODULES`
|
||||
- `img`/`image`/`imagetool` 三个别名在 `_TOOL_ALIASES`
|
||||
- `tests/cli/test_tool_modules.py` 自动覆盖新工具注册
|
||||
|
||||
## 遗留事项
|
||||
|
||||
- **P11 候选方向**(用户已确认为后续阶段输入,不在 P10 范围内):
|
||||
- OCR 文字识别(需 `pytesseract` 新依赖,违反零依赖原则,需用户确认)
|
||||
- 滤镜与增强(模糊/锐化/边缘检测/浮雕/去噪,纯 Pillow 可实现)
|
||||
- 批量文件夹处理(遍历目录批量应用同一操作,生成缩略图网格)
|
||||
- 图片拼接与 GIF(多图水平/垂直拼接,GIF 帧拆分与合成)
|
||||
- **`imagetool.py` 覆盖率 88%**:ImportError 回退分支(`HAS_PIL = False`)
|
||||
在已安装 Pillow 的环境中无法触发,属可接受缺口。
|
||||
- **`image_pipeline` 仅支持链式 DAG**:当前实现是单链(每步依赖前一步),
|
||||
不支持分叉/汇合(如同一源图应用多操作后合并)。如需分叉,可扩展
|
||||
`steps` 为 DAG 描述结构。
|
||||
|
||||
## 归档说明
|
||||
|
||||
iter-22 暂不归档(前 5 次 iter-16~20 已归档至
|
||||
`.trae/skills/pyflowx-development/SKILL.md`,iter-21/22 保留在 docs/)。
|
||||
下次清理窗口为 iter-26 前夕,届时 iter-21/22 一并归档。
|
||||
@@ -0,0 +1,167 @@
|
||||
# P10 图片处理收尾计划(文档同步 + 提交)
|
||||
|
||||
## Context
|
||||
|
||||
P10 图片处理代码已完整实现并通过全部门禁,但**尚未提交**,且过程文档、SKILL 行为同步、project_memory 约定记录三项收尾工作未完成。本计划聚焦于把 P10 工作闭环:编写 iter-22 迭代记录、同步 SKILL.md 行为变更、追加 project_memory 约定、git commit + push。
|
||||
|
||||
用户已确认本次计划范围 = 仅收尾 P10(不含 P11 扩展)。P11 高级功能(OCR/滤镜/批量文件夹/拼接GIF)作为后续阶段输入参考,不在本计划内。
|
||||
|
||||
## 当前状态分析
|
||||
|
||||
### 已完成(未提交)
|
||||
|
||||
```
|
||||
git status --short
|
||||
M src/pyflowx/__init__.py # 导出 image_pipeline
|
||||
M src/pyflowx/cli/pf.py # 注册 imagetool + 别名
|
||||
?? src/pyflowx/imaging.py # image_pipeline DAG 构造器 (130 行)
|
||||
?? src/pyflowx/ops/files/imagetool.py # 11 子命令 (520 行)
|
||||
?? tests/cli/test_imagetool.py # 25 测试 (316 行)
|
||||
?? tests/test_imaging.py # 9 测试 (158 行)
|
||||
?? .trae/documents/ # 本计划文件所在目录
|
||||
```
|
||||
|
||||
### 门禁状态(已验证通过)
|
||||
|
||||
- ruff check / format → 全绿
|
||||
- pyrefly check → 0 错误(68 suppressed)
|
||||
- pytest → 1560 passed
|
||||
- coverage → 97.15%(branch;门槛 95%)
|
||||
- CLI 注册 → `imagetool` 在 `_TOOL_MODULES`,`img`/`image`/`imagetool` 在 `_TOOL_ALIASES`
|
||||
|
||||
### 分支跟踪
|
||||
|
||||
- 当前分支:`develop`
|
||||
- 跟踪远程:`origin/develop`(push 允许,无需询问)
|
||||
|
||||
## 实现步骤
|
||||
|
||||
### 步骤 1:创建 iter-22 迭代记录
|
||||
|
||||
**新建 `.trae/docs/iter-22-image-processing.md`**,参照 [iter-21-p9-feature-extension.md](file:///home/zhou/pyflowx/.trae/docs/iter-21-p9-feature-extension.md) 的结构:
|
||||
|
||||
1. **本轮目标** — P10 三方向:基础操作 7 子命令 / 元数据 4 子命令 / `image_pipeline` DAG 构造器
|
||||
2. **改动文件清单** — 新增 4 文件 + 修改 2 文件(列出每个文件的职责)
|
||||
3. **关键决策与依据**(6 条,基于实际实现踩坑):
|
||||
- **JPG → JPEG 规范化**:`_save_image` 中 `save_fmt == "JPG"` 时改写为 `"JPEG"`,否则 Pillow `img.save(format="JPG")` 抛 `KeyError`
|
||||
- **JPEG 强制 RGB 转换**:JPEG 不支持 alpha 通道,保存前 `img.convert("RGB")` 去 alpha
|
||||
- **EXIF 用 `exif.tobytes()` 而非 `bytes(exif)`**:Pillow 10+ 的 `Image.Exif` 对象 `bytes()` 调用抛 `ValueError: bytes must be in range(0, 256)`,必须用 `tobytes()`
|
||||
- **`quantize(colors=N)` 实际色数可能 < N**:简单图片量化后调色板可能少于请求色数,用 `actual_count = len(palette) // 3` 动态计算并 `min(count, actual_count)` 防越界
|
||||
- **Pillow 10+ Transpose 枚举**:`Image.FLIP_LEFT_RIGHT`(旧)已弃用,用 `Image.Transpose.FLIP_LEFT_RIGHT`(新)
|
||||
- **`_make_step_fn` 闭包工厂**:`image_pipeline` 中若直接在循环内定义闭包,所有任务会捕获最后一次循环的 `input_path`/`output_path`/`params`;提取为独立函数 `_make_step_fn(base_fn, input_path, output_path, params)` 利用每次调用的独立作用域正确捕获
|
||||
4. **验证结果** — 贴上门禁输出(ruff/pyrefly/pytest 1560 passed/coverage 97.15%)
|
||||
5. **遗留事项** — P11 候选方向(OCR/滤镜/批量文件夹/拼接GIF);`imagetool.py` 覆盖率 88%(ImportError 回退分支未覆盖,可接受)
|
||||
6. **归档说明** — iter-22 暂不归档(下次清理窗口为 iter-26 前夕,与 iter-21 一并归档)
|
||||
|
||||
### 步骤 2:同步 SKILL.md 行为变更
|
||||
|
||||
**修改 [.trae/skills/pyflowx-development/SKILL.md](file:///home/zhou/pyflowx/.trae/skills/pyflowx-development/SKILL.md)**:
|
||||
|
||||
1. **更新 frontmatter description** — 末尾追加 "并同步迭代 22(P10)的行为变更",提及图片处理工具
|
||||
2. **更新开头说明**(第 8 行附近)— "并同步迭代 21(P9 功能扩展)与迭代 22(P10 图片处理)的行为变更"
|
||||
3. **新增第十五章 `## 十五、图片处理(P10,iter-22)`**(追加在第十四 章之后),包含 4 个小节:
|
||||
|
||||
#### 可复用模式
|
||||
- **可选依赖 try/except + `_require_*` 守卫**:顶部 `try: from PIL import Image; HAS_PIL = True; except ImportError: HAS_PIL = False`,每个子命令入口 `if not _require_pil(): return` 打印 `pip install pyflowx[office]` 提示
|
||||
- **`@px.tool` 装饰器注册子命令**:`@px.tool("imagetool", subcommand="r", help="调整尺寸")`,subcommand 短别名(r/c/ro/fl/cv/wm/cp/i/e/hi/co)
|
||||
- **`image_pipeline` DAG 构造器**:参照 `compose()` 函数式构造器,每个 step 生成 `TaskSpec`,`depends_on` 链式依赖,`_make_step_fn` 闭包工厂正确捕获每轮参数
|
||||
- **`naming` 模板占位符**:`{stem}`(累积 stem)/`{step}`(操作名)/`{ext}`(当前扩展名,convert 步骤可能改变)
|
||||
|
||||
#### 设计决策
|
||||
- **单一 `imagetool` + office extra**:不拆分为多个工具,所有图片操作集中在 `ops/files/imagetool.py`;Pillow 通过 `pyflowx[office]` extra 安装,零运行时依赖原则不破坏
|
||||
- **`_save_image` 统一保存逻辑**:JPG→JPEG 规范化 + JPEG 强制 RGB + JPEG/WEBP 应用 quality 参数;避免每个子命令重复处理
|
||||
- **`image_pipeline` 委托 imagetool 函数**:不重新实现图片操作,`operations` 字典映射操作名到 `imagetool.image_*` 函数,保持单一职责
|
||||
|
||||
#### 踩坑总结
|
||||
- **Pillow 10+ API 迁移**:`Image.FLIP_LEFT_RIGHT` 已弃用 → `Image.Transpose.FLIP_LEFT_RIGHT`;`bytes(exif)` 抛错 → `exif.tobytes()`
|
||||
- **`quantize(colors=N)` 实际色数可能 < N**:简单图片量化后调色板少于请求色数,必须用 `actual_count = len(palette) // 3` 动态计算
|
||||
- **闭包捕获循环变量陷阱**:`image_pipeline` 中直接在循环内定义闭包会捕获最后一次循环的变量;提取 `_make_step_fn(base_fn, input_path, output_path, params)` 为独立函数利用每次调用的独立作用域
|
||||
- **PLR0912 分支数限制**:`image_exif` 函数最初 13 分支超过 12 限制,提取 `_print_exif`/`_apply_exif_modifications`/`_apply_single_exif_set`/`_save_exif` 4 个辅助函数降至合规
|
||||
|
||||
### 步骤 3:更新 project_memory.md
|
||||
|
||||
**修改 [project_memory.md](file:///home/zhou/.trae-cn/memory/projects/-home-zhou-pyflowx/project_memory.md)**:
|
||||
|
||||
1. **Engineering Conventions 末尾追加** P10 约定(约 8 条):
|
||||
- `imagetool` 模式:try/except PIL 导入 + `HAS_PIL` 标志 + `_require_pil()` 守卫 + `@px.tool` 装饰器
|
||||
- `_save_image` 统一保存:JPG→JPEG 规范化 + JPEG 强制 RGB + JPEG/WEBP 应用 quality
|
||||
- EXIF 序列化用 `exif.tobytes()`(Pillow 10+),不用 `bytes(exif)`
|
||||
- `quantize(colors=N)` 实际色数可能 < N,用 `actual_count = len(palette) // 3` 动态计算
|
||||
- Pillow 10+ 用 `Image.Transpose.FLIP_LEFT_RIGHT`(非弃用的 `Image.FLIP_LEFT_RIGHT`)
|
||||
- `image_pipeline` 用 `_make_step_fn(base_fn, input_path, output_path, params)` 闭包工厂避免循环变量捕获陷阱
|
||||
- `image_pipeline` 委托 `imagetool.image_*` 函数,不重新实现操作(单一职责)
|
||||
- `pytest.importorskip("PIL")` 跳过未安装 Pillow 的测试模块
|
||||
|
||||
2. **Lessons Learned 末尾追加** iter-22 总结条目:
|
||||
- `iter-22 (P10 image processing) completed: imagetool 11 subcommands (resize/crop/rotate/flip/convert/watermark/compress/info/exif/histogram/colors) + image_pipeline DAG constructor; 1560 tests green, coverage 97.15%; SKILL.md synced (added section 15); next archive window at iter-26`
|
||||
|
||||
### 步骤 4:Git commit + push
|
||||
|
||||
按文件名逐个 `git add`(不用 `git add .`/`-A`),然后 commit + push。
|
||||
|
||||
**暂存文件**(8 个):
|
||||
```
|
||||
git add src/pyflowx/ops/files/imagetool.py
|
||||
git add src/pyflowx/imaging.py
|
||||
git add src/pyflowx/__init__.py
|
||||
git add src/pyflowx/cli/pf.py
|
||||
git add tests/cli/test_imagetool.py
|
||||
git add tests/test_imaging.py
|
||||
git add .trae/docs/iter-22-image-processing.md
|
||||
git add .trae/skills/pyflowx-development/SKILL.md
|
||||
```
|
||||
|
||||
**Commit message**(遵循 [.trae/rules/git-commit-message.md](file:///home/zhou/pyflowx/.trae/rules/git-commit-message.md) 中文风格,参照 `5ac7fd3` P9 提交):
|
||||
|
||||
```
|
||||
feat: P10 图片处理 — imagetool 11 子命令(resize/crop/rotate/flip/convert/watermark/compress/info/exif/histogram/colors) + image_pipeline DAG 构造器;1560 测试全绿,覆盖率 97.15%
|
||||
```
|
||||
|
||||
**Push**:`git push`(develop 跟踪 origin/develop,直接 push)
|
||||
|
||||
## 关键文件
|
||||
|
||||
| 操作 | 文件 |
|
||||
|------|------|
|
||||
| 新建 | `.trae/docs/iter-22-image-processing.md`(iter-22 迭代记录) |
|
||||
| 修改 | `.trae/skills/pyflowx-development/SKILL.md`(新增第十五章 + frontmatter 更新) |
|
||||
| 修改 | `/home/zhou/.trae-cn/memory/projects/-home-zhou-pyflowx/project_memory.md`(追加 P10 约定) |
|
||||
| 提交 | 上述 3 文件 + 已实现的 5 文件(imagetool.py/imaging.py/__init__.py/pf.py/2 测试文件) |
|
||||
|
||||
## 复用的现有模式
|
||||
|
||||
- **iter-21 迭代记录结构**:[iter-21-p9-feature-extension.md](file:///home/zhou/pyflowx/.trae/docs/iter-21-p9-feature-extension.md)(本轮目标 / 改动文件清单 / 关键决策与依据 / 验证结果 / 遗留事项 / 归档说明)
|
||||
- **SKILL.md 章节结构**:参照第十四章 `## 十四、任务编排与持久化(P9,iter-21)` 的 `### 可复用模式` / `### 设计决策` / `### 踩坑总结` 三段式
|
||||
- **commit message 风格**:参照 `5ac7fd3` P9 提交(`feat: P9 功能扩展 — ...;1525 测试全绿,覆盖率 97.58%`)
|
||||
- **project_memory 条目风格**:参照第 125 行 iter-21 总结条目
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. **文档完整性**:
|
||||
- `iter-22-image-processing.md` 包含全部 6 个小节(目标/文件/决策/验证/遗留/归档)
|
||||
- `SKILL.md` 第十五章包含可复用模式/设计决策/踩坑总结三小节
|
||||
- `project_memory.md` Engineering Conventions 追加 8 条、Lessons Learned 追加 1 条
|
||||
|
||||
2. **Git 状态**:
|
||||
- `git status` 干净(无未提交文件)
|
||||
- `git log --oneline -1` 显示新 commit
|
||||
- `git push` 成功(origin/develop 已更新)
|
||||
|
||||
3. **门禁复跑**(可选,确保文档修改未误改代码):
|
||||
```bash
|
||||
uvx --from pyflowx pymake tc # ruff + pyrefly + pytest
|
||||
```
|
||||
|
||||
## 验收标准
|
||||
|
||||
- `.trae/docs/iter-22-image-processing.md` 创建完成,结构对齐 iter-21
|
||||
- `.trae/skills/pyflowx-development/SKILL.md` 新增第十五章,frontmatter description 更新
|
||||
- `project_memory.md` 追加 P10 约定(Engineering Conventions + Lessons Learned)
|
||||
- Git commit 成功,push 到 origin/develop 成功
|
||||
- `git status` 干净
|
||||
|
||||
## 不在范围内
|
||||
|
||||
- P11 高级功能(OCR/滤镜/批量文件夹/拼接GIF)— 用户已确认为后续阶段输入,本次不实现
|
||||
- 代码修改 — P10 代码已实现完成,本次仅文档同步 + 提交
|
||||
- 归档 — iter-22 暂不归档(下次清理窗口 iter-26 前夕,与 iter-21 一并归档到 SKILL.md)
|
||||
@@ -1,11 +1,12 @@
|
||||
---
|
||||
name: "pyflowx-development"
|
||||
description: "PyFlowX 项目(DAG 任务调度库)的开发知识库。归档迭代 06-20 中可复用的架构模式、踩坑总结与设计决策,并同步迭代 21(P9)的行为变更。在设计与调度器、CLI、YAML 编排、取消机制、序列化、观测性、错误诊断、状态后端、性能优化、任务编排、检查点恢复、监控导出相关的功能时参考。"
|
||||
description: "PyFlowX 项目(DAG 任务调度库)的开发知识库。归档迭代 06-20 中可复用的架构模式、踩坑总结与设计决策,并同步迭代 21(P9)与迭代 22(P10 图片处理)的行为变更。在设计与调度器、CLI、YAML 编排、取消机制、序列化、观测性、错误诊断、状态后端、性能优化、任务编排、检查点恢复、监控导出、图片处理相关的功能时参考。"
|
||||
---
|
||||
|
||||
# PyFlowX 开发知识库
|
||||
|
||||
本技能归档自迭代 06-20 的过程记录,并同步迭代 21(P9 功能扩展)的行为变更。
|
||||
本技能归档自迭代 06-20 的过程记录,并同步迭代 21(P9 功能扩展)与
|
||||
迭代 22(P10 图片处理)的行为变更。
|
||||
按主题分类整理可复用知识。过程性细节(覆盖率数字、命令输出)已剔除,
|
||||
仅保留架构模式、设计依据与陷阱总结。
|
||||
|
||||
@@ -713,3 +714,71 @@ description: "PyFlowX 项目(DAG 任务调度库)的开发知识库。归档
|
||||
- **`metrics_text` 条件化输出**:最初实现总是输出 HELP/TYPE 行(即便无数据),
|
||||
导致 `reset()` 后文本仍含 `pyflowx_task_total`,违反测试预期。所有
|
||||
section 都应按数据存在性条件化,与 retries/run_total 一致。
|
||||
|
||||
## 十五、图片处理(P10,iter-22)
|
||||
|
||||
### 可复用模式
|
||||
|
||||
- **可选依赖 try/except + `_require_*` 守卫**:模块顶部
|
||||
`try: from PIL import Image, ImageDraw, ImageFont; HAS_PIL = True;
|
||||
except ImportError: HAS_PIL = False`,每个子命令入口
|
||||
`if not _require_pil(): return` 打印 `pip install pyflowx[office]`
|
||||
提示而非崩溃。与 `pdftool.py` 的 `pypdf` 守卫模式一致。
|
||||
- **`@px.tool` 装饰器注册子命令**:`@px.tool("imagetool", subcommand="r",
|
||||
help="调整尺寸")`,subcommand 短别名(r/c/ro/fl/cv/wm/cp/i/e/hi/co)
|
||||
供 CLI 快捷调用(如 `pf img r input.png out.png --width 100`)。
|
||||
- **`image_pipeline` DAG 构造器**:参照 `compose()` 函数式构造器,
|
||||
每个 step 生成 `TaskSpec`,`depends_on` 链式依赖形成单链 DAG;
|
||||
`operations` 字典映射操作名到 `imagetool.image_*` 函数,不重新实现操作。
|
||||
- **`naming` 模板占位符**:`{stem}`(累积 stem,每步更新为输出文件 stem)/
|
||||
`{step}`(操作名)/`{ext}`(当前扩展名,`convert` 步骤可能改变)。
|
||||
默认 `"{stem}_{step}{ext}"`(如 `input_resize.jpg`)。
|
||||
- **`_make_step_fn` 闭包工厂**:提取为独立函数
|
||||
`_make_step_fn(base_fn, input_path, output_path, params)` 返回 `_run`
|
||||
闭包,利用每次调用的独立作用域正确捕获每轮参数,避免循环变量捕获陷阱。
|
||||
- **`pytest.importorskip("PIL")`**:测试模块顶部跳过未安装 Pillow 的环境,
|
||||
避免在未安装 `[office]` extra 的 CI 上失败。
|
||||
|
||||
### 设计决策
|
||||
|
||||
- **单一 `imagetool` + office extra**:不拆分为 `imageops`/`imagemeta`
|
||||
等多工具,所有图片操作集中在 `ops/files/imagetool.py`。Pillow 通过
|
||||
`pyproject.toml` 的 `[office]` extra 安装(`pillow>=10.4.0`),
|
||||
零运行时依赖原则不破坏。
|
||||
- **`_save_image` 统一保存逻辑**:所有需要保存图片的子命令委托
|
||||
`_save_image(img, output, fmt, quality)` 处理 JPG→JPEG 规范化 +
|
||||
JPEG 强制 RGB 转换 + JPEG/WEBP 应用 quality 参数,避免每个子命令
|
||||
重复处理格式与 alpha 通道问题。
|
||||
- **`image_pipeline` 委托 imagetool 函数**:不重新实现图片操作,
|
||||
`operations` 字典映射操作名到 `imagetool.image_*` 函数,保持单一职责
|
||||
与单一实现源。
|
||||
- **EXIF 用 `exif.tobytes()`**:Pillow 10+ 的 `Image.Exif` 对象
|
||||
`bytes(exif)` 抛 `ValueError`,必须用 `exif.tobytes()` 序列化。
|
||||
`_save_exif` 辅助函数封装此逻辑。
|
||||
- **PLR0912 分支数控制**:`image_exif` 函数最初 13 分支超过 12 限制,
|
||||
提取 `_print_exif`/`_apply_exif_modifications`/`_apply_single_exif_set`/
|
||||
`_save_exif` 4 个辅助函数降至合规。
|
||||
|
||||
### 踩坑总结
|
||||
|
||||
- **Pillow 10+ API 迁移**:`Image.FLIP_LEFT_RIGHT`(模块级常量)已弃用,
|
||||
pyrefly 无法解析 → 改用 `Image.Transpose.FLIP_LEFT_RIGHT`(枚举形式);
|
||||
`bytes(exif)` 抛 `ValueError: bytes must be in range(0, 256)` →
|
||||
改用 `exif.tobytes()`。
|
||||
- **`quantize(colors=N)` 实际色数可能 < N**:简单图片(如纯色)量化后
|
||||
调色板可能少于请求色数,直接按 `count` 遍历会 `IndexError`。必须用
|
||||
`actual_count = len(palette) // 3` 动态计算并 `min(count, actual_count)`。
|
||||
- **JPEG 不支持 alpha 通道**:RGBA 模式图片直接保存为 JPEG 抛
|
||||
`cannot write mode RGBA as JPEG`。`_save_image` 中 JPEG 分支强制
|
||||
`img.convert("RGB")` 去 alpha。
|
||||
- **JPG 不是 Pillow 合法格式名**:`img.save(format="JPG")` 抛
|
||||
`KeyError: 'JPG'`。`_save_image` 中 `save_fmt == "JPG"` 时改写为
|
||||
`"JPEG"` 规范化。
|
||||
- **闭包捕获循环变量陷阱**:`image_pipeline` 中若直接在 for 循环内
|
||||
定义闭包 `def _run(): base_fn(input_path, output_path, **params)`,
|
||||
所有任务的 `_run` 会捕获**最后一次循环**的变量。必须提取为独立函数
|
||||
`_make_step_fn(base_fn, input_path, output_path, params)` 利用每次
|
||||
调用的独立作用域。
|
||||
- **`draw.textbbox()` 返回 float**:`text_w = bbox[2] - bbox[0]` 是
|
||||
float,传给 `_resolve_position`(期望 int)会触发类型警告。用
|
||||
`int(bbox[2] - bbox[0])` 显式转换。
|
||||
|
||||
Reference in New Issue
Block a user