vkgs-sim / docs /presentation /pptlib.py
kimjunhan's picture
Add files using upload-large-folder tool
ceea884 verified
Raw History Blame Contribute Delete
6.79 kB
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)