"""
PDF Generator Utility
Handles creation of PDF documents for portfolio exports
"""
import io
import sys
from datetime import datetime
from typing import Any, Dict, List, Optional
from reportlab.lib import colors
from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT, TA_RIGHT
from reportlab.lib.pagesizes import A4, letter
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.platypus import (
HRFlowable,
Image,
PageBreak,
Paragraph,
SimpleDocTemplate,
Spacer,
Table,
TableStyle,
)
class PDFGenerator:
"""Generate professional PDF documents"""
def __init__(self):
"""Initialize PDF generator"""
self.page_size = letter
self.styles = getSampleStyleSheet()
self._setup_custom_styles()
def _setup_custom_styles(self):
"""Setup custom paragraph styles"""
# Title style
self.styles.add(
ParagraphStyle(
name="CustomTitle",
parent=self.styles["Heading1"],
fontSize=24,
textColor=colors.HexColor("#003366"),
spaceAfter=30,
alignment=TA_CENTER,
fontName="Helvetica-Bold",
)
)
# Subtitle style
self.styles.add(
ParagraphStyle(
name="CustomSubtitle",
parent=self.styles["Normal"],
fontSize=14,
textColor=colors.HexColor("#666666"),
spaceAfter=20,
alignment=TA_CENTER,
fontName="Helvetica",
)
)
# Section heading
self.styles.add(
ParagraphStyle(
name="SectionHeading",
parent=self.styles["Heading2"],
fontSize=16,
textColor=colors.HexColor("#003366"),
spaceAfter=12,
spaceBefore=20,
fontName="Helvetica-Bold",
borderWidth=1,
borderColor=colors.HexColor("#003366"),
borderPadding=5,
)
)
# Body text
self.styles.add(
ParagraphStyle(
name="CustomBody",
parent=self.styles["Normal"],
fontSize=11,
textColor=colors.HexColor("#333333"),
spaceAfter=10,
alignment=TA_JUSTIFY,
fontName="Helvetica",
)
)
# Contact info
self.styles.add(
ParagraphStyle(
name="ContactInfo",
parent=self.styles["Normal"],
fontSize=10,
textColor=colors.HexColor("#666666"),
alignment=TA_CENTER,
fontName="Helvetica",
)
)
def generate_portfolio_pdf(self, data: Dict[str, Any]) -> io.BytesIO:
"""
Generate portfolio PDF
Args:
data: Portfolio data containing:
- name: Full name
- title: Professional title
- bio: Biography
- contact: Contact info (email, phone, website, linkedin)
- skills: List or dict of skills
- experience: List of work experiences
- education: List of education entries
- projects: List of projects
- certifications: List of certifications (optional)
Returns:
BytesIO buffer containing the PDF file
"""
buffer = io.BytesIO()
doc = SimpleDocTemplate(
buffer,
pagesize=self.page_size,
rightMargin=0.75 * inch,
leftMargin=0.75 * inch,
topMargin=0.75 * inch,
bottomMargin=0.75 * inch,
)
# Container for the 'Flowable' objects
elements = []
# Header - Name and Title
name = data.get("name", "Your Name")
elements.append(Paragraph(name, self.styles["CustomTitle"]))
title = data.get("title", "Professional Title")
elements.append(Paragraph(title, self.styles["CustomSubtitle"]))
# Contact Information
contact = data.get("contact", {})
contact_parts = []
if contact.get("email"):
contact_parts.append(contact["email"])
if contact.get("phone"):
contact_parts.append(contact["phone"])
if contact.get("website"):
contact_parts.append(
f'{contact["website"]}'
)
if contact.get("linkedin"):
contact_parts.append(f"LinkedIn: {contact['linkedin']}")
if contact_parts:
contact_text = " | ".join(contact_parts)
elements.append(Paragraph(contact_text, self.styles["ContactInfo"]))
elements.append(Spacer(1, 0.2 * inch))
# Horizontal line
elements.append(
HRFlowable(width="100%", thickness=2, color=colors.HexColor("#003366"))
)
elements.append(Spacer(1, 0.2 * inch))
# Bio/Summary
if data.get("bio"):
elements.append(
Paragraph("PROFESSIONAL SUMMARY", self.styles["SectionHeading"])
)
elements.append(Paragraph(data["bio"], self.styles["CustomBody"]))
elements.append(Spacer(1, 0.2 * inch))
# Skills
if data.get("skills"):
elements.append(Paragraph("SKILLS", self.styles["SectionHeading"]))
skills = data["skills"]
if isinstance(skills, dict):
# Categorized skills
for category, skill_list in skills.items():
skills_text = f"{category}: {', '.join(skill_list)}"
elements.append(Paragraph(skills_text, self.styles["CustomBody"]))
else:
# Simple list
skills_text = ", ".join(skills)
elements.append(Paragraph(skills_text, self.styles["CustomBody"]))
elements.append(Spacer(1, 0.2 * inch))
# Work Experience
if data.get("experience"):
elements.append(Paragraph("WORK EXPERIENCE", self.styles["SectionHeading"]))
for exp in data["experience"]:
# Job title and company
job_title = exp.get("title", "Position")
company = exp.get("company", "Company")
dates = f"{exp.get('start_date', 'Start')} - {exp.get('end_date', 'Present')}"
title_text = f"{job_title} at {company}"
elements.append(Paragraph(title_text, self.styles["CustomBody"]))
# Dates and location
location_text = dates
if exp.get("location"):
location_text += f" | {exp['location']}"
elements.append(
Paragraph(f"{location_text}", self.styles["CustomBody"])
)
# Responsibilities
if exp.get("responsibilities"):
for resp in exp["responsibilities"]:
elements.append(
Paragraph(f"• {resp}", self.styles["CustomBody"])
)
elements.append(Spacer(1, 0.15 * inch))
# Projects
if data.get("projects"):
elements.append(Paragraph("PROJECTS", self.styles["SectionHeading"]))
for proj in data["projects"]:
# Project name
proj_name = proj.get("name", "Project")
elements.append(
Paragraph(f"{proj_name}", self.styles["CustomBody"])
)
# Description
if proj.get("description"):
elements.append(
Paragraph(proj["description"], self.styles["CustomBody"])
)
# Technologies
if proj.get("technologies"):
tech_text = (
f"Technologies: {', '.join(proj['technologies'])}"
)
elements.append(Paragraph(tech_text, self.styles["CustomBody"]))
# URL
if proj.get("url"):
url_text = f'{proj["url"]}'
elements.append(Paragraph(url_text, self.styles["CustomBody"]))
elements.append(Spacer(1, 0.15 * inch))
# Education
if data.get("education"):
elements.append(Paragraph("EDUCATION", self.styles["SectionHeading"]))
for edu in data["education"]:
degree = edu.get("degree", "Degree")
field = edu.get("field", "Field")
school = edu.get("school", "School")
grad_date = edu.get("graduation_date", "Graduation Date")
edu_text = f"{degree} in {field}"
elements.append(Paragraph(edu_text, self.styles["CustomBody"]))
school_text = f"{school} | {grad_date}"
elements.append(
Paragraph(f"{school_text}", self.styles["CustomBody"])
)
# GPA or honors
if edu.get("gpa"):
elements.append(
Paragraph(f"GPA: {edu['gpa']}", self.styles["CustomBody"])
)
if edu.get("honors"):
elements.append(Paragraph(edu["honors"], self.styles["CustomBody"]))
elements.append(Spacer(1, 0.15 * inch))
# Certifications
if data.get("certifications"):
elements.append(Paragraph("CERTIFICATIONS", self.styles["SectionHeading"]))
for cert in data["certifications"]:
cert_name = cert.get("name", "Certification")
issuer = cert.get("issuer", "Issuer")
cert_date = cert.get("date", "")
cert_text = f"• {cert_name} - {issuer}"
if cert_date:
cert_text += f" ({cert_date})"
elements.append(Paragraph(cert_text, self.styles["CustomBody"]))
# Footer
elements.append(Spacer(1, 0.5 * inch))
elements.append(
HRFlowable(width="100%", thickness=1, color=colors.HexColor("#CCCCCC"))
)
footer_text = f"Generated on {datetime.now().strftime('%B %d, %Y')}"
elements.append(Paragraph(footer_text, self.styles["ContactInfo"]))
# Build PDF
doc.build(elements)
buffer.seek(0)
return buffer
def generate_simple_pdf(self, content: str, title: str = "Document") -> io.BytesIO:
"""
Generate a simple PDF from text content
Args:
content: Text content
title: Document title
Returns:
BytesIO buffer containing the PDF file
"""
buffer = io.BytesIO()
doc = SimpleDocTemplate(
buffer,
pagesize=self.page_size,
rightMargin=inch,
leftMargin=inch,
topMargin=inch,
bottomMargin=inch,
)
elements = []
# Title
elements.append(Paragraph(title, self.styles["CustomTitle"]))
elements.append(Spacer(1, 0.3 * inch))
# Content
paragraphs = content.split("\n\n")
for para in paragraphs:
if para.strip():
elements.append(Paragraph(para.strip(), self.styles["CustomBody"]))
elements.append(Spacer(1, 0.1 * inch))
# Build PDF
doc.build(elements)
buffer.seek(0)
return buffer
# Singleton instance
_pdf_generator = None
def get_pdf_generator() -> PDFGenerator:
"""Get or create PDFGenerator singleton"""
global _pdf_generator
if _pdf_generator is None:
_pdf_generator = PDFGenerator()
return _pdf_generator