JAA-ATS-Tool / src /pdf_validate.py
saitejatirunagari's picture
feat: live V1 acceptance — independent audit, 2-parser PDF, summary opt, completeness
43c3b77
Raw
History Blame
7.64 kB
"""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 _pdftotext_extract(pdf_path: str) -> Optional[str]:
"""Independent parser #2: the poppler `pdftotext` CLI. None if unavailable."""
import shutil
import subprocess
exe = shutil.which("pdftotext")
if not exe:
return None
try:
out = subprocess.run([exe, "-layout", pdf_path, "-"],
capture_output=True, timeout=30)
txt = out.stdout.decode("utf-8", errors="replace")
return txt if txt.strip() else None
except Exception:
return None
def verify_keywords_two_parsers(pdf_path: str, keywords: List[str]) -> Dict:
"""Confirm each accepted keyword survives BOTH independent parsers (a Python
lib + poppler's pdftotext). Flags any keyword split/corrupted/missing in
either. Falls back to a second Python parser (pymupdf) when pdftotext is
absent, so there are always two independent extractions."""
import re as _re
py = _extract_pdf_text(pdf_path) or ""
cli = _pdftotext_extract(pdf_path)
parser2_name = "pdftotext"
if cli is None:
# fall back to a genuinely different Python engine
try:
import fitz
doc = fitz.open(pdf_path)
cli = "\n".join(p.get_text() for p in doc)
doc.close()
parser2_name = "pymupdf"
except Exception:
cli = ""
def _present(text, kw):
p = _re.sub(r"\s+", " ", (kw or "").lower()).strip()
toks = [_re.escape(t) for t in p.split()]
if not toks:
return False
pat = r"(?<![a-z0-9])" + r"[\s\W]{0,3}".join(toks) + r"(?![a-z0-9])"
return _re.search(pat, text.lower()) is not None
results = []
for kw in keywords:
in_py, in_cli = _present(py, kw), _present(cli, kw)
results.append({"keyword": kw, "parser1_pdfplumber": in_py,
f"parser2_{parser2_name}": in_cli,
"in_both": in_py and in_cli})
return {
"parser1": "pdfplumber/pymupdf", "parser2": parser2_name,
"parser1_chars": len(py), "parser2_chars": len(cli),
"keywords_checked": len(keywords),
"present_in_both": sum(1 for r in results if r["in_both"]),
"missing_or_split": [r["keyword"] for r in results if not r["in_both"]],
"per_keyword": results,
}
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:
s = sec.lower()
for m in re.finditer(r"(?im)^[^\S\n]*([A-Za-z &/]{3,40})[^\S\n]*$", text):
line = m.group(1).strip().lower()
# near-exact heading line only (résumé headings stand alone) — never a
# prose line that merely starts with the section word.
if line == s or line.rstrip("s") == s.rstrip("s") \
or (line.startswith(s) and len(line) <= len(s) + 3):
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}")