feat: PDF support + Split View (P1 roadmap items)
#74 — Support PDF complet: - backend/pdf_reader.py — pymupdf/pypdf text extraction - .pdf added to SUPPORTED_EXTENSIONS in indexer.py - PDF-aware file view API endpoint (metadata, text preview) - Streaming endpoint /api/file/{vault}/pdf/stream - Frontend PDF viewer with iframe, toolbar, download - CSS for PDF viewer container #75 — Split View (PaneManager): - New module frontend/js/pane-manager.js — ES module - 1-4 pane CSS grid with resize handles - Split right/down via keyboard (Ctrl+Alt+\) or PaneManager API - Persistence in localStorage (obsigate-panes) - Collapse to single pane, per-pane active state 285 tests passent
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
"""PDF text extraction for ObsiGate — pypdf backend, pymupdf optional fallback."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PDF_READER: str = "pypdf"
|
||||
try:
|
||||
import fitz # pymupdf
|
||||
PDF_READER = "pymupdf"
|
||||
logger.info("PDF reader: pymupdf (high performance)")
|
||||
except ImportError:
|
||||
try:
|
||||
from pypdf import PdfReader # type: ignore
|
||||
logger.info("PDF reader: pypdf (pure Python)")
|
||||
except ImportError:
|
||||
PdfReader = None # type: ignore
|
||||
logger.warning("No PDF reader available — install pypdf or pymupdf")
|
||||
|
||||
|
||||
def extract_pdf_text(file_path: Path, max_chars: int = 100000) -> str:
|
||||
"""Extract text from a PDF file. Returns empty string on failure."""
|
||||
if PdfReader is None and PDF_READER == "pypdf":
|
||||
return ""
|
||||
try:
|
||||
if PDF_READER == "pymupdf":
|
||||
return _extract_pymupdf(file_path, max_chars)
|
||||
else:
|
||||
return _extract_pypdf(file_path, max_chars)
|
||||
except Exception as e:
|
||||
logger.warning("Failed to extract PDF text from %s: %s", file_path, e)
|
||||
return ""
|
||||
|
||||
|
||||
def extract_pdf_metadata(file_path: Path) -> dict:
|
||||
"""Extract metadata from a PDF file."""
|
||||
info = {"pages": 0, "title": "", "author": ""}
|
||||
try:
|
||||
if PDF_READER == "pymupdf":
|
||||
doc = fitz.open(str(file_path))
|
||||
info["pages"] = doc.page_count
|
||||
meta = doc.metadata or {}
|
||||
info["title"] = meta.get("title", "")
|
||||
info["author"] = meta.get("author", "")
|
||||
doc.close()
|
||||
else:
|
||||
reader = PdfReader(str(file_path))
|
||||
info["pages"] = len(reader.pages)
|
||||
meta = reader.metadata or {}
|
||||
if meta:
|
||||
info["title"] = str(meta.get("/Title", ""))
|
||||
info["author"] = str(meta.get("/Author", ""))
|
||||
except Exception as e:
|
||||
logger.warning("Failed to extract PDF metadata from %s: %s", file_path, e)
|
||||
return info
|
||||
|
||||
|
||||
def _extract_pymupdf(file_path: Path, max_chars: int) -> str:
|
||||
doc = fitz.open(str(file_path))
|
||||
parts = []
|
||||
total = 0
|
||||
for page in doc:
|
||||
text = page.get_text()
|
||||
if text:
|
||||
if total + len(text) > max_chars:
|
||||
remaining = max_chars - total
|
||||
if remaining > 0:
|
||||
parts.append(text[:remaining])
|
||||
break
|
||||
parts.append(text)
|
||||
total += len(text)
|
||||
doc.close()
|
||||
return "\f".join(parts)
|
||||
|
||||
|
||||
def _extract_pypdf(file_path: Path, max_chars: int) -> str:
|
||||
reader = PdfReader(str(file_path))
|
||||
parts = []
|
||||
total = 0
|
||||
for page in reader.pages:
|
||||
text = page.extract_text() or ""
|
||||
if text:
|
||||
if total + len(text) > max_chars:
|
||||
remaining = max_chars - total
|
||||
if remaining > 0:
|
||||
parts.append(text[:remaining])
|
||||
break
|
||||
parts.append(text)
|
||||
total += len(text)
|
||||
return "\f".join(parts)
|
||||
Reference in New Issue
Block a user