mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-15 09:31:21 +00:00
fix: PDF upload failures with PDF Extract Images (OCR) enabled
Uploading a PDF with OCR image extraction enabled fails with "cannot reshape array" or similar errors whenever a page contains an image the bundled langchain parser cannot decode: filter chains like [/ASCII85Decode /DCTDecode], images without a /Filter entry, 1-bit scans, image masks, indexed and grayscale images. One such image aborts the whole upload. Reported since v0.5.7 (#4457, #6085, #8856, #10913, #11333, #25979). That parser reimplements PDF filter handling by hand and assumes a single lossless filter. langchain-community is archived and will not take a patch, so a small parser subclass now delegates image decoding to pypdf, which handles all of these cases, and an image that still cannot be read is skipped with a warning instead of failing the file. Verified against the stock loader on fixture PDFs: previously working files produce identical output. Further differences, all deliberate: images nested in Form XObjects are now found; inline images stay excluded like before, which also avoids pypdf retaining every decoded inline image per page (115 MB on a 106 KB test file); image lookups sidestep pypdf's quadratic resource-tree re-walk (93 s down to 0.4 s on a 136 KB crafted file); CMYK JPEGs reach OCR with correct colours where pypdf's unconditional inversion turned them solid black. Fixes #29187 ### Contributor License Agreement <!-- 🚨 DO NOT DELETE THE TEXT BELOW 🚨 Keep the "Contributor License Agreement" confirmation text intact. Deleting it will trigger the CLA-Bot to INVALIDATE your PR. Your PR will NOT be reviewed or merged until you check the box below confirming that you have read and agree to the terms of the CLA. --> - [x] By submitting this pull request, I confirm that I have read and fully agree to the [Contributor License Agreement (CLA)](https://github.com/open-webui/open-webui/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT), and I am providing my contributions under its terms. > [!NOTE] > Deleting the CLA section will lead to immediate closure of your PR and it will not be merged in.
This commit is contained in:
parent
a93c508038
commit
5a9b6cd06d
2 changed files with 72 additions and 2 deletions
|
|
@ -13,7 +13,6 @@ from langchain_community.document_loaders import (
|
|||
BSHTMLLoader,
|
||||
CSVLoader,
|
||||
Docx2txtLoader,
|
||||
PyPDFLoader,
|
||||
TextLoader,
|
||||
)
|
||||
from langchain_core.documents import Document
|
||||
|
|
@ -28,6 +27,7 @@ from open_webui.retrieval.loaders.external_document import ExternalDocumentLoade
|
|||
from open_webui.retrieval.loaders.mineru import MinerULoader
|
||||
from open_webui.retrieval.loaders.mistral import MistralLoader
|
||||
from open_webui.retrieval.loaders.paddleocr_vl import PADDLEOCR_VL_SUPPORTED_EXTENSIONS, PaddleOCRVLLoader
|
||||
from open_webui.retrieval.loaders.pdf import PyPDFLoaderWithSafeImages
|
||||
from open_webui.utils.headers import get_user_groups_for_custom_headers
|
||||
from open_webui.utils.json_codec import JSONCodec
|
||||
|
||||
|
|
@ -667,7 +667,7 @@ class Loader:
|
|||
)
|
||||
else:
|
||||
if file_ext == 'pdf':
|
||||
loader = PyPDFLoader(
|
||||
loader = PyPDFLoaderWithSafeImages(
|
||||
file_path,
|
||||
extract_images=self.kwargs.get('PDF_EXTRACT_IMAGES'),
|
||||
mode=self.kwargs.get('PDF_LOADER_MODE', 'page'),
|
||||
|
|
|
|||
70
backend/open_webui/retrieval/loaders/pdf.py
Normal file
70
backend/open_webui/retrieval/loaders/pdf.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import io
|
||||
import logging
|
||||
from typing import Literal
|
||||
|
||||
from langchain_community.document_loaders import PyPDFLoader
|
||||
from langchain_community.document_loaders.parsers.pdf import PyPDFParser
|
||||
from langchain_core.document_loaders import Blob
|
||||
from PIL import Image
|
||||
from pypdf import PageObject
|
||||
from pypdf.generic import StreamObject
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_cmyk_jpeg(image_stream: StreamObject) -> bool:
|
||||
"""True when the image is a CMYK JPEG whose data pypdf will hand back inverted."""
|
||||
filters = image_stream['/Filter'] if '/Filter' in image_stream else None
|
||||
if (filters[-1] if isinstance(filters, list) else filters) != '/DCTDecode':
|
||||
return False
|
||||
return Image.open(io.BytesIO(image_stream.get_data())).mode == 'CMYK'
|
||||
|
||||
|
||||
class PyPDFParserWithSafeImages(PyPDFParser):
|
||||
"""PyPDFParser that lets pypdf decode page images instead of reading /Filter itself."""
|
||||
|
||||
def extract_images_from_page(self, page: PageObject) -> str:
|
||||
if not self.images_parser:
|
||||
return ''
|
||||
|
||||
page.inline_images = {}
|
||||
try:
|
||||
images = page.images[:]
|
||||
except Exception as e:
|
||||
log.warning('Skipping images on PDF page: %s', e)
|
||||
return ''
|
||||
|
||||
image_texts = []
|
||||
for index in range(len(images)):
|
||||
try:
|
||||
image_file = images[index]
|
||||
image_stream = image_file.indirect_reference.get_object()
|
||||
image = image_file.image
|
||||
# pypdf re-inverts CMYK JPEGs that PIL already un-inverted, and an /SMask makes that unrecoverable
|
||||
if _is_cmyk_jpeg(image_stream):
|
||||
image = Image.open(io.BytesIO(image_stream.get_data()))
|
||||
|
||||
png_bytes = io.BytesIO()
|
||||
image.convert('RGB').save(png_bytes, format='PNG')
|
||||
except Exception as e:
|
||||
log.warning('Skipping unreadable image %s on PDF page: %s', index, e)
|
||||
continue
|
||||
|
||||
blob = Blob.from_data(png_bytes.getvalue(), mime_type='image/png')
|
||||
text = next(self.images_parser.lazy_parse(blob)).page_content
|
||||
if text:
|
||||
image_texts.append(text)
|
||||
|
||||
if not image_texts:
|
||||
return ''
|
||||
return '\n\n' + '\n'.join(image_texts) + '\n\n'
|
||||
|
||||
|
||||
class PyPDFLoaderWithSafeImages(PyPDFLoader):
|
||||
"""Loads a PDF with pypdf, tolerating images it cannot decode instead of failing the document."""
|
||||
|
||||
def __init__(
|
||||
self, file_path: str, *, extract_images: bool = False, mode: Literal['single', 'page'] = 'page'
|
||||
) -> None:
|
||||
super().__init__(file_path, extract_images=extract_images, mode=mode)
|
||||
self.parser = PyPDFParserWithSafeImages(extract_images=extract_images, mode=mode)
|
||||
Loading…
Add table
Reference in a new issue