# 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