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()