"""Post-generation PDF parsing validation. Success is never declared on the visual PDF alone. After a résumé PDF is produced we re-extract its text with a real parser and verify the content is present, ordered, and parseable — the same way an ATS would read it — and that no hidden/injected keyword layer was smuggled in. """ from __future__ import annotations import re from typing import Dict, List, Optional def _extract_pdf_text(pdf_path: str) -> Optional[str]: """Best-effort text extraction. Tries pdfplumber, then pymupdf. None on failure.""" try: import pdfplumber parts = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: parts.append(page.extract_text() or "") text = "\n".join(parts).strip() if text: return text except Exception: pass try: import fitz # pymupdf doc = fitz.open(pdf_path) text = "\n".join(p.get_text() for p in doc).strip() doc.close() return text or None except Exception: return None def validate_pdf( pdf_path: str, *, expected_sections: Optional[List[str]] = None, expected_contact: Optional[List[str]] = None, forbidden_markers: Optional[List[str]] = None, ) -> Dict: """Parse the generated PDF and verify readability/integrity. Returns a diagnostics dict (stored alongside the output). `ok` is True only when the parser recovered text, expected sections appear in order, contact details are present, and no injected/hidden marker leaked into the text. """ result: Dict = { "ok": False, "parser_recovered_text": False, "char_count": 0, "sections_found": [], "sections_missing": [], "sections_in_order": None, "contact_present": None, "forbidden_markers_found": [], "warnings": [], } text = _extract_pdf_text(pdf_path) if not text: result["warnings"].append("parser recovered no text (image-only or corrupt)") return result result["parser_recovered_text"] = True result["char_count"] = len(text) low = text.lower() # Section presence + order. Match a HEADING line (the section word dominating # its own line), not any prose substring — "communication skills" in a bullet # must not count as the SKILLS heading. expected_sections = expected_sections or ["experience", "education", "skills"] def _heading_pos(sec: str) -> int: for m in re.finditer(r"(?im)^[^\S\n]*([A-Za-z &/]{3,40})[^\S\n]*$", text): line = m.group(1).strip().lower() if line == sec.lower() or line.startswith(sec.lower() + " ") \ or line.rstrip("s") == sec.lower().rstrip("s"): return m.start() return -1 positions = [] for sec in expected_sections: idx = _heading_pos(sec) if idx >= 0: result["sections_found"].append(sec) positions.append((sec, idx)) else: result["sections_missing"].append(sec) ordered_positions = [p for _, p in positions] result["sections_in_order"] = (ordered_positions == sorted(ordered_positions) if len(ordered_positions) > 1 else True) # Contact readability (any expected token present). if expected_contact: result["contact_present"] = any( re.sub(r"\s+", "", c.lower()) in re.sub(r"\s+", "", low) for c in expected_contact if c ) # No injected/hidden marker text (our own ATS comment tags or a hidden layer). markers = forbidden_markers or ["% ats-item", "% ats-skills-other", "ats-inject", "core focus areas include"] for m in markers: if m.lower() in low: result["forbidden_markers_found"].append(m) result["ok"] = ( result["parser_recovered_text"] and not result["sections_missing"] and result["sections_in_order"] is True and not result["forbidden_markers_found"] and (result["contact_present"] in (None, True)) ) if result["sections_missing"]: result["warnings"].append( f"sections not parseable: {result['sections_missing']}") if result["forbidden_markers_found"]: result["warnings"].append( f"injected/hidden markers leaked into text: " f"{result['forbidden_markers_found']}") return result if __name__ == "__main__": # ponytail: runnable self-check (needs a sample PDF) import os, sys sample = sys.argv[1] if len(sys.argv) > 1 else os.path.join( os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "tests", "v2_output.pdf") if os.path.exists(sample): r = validate_pdf(sample, expected_sections=["experience", "education", "skills"]) print("pdf_validate self-check:", {k: r[k] for k in ("ok", "parser_recovered_text", "sections_found", "sections_missing", "forbidden_markers_found")}) else: print(f"pdf_validate self-check SKIPPED — no sample PDF at {sample}")