From 293eb45a1d92a7092b682e77f35dd38273c7c3ee Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 15 Apr 2026 22:08:55 +1000 Subject: [PATCH 1/5] Exclude formats with ! when opening --- Tests/test_image.py | 6 ++++++ src/PIL/Image.py | 52 ++++++++++++++++++++++----------------------- 2 files changed, 32 insertions(+), 26 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index 32c79919595..ca8fccd2c4e 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -128,6 +128,12 @@ def test_open_formats(self) -> None: assert im.mode == "RGB" assert im.size == (128, 128) + @pytest.mark.parametrize("formats", (("!PNG",), ("PNG", "!PNG"))) + def test_open_formats_exclude(self, formats: tuple[str]) -> None: + with pytest.raises(UnidentifiedImageError): + with Image.open("Tests/images/hopper.png", formats=formats): + pass + def test_open_verbose_failure(self, monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(Image, "WARN_POSSIBLE_FORMATS", True) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 574980771f9..f2f5e29e17a 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3598,11 +3598,15 @@ def open( and be opened in binary mode. The file object will also seek to zero before reading. :param mode: The mode. If given, this argument must be "r". - :param formats: A list or tuple of formats to attempt to load the file in. - This can be used to restrict the set of formats checked. - Pass ``None`` to try all supported formats. You can print the set of - available formats by running ``python3 -m PIL`` or using - the :py:func:`PIL.features.pilinfo` function. + :param formats: A list or tuple of formats to attempt to load the file in, e.g. + ("JPEG", "GIF"). This can be used to restrict the set of formats checked. + + To exclude a format, start the format with "!", e.g. ("!EPS", "!PSD"). + + Pass ``None`` to try all supported formats. + + You can print the set of available formats by running ``python3 -m PIL`` or + using the :py:func:`PIL.features.pilinfo` function. :returns: An :py:class:`~PIL.Image.Image` object. :exception FileNotFoundError: If the file cannot be found. :exception PIL.UnidentifiedImageError: If the image cannot be opened and @@ -3622,11 +3626,12 @@ def open( ) raise ValueError(msg) - if formats is None: - formats = ID - elif not isinstance(formats, (list, tuple)): - msg = "formats must be a list or tuple" # type: ignore[unreachable] - raise TypeError(msg) + if formats is not None: + if not isinstance(formats, (list, tuple)): + msg = "formats must be a list or tuple" # type: ignore[unreachable] + raise TypeError(msg) + formats = tuple(format.upper() for format in formats) + exclude = all(format.startswith("!") for format in formats) exclusive_fp = False filename: str | bytes = "" @@ -3657,12 +3662,15 @@ def _open_core( fp: IO[bytes], filename: str | bytes, prefix: bytes, - formats: list[str] | tuple[str, ...], + check_formats: list[str], ) -> ImageFile.ImageFile | None: - for i in formats: - i = i.upper() - if i not in OPEN: - init() + for i in check_formats: + if formats is not None: + if exclude: + if "!" + i in formats: + continue + elif i not in formats or "!" + i in formats: + continue try: factory, accept = OPEN[i] result = not accept or accept(prefix) @@ -3682,21 +3690,13 @@ def _open_core( raise return None - im = _open_core(fp, filename, prefix, formats) - - if im is None and formats is ID: + if im := _open_core(fp, filename, prefix, ID): # Try preinit (few common plugins) then init (all plugins) for loader in (preinit, init): checked_formats = ID.copy() loader() - if formats != checked_formats: - im = _open_core( - fp, - filename, - prefix, - tuple(f for f in formats if f not in checked_formats), - ) - if im is not None: + if check_formats := [f for f in ID if f not in checked_formats]: + if im := _open_core(fp, filename, prefix, check_formats): break if im: From c8370788cda4e35d78373c888a0631b33e8e2228 Mon Sep 17 00:00:00 2001 From: Andrew Murray <3112309+radarhere@users.noreply.github.com> Date: Thu, 16 Apr 2026 00:12:42 +1000 Subject: [PATCH 2/5] Correct logic Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- src/PIL/Image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index f2f5e29e17a..a0f0662749e 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3690,7 +3690,7 @@ def _open_core( raise return None - if im := _open_core(fp, filename, prefix, ID): + if not (im := _open_core(fp, filename, prefix, ID)): # Try preinit (few common plugins) then init (all plugins) for loader in (preinit, init): checked_formats = ID.copy() From a0240162a3a7d167a06895cda501349a96e5a281 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Apr 2026 00:09:23 +1000 Subject: [PATCH 3/5] Update docstring --- src/PIL/Image.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index a0f0662749e..6ba1b5749b5 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3598,10 +3598,12 @@ def open( and be opened in binary mode. The file object will also seek to zero before reading. :param mode: The mode. If given, this argument must be "r". - :param formats: A list or tuple of formats to attempt to load the file in, e.g. - ("JPEG", "GIF"). This can be used to restrict the set of formats checked. + :param formats: A list or tuple of formats to attempt to load the file in, for + example, ``("JPEG", "GIF")``. This can be used to restrict the set of formats + checked. - To exclude a format, start the format with "!", e.g. ("!EPS", "!PSD"). + To exclude a format, start the format with "!", for example, + ``("!EPS", "!PSD")``. Pass ``None`` to try all supported formats. From 606c42273b676c3c2dd02e9c92be56ad7b4ffde6 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Apr 2026 18:20:42 +1000 Subject: [PATCH 4/5] Use sets --- Tests/test_image.py | 5 ++++- src/PIL/Image.py | 24 ++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Tests/test_image.py b/Tests/test_image.py index ca8fccd2c4e..dad269361e2 100644 --- a/Tests/test_image.py +++ b/Tests/test_image.py @@ -128,8 +128,11 @@ def test_open_formats(self) -> None: assert im.mode == "RGB" assert im.size == (128, 128) - @pytest.mark.parametrize("formats", (("!PNG",), ("PNG", "!PNG"))) + @pytest.mark.parametrize("formats", (("!PNG",), ("PNG", "!PNG"), ("JPEG", "!PNG"))) def test_open_formats_exclude(self, formats: tuple[str]) -> None: + with Image.open("Tests/images/hopper.jpg", formats=formats): + pass + with pytest.raises(UnidentifiedImageError): with Image.open("Tests/images/hopper.png", formats=formats): pass diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 6ba1b5749b5..0e3919f8428 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3632,8 +3632,16 @@ def open( if not isinstance(formats, (list, tuple)): msg = "formats must be a list or tuple" # type: ignore[unreachable] raise TypeError(msg) - formats = tuple(format.upper() for format in formats) - exclude = all(format.startswith("!") for format in formats) + + allowed = set() + excluded = set() + for f in formats: + f = f.upper() + if f.startswith("!"): + excluded.add(f[1:]) + else: + allowed.add(f) + allowed -= excluded exclusive_fp = False filename: str | bytes = "" @@ -3666,13 +3674,13 @@ def _open_core( prefix: bytes, check_formats: list[str], ) -> ImageFile.ImageFile | None: + if formats is not None: + if allowed: + check_formats = [f for f in check_formats if f in allowed] + else: + check_formats = [f for f in check_formats if f not in excluded] + for i in check_formats: - if formats is not None: - if exclude: - if "!" + i in formats: - continue - elif i not in formats or "!" + i in formats: - continue try: factory, accept = OPEN[i] result = not accept or accept(prefix) From 1fc8011a57e17b9b15d1e664551030e9b89d65eb Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Thu, 16 Apr 2026 18:33:49 +1000 Subject: [PATCH 5/5] Lint fix --- src/PIL/Image.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/PIL/Image.py b/src/PIL/Image.py index 0e3919f8428..cd14bfb85b3 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -3691,7 +3691,12 @@ def _open_core( im = factory(fp, filename) _decompression_bomb_check(im.size) return im - except (SyntaxError, IndexError, TypeError, struct.error) as e: + except ( # noqa: PERF203 + SyntaxError, + IndexError, + TypeError, + struct.error, + ) as e: if WARN_POSSIBLE_FORMATS: warning_messages.append(i + " opening failed. " + str(e)) except BaseException: