_gen_mes_ppt.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. # -*- coding: utf-8 -*-
  2. """生成六安裕洋MES实施进度汇报 PPT(含甘特图)"""
  3. from __future__ import annotations
  4. import shutil
  5. from datetime import date, timedelta
  6. from pathlib import Path
  7. from PIL import Image
  8. from pptx import Presentation
  9. from pptx.dml.color import RGBColor
  10. from pptx.enum.shapes import MSO_SHAPE
  11. from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
  12. from pptx.oxml.ns import nsmap
  13. from pptx.oxml import parse_xml
  14. from pptx.util import Inches, Pt, Emu
  15. # ---------- 路径 ----------
  16. PIC_DIR = Path(r"C:\Users\j2742\Desktop\temp\大洋\pic")
  17. ASSETS_DIR = Path(r"C:\Users\j2742\.cursor\projects\c-work-demo-dayang-mescloud\assets")
  18. OUT1 = Path(r"C:\Users\j2742\Desktop\temp\大洋\实施文档\六安裕洋MES实施进度汇报.pptx")
  19. OUT2 = Path(r"c:\work\demo\dayang\mescloud\docs\六安裕洋MES实施进度汇报.pptx")
  20. CACHE_DIR = Path(r"c:\work\demo\dayang\mescloud\_ppt_img_cache")
  21. # 用户精选实时效果图(第2页)
  22. EFFECT_IMAGES = [
  23. ("生产管理大屏", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-eb11b4ec-13e0-4792-833d-13a7d304a2a5.png"),
  24. ("设备综合效率", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-74f8c1fc-7426-4472-9dda-a4abdb79cc23.png"),
  25. ("能源监控大屏", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-1759c9f1-d702-4b17-b35b-0d8d8cfd66cf.png"),
  26. ("ERP排产看板", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-220cd888-53b8-4247-b74f-9b3b5d56a8bf.png"),
  27. ("生产追溯明细", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-a4a18a5e-f2ab-42a3-a9c5-2bcfae5d1233.png"),
  28. ("白件气密管理", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-8dd92c21-a7aa-48d0-81a3-4a1107492a9a.png"),
  29. ("弧焊过程参数", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_image-15f57559-f837-464f-af8a-b4e656767283.png"),
  30. ("PDA产线扫码", ASSETS_DIR / "c__Users_j2742_AppData_Roaming_Cursor_User_workspaceStorage_empty-window_images_60b4a34fc5a9f4ca3195ea55e3a2ace1-787d700c-4131-4806-abbf-ff01c10c9679.png"),
  31. ]
  32. # ---------- 配色 ----------
  33. HEADER_BG = RGBColor(0x0B, 0x3D, 0x5C)
  34. ACCENT_GREEN = RGBColor(0x1B, 0x8A, 0x5A)
  35. TEAL_BAR = RGBColor(0x0D, 0x7A, 0x6F)
  36. TEAL_LIGHT = RGBColor(0x2A, 0xA8, 0x98)
  37. AXIS_GRAY = RGBColor(0x90, 0xA4, 0xAE)
  38. TEXT_DARK = RGBColor(0x1A, 0x2A, 0x3A)
  39. TEXT_MUTED = RGBColor(0x5A, 0x6A, 0x7A)
  40. WHITE = RGBColor(0xFF, 0xFF, 0xFF)
  41. CARD_BG = RGBColor(0xF4, 0xF7, 0xFA)
  42. ROW_ALT = RGBColor(0xEE, 0xF4, 0xF8)
  43. LINE_SOFT = RGBColor(0xD0, 0xDC, 0xE4)
  44. SLIDE_W = Inches(13.333)
  45. SLIDE_H = Inches(7.5)
  46. def set_run_font(run, name="微软雅黑", size=12, bold=False, color=TEXT_DARK):
  47. run.font.name = name
  48. run.font.size = Pt(size)
  49. run.font.bold = bold
  50. run.font.color.rgb = color
  51. # 东亚字体
  52. rPr = run._r.get_or_add_rPr()
  53. ea = rPr.find("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}rFonts")
  54. # 使用 a:ea / a:latin
  55. from pptx.oxml.ns import qn
  56. for tag in ("latin", "ea", "cs"):
  57. el = rPr.find(qn(f"a:{tag}"))
  58. if el is None:
  59. el = parse_xml(
  60. f'<a:{tag} xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" typeface="{name}"/>'
  61. )
  62. rPr.append(el)
  63. else:
  64. el.set("typeface", name)
  65. def add_textbox(slide, left, top, width, height, text, size=12, bold=False, color=TEXT_DARK, align=PP_ALIGN.LEFT, font="微软雅黑"):
  66. box = slide.shapes.add_textbox(left, top, width, height)
  67. tf = box.text_frame
  68. tf.word_wrap = True
  69. p = tf.paragraphs[0]
  70. p.alignment = align
  71. run = p.add_run()
  72. run.text = text
  73. set_run_font(run, font, size, bold, color)
  74. return box
  75. def add_rect(slide, left, top, width, height, fill, line=None):
  76. shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, left, top, width, height)
  77. shape.fill.solid()
  78. shape.fill.fore_color.rgb = fill
  79. if line is None:
  80. shape.line.fill.background()
  81. else:
  82. shape.line.color.rgb = line
  83. shape.line.width = Pt(0.75)
  84. return shape
  85. def add_round_rect(slide, left, top, width, height, fill, line=None):
  86. shape = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, left, top, width, height)
  87. shape.fill.solid()
  88. shape.fill.fore_color.rgb = fill
  89. if line is None:
  90. shape.line.fill.background()
  91. else:
  92. shape.line.color.rgb = line
  93. shape.line.width = Pt(0.75)
  94. # 较小圆角
  95. try:
  96. shape.adjustments[0] = 0.08
  97. except Exception:
  98. pass
  99. return shape
  100. def date_to_x(d: date, axis_start: date, axis_end: date, chart_left, chart_width):
  101. total = (axis_end - axis_start).days
  102. if total <= 0:
  103. return chart_left
  104. ratio = max(0.0, min(1.0, (d - axis_start).days / total))
  105. return chart_left + int(chart_width * ratio)
  106. def resize_image(src: Path, max_w: int = 1000) -> Path:
  107. CACHE_DIR.mkdir(parents=True, exist_ok=True)
  108. out = CACHE_DIR / f"{src.stem}_w{max_w}{src.suffix.lower()}"
  109. if out.exists() and out.stat().st_mtime >= src.stat().st_mtime:
  110. return out
  111. img = Image.open(src)
  112. if img.mode in ("RGBA", "P"):
  113. img = img.convert("RGB")
  114. w, h = img.size
  115. if w > max_w:
  116. nh = int(h * max_w / w)
  117. img = img.resize((max_w, nh), Image.Resampling.LANCZOS)
  118. # JPEG 统一
  119. if out.suffix.lower() in (".jpg", ".jpeg"):
  120. img.save(out, "JPEG", quality=88, optimize=True)
  121. else:
  122. img.save(out, "PNG", optimize=True)
  123. return out
  124. def find_image(candidates: list[str]) -> Path | None:
  125. for name in candidates:
  126. p = PIC_DIR / name
  127. if p.exists():
  128. return p
  129. return None
  130. # ---------- 甘特数据(实施阶段)----------
  131. # done=True 已完成;done=False 本月推进中
  132. GANTT_ROWS = [
  133. ("阶段一 环境准备", date(2026, 4, 27), date(2026, 5, 6), True),
  134. ("阶段二 软件预开发", date(2026, 4, 30), date(2026, 5, 6), True),
  135. ("阶段三 蓝图设计", date(2026, 5, 7), date(2026, 6, 1), True),
  136. ("阶段四 硬件与工位对接", date(2026, 5, 8), date(2026, 5, 16), True),
  137. ("阶段五 流程与主数据", date(2026, 5, 18), date(2026, 5, 30), True),
  138. ("阶段六 多系统对接", date(2026, 6, 1), date(2026, 6, 18), True),
  139. ("阶段七 培训驻场", date(2026, 6, 22), date(2026, 6, 27), True),
  140. ("七月 上线陪产", date(2026, 7, 1), date(2026, 7, 24), False),
  141. ]
  142. AXIS_START = date(2026, 4, 27)
  143. AXIS_END = date(2026, 7, 24)
  144. TODAY = date(2026, 7, 21)
  145. ORANGE_BAR = RGBColor(0xE0, 0x8A, 0x2E)
  146. ORANGE_SOFT = RGBColor(0xF5, 0xE6, 0xD0)
  147. WEEKLY_PROGRESS = [
  148. ("阶段三", "5/7-5/10", "五月第一周", "8", "8", "100%"),
  149. ("阶段四", "5/11-5/16", "五月第二周", "9", "9", "100%"),
  150. ("阶段四&五", "5/18-5/23", "五月第三周", "9", "9", "100%"),
  151. ("阶段五", "5/25-5/30", "五月第四周", "7", "7", "100%"),
  152. ("阶段六", "6/1-6/6", "六月第一周", "7", "7", "100%"),
  153. ("阶段六", "6/8-6/13", "六月第二周", "6", "6", "100%"),
  154. ("阶段六", "6/13-6/18", "六月第三周", "10", "10", "100%"),
  155. ("阶段七", "6/22-6/27", "六月第四周", "8", "8", "100%"),
  156. ]
  157. def build_slide1(prs: Presentation):
  158. slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
  159. # 顶栏
  160. add_rect(slide, 0, 0, SLIDE_W, Inches(0.88), HEADER_BG)
  161. add_textbox(
  162. slide, Inches(0.28), Inches(0.10), Inches(8.8), Inches(0.40),
  163. "六安裕洋MES项目实施进度汇报", size=24, bold=True, color=WHITE,
  164. )
  165. add_textbox(
  166. slide, Inches(0.28), Inches(0.50), Inches(9.2), Inches(0.30),
  167. "京宜安 · 截至2026年7月21日 · 5–6月周任务64项已100%完成 · 本月聚焦现场培训/实际上线/陪产",
  168. size=11, bold=False, color=RGBColor(0xB8, 0xD4, 0xE8),
  169. )
  170. # 右上统计胶囊
  171. stats = [("8周次", "5–6月"), ("64/64", "周任务完成"), ("100%", "合格率"), ("7月", "上线陪产")]
  172. x0 = Inches(9.05)
  173. for i, (v, lab) in enumerate(stats):
  174. left = x0 + Inches(i * 1.02)
  175. add_round_rect(slide, left, Inches(0.12), Inches(0.96), Inches(0.64), RGBColor(0x0E, 0x52, 0x78))
  176. tone = ORANGE_BAR if i == 3 else (ACCENT_GREEN if i == 2 else WHITE)
  177. add_textbox(slide, left, Inches(0.14), Inches(0.96), Inches(0.34), v, size=13, bold=True, color=tone, align=PP_ALIGN.CENTER)
  178. add_textbox(slide, left, Inches(0.44), Inches(0.96), Inches(0.28), lab, size=8, color=RGBColor(0xA8, 0xC8, 0xDC), align=PP_ALIGN.CENTER)
  179. # ===== 左侧:周任务明细表 =====
  180. table_left = Inches(0.22)
  181. table_top = Inches(1.00)
  182. table_w = Inches(5.55)
  183. table_h = Inches(4.55)
  184. add_round_rect(slide, table_left, table_top, table_w, table_h, CARD_BG)
  185. add_textbox(
  186. slide, Inches(0.38), Inches(1.08), Inches(5.2), Inches(0.28),
  187. "周任务明细(阶段三~七 · 全部完成)", size=12, bold=True, color=HEADER_BG,
  188. )
  189. # 表头
  190. headers = [("阶段", 0.95), ("起止", 1.15), ("任务周", 1.35), ("完成", 0.70), ("合格率", 0.85)]
  191. hx = Inches(0.38)
  192. hy = Inches(1.42)
  193. add_rect(slide, Inches(0.35), hy, Inches(5.28), Inches(0.30), HEADER_BG)
  194. cx = hx
  195. for title, w in headers:
  196. add_textbox(slide, cx, hy + Inches(0.02), Inches(w), Inches(0.26), title, size=9, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
  197. cx += Inches(w)
  198. row_h = Inches(0.38)
  199. for i, (phase, span, week, plan, done, rate) in enumerate(WEEKLY_PROGRESS):
  200. y = Inches(1.74) + row_h * i
  201. bg = WHITE if i % 2 else ROW_ALT
  202. add_rect(slide, Inches(0.35), y, Inches(5.28), row_h - Inches(0.02), bg)
  203. vals = [phase, span, week, f"{done}/{plan}", rate]
  204. widths = [0.95, 1.15, 1.35, 0.70, 0.85]
  205. cx = hx
  206. for j, (val, w) in enumerate(zip(vals, widths)):
  207. col = ACCENT_GREEN if j >= 3 else TEXT_DARK
  208. add_textbox(
  209. slide, cx, y + Inches(0.04), Inches(w), Inches(0.30),
  210. val, size=9, bold=(j >= 3), color=col, align=PP_ALIGN.CENTER,
  211. )
  212. cx += Inches(w)
  213. add_textbox(
  214. slide, Inches(0.38), Inches(4.90), Inches(5.2), Inches(0.45),
  215. "合计:8 周次 · 任务 64 · 完成 64 · 合格率 100%",
  216. size=11, bold=True, color=ACCENT_GREEN,
  217. )
  218. # ===== 右侧:甘特图 =====
  219. gantt_left = Inches(5.90)
  220. gantt_top = Inches(1.00)
  221. gantt_w = Inches(7.15)
  222. gantt_h = Inches(4.55)
  223. add_round_rect(slide, gantt_left, gantt_top, gantt_w, gantt_h, CARD_BG)
  224. add_textbox(
  225. slide, Inches(6.05), Inches(1.08), Inches(5.0), Inches(0.28),
  226. "实施阶段甘特图(2026-04-27 ~ 07-24)", size=12, bold=True, color=HEADER_BG,
  227. )
  228. add_textbox(
  229. slide, Inches(10.4), Inches(1.10), Inches(2.4), Inches(0.26),
  230. "今日 7/21", size=9, bold=True, color=ORANGE_BAR, align=PP_ALIGN.RIGHT,
  231. )
  232. label_w = Inches(2.15)
  233. chart_left = Inches(6.05) + label_w
  234. chart_right = Inches(12.85)
  235. chart_width = chart_right - chart_left
  236. axis_y = Inches(1.38)
  237. row_h_g = Inches(0.38)
  238. bar_h = Inches(0.20)
  239. first_row_y = Inches(1.68)
  240. ticks = [
  241. date(2026, 4, 27), date(2026, 5, 11), date(2026, 5, 25),
  242. date(2026, 6, 8), date(2026, 6, 22), date(2026, 7, 6), date(2026, 7, 21),
  243. ]
  244. for d in ticks:
  245. x = date_to_x(d, AXIS_START, AXIS_END, chart_left, chart_width)
  246. line = slide.shapes.add_shape(
  247. MSO_SHAPE.RECTANGLE, x, first_row_y - Inches(0.04),
  248. Pt(0.75), row_h_g * len(GANTT_ROWS) + Inches(0.08),
  249. )
  250. line.fill.solid()
  251. line.fill.fore_color.rgb = LINE_SOFT
  252. line.line.fill.background()
  253. add_textbox(
  254. slide, x - Inches(0.28), axis_y, Inches(0.56), Inches(0.26),
  255. f"{d.month}/{d.day}", size=7, color=AXIS_GRAY, align=PP_ALIGN.CENTER,
  256. )
  257. # 今日竖线
  258. tx = date_to_x(TODAY, AXIS_START, AXIS_END, chart_left, chart_width)
  259. today_line = slide.shapes.add_shape(
  260. MSO_SHAPE.RECTANGLE, tx, first_row_y - Inches(0.04),
  261. Pt(1.5), row_h_g * len(GANTT_ROWS) + Inches(0.08),
  262. )
  263. today_line.fill.solid()
  264. today_line.fill.fore_color.rgb = ORANGE_BAR
  265. today_line.line.fill.background()
  266. for i, (name, start, end, done) in enumerate(GANTT_ROWS):
  267. y = first_row_y + row_h_g * i
  268. if i % 2 == 0:
  269. add_rect(slide, Inches(6.00), y, Inches(6.95), row_h_g - Inches(0.02), ROW_ALT if done else ORANGE_SOFT)
  270. add_textbox(
  271. slide, Inches(6.05), y + Inches(0.05), label_w - Inches(0.08), Inches(0.28),
  272. name, size=9, bold=True, color=TEXT_DARK,
  273. )
  274. x1 = date_to_x(start, AXIS_START, AXIS_END, chart_left, chart_width)
  275. x2 = date_to_x(end, AXIS_START, AXIS_END, chart_left, chart_width)
  276. bar_w = max(int(x2 - x1), int(Inches(0.10)))
  277. by = y + (row_h_g - bar_h) // 2 - Inches(0.01)
  278. fill = TEAL_BAR if done else ORANGE_BAR
  279. bar = add_round_rect(slide, x1, by, bar_w, bar_h, fill)
  280. try:
  281. bar.adjustments[0] = 0.35
  282. except Exception:
  283. pass
  284. span = f"{start.month}/{start.day}-{end.month}/{end.day}"
  285. if bar_w > Inches(0.85):
  286. add_textbox(slide, x1, by - Inches(0.02), bar_w, bar_h + Inches(0.04), span, size=7, bold=True, color=WHITE, align=PP_ALIGN.CENTER)
  287. # 图例
  288. ly = Inches(5.15)
  289. add_round_rect(slide, Inches(6.10), ly, Inches(0.22), Inches(0.14), TEAL_BAR)
  290. add_textbox(slide, Inches(6.38), ly - Inches(0.04), Inches(1.4), Inches(0.24), "已完成", size=8, color=TEXT_MUTED)
  291. add_round_rect(slide, Inches(7.85), ly, Inches(0.22), Inches(0.14), ORANGE_BAR)
  292. add_textbox(slide, Inches(8.13), ly - Inches(0.04), Inches(2.6), Inches(0.24), "本月推进中(上线陪产)", size=8, color=TEXT_MUTED)
  293. # ===== 底部:本月重点 =====
  294. add_textbox(
  295. slide, Inches(0.28), Inches(5.70), Inches(4), Inches(0.26),
  296. "本月重点(2026年7月)", size=12, bold=True, color=HEADER_BG,
  297. )
  298. july_items = [
  299. ("现场培训", "操作工/班组长/管理员\n工位扫码、异常处理、后台使用"),
  300. ("实际上线", "HEV线正式跑生产\n数据进MES、大屏实时展示"),
  301. ("陪产支持", "驻场观察产线运行\n问题闭环、系统优化"),
  302. ]
  303. by0 = Inches(6.00)
  304. bw = Inches(4.20)
  305. for i, (title, desc) in enumerate(july_items):
  306. left = Inches(0.28) + bw * i
  307. add_round_rect(slide, left, by0, bw - Inches(0.14), Inches(0.95), WHITE, ORANGE_BAR)
  308. add_textbox(slide, left + Inches(0.14), by0 + Inches(0.08), bw - Inches(0.40), Inches(0.28), title, size=13, bold=True, color=ORANGE_BAR)
  309. box = slide.shapes.add_textbox(left + Inches(0.14), by0 + Inches(0.38), bw - Inches(0.40), Inches(0.52))
  310. tf = box.text_frame
  311. tf.word_wrap = True
  312. p = tf.paragraphs[0]
  313. run = p.add_run()
  314. run.text = desc
  315. set_run_font(run, size=10, color=TEXT_MUTED)
  316. add_textbox(
  317. slide, Inches(0.28), Inches(7.15), Inches(12.7), Inches(0.25),
  318. "六安裕洋 MES · 京宜安实施团队 · 进度汇报 · 第1页 · 2026-07-21",
  319. size=8, color=AXIS_GRAY, align=PP_ALIGN.CENTER,
  320. )
  321. def build_slide2(prs: Presentation, used_images: list[tuple[str, str]]):
  322. slide = prs.slides.add_slide(prs.slide_layouts[6])
  323. add_rect(slide, 0, 0, SLIDE_W, Inches(0.78), HEADER_BG)
  324. add_textbox(
  325. slide, Inches(0.35), Inches(0.18), Inches(10), Inches(0.42),
  326. "效果展示 · 实时运行截图", size=22, bold=True, color=WHITE,
  327. )
  328. add_textbox(
  329. slide, Inches(9.5), Inches(0.25), Inches(3.5), Inches(0.35),
  330. "7月:现场培训 · 实际上线 · 陪产", size=11, color=RGBColor(0xB8, 0xD4, 0xE8), align=PP_ALIGN.RIGHT,
  331. )
  332. # 2x4 网格 — 使用用户精选效果图
  333. caps_and_files = EFFECT_IMAGES
  334. cols = 4
  335. margin_l = Inches(0.32)
  336. margin_t = Inches(0.95)
  337. gap_x, gap_y = Inches(0.18), Inches(0.22)
  338. cell_w = (SLIDE_W - margin_l * 2 - gap_x * (cols - 1)) / cols
  339. cell_h = Inches(2.95)
  340. img_area_h = Inches(2.45)
  341. cap_h = Inches(0.38)
  342. for idx, (caption, src) in enumerate(caps_and_files):
  343. row = idx // cols
  344. col = idx % cols
  345. left = margin_l + col * (cell_w + gap_x)
  346. top = margin_t + row * (cell_h + gap_y)
  347. add_round_rect(slide, left, top, cell_w, cell_h, WHITE, LINE_SOFT)
  348. if not src.exists():
  349. add_textbox(slide, left + Inches(0.1), top + Inches(0.8), cell_w - Inches(0.2), Inches(0.5),
  350. "(图片缺失)", size=12, color=TEXT_MUTED, align=PP_ALIGN.CENTER)
  351. used_images.append((caption, "MISSING"))
  352. else:
  353. cached = resize_image(src, 1200)
  354. with Image.open(cached) as im:
  355. iw, ih = im.size
  356. max_w = cell_w - Inches(0.16)
  357. max_h = img_area_h - Inches(0.12)
  358. scale = min(max_w / iw, max_h / ih)
  359. disp_w = int(iw * scale)
  360. disp_h = int(ih * scale)
  361. if disp_w > max_w:
  362. disp_w = int(max_w)
  363. if disp_h > max_h:
  364. disp_h = int(max_h)
  365. ix = left + (cell_w - disp_w) // 2
  366. iy = top + Inches(0.08) + (max_h - disp_h) // 2
  367. slide.shapes.add_picture(str(cached), ix, iy, width=disp_w, height=disp_h)
  368. used_images.append((caption, src.name))
  369. add_textbox(
  370. slide, left + Inches(0.06), top + img_area_h, cell_w - Inches(0.12), cap_h,
  371. caption, size=11, bold=True, color=HEADER_BG, align=PP_ALIGN.CENTER,
  372. )
  373. add_textbox(
  374. slide, Inches(0.35), Inches(7.15), Inches(12.5), Inches(0.28),
  375. "六安裕洋 MES · 京宜安实施团队 · 效果展示 · 第2页 · 2026-07-21",
  376. size=8, color=AXIS_GRAY, align=PP_ALIGN.CENTER,
  377. )
  378. def main():
  379. OUT1.parent.mkdir(parents=True, exist_ok=True)
  380. OUT2.parent.mkdir(parents=True, exist_ok=True)
  381. CACHE_DIR.mkdir(parents=True, exist_ok=True)
  382. prs = Presentation()
  383. prs.slide_width = SLIDE_W
  384. prs.slide_height = SLIDE_H
  385. build_slide1(prs)
  386. used: list[tuple[str, str]] = []
  387. build_slide2(prs, used)
  388. # 先写仓库副本(避免桌面文件被占用导致整次失败)
  389. prs.save(str(OUT2))
  390. print(f"OUT2: {OUT2} exists={OUT2.exists()} size={OUT2.stat().st_size}")
  391. desktop_out = OUT1
  392. try:
  393. prs.save(str(OUT1))
  394. except PermissionError:
  395. desktop_out = OUT1.with_name("六安裕洋MES实施进度汇报_效果图更新.pptx")
  396. prs.save(str(desktop_out))
  397. print(f"桌面原文件被占用,已另存为: {desktop_out}")
  398. print(f"OUT1: {desktop_out} exists={desktop_out.exists()} size={desktop_out.stat().st_size}")
  399. print("=== 使用图片 ===")
  400. for cap, name in used:
  401. print(f" - {cap}: {name}")
  402. if __name__ == "__main__":
  403. main()