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:
+70
@@ -0,0 +1,70 @@
|
||||
"""Gemeinsame PDF-Verarbeitung fuer Files- und Mail-MCP.
|
||||
|
||||
Text-PDFs -> extrahierter Text. Bildbasierte/gescannte PDFs -> Seiten als PNG
|
||||
gerendert (PyMuPDF) + OCR-Text (tesseract), damit das LLM sie per Vision liest
|
||||
UND durchsuchbaren Text bekommt. Verschluesselte/kaputte PDFs -> graceful.
|
||||
"""
|
||||
import base64
|
||||
import io
|
||||
|
||||
from mcp.types import TextContent, ImageContent, EmbeddedResource, BlobResourceContents
|
||||
|
||||
DPI = 150
|
||||
MAX_PAGES = 20
|
||||
|
||||
|
||||
def _ocr(png_bytes):
|
||||
try:
|
||||
import pytesseract
|
||||
from PIL import Image
|
||||
return pytesseract.image_to_string(Image.open(io.BytesIO(png_bytes)), lang="deu+eng").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def pdf_to_content(content, label, ct="application/pdf", uri=None):
|
||||
uri = uri or f"file://{label}"
|
||||
|
||||
# 1. Echten Text extrahieren
|
||||
try:
|
||||
import pdfplumber
|
||||
with pdfplumber.open(io.BytesIO(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: {label}]\n\n{text[:200000]}")]
|
||||
except Exception:
|
||||
pass # weiter zum Rendern (z.B. verschluesselt -> faellt unten in except)
|
||||
|
||||
# 2. Bildbasiert/gescannt -> Seiten rendern + OCR
|
||||
try:
|
||||
import fitz # PyMuPDF
|
||||
doc = fitz.open(stream=content, filetype="pdf")
|
||||
n = doc.page_count
|
||||
images, ocr_pages = [], []
|
||||
for i in range(min(n, MAX_PAGES)):
|
||||
png = doc[i].get_pixmap(dpi=DPI).tobytes("png")
|
||||
images.append(ImageContent(type="image", data=base64.b64encode(png).decode(), mimeType="image/png"))
|
||||
t = _ocr(png)
|
||||
if t:
|
||||
ocr_pages.append(f"--- Seite {i + 1} (OCR) ---\n{t}")
|
||||
header = f"[PDF '{label}' ist bildbasiert/gescannt ({n} Seite(n)) — als Bilder gerendert"
|
||||
header += " (+ OCR-Text)" if ocr_pages else ""
|
||||
header += ":]"
|
||||
out = [TextContent(type="text", text=header)]
|
||||
if ocr_pages:
|
||||
out.append(TextContent(type="text", text="\n\n".join(ocr_pages)[:200000]))
|
||||
out += images
|
||||
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 '{label}' konnte nicht verarbeitet werden ({e}). Rohdaten folgen.]"),
|
||||
EmbeddedResource(type="resource", resource=BlobResourceContents(
|
||||
uri=uri, blob=base64.b64encode(content).decode(), mimeType=ct)),
|
||||
]
|
||||
Reference in New Issue
Block a user