feat(pdf): gemeinsames pdfutil — Scan-PDFs als Bild + OCR (Files & Mail)

Scan-/bildbasierte PDFs werden jetzt von Files-MCP (read_file) UND Mail-MCP
(read_attachment) ueber das gemeinsame Modul pdfutil.py verarbeitet: Seiten via
PyMuPDF als PNG (150dpi, max 20) + OCR-Text (tesseract deu+eng). Verschluesselte/
kaputte PDFs bleiben graceful. Deps: pymupdf, pytesseract (+ system tesseract-ocr).
76 Tests gruen.
This commit is contained in:
Stefan Lohmaier
2026-06-19 08:46:00 +02:00
parent abeacfc3b8
commit 936ebc2f56
5 changed files with 80 additions and 54 deletions
+2 -32
View File
@@ -13,6 +13,7 @@ from starlette.routing import Mount
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from common import get_current_user, OAUTH_ROUTES, BearerAuthMiddleware
from pdfutil import pdf_to_content
from common import load_config as _lc
_cfg = _lc()
@@ -116,38 +117,7 @@ def read_file(
if any(t in ct for t in TEXT_HINTS):
return [TextContent(type="text", text=r.text[:100000])]
if ct == "application/pdf" or path.lower().endswith(".pdf"):
try:
import pdfplumber
with pdfplumber.open(io.BytesIO(r.content)) as pdf:
pages = []
for i, page in enumerate(pdf.pages, 1):
t = page.extract_text() or ""
if t.strip():
pages.append(f"--- Seite {i} ---\n{t}")
text = "\n\n".join(pages)
if text.strip():
return [TextContent(type="text", text=f"[PDF: {path}]\n\n{text[:200000]}")]
# Kein extrahierbarer Text -> bildbasiert/gescannt: Seiten als Bilder
# rendern, damit das LLM sie per Vision lesen kann (statt nutzloser Rohbytes).
try:
import fitz # PyMuPDF
doc = fitz.open(stream=r.content, filetype="pdf")
n = doc.page_count
MAX_PAGES = 20
out = [TextContent(type="text", text=f"[PDF '{path}' ist bildbasiert/gescannt ({n} Seite(n)) — als Bilder gerendert:]")]
for i in range(min(n, MAX_PAGES)):
pix = doc[i].get_pixmap(dpi=150)
out.append(ImageContent(type="image", data=base64.b64encode(pix.tobytes("png")).decode(), mimeType="image/png"))
if n > MAX_PAGES:
out.append(TextContent(type="text", text=f"[... {n - MAX_PAGES} weitere Seiten ausgelassen (Limit {MAX_PAGES}).]"))
return out
except Exception as e:
return [
TextContent(type="text", text=f"[PDF '{path}' ist bildbasiert; Rendern fehlgeschlagen ({e}). Rohdaten folgen.]"),
EmbeddedResource(type="resource", resource=BlobResourceContents(uri=f"file://{path}", blob=base64.b64encode(r.content).decode(), mimeType=ct)),
]
except Exception as e:
return [TextContent(type="text", text=f"PDF '{path}' konnte nicht gelesen werden: {e}")]
return pdf_to_content(r.content, path, ct)
try:
text = r.content.decode("utf-8")
return [TextContent(type="text", text=text[:100000])]