Snapshot: full project state
This commit is contained in:
120
resume_pdf.py
Normal file
120
resume_pdf.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user