#!/usr/bin/env python3 """Génère le rapport PDF à partir de report/rapport.html, avec PyMuPDF (fitz). Aucune dépendance externe : ni pandoc, ni LaTeX, ni navigateur. On utilise l'API fitz.Story (rendu HTML/CSS -> PDF multi-pages) puis une seconde passe pour le pied de page et les numéros de page. python tools/make_report_pdf.py """ import os import sys import fitz # PyMuPDF ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) HTML = os.path.join(ROOT, "report", "rapport.html") OUT = os.path.join(ROOT, "dist", "Puyaubreau_Russac_rapport.pdf") # Feuille de style : mise en page A4 sobre, titres colorés, tables et blocs
# en Courier pour aligner diagrammes et carte des liserés.
CSS = """
* { font-family: serif; }
body { font-size: 10.5pt; line-height: 1.45; color: #1a1a1a; }
h1, h2, h3, .cover-title, .cover-course, .cover-sub, th { font-family: sans-serif; }
h2 { font-size: 15pt; color: #1c3d5a; margin: 18pt 0 6pt 0;
border-bottom: 1.5px solid #1c3d5a; padding-bottom: 2pt; }
h3 { font-size: 12pt; color: #2a6f97; margin: 12pt 0 3pt 0; }
p { margin: 5pt 0; text-align: justify; }
ul, ol { margin: 4pt 0 4pt 0; }
li { margin: 2pt 0; }
code { font-family: monospace; font-size: 9.5pt; color: #8a2846; }
pre.grid { font-family: monospace; font-size: 9pt; line-height: 1.25;
background: #f4f6f8; border: 1px solid #d3dae0; border-radius: 3px;
padding: 7pt; margin: 6pt 0; color: #14213d; white-space: pre; }
table { border-collapse: collapse; width: 100%; margin: 7pt 0; font-size: 9.5pt; }
th { background: #1c3d5a; color: #ffffff; text-align: left; padding: 4pt 6pt; }
td { border: 1px solid #c5ccd3; padding: 4pt 6pt; vertical-align: top; }
tr:nth-child(even) td { background: #f4f6f8; }
p.note { background: #fff8e6; border-left: 3px solid #e0a526;
padding: 5pt 8pt; margin: 7pt 0; font-size: 9.8pt; }
/* Sommaire */
.toc-title { border: none; color: #1c3d5a; margin-bottom: 4pt; }
ol.toc { font-size: 11pt; line-height: 1.7; color: #1a1a1a; }
ol.toc li { margin: 1pt 0; }
/* Éviter qu'un bloc préformaté ou une table soit coupé entre deux pages. */
pre.grid, table, tr { page-break-inside: avoid; }
h2, h3 { page-break-after: avoid; }
.cover { page-break-after: always; }
ol.toc { page-break-after: always; }
/* Page de titre */
.cover { text-align: center; padding-top: 40pt; }
.cover-univ { font-size: 10pt; color: #555; margin-bottom: 30pt; }
.cover-course { font-size: 13pt; color: #2a6f97; letter-spacing: 1pt; }
.cover-title { font-size: 30pt; color: #1c3d5a; margin: 8pt 0 0 0; }
.cover-sub { font-size: 12pt; color: #333; margin: 4pt 0; }
.cover-authors{ font-size: 15pt; color: #14213d; margin-top: 34pt; font-family: sans-serif; }
.cover-date { font-size: 11pt; color: #555; margin-top: 6pt; }
.cover-meta { font-size: 9.5pt; color: #555; margin-top: 34pt; line-height: 1.6; }
"""
MARGIN = 48 # marge en points (1pt = 1/72")
FOOTER = "Escampe — Puyaubreau / Russac — version finale"
def build():
with open(HTML, "r", encoding="utf-8") as f:
html = f.read()
os.makedirs(os.path.dirname(OUT), exist_ok=True)
mediabox = fitz.paper_rect("a4")
where = mediabox + (MARGIN, MARGIN, -MARGIN, -MARGIN)
# 1) Rendu du flux HTML en pages PDF via Story.
writer = fitz.DocumentWriter(OUT)
story = fitz.Story(html=html, user_css=CSS)
more = 1
pages = 0
while more:
dev = writer.begin_page(mediabox)
more, _ = story.place(where)
story.draw(dev)
writer.end_page()
pages += 1
writer.close()
# 2) Seconde passe : pied de page + numéros « page X / N ».
doc = fitz.open(OUT)
n = doc.page_count
for i, page in enumerate(doc, start=1):
y = page.rect.height - 26
page.insert_text((MARGIN, y), FOOTER, fontname="helv",
fontsize=7.5, color=(0.45, 0.45, 0.45))
label = f"page {i} / {n}"
w = fitz.get_text_length(label, fontname="helv", fontsize=7.5)
page.insert_text((page.rect.width - MARGIN - w, y), label,
fontname="helv", fontsize=7.5, color=(0.45, 0.45, 0.45))
doc.saveIncr()
doc.close()
return pages, n
def verify():
"""Contrôle que les accents survivent au rendu (round-trip texte)."""
doc = fitz.open(OUT)
full = "".join(p.get_text() for p in doc)
doc.close()
# Mots accentués stables, présents dans report/rapport.html (titres de section).
probes = ["liseré", "Présentation", "Modélisation", "Intégration",
"Heuristique", "Puyaubreau"]
missing = [s for s in probes if s not in full]
return missing, len(full)
if __name__ == "__main__":
pages, n = build()
missing, chars = verify()
print(f"PDF écrit : {OUT}")
print(f"Pages : {n} | texte extrait : {chars} caractères")
if missing:
print("ATTENTION, chaînes accentuées introuvables après rendu :", missing)
sys.exit(1)
print("Accents vérifiés (round-trip OK).")