scripts/pptx_create.py
scripts/pptx_create.pyBrowse 8 files
2,201 tokens
8,579 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1#!/usr/bin/env python32"""Create a .pptx presentation from a JSON deck spec.3 4Spec format (all positions/sizes in inches, colors as RRGGBB hex):5{6 "slide_size": "16:9", // or "4:3" (default "16:9")7 "slides": [8 {"layout": "title", "title": "My Deck", "subtitle": "Q3 review"},9 {"layout": "title_content", "title": "Agenda",10 "bullets": ["Top item",11 {"text": "Sub item", "level": 1, "bold": true,12 "size": 18, "color": "CC0000", "font": "Arial",13 "italic": false,14 "link": "https://example.com/agenda"}],15 "background": "1F2937", // solid slide background (hex)16 "footer": "Confidential", // footer placeholder text17 "slide_number": true, // enable slide-number placeholder18 "notes": "Speaker notes for this slide"},19 {"layout": "blank", "title": "Widgets",20 "images": [{"path": "logo.png", "left": 1, "top": 1, "width": 3}],21 "tables": [{"left": 1, "top": 2, "width": 6, "height": 2,22 "rows": [["H1", "H2"], ["a", "b"]]}],23 "shapes": [{"type": "rounded_rectangle", "left": 8, "top": 1,24 "width": 3, "height": 1, "fill": "4472C4",25 "text": "Callout", "text_color": "FFFFFF"}],26 "charts": [{"type": "bar", "left": 1, "top": 3, "width": 6,27 "height": 3.5, "title": "Sales",28 "categories": ["Q1", "Q2"],29 "series": {"North": [10, 20], "South": [7, 13]}}]}30 ]31}32Layouts: title, title_content, section, two_content, title_only, blank33Chart types: bar, bar_h, line, pie Shape types: rectangle,34rounded_rectangle, oval, diamond, right_arrow, chevron35"""36import argparse37import copy38import json39import sys40 41from pptx import Presentation42from pptx.chart.data import CategoryChartData43from pptx.dml.color import RGBColor44from pptx.enum.chart import XL_CHART_TYPE45from pptx.enum.shapes import MSO_SHAPE46from pptx.util import Inches, Pt47 48LAYOUTS = {"title": 0, "title_content": 1, "section": 2,49 "two_content": 3, "title_only": 5, "blank": 6}50CHART_TYPES = {"bar": XL_CHART_TYPE.COLUMN_CLUSTERED,51 "bar_h": XL_CHART_TYPE.BAR_CLUSTERED,52 "line": XL_CHART_TYPE.LINE_MARKERS,53 "pie": XL_CHART_TYPE.PIE}54SHAPE_TYPES = {"rectangle": MSO_SHAPE.RECTANGLE,55 "rounded_rectangle": MSO_SHAPE.ROUNDED_RECTANGLE,56 "oval": MSO_SHAPE.OVAL, "diamond": MSO_SHAPE.DIAMOND,57 "right_arrow": MSO_SHAPE.RIGHT_ARROW,58 "chevron": MSO_SHAPE.CHEVRON}59 60 61def style_run(run, spec):62 """Apply font styling from a bullet/text spec dict to a run."""63 font = run.font64 if spec.get("size"):65 font.size = Pt(spec["size"])66 if spec.get("bold") is not None:67 font.bold = spec["bold"]68 if spec.get("italic") is not None:69 font.italic = spec["italic"]70 if spec.get("font"):71 font.name = spec["font"]72 if spec.get("color"):73 font.color.rgb = RGBColor.from_string(spec["color"])74 if spec.get("link"):75 run.hyperlink.address = spec["link"]76 77 78def add_bullets(text_frame, bullets):79 text_frame.clear()80 for i, item in enumerate(bullets):81 if isinstance(item, str):82 item = {"text": item}83 para = text_frame.paragraphs[0] if i == 0 else text_frame.add_paragraph()84 para.level = int(item.get("level", 0))85 run = para.add_run()86 run.text = item.get("text", "")87 style_run(run, item)88 89 90def copy_layout_placeholder(slide, ph_idx):91 """Copy a layout placeholder (footer=11, slide number=12) onto the92 slide so it actually renders; returns the shape or None if the layout93 does not provide it."""94 for ph in slide.slide_layout.placeholders:95 if ph.placeholder_format.idx == ph_idx:96 slide.shapes._spTree.append(copy.deepcopy(ph._element))97 for shape in slide.placeholders:98 if shape.placeholder_format.idx == ph_idx:99 return shape100 return None101 102 103def build_slide(prs, spec):104 layout_idx = LAYOUTS.get(spec.get("layout", "title_content"), 1)105 slide = prs.slides.add_slide(prs.slide_layouts[layout_idx])106 107 if spec.get("background"):108 fill = slide.background.fill109 fill.solid()110 fill.fore_color.rgb = RGBColor.from_string(spec["background"])111 if spec.get("slide_number"):112 copy_layout_placeholder(slide, 12)113 if spec.get("footer"):114 shape = copy_layout_placeholder(slide, 11)115 if shape is not None:116 shape.text_frame.text = spec["footer"]117 118 if spec.get("title") is not None and slide.shapes.title is not None:119 slide.shapes.title.text = spec["title"]120 if spec.get("subtitle") is not None:121 for ph in slide.placeholders:122 if ph.placeholder_format.idx == 1:123 ph.text = spec["subtitle"]124 break125 if spec.get("bullets"):126 body = next((ph for ph in slide.placeholders127 if ph.placeholder_format.idx != 0), None)128 if body is None:129 body = slide.shapes.add_textbox(Inches(0.5), Inches(1.5),130 Inches(9), Inches(5))131 add_bullets(body.text_frame, spec["bullets"])132 133 for img in spec.get("images", []):134 kwargs = {}135 if img.get("width"):136 kwargs["width"] = Inches(img["width"])137 if img.get("height"):138 kwargs["height"] = Inches(img["height"])139 slide.shapes.add_picture(img["path"], Inches(img.get("left", 1)),140 Inches(img.get("top", 1)), **kwargs)141 142 for tbl in spec.get("tables", []):143 rows = tbl["rows"]144 shape = slide.shapes.add_table(145 len(rows), len(rows[0]), Inches(tbl.get("left", 1)),146 Inches(tbl.get("top", 2)), Inches(tbl.get("width", 6)),147 Inches(tbl.get("height", 2)))148 for r, row in enumerate(rows):149 for c, val in enumerate(row):150 shape.table.cell(r, c).text = str(val)151 152 for shp in spec.get("shapes", []):153 shape = slide.shapes.add_shape(154 SHAPE_TYPES.get(shp.get("type", "rectangle"), MSO_SHAPE.RECTANGLE),155 Inches(shp.get("left", 1)), Inches(shp.get("top", 1)),156 Inches(shp.get("width", 2)), Inches(shp.get("height", 1)))157 if shp.get("fill"):158 shape.fill.solid()159 shape.fill.fore_color.rgb = RGBColor.from_string(shp["fill"])160 if shp.get("text"):161 shape.text_frame.text = shp["text"]162 if shp.get("text_color"):163 run = shape.text_frame.paragraphs[0].runs[0]164 run.font.color.rgb = RGBColor.from_string(shp["text_color"])165 166 for cht in spec.get("charts", []):167 data = CategoryChartData()168 data.categories = cht["categories"]169 for name, values in cht["series"].items():170 data.add_series(name, values)171 frame = slide.shapes.add_chart(172 CHART_TYPES.get(cht.get("type", "bar"),173 XL_CHART_TYPE.COLUMN_CLUSTERED),174 Inches(cht.get("left", 1)), Inches(cht.get("top", 2)),175 Inches(cht.get("width", 6)), Inches(cht.get("height", 4)), data)176 if cht.get("title"):177 frame.chart.has_title = True178 frame.chart.chart_title.text_frame.text = cht["title"]179 180 if spec.get("notes"):181 slide.notes_slide.notes_text_frame.text = spec["notes"]182 return slide183 184 185def main(argv=None):186 if hasattr(sys.stdout, "reconfigure"):187 sys.stdout.reconfigure(encoding="utf-8", errors="replace")188 parser = argparse.ArgumentParser(189 description="Create a .pptx deck from a JSON spec.",190 epilog=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)191 parser.add_argument("spec", help="path to JSON deck spec")192 parser.add_argument("output", help="output .pptx path")193 args = parser.parse_args(argv)194 195 with open(args.spec, encoding="utf-8") as fh:196 spec = json.load(fh)197 198 prs = Presentation()199 if spec.get("slide_size", "16:9") == "16:9":200 prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)201 else:202 prs.slide_width, prs.slide_height = Inches(10), Inches(7.5)203 204 for slide_spec in spec.get("slides", []):205 build_slide(prs, slide_spec)206 207 prs.save(args.output)208 print(json.dumps({"ok": True, "output": args.output,209 "slides": len(prs.slides._sldIdLst)}))210 return 0211 212 213if __name__ == "__main__":214 sys.exit(main())215