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.
This commit is contained in:
Stefan Lohmaier
2026-06-19 08:27:08 +02:00
parent 85f5e26384
commit abeacfc3b8
4 changed files with 37 additions and 5 deletions
+19 -4
View File
@@ -127,10 +127,25 @@ def read_file(
text = "\n\n".join(pages) text = "\n\n".join(pages)
if text.strip(): if text.strip():
return [TextContent(type="text", text=f"[PDF: {path}]\n\n{text[:200000]}")] return [TextContent(type="text", text=f"[PDF: {path}]\n\n{text[:200000]}")]
return [ # Kein extrahierbarer Text -> bildbasiert/gescannt: Seiten als Bilder
TextContent(type="text", text=f"[PDF '{path}' enthaelt keinen extrahierbaren Text (vermutlich Scan). Rohdaten folgen.]"), # rendern, damit das LLM sie per Vision lesen kann (statt nutzloser Rohbytes).
EmbeddedResource(type="resource", resource=BlobResourceContents(uri=f"file://{path}", blob=base64.b64encode(r.content).decode(), mimeType=ct)), 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: except Exception as e:
return [TextContent(type="text", text=f"PDF '{path}' konnte nicht gelesen werden: {e}")] return [TextContent(type="text", text=f"PDF '{path}' konnte nicht gelesen werden: {e}")]
try: try:
+8
View File
@@ -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
+9
View File
@@ -93,3 +93,12 @@ Umlauten/Leerzeichen/Klammern, Unicode/Emoji/RTL-Inhalt, Datei ohne Endung,
passwortgeschuetztes PDF + ZIP, uebergrosse Datei (26 MB > 25-MB-Limit -> passwortgeschuetztes PDF + ZIP, uebergrosse Datei (26 MB > 25-MB-Limit ->
"Datei zu gross"). Alle werden graceful behandelt (kein Crash). Generator: "Datei zu gross"). Alle werden graceful behandelt (kein Crash). Generator:
`gen_edge.py` (braucht pikepdf + zip), Upload via `upload_ocis.sh`. `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`).
+1 -1
View File
@@ -439,7 +439,7 @@ class TestFileTypes:
("testdata/text/readme.md", {"text"}), ("testdata/text/readme.md", {"text"}),
("testdata/text/data.csv", {"text"}), ("testdata/text/data.csv", {"text"}),
("testdata/documents/document.pdf", {"text"}), # Text-PDF -> extrahiert ("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/report.docx", {"text"}),
("testdata/documents/budget.xlsx", {"text"}), ("testdata/documents/budget.xlsx", {"text"}),
("testdata/documents/slides.pptx", {"text"}), ("testdata/documents/slides.pptx", {"text"}),