From abeacfc3b8b94e2068e476a17d6bb89af50e7d54 Mon Sep 17 00:00:00 2001 From: Stefan Lohmaier Date: Fri, 19 Jun 2026 08:27:08 +0200 Subject: [PATCH] feat(files): gescannte/bildbasierte PDFs als Bilder rendern (Vision) read_file gab bei Scan-PDFs (kein extrahierbarer Text) nur nutzlose Rohbytes zurueck -> claude.ai konnte sie nicht lesen. Jetzt: PyMuPDF rendert die Seiten als PNG (150 dpi, max 20 Seiten) -> ImageContent, das LLM liest sie per Vision. Verschluesselte/kaputte PDFs bleiben graceful. TestFileTypes scanned.pdf -> image. Produktiv-Fix fuer alle User. Dep: pymupdf (requirements-extra.txt). 76 Tests gruen. --- files/server.py | 23 +++++++++++++++++++---- requirements-extra.txt | 8 ++++++++ tests/MCPTEST.md | 9 +++++++++ tests/test_all.py | 2 +- 4 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 requirements-extra.txt diff --git a/files/server.py b/files/server.py index 505c497..2b252b4 100644 --- a/files/server.py +++ b/files/server.py @@ -127,10 +127,25 @@ def read_file( text = "\n\n".join(pages) if text.strip(): return [TextContent(type="text", text=f"[PDF: {path}]\n\n{text[:200000]}")] - return [ - TextContent(type="text", text=f"[PDF '{path}' enthaelt keinen extrahierbaren Text (vermutlich Scan). Rohdaten folgen.]"), - EmbeddedResource(type="resource", resource=BlobResourceContents(uri=f"file://{path}", blob=base64.b64encode(r.content).decode(), mimeType=ct)), - ] + # 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}")] try: diff --git a/requirements-extra.txt b/requirements-extra.txt new file mode 100644 index 0000000..d32295c --- /dev/null +++ b/requirements-extra.txt @@ -0,0 +1,8 @@ +httpx==0.28.1 +httpx-sse==0.4.3 +mcp==1.27.2 +openpyxl==3.1.5 +pdfplumber==0.11.9 +pillow==12.2.0 +PyMuPDF==1.27.2.3 +python-docx==1.2.0 diff --git a/tests/MCPTEST.md b/tests/MCPTEST.md index 0c82c38..62f3c03 100644 --- a/tests/MCPTEST.md +++ b/tests/MCPTEST.md @@ -93,3 +93,12 @@ Umlauten/Leerzeichen/Klammern, Unicode/Emoji/RTL-Inhalt, Datei ohne Endung, passwortgeschuetztes PDF + ZIP, uebergrosse Datei (26 MB > 25-MB-Limit -> "Datei zu gross"). Alle werden graceful behandelt (kein Crash). Generator: `gen_edge.py` (braucht pikepdf + zip), Upload via `upload_ocis.sh`. + +## Verbesserung: bildbasierte/gescannte PDFs (2026-06-19) + +Frueher gab `read_file` bei Scan-PDFs (kein extrahierbarer Text) nur Rohbytes +(`EmbeddedResource`) zurueck — claude.ai konnte den Inhalt nicht lesen. Jetzt werden +solche PDFs mit **PyMuPDF** seitenweise als **PNG-Bilder** (150 dpi, max 20 Seiten) +gerendert und als `ImageContent` zurueckgegeben -> das LLM liest sie per Vision. +Produktiv-Feature (gilt fuer alle User). Test: `TestFileTypes` scanned.pdf -> `image`. +Runtime-Dep: `pymupdf` (siehe `requirements-extra.txt`). diff --git a/tests/test_all.py b/tests/test_all.py index b0c3855..106c4ec 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -439,7 +439,7 @@ class TestFileTypes: ("testdata/text/readme.md", {"text"}), ("testdata/text/data.csv", {"text"}), ("testdata/documents/document.pdf", {"text"}), # Text-PDF -> extrahiert - ("testdata/documents/scanned.pdf", {"text", "resource"}),# Scan-PDF + ("testdata/documents/scanned.pdf", {"image"}), # Scan-PDF -> als Bild gerendert ("testdata/documents/report.docx", {"text"}), ("testdata/documents/budget.xlsx", {"text"}), ("testdata/documents/slides.pptx", {"text"}),