#!/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 zipfile 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 ") 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'' 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'{faces}' + "".join((P / "slides" / f"{sid}.html").read_text(encoding="utf-8") for sid in deck["order"]) + "", 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"", (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)) def repareer_notitiemaster(pad: Path) -> bool: """python-pptx schrijft de notitiemaster wel als onderdeel, maar noemt hem niet in presentation.xml. PowerPoint laat de notities dan weg. Deze reparatie zet de ontbrekende regel erin, op de plek die het schema voorschrijft: direct na de dia-master.""" with zipfile.ZipFile(pad) as bron: pres = bron.read("ppt/presentation.xml").decode("utf-8") if "notesMasterIdLst" in pres: return False rels = bron.read("ppt/_rels/presentation.xml.rels").decode("utf-8") m = re.search(r'Id="([^"]+)"[^>]*notesMaster[^>]*/>', rels) if not m: return False pres = pres.replace( "", f'' f"", 1) onderdelen = [(i, bron.read(i.filename)) for i in bron.infolist()] tijdelijk = pad.with_suffix(".tmp") with zipfile.ZipFile(tijdelijk, "w", zipfile.ZIP_DEFLATED) as nieuw_zip: for info, data in onderdelen: nieuw_zip.writestr(info, pres.encode("utf-8") if info.filename == "ppt/presentation.xml" else data) tijdelijk.replace(pad) return True gerepareerd = repareer_notitiemaster(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}" + (", notitiemaster toegevoegd" if gerepareerd else ""))