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

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

123 

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 ) 

127 

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

129 

130 pdf_page_infos: list[_PDFPageInfo] = [] 

131 

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 ) 

151 

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 ) 

169 

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 ) 

178 

179 return {"image_contents": image_contents}