From 65807eddffa4b7f8cff19f88f566683477e679fe Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Aug 2026 08:31:18 +1000 Subject: [PATCH 1/5] Use (0, 0) as StubImageFile placeholder size --- Tests/test_file_bufrstub.py | 2 +- Tests/test_file_gribstub.py | 2 +- Tests/test_file_hdf5stub.py | 2 +- src/PIL/BufrStubImagePlugin.py | 1 - src/PIL/GribStubImagePlugin.py | 1 - src/PIL/Hdf5StubImagePlugin.py | 1 - src/PIL/ImageFile.py | 6 +++++- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index b5e726832e0..45673d5bf82 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -23,7 +23,7 @@ def test_open() -> None: # Dummy data from the stub assert im.mode == "F" - assert im.size == (1, 1) + assert im.size == (0, 0) def test_invalid_file() -> None: diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index 520d63a1b2d..e5ef5c04348 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -23,7 +23,7 @@ def test_open() -> None: # Dummy data from the stub assert im.mode == "F" - assert im.size == (1, 1) + assert im.size == (0, 0) def test_invalid_file() -> None: diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 853663d40d0..9247ba13ab6 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -22,7 +22,7 @@ def test_open() -> None: # Dummy data from the stub assert im.mode == "F" - assert im.size == (1, 1) + assert im.size == (0, 0) def test_invalid_file() -> None: diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 5250b7caf4b..9f548299563 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -53,7 +53,6 @@ def _open(self) -> None: # make something up self._mode = "F" - self._size = 1, 1 def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index aa1b1eda586..2aea555b17b 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -53,7 +53,6 @@ def _open(self) -> None: # make something up self._mode = "F" - self._size = 1, 1 def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index 16af6504070..348e3212bf4 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -53,7 +53,6 @@ def _open(self) -> None: # make something up self._mode = "F" - self._size = 1, 1 def _load(self) -> ImageFile.StubHandler | None: return _handler diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 7bc0ba8ddb0..08c46106d2b 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -163,7 +163,11 @@ def __init__( ) as v: raise SyntaxError(v) from v - if not self.mode or self.size[0] <= 0 or self.size[1] <= 0: + if not self.mode or ( + min(self.size) < 0 + if isinstance(self, StubImageFile) + else min(self.size) <= 0 + ): msg = "not identified by this driver" raise SyntaxError(msg) except BaseException: From 0c7f21a2140eaa77dd3d7a6bd96f5293640be495 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Aug 2026 07:41:09 +1000 Subject: [PATCH 2/5] Set _handler as property on StubImageFile --- Tests/test_file_wmf.py | 2 +- src/PIL/BufrStubImagePlugin.py | 13 +++--- src/PIL/GribStubImagePlugin.py | 13 +++--- src/PIL/Hdf5StubImagePlugin.py | 13 +++--- src/PIL/ImageFile.py | 2 + src/PIL/WmfImagePlugin.py | 75 +++++++++++++++++----------------- 6 files changed, 58 insertions(+), 60 deletions(-) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 698853d7ed0..0c9ac7abbcb 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -79,7 +79,7 @@ def save(self, im: Image.Image, fp: IO[bytes], filename: str) -> None: self.methodCalled = True handler = TestHandler() - original_handler = WmfImagePlugin._handler + original_handler = WmfImagePlugin.WmfStubImageFile._handler WmfImagePlugin.register_handler(handler) im = hopper() diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 9f548299563..0b4eb4a8a16 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -18,8 +18,6 @@ if TYPE_CHECKING: from typing import IO -_handler = None - def register_handler(handler: ImageFile.StubHandler | None) -> None: """ @@ -27,8 +25,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: :param handler: Handler object. """ - global _handler - _handler = handler + BufrStubImageFile._handler = handler # -------------------------------------------------------------------- @@ -55,14 +52,16 @@ def _open(self) -> None: self._mode = "F" def _load(self) -> ImageFile.StubHandler | None: - return _handler + return self._handler def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): + if BufrStubImageFile._handler is None or not hasattr( + BufrStubImageFile._handler, "save" + ): msg = "BUFR save handler not installed" raise OSError(msg) - _handler.save(im, fp, filename) + BufrStubImageFile._handler.save(im, fp, filename) # -------------------------------------------------------------------- diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 2aea555b17b..59a963de458 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -18,8 +18,6 @@ if TYPE_CHECKING: from typing import IO -_handler = None - def register_handler(handler: ImageFile.StubHandler | None) -> None: """ @@ -27,8 +25,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: :param handler: Handler object. """ - global _handler - _handler = handler + GribStubImageFile._handler = handler # -------------------------------------------------------------------- @@ -55,14 +52,16 @@ def _open(self) -> None: self._mode = "F" def _load(self) -> ImageFile.StubHandler | None: - return _handler + return self._handler def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): + if GribStubImageFile._handler is None or not hasattr( + GribStubImageFile._handler, "save" + ): msg = "GRIB save handler not installed" raise OSError(msg) - _handler.save(im, fp, filename) + GribStubImageFile._handler.save(im, fp, filename) # -------------------------------------------------------------------- diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index 348e3212bf4..6d16316dc3d 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -18,8 +18,6 @@ if TYPE_CHECKING: from typing import IO -_handler = None - def register_handler(handler: ImageFile.StubHandler | None) -> None: """ @@ -27,8 +25,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: :param handler: Handler object. """ - global _handler - _handler = handler + HDF5StubImageFile._handler = handler # -------------------------------------------------------------------- @@ -55,14 +52,16 @@ def _open(self) -> None: self._mode = "F" def _load(self) -> ImageFile.StubHandler | None: - return _handler + return self._handler def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): + if HDF5StubImageFile._handler is None or not hasattr( + HDF5StubImageFile._handler, "save" + ): msg = "HDF5 save handler not installed" raise OSError(msg) - _handler.save(im, fp, filename) + HDF5StubImageFile._handler.save(im, fp, filename) # -------------------------------------------------------------------- diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 08c46106d2b..e6ac891dfbc 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -488,6 +488,8 @@ class StubImageFile(ImageFile, metaclass=abc.ABCMeta): certain format, but relies on external code to load the file. """ + _handler: StubHandler | None = None + @abc.abstractmethod def _open(self) -> None: pass diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index d30d1a9d339..90c07ab2efe 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -29,41 +29,6 @@ if TYPE_CHECKING: from typing import IO -_handler = None - - -def register_handler(handler: ImageFile.StubHandler | None) -> None: - """ - Install application-specific WMF image handler. - - :param handler: Handler object. - """ - global _handler - _handler = handler - - -if hasattr(Image.core, "drawwmf"): - # install default handler (windows only) - - class WmfHandler(ImageFile.StubHandler): - def open(self, im: ImageFile.StubImageFile) -> None: - self.bbox = im.info["wmf_bbox"] - - def load(self, im: ImageFile.StubImageFile) -> Image.Image: - assert im.fp is not None - im.fp.seek(0) # rewind - return Image.frombytes( - "RGB", - im.size, - Image.core.drawwmf(im.fp.read(), im.size, self.bbox), - "raw", - "BGR", - (im.size[0] * 3 + 3) & -4, - -1, - ) - - register_handler(WmfHandler()) - # # -------------------------------------------------------------------- # Read WMF file @@ -150,7 +115,7 @@ def _open(self) -> None: self._size = size def _load(self) -> ImageFile.StubHandler | None: - return _handler + return self._handler def load( self, dpi: float | tuple[float, float] | None = None @@ -168,10 +133,44 @@ def load( def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: - if _handler is None or not hasattr(_handler, "save"): + if WmfStubImageFile._handler is None or not hasattr( + WmfStubImageFile._handler, "save" + ): msg = "WMF save handler not installed" raise OSError(msg) - _handler.save(im, fp, filename) + WmfStubImageFile._handler.save(im, fp, filename) + + +def register_handler(handler: ImageFile.StubHandler | None) -> None: + """ + Install application-specific WMF image handler. + + :param handler: Handler object. + """ + WmfStubImageFile._handler = handler + + +if hasattr(Image.core, "drawwmf"): + # install default handler (windows only) + + class WmfHandler(ImageFile.StubHandler): + def open(self, im: ImageFile.StubImageFile) -> None: + self.bbox = im.info["wmf_bbox"] + + def load(self, im: ImageFile.StubImageFile) -> Image.Image: + assert im.fp is not None + im.fp.seek(0) # rewind + return Image.frombytes( + "RGB", + im.size, + Image.core.drawwmf(im.fp.read(), im.size, self.bbox), + "raw", + "BGR", + (im.size[0] * 3 + 3) & -4, + -1, + ) + + register_handler(WmfHandler()) # From 7647548307733cc008a45f6ce63d740c0f1fc639 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Aug 2026 06:58:34 +1000 Subject: [PATCH 3/5] If open() is used, require StubHandler to set size there --- Tests/test_file_bufrstub.py | 1 + Tests/test_file_gribstub.py | 1 + Tests/test_file_hdf5stub.py | 1 + src/PIL/ImageFile.py | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/test_file_bufrstub.py b/Tests/test_file_bufrstub.py index 45673d5bf82..2a7bf30ad4c 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -61,6 +61,7 @@ class TestHandler(ImageFile.StubHandler): def open(self, im: ImageFile.StubImageFile) -> None: self.opened = True + im._size = (1, 1) def load(self, im: ImageFile.StubImageFile) -> Image.Image: self.loaded = True diff --git a/Tests/test_file_gribstub.py b/Tests/test_file_gribstub.py index e5ef5c04348..6bd1df2fac0 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -61,6 +61,7 @@ class TestHandler(ImageFile.StubHandler): def open(self, im: Image.Image) -> None: self.opened = True + im._size = (1, 1) def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True diff --git a/Tests/test_file_hdf5stub.py b/Tests/test_file_hdf5stub.py index 9247ba13ab6..0ef19020098 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -63,6 +63,7 @@ class TestHandler(ImageFile.StubHandler): def open(self, im: Image.Image) -> None: self.opened = True + im._size = (1, 1) def load(self, im: ImageFile.ImageFile) -> Image.Image: self.loaded = True diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index e6ac891dfbc..73ee67d30d9 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -165,7 +165,7 @@ def __init__( if not self.mode or ( min(self.size) < 0 - if isinstance(self, StubImageFile) + if isinstance(self, StubImageFile) and self._handler is None else min(self.size) <= 0 ): msg = "not identified by this driver" From de509bb103ebc05e2e71c3de9ff36773c733272d Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Aug 2026 07:44:30 +1000 Subject: [PATCH 4/5] Remove StubImageFile _load() --- src/PIL/BufrStubImagePlugin.py | 3 --- src/PIL/GribStubImagePlugin.py | 3 --- src/PIL/Hdf5StubImagePlugin.py | 3 --- src/PIL/ImageFile.py | 15 ++++----------- src/PIL/WmfImagePlugin.py | 3 --- 5 files changed, 4 insertions(+), 23 deletions(-) diff --git a/src/PIL/BufrStubImagePlugin.py b/src/PIL/BufrStubImagePlugin.py index 0b4eb4a8a16..8a4e3eab6a5 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -51,9 +51,6 @@ def _open(self) -> None: # make something up self._mode = "F" - def _load(self) -> ImageFile.StubHandler | None: - return self._handler - def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if BufrStubImageFile._handler is None or not hasattr( diff --git a/src/PIL/GribStubImagePlugin.py b/src/PIL/GribStubImagePlugin.py index 59a963de458..7549b4fc5c0 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -51,9 +51,6 @@ def _open(self) -> None: # make something up self._mode = "F" - def _load(self) -> ImageFile.StubHandler | None: - return self._handler - def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if GribStubImageFile._handler is None or not hasattr( diff --git a/src/PIL/Hdf5StubImagePlugin.py b/src/PIL/Hdf5StubImagePlugin.py index 6d16316dc3d..a992159dedb 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -51,9 +51,6 @@ def _open(self) -> None: # make something up self._mode = "F" - def _load(self) -> ImageFile.StubHandler | None: - return self._handler - def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: if HDF5StubImageFile._handler is None or not hasattr( diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 73ee67d30d9..563622786f3 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -151,9 +151,8 @@ def __init__( try: self._open() - if isinstance(self, StubImageFile): - if loader := self._load(): - loader.open(self) + if isinstance(self, StubImageFile) and self._handler: + self._handler.open(self) except ( IndexError, # end of data TypeError, # end of data (ord) @@ -495,22 +494,16 @@ def _open(self) -> None: pass def load(self) -> Image.core.PixelAccess | None: - loader = self._load() - if loader is None: + if self._handler is None: msg = f"cannot find loader for this {self.format} file" raise OSError(msg) - image = loader.load(self) + image = self._handler.load(self) assert image is not None # become the other object (!) self.__class__ = image.__class__ # type: ignore[assignment] self.__dict__ = image.__dict__ return image.load() - @abc.abstractmethod - def _load(self) -> StubHandler | None: - """(Hook) Find actual image loader.""" - pass - class Parser: """ diff --git a/src/PIL/WmfImagePlugin.py b/src/PIL/WmfImagePlugin.py index 90c07ab2efe..fb49bc5a433 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -114,9 +114,6 @@ def _open(self) -> None: self._mode = "RGB" self._size = size - def _load(self) -> ImageFile.StubHandler | None: - return self._handler - def load( self, dpi: float | tuple[float, float] | None = None ) -> Image.core.PixelAccess | None: From e9f2e0222f4cbd6ed101df78803ca85b7e902867 Mon Sep 17 00:00:00 2001 From: Andrew Murray Date: Wed, 19 Aug 2026 08:27:45 +1000 Subject: [PATCH 5/5] Mark StubHandler.open() as abstract --- Tests/test_file_wmf.py | 3 +++ src/PIL/ImageFile.py | 1 + 2 files changed, 4 insertions(+) diff --git a/Tests/test_file_wmf.py b/Tests/test_file_wmf.py index 0c9ac7abbcb..c837583f15a 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -72,6 +72,9 @@ def test_register_handler(tmp_path: Path) -> None: class TestHandler(ImageFile.StubHandler): methodCalled = False + def open(self, im: ImageFile.StubImageFile) -> None: + im._size = (1, 1) + def load(self, im: ImageFile.StubImageFile) -> Image.Image: return Image.new("RGB", (1, 1)) diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py index 563622786f3..14038e4732d 100644 --- a/src/PIL/ImageFile.py +++ b/src/PIL/ImageFile.py @@ -471,6 +471,7 @@ def _seek_check(self, frame: int) -> bool: class StubHandler(abc.ABC): + @abc.abstractmethod def open(self, im: StubImageFile) -> None: pass