mirror of
https://github.com/hanzoskill/resume-optimizer.git
synced 2026-10-30 21:04:48 +00:00
488 lines
17 KiB
Python
488 lines
17 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Resume PDF Generator
|
|
Generates professional, ATS-friendly PDF resumes from JSON input.
|
|
"""
|
|
|
|
import json
|
|
import argparse
|
|
from pathlib import Path
|
|
|
|
try:
|
|
from reportlab.lib.pagesizes import letter
|
|
from reportlab.lib.units import inch
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib.enums import TA_LEFT, TA_CENTER
|
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
|
|
from reportlab.lib.colors import HexColor
|
|
except ImportError:
|
|
print("Installing reportlab...")
|
|
import subprocess
|
|
subprocess.check_call(['pip', 'install', 'reportlab', '--break-system-packages', '-q'])
|
|
from reportlab.lib.pagesizes import letter
|
|
from reportlab.lib.units import inch
|
|
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
|
from reportlab.lib.enums import TA_LEFT, TA_CENTER
|
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, HRFlowable
|
|
from reportlab.lib.colors import HexColor
|
|
|
|
|
|
class ResumeGenerator:
|
|
"""Generate professional PDF resumes from structured data."""
|
|
|
|
# Colors
|
|
DARK_GRAY = HexColor('#333333')
|
|
MEDIUM_GRAY = HexColor('#666666')
|
|
LIGHT_GRAY = HexColor('#999999')
|
|
DIVIDER_COLOR = HexColor('#CCCCCC')
|
|
|
|
def __init__(self, output_path: str, margin: float = 0.75):
|
|
self.output_path = output_path
|
|
self.margin = margin * inch
|
|
self.styles = self._create_styles()
|
|
self.story = []
|
|
|
|
def _create_styles(self):
|
|
"""Create custom paragraph styles for resume sections."""
|
|
styles = getSampleStyleSheet()
|
|
|
|
# Name style (large, bold, centered)
|
|
styles.add(ParagraphStyle(
|
|
'Name',
|
|
parent=styles['Normal'],
|
|
fontName='Helvetica-Bold',
|
|
fontSize=18,
|
|
leading=22,
|
|
alignment=TA_CENTER,
|
|
textColor=self.DARK_GRAY,
|
|
spaceAfter=4
|
|
))
|
|
|
|
# Contact info style (centered, smaller)
|
|
styles.add(ParagraphStyle(
|
|
'Contact',
|
|
parent=styles['Normal'],
|
|
fontName='Helvetica',
|
|
fontSize=10,
|
|
leading=12,
|
|
alignment=TA_CENTER,
|
|
textColor=self.MEDIUM_GRAY,
|
|
spaceAfter=12
|
|
))
|
|
|
|
# Section header style
|
|
styles.add(ParagraphStyle(
|
|
'SectionHeader',
|
|
parent=styles['Normal'],
|
|
fontName='Helvetica-Bold',
|
|
fontSize=11,
|
|
leading=14,
|
|
textColor=self.DARK_GRAY,
|
|
spaceBefore=12,
|
|
spaceAfter=6
|
|
))
|
|
|
|
# Subsection header (job title, company)
|
|
styles.add(ParagraphStyle(
|
|
'SubsectionHeader',
|
|
parent=styles['Normal'],
|
|
fontName='Helvetica-Bold',
|
|
fontSize=10,
|
|
leading=13,
|
|
textColor=self.DARK_GRAY,
|
|
spaceBefore=8,
|
|
spaceAfter=2
|
|
))
|
|
|
|
# Job details (company, location, dates)
|
|
styles.add(ParagraphStyle(
|
|
'JobDetails',
|
|
parent=styles['Normal'],
|
|
fontName='Helvetica',
|
|
fontSize=10,
|
|
leading=12,
|
|
textColor=self.MEDIUM_GRAY,
|
|
spaceAfter=4
|
|
))
|
|
|
|
# Body text style (custom)
|
|
styles.add(ParagraphStyle(
|
|
'ResumeBody',
|
|
parent=styles['Normal'],
|
|
fontName='Helvetica',
|
|
fontSize=10,
|
|
leading=13,
|
|
textColor=self.DARK_GRAY,
|
|
spaceAfter=6
|
|
))
|
|
|
|
# Bullet point style
|
|
styles.add(ParagraphStyle(
|
|
'ResumeBullet',
|
|
parent=styles['Normal'],
|
|
fontName='Helvetica',
|
|
fontSize=10,
|
|
leading=13,
|
|
textColor=self.DARK_GRAY,
|
|
leftIndent=12,
|
|
spaceAfter=3,
|
|
bulletIndent=0
|
|
))
|
|
|
|
# Skills category style
|
|
styles.add(ParagraphStyle(
|
|
'SkillCategory',
|
|
parent=styles['Normal'],
|
|
fontName='Helvetica-Bold',
|
|
fontSize=10,
|
|
leading=13,
|
|
textColor=self.DARK_GRAY,
|
|
spaceAfter=2
|
|
))
|
|
|
|
# Skills list style
|
|
styles.add(ParagraphStyle(
|
|
'SkillsList',
|
|
parent=styles['Normal'],
|
|
fontName='Helvetica',
|
|
fontSize=10,
|
|
leading=13,
|
|
textColor=self.DARK_GRAY,
|
|
spaceAfter=4
|
|
))
|
|
|
|
return styles
|
|
|
|
def add_divider(self):
|
|
"""Add a horizontal divider line."""
|
|
self.story.append(Spacer(1, 4))
|
|
self.story.append(HRFlowable(
|
|
width="100%",
|
|
thickness=0.5,
|
|
color=self.DIVIDER_COLOR,
|
|
spaceBefore=2,
|
|
spaceAfter=6
|
|
))
|
|
|
|
def add_contact(self, contact: dict):
|
|
"""Add contact information section."""
|
|
name = contact.get('name', '')
|
|
self.story.append(Paragraph(name, self.styles['Name']))
|
|
|
|
# Build contact line
|
|
contact_parts = []
|
|
if contact.get('location'):
|
|
contact_parts.append(contact['location'])
|
|
if contact.get('phone'):
|
|
contact_parts.append(contact['phone'])
|
|
if contact.get('email'):
|
|
contact_parts.append(contact['email'])
|
|
if contact.get('linkedin'):
|
|
contact_parts.append(contact['linkedin'])
|
|
if contact.get('website'):
|
|
contact_parts.append(contact['website'])
|
|
|
|
contact_line = ' | '.join(contact_parts)
|
|
self.story.append(Paragraph(contact_line, self.styles['Contact']))
|
|
|
|
def add_summary(self, summary: str):
|
|
"""Add professional summary section."""
|
|
self.story.append(Paragraph('PROFESSIONAL SUMMARY', self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
self.story.append(Paragraph(summary, self.styles['ResumeBody']))
|
|
|
|
def add_skills(self, skills: list, header: str = 'SKILLS'):
|
|
"""Add skills section with categories."""
|
|
self.story.append(Paragraph(header, self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
|
|
for skill_group in skills:
|
|
category = skill_group.get('category', '')
|
|
items = skill_group.get('items', [])
|
|
skills_text = f"<b>{category}:</b> {', '.join(items)}"
|
|
self.story.append(Paragraph(skills_text, self.styles['SkillsList']))
|
|
|
|
def add_experience(self, experience: list):
|
|
"""Add work experience section."""
|
|
self.story.append(Paragraph('PROFESSIONAL EXPERIENCE', self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
|
|
for job in experience:
|
|
# Job title
|
|
title = job.get('title', '')
|
|
self.story.append(Paragraph(f"<b>{title}</b>", self.styles['SubsectionHeader']))
|
|
|
|
# Company, location, dates
|
|
company = job.get('company', '')
|
|
location = job.get('location', '')
|
|
dates = job.get('dates', '')
|
|
details_line = f"{company}, {location} | {dates}"
|
|
self.story.append(Paragraph(details_line, self.styles['JobDetails']))
|
|
|
|
# Bullets
|
|
for bullet in job.get('bullets', []):
|
|
bullet_text = f"• {bullet}"
|
|
self.story.append(Paragraph(bullet_text, self.styles['ResumeBullet']))
|
|
|
|
# Key skills if present (for combination format)
|
|
if job.get('key_skills'):
|
|
skills_text = f"<i>Key Skills: {', '.join(job['key_skills'])}</i>"
|
|
self.story.append(Paragraph(skills_text, self.styles['JobDetails']))
|
|
|
|
def add_education(self, education: list):
|
|
"""Add education section."""
|
|
self.story.append(Paragraph('EDUCATION', self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
|
|
for edu in education:
|
|
degree = edu.get('degree', '')
|
|
major = edu.get('major', '')
|
|
title = f"{degree}, {major}" if major else degree
|
|
self.story.append(Paragraph(f"<b>{title}</b>", self.styles['SubsectionHeader']))
|
|
|
|
school = edu.get('school', '')
|
|
location = edu.get('location', '')
|
|
date = edu.get('date', '')
|
|
details = f"{school}, {location} | {date}"
|
|
self.story.append(Paragraph(details, self.styles['JobDetails']))
|
|
|
|
# Additional details (GPA, honors, etc.)
|
|
for detail in edu.get('details', []):
|
|
self.story.append(Paragraph(f"• {detail}", self.styles['ResumeBullet']))
|
|
|
|
def add_certifications(self, certifications: list):
|
|
"""Add certifications section."""
|
|
if not certifications:
|
|
return
|
|
|
|
self.story.append(Paragraph('CERTIFICATIONS', self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
|
|
for cert in certifications:
|
|
name = cert.get('name', '')
|
|
issuer = cert.get('issuer', '')
|
|
date = cert.get('date', '')
|
|
cert_line = f"<b>{name}</b> | {issuer} | {date}"
|
|
self.story.append(Paragraph(cert_line, self.styles['ResumeBody']))
|
|
|
|
def add_projects(self, projects: list):
|
|
"""Add projects section."""
|
|
if not projects:
|
|
return
|
|
|
|
self.story.append(Paragraph('PROJECTS', self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
|
|
for project in projects:
|
|
name = project.get('name', '')
|
|
tech = project.get('technologies', '')
|
|
self.story.append(Paragraph(f"<b>{name}</b>", self.styles['SubsectionHeader']))
|
|
|
|
if tech:
|
|
self.story.append(Paragraph(f"<i>{tech}</i>", self.styles['JobDetails']))
|
|
|
|
desc = project.get('description', '')
|
|
if desc:
|
|
self.story.append(Paragraph(f"• {desc}", self.styles['ResumeBullet']))
|
|
|
|
link = project.get('link', '')
|
|
if link:
|
|
self.story.append(Paragraph(f"Link: {link}", self.styles['JobDetails']))
|
|
|
|
def add_key_achievements(self, achievements: list):
|
|
"""Add key achievements section (for combination format)."""
|
|
if not achievements:
|
|
return
|
|
|
|
self.story.append(Paragraph('KEY ACHIEVEMENTS', self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
|
|
for achievement in achievements:
|
|
title = achievement.get('title', '')
|
|
desc = achievement.get('description', '')
|
|
text = f"<b>{title}:</b> {desc}"
|
|
self.story.append(Paragraph(f"• {text}", self.styles['ResumeBullet']))
|
|
|
|
def add_core_competencies(self, competencies: list):
|
|
"""Add core competencies section (for functional format)."""
|
|
if not competencies:
|
|
return
|
|
|
|
self.story.append(Paragraph('CORE COMPETENCIES', self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
|
|
for comp in competencies:
|
|
category = comp.get('category', '')
|
|
items = comp.get('items', [])
|
|
|
|
self.story.append(Paragraph(f"<b>{category}</b>", self.styles['SkillCategory']))
|
|
for item in items:
|
|
self.story.append(Paragraph(f"• {item}", self.styles['ResumeBullet']))
|
|
|
|
def add_relevant_experience(self, relevant_exp: list):
|
|
"""Add relevant experience section (for functional format)."""
|
|
if not relevant_exp:
|
|
return
|
|
|
|
self.story.append(Paragraph('RELEVANT EXPERIENCE', self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
|
|
for exp in relevant_exp:
|
|
skill_area = exp.get('skill_area', '')
|
|
self.story.append(Paragraph(f"<b>{skill_area}</b>", self.styles['SubsectionHeader']))
|
|
|
|
for achievement in exp.get('achievements', []):
|
|
title = achievement.get('title', '')
|
|
context = achievement.get('context', '')
|
|
|
|
self.story.append(Paragraph(f"<b>{title}</b>", self.styles['ResumeBody']))
|
|
if context:
|
|
self.story.append(Paragraph(f"<i>{context}</i>", self.styles['JobDetails']))
|
|
|
|
for bullet in achievement.get('bullets', []):
|
|
self.story.append(Paragraph(f"• {bullet}", self.styles['ResumeBullet']))
|
|
|
|
def add_employment_history(self, history: list):
|
|
"""Add brief employment history (for functional format)."""
|
|
if not history:
|
|
return
|
|
|
|
self.story.append(Paragraph('EMPLOYMENT HISTORY', self.styles['SectionHeader']))
|
|
self.add_divider()
|
|
|
|
for job in history:
|
|
title = job.get('title', '')
|
|
company = job.get('company', '')
|
|
location = job.get('location', '')
|
|
dates = job.get('dates', '')
|
|
line = f"<b>{title}</b> | {company}, {location} | {dates}"
|
|
self.story.append(Paragraph(line, self.styles['ResumeBody']))
|
|
|
|
def generate_chronological(self, data: dict):
|
|
"""Generate chronological format resume."""
|
|
self.add_contact(data.get('contact', {}))
|
|
|
|
if data.get('summary'):
|
|
self.add_summary(data['summary'])
|
|
|
|
if data.get('experience'):
|
|
self.add_experience(data['experience'])
|
|
|
|
if data.get('education'):
|
|
self.add_education(data['education'])
|
|
|
|
if data.get('skills'):
|
|
self.add_skills(data['skills'])
|
|
|
|
if data.get('certifications'):
|
|
self.add_certifications(data['certifications'])
|
|
|
|
if data.get('projects'):
|
|
self.add_projects(data['projects'])
|
|
|
|
def generate_functional(self, data: dict):
|
|
"""Generate functional format resume."""
|
|
self.add_contact(data.get('contact', {}))
|
|
|
|
if data.get('summary'):
|
|
self.add_summary(data['summary'])
|
|
|
|
if data.get('core_competencies'):
|
|
self.add_core_competencies(data['core_competencies'])
|
|
|
|
if data.get('relevant_experience'):
|
|
self.add_relevant_experience(data['relevant_experience'])
|
|
|
|
if data.get('employment_history'):
|
|
self.add_employment_history(data['employment_history'])
|
|
|
|
if data.get('skills'):
|
|
self.add_skills(data['skills'], 'TECHNICAL SKILLS')
|
|
|
|
if data.get('education'):
|
|
self.add_education(data['education'])
|
|
|
|
if data.get('certifications'):
|
|
self.add_certifications(data['certifications'])
|
|
|
|
if data.get('projects'):
|
|
self.add_projects(data['projects'])
|
|
|
|
def generate_combination(self, data: dict):
|
|
"""Generate combination format resume."""
|
|
self.add_contact(data.get('contact', {}))
|
|
|
|
if data.get('summary'):
|
|
self.add_summary(data['summary'])
|
|
|
|
if data.get('skills'):
|
|
self.add_skills(data['skills'], 'CORE COMPETENCIES & SKILLS')
|
|
|
|
if data.get('key_achievements'):
|
|
self.add_key_achievements(data['key_achievements'])
|
|
|
|
if data.get('experience'):
|
|
self.add_experience(data['experience'])
|
|
|
|
if data.get('education'):
|
|
self.add_education(data['education'])
|
|
|
|
if data.get('certifications'):
|
|
self.add_certifications(data['certifications'])
|
|
|
|
if data.get('projects'):
|
|
self.add_projects(data['projects'])
|
|
|
|
def generate(self, data: dict):
|
|
"""Generate resume PDF based on format specified in data."""
|
|
format_type = data.get('format', 'chronological').lower()
|
|
|
|
# Create document
|
|
doc = SimpleDocTemplate(
|
|
self.output_path,
|
|
pagesize=letter,
|
|
leftMargin=self.margin,
|
|
rightMargin=self.margin,
|
|
topMargin=self.margin,
|
|
bottomMargin=self.margin
|
|
)
|
|
|
|
# Generate based on format
|
|
if format_type == 'functional':
|
|
self.generate_functional(data)
|
|
elif format_type == 'combination':
|
|
self.generate_combination(data)
|
|
else: # default to chronological
|
|
self.generate_chronological(data)
|
|
|
|
# Build PDF
|
|
doc.build(self.story)
|
|
print(f"✅ Resume generated: {self.output_path}")
|
|
return self.output_path
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description='Generate professional PDF resumes')
|
|
parser.add_argument('--input', '-i', required=True, help='Input JSON file with resume data')
|
|
parser.add_argument('--output', '-o', required=True, help='Output PDF file path')
|
|
parser.add_argument('--format', '-f', choices=['chronological', 'functional', 'combination'],
|
|
default='chronological', help='Resume format (can also be specified in JSON)')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Load resume data
|
|
with open(args.input, 'r') as f:
|
|
data = json.load(f)
|
|
|
|
# Override format if specified in args
|
|
if args.format:
|
|
data['format'] = args.format
|
|
|
|
# Generate resume
|
|
generator = ResumeGenerator(args.output)
|
|
generator.generate(data)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|