259 lines
11 KiB
Python
259 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""ambre-tool-pptx-render.py — Premium PowerPoint from JSON"""
|
|
import sys, json
|
|
from pptx import Presentation
|
|
from pptx.util import Inches, Pt, Emu
|
|
from pptx.dml.color import RGBColor
|
|
from pptx.enum.shapes import MSO_SHAPE
|
|
from pptx.enum.text import PP_ALIGN
|
|
from datetime import datetime
|
|
|
|
WIDTH, HEIGHT = Inches(13.333), Inches(7.5) # 16:9
|
|
PRIMARY = RGBColor(0x4f, 0x46, 0xe5)
|
|
ACCENT = RGBColor(0x10, 0xb9, 0x81)
|
|
DARK = RGBColor(0x0f, 0x17, 0x2a)
|
|
LIGHT = RGBColor(0xf8, 0xfa, 0xfc)
|
|
GRAY = RGBColor(0x64, 0x74, 0x8b)
|
|
|
|
def gradient_bg(slide, c1=DARK, c2=PRIMARY):
|
|
"""Add full-slide gradient rect as background"""
|
|
left = top = 0
|
|
shape = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, WIDTH, HEIGHT)
|
|
shape.fill.solid()
|
|
shape.fill.fore_color.rgb = c1
|
|
shape.line.fill.background()
|
|
# Lower to back
|
|
spTree = shape._element.getparent()
|
|
spTree.remove(shape._element)
|
|
spTree.insert(2, shape._element)
|
|
return shape
|
|
|
|
def add_text(slide, text, left, top, width, height, size=18, bold=False, color=DARK, align=PP_ALIGN.LEFT):
|
|
tb = slide.shapes.add_textbox(left, top, width, height)
|
|
tf = tb.text_frame
|
|
tf.word_wrap = True
|
|
tf.margin_left = Emu(0); tf.margin_right = Emu(0)
|
|
tf.margin_top = Emu(0); tf.margin_bottom = Emu(0)
|
|
p = tf.paragraphs[0]
|
|
p.alignment = align
|
|
r = p.add_run()
|
|
r.text = text
|
|
r.font.size = Pt(size)
|
|
r.font.bold = bold
|
|
r.font.color.rgb = color
|
|
r.font.name = 'Calibri'
|
|
return tb
|
|
|
|
def add_title_slide(prs, data):
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6]) # blank
|
|
gradient_bg(slide, DARK, PRIMARY)
|
|
|
|
# Title
|
|
add_text(slide, data.get('title', 'Titre'), Inches(1), Inches(2.5), Inches(11.3), Inches(1.5),
|
|
size=48, bold=True, color=RGBColor(0xff,0xff,0xff), align=PP_ALIGN.CENTER)
|
|
|
|
# Subtitle
|
|
if data.get('subtitle'):
|
|
add_text(slide, data['subtitle'], Inches(1), Inches(4.2), Inches(11.3), Inches(1),
|
|
size=22, color=RGBColor(0xcb, 0xd5, 0xe1), align=PP_ALIGN.CENTER)
|
|
|
|
# Author footer
|
|
add_text(slide, data.get('author','WEVAL Consulting') + ' - ' + datetime.now().strftime('%d %B %Y'),
|
|
Inches(1), Inches(6.8), Inches(11.3), Inches(0.5),
|
|
size=12, color=RGBColor(0x94,0xa3,0xb8), align=PP_ALIGN.CENTER)
|
|
|
|
def add_content_slide(prs, data):
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
# Light bg
|
|
bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, WIDTH, HEIGHT)
|
|
bg.fill.solid(); bg.fill.fore_color.rgb = LIGHT; bg.line.fill.background()
|
|
spTree = bg._element.getparent(); spTree.remove(bg._element); spTree.insert(2, bg._element)
|
|
|
|
# Top accent bar
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, WIDTH, Inches(0.4))
|
|
bar.fill.solid(); bar.fill.fore_color.rgb = PRIMARY; bar.line.fill.background()
|
|
|
|
# Title
|
|
add_text(slide, data.get('title','Section'), Inches(0.6), Inches(0.7), Inches(12), Inches(0.9),
|
|
size=32, bold=True, color=DARK)
|
|
|
|
# Bullets
|
|
bullets = data.get('bullets', [])
|
|
if bullets:
|
|
tb = slide.shapes.add_textbox(Inches(0.8), Inches(1.9), Inches(12), Inches(5))
|
|
tf = tb.text_frame; tf.word_wrap = True
|
|
for i, b in enumerate(bullets):
|
|
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
|
p.level = 0
|
|
r = p.add_run()
|
|
r.text = ' ' + str(b)
|
|
r.font.size = Pt(20)
|
|
r.font.color.rgb = DARK
|
|
r.font.name = 'Calibri'
|
|
p.space_after = Pt(14)
|
|
|
|
# Footer
|
|
add_text(slide, data.get('author','WEVAL'), Inches(0.6), Inches(7.0), Inches(12), Inches(0.3),
|
|
size=10, color=GRAY)
|
|
|
|
def add_two_column_slide(prs, data):
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, WIDTH, HEIGHT)
|
|
bg.fill.solid(); bg.fill.fore_color.rgb = LIGHT; bg.line.fill.background()
|
|
spTree = bg._element.getparent(); spTree.remove(bg._element); spTree.insert(2, bg._element)
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, WIDTH, Inches(0.4))
|
|
bar.fill.solid(); bar.fill.fore_color.rgb = PRIMARY; bar.line.fill.background()
|
|
|
|
add_text(slide, data.get('title','Comparaison'), Inches(0.6), Inches(0.7), Inches(12), Inches(0.9),
|
|
size=30, bold=True, color=DARK)
|
|
|
|
# Left column
|
|
left_data = data.get('left', {})
|
|
left_box = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(0.6), Inches(2), Inches(5.8), Inches(4.8))
|
|
left_box.fill.solid(); left_box.fill.fore_color.rgb = RGBColor(0xe0, 0xe7, 0xff)
|
|
left_box.line.color.rgb = PRIMARY; left_box.line.width = Pt(2)
|
|
|
|
add_text(slide, left_data.get('heading','Gauche'), Inches(0.8), Inches(2.2), Inches(5.4), Inches(0.6),
|
|
size=20, bold=True, color=PRIMARY)
|
|
|
|
items = left_data.get('items', [])
|
|
if items:
|
|
tb = slide.shapes.add_textbox(Inches(0.9), Inches(3), Inches(5.2), Inches(3.5))
|
|
tf = tb.text_frame; tf.word_wrap = True
|
|
for i, it in enumerate(items):
|
|
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
|
r = p.add_run(); r.text = ' ' + str(it); r.font.size = Pt(15); r.font.color.rgb = DARK
|
|
p.space_after = Pt(8)
|
|
|
|
# Right column
|
|
right_data = data.get('right', {})
|
|
right_box = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, Inches(6.9), Inches(2), Inches(5.8), Inches(4.8))
|
|
right_box.fill.solid(); right_box.fill.fore_color.rgb = RGBColor(0xd1, 0xfa, 0xe5)
|
|
right_box.line.color.rgb = ACCENT; right_box.line.width = Pt(2)
|
|
|
|
add_text(slide, right_data.get('heading','Droite'), Inches(7.1), Inches(2.2), Inches(5.4), Inches(0.6),
|
|
size=20, bold=True, color=ACCENT)
|
|
|
|
items2 = right_data.get('items', [])
|
|
if items2:
|
|
tb = slide.shapes.add_textbox(Inches(7.2), Inches(3), Inches(5.2), Inches(3.5))
|
|
tf = tb.text_frame; tf.word_wrap = True
|
|
for i, it in enumerate(items2):
|
|
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
|
r = p.add_run(); r.text = ' ' + str(it); r.font.size = Pt(15); r.font.color.rgb = DARK
|
|
p.space_after = Pt(8)
|
|
|
|
def add_stats_slide(prs, data):
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, WIDTH, HEIGHT)
|
|
bg.fill.solid(); bg.fill.fore_color.rgb = DARK; bg.line.fill.background()
|
|
spTree = bg._element.getparent(); spTree.remove(bg._element); spTree.insert(2, bg._element)
|
|
|
|
add_text(slide, data.get('title','Chiffres cles'), Inches(0.6), Inches(0.7), Inches(12), Inches(0.9),
|
|
size=32, bold=True, color=RGBColor(0xff,0xff,0xff), align=PP_ALIGN.CENTER)
|
|
|
|
stats = data.get('stats', [])[:4]
|
|
if stats:
|
|
n = len(stats)
|
|
card_w = (WIDTH - Inches(1.2) - Inches(0.4)*(n-1)) / n if n else Inches(3)
|
|
for i, s in enumerate(stats):
|
|
x = Inches(0.6) + (card_w + Inches(0.4)) * i
|
|
card = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, x, Inches(2.3), card_w, Inches(3.5))
|
|
card.fill.solid(); card.fill.fore_color.rgb = PRIMARY
|
|
card.line.fill.background()
|
|
|
|
# Value
|
|
tb1 = slide.shapes.add_textbox(x, Inches(2.8), card_w, Inches(1.8))
|
|
tf1 = tb1.text_frame; tf1.word_wrap = True
|
|
p1 = tf1.paragraphs[0]; p1.alignment = PP_ALIGN.CENTER
|
|
r1 = p1.add_run(); r1.text = str(s.get('value',''))
|
|
r1.font.size = Pt(54); r1.font.bold = True; r1.font.color.rgb = RGBColor(0xff,0xff,0xff)
|
|
|
|
# Label
|
|
tb2 = slide.shapes.add_textbox(x, Inches(4.6), card_w, Inches(1))
|
|
tf2 = tb2.text_frame; tf2.word_wrap = True
|
|
p2 = tf2.paragraphs[0]; p2.alignment = PP_ALIGN.CENTER
|
|
r2 = p2.add_run(); r2.text = str(s.get('label',''))
|
|
r2.font.size = Pt(14); r2.font.color.rgb = RGBColor(0xcb,0xd5,0xe1)
|
|
|
|
def add_table_slide(prs, data):
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
bg = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, WIDTH, HEIGHT)
|
|
bg.fill.solid(); bg.fill.fore_color.rgb = LIGHT; bg.line.fill.background()
|
|
spTree = bg._element.getparent(); spTree.remove(bg._element); spTree.insert(2, bg._element)
|
|
bar = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, WIDTH, Inches(0.4))
|
|
bar.fill.solid(); bar.fill.fore_color.rgb = PRIMARY; bar.line.fill.background()
|
|
|
|
add_text(slide, data.get('title','Tableau'), Inches(0.6), Inches(0.7), Inches(12), Inches(0.9),
|
|
size=30, bold=True, color=DARK)
|
|
|
|
headers = data.get('headers', [])
|
|
rows = data.get('rows', [])
|
|
if headers and rows:
|
|
n_cols = len(headers)
|
|
n_rows = len(rows) + 1
|
|
tbl_shape = slide.shapes.add_table(n_rows, n_cols, Inches(0.6), Inches(2), Inches(12), Inches(5))
|
|
tbl = tbl_shape.table
|
|
for i, h in enumerate(headers):
|
|
cell = tbl.cell(0, i)
|
|
cell.fill.solid(); cell.fill.fore_color.rgb = PRIMARY
|
|
p = cell.text_frame.paragraphs[0]
|
|
p.alignment = PP_ALIGN.CENTER
|
|
r = p.add_run(); r.text = str(h); r.font.size = Pt(14); r.font.bold = True
|
|
r.font.color.rgb = RGBColor(0xff,0xff,0xff)
|
|
for r_idx, row in enumerate(rows):
|
|
for c_idx, val in enumerate(row[:n_cols]):
|
|
cell = tbl.cell(r_idx+1, c_idx)
|
|
if r_idx % 2 == 0:
|
|
cell.fill.solid(); cell.fill.fore_color.rgb = RGBColor(0xf8,0xfa,0xfc)
|
|
p = cell.text_frame.paragraphs[0]
|
|
r = p.add_run(); r.text = str(val); r.font.size = Pt(12); r.font.color.rgb = DARK
|
|
|
|
def add_conclusion_slide(prs, data):
|
|
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
gradient_bg(slide, PRIMARY, DARK)
|
|
|
|
add_text(slide, data.get('title','Conclusion'), Inches(1), Inches(1.5), Inches(11.3), Inches(1),
|
|
size=40, bold=True, color=RGBColor(0xff,0xff,0xff), align=PP_ALIGN.CENTER)
|
|
|
|
if data.get('text'):
|
|
add_text(slide, data['text'], Inches(1.5), Inches(3), Inches(10.3), Inches(1.5),
|
|
size=18, color=RGBColor(0xcb,0xd5,0xe1), align=PP_ALIGN.CENTER)
|
|
|
|
bullets = data.get('bullets', [])
|
|
if bullets:
|
|
tb = slide.shapes.add_textbox(Inches(2), Inches(4.8), Inches(9.3), Inches(2.2))
|
|
tf = tb.text_frame; tf.word_wrap = True
|
|
for i, b in enumerate(bullets):
|
|
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
|
p.alignment = PP_ALIGN.CENTER
|
|
r = p.add_run(); r.text = str(b); r.font.size = Pt(16); r.font.color.rgb = RGBColor(0xff,0xff,0xff)
|
|
p.space_after = Pt(8)
|
|
|
|
def main():
|
|
if len(sys.argv) < 3: sys.exit("Usage: render <input.json> <output.pptx>")
|
|
with open(sys.argv[1], 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
prs = Presentation()
|
|
prs.slide_width = WIDTH
|
|
prs.slide_height = HEIGHT
|
|
|
|
# Cover
|
|
add_title_slide(prs, data)
|
|
|
|
for sl in data.get('slides', []):
|
|
t = sl.get('type', 'content')
|
|
if t == 'title': add_title_slide(prs, sl)
|
|
elif t == 'two_column': add_two_column_slide(prs, sl)
|
|
elif t == 'stats': add_stats_slide(prs, sl)
|
|
elif t == 'table': add_table_slide(prs, sl)
|
|
elif t == 'conclusion': add_conclusion_slide(prs, sl)
|
|
else: add_content_slide(prs, sl)
|
|
|
|
prs.save(sys.argv[2])
|
|
print(f"OK: {sys.argv[2]} ({len(prs.slides)} slides)")
|
|
|
|
if __name__ == '__main__':
|
|
main()
|