Spaces:
Sleeping
Sleeping
| """ | |
| Canonical renderer β takes a `Resume` model and produces a single | |
| canonical visual format DOCX (and via pdf_writer, the matching PDF). | |
| The visual is locked. Every tailored resume looks identical: | |
| - Header: name (20pt bold, centered) + contact (10pt, centered) | |
| - Section headers: 11pt bold indigo, ALL CAPS, thin underline rule | |
| - Role: title (11pt bold) + companyΒ·location (10pt italic gray) + dates (10pt italic gray) | |
| - Bullets: 10.5pt, hanging indent, single line spacing | |
| - Achievements: 3-5 quantified bullets | |
| - Education: degree (bold) + institutionΒ·dates (italic gray) | |
| NO Skills/Competencies section anywhere. NO sub-sections. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import re | |
| from docx import Document | |
| from docx.shared import Pt, RGBColor, Inches | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING | |
| from docx.oxml.ns import qn | |
| from docx.oxml import OxmlElement | |
| from .resume_model import Resume, Role, Education | |
| # Colors used throughout β single source of truth | |
| _NAME_COLOR = RGBColor(0x1A, 0x1A, 0x2E) # near-black navy | |
| _BODY_COLOR = RGBColor(0x1F, 0x29, 0x37) # body text | |
| _META_COLOR = RGBColor(0x55, 0x5B, 0x68) # italic gray | |
| _SECTION_COLOR = RGBColor(0x16, 0x48, 0x9E) # indigo blue | |
| _HR_LINE_COLOR = "16489E" | |
| def render_resume_docx(resume: Resume, filepath: str) -> str: | |
| """ | |
| Render the Resume model to a DOCX at filepath. Returns the filepath. | |
| Visual spec is fixed β every resume looks identical. | |
| """ | |
| doc = Document() | |
| _set_page_margins(doc) | |
| _set_default_paragraph_style(doc) | |
| _write_header(doc, resume) | |
| _write_summary(doc, resume) | |
| _write_experience(doc, resume) | |
| _write_achievements(doc, resume) | |
| _write_education(doc, resume) | |
| os.makedirs(os.path.dirname(filepath), exist_ok=True) | |
| doc.save(filepath) | |
| return filepath | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Document setup | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _set_page_margins(doc: Document) -> None: | |
| for section in doc.sections: | |
| section.top_margin = Inches(0.6) | |
| section.bottom_margin = Inches(0.6) | |
| section.left_margin = Inches(0.7) | |
| section.right_margin = Inches(0.7) | |
| def _set_default_paragraph_style(doc: Document) -> None: | |
| """Set the base style: Calibri 10.5pt, single-line spacing.""" | |
| style = doc.styles["Normal"] | |
| style.font.name = "Calibri" | |
| style.font.size = Pt(10.5) | |
| style.paragraph_format.space_after = Pt(0) | |
| style.paragraph_format.line_spacing = 1.15 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Header (name + contact) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _write_header(doc: Document, resume: Resume) -> None: | |
| name_para = doc.add_paragraph() | |
| name_para.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| name_para.paragraph_format.space_after = Pt(2) | |
| run = name_para.add_run(resume.name.upper()) | |
| run.bold = True | |
| run.font.name = "Calibri" | |
| run.font.size = Pt(20) | |
| run.font.color.rgb = _NAME_COLOR | |
| contact_line = resume.contact.render_line() | |
| if contact_line: | |
| cp = doc.add_paragraph(contact_line) | |
| cp.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| cp.paragraph_format.space_after = Pt(6) | |
| for r in cp.runs: | |
| r.font.name = "Calibri" | |
| r.font.size = Pt(10) | |
| r.font.color.rgb = _META_COLOR | |
| # Thin horizontal rule under the header | |
| rule = doc.add_paragraph() | |
| rule.paragraph_format.space_after = Pt(6) | |
| _add_bottom_border(rule, size="6", color=_HR_LINE_COLOR) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Sections | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _write_summary(doc: Document, resume: Resume) -> None: | |
| if not resume.summary: | |
| return | |
| _add_section_header(doc, "PROFESSIONAL SUMMARY") | |
| p = doc.add_paragraph(resume.summary) | |
| p.paragraph_format.space_after = Pt(6) | |
| p.paragraph_format.line_spacing = 1.25 | |
| for r in p.runs: | |
| r.font.name = "Calibri" | |
| r.font.size = Pt(10.5) | |
| r.font.color.rgb = _BODY_COLOR | |
| def _write_experience(doc: Document, resume: Resume) -> None: | |
| if not resume.roles: | |
| return | |
| _add_section_header(doc, "PROFESSIONAL EXPERIENCE") | |
| for role in resume.roles: | |
| _write_role(doc, role) | |
| def _write_role(doc: Document, role: Role) -> None: | |
| # Role title β bold, 11pt | |
| title_para = doc.add_paragraph() | |
| title_para.paragraph_format.space_before = Pt(8) | |
| title_para.paragraph_format.space_after = Pt(0) | |
| run = title_para.add_run(role.title) | |
| run.bold = True | |
| run.font.name = "Calibri" | |
| run.font.size = Pt(11) | |
| run.font.color.rgb = _NAME_COLOR | |
| # Company Β· Location Β· Dates β italic gray, 10pt | |
| meta_parts = [] | |
| if role.company: | |
| meta_parts.append(role.company) | |
| if role.location: | |
| meta_parts.append(role.location) | |
| if role.dates: | |
| meta_parts.append(role.dates) | |
| meta_text = " Β· ".join(meta_parts) | |
| if meta_text: | |
| meta_para = doc.add_paragraph() | |
| meta_para.paragraph_format.space_after = Pt(3) | |
| run = meta_para.add_run(meta_text) | |
| run.italic = True | |
| run.font.name = "Calibri" | |
| run.font.size = Pt(10) | |
| run.font.color.rgb = _META_COLOR | |
| # Bullets | |
| for bullet in role.bullets: | |
| p = doc.add_paragraph(style="List Bullet") | |
| p.paragraph_format.space_after = Pt(2) | |
| p.paragraph_format.left_indent = Inches(0.2) | |
| run = p.add_run(_clean_bullet_text(bullet)) | |
| run.font.name = "Calibri" | |
| run.font.size = Pt(10.5) | |
| run.font.color.rgb = _BODY_COLOR | |
| def _write_achievements(doc: Document, resume: Resume) -> None: | |
| if not resume.achievements: | |
| return | |
| _add_section_header(doc, "KEY ACHIEVEMENTS") | |
| for ach in resume.achievements: | |
| p = doc.add_paragraph(style="List Bullet") | |
| p.paragraph_format.space_after = Pt(2) | |
| p.paragraph_format.left_indent = Inches(0.2) | |
| run = p.add_run(_clean_bullet_text(ach)) | |
| run.font.name = "Calibri" | |
| run.font.size = Pt(10.5) | |
| run.font.color.rgb = _BODY_COLOR | |
| def _write_education(doc: Document, resume: Resume) -> None: | |
| if not resume.education: | |
| return | |
| _add_section_header(doc, "EDUCATION") | |
| for edu in resume.education: | |
| # Degree β bold 10.5pt | |
| deg_para = doc.add_paragraph() | |
| deg_para.paragraph_format.space_before = Pt(4) | |
| deg_para.paragraph_format.space_after = Pt(0) | |
| run = deg_para.add_run(edu.degree) | |
| run.bold = True | |
| run.font.name = "Calibri" | |
| run.font.size = Pt(10.5) | |
| run.font.color.rgb = _NAME_COLOR | |
| # Institution Β· Dates β italic gray | |
| meta_parts = [] | |
| if edu.institution: | |
| meta_parts.append(edu.institution) | |
| if edu.dates: | |
| meta_parts.append(edu.dates) | |
| meta_text = " Β· ".join(meta_parts) | |
| if meta_text: | |
| ip = doc.add_paragraph() | |
| ip.paragraph_format.space_after = Pt(2) | |
| run = ip.add_run(meta_text) | |
| run.italic = True | |
| run.font.name = "Calibri" | |
| run.font.size = Pt(10) | |
| run.font.color.rgb = _META_COLOR | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Section header helper | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _add_section_header(doc: Document, title: str) -> None: | |
| """ALL-CAPS section header with thin indigo underline.""" | |
| p = doc.add_paragraph() | |
| p.paragraph_format.space_before = Pt(10) | |
| p.paragraph_format.space_after = Pt(4) | |
| run = p.add_run(title.upper()) | |
| run.bold = True | |
| run.font.name = "Calibri" | |
| run.font.size = Pt(11) | |
| run.font.color.rgb = _SECTION_COLOR | |
| _add_bottom_border(p) | |
| def _add_bottom_border(paragraph, size: str = "6", color: str = _HR_LINE_COLOR) -> None: | |
| pPr = paragraph._p.get_or_add_pPr() | |
| pBdr = OxmlElement("w:pBdr") | |
| bottom = OxmlElement("w:bottom") | |
| bottom.set(qn("w:val"), "single") | |
| bottom.set(qn("w:sz"), size) | |
| bottom.set(qn("w:space"), "1") | |
| bottom.set(qn("w:color"), color) | |
| pBdr.append(bottom) | |
| pPr.append(pBdr) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Helpers | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _clean_bullet_text(text: str) -> str: | |
| """Strip leading bullet chars and collapse whitespace.""" | |
| s = re.sub(r"^[β’\-ββ*βͺβ\s]+", "", text) | |
| return re.sub(r"\s+", " ", s).strip() | |