First commit
This commit is contained in:
+160
@@ -0,0 +1,160 @@
|
||||
import sys
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.colors import HexColor
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from config import (
|
||||
USABLE_WIDTH, USABLE_HEIGHT, MARGIN_X_PT, MARGIN_Y_PT,
|
||||
MINIMUM_POLICE_SIZE, COLOR_PRIMARY, COLOR_SECONDARY, COLOR_TEXT
|
||||
)
|
||||
|
||||
def create_styles(scale: float):
|
||||
"""Génère les styles dynamiques appliqués au texte selon le facteur d'échelle."""
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
title_size = max(14 * scale, 9)
|
||||
heading_size = max(11 * scale, 8)
|
||||
body_size = 9 * scale
|
||||
leading_ratio = 1.25
|
||||
|
||||
return {
|
||||
"Name": ParagraphStyle(
|
||||
"NameStyle",
|
||||
fontName="Helvetica-Bold",
|
||||
fontSize=title_size * 1.3,
|
||||
leading=title_size * 1.3 * leading_ratio,
|
||||
textColor=HexColor(COLOR_PRIMARY),
|
||||
spaceAfter=2 * scale
|
||||
),
|
||||
"JobTitle": ParagraphStyle(
|
||||
"JobTitleStyle",
|
||||
fontName="Helvetica-Bold",
|
||||
fontSize=title_size,
|
||||
leading=title_size * leading_ratio,
|
||||
textColor=HexColor(COLOR_SECONDARY),
|
||||
spaceAfter=4 * scale
|
||||
),
|
||||
"SectionHeading": ParagraphStyle(
|
||||
"SectionHeadingStyle",
|
||||
fontName="Helvetica-Bold",
|
||||
fontSize=heading_size,
|
||||
leading=heading_size * leading_ratio,
|
||||
textColor=HexColor(COLOR_PRIMARY),
|
||||
spaceBefore=6 * scale,
|
||||
spaceAfter=2 * scale
|
||||
),
|
||||
"Body": ParagraphStyle(
|
||||
"BodyStyle",
|
||||
fontName="Helvetica",
|
||||
fontSize=body_size,
|
||||
leading=body_size * leading_ratio,
|
||||
textColor=HexColor(COLOR_TEXT),
|
||||
spaceAfter=2 * scale
|
||||
),
|
||||
"SubText": ParagraphStyle(
|
||||
"SubTextStyle",
|
||||
fontName="Helvetica-Oblique",
|
||||
fontSize=max(body_size * 0.9, 6),
|
||||
leading=max(body_size * 0.9, 6) * leading_ratio,
|
||||
textColor=HexColor(COLOR_TEXT)
|
||||
),
|
||||
"body_font_size": body_size
|
||||
}
|
||||
|
||||
def build_story(data: dict, styles: dict, scale: float):
|
||||
"""Construit la liste d'éléments Flowable du CV."""
|
||||
story = []
|
||||
|
||||
# 1. En-tête : Identité
|
||||
id_data = data["identity"]
|
||||
context = data["context"]
|
||||
links = data["hyperlinks"]
|
||||
|
||||
full_name = f"{id_data.get('first_name', '')} {id_data.get('last_name', '')}"
|
||||
story.append(Paragraph(full_name, styles["Name"]))
|
||||
story.append(Paragraph(context.get("title", ""), styles["JobTitle"]))
|
||||
|
||||
# Infos contact & liens
|
||||
contact_bits = [id_data.get("email"), id_data.get("phone"), id_data.get("location")]
|
||||
for k, v in links.items():
|
||||
contact_bits.append(f'<a href="{v}"><u>{k}</u></a>')
|
||||
|
||||
contact_line = " • ".join(filter(None, contact_bits))
|
||||
story.append(Paragraph(contact_line, styles["SubText"]))
|
||||
story.append(Spacer(1, 4 * scale))
|
||||
story.append(HRFlowable(width="100%", thickness=1, color=HexColor(COLOR_SECONDARY), spaceAfter=6 * scale))
|
||||
|
||||
# 2. Résumé
|
||||
if context.get("summary"):
|
||||
story.append(Paragraph("PROFIL", styles["SectionHeading"]))
|
||||
story.append(Paragraph(context["summary"], styles["Body"]))
|
||||
|
||||
# 3. Expériences
|
||||
if data.get("experiences"):
|
||||
story.append(Paragraph("EXPÉRIENCES PROFESSIONNELLES", styles["SectionHeading"]))
|
||||
for exp in data["experiences"]:
|
||||
header = f"<b>{exp.get('role', '')}</b> — {exp.get('company', '')} ({exp.get('dates', '')})"
|
||||
story.append(Paragraph(header, styles["Body"]))
|
||||
for detail in exp.get("details", []):
|
||||
bullet = f"• {detail}"
|
||||
story.append(Paragraph(bullet, styles["Body"]))
|
||||
|
||||
# 4. Langues & Hobbies
|
||||
if data.get("languages"):
|
||||
story.append(Paragraph("LANGUES", styles["SectionHeading"]))
|
||||
langs = [f"<b>{k}:</b> {v}" for k, v in data["languages"].items()]
|
||||
story.append(Paragraph(" • ".join(langs), styles["Body"]))
|
||||
|
||||
if data.get("hobbies"):
|
||||
story.append(Paragraph("CENTRES D'INTÉRÊT", styles["SectionHeading"]))
|
||||
story.append(Paragraph(" • ".join(data["hobbies"]), styles["Body"]))
|
||||
|
||||
return story
|
||||
|
||||
def measure_story_height(story: list) -> float:
|
||||
"""Calcule la hauteur totale cumulée du contenu."""
|
||||
total_height = 0
|
||||
for item in story:
|
||||
_, h = item.wrap(USABLE_WIDTH, USABLE_HEIGHT)
|
||||
total_height += h
|
||||
return total_height
|
||||
|
||||
def generate_pdf(data: dict, output_path: str):
|
||||
"""Calcule l'échelle optimale et génère le PDF."""
|
||||
scale = 1.0
|
||||
min_scale = 0.4
|
||||
step = 0.03
|
||||
|
||||
# Ajustement dynamique jusqu'à ce que le contenu rentre sur 1 page
|
||||
while scale >= min_scale:
|
||||
styles = create_styles(scale)
|
||||
story = build_story(data, styles, scale)
|
||||
height = measure_story_height(story)
|
||||
|
||||
if height <= USABLE_HEIGHT:
|
||||
break
|
||||
scale -= step
|
||||
|
||||
critical_font_size = styles["body_font_size"]
|
||||
|
||||
# Alerte console si la police est sous le seuil critique
|
||||
if critical_font_size < MINIMUM_POLICE_SIZE:
|
||||
print(f"\n⚠️ [ALERTE TAILLE POLICE]")
|
||||
print(f"La taille de police nécessaire ({critical_font_size:.1f}pt) est inférieure "
|
||||
f"au seuil configuré ({MINIMUM_POLICE_SIZE}pt).")
|
||||
choice = input("Voulez-vous quand même générer le fichier ? (o/N) : ").strip().lower()
|
||||
if choice not in ['o', 'oui']:
|
||||
print("Génération annulée.")
|
||||
sys.exit(0)
|
||||
|
||||
# Document final
|
||||
doc = SimpleDocTemplate(
|
||||
output_path,
|
||||
pagesize=A4,
|
||||
leftMargin=MARGIN_X_PT,
|
||||
rightMargin=MARGIN_X_PT,
|
||||
topMargin=MARGIN_Y_PT,
|
||||
bottomMargin=MARGIN_Y_PT
|
||||
)
|
||||
doc.build(story)
|
||||
print(f"✅ CV généré avec succès : {output_path}")
|
||||
Reference in New Issue
Block a user