1- """Headless cross-platform text clipboard.
1+ """Headless cross-platform text + image clipboard.
22
3- Windows uses Win32 clipboard API via ctypes.
4- macOS shells out to pbcopy / pbpaste.
5- Linux shells out to xclip or xsel (whichever is available).
3+ Windows uses Win32 clipboard API via ctypes (CF_UNICODETEXT for text,
4+ CF_DIB for image).
5+ macOS shells out to pbcopy / pbpaste for text; image support requires
6+ PyObjC and is best effort.
7+ Linux shells out to xclip / xsel for text and ``xclip -t image/png`` for
8+ images.
69
710All functions raise ``RuntimeError`` if the platform backend is missing so
811callers can degrade gracefully.
912"""
1013import shutil
1114import subprocess # nosec B404 # reason: required for pbcopy/pbpaste/xclip/xsel
1215import sys
16+ from io import BytesIO
1317from typing import Optional
1418
1519
@@ -35,6 +39,30 @@ def set_clipboard(text: str) -> None:
3539 _linux_set (text )
3640
3741
42+ def get_clipboard_image () -> Optional [bytes ]:
43+ """Return the clipboard's image as PNG bytes, or ``None`` if no image."""
44+ if sys .platform .startswith ("win" ):
45+ return _win_get_image ()
46+ if sys .platform == "darwin" :
47+ return _mac_get_image ()
48+ return _linux_get_image ()
49+
50+
51+ def set_clipboard_image (png_bytes : bytes ) -> None :
52+ """Place a PNG image (as bytes) onto the clipboard."""
53+ if not isinstance (png_bytes , (bytes , bytearray )):
54+ raise TypeError ("set_clipboard_image expects bytes" )
55+ if not png_bytes :
56+ raise ValueError ("png_bytes is empty" )
57+ if sys .platform .startswith ("win" ):
58+ _win_set_image (bytes (png_bytes ))
59+ return
60+ if sys .platform == "darwin" :
61+ _mac_set_image (bytes (png_bytes ))
62+ return
63+ _linux_set_image (bytes (png_bytes ))
64+
65+
3866# === Windows backend =========================================================
3967
4068def _win_get () -> str :
@@ -160,3 +188,125 @@ def _linux_set(text: str) -> None:
160188 write_cmd , input = text .encode ("utf-8" ),
161189 check = True , timeout = 5 ,
162190 )
191+
192+
193+ # === Image clipboard backends ===============================================
194+
195+
196+ def _win_get_image () -> Optional [bytes ]:
197+ """Return the Windows clipboard image as PNG bytes, or None."""
198+ try :
199+ from PIL import ImageGrab # noqa: PLC0415 lazy import
200+ except ImportError as error :
201+ raise RuntimeError (
202+ "Pillow is required for clipboard image support"
203+ ) from error
204+ image = ImageGrab .grabclipboard ()
205+ if image is None or isinstance (image , list ):
206+ return None
207+ buffer = BytesIO ()
208+ if image .mode != "RGB" :
209+ image = image .convert ("RGB" )
210+ image .save (buffer , format = "PNG" )
211+ return buffer .getvalue ()
212+
213+
214+ def _win_set_image (png_bytes : bytes ) -> None :
215+ """Set the Windows clipboard image from PNG bytes (CF_DIB)."""
216+ try :
217+ from PIL import Image # noqa: PLC0415 lazy import
218+ except ImportError as error :
219+ raise RuntimeError (
220+ "Pillow is required for clipboard image support"
221+ ) from error
222+ image = Image .open (BytesIO (png_bytes ))
223+ if image .mode != "RGB" :
224+ image = image .convert ("RGB" )
225+ bmp_buf = BytesIO ()
226+ image .save (bmp_buf , format = "BMP" )
227+ # CF_DIB excludes the 14-byte BITMAPFILEHEADER prefix that BMP files use.
228+ dib = bmp_buf .getvalue ()[14 :]
229+
230+ import ctypes # noqa: PLC0415
231+ from ctypes import wintypes # noqa: PLC0415
232+
233+ user32 = ctypes .WinDLL ("user32" , use_last_error = True )
234+ kernel32 = ctypes .WinDLL ("kernel32" , use_last_error = True )
235+ cf_dib = 8
236+ gmem_moveable = 0x0002
237+
238+ user32 .OpenClipboard .argtypes = [wintypes .HWND ]
239+ user32 .OpenClipboard .restype = wintypes .BOOL
240+ user32 .EmptyClipboard .restype = wintypes .BOOL
241+ user32 .SetClipboardData .argtypes = [wintypes .UINT , wintypes .HANDLE ]
242+ user32 .SetClipboardData .restype = wintypes .HANDLE
243+ user32 .CloseClipboard .restype = wintypes .BOOL
244+ kernel32 .GlobalAlloc .argtypes = [wintypes .UINT , ctypes .c_size_t ]
245+ kernel32 .GlobalAlloc .restype = wintypes .HGLOBAL
246+ kernel32 .GlobalLock .argtypes = [wintypes .HGLOBAL ]
247+ kernel32 .GlobalLock .restype = ctypes .c_void_p
248+ kernel32 .GlobalUnlock .argtypes = [wintypes .HGLOBAL ]
249+
250+ handle = kernel32 .GlobalAlloc (gmem_moveable , len (dib ))
251+ if not handle :
252+ raise RuntimeError ("GlobalAlloc failed" )
253+ pointer = kernel32 .GlobalLock (handle )
254+ if not pointer :
255+ raise RuntimeError ("GlobalLock failed" )
256+ ctypes .memmove (pointer , dib , len (dib ))
257+ kernel32 .GlobalUnlock (handle )
258+ if not user32 .OpenClipboard (None ):
259+ raise RuntimeError ("OpenClipboard failed" )
260+ try :
261+ user32 .EmptyClipboard ()
262+ if not user32 .SetClipboardData (cf_dib , handle ):
263+ raise RuntimeError ("SetClipboardData(CF_DIB) failed" )
264+ finally :
265+ user32 .CloseClipboard ()
266+
267+
268+ def _mac_get_image () -> Optional [bytes ]:
269+ """Read clipboard image via Pillow's ImageGrab; raises if PIL missing."""
270+ try :
271+ from PIL import ImageGrab # noqa: PLC0415
272+ except ImportError as error :
273+ raise RuntimeError (
274+ "Pillow is required for clipboard image support on macOS"
275+ ) from error
276+ image = ImageGrab .grabclipboard ()
277+ if image is None or isinstance (image , list ):
278+ return None
279+ buffer = BytesIO ()
280+ if image .mode != "RGB" :
281+ image = image .convert ("RGB" )
282+ image .save (buffer , format = "PNG" )
283+ return buffer .getvalue ()
284+
285+
286+ def _mac_set_image (_png_bytes : bytes ) -> None :
287+ raise RuntimeError (
288+ "Setting clipboard images on macOS requires PyObjC; not yet supported"
289+ )
290+
291+
292+ def _linux_get_image () -> Optional [bytes ]:
293+ if not shutil .which ("xclip" ):
294+ raise RuntimeError ("Install xclip for Linux clipboard image support" )
295+ # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit.dangerous-subprocess-use-audit
296+ result = subprocess .run ( # nosec B603 B607 # reason: hard-coded argv to xclip
297+ ["xclip" , "-selection" , "clipboard" , "-t" , "image/png" , "-o" ],
298+ capture_output = True , check = False , timeout = 5 ,
299+ )
300+ if result .returncode != 0 or not result .stdout :
301+ return None
302+ return result .stdout
303+
304+
305+ def _linux_set_image (png_bytes : bytes ) -> None :
306+ if not shutil .which ("xclip" ):
307+ raise RuntimeError ("Install xclip for Linux clipboard image support" )
308+ # nosemgrep: python.lang.security.audit.dangerous-subprocess-use-audit.dangerous-subprocess-use-audit
309+ subprocess .run ( # nosec B603 B607 # reason: hard-coded argv to xclip
310+ ["xclip" , "-selection" , "clipboard" , "-t" , "image/png" , "-i" ],
311+ input = png_bytes , check = True , timeout = 5 ,
312+ )
0 commit comments