33import asyncio
44import json
55import logging
6+ from pathlib import Path
67from typing import TYPE_CHECKING
78
89import aiofiles
2425 ElementNotFound ,
2526 ElementNotInteractable ,
2627 ElementNotVisible ,
28+ InvalidFileExtension ,
29+ MissingScreenshotPath ,
2730 WaitElementTimeout ,
2831)
2932from pydoll .protocol .input .types import (
@@ -226,12 +229,48 @@ async def get_siblings_elements(
226229 logger .debug (f'Siblings found: { len (siblings )} ' )
227230 return siblings
228231
229- async def take_screenshot (self , path : str , quality : int = 100 ):
232+ async def take_screenshot (
233+ self ,
234+ path : Optional [str | Path ] = None ,
235+ quality : int = 100 ,
236+ as_base64 : bool = False ,
237+ ) -> Optional [str ]:
230238 """
231239 Capture screenshot of this element only.
232240
233241 Automatically scrolls element into view before capturing.
242+
243+ Args:
244+ path: File path for screenshot (extension determines format).
245+ quality: Image quality 0-100 (default 100).
246+ as_base64: Return as base64 string instead of saving file.
247+
248+ Returns:
249+ Base64 screenshot data if as_base64=True, None otherwise.
250+
251+ Raises:
252+ InvalidFileExtension: If file extension not supported.
253+ MissingScreenshotPath: If path is None and as_base64 is False.
234254 """
255+ if not path and not as_base64 :
256+ raise MissingScreenshotPath ()
257+
258+ if path and isinstance (path , str ):
259+ output_extension = path .split ('.' )[- 1 ]
260+ elif path and isinstance (path , Path ):
261+ output_extension = path .suffix .lstrip ('.' )
262+ else :
263+ output_extension = ScreenshotFormat .JPEG
264+
265+ # Normalize jpg to jpeg (CDP only accepts jpeg)
266+ if output_extension == 'jpg' :
267+ output_extension = 'jpeg'
268+
269+ if not ScreenshotFormat .has_value (output_extension ):
270+ raise InvalidFileExtension (f'{ output_extension } extension is not supported.' )
271+
272+ file_format = ScreenshotFormat .get_value (output_extension )
273+
235274 bounds = await self .get_bounds_using_js ()
236275 clip = Viewport (
237276 x = bounds ['x' ],
@@ -241,18 +280,29 @@ async def take_screenshot(self, path: str, quality: int = 100):
241280 scale = 1 ,
242281 )
243282 logger .debug (
244- f'Taking element screenshot: path={ path } , quality={ quality } , '
283+ f'Taking element screenshot: path={ path } , quality={ quality } , as_base64= { as_base64 } , '
245284 f'clip={{x: { clip ["x" ]} , y: { clip ["y" ]} , w: { clip ["width" ]} , h: { clip ["height" ]} }}'
246285 )
286+
247287 screenshot : CaptureScreenshotResponse = await self ._connection_handler .execute_command (
248288 PageCommands .capture_screenshot (
249- format = ScreenshotFormat . JPEG , clip = clip , quality = quality
289+ format = file_format , clip = clip , quality = quality
250290 )
251291 )
252- async with aiofiles .open (path , 'wb' ) as file :
253- image_bytes = decode_base64_to_bytes (screenshot ['result' ]['data' ])
254- await file .write (image_bytes )
255- logger .info (f'Element screenshot saved: { path } ' )
292+
293+ screenshot_data = screenshot ['result' ]['data' ]
294+
295+ if as_base64 :
296+ logger .info ('Element screenshot captured and returned as base64' )
297+ return screenshot_data
298+
299+ if path :
300+ image_bytes = decode_base64_to_bytes (screenshot_data )
301+ async with aiofiles .open (str (path ), 'wb' ) as file :
302+ await file .write (image_bytes )
303+ logger .info (f'Element screenshot saved: { path } ' )
304+
305+ return None
256306
257307 def get_attribute (self , name : str ) -> Optional [str ]:
258308 """
@@ -417,7 +467,7 @@ async def insert_text(self, text: str):
417467 logger .info (f'Inserting text on element (length={ len (text )} )' )
418468 await self ._execute_command (InputCommands .insert_text (text ))
419469
420- async def set_input_files (self , files : list [str ]):
470+ async def set_input_files (self , files : str | Path | list [str | Path ]):
421471 """
422472 Set file paths for file input element.
423473
@@ -432,9 +482,10 @@ async def set_input_files(self, files: list[str]):
432482 or self ._attributes .get ('type' , '' ).lower () != 'file'
433483 ):
434484 raise ElementNotAFileInput ()
435- logger .info (f'Setting input files: count={ len (files )} ' )
485+ files_list = [str (file ) for file in files ] if isinstance (files , list ) else [str (files )]
486+ logger .info (f'Setting input files: count={ len (files_list )} ' )
436487 await self ._execute_command (
437- DomCommands .set_file_input_files (files = files , object_id = self ._object_id )
488+ DomCommands .set_file_input_files (files = files_list , object_id = self ._object_id )
438489 )
439490
440491 async def type_text (self , text : str , interval : float = 0.1 ):
0 commit comments