153 lines
5.3 KiB
Python
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()
|