|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:]))
|
|
|