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