Spaces:
Running
Running
File size: 9,609 Bytes
79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c c2f9ec8 79b5c9c |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 |
import logging
import os
import re
from datetime import datetime
from dateutil.parser import parse as date_parse
from docx import Document
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT, WD_TAB_ALIGNMENT
from docx.shared import Inches, Pt
logger = logging.getLogger(__name__)
def fmt_range(raw: str) -> str:
"""Formats a date range string nicely."""
if not raw:
return ""
parts = [p.strip() for p in re.split(r"\s*[β-]\s*", raw)]
formatted_parts = []
for part in parts:
if part.lower() == "present":
formatted_parts.append("Present")
else:
try:
date_obj = date_parse(part, fuzzy=True, default=datetime(1900, 1, 1))
if date_obj.year == 1900:
formatted_parts.append(part)
else:
formatted_parts.append(date_obj.strftime("%B %Y"))
except (ValueError, TypeError):
formatted_parts.append(part)
return " β ".join(formatted_parts)
def add_section_heading(doc, text):
"""Adds a centered section heading."""
p = doc.add_paragraph()
run = p.add_run(text.upper())
run.bold = True
font = run.font
font.size = Pt(12)
font.name = 'Arial'
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
p.paragraph_format.space_after = Pt(6)
def build_resume_from_data(tmpl: str, sections: dict, remove_blank_pages_enabled: bool = True) -> Document:
"""
Builds a formatted resume from structured data, inserting header/footer images and logging the process.
"""
logger.info("BUILDER: Starting image-based resume build process.")
try:
# 1. Create a new blank document, ignoring the template file
doc = Document()
logger.info("BUILDER: Successfully created a new blank document.")
# Get section and enable different first page header/footer
section = doc.sections[0]
section.different_first_page = True
# Move header and footer to the very edge of the page
section.header_distance = Pt(0)
section.footer_distance = Pt(0)
logger.info("BUILDER: Set header/footer distance to 0 to remove whitespace.")
# 2. Define image paths relative to the project root
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(script_dir)
header_path = os.path.join(project_root, 'header.png')
footer_path = os.path.join(project_root, 'footer.png')
logger.info(f"BUILDER: Attempting to use header image from: {header_path}")
logger.info(f"BUILDER: Attempting to use footer image from: {footer_path}")
if not os.path.exists(header_path):
logger.error(f"BUILDER FATAL: Header image not found at '{header_path}'. Cannot proceed.")
return doc # Return empty doc
if not os.path.exists(footer_path):
logger.error(f"BUILDER FATAL: Footer image not found at '{footer_path}'. Cannot proceed.")
return doc # Return empty doc
# 3. Setup Headers
candidate_name = sections.get("Name", "Candidate Name Not Found")
experiences = sections.get("StructuredExperiences", [])
job_title = experiences[0].get("title", "") if experiences else ""
# -- First Page Header (Image + Name + Title) --
first_page_header = section.first_page_header
first_page_header.is_linked_to_previous = False
# Safely get or create a paragraph for the image
p_header_img_first = first_page_header.paragraphs[0] if first_page_header.paragraphs else first_page_header.add_paragraph()
p_header_img_first.clear()
p_header_img_first.paragraph_format.space_before = Pt(0)
p_header_img_first.paragraph_format.space_after = Pt(0)
p_header_img_first.paragraph_format.left_indent = -section.left_margin
p_header_img_first.add_run().add_picture(header_path, width=section.page_width)
logger.info("BUILDER: Inserted header.png into FIRST PAGE header.")
# Add Name
p_name = first_page_header.add_paragraph()
run_name = p_name.add_run(candidate_name.upper())
run_name.font.name = 'Arial'
run_name.font.size = Pt(14)
run_name.bold = True
p_name.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
p_name.paragraph_format.space_before = Pt(6)
p_name.paragraph_format.space_after = Pt(0)
logger.info(f"BUILDER: Added candidate name '{candidate_name}' to FIRST PAGE header.")
# Add Job Title
if job_title:
p_title = first_page_header.add_paragraph()
run_title = p_title.add_run(job_title)
run_title.font.name = 'Arial'
run_title.font.size = Pt(11)
p_title.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
p_title.paragraph_format.space_before = Pt(0)
logger.info(f"BUILDER: Added job title '{job_title}' to FIRST PAGE header.")
# -- Primary Header for subsequent pages (Image Only) --
primary_header = section.header
primary_header.is_linked_to_previous = False
# Safely get or create a paragraph for the image
p_header_img_primary = primary_header.paragraphs[0] if primary_header.paragraphs else primary_header.add_paragraph()
p_header_img_primary.clear()
p_header_img_primary.paragraph_format.space_before = Pt(0)
p_header_img_primary.paragraph_format.space_after = Pt(0)
p_header_img_primary.paragraph_format.left_indent = -section.left_margin
p_header_img_primary.add_run().add_picture(header_path, width=section.page_width)
logger.info("BUILDER: Inserted header.png into PRIMARY header for subsequent pages.")
# 4. Insert Footer Image (same for all pages)
footer = section.footer
footer.is_linked_to_previous = False
# Safely get or create a paragraph for the image
p_footer_img = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
p_footer_img.clear()
p_footer_img.paragraph_format.space_before = Pt(0)
p_footer_img.paragraph_format.space_after = Pt(0)
p_footer_img.paragraph_format.left_indent = -section.left_margin
p_footer_img.add_run().add_picture(footer_path, width=section.page_width)
# Link the first page footer to the primary footer so we only define it once.
section.first_page_footer.is_linked_to_previous = True
logger.info("BUILDER: Inserted footer.png and configured for all pages.")
# 5. Build Resume Body
logger.info("BUILDER: Proceeding to add structured resume content to document body.")
# --- Professional Summary ---
if sections.get("Summary"):
add_section_heading(doc, "Professional Summary")
doc.add_paragraph(sections["Summary"]).paragraph_format.space_after = Pt(12)
# --- Skills ---
if sections.get("Skills"):
add_section_heading(doc, "Skills")
skills_text = ", ".join(sections["Skills"])
p = doc.add_paragraph(skills_text)
p.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
p.paragraph_format.space_after = Pt(12)
# --- Professional Experience ---
if experiences:
add_section_heading(doc, "Professional Experience")
for exp in experiences:
if not isinstance(exp, dict):
continue
p = doc.add_paragraph()
p.add_run(exp.get("title", "N/A")).bold = True
p.add_run(" | ").bold = True
p.add_run(exp.get("company", "N/A")).italic = True
p.add_run(f'\t{fmt_range(exp.get("date_range", ""))}')
tab_stops = p.paragraph_format.tab_stops
tab_stops.add_tab_stop(Inches(6.5), WD_TAB_ALIGNMENT.RIGHT)
responsibilities = exp.get("responsibilities", [])
if responsibilities and isinstance(responsibilities, list):
for resp in responsibilities:
if resp.strip():
try:
p_resp = doc.add_paragraph(resp, style='List Bullet')
except KeyError:
p_resp = doc.add_paragraph(f"β’ {resp}")
p_resp.paragraph_format.left_indent = Inches(0.25)
p_resp.paragraph_format.space_before = Pt(0)
p_resp.paragraph_format.space_after = Pt(3)
doc.add_paragraph().paragraph_format.space_after = Pt(6)
# --- Education ---
if sections.get("Education"):
add_section_heading(doc, "Education")
for edu in sections.get("Education", []):
if edu.strip():
try:
p_edu = doc.add_paragraph(edu, style='List Bullet')
except KeyError:
p_edu = doc.add_paragraph(f"β’ {edu}")
p_edu.paragraph_format.left_indent = Inches(0.25)
logger.info("BUILDER: Resume build process completed successfully.")
return doc
except Exception:
logger.error("BUILDER: An unexpected error occurred during resume generation.", exc_info=True)
return Document()
|