Spaces:
Sleeping
Sleeping
| 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() | |