From 8404ea5fe5df40fc34aa1e51403dd6fce0778b8a Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jun 2026 10:12:57 +1000 Subject: [PATCH 1/3] Use os.startfile in WindowsViewer show_file --- src/PIL/ImageShow.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 2734f68b173..021f43ca6d1 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, + ] + ) + # -------------------------------------------------------------------- @@ -143,11 +157,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 +187,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 From b0e06caa64c1405aa3da0bb1d2bd9a77ca22de7f Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Sat, 20 Jun 2026 10:05:13 +1000 Subject: [PATCH 2/3] Raise ValueError on double quote in WindowsViewer file --- Tests/test_imageshow.py | 6 ++++++ src/PIL/ImageShow.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index 8d6731acc91..fe625e9c77c 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -105,6 +105,12 @@ 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('"') + + 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 021f43ca6d1..332c780703c 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -145,6 +145,9 @@ 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) return ( f'start "Pillow" /WAIT "{file}" ' "&& ping -n 4 127.0.0.1 >NUL " From 88194166691b7b603529b8b036ab3ab9cedd2de4 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Fri, 19 Jun 2026 22:31:39 +1000 Subject: [PATCH 3/3] Prevent variable expansion in WindowsViewer get_command --- Tests/test_imageshow.py | 3 +++ src/PIL/ImageShow.py | 1 + 2 files changed, 4 insertions(+) diff --git a/Tests/test_imageshow.py b/Tests/test_imageshow.py index fe625e9c77c..1441b2e55d1 100644 --- a/Tests/test_imageshow.py +++ b/Tests/test_imageshow.py @@ -110,6 +110,9 @@ def test_windowsviewer() -> None: 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") diff --git a/src/PIL/ImageShow.py b/src/PIL/ImageShow.py index 332c780703c..dd8aa0d36c2 100644 --- a/src/PIL/ImageShow.py +++ b/src/PIL/ImageShow.py @@ -148,6 +148,7 @@ 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 "