Files
html/api/ambre-tool-pptx-render.py

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()