Files
PdfScrapper/pdfscrapper/pdf_builder.py
T
2026-08-11 15:41:55 +02:00

153 lines
5.3 KiB
Python

"""Construction du PDF final à partir des extraits capturés."""
from __future__ import annotations
from functools import partial
from xml.sax.saxutils import escape
from reportlab.lib import colors
from reportlab.lib.pagesizes import A4, LETTER
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import cm
from reportlab.platypus import PageBreak, Paragraph, SimpleDocTemplate, Spacer
from .models import Clip
from .pdf_style import PdfStyle, bold_variant
_PAGE_SIZES = {"A4": A4, "LETTER": LETTER}
def build_pdf(clips: list[Clip], output_path: str, style: PdfStyle | None = None) -> None:
"""Génère un PDF regroupant tous les extraits, dans l'ordre fourni,
selon la mise en forme décrite par `style` (voir pdf_style.py).
Chaque extrait démarre — selon `style.page_break_between_clips` — sur
une nouvelle page ou juste après un espacement, précédé de son titre
(personnalisable, voir MainWindow._rename_selected_clip) et,
optionnellement, de son URL d'origine et de la date/heure de capture.
Si `style.include_toc` est actif et qu'il y a plus d'un extrait, un
sommaire avec liens cliquables vers chaque section est inséré après
le titre du document.
"""
if not clips:
raise ValueError("Aucun extrait à exporter.")
style = style or PdfStyle()
pagesize = _PAGE_SIZES.get(style.page_size, A4)
margin = style.margin_cm * cm
doc = SimpleDocTemplate(
output_path,
pagesize=pagesize,
leftMargin=margin,
rightMargin=margin,
topMargin=margin,
bottomMargin=margin,
title=style.book_title,
)
base_styles = getSampleStyleSheet()
bold_font = bold_variant(style.font_name)
title_style = ParagraphStyle(
"BookTitle",
parent=base_styles["Title"],
fontName=bold_font,
textColor=colors.HexColor(style.text_color),
)
toc_heading_style = ParagraphStyle(
"TocHeading",
parent=base_styles["Heading1"],
fontName=bold_font,
textColor=colors.HexColor(style.heading_color),
spaceAfter=8,
)
toc_entry_style = ParagraphStyle(
"TocEntry",
parent=base_styles["Normal"],
fontName=style.font_name,
fontSize=style.body_font_size,
leftIndent=0.5 * cm,
spaceAfter=6,
)
heading_style = ParagraphStyle(
"ClipHeading",
parent=base_styles["Heading2"],
fontName=bold_font,
fontSize=style.heading_font_size,
leading=style.heading_font_size * 1.2,
textColor=colors.HexColor(style.heading_color),
spaceBefore=0,
spaceAfter=2,
)
meta_style = ParagraphStyle(
"ClipMeta",
parent=base_styles["Normal"],
fontName=style.font_name,
fontSize=8,
textColor=colors.grey,
spaceAfter=10,
)
body_style = ParagraphStyle(
"ClipBody",
parent=base_styles["BodyText"],
fontName=style.font_name,
fontSize=style.body_font_size,
leading=style.body_font_size * 1.4,
textColor=colors.HexColor(style.text_color),
spaceAfter=10,
)
story: list = [Paragraph(escape(style.book_title), title_style), Spacer(1, 1 * cm)]
show_toc = style.include_toc and len(clips) > 1
if show_toc:
story.append(Paragraph("Sommaire", toc_heading_style))
for index, clip in enumerate(clips):
anchor = f"pdfscrapper_clip_{index}"
entry_text = (
f'<a href="#{anchor}" color="{style.heading_color}"><u>'
f"{escape(clip.title or 'Sans titre')}</u></a>"
)
story.append(Paragraph(entry_text, toc_entry_style))
story.append(PageBreak())
for index, clip in enumerate(clips):
if index > 0:
if style.page_break_between_clips:
story.append(PageBreak())
else:
story.append(Spacer(1, 0.6 * cm))
# Ancre invisible utilisée par les liens du sommaire (reportlab
# résout <a href="#nom"> vers <a name="nom"/> n'importe où dans le
# document, y compris sur une page ultérieure).
anchor_tag = f'<a name="pdfscrapper_clip_{index}"/>' if show_toc else ""
story.append(Paragraph(anchor_tag + escape(clip.title or "Sans titre"), heading_style))
if style.show_meta:
meta_text = (
f"{escape(clip.url)} — capturé le "
f"{clip.captured_at.strftime('%d/%m/%Y %H:%M')}"
)
story.append(Paragraph(meta_text, meta_style))
for paragraph in clip.text.split("\n"):
paragraph = paragraph.strip()
if paragraph:
story.append(Paragraph(escape(paragraph), body_style))
paint_background = partial(_paint_background, style=style)
doc.build(story, onFirstPage=paint_background, onLaterPages=paint_background)
def _paint_background(canvas, doc, style: PdfStyle) -> None:
"""Remplit la page d'une couleur de fond avant que le contenu ne soit
dessiné par-dessus (appelé par reportlab pour chaque page)."""
if style.background_color.lower() in ("#ffffff", "white", "#fff"):
return
canvas.saveState()
canvas.setFillColor(colors.HexColor(style.background_color))
canvas.rect(0, 0, doc.pagesize[0], doc.pagesize[1], fill=1, stroke=0)
canvas.restoreState()