import copy import json from PIL import Image from pptx import Presentation from pptx.chart.data import CategoryChartData from pptx.dml.color import RGBColor from pptx.enum.chart import XL_CHART_TYPE, XL_LABEL_POSITION from pptx.enum.shapes import MSO_SHAPE from pptx.enum.text import MSO_ANCHOR, PP_ALIGN from pptx.oxml.ns import qn from pptx.util import Inches, Pt A = "/tmp/claude-1000/-home-mm-Downloads-vkgs-sim/8bf0e66c-975d-4257-af27-61bc25ee4316/scratchpad/deck/" NAVY, BLUE, LIGHT, PALE, GRAY, ORANGE, WHITE = (RGBColor(0x1F, 0x3B, 0x70), RGBColor(0x44, 0x72, 0xC4), RGBColor(0xDC, 0xE6, 0xF4), RGBColor(0xF2, 0xF5, 0xFA), RGBColor(0x40, 0x40, 0x40), RGBColor(0xED, 0x7D, 0x31), RGBColor(0xFF, 0xFF, 0xFF)) FONT = "맑은 고딕" KEEP = {"슬라이드 번호 개체 틀 1", "제목 1", "그룹 6", "TextBox 9", "TextBox 4"} def shape(sl, name): for sh in sl.shapes: if sh.name == name: return sh raise KeyError(name) def set_runs_text(tf, text): """첫 문단 첫 run 의 서식으로 text 만 남긴다.""" p0 = tf.paragraphs[0] for p in tf.paragraphs[1:]: p._p.getparent().remove(p._p) runs = p0.runs if not runs: p0.text = text return runs[0].text = text for r in runs[1:]: r._r.getparent().remove(r._r) def set_paragraphs(tf, items): """첫 문단을 본으로 문단마다 복사 (글머리표·간격 유지).""" p0 = tf.paragraphs[0]._p for p in tf.paragraphs[1:]: p._p.getparent().remove(p._p) parent = p0.getparent() proto = copy.deepcopy(p0) parent.remove(p0) for it in items: p = copy.deepcopy(proto) rs = p.findall(qn("a:r")) for r in rs[1:]: p.remove(r) if rs: rs[0].find(qn("a:t")).text = it parent.append(p) def body(sl, tab, heading, bullets): for sh in list(sl.shapes): if sh.name not in KEEP: sh._element.getparent().remove(sh._element) set_runs_text(shape(sl, "제목 1").text_frame, "VKGS-Sim") set_runs_text(shape(sl, "TextBox 4").text_frame, tab) grp = shape(sl, "그룹 6") for c in grp.shapes: if c.has_text_frame and c.text_frame.text.strip(): set_runs_text(c.text_frame, heading) set_paragraphs(shape(sl, "TextBox 9").text_frame, bullets) return 1.82 + 0.28 * sum(1 + len(b) // 95 for b in bullets) + 0.15 # 본문 다음 y (12pt, 줄간격 150%) def notes(sl, text): sl.notes_slide.notes_text_frame.text = text def tb(sl, x, y, w, h, text, size=12, bold=False, color=GRAY, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP, font=FONT): t = sl.shapes.add_textbox(Inches(x), Inches(y), Inches(w), Inches(h)) tf = t.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Inches(0.04) tf.margin_top = tf.margin_bottom = Inches(0.02) tf.vertical_anchor = anchor for i, line in enumerate(text.split("\n")): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = align r = p.add_run() r.text = line r.font.size, r.font.bold, r.font.color.rgb, r.font.name = Pt(size), bold, color, font return t def box(sl, x, y, w, h, text, fill=LIGHT, color=GRAY, size=12, bold=False, line=None, align=PP_ALIGN.CENTER, title=None): s = sl.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h)) s.adjustments[0] = 0.08 s.fill.solid() s.fill.fore_color.rgb = fill if line: s.line.color.rgb = line s.line.width = Pt(1.25) else: s.line.fill.background() s.shadow.inherit = False tf = s.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Inches(0.1) tf.margin_top = tf.margin_bottom = Inches(0.06) tf.vertical_anchor = MSO_ANCHOR.MIDDLE if title is None else MSO_ANCHOR.TOP lines = ([(title, True, size + 1)] if title else []) + [(l, bold, size) for l in text.split("\n") if l] for i, (l, b, sz) in enumerate(lines): p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() p.alignment = PP_ALIGN.CENTER if (title and i == 0) else align p.space_after = Pt(3) r = p.add_run() r.text = l r.font.size, r.font.bold, r.font.color.rgb, r.font.name = Pt(sz), b, color, FONT return s def arrow(sl, x, y, w=0.36, h=0.24, color=BLUE): s = sl.shapes.add_shape(MSO_SHAPE.RIGHT_ARROW, Inches(x), Inches(y), Inches(w), Inches(h)) s.fill.solid() s.fill.fore_color.rgb = color s.line.fill.background() s.shadow.inherit = False return s def image(sl, path, x, y, w=None, h=None): iw, ih = Image.open(path).size if w and h: # 상자 안에 맞춘다 (contain), 가운데 s = min(w / iw, h / ih) ww, hh = iw * s, ih * s return sl.shapes.add_picture(path, Inches(x + (w - ww) / 2), Inches(y + (h - hh) / 2), Inches(ww), Inches(hh)) if w: return sl.shapes.add_picture(path, Inches(x), Inches(y), Inches(w), Inches(w * ih / iw)) return sl.shapes.add_picture(path, Inches(x), Inches(y), Inches(h * iw / ih), Inches(h)) def table(sl, x, y, w, rows, colw, size=11, rowh=0.3, align_right=()): t = sl.shapes.add_table(len(rows), len(rows[0]), Inches(x), Inches(y), Inches(w), Inches(rowh * len(rows))).table for j, cw in enumerate(colw): t.columns[j].width = Inches(w * cw) for i, row in enumerate(rows): t.rows[i].height = Inches(rowh) for j, v in enumerate(row): c = t.cell(i, j) c.margin_left = c.margin_right = Inches(0.08) c.margin_top = c.margin_bottom = Inches(0.03) c.vertical_anchor = MSO_ANCHOR.MIDDLE c.fill.solid() c.fill.fore_color.rgb = NAVY if i == 0 else (PALE if i % 2 == 0 else WHITE) tf = c.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.text = "" p.alignment = PP_ALIGN.RIGHT if (j in align_right and i > 0) else PP_ALIGN.LEFT r = p.add_run() r.text = str(v) r.font.size, r.font.name = Pt(size), FONT r.font.bold = i == 0 r.font.color.rgb = WHITE if i == 0 else GRAY return t def kpi(sl, x, y, w, num, label, color=NAVY): s = box(sl, x, y, w, 1.05, "", fill=PALE, line=LIGHT) tb(sl, x + 0.1, y + 0.08, w - 0.2, 0.55, num, size=26, bold=True, color=color, align=PP_ALIGN.CENTER) tb(sl, x + 0.1, y + 0.64, w - 0.2, 0.35, label, size=11, color=GRAY, align=PP_ALIGN.CENTER) return s def caption(sl, x, y, w, text): return tb(sl, x, y, w, 0.32, text, size=12, bold=True, color=GRAY, align=PP_ALIGN.CENTER)