chore: import upstream snapshot with attribution
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Update Platform Components Table / update (push) Has been cancelled
Docker image release / Build base image (push) Has been cancelled
Sync docs with Docusaurus / sync (push) Has been cancelled
Tests / Check if changed (push) Has been cancelled
Tests / format (push) Has been cancelled
Tests / check-imports (push) Has been cancelled
Tests / Unit / macos-latest (push) Has been cancelled
Tests / Unit / ubuntu-latest (push) Has been cancelled
Tests / Unit / windows-latest (push) Has been cancelled
Tests / mypy (push) Has been cancelled
Tests / Integration / ubuntu-latest (push) Has been cancelled
Tests / Integration / macos-latest (push) Has been cancelled
Tests / Integration / windows-latest (push) Has been cancelled
Tests / notify-slack-on-failure (push) Has been cancelled
Tests / Mark tests as completed (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
from typing import Literal
|
||||
|
||||
from haystack import Document, component, logging
|
||||
from haystack.components.converters.image.image_utils import (
|
||||
_batch_convert_pdf_pages_to_images,
|
||||
_encode_image_to_base64,
|
||||
_extract_image_sources_info,
|
||||
_PDFPageInfo,
|
||||
pillow_import,
|
||||
pypdfium2_import,
|
||||
)
|
||||
from haystack.dataclasses import ByteStream
|
||||
from haystack.dataclasses.image_content import ImageContent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@component
|
||||
class DocumentToImageContent:
|
||||
"""
|
||||
Converts documents sourced from PDF and image files into ImageContents.
|
||||
|
||||
This component processes a list of documents and extracts visual content from supported file formats, converting
|
||||
them into ImageContents that can be used for multimodal AI tasks. It handles both direct image files and PDF
|
||||
documents by extracting specific pages as images.
|
||||
|
||||
Documents are expected to have metadata containing:
|
||||
- The `file_path_meta_field` key with a valid file path that exists when combined with `root_path`
|
||||
- A supported image format (MIME type must be one of the supported image types)
|
||||
- For PDF files, a `page_number` key specifying which page to extract
|
||||
|
||||
### Usage example
|
||||
|
||||
```python
|
||||
from haystack import Document
|
||||
from haystack.components.converters.image.document_to_image import DocumentToImageContent
|
||||
|
||||
converter = DocumentToImageContent(
|
||||
file_path_meta_field="file_path",
|
||||
root_path="test/test_files",
|
||||
detail="high",
|
||||
size=(800, 600)
|
||||
)
|
||||
|
||||
documents = [
|
||||
Document(content="Optional description of apple.jpg", meta={"file_path": "images/apple.jpg"}),
|
||||
Document(
|
||||
content="Optional description of sample_pdf_1.pdf",
|
||||
meta={"file_path": "pdf/sample_pdf_1.pdf", "page_number": 1}
|
||||
)
|
||||
]
|
||||
|
||||
result = converter.run(documents)
|
||||
image_contents = result["image_contents"]
|
||||
# [ImageContent(
|
||||
# base64_image='/9j/4A...', mime_type='image/jpeg', detail='high', meta={'file_path': 'images/apple.jpg'}
|
||||
# ),
|
||||
# ImageContent(
|
||||
# base64_image='/9j/4A...', mime_type='image/jpeg', detail='high',
|
||||
# meta={'file_path': 'pdf/sample_pdf_1.pdf', 'page_number': 1})
|
||||
# )]
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
file_path_meta_field: str = "file_path",
|
||||
root_path: str | None = None,
|
||||
detail: Literal["auto", "high", "low"] | None = None,
|
||||
size: tuple[int, int] | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Initialize the DocumentToImageContent component.
|
||||
|
||||
:param file_path_meta_field: The metadata field in the Document that contains the file path to the image or PDF.
|
||||
:param root_path: The root directory path where document files are located. If provided, file paths in
|
||||
document metadata will be resolved relative to this path. If None, file paths are treated as absolute paths.
|
||||
:param detail: Optional detail level of the image (only supported by OpenAI). Can be "auto", "high", or "low".
|
||||
This will be passed to the created ImageContent objects.
|
||||
:param size: If provided, resizes the image to fit within the specified dimensions (width, height) while
|
||||
maintaining aspect ratio. This reduces file size, memory usage, and processing time, which is beneficial
|
||||
when working with models that have resolution constraints or when transmitting images to remote services.
|
||||
"""
|
||||
pillow_import.check()
|
||||
pypdfium2_import.check()
|
||||
|
||||
self.file_path_meta_field = file_path_meta_field
|
||||
self.root_path = root_path or ""
|
||||
self.detail = detail
|
||||
self.size = size
|
||||
|
||||
@component.output_types(image_contents=list[ImageContent | None])
|
||||
def run(self, documents: list[Document]) -> dict[str, list[ImageContent | None]]:
|
||||
"""
|
||||
Convert documents with image or PDF sources into ImageContent objects.
|
||||
|
||||
This method processes the input documents, extracting images from supported file formats and converting them
|
||||
into ImageContent objects.
|
||||
|
||||
:param documents: A list of documents to process. Each document should have metadata containing at minimum
|
||||
a 'file_path_meta_field' key. PDF documents additionally require a 'page_number' key to specify which
|
||||
page to convert.
|
||||
|
||||
:returns:
|
||||
Dictionary containing one key:
|
||||
- "image_contents": ImageContents created from the processed documents. These contain base64-encoded image
|
||||
data and metadata. The order corresponds to order of input documents.
|
||||
:raises ValueError:
|
||||
If any document is missing the required metadata keys, has an invalid file path, or has an unsupported
|
||||
MIME type. The error message will specify which document and what information is missing or incorrect.
|
||||
"""
|
||||
if not documents:
|
||||
return {"image_contents": []}
|
||||
|
||||
images_source_info = _extract_image_sources_info(
|
||||
documents=documents, file_path_meta_field=self.file_path_meta_field, root_path=self.root_path
|
||||
)
|
||||
|
||||
image_contents: list[ImageContent | None] = [None] * len(documents)
|
||||
|
||||
pdf_page_infos: list[_PDFPageInfo] = []
|
||||
|
||||
for doc_idx, image_source_info in enumerate(images_source_info):
|
||||
mime_type = image_source_info["mime_type"]
|
||||
path = image_source_info["path"]
|
||||
if mime_type == "application/pdf":
|
||||
# Store PDF documents for later processing
|
||||
page_number = image_source_info.get("page_number")
|
||||
assert page_number is not None # checked in _extract_image_sources_info but mypy doesn't know that
|
||||
pdf_page_info: _PDFPageInfo = {"doc_idx": doc_idx, "path": path, "page_number": page_number}
|
||||
pdf_page_infos.append(pdf_page_info)
|
||||
else:
|
||||
# Process images directly
|
||||
bytestream = ByteStream.from_file_path(filepath=path, mime_type=mime_type)
|
||||
_, base64_image = _encode_image_to_base64(bytestream=bytestream, size=self.size)
|
||||
image_contents[doc_idx] = ImageContent(
|
||||
base64_image=base64_image,
|
||||
mime_type=mime_type,
|
||||
detail=self.detail,
|
||||
meta={"file_path": documents[doc_idx].meta[self.file_path_meta_field]},
|
||||
)
|
||||
|
||||
# efficiently convert PDF pages to images: each PDF is opened and processed only once
|
||||
pdf_page_infos_by_doc_idx: dict[int, _PDFPageInfo] = {
|
||||
pdf_page_info["doc_idx"]: pdf_page_info for pdf_page_info in pdf_page_infos
|
||||
}
|
||||
pdf_images_by_doc_idx = _batch_convert_pdf_pages_to_images(
|
||||
pdf_page_infos=pdf_page_infos, size=self.size, return_base64=True
|
||||
)
|
||||
for doc_idx, base64_pdf_image in pdf_images_by_doc_idx.items():
|
||||
meta = {
|
||||
"file_path": documents[doc_idx].meta[self.file_path_meta_field],
|
||||
"page_number": pdf_page_infos_by_doc_idx[doc_idx]["page_number"],
|
||||
}
|
||||
# we know that base64_pdf_image is a string because we set return_base64=True but mypy doesn't know that
|
||||
assert isinstance(base64_pdf_image, str)
|
||||
image_contents[doc_idx] = ImageContent(
|
||||
base64_image=base64_pdf_image, mime_type="image/jpeg", detail=self.detail, meta=meta
|
||||
)
|
||||
|
||||
none_image_contents_doc_ids = [
|
||||
documents[doc_idx].id for doc_idx, image_content in enumerate(image_contents) if image_content is None
|
||||
]
|
||||
if none_image_contents_doc_ids:
|
||||
logger.warning(
|
||||
"Conversion failed for some documents. Their output will be None. Document IDs: {document_ids}",
|
||||
document_ids=none_image_contents_doc_ids,
|
||||
)
|
||||
|
||||
return {"image_contents": image_contents}
|
||||
Reference in New Issue
Block a user