From 793bd0bbd5ac6d712ba3111064a0626e68f3da01 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 8c6bb1a69f7..d875f17d5cc 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -20,7 +20,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 05925d50202..db9f48ec7df 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -20,7 +20,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 e1a56309b90..874da3c4764 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -19,7 +19,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 d82c4c746c3..576c9fdb6bf 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -50,7 +50,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 3784ef2f134..06490d324be 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -50,7 +50,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 1a56660f7bd..34ae0d964a1 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -50,7 +50,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 eaa1e45812c..702c0b628bc 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 559979359ddc3c088ef9cbcaba9fc6b95408b485 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 56901f46b07..471820ef9b0 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -76,7 +76,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 576c9fdb6bf..98e2543e4f7 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -15,8 +15,6 @@ from . import Image, ImageFile -_handler = None - def register_handler(handler: ImageFile.StubHandler | None) -> None: """ @@ -24,8 +22,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: :param handler: Handler object. """ - global _handler - _handler = handler + BufrStubImageFile._handler = handler # -------------------------------------------------------------------- @@ -52,14 +49,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 06490d324be..c9da7e70239 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -15,8 +15,6 @@ from . import Image, ImageFile -_handler = None - def register_handler(handler: ImageFile.StubHandler | None) -> None: """ @@ -24,8 +22,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: :param handler: Handler object. """ - global _handler - _handler = handler + GribStubImageFile._handler = handler # -------------------------------------------------------------------- @@ -52,14 +49,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 34ae0d964a1..724ddab4dc1 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -15,8 +15,6 @@ from . import Image, ImageFile -_handler = None - def register_handler(handler: ImageFile.StubHandler | None) -> None: """ @@ -24,8 +22,7 @@ def register_handler(handler: ImageFile.StubHandler | None) -> None: :param handler: Handler object. """ - global _handler - _handler = handler + HDF5StubImageFile._handler = handler # -------------------------------------------------------------------- @@ -52,14 +49,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 702c0b628bc..1c025bba8d6 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 f5e244782fc..bcfebf7b835 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -27,41 +27,6 @@ from ._binary import si16le as short from ._binary import si32le as _long -_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 @@ -148,7 +113,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 @@ -166,10 +131,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 d9ea91073fd1a63420b8f2f4ca3c6dc6e9e015d2 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 d875f17d5cc..48949ad2fe2 100644 --- a/Tests/test_file_bufrstub.py +++ b/Tests/test_file_bufrstub.py @@ -58,6 +58,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 db9f48ec7df..45b5565e505 100644 --- a/Tests/test_file_gribstub.py +++ b/Tests/test_file_gribstub.py @@ -58,6 +58,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 874da3c4764..c22196bd640 100644 --- a/Tests/test_file_hdf5stub.py +++ b/Tests/test_file_hdf5stub.py @@ -60,6 +60,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 1c025bba8d6..1b04e25df92 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 d318ae65ff436f0990af014bcea48985916c39ed 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 98e2543e4f7..ef7b2d74cb4 100644 --- a/src/PIL/BufrStubImagePlugin.py +++ b/src/PIL/BufrStubImagePlugin.py @@ -48,9 +48,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 c9da7e70239..c24d1d04767 100644 --- a/src/PIL/GribStubImagePlugin.py +++ b/src/PIL/GribStubImagePlugin.py @@ -48,9 +48,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 724ddab4dc1..0996afac822 100644 --- a/src/PIL/Hdf5StubImagePlugin.py +++ b/src/PIL/Hdf5StubImagePlugin.py @@ -48,9 +48,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 1b04e25df92..bc6b98281b4 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 bcfebf7b835..b3cf052feca 100644 --- a/src/PIL/WmfImagePlugin.py +++ b/src/PIL/WmfImagePlugin.py @@ -112,9 +112,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 b193011e97a30509cb412870c66c2b8da623901a 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 471820ef9b0..451a726ae43 100644 --- a/Tests/test_file_wmf.py +++ b/Tests/test_file_wmf.py @@ -69,6 +69,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 bc6b98281b4..e087365f5cf 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