121 lines
3.8 KiB
Python
121 lines
3.8 KiB
Python
# Procyon — one-page PDF resume renderer (fpdf2).
|
|
# Takes a structured resume dict and lays it out on a single letter page.
|
|
|
|
import os
|
|
|
|
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'data', 'resumes')
|
|
|
|
DEJAVU = {
|
|
'': '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',
|
|
'B': '/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf',
|
|
'I': '/usr/share/fonts/truetype/dejavu/DejaVuSans-Oblique.ttf',
|
|
'BI': '/usr/share/fonts/truetype/dejavu/DejaVuSans-BoldOblique.ttf',
|
|
}
|
|
|
|
|
|
def _register_fonts(pdf):
|
|
if all(os.path.exists(p) for p in DEJAVU.values()):
|
|
pdf.add_font('DejaVu', '', DEJAVU[''])
|
|
pdf.add_font('DejaVu', 'B', DEJAVU['B'])
|
|
pdf.add_font('DejaVu', 'I', DEJAVU['I'])
|
|
pdf.add_font('DejaVu', 'BI', DEJAVU['BI'])
|
|
return 'DejaVu'
|
|
return 'Helvetica'
|
|
|
|
|
|
def _sanitize(text):
|
|
if not text:
|
|
return ''
|
|
repl = {'\u2014': '-', '\u2013': '-', '\u2192': '->', '\u2022': '\u00b7',
|
|
'\u2018': "'", '\u2019': "'", '\u201c': '"', '\u201d': '"', '\u2026': '...'}
|
|
for k, v in repl.items():
|
|
text = text.replace(k, v)
|
|
return text.encode('latin-1', 'replace').decode('latin-1')
|
|
|
|
|
|
def render(resume, out_path=None, name='Indiana Holmes'):
|
|
"""resume: dict with header/summary/skills/experience/education. Returns PDF path."""
|
|
from fpdf import FPDF
|
|
|
|
pdf = FPDF(format='letter')
|
|
pdf.set_margins(13, 13, 13)
|
|
pdf.set_auto_page_break(True, margin=11)
|
|
pdf.add_page()
|
|
font = _register_fonts(pdf)
|
|
|
|
def clean(s):
|
|
return s if font == 'DejaVu' else _sanitize(s)
|
|
|
|
def cell(w, h, txt, style='', size=9):
|
|
pdf.set_font(font, style, size)
|
|
pdf.cell(w, h, clean(txt), new_x="LMARGIN", new_y="NEXT")
|
|
|
|
def mc(w, h, txt, style='', size=9, indent=None):
|
|
pdf.set_font(font, style, size)
|
|
if indent is not None:
|
|
pdf.set_x(indent)
|
|
pdf.multi_cell(w, h, clean(txt), new_x="LMARGIN", new_y="NEXT")
|
|
|
|
def colored(r, g, b):
|
|
pdf.set_text_color(r, g, b)
|
|
|
|
# --- Header ---
|
|
cell(0, 8, name, 'B', 17)
|
|
if resume.get('contact'):
|
|
colored(90, 100, 120)
|
|
mc(0, 4.2, resume['contact'], '', 8.5)
|
|
colored(20, 20, 20)
|
|
if resume.get('headline'):
|
|
pdf.ln(0.5)
|
|
colored(0, 90, 120)
|
|
mc(0, 4.6, resume['headline'], 'BI', 9.5)
|
|
colored(20, 20, 20)
|
|
|
|
def section(title):
|
|
pdf.ln(2)
|
|
colored(0, 70, 100)
|
|
cell(0, 5, title.upper(), 'B', 10)
|
|
colored(20, 20, 20)
|
|
pdf.set_draw_color(0, 70, 100)
|
|
pdf.set_line_width(0.3)
|
|
pdf.line(13, pdf.get_y(), 202.9, pdf.get_y())
|
|
pdf.ln(1.2)
|
|
|
|
if resume.get('summary'):
|
|
mc(0, 4.4, resume['summary'], '', 9)
|
|
pdf.ln(1)
|
|
|
|
if resume.get('skills'):
|
|
section('Core Skills')
|
|
mc(0, 4.4, ' \u00b7 '.join(resume['skills']), '', 8.5)
|
|
|
|
if resume.get('experience'):
|
|
section('Experience')
|
|
for e in resume['experience']:
|
|
cell(0, 4.6, e.get('title', ''), 'B', 9.5)
|
|
meta = ' \u2014 '.join(x for x in [e.get('company', ''), e.get('dates', '')] if x)
|
|
colored(80, 90, 110)
|
|
mc(0, 4.2, meta, 'I', 8.5)
|
|
colored(20, 20, 20)
|
|
for b in e.get('bullets', []):
|
|
mc(0, 4.2, '\u00b7 ' + b, '', 8.5, indent=16)
|
|
pdf.ln(0.8)
|
|
|
|
if resume.get('projects'):
|
|
section('Key Projects')
|
|
for p in resume['projects']:
|
|
cell(0, 4.6, p.get('title', ''), 'B', 9)
|
|
for b in p.get('bullets', []):
|
|
mc(0, 4.2, '\u00b7 ' + b, '', 8.5, indent=16)
|
|
pdf.ln(0.8)
|
|
|
|
if resume.get('education'):
|
|
section('Education')
|
|
mc(0, 4.4, resume['education'], '', 9)
|
|
|
|
os.makedirs(DATA_DIR, exist_ok=True)
|
|
if not out_path:
|
|
out_path = os.path.join(DATA_DIR, 'resume.pdf')
|
|
pdf.output(out_path)
|
|
return out_path
|