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'{k}') 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"{exp.get('role', '')} — {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"{k}: {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}")