ImageShow.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. #
  2. # The Python Imaging Library.
  3. # $Id$
  4. #
  5. # im.show() drivers
  6. #
  7. # History:
  8. # 2008-04-06 fl Created
  9. #
  10. # Copyright (c) Secret Labs AB 2008.
  11. #
  12. # See the README file for information on usage and redistribution.
  13. #
  14. from __future__ import annotations
  15. import abc
  16. import os
  17. import shutil
  18. import subprocess
  19. import sys
  20. from shlex import quote
  21. from typing import Any
  22. from . import Image
  23. _viewers = []
  24. def register(viewer, order: int = 1) -> None:
  25. """
  26. The :py:func:`register` function is used to register additional viewers::
  27. from PIL import ImageShow
  28. ImageShow.register(MyViewer()) # MyViewer will be used as a last resort
  29. ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised
  30. ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised
  31. :param viewer: The viewer to be registered.
  32. :param order:
  33. Zero or a negative integer to prepend this viewer to the list,
  34. a positive integer to append it.
  35. """
  36. try:
  37. if issubclass(viewer, Viewer):
  38. viewer = viewer()
  39. except TypeError:
  40. pass # raised if viewer wasn't a class
  41. if order > 0:
  42. _viewers.append(viewer)
  43. else:
  44. _viewers.insert(0, viewer)
  45. def show(image: Image.Image, title: str | None = None, **options: Any) -> bool:
  46. r"""
  47. Display a given image.
  48. :param image: An image object.
  49. :param title: Optional title. Not all viewers can display the title.
  50. :param \**options: Additional viewer options.
  51. :returns: ``True`` if a suitable viewer was found, ``False`` otherwise.
  52. """
  53. for viewer in _viewers:
  54. if viewer.show(image, title=title, **options):
  55. return True
  56. return False
  57. class Viewer:
  58. """Base class for viewers."""
  59. # main api
  60. def show(self, image: Image.Image, **options: Any) -> int:
  61. """
  62. The main function for displaying an image.
  63. Converts the given image to the target format and displays it.
  64. """
  65. if not (
  66. image.mode in ("1", "RGBA")
  67. or (self.format == "PNG" and image.mode in ("I;16", "LA"))
  68. ):
  69. base = Image.getmodebase(image.mode)
  70. if image.mode != base:
  71. image = image.convert(base)
  72. return self.show_image(image, **options)
  73. # hook methods
  74. format: str | None = None
  75. """The format to convert the image into."""
  76. options: dict[str, Any] = {}
  77. """Additional options used to convert the image."""
  78. def get_format(self, image: Image.Image) -> str | None:
  79. """Return format name, or ``None`` to save as PGM/PPM."""
  80. return self.format
  81. def get_command(self, file: str, **options: Any) -> str:
  82. """
  83. Returns the command used to display the file.
  84. Not implemented in the base class.
  85. """
  86. msg = "unavailable in base viewer"
  87. raise NotImplementedError(msg)
  88. def save_image(self, image: Image.Image) -> str:
  89. """Save to temporary file and return filename."""
  90. return image._dump(format=self.get_format(image), **self.options)
  91. def show_image(self, image: Image.Image, **options: Any) -> int:
  92. """Display the given image."""
  93. return self.show_file(self.save_image(image), **options)
  94. def show_file(self, path: str, **options: Any) -> int:
  95. """
  96. Display given file.
  97. """
  98. if not os.path.exists(path):
  99. raise FileNotFoundError
  100. os.system(self.get_command(path, **options)) # nosec
  101. return 1
  102. # --------------------------------------------------------------------
  103. class WindowsViewer(Viewer):
  104. """The default viewer on Windows is the default system application for PNG files."""
  105. format = "PNG"
  106. options = {"compress_level": 1, "save_all": True}
  107. def get_command(self, file: str, **options: Any) -> str:
  108. return (
  109. f'start "Pillow" /WAIT "{file}" '
  110. "&& ping -n 4 127.0.0.1 >NUL "
  111. f'&& del /f "{file}"'
  112. )
  113. def show_file(self, path: str, **options: Any) -> int:
  114. """
  115. Display given file.
  116. """
  117. if not os.path.exists(path):
  118. raise FileNotFoundError
  119. subprocess.Popen(
  120. self.get_command(path, **options),
  121. shell=True,
  122. creationflags=getattr(subprocess, "CREATE_NO_WINDOW"),
  123. ) # nosec
  124. return 1
  125. if sys.platform == "win32":
  126. register(WindowsViewer)
  127. class MacViewer(Viewer):
  128. """The default viewer on macOS using ``Preview.app``."""
  129. format = "PNG"
  130. options = {"compress_level": 1, "save_all": True}
  131. def get_command(self, file: str, **options: Any) -> str:
  132. # on darwin open returns immediately resulting in the temp
  133. # file removal while app is opening
  134. command = "open -a Preview.app"
  135. command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&"
  136. return command
  137. def show_file(self, path: str, **options: Any) -> int:
  138. """
  139. Display given file.
  140. """
  141. if not os.path.exists(path):
  142. raise FileNotFoundError
  143. subprocess.call(["open", "-a", "Preview.app", path])
  144. executable = sys.executable or shutil.which("python3")
  145. if executable:
  146. subprocess.Popen(
  147. [
  148. executable,
  149. "-c",
  150. "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])",
  151. path,
  152. ]
  153. )
  154. return 1
  155. if sys.platform == "darwin":
  156. register(MacViewer)
  157. class UnixViewer(Viewer):
  158. format = "PNG"
  159. options = {"compress_level": 1, "save_all": True}
  160. @abc.abstractmethod
  161. def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
  162. pass
  163. def get_command(self, file: str, **options: Any) -> str:
  164. command = self.get_command_ex(file, **options)[0]
  165. return f"{command} {quote(file)}"
  166. class XDGViewer(UnixViewer):
  167. """
  168. The freedesktop.org ``xdg-open`` command.
  169. """
  170. def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
  171. command = executable = "xdg-open"
  172. return command, executable
  173. def show_file(self, path: str, **options: Any) -> int:
  174. """
  175. Display given file.
  176. """
  177. if not os.path.exists(path):
  178. raise FileNotFoundError
  179. subprocess.Popen(["xdg-open", path])
  180. return 1
  181. class DisplayViewer(UnixViewer):
  182. """
  183. The ImageMagick ``display`` command.
  184. This viewer supports the ``title`` parameter.
  185. """
  186. def get_command_ex(
  187. self, file: str, title: str | None = None, **options: Any
  188. ) -> tuple[str, str]:
  189. command = executable = "display"
  190. if title:
  191. command += f" -title {quote(title)}"
  192. return command, executable
  193. def show_file(self, path: str, **options: Any) -> int:
  194. """
  195. Display given file.
  196. """
  197. if not os.path.exists(path):
  198. raise FileNotFoundError
  199. args = ["display"]
  200. title = options.get("title")
  201. if title:
  202. args += ["-title", title]
  203. args.append(path)
  204. subprocess.Popen(args)
  205. return 1
  206. class GmDisplayViewer(UnixViewer):
  207. """The GraphicsMagick ``gm display`` command."""
  208. def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
  209. executable = "gm"
  210. command = "gm display"
  211. return command, executable
  212. def show_file(self, path: str, **options: Any) -> int:
  213. """
  214. Display given file.
  215. """
  216. if not os.path.exists(path):
  217. raise FileNotFoundError
  218. subprocess.Popen(["gm", "display", path])
  219. return 1
  220. class EogViewer(UnixViewer):
  221. """The GNOME Image Viewer ``eog`` command."""
  222. def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]:
  223. executable = "eog"
  224. command = "eog -n"
  225. return command, executable
  226. def show_file(self, path: str, **options: Any) -> int:
  227. """
  228. Display given file.
  229. """
  230. if not os.path.exists(path):
  231. raise FileNotFoundError
  232. subprocess.Popen(["eog", "-n", path])
  233. return 1
  234. class XVViewer(UnixViewer):
  235. """
  236. The X Viewer ``xv`` command.
  237. This viewer supports the ``title`` parameter.
  238. """
  239. def get_command_ex(
  240. self, file: str, title: str | None = None, **options: Any
  241. ) -> tuple[str, str]:
  242. # note: xv is pretty outdated. most modern systems have
  243. # imagemagick's display command instead.
  244. command = executable = "xv"
  245. if title:
  246. command += f" -name {quote(title)}"
  247. return command, executable
  248. def show_file(self, path: str, **options: Any) -> int:
  249. """
  250. Display given file.
  251. """
  252. if not os.path.exists(path):
  253. raise FileNotFoundError
  254. args = ["xv"]
  255. title = options.get("title")
  256. if title:
  257. args += ["-name", title]
  258. args.append(path)
  259. subprocess.Popen(args)
  260. return 1
  261. if sys.platform not in ("win32", "darwin"): # unixoids
  262. if shutil.which("xdg-open"):
  263. register(XDGViewer)
  264. if shutil.which("display"):
  265. register(DisplayViewer)
  266. if shutil.which("gm"):
  267. register(GmDisplayViewer)
  268. if shutil.which("eog"):
  269. register(EogViewer)
  270. if shutil.which("xv"):
  271. register(XVViewer)
  272. class IPythonViewer(Viewer):
  273. """The viewer for IPython frontends."""
  274. def show_image(self, image: Image.Image, **options: Any) -> int:
  275. ipython_display(image)
  276. return 1
  277. try:
  278. from IPython.display import display as ipython_display
  279. except ImportError:
  280. pass
  281. else:
  282. register(IPythonViewer)
  283. if __name__ == "__main__":
  284. if len(sys.argv) < 2:
  285. print("Syntax: python3 ImageShow.py imagefile [title]")
  286. sys.exit()
  287. with Image.open(sys.argv[1]) as im:
  288. print(show(im, *sys.argv[2:]))