Spaces:
Sleeping
Sleeping
| """ | |
| DOCX → PDF conversion. | |
| Strategy: | |
| 1. Windows + MS Word installed → docx2pdf (perfect fidelity) | |
| 2. Anywhere else (HF Spaces, Linux) → reportlab re-render from docx text | |
| """ | |
| import os | |
| import re | |
| import logging | |
| log = logging.getLogger("pdf_writer") | |
| _WORD_AVAILABLE = None # cached after first check | |
| def _word_available() -> bool: | |
| global _WORD_AVAILABLE | |
| if _WORD_AVAILABLE is not None: | |
| return _WORD_AVAILABLE | |
| if os.name != "nt": | |
| _WORD_AVAILABLE = False | |
| return False | |
| try: | |
| import win32com.client # noqa: F401 (docx2pdf dependency) | |
| import pythoncom | |
| pythoncom.CoInitialize() | |
| try: | |
| import win32com.client as wc | |
| w = wc.Dispatch("Word.Application") | |
| w.Quit() | |
| _WORD_AVAILABLE = True | |
| finally: | |
| pythoncom.CoUninitialize() | |
| except Exception: | |
| _WORD_AVAILABLE = False | |
| return _WORD_AVAILABLE | |
| def convert_folder(folder: str) -> dict: | |
| """ | |
| Convert every .docx in folder to .pdf. | |
| Returns {abs_docx_path: pdf_path_or_empty}. | |
| Uses ONE Word session for the whole folder when Word is available | |
| (much faster + stable than per-file), else reportlab per file. | |
| """ | |
| from pathlib import Path | |
| docx_files = sorted(Path(folder).glob("*.docx")) | |
| result = {} | |
| if not docx_files: | |
| return result | |
| # Only convert files that don't already have a PDF | |
| to_convert = [f for f in docx_files if not f.with_suffix(".pdf").exists()] | |
| if to_convert and _word_available(): | |
| try: | |
| import pythoncom | |
| pythoncom.CoInitialize() | |
| try: | |
| from docx2pdf import convert | |
| if len(to_convert) == len(docx_files): | |
| convert(folder) # whole folder in one Word session | |
| else: | |
| for f in to_convert: | |
| convert(str(f), str(f.with_suffix(".pdf"))) | |
| finally: | |
| pythoncom.CoUninitialize() | |
| except Exception as e: | |
| log.warning(f"Batch docx2pdf failed: {e} — falling back to reportlab per file") | |
| for f in docx_files: | |
| pdf = str(f.with_suffix(".pdf")) | |
| if not os.path.exists(pdf): | |
| pdf = _reportlab_render(str(f), pdf) | |
| result[os.path.abspath(str(f))] = pdf if pdf and os.path.exists(pdf) else "" | |
| return result | |
| def docx_to_pdf(docx_path: str) -> str: | |
| """ | |
| Convert a DOCX resume to PDF next to it. Returns the PDF path ('' on failure). | |
| Thread-safe: each call initializes its own COM context on Windows. | |
| """ | |
| if not docx_path or not os.path.exists(docx_path): | |
| return "" | |
| pdf_path = os.path.splitext(docx_path)[0] + ".pdf" | |
| if _word_available(): | |
| try: | |
| import pythoncom | |
| pythoncom.CoInitialize() | |
| try: | |
| from docx2pdf import convert | |
| convert(docx_path, pdf_path) | |
| finally: | |
| pythoncom.CoUninitialize() | |
| if os.path.exists(pdf_path): | |
| return pdf_path | |
| except Exception as e: | |
| log.warning(f"docx2pdf failed for {os.path.basename(docx_path)}: {e} — falling back to reportlab") | |
| return _reportlab_render(docx_path, pdf_path) | |
| def _reportlab_render(docx_path: str, pdf_path: str) -> str: | |
| """Re-render the DOCX content as a clean styled PDF using reportlab.""" | |
| try: | |
| from docx import Document | |
| from reportlab.lib.pagesizes import A4 | |
| from reportlab.lib.units import inch | |
| from reportlab.lib.colors import HexColor | |
| from reportlab.lib.styles import ParagraphStyle | |
| from reportlab.lib.enums import TA_CENTER | |
| from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer | |
| doc = Document(docx_path) | |
| paragraphs = [(p.text, p.style.name if p.style else "") for p in doc.paragraphs] | |
| styles = { | |
| "name": ParagraphStyle("name", fontName="Helvetica-Bold", fontSize=18, | |
| textColor=HexColor("#1A1A2E"), alignment=TA_CENTER, spaceAfter=4), | |
| "contact": ParagraphStyle("contact", fontName="Helvetica", fontSize=9, | |
| textColor=HexColor("#444444"), alignment=TA_CENTER, spaceAfter=6), | |
| "header": ParagraphStyle("header", fontName="Helvetica-Bold", fontSize=11, | |
| textColor=HexColor("#16489E"), spaceBefore=10, spaceAfter=4), | |
| "bullet": ParagraphStyle("bullet", fontName="Helvetica", fontSize=10, | |
| leftIndent=14, bulletIndent=4, spaceAfter=2, leading=13), | |
| "body": ParagraphStyle("body", fontName="Helvetica", fontSize=10, | |
| spaceAfter=3, leading=13), | |
| } | |
| pdf = SimpleDocTemplate(pdf_path, pagesize=A4, | |
| topMargin=0.6 * inch, bottomMargin=0.6 * inch, | |
| leftMargin=0.7 * inch, rightMargin=0.7 * inch) | |
| flow = [] | |
| first_text_seen = False | |
| def esc(t): | |
| return t.replace("&", "&").replace("<", "<").replace(">", ">") | |
| for text, style_name in paragraphs: | |
| text = text.strip() | |
| if not text: | |
| continue | |
| if set(text) <= {"─", "-", "—", "_"}: | |
| continue # horizontal rules | |
| if not first_text_seen: | |
| flow.append(Paragraph(esc(text), styles["name"])) | |
| first_text_seen = True | |
| elif "|" in text and ("@" in text or re.search(r"\+?\d{6,}", text)): | |
| flow.append(Paragraph(esc(text), styles["contact"])) | |
| elif re.match(r"^[A-Z][A-Z\s&/]+$", text) and len(text) > 3: | |
| flow.append(Paragraph(esc(text), styles["header"])) | |
| elif style_name == "List Bullet" or text.startswith(("•", "-", "–", "▪")): | |
| clean = text.lstrip("•-–▪* ").strip() | |
| flow.append(Paragraph(f"• {esc(clean)}", styles["bullet"])) | |
| else: | |
| flow.append(Paragraph(esc(text), styles["body"])) | |
| if not flow: | |
| return "" | |
| flow.append(Spacer(1, 6)) | |
| pdf.build(flow) | |
| return pdf_path if os.path.exists(pdf_path) else "" | |
| except Exception as e: | |
| log.error(f"reportlab PDF render failed for {os.path.basename(docx_path)}: {e}") | |
| return "" | |