Files
ai-job-search/tools/verify_pdf.py
T

105 lines
3.2 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/env python3
"""Verify that a generated PDF has the expected pages and extractable text."""
import argparse
import re
import subprocess
import sys
from pathlib import Path
class VerificationError(Exception):
"""Raised when a generated PDF does not satisfy its checks."""
def run_tool(command):
try:
return subprocess.run(
command,
check=True,
capture_output=True,
text=True,
).stdout
except FileNotFoundError as exc:
raise VerificationError(
f"required command '{command[0]}' was not found. "
"Install poppler-utils (macOS: brew install poppler, "
"Debian/Ubuntu: apt install poppler-utils, Windows: choco install poppler)"
) from exc
except subprocess.CalledProcessError as exc:
detail = (exc.stderr or "").strip() or (exc.stdout or "").strip()
detail = detail or "command failed"
raise VerificationError(f"{command[0]} could not read the PDF: {detail}") from exc
def parse_page_count(pdfinfo_output):
match = re.search(r"^Pages:\s+(\d+)\s*$", pdfinfo_output, re.MULTILINE)
if not match:
raise VerificationError("pdfinfo output did not contain a page count")
return int(match.group(1))
def normalize_text(text):
return " ".join(text.split())
def verify_pdf(pdf_path, expected_pages=None, min_chars=1, required_text=()):
pdf_path = Path(pdf_path)
if not pdf_path.is_file():
raise VerificationError(f"PDF does not exist: {pdf_path}")
if expected_pages is not None:
actual_pages = parse_page_count(run_tool(["pdfinfo", str(pdf_path)]))
if actual_pages != expected_pages:
raise VerificationError(
f"expected {expected_pages} page(s), found {actual_pages}"
)
extracted_text = normalize_text(
run_tool(["pdftotext", "-layout", str(pdf_path), "-"])
)
if len(extracted_text) < min_chars:
raise VerificationError(
f"text layer has {len(extracted_text)} character(s); expected at least {min_chars}"
)
for required in required_text:
if normalize_text(required) not in extracted_text:
raise VerificationError(f"text layer is missing required text: {required!r}")
def build_parser():
parser = argparse.ArgumentParser(
description="Verify a PDF's page count and ATS-readable text layer."
)
parser.add_argument("pdf", type=Path, help="PDF file to verify")
parser.add_argument("--pages", type=int, help="required exact page count")
parser.add_argument(
"--min-chars",
type=int,
default=1,
help="minimum non-whitespace text-layer characters (default: 1)",
)
parser.add_argument(
"--contains",
action="append",
default=[],
help="text that must appear after whitespace normalization; repeatable",
)
return parser
def main(argv=None):
args = build_parser().parse_args(argv)
try:
verify_pdf(args.pdf, args.pages, args.min_chars, args.contains)
except VerificationError as exc:
print(f"Error: {args.pdf}: {exc}", file=sys.stderr)
return 1
print(f"Verified {args.pdf}")
return 0
if __name__ == "__main__":
sys.exit(main())