Files
dify-handleidingen/_gereedschap/maak_pptx.py
T

119 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Maakt een PowerPoint-bestand van een gebouwde presentatie uit deze repository.
Elke dia wordt als beeld op een 16:9-slide gezet en de sprekersnotities komen in het
notitieveld, zodat PowerPoint ze in de sprekersweergave op jouw scherm toont terwijl de zaal
alleen de dia ziet. Het beeld is een afbeelding: de opmaak is precies gelijk aan de PDF, maar
de tekst is in PowerPoint niet te bewerken. Wijzigen doe je in maak_dias.py en dan opnieuw
bouwen.
Gebruik: python3 ../_gereedschap/maak_pptx.py
vanuit de map van de presentatie (00-sessie, 00-lezing, 00-presentatie). Het script
zoekt zelf de dia-PDF, dus die van de dia's en niet de notitieversie. Staat er meer dan
een, geef dan de naam mee: python3 ../_gereedschap/maak_pptx.py sessie-agents-en-projecten.pdf
Staat er een notities-begeleider.json naast maak_dias.py, dan gebruikt het script die
uitgebreide notities en zegt het erbij; dat bestand is privé, dus deel die pptx niet.
Vereist: pdftoppm (Poppler) en python-pptx.
"""
import json
import re
import subprocess
import sys
import tempfile
from pathlib import Path
try:
from pptx import Presentation
from pptx.util import Inches
except ImportError:
sys.exit("python-pptx ontbreekt. Bijvoorbeeld: ~/.local/venvs/uitlegvideo/bin/python "
"../_gereedschap/maak_pptx.py <dia-pdf>")
def zoek_dia_pdf(waar: Path) -> Path:
"""De PDF van de dia's in deze map: zonder -met-notities in de naam."""
if not (waar / "maak_dias.py").exists():
sys.exit(f"{waar} is geen presentatiemap. Draai dit vanuit 00-sessie, 00-lezing of "
f"00-presentatie, of geef het pad naar de dia-PDF mee.")
kandidaten = [f for f in sorted(waar.glob("*.pdf")) if not f.stem.endswith("-met-notities")]
if not kandidaten:
sys.exit(f"geen dia-PDF in {waar.name}; bouw hem eerst met python3 bouw_pdf.py")
if len(kandidaten) > 1:
namen = ", ".join(f.name for f in kandidaten[:5])
sys.exit(f"meerdere PDF's in {waar.name} ({namen}); geef de naam van de dia-PDF mee")
return kandidaten[0]
if len(sys.argv) > 1:
pdf = Path(sys.argv[1]).resolve()
if not pdf.exists():
sys.exit(f"niet gevonden: {pdf}")
if pdf.stem.endswith("-met-notities"):
sys.exit(f"{pdf.name} is de notitieversie. Geef de PDF van de dia's, of laat het argument weg.")
else:
pdf = zoek_dia_pdf(Path.cwd())
MAP = pdf.parent
if not (MAP / "maak_dias.py").exists():
sys.exit(f"geen maak_dias.py in {MAP}; draai dit vanuit de map van een presentatie")
P = MAP / "project"
PRIVE = (MAP / "notities-begeleider.json").exists()
# 1. dia's en PDF opnieuw bouwen, zodat beeld en notities bij elkaar horen
subprocess.run([sys.executable, str(MAP / "maak_dias.py")] + (["--notities"] if PRIVE else []),
check=True, capture_output=True)
deck = json.loads((P / "deck.json").read_text(encoding="utf-8"))
faces = "".join(f'<link rel="stylesheet" href="{f["href"]}">' for f in deck["faces"].values())
tmp = Path(tempfile.mkdtemp(prefix="pptx-"))
css = """
@page { size: 1920px 1080px; margin: 0; }
html, body { margin: 0; padding: 0; }
* { box-sizing: border-box; margin: 0; padding: 0; }
section { position: relative; width: 1920px; height: 1080px; overflow: hidden; page-break-after: always; break-after: page; }
section:last-child { page-break-after: auto; }
aside { display: none; }
ul { padding-left: 1.1em; }
table { border-collapse: collapse; width: 100%; }
th, td { border: 1px solid #D8D2C4; padding: 12px 18px; text-align: left; vertical-align: top; }
img { max-width: 100%; }
"""
(tmp / "dias.html").write_text(
f'<!DOCTYPE html><html lang="nl"><head><meta charset="utf-8">{faces}<style>{css}</style></head><body>'
+ "".join((P / "slides" / f"{sid}.html").read_text(encoding="utf-8") for sid in deck["order"])
+ "</body></html>", encoding="utf-8")
subprocess.run(["google-chrome", "--headless=new", "--disable-gpu", "--no-pdf-header-footer",
"--virtual-time-budget=8000", f"--print-to-pdf={tmp / 'dias.pdf'}", (tmp / "dias.html").as_uri()],
check=True, capture_output=True)
# 2. elke pagina als beeld
subprocess.run(["pdftoppm", "-r", "100", "-jpeg", "-jpegopt", "quality=88",
str(tmp / "dias.pdf"), str(tmp / "dia")], check=True)
beelden = sorted(tmp.glob("dia-*.jpg"))
if len(beelden) != len(deck["order"]):
sys.exit(f"{len(beelden)} beelden voor {len(deck['order'])} dia's; bouw de dia's opnieuw")
def notities(sid):
tekst = re.search(r"<aside>(.*?)</aside>",
(P / "slides" / f"{sid}.html").read_text(encoding="utf-8"), re.S)
tekst = (tekst.group(1).strip() if tekst else "")
tekst = re.sub(r"<[^>]+>", "", tekst)
# de privénotities staan als BEDOELING: ... | ZEGGEN: ... op één regel; leesbaar maken
return "\n\n".join(d.strip() for d in tekst.split(" | ") if d.strip())
# 3. de pptx: 16:9, elk beeld schermvullend, notities in het notitieveld
prs = Presentation()
prs.slide_width, prs.slide_height = Inches(13.333), Inches(7.5)
leeg = prs.slide_layouts[6] # blank
for n, sid in enumerate(deck["order"]):
dia = prs.slides.add_slide(leeg)
dia.shapes.add_picture(str(beelden[n]), 0, 0, width=prs.slide_width, height=prs.slide_height)
dia.notes_slide.notes_text_frame.text = notities(sid)
uit = pdf.with_suffix(".pptx")
prs.save(str(uit))
# 4. de dia-bestanden terugzetten op de publieke versie
subprocess.run([sys.executable, str(MAP / "maak_dias.py")], check=True, capture_output=True)
soort = "uitgebreide begeleidersnotities (privé, niet delen)" if PRIVE else "de notities uit de dia's"
print(f"{uit.name}: {uit.stat().st_size // 1024} kB, {len(deck['order'])} dia's, met {soort}")