diff --git a/shared/website-taal-analyse/scripts/rapport_generator.py b/shared/website-taal-analyse/scripts/rapport_generator.py new file mode 100644 index 0000000..0ea8824 --- /dev/null +++ b/shared/website-taal-analyse/scripts/rapport_generator.py @@ -0,0 +1,464 @@ +""" +Website Taal-, Consistentie- & LLM-analyse — PDF Rapport Generator +==================================================================== +Gebruik dit script als startpunt. Vervang de inhoud van BEVINDINGEN +met de werkelijk gevonden actiepunten. De stijlfuncties en layoutlogica +hoef je niet aan te passen. + +Gebruik: + pip install reportlab --break-system-packages -q + python rapport_generator.py +""" + +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.units import cm +from reportlab.lib import colors +from reportlab.platypus import ( + SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, + HRFlowable, KeepTogether, PageBreak +) +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT +from datetime import date +import os + +# ── Configuratie — pas dit aan per opdracht ─────────────────────────────────── +MERKNAAM = "HelloPrint" # Merknaam van de geanalyseerde site +DOMEINNAAM = "helloprint.com/nl-nl" # Domein / pad +DOELGROEP = "B2B klein zakelijk (tot 10 medewerkers)" +LOCALE = "nl-NL" +OUTPUT = f"/mnt/user-data/outputs/{MERKNAAM}_Taal_Consistentie_LLM_{date.today().strftime('%Y%m%d')}.pdf" +# ───────────────────────────────────────────────────────────────────────────── + +doc = SimpleDocTemplate( + OUTPUT, pagesize=A4, + rightMargin=2.2*cm, leftMargin=2.2*cm, + topMargin=2.5*cm, bottomMargin=2.5*cm, +) + +# ── Kleurenpalet ────────────────────────────────────────────────────────────── +GROEN = colors.HexColor("#1a6b2f") +GROEN_L = colors.HexColor("#eaf5ec") +BLAUW = colors.HexColor("#1a3a6b") +BLAUW_L = colors.HexColor("#e8eef5") +PAARS = colors.HexColor("#5b2d8e") +PAARS_L = colors.HexColor("#f3eefb") +TEAL = colors.HexColor("#0e6b5e") +TEAL_L = colors.HexColor("#e6f4f2") +ROOD = colors.HexColor("#b03030") +ROOD_L = colors.HexColor("#fdf0ee") +ORANJE = colors.HexColor("#c05c00") +ORANJE_L = colors.HexColor("#fef5e7") +DONKER = colors.HexColor("#1a1a1a") +GRIJS = colors.HexColor("#f4f4f4") +GRIJS2 = colors.HexColor("#e8e8e8") +LICHT = colors.HexColor("#777777") +WIT = colors.white + +# ── Stijlen ─────────────────────────────────────────────────────────────────── +def ps(name, **kw): + return ParagraphStyle(name, **kw) + +BODY = ps("B", fontSize=9.5, leading=14, fontName="Helvetica", textColor=DONKER, spaceAfter=4) +SMALL = ps("S", fontSize=8.5, leading=12, fontName="Helvetica", textColor=LICHT) +H1 = ps("H1", fontSize=13, leading=17, fontName="Helvetica-Bold", textColor=BLAUW, spaceBefore=16, spaceAfter=5) +H2 = ps("H2", fontSize=10.5,leading=14, fontName="Helvetica-Bold", textColor=DONKER, spaceBefore=8, spaceAfter=3) + +# ── Thema- en prioriteitsconfiguratie ───────────────────────────────────────── +THEMA = { + "TAAL": (BLAUW, BLAUW_L, "T"), + "CONSISTENTIE": (GROEN, GROEN_L, "C"), + "LLM": (PAARS, PAARS_L, "L"), + "NAV": (TEAL, TEAL_L, "N"), +} +PRIO = { + "HOOG": ROOD, + "MIDDEL": ORANJE, + "LAAG": GROEN, +} +_tellers = {k: 0 for k in THEMA} + + +def omslagrij(tekst, stijlkw, achtergrond): + """Eén gekleurde rij voor het omslagblok.""" + t = Table([[Paragraph(tekst, ps(f"omslag_{hash(tekst)}", **stijlkw))]], colWidths=[17*cm]) + t.setStyle(TableStyle([ + ("BACKGROUND", (0,0), (-1,-1), achtergrond), + ("TOPPADDING", (0,0), (-1,-1), 6), + ("BOTTOMPADDING", (0,0), (-1,-1), 6), + ("LEFTPADDING", (0,0), (-1,-1), 20), + ("RIGHTPADDING", (0,0), (-1,-1), 20), + ])) + return t + + +def actiepunt(thema, prio, titel, probleem, vindplaats, actie): + """ + Geeft een opgemaakte actiepunt-blok terug als ReportLab Flowable. + + Parameters + ---------- + thema : "TAAL" | "CONSISTENTIE" | "LLM" | "NAV" + prio : "HOOG" | "MIDDEL" | "LAAG" + titel : Korte probleemtitel (string) + probleem : Uitleg van het probleem (mag HTML-tags bevatten) + vindplaats : Exacte locatie op de pagina + actie : Concreet advies (mag HTML-tags bevatten) + """ + _tellers[thema] += 1 + nr = _tellers[thema] + tk, tl, afk = THEMA[thema] + pk = PRIO[prio] + + uid = f"{thema}{nr}" + + def badge(tekst, breedte, achtergrond): + t = Table([[Paragraph(f"{tekst}", + ps(f"bdg_{uid}_{tekst}", fontSize=8, fontName="Helvetica-Bold", + textColor=WIT, leading=10, alignment=TA_CENTER))]], + colWidths=[breedte]) + t.setStyle(TableStyle([ + ("BACKGROUND", (0,0), (-1,-1), achtergrond), + ("TOPPADDING", (0,0), (-1,-1), 4), + ("BOTTOMPADDING", (0,0), (-1,-1), 4), + ("LEFTPADDING", (0,0), (-1,-1), 2), + ("RIGHTPADDING", (0,0), (-1,-1), 2), + ("ROUNDEDCORNERS", [3]), + ])) + return t + + labels = Table([[badge(f"{afk}{nr}", 0.9*cm, tk), + Spacer(0.25*cm, 1), + badge(prio, 1.4*cm, pk)]], + colWidths=[1*cm, 0.35*cm, 1.5*cm]) + labels.setStyle(TableStyle([ + ("TOPPADDING", (0,0), (-1,-1), 0), + ("BOTTOMPADDING", (0,0), (-1,-1), 0), + ("LEFTPADDING", (0,0), (-1,-1), 0), + ("RIGHTPADDING", (0,0), (-1,-1), 0), + ])) + + inhoud = [ + Paragraph(f"{titel}", + ps(f"ti_{uid}", fontSize=10.5, fontName="Helvetica-Bold", + textColor=DONKER, leading=14, spaceAfter=3)), + Paragraph(f"Probleem: {probleem}", + ps(f"pr_{uid}", fontSize=9, fontName="Helvetica", + textColor=DONKER, leading=13, spaceAfter=2)), + Paragraph(f"Vindplaats: {vindplaats}", + ps(f"vp_{uid}", fontSize=8.5, fontName="Helvetica", + textColor=LICHT, leading=12, spaceAfter=3)), + Paragraph(f"\u25B8 Actie: {actie}", + ps(f"ac_{uid}", fontSize=9, fontName="Helvetica-Bold", + textColor=tk, leading=13)), + ] + + blok = Table([[labels, inhoud]], colWidths=[3.1*cm, 13.9*cm]) + blok.setStyle(TableStyle([ + ("VALIGN", (0,0), (-1,-1), "TOP"), + ("BACKGROUND", (0,0), (-1,-1), tl), + ("TOPPADDING", (0,0), (-1,-1), 10), + ("BOTTOMPADDING", (0,0), (-1,-1), 10), + ("LEFTPADDING", (0,0), (-1,-1), 10), + ("RIGHTPADDING", (0,0), (-1,-1), 10), + ("ROUNDEDCORNERS", [5]), + ])) + return KeepTogether([blok, Spacer(1, 7)]) + + +def nav_tabel(items, kolombreedtes=(3.2*cm, 1.3*cm, 12.5*cm)): + """ + Geeft een opgemaakte navigatiebeoordelingstabel terug. + + items = lijst van tuples: + (nav_naam, symbool, kleur, toelichting) + symbool: "✓" | "△" | "✗" + kleur: GROEN | ORANJE | ROOD + """ + data = [[ + Paragraph("Menu-item", + ps("nv_h1", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)), + Paragraph("Oordeel", + ps("nv_h2", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)), + Paragraph("Toelichting", + ps("nv_h3", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)), + ]] + for naam, symbool, kleur, toelichting in items: + uid = naam.replace(" ", "_") + data.append([ + Paragraph(f"{naam}", + ps(f"nv_naam_{uid}", fontSize=9, fontName="Helvetica-Bold", textColor=DONKER, leading=12)), + Paragraph(symbool, + ps(f"nv_sym_{uid}", fontSize=11, fontName="Helvetica-Bold", + textColor=kleur, leading=14, alignment=TA_CENTER)), + Paragraph(toelichting, + ps(f"nv_toe_{uid}", fontSize=8.5, fontName="Helvetica", textColor=DONKER, leading=12)), + ]) + t = Table(data, colWidths=list(kolombreedtes)) + t.setStyle(TableStyle([ + ("BACKGROUND", (0,0), (-1,0), BLAUW), + ("ROWBACKGROUNDS", (0,1), (-1,-1), [WIT, GRIJS]), + ("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#dddddd")), + ("TOPPADDING", (0,0), (-1,-1), 7), + ("BOTTOMPADDING", (0,0), (-1,-1), 7), + ("LEFTPADDING", (0,0), (-1,-1), 8), + ("RIGHTPADDING", (0,0), (-1,-1), 8), + ("VALIGN", (0,0), (-1,-1), "TOP"), + ])) + return t + + +def mapping_tabel(rijen, header_kleur=TEAL): + """ + Vergelijkingstabel nav-item vs. pagina-naam vs. status. + + rijen = lijst van tuples: + (nav_naam, pagina_naam, status_tekst, status_kleur) + """ + data = [[ + Paragraph("Nav-item", + ps("mp_h1", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)), + Paragraph("Naam op overzichtspagina", + ps("mp_h2", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)), + Paragraph("Status", + ps("mp_h3", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)), + ]] + for nav, pagina, status, kleur in rijen: + uid = nav.replace(" ", "_") + data.append([ + Paragraph(nav, ps(f"mp_n_{uid}", fontSize=9, fontName="Helvetica-Bold", textColor=DONKER, leading=12)), + Paragraph(pagina, ps(f"mp_p_{uid}", fontSize=9, fontName="Helvetica", textColor=DONKER, leading=12)), + Paragraph(status, ps(f"mp_s_{uid}", fontSize=8.5, fontName="Helvetica-Bold", + textColor=kleur, leading=12, alignment=TA_CENTER)), + ]) + t = Table(data, colWidths=[3.5*cm, 7.5*cm, 6*cm]) + t.setStyle(TableStyle([ + ("BACKGROUND", (0,0), (-1,0), header_kleur), + ("ROWBACKGROUNDS", (0,1), (-1,-1), [WIT, GRIJS]), + ("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#dddddd")), + ("TOPPADDING", (0,0), (-1,-1), 7), + ("BOTTOMPADDING", (0,0), (-1,-1), 7), + ("LEFTPADDING", (0,0), (-1,-1), 8), + ("RIGHTPADDING", (0,0), (-1,-1), 8), + ("VALIGN", (0,0), (-1,-1), "MIDDLE"), + ])) + return t + + +def prioriteitsmatrix(acties): + """ + Slottabel met alle actiepunten, thema, impact en doorlooptijd. + + acties = lijst van tuples: + (nr, omschrijving, thema_label, impact_kleur, doorlooptijd) + """ + data = [[ + Paragraph("#", ps("pm_h0", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)), + Paragraph("Actie", ps("pm_h1", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12)), + Paragraph("Thema", ps("pm_h2", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)), + Paragraph("Impact", ps("pm_h3", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)), + Paragraph("Doorlooptijd", ps("pm_h4", fontSize=9, textColor=WIT, fontName="Helvetica-Bold", leading=12, alignment=TA_CENTER)), + ]] + for nr, omschr, thema_lbl, impact_kleur, tijd in acties: + data.append([ + Paragraph(str(nr), ps(f"pm_nr_{nr}", fontSize=9, fontName="Helvetica-Bold", textColor=DONKER, leading=12, alignment=TA_CENTER)), + Paragraph(omschr, ps(f"pm_om_{nr}", fontSize=9, fontName="Helvetica", textColor=DONKER, leading=13)), + Paragraph(thema_lbl, ps(f"pm_th_{nr}", fontSize=8.5, fontName="Helvetica", textColor=LICHT, leading=12, alignment=TA_CENTER)), + Paragraph("■", ps(f"pm_ic_{nr}", fontSize=12, fontName="Helvetica-Bold", textColor=impact_kleur, leading=14, alignment=TA_CENTER)), + Paragraph(tijd, ps(f"pm_ti_{nr}", fontSize=8.5, fontName="Helvetica", textColor=LICHT, leading=12, alignment=TA_CENTER)), + ]) + t = Table(data, colWidths=[0.8*cm, 9.2*cm, 2.4*cm, 1.8*cm, 2.8*cm]) + t.setStyle(TableStyle([ + ("BACKGROUND", (0,0), (-1,0), BLAUW), + ("ROWBACKGROUNDS", (0,1), (-1,-1), [WIT, GRIJS]), + ("GRID", (0,0), (-1,-1), 0.3, colors.HexColor("#e0e0e0")), + ("TOPPADDING", (0,0), (-1,-1), 6), + ("BOTTOMPADDING", (0,0), (-1,-1), 6), + ("LEFTPADDING", (0,0), (-1,-1), 7), + ("RIGHTPADDING", (0,0), (-1,-1), 7), + ("VALIGN", (0,0), (-1,-1), "MIDDLE"), + ])) + return t + + +# ══════════════════════════════════════════════════════════════════════════════ +# RAPPORT — pas de inhoud hieronder aan per opdracht +# ══════════════════════════════════════════════════════════════════════════════ + +story = [] + +# ── Omslag ──────────────────────────────────────────────────────────────────── +story.append(omslagrij( + "Analyse rapport", + dict(fontSize=9.5, fontName="Helvetica", textColor=colors.HexColor("#a8c0e0"), leading=14), + BLAUW)) +story.append(omslagrij( + "Taal • Consistentie • LLM-vindbaarheid", + dict(fontSize=24, fontName="Helvetica-Bold", textColor=WIT, leading=30), + BLAUW)) +story.append(omslagrij( + f"{DOMEINNAAM}  |  Doelgroep: {DOELGROEP}  |  Locale: {LOCALE}", + dict(fontSize=10, fontName="Helvetica", textColor=colors.HexColor("#9ab8d8"), leading=14), + BLAUW)) + +story.append(Spacer(1, 4)) + +meta = Table([[ + Paragraph(f"Datum: {date.today().strftime('%d %B %Y')}", SMALL), + Paragraph(f"Pagina: {DOMEINNAAM}", SMALL), + Paragraph("Opgesteld door: Claude (Anthropic)", SMALL), +]], colWidths=[5.5*cm, 6.5*cm, 5*cm]) +meta.setStyle(TableStyle([ + ("BACKGROUND", (0,0), (-1,-1), GRIJS), + ("TOPPADDING", (0,0), (-1,-1), 7), + ("BOTTOMPADDING", (0,0), (-1,-1), 7), + ("LEFTPADDING", (0,0), (-1,-1), 8), + ("RIGHTPADDING", (0,0), (-1,-1), 8), +])) +story.append(meta) +story.append(Spacer(1, 14)) + +story.append(Paragraph( + f"Dit rapport analyseert {DOMEINNAAM} op taal, consistentie en LLM-vindbaarheid. " + f"De primaire doelgroep is {DOELGROEP.lower()}. " + "Elk knelpunt heeft een concreet actiepunt met vindplaats en aanbeveling.", + BODY)) +story.append(Spacer(1, 10)) + +# ── SECTIE 1 — TAAL ─────────────────────────────────────────────────────────── +story.append(Paragraph("1   Taal — zuiverheid en geschiktheid voor de doelgroep", H1)) +story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=8)) +story.append(Paragraph( + "Vul hier een korte inleiding in over de algemene taalstijl van de geanalyseerde pagina.", + BODY)) +story.append(Spacer(1, 8)) + +# Voeg hier actiepunten toe met de actiepunt()-functie: +story.append(actiepunt( + "TAAL", "HOOG", + "Titel van het taalprobleem", + "Beschrijving van het probleem. Waarom is dit een probleem voor de doelgroep?", + "Exacte locatie op de pagina (bijv. 'Hoofdnavigatie — item 3', '<title>-tag', 'H1-heading')", + "Concreet advies. Geef bij voorkeur een voorbeeldtekst: 'Vervang X door Y.'")) + +# ── SECTIE 2 — MENUNAMEN ────────────────────────────────────────────────────── +story.append(Paragraph("2   Menunamen — helderheid voor de bezoeker", H1)) +story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=8)) +story.append(Paragraph( + "Beoordeling van elk navigatie-item vanuit het perspectief van de doelgroep.", + BODY)) +story.append(Spacer(1, 8)) + +# Voorbeeld nav_tabel() — pas items aan op basis van de geanalyseerde navigatie: +story.append(nav_tabel([ + ("Menu-item 1", "✓", GROEN, "Helder en herkenbaar."), + ("Menu-item 2", "△", ORANJE, "Dubbelzinnig of te vaag. Toelichting."), + ("Menu-item 3", "✗", ROOD, "Problematisch. Reden + aanbeveling."), +])) + +story.append(Spacer(1, 10)) + +# ── SECTIE 3 — CONSISTENTIE ─────────────────────────────────────────────────── +story.append(Paragraph("3   Consistentie — toon, aanspreekvorm en categorienamen", H1)) +story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=8)) +story.append(Paragraph("Inleiding consistentie.", BODY)) +story.append(Spacer(1, 8)) + +story.append(actiepunt( + "CONSISTENTIE", "HOOG", + "Consistentieprobleem", + "Beschrijving.", "Vindplaats.", "Actie.")) + +# ── SECTIE 4 — LLM-VINDBAARHEID ─────────────────────────────────────────────── +story.append(Paragraph("4   LLM-vindbaarheid", H1)) +story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=8)) +story.append(Paragraph("Inleiding LLM.", BODY)) +story.append(Spacer(1, 8)) + +story.append(actiepunt( + "LLM", "HOOG", + "LLM-probleem", + "Beschrijving.", "Vindplaats.", "Actie.")) + +# ── SECTIE 5 — NAVIGATIESTRUCTUUR (optioneel) ───────────────────────────────── +# Voeg deze sectie toe als je ook een overzichts-/categoriepagina hebt geanalyseerd. +# +# story.append(PageBreak()) +# story.append(Paragraph("5   Navigatiestructuur — nav vs. overzichtspagina", H1)) +# story.append(HRFlowable(width="100%", thickness=1.5, color=TEAL, spaceAfter=8)) +# story.append(Paragraph("5.1 Naamvergelijking", H2)) +# story.append(mapping_tabel([ +# ("Nav-item 1", "Paginanaam 1", "✓ Consistent", GROEN), +# ("Nav-item 2", "Andere naam op pagina", "✗ Mismatch", ROOD), +# ("Nav-item 3", "Subcategorie onder 'Overig'", "△ Inconsistent", ORANJE), +# ])) + +# ── SECTIE 6 — PRIORITEITSOVERZICHT ────────────────────────────────────────── +story.append(PageBreak()) +story.append(Paragraph("Prioriteitsoverzicht — wat als eerste aanpakken", H1)) +story.append(HRFlowable(width="100%", thickness=1.5, color=BLAUW, spaceAfter=10)) + +# Vul aan met alle actiepunten uit het rapport: +story.append(prioriteitsmatrix([ + ("1", "Omschrijving actie 1", "TAAL", ROOD, "1 dag"), + ("2", "Omschrijving actie 2", "CONS", ROOD, "1 dag"), + ("3", "Omschrijving actie 3", "LLM", ROOD, "1–2 wkn"), + ("4", "Omschrijving actie 4", "NAV", ORANJE, "1 week"), + ("5", "Omschrijving actie 5", "LLM", GROEN, "Doorlopend"), +])) + +story.append(Spacer(1, 8)) + +# Legenda +legenda_impact = Table([[ + Paragraph("■ Hoog — direct zichtbaar, groot effect", + ps("leg1", fontSize=8.5, fontName="Helvetica", textColor=ROOD, leading=12)), + Paragraph("■ Middel — merkbaar effect, planbaar", + ps("leg2", fontSize=8.5, fontName="Helvetica", textColor=ORANJE, leading=12)), + Paragraph("■ Laag — finetuning / continu", + ps("leg3", fontSize=8.5, fontName="Helvetica", textColor=GROEN, leading=12)), +]], colWidths=[6*cm, 6*cm, 5*cm]) +legenda_impact.setStyle(TableStyle([ + ("TOPPADDING", (0,0), (-1,-1), 0), + ("BOTTOMPADDING", (0,0), (-1,-1), 0), + ("LEFTPADDING", (0,0), (-1,-1), 0), + ("RIGHTPADDING", (0,0), (-1,-1), 0), +])) +story.append(legenda_impact) +story.append(Spacer(1, 10)) + +legenda_thema = Table([[ + Paragraph("T = Taal", ps("lt1", fontSize=8.5, fontName="Helvetica", textColor=BLAUW, leading=12)), + Paragraph("C = Consistentie", ps("lt2", fontSize=8.5, fontName="Helvetica", textColor=GROEN, leading=12)), + Paragraph("L = LLM", ps("lt3", fontSize=8.5, fontName="Helvetica", textColor=PAARS, leading=12)), + Paragraph("N = Navigatie", ps("lt4", fontSize=8.5, fontName="Helvetica", textColor=TEAL, leading=12)), +]], colWidths=[4*cm, 4*cm, 4*cm, 5*cm]) +legenda_thema.setStyle(TableStyle([ + ("TOPPADDING", (0,0), (-1,-1), 0), + ("BOTTOMPADDING", (0,0), (-1,-1), 0), + ("LEFTPADDING", (0,0), (-1,-1), 0), + ("RIGHTPADDING", (0,0), (-1,-1), 0), +])) +story.append(legenda_thema) +story.append(Spacer(1, 20)) + +# ── Footer ──────────────────────────────────────────────────────────────────── +story.append(HRFlowable(width="100%", thickness=0.8, color=GRIJS2, spaceAfter=8)) +footer_t = Table([[ + Paragraph(f"{MERKNAAM} — Taal, Consistentie & LLM-vindbaarheid — Vertrouwelijk", SMALL), + Paragraph(f"Opgesteld {date.today().strftime('%d-%m-%Y')} | Claude (Anthropic)", + ps("footer_r", fontSize=8, textColor=LICHT, fontName="Helvetica", + leading=11, alignment=TA_RIGHT)), +]], colWidths=[9*cm, 8*cm]) +footer_t.setStyle(TableStyle([ + ("TOPPADDING", (0,0), (-1,-1), 0), + ("BOTTOMPADDING", (0,0), (-1,-1), 0), + ("LEFTPADDING", (0,0), (-1,-1), 0), + ("RIGHTPADDING", (0,0), (-1,-1), 0), + ("VALIGN", (0,0), (-1,-1), "MIDDLE"), +])) +story.append(footer_t) + +# ── Bouw PDF ────────────────────────────────────────────────────────────────── +doc.build(story) +print(f"PDF klaar: {OUTPUT}") \ No newline at end of file