| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| from __future__ import annotations |
|
|
| import abc |
| import os |
| import shutil |
| import subprocess |
| import sys |
| from shlex import quote |
| from typing import Any |
|
|
| from . import Image |
|
|
| _viewers = [] |
|
|
|
|
| def register(viewer: type[Viewer] | Viewer, order: int = 1) -> None: |
| """ |
| The :py:func:`register` function is used to register additional viewers:: |
| |
| from PIL import ImageShow |
| ImageShow.register(MyViewer()) # MyViewer will be used as a last resort |
| ImageShow.register(MySecondViewer(), 0) # MySecondViewer will be prioritised |
| ImageShow.register(ImageShow.XVViewer(), 0) # XVViewer will be prioritised |
| |
| :param viewer: The viewer to be registered. |
| :param order: |
| Zero or a negative integer to prepend this viewer to the list, |
| a positive integer to append it. |
| """ |
| if isinstance(viewer, type) and issubclass(viewer, Viewer): |
| viewer = viewer() |
| if order > 0: |
| _viewers.append(viewer) |
| else: |
| _viewers.insert(0, viewer) |
|
|
|
|
| def show(image: Image.Image, title: str | None = None, **options: Any) -> bool: |
| r""" |
| Display a given image. |
| |
| :param image: An image object. |
| :param title: Optional title. Not all viewers can display the title. |
| :param \**options: Additional viewer options. |
| :returns: ``True`` if a suitable viewer was found, ``False`` otherwise. |
| """ |
| for viewer in _viewers: |
| if viewer.show(image, title=title, **options): |
| return True |
| return False |
|
|
|
|
| class Viewer: |
| """Base class for viewers.""" |
|
|
| |
|
|
| def show(self, image: Image.Image, **options: Any) -> int: |
| """ |
| The main function for displaying an image. |
| Converts the given image to the target format and displays it. |
| """ |
|
|
| if not ( |
| image.mode in ("1", "RGBA") |
| or (self.format == "PNG" and image.mode in ("I;16", "LA")) |
| ): |
| base = Image.getmodebase(image.mode) |
| if image.mode != base: |
| image = image.convert(base) |
|
|
| return self.show_image(image, **options) |
|
|
| |
|
|
| format: str | None = None |
| """The format to convert the image into.""" |
| options: dict[str, Any] = {} |
| """Additional options used to convert the image.""" |
|
|
| def get_format(self, image: Image.Image) -> str | None: |
| """Return format name, or ``None`` to save as PGM/PPM.""" |
| return self.format |
|
|
| def get_command(self, file: str, **options: Any) -> str: |
| """ |
| Returns the command used to display the file. |
| Not implemented in the base class. |
| """ |
| msg = "unavailable in base viewer" |
| raise NotImplementedError(msg) |
|
|
| def save_image(self, image: Image.Image) -> str: |
| """Save to temporary file and return filename.""" |
| return image._dump(format=self.get_format(image), **self.options) |
|
|
| def show_image(self, image: Image.Image, **options: Any) -> int: |
| """Display the given image.""" |
| return self.show_file(self.save_image(image), **options) |
|
|
| def show_file(self, path: str, **options: Any) -> int: |
| """ |
| Display given file. |
| """ |
| if not os.path.exists(path): |
| raise FileNotFoundError |
| os.system(self.get_command(path, **options)) |
| return 1 |
|
|
|
|
| |
|
|
|
|
| class WindowsViewer(Viewer): |
| """The default viewer on Windows is the default system application for PNG files.""" |
|
|
| format = "PNG" |
| options = {"compress_level": 1, "save_all": True} |
|
|
| def get_command(self, file: str, **options: Any) -> str: |
| return ( |
| f'start "Pillow" /WAIT "{file}" ' |
| "&& ping -n 4 127.0.0.1 >NUL " |
| f'&& del /f "{file}"' |
| ) |
|
|
| def show_file(self, path: str, **options: Any) -> int: |
| """ |
| Display given file. |
| """ |
| if not os.path.exists(path): |
| raise FileNotFoundError |
| subprocess.Popen( |
| self.get_command(path, **options), |
| shell=True, |
| creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), |
| ) |
| return 1 |
|
|
|
|
| if sys.platform == "win32": |
| register(WindowsViewer) |
|
|
|
|
| class MacViewer(Viewer): |
| """The default viewer on macOS using ``Preview.app``.""" |
|
|
| format = "PNG" |
| options = {"compress_level": 1, "save_all": True} |
|
|
| def get_command(self, file: str, **options: Any) -> str: |
| |
| |
| command = "open -a Preview.app" |
| command = f"({command} {quote(file)}; sleep 20; rm -f {quote(file)})&" |
| return command |
|
|
| def show_file(self, path: str, **options: Any) -> int: |
| """ |
| Display given file. |
| """ |
| if not os.path.exists(path): |
| raise FileNotFoundError |
| subprocess.call(["open", "-a", "Preview.app", path]) |
|
|
| pyinstaller = getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS") |
| executable = (not pyinstaller and sys.executable) or shutil.which("python3") |
| if executable: |
| subprocess.Popen( |
| [ |
| executable, |
| "-c", |
| "import os, sys, time; time.sleep(20); os.remove(sys.argv[1])", |
| path, |
| ] |
| ) |
| return 1 |
|
|
|
|
| if sys.platform == "darwin": |
| register(MacViewer) |
|
|
|
|
| class UnixViewer(abc.ABC, Viewer): |
| format = "PNG" |
| options = {"compress_level": 1, "save_all": True} |
|
|
| @abc.abstractmethod |
| def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: |
| pass |
|
|
| def get_command(self, file: str, **options: Any) -> str: |
| command = self.get_command_ex(file, **options)[0] |
| return f"{command} {quote(file)}" |
|
|
|
|
| class XDGViewer(UnixViewer): |
| """ |
| The freedesktop.org ``xdg-open`` command. |
| """ |
|
|
| def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: |
| command = executable = "xdg-open" |
| return command, executable |
|
|
| def show_file(self, path: str, **options: Any) -> int: |
| """ |
| Display given file. |
| """ |
| if not os.path.exists(path): |
| raise FileNotFoundError |
| subprocess.Popen(["xdg-open", path]) |
| return 1 |
|
|
|
|
| class DisplayViewer(UnixViewer): |
| """ |
| The ImageMagick ``display`` command. |
| This viewer supports the ``title`` parameter. |
| """ |
|
|
| def get_command_ex( |
| self, file: str, title: str | None = None, **options: Any |
| ) -> tuple[str, str]: |
| command = executable = "display" |
| if title: |
| command += f" -title {quote(title)}" |
| return command, executable |
|
|
| def show_file(self, path: str, **options: Any) -> int: |
| """ |
| Display given file. |
| """ |
| if not os.path.exists(path): |
| raise FileNotFoundError |
| args = ["display"] |
| title = options.get("title") |
| if title: |
| args += ["-title", title] |
| args.append(path) |
|
|
| subprocess.Popen(args) |
| return 1 |
|
|
|
|
| class GmDisplayViewer(UnixViewer): |
| """The GraphicsMagick ``gm display`` command.""" |
|
|
| def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: |
| executable = "gm" |
| command = "gm display" |
| return command, executable |
|
|
| def show_file(self, path: str, **options: Any) -> int: |
| """ |
| Display given file. |
| """ |
| if not os.path.exists(path): |
| raise FileNotFoundError |
| subprocess.Popen(["gm", "display", path]) |
| return 1 |
|
|
|
|
| class EogViewer(UnixViewer): |
| """The GNOME Image Viewer ``eog`` command.""" |
|
|
| def get_command_ex(self, file: str, **options: Any) -> tuple[str, str]: |
| executable = "eog" |
| command = "eog -n" |
| return command, executable |
|
|
| def show_file(self, path: str, **options: Any) -> int: |
| """ |
| Display given file. |
| """ |
| if not os.path.exists(path): |
| raise FileNotFoundError |
| subprocess.Popen(["eog", "-n", path]) |
| return 1 |
|
|
|
|
| class XVViewer(UnixViewer): |
| """ |
| The X Viewer ``xv`` command. |
| This viewer supports the ``title`` parameter. |
| """ |
|
|
| def get_command_ex( |
| self, file: str, title: str | None = None, **options: Any |
| ) -> tuple[str, str]: |
| |
| |
| command = executable = "xv" |
| if title: |
| command += f" -name {quote(title)}" |
| return command, executable |
|
|
| def show_file(self, path: str, **options: Any) -> int: |
| """ |
| Display given file. |
| """ |
| if not os.path.exists(path): |
| raise FileNotFoundError |
| args = ["xv"] |
| title = options.get("title") |
| if title: |
| args += ["-name", title] |
| args.append(path) |
|
|
| subprocess.Popen(args) |
| return 1 |
|
|
|
|
| if sys.platform not in ("win32", "darwin"): |
| if shutil.which("xdg-open"): |
| register(XDGViewer) |
| if shutil.which("display"): |
| register(DisplayViewer) |
| if shutil.which("gm"): |
| register(GmDisplayViewer) |
| if shutil.which("eog"): |
| register(EogViewer) |
| if shutil.which("xv"): |
| register(XVViewer) |
|
|
|
|
| class IPythonViewer(Viewer): |
| """The viewer for IPython frontends.""" |
|
|
| def show_image(self, image: Image.Image, **options: Any) -> int: |
| ipython_display(image) |
| return 1 |
|
|
|
|
| try: |
| from IPython.display import display as ipython_display |
| except ImportError: |
| pass |
| else: |
| register(IPythonViewer) |
|
|
|
|
| if __name__ == "__main__": |
| if len(sys.argv) < 2: |
| print("Syntax: python3 ImageShow.py imagefile [title]") |
| sys.exit() |
|
|
| with Image.open(sys.argv[1]) as im: |
| print(show(im, *sys.argv[2:])) |
|
|