feat: presentaties ook als PowerPoint, met de notities in het notitieveld

- _gereedschap/maak_pptx.py: dia als beeld op 16:9, notities in de notitiepagina
- pptx voor de sessie en de lezing; die van de workshop blijft buiten git (privenotities)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Guido van Dijk
2026-09-22 21:08:19 +02:00
parent c453edfe8c
commit b1e9ffee9b
4 changed files with 96 additions and 0 deletions
+1
View File
@@ -10,3 +10,4 @@ _gereedschap/extract.py
NOTITIES-intern.md
00-presentatie/notities-begeleider.json
00-presentatie/presentatie-met-notities.pdf
00-presentatie/presentatie-workshop-ai-agents.pptx
Binary file not shown.
Binary file not shown.
+95
View File
@@ -0,0 +1,95 @@
#!/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 <dia-pdf>
Bijvoorbeeld in 00-sessie: 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>")
pdf = Path(sys.argv[1]).resolve()
if not pdf.exists():
sys.exit(f"niet gevonden: {pdf}")
MAP = pdf.parent
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}")