Corin1998 commited on
Commit
395f954
·
verified ·
1 Parent(s): f73aa58

Create ppx_builder.py

Browse files
Files changed (1) hide show
  1. modules/ppx_builder.py +185 -0
modules/ppx_builder.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ from typing import List, Tuple, Dict, Any, Optional
3
+ from pptx import Presentation
4
+ from pptx.util import Inches, Pt
5
+ from pptx.enum.text import PP_ALIGN
6
+ from pptx.dml.color import RGBColor
7
+ from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
8
+ from pptx.enum.dml import MSO_THEME_COLOR
9
+ from PIL import Image
10
+ import matplotlib.pyplot as plt
11
+
12
+ def _add_logo(slide, logo_bytes: Optional[bytes]):
13
+ if not logo_bytes:
14
+ return
15
+ img = Image.open(io.BytesIO(logo_bytes)).convert("RGBA")
16
+ w, h = img.size
17
+ ratio = min(max_w/ w, max_h/h)
18
+ new_size = (int(w * ratio), int(h * ratio))
19
+ resized = img.resize(new_size)
20
+ b = io.BytesIO()
21
+ resized.save(b, format="PNG")
22
+ b.seek(0)
23
+ # Place top-right with small margin
24
+ left = slide.width - Inches(0.5) - max_w
25
+ top = Inches(0.2)
26
+ slide.shapes.add_picture(b,left, top)
27
+
28
+ def _apply_theme_bg(slide, rgb):
29
+ fill = slide.background.fill
30
+ fill.solid()
31
+ fill.fore_color.rgb = RGBColor(*rgb)
32
+
33
+ def _title_slide(prs, title_text: str, theme_bg, logo_bytes):
34
+ slide_layout = prs.slide_layouts[0] # Title Slide layout
35
+ slide = prs.slides.add_slide(slide_layout)
36
+ subtitle = slide.placeholders[1]
37
+ title.text = title_text
38
+ subtitle.text = "自動生成プレゼンテーション"
39
+ # Accent band
40
+ _apply_theme_bg(slide, theme_rgb)
41
+ # Put a white rouded rectangle for title readability
42
+ left = Inches(0.6)
43
+ top = Inches(1.8)
44
+ width = prs.slide_width - Inches(1.2)
45
+ height = Inches(2.2)
46
+ box = slide.shapes.add_shape(
47
+ MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, left, top, width, height
48
+ )
49
+ box.fill.solid()
50
+ box.fill.fore_color.rgb = RGBColor(255, 255, 255)
51
+ box.line.color.rgb = RGBColor(0, 0, 0)
52
+ box.line.transparency = 0.8
53
+ # Reposition title inside box
54
+ title.left = left + Inches(0.3)
55
+ title.top = top + Inches(0.3)
56
+ title.width = width - Inches(0.6)
57
+ title.height = Inches(1.4)
58
+ for p in title.text_frame.paragraphs:
59
+ p.font.size = Pt(40)
60
+ p.font.bold = True
61
+ # Subtitle
62
+ subtitle.left = left + Inches(0.3)
63
+ subtitle.top = top + Inches(1.6)
64
+ subtitle.width = width - Inches(0.6)
65
+ subtitle.height = Inches(0.8)
66
+ for p in subtitle.text_frame.paragraphs:
67
+ p.font.size = Pt(16)
68
+ p.font.bild = False
69
+ _add_logo(slide, logo_bytes)
70
+
71
+ def _summary_slide(prs, summary: str)
72
+ if not summary:
73
+ return
74
+ slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title and Content
75
+ slide.shapes.title.text = "エグゼクティブサマリー"
76
+ tf = slide.placeholders[1].text_frame
77
+ tf.clear()
78
+ # Split summary into bullet-ish lines
79
+ lines = [ln.strip() for ln in summary.splitlines() if ln.strip()]
80
+ if not lines:
81
+ lines = [summary]
82
+ for i, line in enumerate(lines):
83
+ p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
84
+ p.text = ln
85
+ p.level = 0
86
+
87
+ def _section_slide(prs, title: str, bullets: List[str]):
88
+ slide = prs.slides.add_slide(prs.slide_layouts[1])
89
+ slide.shapes.title.text = title[:90]
90
+ tf = slide.placeholders[1].text_frame
91
+ tf.clear()
92
+ if not bullets:
93
+ bullets = ["(要点なし)"]
94
+ for i, b in enumerate(bullets[:12]):
95
+ p = tf.add_paragraph() if i > 0 else tf.paragraphs[0]
96
+ p.text = b
97
+ p.level = 0
98
+
99
+ def _table_slide(prs, titile: str, pairs: List[tuple]):
100
+ slide = prs.slides.add_slide(prs.slide_layouts[5]) # Title and Content
101
+ slide.shapes.title.text = title
102
+ rows = len(pairs) + 1
103
+ cols = 2
104
+ left = Inches(0.5)
105
+ top = Inches(1.8)
106
+ width = prs.slide_width - Inches(1.0)
107
+ height = prs.slide_height - top - Inches(2.6)
108
+ table = slide.shapes.add_table(rows, cols, left, top, width, height).table
109
+ table.cell(0, 0).text = "項目"
110
+ table.cell(0, 1).text = "値"
111
+ for r, (k, v) in enumerate(pairs, start=1):
112
+ table.cell(r, 0).text = str(k)
113
+ table.cell(r, 1).text = str(v)
114
+
115
+ def _chart_slide(prs, title: str, series: List[tuple]):
116
+ slide = prs.slides.add_slide(prs.slide_layouts[5]) # Title Only
117
+ slide.shapes.title.text = title
118
+ # Build bar chart via matplotlib and embed as image
119
+ labels = [x[0] for x in series]
120
+ values = [x[1] for x in series]
121
+ fig = plt.figure(figsize=(6,4.5))
122
+ plt.bar(range(len(values)), labels, rotation=20, ha='right')
123
+ plt.tight_layout()
124
+ buf = io.BytesIO()
125
+ fig.savefig(buf, format='png', dpi=200)
126
+ plt.close(fig)
127
+ buf.seek(0)
128
+ left = Inches(1.6)
129
+ width = prs.slide_width - Inches(1.0)
130
+ height = prs.slide_height - Inches(2.2)
131
+ slide.shapes.add_picture(buf, left, top, width=width, height=height)
132
+
133
+ def _add_footer(prs, theme_tgb):
134
+ for slide in prs.slides:
135
+ left = Inches(0.3)
136
+ top = prs.slide_height - Inches(0.4)
137
+ width = prs.slide_width - Inches(0.6)
138
+ height = Inches(0.3)
139
+ shp = slide.shapes.add_shape(
140
+ MSO_AUTO_SHAPE_TYPE.RECTANGLE, left, top, width, height
141
+ )
142
+ shp.fill.solid()
143
+ shp.fill.fore_color.rgb = RGBColor(*theme_rgb)
144
+ shp.line.fill.background()
145
+ # Slide number text boxz
146
+ tx = slide.shapes.add_textbox(prs.slide_width - Inches(1.0), top, Inches(0.05), Inches(0.8), Inches(0.3))
147
+ tf = tx.text_frame
148
+ p = tf.paragraphs[0]
149
+ p.text = f"Slide {slide.slide_id % 10000}"
150
+ p.font.size = Pt(10)
151
+ p.alignment = PP_ALIGN.RIGHT
152
+
153
+ def build_presentation(out_path:str,
154
+ title: str,
155
+ theme_rgb: tuple
156
+ logo_bytes: Optional[bytes],
157
+ executive_summary: Optional[str],
158
+ sections: List[Tuple[str, str]],
159
+ bullets_by_section: Dict[str, List[str]],
160
+ tables: List[Dict[str, Any]],
161
+ charts: List[Dict[str, Any]]):
162
+ prs = Presentation()
163
+
164
+ # Title slide
165
+ _title_slide(prs, title, theme_rgb, logo_bytes)
166
+
167
+ # Summary
168
+ _summary_slide(prs, executive_summary)
169
+
170
+ # Sections
171
+ for idx,(sec_title, _body) in enumerate(sections):
172
+ bullets = bullets_by_section.get(sec_title, [])
173
+ _section_slide(prs, sec_title, bullets)
174
+
175
+ # Tables
176
+ for tbl in tables:
177
+ _table_slide(prs, tbl.get("title", "表"), tbl.get("pairs", []))
178
+
179
+ # Charts
180
+ for ch in charts:
181
+ _chart_slide(prs, ch.get("title", "チャート"), ch.get("series", []))
182
+
183
+ _add_footer(prs, theme_rgb)
184
+
185
+ prs.save(out_path)