Spaces:
Sleeping
Sleeping
| import io | |
| from typing import List, Tuple, Dict, Any, Optional | |
| from pptx import Presentation | |
| from pptx.util import Inches, Pt | |
| from pptx.enum.text import PP_ALIGN | |
| from pptx.dml.color import RGBColor | |
| from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE | |
| from PIL import Image | |
| import matplotlib.pyplot as plt | |
| def _add_logo(prs: Presentation, slide, logo_bytes: Optional[bytes]): | |
| if not logo_bytes: | |
| return | |
| img = Image.open(io.BytesIO(logo_bytes)).convert("RGBA") | |
| # Resize to fit width<=2.0in, height<=1.0in | |
| max_w, max_h = Inches(2.0), Inches(1.0) | |
| w, h = img.size | |
| ratio = min(max_w / max(w, 1), max_h / max(h, 1)) | |
| new_size = (max(1, int(w * ratio)), max(1, int(h * ratio))) | |
| resized = img.resize(new_size) | |
| b = io.BytesIO() | |
| resized.save(b, format="PNG") | |
| b.seek(0) | |
| # Place top-right with small margin | |
| left = prs.slide_width - max_w - Inches(0.5) | |
| top = Inches(0.2) | |
| slide.shapes.add_picture(b, left, top) | |
| def _apply_theme_bg(slide, rgb): | |
| fill = slide.background.fill | |
| fill.solid() | |
| fill.fore_color.rgb = RGBColor(*rgb) | |
| def _title_slide(prs, title_text: str, theme_rgb, logo_bytes): | |
| slide_layout = prs.slide_layouts[0] # Title slide | |
| slide = prs.slides.add_slide(slide_layout) | |
| title = slide.shapes.title | |
| subtitle = slide.placeholders[1] | |
| title.text = title_text | |
| subtitle.text = "自動生成プレゼンテーション" | |
| # Accent band | |
| _apply_theme_bg(slide, theme_rgb) | |
| # White rounded rectangle for readability | |
| left = Inches(0.6) | |
| top = Inches(1.8) | |
| width = prs.slide_width - Inches(1.2) | |
| height = Inches(2.2) | |
| box = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.ROUNDED_RECTANGLE, left, top, width, height) | |
| box.fill.solid() | |
| box.fill.fore_color.rgb = RGBColor(255, 255, 255) | |
| box.line.color.rgb = RGBColor(0, 0, 0) | |
| box.line.transparency = 0.8 | |
| # Reposition title | |
| title.left = left + Inches(0.3) | |
| title.top = top + Inches(0.3) | |
| title.width = width - Inches(0.6) | |
| title.height = Inches(1.4) | |
| for p in title.text_frame.paragraphs: | |
| p.font.size = Pt(40) | |
| p.font.bold = True | |
| # Subtitle | |
| subtitle.left = left + Inches(0.3) | |
| subtitle.top = top + Inches(1.6) | |
| subtitle.width = width - Inches(0.6) | |
| subtitle.height = Inches(0.8) | |
| for p in subtitle.text_frame.paragraphs: | |
| p.font.size = Pt(16) | |
| p.font.bold = False | |
| _add_logo(prs, slide, logo_bytes) | |
| def _summary_slide(prs, summary: str): | |
| if not summary: | |
| return | |
| slide = prs.slides.add_slide(prs.slide_layouts[1]) # Title and Content | |
| slide.shapes.title.text = "エグゼクティブサマリー" | |
| tf = slide.placeholders[1].text_frame | |
| tf.clear() | |
| # Split summary into bullet-ish lines | |
| lines = [ln.strip() for ln in summary.splitlines() if ln.strip()] | |
| if not lines: | |
| lines = [summary] | |
| for i, ln in enumerate(lines): | |
| p = tf.add_paragraph() if i > 0 else tf.paragraphs[0] | |
| p.text = ln | |
| p.level = 0 | |
| def _section_slide(prs, title: str, bullets: List[str]): | |
| slide = prs.slides.add_slide(prs.slide_layouts[1]) | |
| slide.shapes.title.text = title[:90] | |
| tf = slide.placeholders[1].text_frame | |
| tf.clear() | |
| if not bullets: | |
| bullets = ["(要点なし)"] | |
| for i, b in enumerate(bullets[:12]): | |
| p = tf.add_paragraph() if i > 0 else tf.paragraphs[0] | |
| p.text = b | |
| p.level = 0 | |
| def _table_slide(prs, title: str, pairs: List[tuple]): | |
| slide = prs.slides.add_slide(prs.slide_layouts[5]) # Title Only | |
| slide.shapes.title.text = title | |
| rows = len(pairs) + 1 | |
| cols = 2 | |
| left = Inches(0.5) | |
| top = Inches(1.8) | |
| width = prs.slide_width - Inches(1.0) | |
| height = prs.slide_height - Inches(2.6) | |
| table = slide.shapes.add_table(rows, cols, left, top, width, height).table | |
| table.cell(0, 0).text = "項目" | |
| table.cell(0, 1).text = "値" | |
| for r, (k, v) in enumerate(pairs, start=1): | |
| table.cell(r, 0).text = str(k) | |
| table.cell(r, 1).text = str(v) | |
| def _chart_slide(prs, title: str, series: List[tuple]): | |
| slide = prs.slides.add_slide(prs.slide_layouts[5]) # Title Only | |
| slide.shapes.title.text = title | |
| # Build bar chart via matplotlib and embed as image | |
| labels = [x[0] for x in series] | |
| values = [x[1] for x in series] | |
| fig = plt.figure(figsize=(8, 4.5)) | |
| plt.bar(range(len(values)), values) | |
| plt.xticks(range(len(labels)), labels, rotation=20, ha='right') | |
| plt.tight_layout() | |
| buf = io.BytesIO() | |
| fig.savefig(buf, format='png', dpi=200) | |
| plt.close(fig) | |
| buf.seek(0) | |
| left = Inches(0.5) | |
| top = Inches(1.6) | |
| width = prs.slide_width - Inches(1.0) | |
| height = prs.slide_height - Inches(2.2) | |
| slide.shapes.add_picture(buf, left, top, width=width, height=height) | |
| def _add_footer(prs, theme_rgb): | |
| for idx, slide in enumerate(prs.slides, start=1): | |
| left = Inches(0.3) | |
| top = prs.slide_height - Inches(0.4) | |
| width = prs.slide_width - Inches(0.6) | |
| height = Inches(0.3) | |
| shp = slide.shapes.add_shape(MSO_AUTO_SHAPE_TYPE.RECTANGLE, left, top, width, height) | |
| shp.fill.solid() | |
| shp.fill.fore_color.rgb = RGBColor(*theme_rgb) | |
| shp.line.fill.background() | |
| # Slide number text box | |
| tx = slide.shapes.add_textbox(prs.slide_width - Inches(1.0), top - Inches(0.05), Inches(0.8), Inches(0.3)) | |
| tf = tx.text_frame | |
| p = tf.paragraphs[0] | |
| p.text = f"{idx}" | |
| p.font.size = Pt(10) | |
| p.alignment = PP_ALIGN.RIGHT | |
| def build_presentation(output_path: str, | |
| title: str, | |
| theme_rgb: tuple, | |
| logo_bytes: Optional[bytes], | |
| executive_summary: Optional[str], | |
| sections: List[Tuple[str, str]], | |
| bullets_by_section: Dict[int, List[str]], | |
| tables: List[Dict[str, Any]], | |
| charts: List[Dict[str, Any]]): | |
| prs = Presentation() | |
| # Title slide | |
| _title_slide(prs, title, theme_rgb, logo_bytes) | |
| # Summary | |
| _summary_slide(prs, executive_summary) | |
| # Sections | |
| for idx, (sec_title, _body) in enumerate(sections): | |
| bullets = bullets_by_section.get(idx, []) | |
| _section_slide(prs, sec_title, bullets) | |
| # Tables | |
| for tbl in tables: | |
| _table_slide(prs, tbl.get("title", "表"), tbl.get("pairs", [])) | |
| # Charts | |
| for ch in charts: | |
| _chart_slide(prs, ch.get("title", "チャート"), ch.get("series", [])) | |
| _add_footer(prs, theme_rgb) | |
| prs.save(output_path) | |