ImageGrab.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. #
  2. # The Python Imaging Library
  3. # $Id$
  4. #
  5. # screen grabber
  6. #
  7. # History:
  8. # 2001-04-26 fl created
  9. # 2001-09-17 fl use builtin driver, if present
  10. # 2002-11-19 fl added grabclipboard support
  11. #
  12. # Copyright (c) 2001-2002 by Secret Labs AB
  13. # Copyright (c) 2001-2002 by Fredrik Lundh
  14. #
  15. # See the README file for information on usage and redistribution.
  16. #
  17. from __future__ import annotations
  18. import io
  19. import os
  20. import shutil
  21. import subprocess
  22. import sys
  23. import tempfile
  24. from . import Image
  25. TYPE_CHECKING = False
  26. if TYPE_CHECKING:
  27. from . import ImageWin
  28. def grab(
  29. bbox: tuple[int, int, int, int] | None = None,
  30. include_layered_windows: bool = False,
  31. all_screens: bool = False,
  32. xdisplay: str | None = None,
  33. window: int | ImageWin.HWND | None = None,
  34. ) -> Image.Image:
  35. im: Image.Image
  36. if xdisplay is None:
  37. if sys.platform == "darwin":
  38. fh, filepath = tempfile.mkstemp(".png")
  39. os.close(fh)
  40. args = ["screencapture"]
  41. if window is not None:
  42. args += ["-l", str(window)]
  43. elif bbox:
  44. left, top, right, bottom = bbox
  45. args += ["-R", f"{left},{top},{right-left},{bottom-top}"]
  46. args += ["-x", filepath]
  47. retcode = subprocess.call(args)
  48. if retcode:
  49. raise subprocess.CalledProcessError(retcode, args)
  50. im = Image.open(filepath)
  51. im.load()
  52. os.unlink(filepath)
  53. if bbox:
  54. if window is not None:
  55. # Determine if the window was in Retina mode or not
  56. # by capturing it without the shadow,
  57. # and checking how different the width is
  58. fh, filepath = tempfile.mkstemp(".png")
  59. os.close(fh)
  60. args = ["screencapture", "-l", str(window), "-o", "-x", filepath]
  61. retcode = subprocess.call(args)
  62. if retcode:
  63. raise subprocess.CalledProcessError(retcode, args)
  64. with Image.open(filepath) as im_no_shadow:
  65. retina = im.width - im_no_shadow.width > 100
  66. os.unlink(filepath)
  67. # Since screencapture's -R does not work with -l,
  68. # crop the image manually
  69. if retina:
  70. left, top, right, bottom = bbox
  71. im_cropped = im.resize(
  72. (right - left, bottom - top),
  73. box=tuple(coord * 2 for coord in bbox),
  74. )
  75. else:
  76. im_cropped = im.crop(bbox)
  77. im.close()
  78. return im_cropped
  79. else:
  80. im_resized = im.resize((right - left, bottom - top))
  81. im.close()
  82. return im_resized
  83. return im
  84. elif sys.platform == "win32":
  85. if window is not None:
  86. all_screens = -1
  87. offset, size, data = Image.core.grabscreen_win32(
  88. include_layered_windows,
  89. all_screens,
  90. int(window) if window is not None else 0,
  91. )
  92. im = Image.frombytes(
  93. "RGB",
  94. size,
  95. data,
  96. # RGB, 32-bit line padding, origin lower left corner
  97. "raw",
  98. "BGR",
  99. (size[0] * 3 + 3) & -4,
  100. -1,
  101. )
  102. if bbox:
  103. x0, y0 = offset
  104. left, top, right, bottom = bbox
  105. im = im.crop((left - x0, top - y0, right - x0, bottom - y0))
  106. return im
  107. # Cast to Optional[str] needed for Windows and macOS.
  108. display_name: str | None = xdisplay
  109. try:
  110. if not Image.core.HAVE_XCB:
  111. msg = "Pillow was built without XCB support"
  112. raise OSError(msg)
  113. size, data = Image.core.grabscreen_x11(display_name)
  114. except OSError:
  115. if display_name is None and sys.platform not in ("darwin", "win32"):
  116. if shutil.which("gnome-screenshot"):
  117. args = ["gnome-screenshot", "-f"]
  118. elif shutil.which("grim"):
  119. args = ["grim"]
  120. elif shutil.which("spectacle"):
  121. args = ["spectacle", "-n", "-b", "-f", "-o"]
  122. else:
  123. raise
  124. fh, filepath = tempfile.mkstemp(".png")
  125. os.close(fh)
  126. args.append(filepath)
  127. retcode = subprocess.call(args)
  128. if retcode:
  129. raise subprocess.CalledProcessError(retcode, args)
  130. im = Image.open(filepath)
  131. im.load()
  132. os.unlink(filepath)
  133. if bbox:
  134. im_cropped = im.crop(bbox)
  135. im.close()
  136. return im_cropped
  137. return im
  138. else:
  139. raise
  140. else:
  141. im = Image.frombytes("RGB", size, data, "raw", "BGRX", size[0] * 4, 1)
  142. if bbox:
  143. im = im.crop(bbox)
  144. return im
  145. def grabclipboard() -> Image.Image | list[str] | None:
  146. if sys.platform == "darwin":
  147. p = subprocess.run(
  148. ["osascript", "-e", "get the clipboard as «class PNGf»"],
  149. capture_output=True,
  150. )
  151. if p.returncode != 0:
  152. return None
  153. import binascii
  154. data = io.BytesIO(binascii.unhexlify(p.stdout[11:-3]))
  155. return Image.open(data)
  156. elif sys.platform == "win32":
  157. fmt, data = Image.core.grabclipboard_win32()
  158. if fmt == "file": # CF_HDROP
  159. import struct
  160. o = struct.unpack_from("I", data)[0]
  161. if data[16] == 0:
  162. files = data[o:].decode("mbcs").split("\0")
  163. else:
  164. files = data[o:].decode("utf-16le").split("\0")
  165. return files[: files.index("")]
  166. if isinstance(data, bytes):
  167. data = io.BytesIO(data)
  168. if fmt == "png":
  169. from . import PngImagePlugin
  170. return PngImagePlugin.PngImageFile(data)
  171. elif fmt == "DIB":
  172. from . import BmpImagePlugin
  173. return BmpImagePlugin.DibImageFile(data)
  174. return None
  175. else:
  176. if os.getenv("WAYLAND_DISPLAY"):
  177. session_type = "wayland"
  178. elif os.getenv("DISPLAY"):
  179. session_type = "x11"
  180. else: # Session type check failed
  181. session_type = None
  182. if shutil.which("wl-paste") and session_type in ("wayland", None):
  183. args = ["wl-paste", "-t", "image"]
  184. elif shutil.which("xclip") and session_type in ("x11", None):
  185. args = ["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]
  186. else:
  187. msg = "wl-paste or xclip is required for ImageGrab.grabclipboard() on Linux"
  188. raise NotImplementedError(msg)
  189. p = subprocess.run(args, capture_output=True)
  190. if p.returncode != 0:
  191. err = p.stderr
  192. for silent_error in [
  193. # wl-paste, when the clipboard is empty
  194. b"Nothing is copied",
  195. # Ubuntu/Debian wl-paste, when the clipboard is empty
  196. b"No selection",
  197. # Ubuntu/Debian wl-paste, when an image isn't available
  198. b"No suitable type of content copied",
  199. # wl-paste or Ubuntu/Debian xclip, when an image isn't available
  200. b" not available",
  201. # xclip, when an image isn't available
  202. b"cannot convert ",
  203. # xclip, when the clipboard isn't initialized
  204. b"xclip: Error: There is no owner for the ",
  205. ]:
  206. if silent_error in err:
  207. return None
  208. msg = f"{args[0]} error"
  209. if err:
  210. msg += f": {err.strip().decode()}"
  211. raise ChildProcessError(msg)
  212. data = io.BytesIO(p.stdout)
  213. im = Image.open(data)
  214. im.load()
  215. return im