refactor(ppt): 版式 helper 抽成可 import 模块 + 修中文字体没真生效 + quality_check 配色查纳入填充色
逐页生成是"每页一个 run_python",以前 ~150 行 helper(配色常量/add_textbox/ apply_brand 等)要在每页里默写一遍 —— 烧 token 且长 deck 里坐标会漂(第 7 页 apply_brand 跟第 2 页对不上)。 - 新增 scripts/pptx_helpers.py:每页 import pptx_helpers as P 调用;含 new_presentation / load(按文件实际尺寸回填画布常量,逐页进程间同步) / add_slide / set_palette(默认 商务红,spec_path= 自动取 spec 前 3 个 hex 作主/辅/强调)入口。 - 字体修复:python-pptx font.name 只写 <a:latin>,中文字形走 <a:ea> 槽位没设 —— "指定微软雅黑却没真生效"的根因。set_text 改为同时写 latin=Arial + ea/cs=微软雅黑, 中英混排各命中正确字体。 - quality_check.py 配色检查纳入形状填充色(品牌条/徽章/圆点/标签/底块以前全漏), 并把粗阈值"≤5 色"改成贴合三色制的"非灰阶色 ≤3"(灰/黑/白按 R/G/B 极差 ≤12 排除), 否则计入填充后合规商务红 deck 会狂报假阳;spec 比对也只比非灰阶色。 - 联动:layouts.md(helper 块换成 import 起手 + API 速查,9 个示例全改 P. 调用)、 icons.md A5 示例、SKILL.md 资源/阶段二、PROGRESS.md。 冒烟测试过:ea 字体确写入、set_palette 覆盖生效、quality_check 正常解析; 合规红 deck 无配色 warning、塞 4+ 彩色触发、ACCENT 强调线填充被正确捕获。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
af97dd7c62
commit
07959eb738
|
|
@ -23,6 +23,8 @@
|
|||
|
||||
### 2026-06-04
|
||||
|
||||
- **ppt skill 版式 helper 收进可 import 的模块 + 修中文字体没真生效**:逐页生成是「每页一个 run_python」,以前 ~150 行 helper(配色常量/`add_textbox`/`apply_brand` 等)要在每页里默写一遍 —— 烧 token 且长 deck 里坐标会漂(第 7 页 `apply_brand` 跟第 2 页对不上)。抽出 `skills/ppt/scripts/pptx_helpers.py`,每页 `import pptx_helpers as P` 调用;新增 `new_presentation`/`load`(按文件实际尺寸回填画布常量,逐页进程间同步)/`add_slide`/`set_palette`(默认商务红,`spec_path=` 自动取 spec 前 3 个 hex 作主/辅/强调)入口。**字体修复**:python-pptx `font.name` 只写 `<a:latin>`,中文字形走 `<a:ea>` 槽位没设 → 「指定微软雅黑却没真生效」的根因;`set_text` 改为同时写 latin=Arial + ea/cs=微软雅黑,中英混排各命中正确字体。改 `layouts.md`(helper 块换成 import 起手 + API 速查,9 个示例全改 `P.` 调用)、`icons.md` A5 示例、`SKILL.md` 资源/阶段二。冒烟测试过:`ea` 确写入、`set_palette` 覆盖生效、quality_check 正常解析。
|
||||
- **ppt `quality_check.py` 配色检查纳入形状填充色 + 改按三色制判定**:原来只数 `run.font.color`(文字色),品牌条/徽章/圆点/标签/底块的**填充色全漏**——而这些恰是最易跑偏处。加 `_shape_fill_hex`(取纯色实心填充,主题色/非实心挡掉)并入 `seen_colors`。同时把粗阈值「≤5 色」改成贴合三色制的「非灰阶色 ≤3」:`_is_neutral`(R/G/B 极差 ≤12 视为灰/黑/白)把中性色排除——否则一旦计入填充,合规商务红 deck(INK/GREY/HAIRLINE/BG/WHITE+3 红)轻松超 6 狂报假阳;spec 比对也只比非灰阶色。测试过:合规红 deck 无配色 warning、塞 4+ 彩色触发、ACCENT 强调线填充被正确捕获。
|
||||
- **前端顶栏展示用户已用存储**:后端已有 `user_disk_usage` 表(后台 15min 扫描落库),但无对外查询口。加 `GET /v1/user/storage`(`Depends(require_user)`),返 `{bytes_used, file_count, limit_bytes, scanned_at}`,`limit_bytes` 由 `parse_bytes(quotas.disk_bytes_per_user)` 得(≤0/None=不限)。`disk_quota.get_user_usage` 扩为返 `(bytes,count,scanned_at)` 三元组(复用而非新开函数,顺手改唯一调用方 `check_disk_quota` 解包)。前端 `dev.html` 右侧「文件」面板底部钉一条进度条+文字指示器(`#pane-right` 改 flex 列让 `#file-list` 独占滚动、存储条钉底;`loadStorage()` 在 `enterApp` 拉一次;不限额时只显已用、隐进度条;超额变红;hover 显文件数+统计时间)。
|
||||
- **sandbox 容器 env 收编到一处 + shell 也注入(修两个只读 rootfs 副作用)**:① `PYTHONPATH=/sandbox:/workspace` 原先只 `run_python` 注入,shell 里 `python -c "from skills..."` 撞 ModuleNotFoundError;② `--read-only` rootfs 下 `/home/zcbot` 不可写,matplotlib/fontconfig 往 `~/.config`/`~/.cache` 写缓存刷 "Read-only file system" / "No writable cache" 噪音。改:`executor_docker.py` 抽 `_CONTAINER_ENV = {PYTHONPATH, HOME=/tmp}`,shell/run_python/fs 三路共用(`-e` 确定性覆盖)—— `HOME=/tmp` 一刀让缓存落 tmpfs(matplotlib→/tmp/.config、fontconfig→/tmp/.cache),不用逐个 MPLCONFIGDIR/XDG_CACHE_HOME。纯代码改,重启 web 生效,免重建镜像。
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ description: 生成 PowerPoint 演示文稿 (.pptx) 文件。✅ 触发:用户
|
|||
把材料变成可演示的 .pptx。**先定调,再出稿,再验收** —— 不要一口气把整份 deck 丢出去。
|
||||
|
||||
## 资源
|
||||
- `scripts/pptx_helpers.py` —— **版式工具箱模块**:配色/字体常量 + `new_presentation`/`load`/`add_slide`/`set_palette` + `add_textbox`/`add_rect`/`add_dot`/`add_badge`/`page_title`/`apply_brand` 等 helper。每页 `import pptx_helpers as P` 调用,**不要把 helper 源码默写进 run_python**
|
||||
- `references/design_principles.md` —— 画布尺寸 + 字号/配色/留白/字数预算等硬规则
|
||||
- `references/layouts.md` —— 9 种版式的 python-pptx 起手代码 + 安全区/越界保护 + `apply_brand` 品牌条
|
||||
- `references/layouts.md` —— 9 种版式的调用示例(基于 `pptx_helpers`)+ helper API 速查 + 安全区/越界保护 + `apply_brand` 品牌条
|
||||
- `references/icons.md` —— 业务图标两层:Iconify (在线/本地缓存) / unicode 字形兜底
|
||||
- `assets/icons/` —— **只读**种子图标库 (skill 自带的商务红 tabler 集,见 `INDEX.md`;docker 沙盒里 skills 是只读挂载。新拉的图标写 `<task_dir>/assets/icons/`)
|
||||
- 素材摄取: 用 `markitdown` CLI 把 PDF/DOCX/PPTX/XLSX/HTML/URL 转干净 Markdown,统一落到 `<task_dir>/source/<name>.md`(同 working_dir 多 task 共享 source 池)
|
||||
|
|
@ -75,7 +76,7 @@ glob <task_dir>/*-<task_short_id>-*.spec.md → 按文件名字典序排,取最
|
|||
每页流程:
|
||||
1. 读 current spec(即使刚读过)
|
||||
2. **图标先于版式**: 这一页要用什么概念图标? 先 `glob` 两处看有没有现成 —— 种子库 `<skill_dir>/assets/icons/`(只读,`<skill_dir>` 是 `load_skill` 头里的绝对路径)+ 本 task `<task_dir>/assets/icons/`;没有就 `python <skill_dir>/scripts/fetch_icon.py <name> --set tabler --color C00000 --size 128 -o <task_dir>/assets/icons/...` 拉一个(种子库只读,新图标落 task 目录);`add_picture` 嵌入。**几何形状(圆点/徽章/装饰线)不算图标,走 layouts.md helper 即可**
|
||||
3. 写一个 `run_python` block,用 python-pptx 添加这一页 (载入已有 .pptx → append slide → save)
|
||||
3. 写一个 `run_python` block 添加这一页:顶部 `import pptx_helpers as P`(`sys.path` 指到 `<skill_dir>/scripts`)→ `prs = P.load(...)`(首页用 `P.new_presentation`)→ `P.set_palette(spec_path=...)`(每页重读 spec 注入配色)→ `P.add_slide` + 各 helper → `prs.save`。**helper 一律 `P.xxx` 调用,不默写源码**(防长 deck 漂移),起手见 `layouts.md §通用起手`
|
||||
4. 报这一页:版式、标题、要点条数、用了哪些图标
|
||||
5. 用户确认 / 微调后再下一页
|
||||
6. 用户确认了**实质改动**(改版式 / 换图标 / 改文案要点 / 增删页 / 调主色)后,追加一行到 `<task_dir>/REVISIONS.md` —— 见 §修订日志
|
||||
|
|
|
|||
|
|
@ -54,13 +54,15 @@ slide.shapes.add_picture(
|
|||
需要"调研→设计→开发→测试→上线"这种横向流程时,**不要用 PowerPoint 内置 PENTAGON**(视觉陈旧),改用 Iconify 的 `chevron-right` + 文本组合:
|
||||
|
||||
```python
|
||||
from pptx.util import Inches, Pt
|
||||
from pptx.util import Inches
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
# 假设页面顶部已 import pptx_helpers as P,且 slide 已建(见 layouts.md §通用起手)
|
||||
stages = ["调研","设计","开发","测试","上线"]
|
||||
icon_path = "<task_dir>/assets/icons/tabler_chevron-right_C00000_64.png" # 先 fetch_icon.py 拉到 task,种子库没有 chevron-right_64
|
||||
for i, label in enumerate(stages):
|
||||
x = 0.7 + i * 2.4
|
||||
add_textbox(slide, x, 3.7, 1.8, 0.5, label, 16, bold=True,
|
||||
color=PRIMARY, align=PP_ALIGN.CENTER, name=f"stage_{i}")
|
||||
P.add_textbox(slide, x, 3.7, 1.8, 0.5, label, 16, bold=True,
|
||||
color=P.PRIMARY, align=PP_ALIGN.CENTER, name=f"stage_{i}")
|
||||
if i < len(stages) - 1: # 节点间放 chevron
|
||||
slide.shapes.add_picture(icon_path, Inches(x + 1.85), Inches(3.7),
|
||||
width=Inches(0.4))
|
||||
|
|
|
|||
|
|
@ -1,188 +1,78 @@
|
|||
# 9 种常用版式 (16:9, 13.33×7.5 in)
|
||||
|
||||
> **2.0 版本要点**:大幅减少满铺色块,引入 MSO_SHAPE 图标点缀,所有元素经 safe_area 校验不会越出画布。
|
||||
> **要点**:版式 helper 已全部收进 `scripts/pptx_helpers.py`,**不要再把 helper 源码默写进每页的 run_python** —— 每页只 `import pptx_helpers as P` 然后调用。这样长 deck 里不会出现第 7 页和第 2 页的 `apply_brand` 坐标对不上的漂移,也省 token。配色用 current spec(命名见 SKILL.md §阶段一)里的实际 hex —— 通过 `P.set_palette()` 注入,默认商务红。
|
||||
|
||||
复制 → 改文案 → 跑。配色用 current spec(命名见 SKILL.md §阶段一)里的实际 hex 替换占位。
|
||||
|
||||
## 通用起手 + 安全辅助
|
||||
## 通用起手 (每页 run_python 顶部)
|
||||
|
||||
```python
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt, Emu
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
import sys
|
||||
sys.path.insert(0, "<skill_dir>/scripts") # <skill_dir> 用 system prompt 注入的绝对路径替换
|
||||
import pptx_helpers as P
|
||||
|
||||
# ---- 配色 (商务红 — 硬约束默认) ----
|
||||
# ⛔ 不允许擅自换色:除非用户明确点名其它配色 (例:"做成蓝色") 或 spec 已写其它 hex,
|
||||
# 否则就是这套商务红。禁止以"这个场景蓝色更专业"这类自我合理化做替换。
|
||||
PRIMARY = RGBColor(0xC0, 0x00, 0x00) # 深红 - 标题/强调/关键数据
|
||||
SECONDARY = RGBColor(0xE1, 0x55, 0x54) # 砖红 - 次要图形
|
||||
ACCENT = RGBColor(0xFF, 0xC1, 0x07) # 金黄 - 关键数据点/CTA
|
||||
INK = RGBColor(0x1F, 0x1F, 0x1F)
|
||||
GREY = RGBColor(0x59, 0x59, 0x59)
|
||||
GREY_LIGHT = RGBColor(0x88, 0x88, 0x88)
|
||||
BG = RGBColor(0xFA, 0xFA, 0xFA) # 背景近白
|
||||
WHITE = RGBColor(255, 255, 255)
|
||||
# —— 第一页(创建 deck)——
|
||||
prs = P.new_presentation("16:9") # 默认 16:9;可传 "4:3" / "9:16" / "3:4"
|
||||
P.set_palette(spec_path="<task_dir>/<today>-<task_short_id>-<task_name>.spec.md")
|
||||
slide = P.add_slide(prs)
|
||||
# ... 见下面各 L 版式 ...
|
||||
prs.save("<task_dir>/<topic>.pptx")
|
||||
|
||||
CN_FONT = "微软雅黑"
|
||||
EN_FONT = "Arial"
|
||||
|
||||
# ---- 画布与安全区 ----
|
||||
prs = Presentation()
|
||||
prs.slide_width = Inches(13.33)
|
||||
prs.slide_height = Inches(7.5)
|
||||
SLIDE_W = 13.33
|
||||
SLIDE_H = 7.5
|
||||
MARGIN_X = 0.7 # 左右
|
||||
MARGIN_Y = 0.5 # 上下
|
||||
SAFE_LEFT = MARGIN_X
|
||||
SAFE_TOP = MARGIN_Y
|
||||
SAFE_RIGHT = SLIDE_W - MARGIN_X
|
||||
SAFE_BOTTOM = SLIDE_H - MARGIN_Y
|
||||
SAFE_W = SAFE_RIGHT - SAFE_LEFT # 11.93
|
||||
SAFE_H = SAFE_BOTTOM - SAFE_TOP # 6.5
|
||||
BLANK = prs.slide_layouts[6]
|
||||
|
||||
def assert_inside(left, top, width, height, name=""):
|
||||
"""放置前调一次。越界直接报错而不是悄悄超出。"""
|
||||
if left < 0 or top < 0:
|
||||
raise ValueError(f"[{name}] 左/上为负: ({left}, {top})")
|
||||
if left + width > SLIDE_W + 1e-3:
|
||||
raise ValueError(f"[{name}] 右越界: {left}+{width} > {SLIDE_W}")
|
||||
if top + height > SLIDE_H + 1e-3:
|
||||
raise ValueError(f"[{name}] 下越界: {top}+{height} > {SLIDE_H}")
|
||||
|
||||
# ---- 文本辅助 (默认 word_wrap, shrink-to-fit 兜底) ----
|
||||
def set_text(tf, text, size, bold=False, color=INK, align=PP_ALIGN.LEFT,
|
||||
font=CN_FONT):
|
||||
tf.text = text
|
||||
p = tf.paragraphs[0]; p.alignment = align
|
||||
r = p.runs[0]
|
||||
r.font.name = font; r.font.size = Pt(size); r.font.bold = bold
|
||||
r.font.color.rgb = color
|
||||
|
||||
def add_textbox(slide, left, top, width, height, text, size,
|
||||
bold=False, color=INK, align=PP_ALIGN.LEFT,
|
||||
anchor=MSO_ANCHOR.TOP, font=CN_FONT, shrink=True,
|
||||
name="textbox"):
|
||||
assert_inside(left, top, width, height, name)
|
||||
tb = slide.shapes.add_textbox(Inches(left), Inches(top),
|
||||
Inches(width), Inches(height))
|
||||
tf = tb.text_frame
|
||||
tf.vertical_anchor = anchor
|
||||
tf.word_wrap = True
|
||||
if shrink:
|
||||
# 文字超出框高时自动收缩字号 (兜底,不替代字数预算)
|
||||
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
|
||||
set_text(tf, text, size, bold, color, align, font)
|
||||
return tb
|
||||
|
||||
# ---- 形状辅助 (无边线实心填充) ----
|
||||
def add_rect(slide, left, top, width, height, fill, name="rect"):
|
||||
assert_inside(left, top, width, height, name)
|
||||
s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(left), Inches(top),
|
||||
Inches(width), Inches(height))
|
||||
s.fill.solid(); s.fill.fore_color.rgb = fill
|
||||
s.line.fill.background()
|
||||
return s
|
||||
|
||||
def add_shape(slide, kind, left, top, width, height, fill, name="shape"):
|
||||
assert_inside(left, top, width, height, name)
|
||||
s = slide.shapes.add_shape(kind, Inches(left), Inches(top),
|
||||
Inches(width), Inches(height))
|
||||
s.fill.solid(); s.fill.fore_color.rgb = fill
|
||||
s.line.fill.background()
|
||||
return s
|
||||
|
||||
def add_dot(slide, x, y, size=0.18, color=ACCENT):
|
||||
return add_shape(slide, MSO_SHAPE.OVAL, x, y, size, size, color, "dot")
|
||||
|
||||
def add_accent_line(slide, x, y, length=1.0, thickness=0.05, color=ACCENT):
|
||||
"""标题下面那条强调线,替代大色块"""
|
||||
return add_rect(slide, x, y, length, thickness, color, "accent_line")
|
||||
|
||||
def add_badge(slide, x, y, num, diameter=0.7, fill=PRIMARY, fg=WHITE):
|
||||
"""编号徽章 (圆 + 数字)"""
|
||||
c = add_shape(slide, MSO_SHAPE.OVAL, x, y, diameter, diameter, fill, "badge")
|
||||
tf = c.text_frame; tf.text = str(num)
|
||||
p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER
|
||||
r = p.runs[0]
|
||||
r.font.bold = True; r.font.size = Pt(int(diameter * 28))
|
||||
r.font.color.rgb = fg; r.font.name = EN_FONT
|
||||
return c
|
||||
|
||||
# ---- 标题套件 (内页通用) ----
|
||||
def page_title(slide, text, page_num=None, total=None, footer="项目汇报"):
|
||||
add_textbox(slide, SAFE_LEFT, SAFE_TOP, SAFE_W, 0.7, text,
|
||||
32, bold=True, color=PRIMARY, name="title")
|
||||
add_accent_line(slide, SAFE_LEFT, SAFE_TOP + 0.85,
|
||||
length=0.8, color=ACCENT)
|
||||
if page_num is not None and total is not None:
|
||||
add_textbox(slide, SAFE_LEFT, 7.0, 6, 0.4, footer,
|
||||
11, color=GREY_LIGHT, shrink=False, name="footer")
|
||||
add_textbox(slide, 12.0, 7.0, 1.2, 0.4, f"{page_num} / {total}",
|
||||
11, color=GREY_LIGHT, align=PP_ALIGN.RIGHT,
|
||||
shrink=False, name="page_num")
|
||||
|
||||
# ---- 品牌条 (每页起手必调,确保不是裸白纸) ----
|
||||
def apply_brand(slide, kind="inner"):
|
||||
"""统一品牌锚点。每个版式第一行调用,给一条窄的主色锚点 + 必要时浅底。
|
||||
kind:
|
||||
cover —— 封面: 左侧主色长竖条 + 顶部短横
|
||||
inner —— 内页 (默认): 左侧主色窄条 (从标题到底部)
|
||||
section —— 分章: 整页浅灰 + 左侧强调色粗竖条
|
||||
end —— 结尾: 整页浅灰 + 顶/底强调色短线
|
||||
"""
|
||||
if kind == "cover":
|
||||
# 顶部短主色横线 + 左侧主色长竖条 + 底部细灰线
|
||||
add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
|
||||
add_rect(slide, 0.7, 0.6, 0.8, 0.06, PRIMARY, "brand_top_line")
|
||||
add_rect(slide, SAFE_LEFT, 7.18, SAFE_W, 0.02,
|
||||
RGBColor(0xDD, 0xDD, 0xDD), "brand_btm_hairline")
|
||||
elif kind == "section":
|
||||
add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
|
||||
add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
|
||||
add_rect(slide, 0.7, 2.5, 0.08, 2.5, ACCENT, "brand_section_bar")
|
||||
elif kind == "end":
|
||||
add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
|
||||
add_rect(slide, SAFE_LEFT, 0.6, 0.8, 0.06, ACCENT, "brand_top_line")
|
||||
add_rect(slide, SAFE_RIGHT - 0.8, 6.85, 0.8, 0.06, ACCENT,
|
||||
"brand_btm_line")
|
||||
else: # inner — 默认内页
|
||||
# 左侧主色窄条 (从标题区到页脚)
|
||||
add_rect(slide, 0, 0, 0.10, SLIDE_H, PRIMARY, "brand_left_bar")
|
||||
# 底部一条细灰线 (页脚分隔)
|
||||
add_rect(slide, SAFE_LEFT, 7.18, SAFE_W, 0.02,
|
||||
RGBColor(0xDD, 0xDD, 0xDD), "brand_btm_hairline")
|
||||
# —— 后续页(追加到已有 deck)——
|
||||
prs = P.load("<task_dir>/<topic>.pptx") # 从文件实际尺寸回填画布常量
|
||||
P.set_palette(spec_path="<task_dir>/<today>-<task_short_id>-<task_name>.spec.md") # 每页都重读 spec
|
||||
slide = P.add_slide(prs)
|
||||
# ... 见下面各 L 版式 ...
|
||||
prs.save("<task_dir>/<topic>.pptx")
|
||||
```
|
||||
|
||||
> **要点**:
|
||||
> - `assert_inside` 阻止任何越界。元素超出画布会立刻报 `ValueError`,而不是悄悄裁剪
|
||||
> - `add_textbox` 默认 `word_wrap=True` + `auto_size=TEXT_TO_SHAPE_AND_FIT_TEXT` —— 文字溢出自动缩字号
|
||||
> - `page_title` 用细线代替大块色填,所有内页统一调用
|
||||
⚠️ 一律用 `P.xxx`(不要 `from pptx_helpers import *`)—— `set_palette` 靠改模块属性覆盖配色,`import *` 会把旧绑定拷进页面命名空间导致覆盖不生效。
|
||||
|
||||
## Helper API 速查 (都在 `P.` 命名空间下)
|
||||
|
||||
**画布 / 配色入口**
|
||||
- `P.new_presentation(canvas="16:9")` → 建空 deck,设画布,回填 `P.SLIDE_W/H` 与安全区
|
||||
- `P.load(path)` → 载入已有 deck,按文件实际尺寸回填画布常量(逐页进程间自动同步)
|
||||
- `P.add_slide(prs)` → 追加一张空白版式(layout 6)slide
|
||||
- `P.set_palette(primary=, secondary=, accent=, cn_font=, en_font=, spec_path=)` → 覆盖主题色/字体;传 `spec_path` 自动从 spec.md 按文档顺序取前 3 个 #hex 作 主/辅/强调;**默认商务红,什么都不传无副作用**
|
||||
|
||||
**颜色常量**:`P.PRIMARY` `P.SECONDARY` `P.ACCENT` `P.INK` `P.GREY` `P.GREY_LIGHT` `P.HAIRLINE` `P.BG` `P.WHITE`
|
||||
**字体常量**:`P.CN_FONT`(微软雅黑) `P.EN_FONT`(Arial)
|
||||
**画布常量**:`P.SLIDE_W` `P.SLIDE_H` `P.SAFE_LEFT/TOP/RIGHT/BOTTOM` `P.SAFE_W` `P.SAFE_H`
|
||||
|
||||
**放置 helper**(全部内置 `assert_inside` 越界即报错)
|
||||
- `P.add_textbox(slide, left, top, w, h, text, size, bold=False, color=P.INK, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, font=None, shrink=True, name=...)` → 文本框;`font=None` 自动 latin=Arial + 东亚=微软雅黑(**中文真落到雅黑靠这个**),传 `font` 则两槽都用它(纯英文大字/数字)
|
||||
- `P.add_rect(slide, left, top, w, h, fill, name=...)` → 无边线实心矩形
|
||||
- `P.add_shape(slide, kind, left, top, w, h, fill, name=...)` → 任意 MSO_SHAPE(`kind` 用 `MSO_SHAPE.XXX`)
|
||||
- `P.add_dot(slide, x, y, size=0.18, color=P.ACCENT)` → 圆点(bullet 前缀)
|
||||
- `P.add_accent_line(slide, x, y, length=1.0, thickness=0.05, color=P.ACCENT)` → 强调短线
|
||||
- `P.add_badge(slide, x, y, num, diameter=0.7, fill=P.PRIMARY, fg=P.WHITE)` → 编号徽章(圆+数字)
|
||||
- `P.page_title(slide, text, page_num=None, total=None, footer="项目汇报")` → 内页标题+强调线(+可选页脚页码)
|
||||
- `P.apply_brand(slide, kind)` → 品牌锚点,`kind` ∈ `"cover"/"inner"/"section"/"end"`;**每页第一行必调**
|
||||
- `P.assert_inside(left, top, w, h, name="")` → 手动越界校验(上面的 helper 已内置)
|
||||
|
||||
> `MSO_SHAPE` / `PP_ALIGN` / `MSO_ANCHOR` 等枚举若页面里要直接用,自行 `from pptx.enum.shapes import MSO_SHAPE` 等(`pptx_helpers` 内部已 import,但不重导出)。
|
||||
|
||||
---
|
||||
|
||||
## L1 · 封面 (Cover) —— 主色长竖条锚点
|
||||
|
||||
```python
|
||||
slide = prs.slides.add_slide(BLANK)
|
||||
apply_brand(slide, "cover") # 左侧主色长竖条 + 顶部短横
|
||||
slide = P.add_slide(prs)
|
||||
P.apply_brand(slide, "cover") # 左侧主色长竖条 + 顶部短横
|
||||
|
||||
# 主标题 (避开左竖条)
|
||||
add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "项目名称 / 演示主题",
|
||||
44, bold=True, color=INK, name="cover_title")
|
||||
P.add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "项目名称 / 演示主题",
|
||||
44, bold=True, color=P.INK, name="cover_title")
|
||||
# 副标题 (灰色,弱化)
|
||||
add_textbox(slide, 0.9, 4.1, 11.9, 0.6, "一句话副标题或定位",
|
||||
22, color=GREY, name="cover_sub")
|
||||
P.add_textbox(slide, 0.9, 4.1, 11.9, 0.6, "一句话副标题或定位",
|
||||
22, color=P.GREY, name="cover_sub")
|
||||
# 汇报人 / 日期
|
||||
add_textbox(slide, 0.9, 6.4, 11.9, 0.4,
|
||||
"汇报人 · 部门 · 2026-05-06", 14, color=GREY_LIGHT,
|
||||
name="cover_meta")
|
||||
P.add_textbox(slide, 0.9, 6.4, 11.9, 0.4,
|
||||
"汇报人 · 部门 · 2026-05-06", 14, color=P.GREY_LIGHT,
|
||||
name="cover_meta")
|
||||
# 右下角小图标点缀 (五角星,可选)
|
||||
add_shape(slide, MSO_SHAPE.STAR_5_POINT, 12.2, 6.3, 0.5, 0.5, ACCENT,
|
||||
"deco_star")
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
P.add_shape(slide, MSO_SHAPE.STAR_5_POINT, 12.2, 6.3, 0.5, 0.5, P.ACCENT,
|
||||
"deco_star")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -190,17 +80,18 @@ add_shape(slide, MSO_SHAPE.STAR_5_POINT, 12.2, 6.3, 0.5, 0.5, ACCENT,
|
|||
## L2 · 目录 (Agenda) —— 编号徽章 + 文字
|
||||
|
||||
```python
|
||||
slide = prs.slides.add_slide(BLANK)
|
||||
apply_brand(slide, "inner")
|
||||
page_title(slide, "目录")
|
||||
from pptx.enum.text import MSO_ANCHOR
|
||||
slide = P.add_slide(prs)
|
||||
P.apply_brand(slide, "inner")
|
||||
P.page_title(slide, "目录")
|
||||
|
||||
items = ["背景与现状", "核心问题", "解决方案", "实施计划", "预期成果"]
|
||||
for i, item in enumerate(items):
|
||||
y = 1.9 + i * 0.95
|
||||
add_badge(slide, SAFE_LEFT, y, i + 1, diameter=0.65)
|
||||
add_textbox(slide, SAFE_LEFT + 1.0, y, SAFE_W - 1.0, 0.65,
|
||||
item, 22, color=INK, anchor=MSO_ANCHOR.MIDDLE,
|
||||
name=f"agenda_{i}")
|
||||
P.add_badge(slide, P.SAFE_LEFT, y, i + 1, diameter=0.65)
|
||||
P.add_textbox(slide, P.SAFE_LEFT + 1.0, y, P.SAFE_W - 1.0, 0.65,
|
||||
item, 22, color=P.INK, anchor=MSO_ANCHOR.MIDDLE,
|
||||
name=f"agenda_{i}")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -208,22 +99,24 @@ for i, item in enumerate(items):
|
|||
## L3 · 章节分隔 (Section Divider) —— 浅色背景 + 大字编号
|
||||
|
||||
```python
|
||||
slide = prs.slides.add_slide(BLANK)
|
||||
apply_brand(slide, "section") # 整页浅灰 + 主色左竖条 + 强调装饰
|
||||
# 大编号 (主色,描边视觉感)
|
||||
add_textbox(slide, 1.1, 2.0, 4, 2.5, "01", 160, bold=True,
|
||||
color=PRIMARY, font=EN_FONT, name="sec_num")
|
||||
from pptx.enum.text import MSO_ANCHOR
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
slide = P.add_slide(prs)
|
||||
P.apply_brand(slide, "section") # 整页浅灰 + 主色左竖条 + 强调装饰
|
||||
# 大编号 (主色;font=EN_FONT 让数字走 Arial)
|
||||
P.add_textbox(slide, 1.1, 2.0, 4, 2.5, "01", 160, bold=True,
|
||||
color=P.PRIMARY, font=P.EN_FONT, name="sec_num")
|
||||
# 章节名
|
||||
add_textbox(slide, 5.5, 2.8, 7, 1.0, "背景与现状",
|
||||
44, bold=True, color=INK, anchor=MSO_ANCHOR.MIDDLE,
|
||||
name="sec_title")
|
||||
P.add_textbox(slide, 5.5, 2.8, 7, 1.0, "背景与现状",
|
||||
44, bold=True, color=P.INK, anchor=MSO_ANCHOR.MIDDLE,
|
||||
name="sec_title")
|
||||
# 引言
|
||||
add_textbox(slide, 5.5, 4.0, 7, 0.6,
|
||||
"本章讨论行业现状与机会窗口", 18, color=GREY,
|
||||
name="sec_lead")
|
||||
P.add_textbox(slide, 5.5, 4.0, 7, 0.6,
|
||||
"本章讨论行业现状与机会窗口", 18, color=P.GREY,
|
||||
name="sec_lead")
|
||||
# 装饰小图标
|
||||
add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 5.5, 5.0, 0.6, 0.3, ACCENT,
|
||||
"sec_arrow")
|
||||
P.add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 5.5, 5.0, 0.6, 0.3, P.ACCENT,
|
||||
"sec_arrow")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -231,9 +124,10 @@ add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 5.5, 5.0, 0.6, 0.3, ACCENT,
|
|||
## L4 · 要点 (Bullets) —— 圆点 + 文字,无大块色
|
||||
|
||||
```python
|
||||
slide = prs.slides.add_slide(BLANK)
|
||||
apply_brand(slide, "inner")
|
||||
page_title(slide, "核心结论")
|
||||
from pptx.enum.text import MSO_ANCHOR
|
||||
slide = P.add_slide(prs)
|
||||
P.apply_brand(slide, "inner")
|
||||
P.page_title(slide, "核心结论")
|
||||
|
||||
bullets = [
|
||||
"结论一:用一句话讲清楚",
|
||||
|
|
@ -243,10 +137,10 @@ bullets = [
|
|||
]
|
||||
for i, b in enumerate(bullets):
|
||||
y = 2.0 + i * 0.95
|
||||
add_dot(slide, SAFE_LEFT + 0.05, y + 0.22, size=0.18, color=ACCENT)
|
||||
add_textbox(slide, SAFE_LEFT + 0.45, y, SAFE_W - 0.45, 0.6,
|
||||
b, 22, color=INK, anchor=MSO_ANCHOR.MIDDLE,
|
||||
name=f"bullet_{i}")
|
||||
P.add_dot(slide, P.SAFE_LEFT + 0.05, y + 0.22, size=0.18)
|
||||
P.add_textbox(slide, P.SAFE_LEFT + 0.45, y, P.SAFE_W - 0.45, 0.6,
|
||||
b, 22, color=P.INK, anchor=MSO_ANCHOR.MIDDLE,
|
||||
name=f"bullet_{i}")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -254,39 +148,39 @@ for i, b in enumerate(bullets):
|
|||
## L5 · 双栏对比 (Two-Column) —— 中线分隔,小色块标签
|
||||
|
||||
```python
|
||||
slide = prs.slides.add_slide(BLANK)
|
||||
apply_brand(slide, "inner")
|
||||
page_title(slide, "现状 vs 改进后")
|
||||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
|
||||
slide = P.add_slide(prs)
|
||||
P.apply_brand(slide, "inner")
|
||||
P.page_title(slide, "现状 vs 改进后")
|
||||
|
||||
mid_x = SLIDE_W / 2
|
||||
mid_x = P.SLIDE_W / 2
|
||||
|
||||
# 中间细分隔线 (替代两块大矩形)
|
||||
add_rect(slide, mid_x - 0.02, 2.0, 0.04, 4.8, RGBColor(0xDD, 0xDD, 0xDD),
|
||||
"divider")
|
||||
P.add_rect(slide, mid_x - 0.02, 2.0, 0.04, 4.8, P.HAIRLINE, "divider")
|
||||
|
||||
# 左栏小标签 (色块只占小区域)
|
||||
add_rect(slide, SAFE_LEFT, 2.0, 0.8, 0.35, GREY, "left_tag")
|
||||
add_textbox(slide, SAFE_LEFT, 2.0, 0.8, 0.35, "现状", 14, bold=True,
|
||||
color=WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
|
||||
shrink=False, name="left_label")
|
||||
P.add_rect(slide, P.SAFE_LEFT, 2.0, 0.8, 0.35, P.GREY, "left_tag")
|
||||
P.add_textbox(slide, P.SAFE_LEFT, 2.0, 0.8, 0.35, "现状", 14, bold=True,
|
||||
color=P.WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
|
||||
shrink=False, name="left_label")
|
||||
left_pts = ["问题 A: 描述", "问题 B: 描述", "问题 C: 描述"]
|
||||
for i, p in enumerate(left_pts):
|
||||
add_dot(slide, SAFE_LEFT + 0.05, 2.7 + i * 0.7 + 0.18, color=GREY)
|
||||
add_textbox(slide, SAFE_LEFT + 0.45, 2.7 + i * 0.7,
|
||||
mid_x - SAFE_LEFT - 0.7, 0.55, p, 18, color=INK,
|
||||
anchor=MSO_ANCHOR.MIDDLE, name=f"l_pt_{i}")
|
||||
P.add_dot(slide, P.SAFE_LEFT + 0.05, 2.7 + i * 0.7 + 0.18, color=P.GREY)
|
||||
P.add_textbox(slide, P.SAFE_LEFT + 0.45, 2.7 + i * 0.7,
|
||||
mid_x - P.SAFE_LEFT - 0.7, 0.55, p, 18, color=P.INK,
|
||||
anchor=MSO_ANCHOR.MIDDLE, name=f"l_pt_{i}")
|
||||
|
||||
# 右栏小标签
|
||||
add_rect(slide, mid_x + 0.3, 2.0, 0.8, 0.35, PRIMARY, "right_tag")
|
||||
add_textbox(slide, mid_x + 0.3, 2.0, 0.8, 0.35, "改进后", 14, bold=True,
|
||||
color=WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
|
||||
shrink=False, name="right_label")
|
||||
P.add_rect(slide, mid_x + 0.3, 2.0, 0.8, 0.35, P.PRIMARY, "right_tag")
|
||||
P.add_textbox(slide, mid_x + 0.3, 2.0, 0.8, 0.35, "改进后", 14, bold=True,
|
||||
color=P.WHITE, align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE,
|
||||
shrink=False, name="right_label")
|
||||
right_pts = ["改善 A: 描述", "改善 B: 描述", "改善 C: 描述"]
|
||||
for i, p in enumerate(right_pts):
|
||||
add_dot(slide, mid_x + 0.35, 2.7 + i * 0.7 + 0.18, color=ACCENT)
|
||||
add_textbox(slide, mid_x + 0.75, 2.7 + i * 0.7,
|
||||
SAFE_RIGHT - mid_x - 0.75, 0.55, p, 18, color=INK,
|
||||
anchor=MSO_ANCHOR.MIDDLE, name=f"r_pt_{i}")
|
||||
P.add_dot(slide, mid_x + 0.35, 2.7 + i * 0.7 + 0.18, color=P.ACCENT)
|
||||
P.add_textbox(slide, mid_x + 0.75, 2.7 + i * 0.7,
|
||||
P.SAFE_RIGHT - mid_x - 0.75, 0.55, p, 18, color=P.INK,
|
||||
anchor=MSO_ANCHOR.MIDDLE, name=f"r_pt_{i}")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -294,21 +188,22 @@ for i, p in enumerate(right_pts):
|
|||
## L6 · 图表为主 (Chart-focus) —— 标题 + 一句结论 + 大图
|
||||
|
||||
```python
|
||||
from pptx.util import Inches
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
# chart.png 已用 matplotlib 生成 (见 design_principles.md §7)
|
||||
slide = prs.slides.add_slide(BLANK)
|
||||
apply_brand(slide, "inner")
|
||||
page_title(slide, "季度营收持续增长")
|
||||
slide = P.add_slide(prs)
|
||||
P.apply_brand(slide, "inner")
|
||||
P.page_title(slide, "季度营收持续增长")
|
||||
# 一句话结论
|
||||
add_textbox(slide, SAFE_LEFT, SAFE_TOP + 1.1, SAFE_W, 0.5,
|
||||
"Q4 同比增长 158%,创历史新高", 18, color=GREY,
|
||||
name="lead")
|
||||
# 图表 (居中,占 9 寸宽,高度自适应)
|
||||
slide.shapes.add_picture("chart.png", Inches(2.2), Inches(2.4),
|
||||
width=Inches(8.9))
|
||||
P.add_textbox(slide, P.SAFE_LEFT, P.SAFE_TOP + 1.1, P.SAFE_W, 0.5,
|
||||
"Q4 同比增长 158%,创历史新高", 18, color=P.GREY, name="lead")
|
||||
# 图表 (居中,占 8.9 寸宽,高度自适应 —— 只给 width 等比缩放)
|
||||
slide.shapes.add_picture("<task_dir>/slides/chart.png", Inches(2.2),
|
||||
Inches(2.4), width=Inches(8.9))
|
||||
# 数据来源 (右下角弱化)
|
||||
add_textbox(slide, SAFE_LEFT, 6.95, SAFE_W, 0.4,
|
||||
"数据来源: 公司年报 2025", 11, color=GREY_LIGHT,
|
||||
align=PP_ALIGN.RIGHT, shrink=False, name="source")
|
||||
P.add_textbox(slide, P.SAFE_LEFT, 6.95, P.SAFE_W, 0.4,
|
||||
"数据来源: 公司年报 2025", 11, color=P.GREY_LIGHT,
|
||||
align=PP_ALIGN.RIGHT, shrink=False, name="source")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -318,21 +213,23 @@ add_textbox(slide, SAFE_LEFT, 6.95, SAFE_W, 0.4,
|
|||
> 之前用满铺图 + 半透明遮罩,效果不稳定。改成"图占 60% + 文字独立区"。
|
||||
|
||||
```python
|
||||
slide = prs.slides.add_slide(BLANK)
|
||||
# 左侧图占 60% 宽
|
||||
slide.shapes.add_picture("hero.jpg", Inches(0), Inches(0),
|
||||
width=Inches(8), height=Inches(7.5))
|
||||
from pptx.util import Inches
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
slide = P.add_slide(prs)
|
||||
# 左侧图占 60% 宽 (只给 width 或 height 一项,避免变形;此处图需正好铺满左 8 寸高 7.5 寸时按素材比例取舍)
|
||||
slide.shapes.add_picture("<task_dir>/slides/hero.jpg", Inches(0), Inches(0),
|
||||
height=Inches(7.5))
|
||||
# 右侧浅灰背景区放文字
|
||||
add_rect(slide, 8, 0, 5.33, 7.5, BG, "text_panel")
|
||||
add_rect(slide, 8.4, 1.0, 0.06, 0.8, ACCENT, "deco_bar") # 装饰短线
|
||||
add_textbox(slide, 8.4, 2.0, 4.6, 1.6, "走进未来", 36,
|
||||
bold=True, color=INK, name="img_title")
|
||||
add_textbox(slide, 8.4, 3.8, 4.6, 1.5,
|
||||
"用一两句话点出主旨,不要把演讲稿搬上来。",
|
||||
18, color=GREY, name="img_caption")
|
||||
P.add_rect(slide, 8, 0, 5.33, 7.5, P.BG, "text_panel")
|
||||
P.add_rect(slide, 8.4, 1.0, 0.06, 0.8, P.ACCENT, "deco_bar") # 装饰短线
|
||||
P.add_textbox(slide, 8.4, 2.0, 4.6, 1.6, "走进未来", 36,
|
||||
bold=True, color=P.INK, name="img_title")
|
||||
P.add_textbox(slide, 8.4, 3.8, 4.6, 1.5,
|
||||
"用一两句话点出主旨,不要把演讲稿搬上来。",
|
||||
18, color=P.GREY, name="img_caption")
|
||||
# 图标:右下角的箭头,引导视线
|
||||
add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 8.4, 6.5, 0.7, 0.35, ACCENT,
|
||||
"img_cta")
|
||||
P.add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 8.4, 6.5, 0.7, 0.35, P.ACCENT,
|
||||
"img_cta")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -340,20 +237,21 @@ add_shape(slide, MSO_SHAPE.RIGHT_ARROW, 8.4, 6.5, 0.7, 0.35, ACCENT,
|
|||
## L8 · 金句 / 大字 (Quote) —— 留白主导,装饰极简
|
||||
|
||||
```python
|
||||
slide = prs.slides.add_slide(BLANK)
|
||||
apply_brand(slide, "inner")
|
||||
# 左上大引号 (用 STAR 不合适;用字形)
|
||||
add_textbox(slide, 0.8, 0.6, 1.5, 1.5, '"', 200, bold=True,
|
||||
color=ACCENT, font=EN_FONT, shrink=False, name="quote_mark")
|
||||
from pptx.enum.text import MSO_ANCHOR
|
||||
slide = P.add_slide(prs)
|
||||
P.apply_brand(slide, "inner")
|
||||
# 左上大引号 (用字形;font=EN_FONT 走 Arial)
|
||||
P.add_textbox(slide, 0.8, 0.6, 1.5, 1.5, '"', 200, bold=True,
|
||||
color=P.ACCENT, font=P.EN_FONT, shrink=False, name="quote_mark")
|
||||
# 金句 (深色,留白多)
|
||||
add_textbox(slide, 1.5, 2.7, 10.5, 2.0,
|
||||
"把复杂留给我们,把简单留给用户。", 36, bold=True,
|
||||
color=INK, anchor=MSO_ANCHOR.MIDDLE, name="quote_text")
|
||||
P.add_textbox(slide, 1.5, 2.7, 10.5, 2.0,
|
||||
"把复杂留给我们,把简单留给用户。", 36, bold=True,
|
||||
color=P.INK, anchor=MSO_ANCHOR.MIDDLE, name="quote_text")
|
||||
# 装饰短线
|
||||
add_accent_line(slide, 1.5, 5.0, length=0.5, color=ACCENT)
|
||||
P.add_accent_line(slide, 1.5, 5.0, length=0.5)
|
||||
# 出处
|
||||
add_textbox(slide, 1.5, 5.2, 10.5, 0.5, "—— 公司价值观 2025",
|
||||
16, color=GREY, name="quote_attr")
|
||||
P.add_textbox(slide, 1.5, 5.2, 10.5, 0.5, "—— 公司价值观 2025",
|
||||
16, color=P.GREY, name="quote_attr")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -363,16 +261,17 @@ add_textbox(slide, 1.5, 5.2, 10.5, 0.5, "—— 公司价值观 2025",
|
|||
> **不是可选** —— 任何 deck 都必须以这页收尾。无论是汇报、提案、路演,缺尾页等于"话没说完"。
|
||||
|
||||
```python
|
||||
slide = prs.slides.add_slide(BLANK)
|
||||
apply_brand(slide, "end") # 整页浅灰 + 顶/底强调短线
|
||||
add_textbox(slide, 0, 2.5, SLIDE_W, 1.6, "Thank You", 80, bold=True,
|
||||
color=PRIMARY, align=PP_ALIGN.CENTER, font=EN_FONT,
|
||||
name="thanks")
|
||||
add_textbox(slide, 0, 4.3, SLIDE_W, 0.6, "欢迎提问与讨论",
|
||||
22, color=ACCENT, align=PP_ALIGN.CENTER, name="qa")
|
||||
add_textbox(slide, 0, 6.2, SLIDE_W, 0.5,
|
||||
"联系方式 / 邮箱 / 公众号", 14, color=GREY_LIGHT,
|
||||
align=PP_ALIGN.CENTER, name="contact")
|
||||
from pptx.enum.text import PP_ALIGN
|
||||
slide = P.add_slide(prs)
|
||||
P.apply_brand(slide, "end") # 整页浅灰 + 顶/底强调短线
|
||||
P.add_textbox(slide, 0, 2.5, P.SLIDE_W, 1.6, "Thank You", 80, bold=True,
|
||||
color=P.PRIMARY, align=PP_ALIGN.CENTER, font=P.EN_FONT,
|
||||
name="thanks")
|
||||
P.add_textbox(slide, 0, 4.3, P.SLIDE_W, 0.6, "欢迎提问与讨论",
|
||||
22, color=P.ACCENT, align=PP_ALIGN.CENTER, name="qa")
|
||||
P.add_textbox(slide, 0, 6.2, P.SLIDE_W, 0.5,
|
||||
"联系方式 / 邮箱 / 公众号", 14, color=P.GREY_LIGHT,
|
||||
align=PP_ALIGN.CENTER, name="contact")
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -392,6 +291,7 @@ add_textbox(slide, 0, 6.2, SLIDE_W, 0.5,
|
|||
|
||||
## 三个常犯的越界场景
|
||||
|
||||
1. **bullet 字数超额** —— 22pt 在 11.5 寸宽下每行约 50 个中文字。超过 1 行就溢出 0.7 in 高的框。**用 `assert_inside` + `auto_size=TEXT_TO_SHAPE_AND_FIT_TEXT` 兜底**;但根本解法是**字数压缩**(见 design_principles.md §字数预算)
|
||||
1. **bullet 字数超额** —— 22pt 在 11.5 寸宽下每行约 50 个中文字。超过 1 行就溢出 0.7 in 高的框。`add_textbox` 内置 `assert_inside` + shrink-to-fit 兜底;但根本解法是**字数压缩**(见 design_principles.md §字数预算)
|
||||
2. **标题占两行** —— 标题在 0.7 in 高的框里,32pt 单行高约 0.45 in,**两行就溢出**。中文标题 ≤ 30 字
|
||||
3. **图片不等比拉伸** —— `add_picture(width=, height=)` 同时给会变形;**只给 width 或 height 一项**
|
||||
```
|
||||
|
|
|
|||
|
|
@ -0,0 +1,316 @@
|
|||
"""pptx_helpers.py — PPT skill 的共享版式工具箱。
|
||||
|
||||
逐页生成时**每页一个 run_python**(载入已有 .pptx → append 一页 → save),
|
||||
这些 helper 以前要在每页里重新默写一遍 —— 既烧 token 又会在长 deck 里漂移
|
||||
(第 7 页的 apply_brand 坐标和第 2 页写得不一样)。收进本模块后,每页只 import。
|
||||
|
||||
用法(在 run_python block 顶部):
|
||||
|
||||
import sys; sys.path.insert(0, "<skill_dir>/scripts") # <skill_dir> 用 system prompt 注入值
|
||||
import pptx_helpers as P
|
||||
|
||||
# —— 第一页(创建)——
|
||||
prs = P.new_presentation("16:9") # 默认 16:9,可传 4:3 / 9:16 / 3:4
|
||||
P.set_palette(spec_path="<task_dir>/...spec.md") # 默认商务红;spec 覆盖了才需要
|
||||
slide = P.add_slide(prs)
|
||||
P.apply_brand(slide, "cover")
|
||||
P.add_textbox(slide, 0.9, 2.6, 11.9, 1.4, "标题", 44, bold=True, color=P.INK)
|
||||
prs.save("<task_dir>/<topic>.pptx")
|
||||
|
||||
# —— 后续页(追加)——
|
||||
prs = P.load("<task_dir>/<topic>.pptx") # 从文件实际尺寸回填画布常量
|
||||
P.set_palette(spec_path="<task_dir>/...spec.md") # 每页都重读 spec(同 SKILL.md 规则)
|
||||
slide = P.add_slide(prs)
|
||||
...
|
||||
prs.save("<task_dir>/<topic>.pptx")
|
||||
|
||||
⚠️ 一律用 `P.xxx` 访问颜色常量与函数 —— set_palette 靠改模块属性生效,
|
||||
`from pptx_helpers import *` 会把旧绑定拷进页面命名空间,覆盖配色不生效。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from pptx import Presentation
|
||||
from pptx.util import Inches, Pt
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR, MSO_AUTO_SIZE
|
||||
from pptx.enum.shapes import MSO_SHAPE
|
||||
from pptx.oxml.ns import qn
|
||||
|
||||
# ============================================================
|
||||
# 配色 (商务红 — 硬约束默认)
|
||||
# ============================================================
|
||||
# ⛔ 不允许擅自换色:除非用户明确点名其它配色 或 spec 已写其它 hex,否则就是这套红。
|
||||
# 要换走 set_palette(),禁止以"这场景蓝色更专业"这类自我合理化做替换。
|
||||
PRIMARY = RGBColor(0xC0, 0x00, 0x00) # 深红 - 标题/强调/关键数据
|
||||
SECONDARY = RGBColor(0xE1, 0x55, 0x54) # 砖红 - 次要图形
|
||||
ACCENT = RGBColor(0xFF, 0xC1, 0x07) # 金黄 - 关键数据点/CTA
|
||||
INK = RGBColor(0x1F, 0x1F, 0x1F)
|
||||
GREY = RGBColor(0x59, 0x59, 0x59)
|
||||
GREY_LIGHT = RGBColor(0x88, 0x88, 0x88)
|
||||
HAIRLINE = RGBColor(0xDD, 0xDD, 0xDD) # 细分隔线
|
||||
BG = RGBColor(0xFA, 0xFA, 0xFA) # 背景近白
|
||||
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
|
||||
|
||||
CN_FONT = "微软雅黑" # 中文字形走 <a:ea> 槽位
|
||||
EN_FONT = "Arial" # 拉丁字形走 <a:latin> 槽位
|
||||
|
||||
# ============================================================
|
||||
# 画布与安全区 (new_presentation / load 会按实际尺寸回填这些)
|
||||
# ============================================================
|
||||
SLIDE_W = 13.33
|
||||
SLIDE_H = 7.5
|
||||
MARGIN_X = 0.7
|
||||
MARGIN_Y = 0.5
|
||||
SAFE_LEFT = MARGIN_X
|
||||
SAFE_TOP = MARGIN_Y
|
||||
SAFE_RIGHT = SLIDE_W - MARGIN_X
|
||||
SAFE_BOTTOM = SLIDE_H - MARGIN_Y
|
||||
SAFE_W = SAFE_RIGHT - SAFE_LEFT
|
||||
SAFE_H = SAFE_BOTTOM - SAFE_TOP
|
||||
|
||||
_CANVAS = {
|
||||
"16:9": (13.33, 7.5),
|
||||
"4:3": (10.0, 7.5),
|
||||
"9:16": (7.5, 13.33),
|
||||
"3:4": (7.5, 10.0),
|
||||
}
|
||||
|
||||
|
||||
def _recompute_safe() -> None:
|
||||
global SAFE_LEFT, SAFE_TOP, SAFE_RIGHT, SAFE_BOTTOM, SAFE_W, SAFE_H
|
||||
SAFE_LEFT = MARGIN_X
|
||||
SAFE_TOP = MARGIN_Y
|
||||
SAFE_RIGHT = SLIDE_W - MARGIN_X
|
||||
SAFE_BOTTOM = SLIDE_H - MARGIN_Y
|
||||
SAFE_W = SAFE_RIGHT - SAFE_LEFT
|
||||
SAFE_H = SAFE_BOTTOM - SAFE_TOP
|
||||
|
||||
|
||||
def new_presentation(canvas: str = "16:9") -> Presentation:
|
||||
"""建空白 deck 并设画布尺寸,同步回填模块的安全区常量。第一页用。"""
|
||||
global SLIDE_W, SLIDE_H
|
||||
if canvas not in _CANVAS:
|
||||
raise ValueError(f"未知画布 {canvas!r},支持 {list(_CANVAS)}")
|
||||
SLIDE_W, SLIDE_H = _CANVAS[canvas]
|
||||
_recompute_safe()
|
||||
prs = Presentation()
|
||||
prs.slide_width = Inches(SLIDE_W)
|
||||
prs.slide_height = Inches(SLIDE_H)
|
||||
return prs
|
||||
|
||||
|
||||
def load(path) -> Presentation:
|
||||
"""载入已有 deck,并按文件实际尺寸回填模块画布常量(逐页进程间自动同步)。"""
|
||||
global SLIDE_W, SLIDE_H
|
||||
prs = Presentation(str(path))
|
||||
SLIDE_W = prs.slide_width / 914400
|
||||
SLIDE_H = prs.slide_height / 914400
|
||||
_recompute_safe()
|
||||
return prs
|
||||
|
||||
|
||||
def add_slide(prs: Presentation):
|
||||
"""追加一张空白版式(layout 6)的 slide。"""
|
||||
return prs.slides.add_slide(prs.slide_layouts[6])
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 配色覆盖 (默认商务红;spec 写了别的色才调)
|
||||
# ============================================================
|
||||
def _to_rgb(h: str) -> RGBColor:
|
||||
return RGBColor.from_string(h.lstrip("#").upper())
|
||||
|
||||
|
||||
def set_palette(primary: str | None = None, secondary: str | None = None,
|
||||
accent: str | None = None, cn_font: str | None = None,
|
||||
en_font: str | None = None, spec_path=None) -> None:
|
||||
"""覆盖主题色 / 字体。逐页生成时每页都调一次(对齐 SKILL.md「每页重读 spec」)。
|
||||
|
||||
- 显式传 primary/secondary/accent(hex,带不带 # 都行)即覆盖对应色。
|
||||
- 传 spec_path:从 spec.md 按文档顺序取前 3 个 #hex 作 主/辅/强调
|
||||
(spec 模板里配色行是 hex 唯一出现处)。找不到则保持商务红默认。
|
||||
- 都不传 = 维持商务红,无副作用。
|
||||
"""
|
||||
global PRIMARY, SECONDARY, ACCENT, CN_FONT, EN_FONT
|
||||
if spec_path:
|
||||
p = Path(spec_path)
|
||||
if p.exists():
|
||||
hexes = re.findall(r"#([0-9A-Fa-f]{6})", p.read_text(encoding="utf-8"))
|
||||
if len(hexes) >= 1 and primary is None:
|
||||
primary = hexes[0]
|
||||
if len(hexes) >= 2 and secondary is None:
|
||||
secondary = hexes[1]
|
||||
if len(hexes) >= 3 and accent is None:
|
||||
accent = hexes[2]
|
||||
if primary:
|
||||
PRIMARY = _to_rgb(primary)
|
||||
if secondary:
|
||||
SECONDARY = _to_rgb(secondary)
|
||||
if accent:
|
||||
ACCENT = _to_rgb(accent)
|
||||
if cn_font:
|
||||
CN_FONT = cn_font
|
||||
if en_font:
|
||||
EN_FONT = en_font
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 安全区校验
|
||||
# ============================================================
|
||||
def assert_inside(left, top, width, height, name="") -> None:
|
||||
"""放置前调一次。越界直接报错而不是悄悄超出。"""
|
||||
if left < 0 or top < 0:
|
||||
raise ValueError(f"[{name}] 左/上为负: ({left}, {top})")
|
||||
if left + width > SLIDE_W + 1e-3:
|
||||
raise ValueError(f"[{name}] 右越界: {left}+{width} > {SLIDE_W}")
|
||||
if top + height > SLIDE_H + 1e-3:
|
||||
raise ValueError(f"[{name}] 下越界: {top}+{height} > {SLIDE_H}")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 文本辅助
|
||||
# ============================================================
|
||||
def _apply_run_font(run, size, bold, color, latin_font, ea_font) -> None:
|
||||
"""设字号/粗细/颜色 + 同时设 latin(拉丁)与 ea/cs(东亚)字体。
|
||||
|
||||
关键:python-pptx 的 `run.font.name = x` 只写 <a:latin>。中文字形走 <a:ea>
|
||||
槽位,不设的话会落到主题默认字体 —— 这就是「指定了微软雅黑却没真生效」的根因。
|
||||
这里 latin=英文体、ea/cs=中文体,中英混排各自命中正确字体。
|
||||
"""
|
||||
run.font.size = Pt(size)
|
||||
run.font.bold = bold
|
||||
run.font.color.rgb = color
|
||||
run.font.name = latin_font # <a:latin>
|
||||
rPr = run._r.get_or_add_rPr()
|
||||
for tag in ("a:ea", "a:cs"):
|
||||
el = rPr.find(qn(tag))
|
||||
if el is None:
|
||||
el = rPr.makeelement(qn(tag), {})
|
||||
rPr.append(el)
|
||||
el.set("typeface", ea_font)
|
||||
|
||||
|
||||
def set_text(tf, text, size, bold=False, color=INK, align=PP_ALIGN.LEFT,
|
||||
font=None) -> None:
|
||||
"""写单段文本并设样式。font=None → 拉丁 EN_FONT + 东亚 CN_FONT(推荐);
|
||||
传 font 则 latin 与 ea 都用它(纯英文大字 / 纯数字时用)。"""
|
||||
latin = font or EN_FONT
|
||||
ea = font or CN_FONT
|
||||
tf.text = text
|
||||
p = tf.paragraphs[0]
|
||||
p.alignment = align
|
||||
_apply_run_font(p.runs[0], size, bold, color, latin, ea)
|
||||
|
||||
|
||||
def add_textbox(slide, left, top, width, height, text, size,
|
||||
bold=False, color=INK, align=PP_ALIGN.LEFT,
|
||||
anchor=MSO_ANCHOR.TOP, font=None, shrink=True,
|
||||
name="textbox"):
|
||||
"""加文本框。默认 word_wrap + shrink-to-fit 兜底(不替代字数预算)。"""
|
||||
assert_inside(left, top, width, height, name)
|
||||
tb = slide.shapes.add_textbox(Inches(left), Inches(top),
|
||||
Inches(width), Inches(height))
|
||||
tf = tb.text_frame
|
||||
tf.vertical_anchor = anchor
|
||||
tf.word_wrap = True
|
||||
if shrink:
|
||||
tf.auto_size = MSO_AUTO_SIZE.TEXT_TO_FIT_SHAPE
|
||||
set_text(tf, text, size, bold, color, align, font)
|
||||
return tb
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 形状辅助 (无边线实心填充)
|
||||
# ============================================================
|
||||
def add_rect(slide, left, top, width, height, fill, name="rect"):
|
||||
assert_inside(left, top, width, height, name)
|
||||
s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, Inches(left), Inches(top),
|
||||
Inches(width), Inches(height))
|
||||
s.fill.solid()
|
||||
s.fill.fore_color.rgb = fill
|
||||
s.line.fill.background()
|
||||
return s
|
||||
|
||||
|
||||
def add_shape(slide, kind, left, top, width, height, fill, name="shape"):
|
||||
assert_inside(left, top, width, height, name)
|
||||
s = slide.shapes.add_shape(kind, Inches(left), Inches(top),
|
||||
Inches(width), Inches(height))
|
||||
s.fill.solid()
|
||||
s.fill.fore_color.rgb = fill
|
||||
s.line.fill.background()
|
||||
return s
|
||||
|
||||
|
||||
def add_dot(slide, x, y, size=0.18, color=None):
|
||||
return add_shape(slide, MSO_SHAPE.OVAL, x, y, size, size,
|
||||
ACCENT if color is None else color, "dot")
|
||||
|
||||
|
||||
def add_accent_line(slide, x, y, length=1.0, thickness=0.05, color=None):
|
||||
"""标题下面那条强调线,替代大色块。"""
|
||||
return add_rect(slide, x, y, length, thickness,
|
||||
ACCENT if color is None else color, "accent_line")
|
||||
|
||||
|
||||
def add_badge(slide, x, y, num, diameter=0.7, fill=None, fg=None):
|
||||
"""编号徽章 (圆 + 数字)。"""
|
||||
c = add_shape(slide, MSO_SHAPE.OVAL, x, y, diameter, diameter,
|
||||
PRIMARY if fill is None else fill, "badge")
|
||||
tf = c.text_frame
|
||||
tf.text = str(num)
|
||||
p = tf.paragraphs[0]
|
||||
p.alignment = PP_ALIGN.CENTER
|
||||
_apply_run_font(p.runs[0], int(diameter * 28), True,
|
||||
WHITE if fg is None else fg, EN_FONT, EN_FONT)
|
||||
return c
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 标题套件 (内页通用)
|
||||
# ============================================================
|
||||
def page_title(slide, text, page_num=None, total=None, footer="项目汇报"):
|
||||
add_textbox(slide, SAFE_LEFT, SAFE_TOP, SAFE_W, 0.7, text,
|
||||
32, bold=True, color=PRIMARY, name="title")
|
||||
add_accent_line(slide, SAFE_LEFT, SAFE_TOP + 0.85, length=0.8)
|
||||
if page_num is not None and total is not None:
|
||||
add_textbox(slide, SAFE_LEFT, SLIDE_H - 0.5, 6, 0.4, footer,
|
||||
11, color=GREY_LIGHT, shrink=False, name="footer")
|
||||
add_textbox(slide, SLIDE_W - 1.33, SLIDE_H - 0.5, 1.2, 0.4,
|
||||
f"{page_num} / {total}", 11, color=GREY_LIGHT,
|
||||
align=PP_ALIGN.RIGHT, shrink=False, name="page_num")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 品牌条 (每页起手必调,确保不是裸白纸)
|
||||
# ============================================================
|
||||
def apply_brand(slide, kind="inner"):
|
||||
"""统一品牌锚点。每个版式第一行调用。
|
||||
cover —— 左侧主色长竖条 + 顶部短横 + 底部细灰线
|
||||
inner —— (默认) 左侧主色窄条 + 底部细灰线
|
||||
section —— 整页浅灰 + 左侧主色竖条 + 强调色粗竖条
|
||||
end —— 整页浅灰 + 顶/底强调色短线
|
||||
"""
|
||||
btm = SLIDE_H - 0.32
|
||||
if kind == "cover":
|
||||
add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
|
||||
add_rect(slide, 0.7, 0.6, 0.8, 0.06, PRIMARY, "brand_top_line")
|
||||
add_rect(slide, SAFE_LEFT, btm, SAFE_W, 0.02, HAIRLINE,
|
||||
"brand_btm_hairline")
|
||||
elif kind == "section":
|
||||
add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
|
||||
add_rect(slide, 0, 0, 0.18, SLIDE_H, PRIMARY, "brand_left_bar")
|
||||
add_rect(slide, 0.7, SLIDE_H / 3, 0.08, SLIDE_H / 3, ACCENT,
|
||||
"brand_section_bar")
|
||||
elif kind == "end":
|
||||
add_rect(slide, 0, 0, SLIDE_W, SLIDE_H, BG, "brand_bg")
|
||||
add_rect(slide, SAFE_LEFT, 0.6, 0.8, 0.06, ACCENT, "brand_top_line")
|
||||
add_rect(slide, SAFE_RIGHT - 0.8, SLIDE_H - 0.65, 0.8, 0.06, ACCENT,
|
||||
"brand_btm_line")
|
||||
else: # inner
|
||||
add_rect(slide, 0, 0, 0.10, SLIDE_H, PRIMARY, "brand_left_bar")
|
||||
add_rect(slide, SAFE_LEFT, btm, SAFE_W, 0.02, HAIRLINE,
|
||||
"brand_btm_hairline")
|
||||
|
|
@ -9,7 +9,8 @@
|
|||
- 每页有标题
|
||||
- 每页 bullet ≤ 5 条
|
||||
- 文字字号 ≥ 14pt (除页脚)
|
||||
- 颜色集合 ≤ 5 种 (粗略统计)
|
||||
- 非灰阶(彩色)≤ 3 种 (三色制;文字色 + 形状填充色都计,灰阶/白不计)
|
||||
- 出现 spec 之外的非灰阶色 (擅自换色 / 非主题色)
|
||||
- 没有 untitled / output / placeholder 等占位文件名
|
||||
- **形状不越出画布边界** (left+width / top+height 超界即报)
|
||||
- **textbox 文本估算行数 > 框高度** —— 推断溢出
|
||||
|
|
@ -29,11 +30,37 @@ from pathlib import Path
|
|||
try:
|
||||
from pptx import Presentation
|
||||
from pptx.util import Pt
|
||||
from pptx.enum.dml import MSO_FILL, MSO_COLOR_TYPE
|
||||
except ImportError:
|
||||
print("[fatal] pip install python-pptx", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
# ---- 颜色辅助 ----
|
||||
|
||||
def _is_neutral(hex6: str) -> bool:
|
||||
"""灰阶/黑/白判定:R/G/B 极差 ≤ 12 即视为中性色(三色制里不计入彩色)。"""
|
||||
try:
|
||||
r, g, b = int(hex6[0:2], 16), int(hex6[2:4], 16), int(hex6[4:6], 16)
|
||||
except (ValueError, IndexError):
|
||||
return False
|
||||
return max(r, g, b) - min(r, g, b) <= 12
|
||||
|
||||
|
||||
def _shape_fill_hex(shape) -> str | None:
|
||||
"""取形状的纯色填充 hex(大写,无 #)。非实心 / 主题色 / 取不到 → None。"""
|
||||
try:
|
||||
fill = shape.fill
|
||||
if fill.type != MSO_FILL.SOLID:
|
||||
return None
|
||||
fc = fill.fore_color
|
||||
if fc.type != MSO_COLOR_TYPE.RGB: # 主题色访问 .rgb 会抛,先挡掉
|
||||
return None
|
||||
return str(fc.rgb).upper()
|
||||
except (TypeError, AttributeError, KeyError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
# ---- spec 解析 (松散 markdown 解析,够用就行) ----
|
||||
|
||||
def parse_spec(spec_path: Path) -> dict:
|
||||
|
|
@ -131,6 +158,11 @@ def check_pptx(path: Path, spec: dict) -> tuple[list, list]:
|
|||
f"(画布 {slide_h_in:.2f},shape 底 {top_in + h_in:.2f})"
|
||||
)
|
||||
|
||||
# ---- 形状填充色 (品牌条/徽章/圆点/标签/底块) ----
|
||||
fill_hex = _shape_fill_hex(shape)
|
||||
if fill_hex:
|
||||
seen_colors.add(fill_hex)
|
||||
|
||||
if not shape.has_text_frame:
|
||||
continue
|
||||
tf = shape.text_frame
|
||||
|
|
@ -211,16 +243,22 @@ def check_pptx(path: Path, spec: dict) -> tuple[list, list]:
|
|||
f"第 {idx} 页有 {small_font_count} 处字号 < 14pt,投影看不清"
|
||||
)
|
||||
|
||||
if len(seen_colors) > 6:
|
||||
# 三色制按"非灰阶色"判定:灰/黑/白不计 (design_principles §2「其他全部用灰阶」)
|
||||
chromatic = {c for c in seen_colors if not _is_neutral(c)}
|
||||
if len(chromatic) > 3:
|
||||
warnings.append(
|
||||
f"颜色 {len(seen_colors)} 种 (含不同灰阶),理想 ≤ 5;考虑收敛到三色制"
|
||||
f"非灰阶色 {len(chromatic)} 种 (三色制上限 3): "
|
||||
f"{', '.join('#' + c for c in sorted(chromatic))};收敛到主/辅/强调三色"
|
||||
)
|
||||
|
||||
if spec_colors and seen_colors:
|
||||
unmatched = seen_colors - spec_colors
|
||||
if len(unmatched) > 3:
|
||||
if spec_colors:
|
||||
spec_chromatic = {c for c in spec_colors if not _is_neutral(c)}
|
||||
extra = chromatic - spec_chromatic
|
||||
if extra:
|
||||
warnings.append(
|
||||
f"出现 {len(unmatched)} 个 spec 之外的颜色,可能用了 matplotlib 默认色板"
|
||||
f"出现 spec 之外的非灰阶色 {', '.join('#' + c for c in sorted(extra))};"
|
||||
f"擅自换色 / 非主题色 (spec 定的是 "
|
||||
f"{', '.join('#' + c for c in sorted(spec_chromatic))})"
|
||||
)
|
||||
|
||||
return errors, warnings
|
||||
|
|
|
|||
Loading…
Reference in New Issue