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

1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4 

5from typing import Literal 

6 

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 

18 

19logger = logging.getLogger(__name__) 

20 

21 

22@component 

23class DocumentToImageContent: 

24 """ 

25 Converts documents sourced from PDF and image files into ImageContents. 

26 

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. 

30 

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 

35 

36 ### Usage example 

37 

38 ```python 

39 from haystack import Document 

40 from haystack.components.converters.image.document_to_image import DocumentToImageContent 

41 

42 converter = DocumentToImageContent( 

43 file_path_meta_field="file_path", 

44 root_path="test/test_files", 

45 detail="high", 

46 size=(800, 600) 

47 ) 

48 

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 ] 

56 

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 """ 

68 

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. 

79 

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() 

95 

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 

100 

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. 

105 

106 This method processes the input documents, extracting images from supported file formats and converting them 

107 into ImageContent objects. 

108 

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. 

112 

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": []} 

122 

123 image_contents: list[ImageContent | None] = [None] * len(documents) 

124 

125 pdf_page_infos: list[_PDFPageInfo] = [] 

126 

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 

139 

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 ) 

158 

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 ) 

176 

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 ) 

185 

186 return {"image_contents": image_contents}