Coverage for haystack/components/converters/image/document_to_image.py: 100%
47 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 09:42 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 09:42 +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 the order of the input documents. A document that is
117 missing the required metadata keys, has an invalid file path, has an unsupported MIME type, or points
118 to a PDF page that cannot be converted gets None in its position and a logged warning with the reason.
119 """
120 if not documents:
121 return {"image_contents": []}
123 image_contents: list[ImageContent | None] = [None] * len(documents)
125 pdf_page_infos: list[_PDFPageInfo] = []
127 for doc_idx, document in enumerate(documents):
128 # Validate each document on its own so one invalid document leaves None in its slot
129 # instead of failing the whole batch
130 try:
131 image_source_info = _extract_image_sources_info(
132 documents=[document], file_path_meta_field=self.file_path_meta_field, root_path=self.root_path
133 )[0]
134 except ValueError as error:
135 logger.warning(
136 "Skipping document with ID {document_id}: {error}", document_id=document.id, error=str(error)
137 )
138 continue
140 mime_type = image_source_info["mime_type"]
141 path = image_source_info["path"]
142 if mime_type == "application/pdf":
143 # Store PDF documents for later processing
144 page_number = image_source_info.get("page_number")
145 assert page_number is not None # checked in _extract_image_sources_info but mypy doesn't know that
146 pdf_page_info: _PDFPageInfo = {"doc_idx": doc_idx, "path": path, "page_number": page_number}
147 pdf_page_infos.append(pdf_page_info)
148 else:
149 # Process images directly
150 bytestream = ByteStream.from_file_path(filepath=path, mime_type=mime_type)
151 _, base64_image = _encode_image_to_base64(bytestream=bytestream, size=self.size)
152 image_contents[doc_idx] = ImageContent(
153 base64_image=base64_image,
154 mime_type=mime_type,
155 detail=self.detail,
156 meta={"file_path": document.meta[self.file_path_meta_field]},
157 )
159 # efficiently convert PDF pages to images: each PDF is opened and processed only once
160 pdf_page_infos_by_doc_idx: dict[int, _PDFPageInfo] = {
161 pdf_page_info["doc_idx"]: pdf_page_info for pdf_page_info in pdf_page_infos
162 }
163 pdf_images_by_doc_idx = _batch_convert_pdf_pages_to_images(
164 pdf_page_infos=pdf_page_infos, size=self.size, return_base64=True
165 )
166 for doc_idx, base64_pdf_image in pdf_images_by_doc_idx.items():
167 meta = {
168 "file_path": documents[doc_idx].meta[self.file_path_meta_field],
169 "page_number": pdf_page_infos_by_doc_idx[doc_idx]["page_number"],
170 }
171 # we know that base64_pdf_image is a string because we set return_base64=True but mypy doesn't know that
172 assert isinstance(base64_pdf_image, str)
173 image_contents[doc_idx] = ImageContent(
174 base64_image=base64_pdf_image, mime_type="image/jpeg", detail=self.detail, meta=meta
175 )
177 none_image_contents_doc_ids = [
178 documents[doc_idx].id for doc_idx, image_content in enumerate(image_contents) if image_content is None
179 ]
180 if none_image_contents_doc_ids:
181 logger.warning(
182 "Conversion failed for some documents. Their output will be None. Document IDs: {document_ids}",
183 document_ids=none_image_contents_doc_ids,
184 )
186 return {"image_contents": image_contents}