File size: 2,048 Bytes
c8f4a46
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import re
from pathlib import Path

import fitz  # PyMuPDF

from app.core.logging import get_logger

logger = get_logger(__name__)

# If extracted text is fewer than this many characters it's almost certainly
# a scanned / image-only PDF that we cannot handle in MVP.
SCANNED_THRESHOLD = 100


class ExtractionError(Exception):
    """Raised when the PDF cannot be meaningfully extracted."""


def extract_text(path: Path) -> tuple[str, int]:
    """
    Extract and clean text from a digital PDF.

    Returns
    -------
    text : str
        Cleaned, whitespace-normalised body text.
    page_count : int
        Number of pages in the document.

    Raises
    ------
    ExtractionError
        If the file is unreadable or appears to be a scanned/image PDF.
    """
    try:
        doc = fitz.open(str(path))
    except Exception as exc:
        logger.error("Could not open PDF %s: %s", path, exc)
        raise ExtractionError("Could not read this PDF.") from exc

    page_count = len(doc)
    raw_pages: list[str] = []

    for page in doc:
        raw_pages.append(page.get_text("text"))  # type: ignore[arg-type]

    doc.close()

    full_text = "\n".join(raw_pages)
    cleaned = _clean(full_text)

    if len(cleaned) < SCANNED_THRESHOLD:
        logger.warning(
            "PDF %s yielded only %d chars — likely scanned", path, len(cleaned)
        )
        raise ExtractionError(
            "This looks like a scanned resume; MVP supports digital text PDFs only."
        )

    logger.info("Extracted %d chars from %d-page PDF %s", len(cleaned), page_count, path.name)
    return cleaned, page_count


def _clean(raw: str) -> str:
    """Normalise whitespace and remove common PDF artifacts."""
    # Collapse runs of whitespace that aren't newlines
    text = re.sub(r"[ \t]+", " ", raw)
    # Collapse 3+ consecutive blank lines into 2
    text = re.sub(r"\n{3,}", "\n\n", text)
    # Strip leading/trailing whitespace per line
    lines = [line.strip() for line in text.splitlines()]
    return "\n".join(lines).strip()