Coverage for haystack/components/converters/image/document_to_image.py: 100%
43 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5from typing import Literal
7from haystack import Document, component, logging
8from haystack.components.converters.image.image_utils import (
9 _batch_convert_pdf_pages_to_images,
10 _encode_image_to_base64,
11 _extract_image_sources_info,
12 _PDFPageInfo,
13 pillow_import,
14 pypdfium2_import,
15)
16from haystack.dataclasses import ByteStream
17from haystack.dataclasses.image_content import ImageContent
19logger = logging.getLogger(__name__)
22@component
23class DocumentToImageContent:
24 """
25 Converts documents sourced from PDF and image files into ImageContents.
27 This component processes a list of documents and extracts visual content from supported file formats, converting
28 them into ImageContents that can be used for multimodal AI tasks. It handles both direct image files and PDF
29 documents by extracting specific pages as images.
31 Documents are expected to have metadata containing:
32 - The `file_path_meta_field` key with a valid file path that exists when combined with `root_path`
33 - A supported image format (MIME type must be one of the supported image types)
34 - For PDF files, a `page_number` key specifying which page to extract
36 ### Usage example
38 ```python
39 from haystack import Document
40 from haystack.components.converters.image.document_to_image import DocumentToImageContent
42 converter = DocumentToImageContent(
43 file_path_meta_field="file_path",
44 root_path="test/test_files",
45 detail="high",
46 size=(800, 600)
47 )
49 documents = [
50 Document(content="Optional description of apple.jpg", meta={"file_path": "images/apple.jpg"}),
51 Document(
52 content="Optional description of sample_pdf_1.pdf",
53 meta={"file_path": "pdf/sample_pdf_1.pdf", "page_number": 1}
54 )
55 ]
57 result = converter.run(documents)
58 image_contents = result["image_contents"]
59 # [ImageContent(
60 # base64_image='/9j/4A...', mime_type='image/jpeg', detail='high', meta={'file_path': 'images/apple.jpg'}
61 # ),
62 # ImageContent(
63 # base64_image='/9j/4A...', mime_type='image/jpeg', detail='high',
64 # meta={'file_path': 'pdf/sample_pdf_1.pdf', 'page_number': 1})
65 # )]
66 ```
67 """
69 def __init__(
70 self,
71 *,
72 file_path_meta_field: str = "file_path",
73 root_path: str | None = None,
74 detail: Literal["auto", "high", "low"] | None = None,
75 size: tuple[int, int] | None = None,
76 ) -> None:
77 """
78 Initialize the DocumentToImageContent component.
80 :param file_path_meta_field: The metadata field in the Document that contains the file path to the image or PDF.
81 :param root_path: The root directory path where document files are located. If provided, file paths in
82 document metadata will be resolved relative to this path and are guaranteed to stay within it. If None,
83 file paths are treated as absolute paths with no containment check.
84 Security: this component reads the file referenced by `file_path_meta_field` from the host filesystem. If
85 document metadata may be influenced by untrusted input, set `root_path` to a dedicated data directory so
86 that path-traversal payloads (e.g. absolute paths or `../`) are rejected instead of read.
87 :param detail: Optional detail level of the image (only supported by OpenAI). Can be "auto", "high", or "low".
88 This will be passed to the created ImageContent objects.
89 :param size: If provided, resizes the image to fit within the specified dimensions (width, height) while
90 maintaining aspect ratio. This reduces file size, memory usage, and processing time, which is beneficial
91 when working with models that have resolution constraints or when transmitting images to remote services.
92 """
93 pillow_import.check()
94 pypdfium2_import.check()
96 self.file_path_meta_field = file_path_meta_field
97 self.root_path = root_path or ""
98 self.detail = detail
99 self.size = size
101 @component.output_types(image_contents=list[ImageContent | None])
102 def run(self, documents: list[Document]) -> dict[str, list[ImageContent | None]]:
103 """
104 Convert documents with image or PDF sources into ImageContent objects.
106 This method processes the input documents, extracting images from supported file formats and converting them
107 into ImageContent objects.
109 :param documents: A list of documents to process. Each document should have metadata containing at minimum
110 a 'file_path_meta_field' key. PDF documents additionally require a 'page_number' key to specify which
111 page to convert.
113 :returns:
114 Dictionary containing one key:
115 - "image_contents": ImageContents created from the processed documents. These contain base64-encoded image
116 data and metadata. The order corresponds to order of input documents.
117 :raises ValueError:
118 If any document is missing the required metadata keys, has an invalid file path, or has an unsupported
119 MIME type. The error message will specify which document and what information is missing or incorrect.
120 """
121 if not documents:
122 return {"image_contents": []}
124 images_source_info = _extract_image_sources_info(
125 documents=documents, file_path_meta_field=self.file_path_meta_field, root_path=self.root_path
126 )
128 image_contents: list[ImageContent | None] = [None] * len(documents)
130 pdf_page_infos: list[_PDFPageInfo] = []
132 for doc_idx, image_source_info in enumerate(images_source_info):
133 mime_type = image_source_info["mime_type"]
134 path = image_source_info["path"]
135 if mime_type == "application/pdf":
136 # Store PDF documents for later processing
137 page_number = image_source_info.get("page_number")
138 assert page_number is not None # checked in _extract_image_sources_info but mypy doesn't know that
139 pdf_page_info: _PDFPageInfo = {"doc_idx": doc_idx, "path": path, "page_number": page_number}
140 pdf_page_infos.append(pdf_page_info)
141 else:
142 # Process images directly
143 bytestream = ByteStream.from_file_path(filepath=path, mime_type=mime_type)
144 _, base64_image = _encode_image_to_base64(bytestream=bytestream, size=self.size)
145 image_contents[doc_idx] = ImageContent(
146 base64_image=base64_image,
147 mime_type=mime_type,
148 detail=self.detail,
149 meta={"file_path": documents[doc_idx].meta[self.file_path_meta_field]},
150 )
152 # efficiently convert PDF pages to images: each PDF is opened and processed only once
153 pdf_page_infos_by_doc_idx: dict[int, _PDFPageInfo] = {
154 pdf_page_info["doc_idx"]: pdf_page_info for pdf_page_info in pdf_page_infos
155 }
156 pdf_images_by_doc_idx = _batch_convert_pdf_pages_to_images(
157 pdf_page_infos=pdf_page_infos, size=self.size, return_base64=True
158 )
159 for doc_idx, base64_pdf_image in pdf_images_by_doc_idx.items():
160 meta = {
161 "file_path": documents[doc_idx].meta[self.file_path_meta_field],
162 "page_number": pdf_page_infos_by_doc_idx[doc_idx]["page_number"],
163 }
164 # we know that base64_pdf_image is a string because we set return_base64=True but mypy doesn't know that
165 assert isinstance(base64_pdf_image, str)
166 image_contents[doc_idx] = ImageContent(
167 base64_image=base64_pdf_image, mime_type="image/jpeg", detail=self.detail, meta=meta
168 )
170 none_image_contents_doc_ids = [
171 documents[doc_idx].id for doc_idx, image_content in enumerate(image_contents) if image_content is None
172 ]
173 if none_image_contents_doc_ids:
174 logger.warning(
175 "Conversion failed for some documents. Their output will be None. Document IDs: {document_ids}",
176 document_ids=none_image_contents_doc_ids,
177 )
179 return {"image_contents": image_contents}