diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 8d6731acc91..1441b2e55d1 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -105,6 +105,15 @@ def test_viewers(viewer: ImageShow.Viewer) -> None: pass +def test_windowsviewer() -> None: + viewer = ImageShow.WindowsViewer() + with pytest.raises(ValueError, match="cannot contain double quotes"): + viewer.get_command('"') + + # Check that percentages are escaped + assert "%" not in viewer.get_command("%").replace('""%""', "") + + def test_ipythonviewer() -> None: pytest.importorskip("IPython", reason="IPython not installed") for viewer in ImageShow._viewers: diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 2734f68b173..dd8aa0d36c2 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -120,6 +120,20 @@ def show_file(self, path: str, **options: Any) -> int: os.system(self.get_command(path, **options)) # nosec return 1 + def _remove_path_after_delay(self, path: str) -> None: + 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, + ] + ) + # -------------------------------------------------------------------- @@ -131,6 +145,10 @@ class WindowsViewer(Viewer): options = {"compress_level": 1, "save_all": True} def get_command(self, file: str, **options: Any) -> str: + if '"' in file: + msg = "Windows filenames cannot contain double quotes" + raise ValueError(msg) + file = file.replace("%", '"%"') return ( f'start "Pillow" /WAIT "{file}" ' "&& ping -n 4 127.0.0.1 >NUL " @@ -143,11 +161,9 @@ def show_file(self, path: str, **options: Any) -> int: """ if not os.path.exists(path): raise FileNotFoundError - subprocess.Popen( - self.get_command(path, **options), - shell=True, - creationflags=getattr(subprocess, "CREATE_NO_WINDOW"), - ) # nosec + if sys.platform == "win32": + os.startfile(path) + self._remove_path_after_delay(path) return 1 @@ -175,18 +191,7 @@ def show_file(self, path: str, **options: Any) -> int: 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, - ] - ) + self._remove_path_after_delay(path) return 1