| from __future__ import annotations |
|
|
| import hashlib |
| from collections import Counter |
| from pathlib import Path |
|
|
| import pymupdf |
|
|
| from .models import ( |
| ExtractedDocument, |
| ExtractedPage, |
| ExtractionMethod, |
| WordBox, |
| ) |
|
|
|
|
| class ExtractionError(RuntimeError): |
| """Raised when a document cannot be converted into positioned words.""" |
|
|
|
|
| SUPPORTED_SUFFIXES = {".pdf", ".png", ".jpg", ".jpeg", ".tif", ".tiff"} |
|
|
|
|
| def _sha256(path: Path) -> str: |
| digest = hashlib.sha256() |
| with path.open("rb") as source: |
| for chunk in iter(lambda: source.read(1024 * 1024), b""): |
| digest.update(chunk) |
| return digest.hexdigest() |
|
|
|
|
| def _word_boxes( |
| raw_words: list[tuple], |
| page_number: int, |
| page_width: float, |
| page_height: float, |
| method: ExtractionMethod, |
| orientation_degrees: int, |
| ) -> list[WordBox]: |
| if orientation_degrees in {90, 270}: |
| normalized_width = page_height |
| normalized_height = page_width |
| else: |
| normalized_width = page_width |
| normalized_height = page_height |
| x_scale = 950.0 / normalized_width |
| y_scale = 1200.0 / normalized_height |
| confidence = 1.0 if method == ExtractionMethod.NATIVE_PDF else 0.78 |
| boxes: list[WordBox] = [] |
| for item in raw_words: |
| text = str(item[4]).strip() |
| if not text: |
| continue |
| x0, y0, x1, y1 = map(float, item[:4]) |
| if orientation_degrees == 90: |
| x0, y0, x1, y1 = page_height - y1, x0, page_height - y0, x1 |
| elif orientation_degrees == 180: |
| x0, y0, x1, y1 = ( |
| page_width - x1, |
| page_height - y1, |
| page_width - x0, |
| page_height - y0, |
| ) |
| elif orientation_degrees == 270: |
| x0, y0, x1, y1 = y0, page_width - x1, y1, page_width - x0 |
| boxes.append( |
| WordBox( |
| text=text, |
| page=page_number, |
| x0=x0 * x_scale, |
| y0=y0 * y_scale, |
| x1=x1 * x_scale, |
| y1=y1 * y_scale, |
| method=method, |
| confidence=confidence, |
| ) |
| ) |
| return sorted(boxes, key=lambda word: (word.y0, word.x0)) |
|
|
|
|
| def _dominant_orientation(page, text_page=None) -> int: |
| text_dict = page.get_text("dict", textpage=text_page) |
| directions: Counter[tuple[float, float]] = Counter() |
| for block in text_dict.get("blocks", []): |
| for line in block.get("lines", []): |
| direction = tuple(round(float(value), 1) for value in line.get("dir", (1, 0))) |
| weight = sum(len(span.get("text", "")) for span in line.get("spans", [])) |
| directions[direction] += weight |
| if not directions: |
| return 0 |
| direction, _ = directions.most_common(1)[0] |
| return { |
| (1.0, 0.0): 0, |
| (0.0, -1.0): 90, |
| (-1.0, 0.0): 180, |
| (0.0, 1.0): 270, |
| }.get(direction, 0) |
|
|
|
|
| def extract_document(path: str | Path) -> ExtractedDocument: |
| source_path = Path(path).expanduser().resolve() |
| if not source_path.is_file(): |
| raise ExtractionError(f"Dosya bulunamadı: {source_path}") |
| if source_path.suffix.lower() not in SUPPORTED_SUFFIXES: |
| raise ExtractionError( |
| "Desteklenmeyen dosya türü. PDF, PNG, JPG veya TIFF yükleyin." |
| ) |
|
|
| try: |
| document = pymupdf.open(source_path) |
| except Exception as exc: |
| raise ExtractionError(f"Belge açılamadı: {exc}") from exc |
|
|
| pages: list[ExtractedPage] = [] |
| for page_index, page in enumerate(document): |
| native_words = page.get_text("words", sort=True) |
| native_text_length = sum(len(str(word[4])) for word in native_words) |
| if native_text_length >= 40: |
| method = ExtractionMethod.NATIVE_PDF |
| raw_words = native_words |
| text_page = None |
| else: |
| try: |
| text_page = page.get_textpage_ocr( |
| language="tur+eng", |
| dpi=300, |
| full=True, |
| ) |
| raw_words = page.get_text("words", textpage=text_page, sort=True) |
| method = ExtractionMethod.OCR |
| except Exception as exc: |
| raise ExtractionError( |
| "Belge metin katmanı içermiyor ve OCR başlatılamadı. " |
| "Tesseract ile Türkçe/İngilizce dil paketlerinin kurulu " |
| f"olduğunu doğrulayın. Ayrıntı: {exc}" |
| ) from exc |
|
|
| orientation_degrees = _dominant_orientation(page, text_page) |
| words = _word_boxes( |
| raw_words=raw_words, |
| page_number=page_index + 1, |
| page_width=float(page.rect.width), |
| page_height=float(page.rect.height), |
| method=method, |
| orientation_degrees=orientation_degrees, |
| ) |
| if not words: |
| raise ExtractionError(f"{page_index + 1}. sayfadan metin çıkarılamadı.") |
| pages.append( |
| ExtractedPage( |
| number=page_index + 1, |
| width=950.0, |
| height=1200.0, |
| words=words, |
| method=method, |
| orientation_degrees=orientation_degrees, |
| ) |
| ) |
|
|
| document.close() |
| return ExtractedDocument( |
| source_path=str(source_path), |
| source_filename=source_path.name, |
| sha256=_sha256(source_path), |
| pages=pages, |
| ) |
|
|