From d00f3656a684825569152ea1d6e74526ffa5d14b Mon Sep 17 00:00:00 2001
From: Andrew Murray <radarhere@users.noreply.github.com>
Date: Thu, 29 Aug 2024 22:51:15 +1000
Subject: [PATCH 1/2] Changed im.tile to never be None

---
 Tests/test_imagefile.py        | 24 +++++++++++++++++-------
 docs/example/DdsImagePlugin.py |  4 +++-
 src/PIL/BlpImagePlugin.py      |  7 +++++--
 src/PIL/BmpImagePlugin.py      |  2 +-
 src/PIL/CurImagePlugin.py      |  4 ++--
 src/PIL/DdsImagePlugin.py      |  2 +-
 src/PIL/FitsImagePlugin.py     |  2 +-
 src/PIL/FliImagePlugin.py      |  2 +-
 src/PIL/FpxImagePlugin.py      |  6 +++---
 src/PIL/FtexImagePlugin.py     |  4 ++--
 src/PIL/GdImageFile.py         |  2 +-
 src/PIL/GifImagePlugin.py      |  2 +-
 src/PIL/IcoImagePlugin.py      |  2 +-
 src/PIL/ImImagePlugin.py       | 22 ++++++++++++++++------
 src/PIL/Image.py               |  5 ++++-
 src/PIL/ImageFile.py           | 27 ++++++++++++++++++---------
 src/PIL/ImtImagePlugin.py      |  2 +-
 src/PIL/IptcImagePlugin.py     |  4 +++-
 src/PIL/Jpeg2KImagePlugin.py   |  5 +++--
 src/PIL/JpegImagePlugin.py     |  8 ++++++--
 src/PIL/McIdasImagePlugin.py   |  4 +++-
 src/PIL/MpoImagePlugin.py      |  5 ++++-
 src/PIL/MspImagePlugin.py      |  4 ++--
 src/PIL/PcdImagePlugin.py      |  2 +-
 src/PIL/PcxImagePlugin.py      |  4 +++-
 src/PIL/PixarImagePlugin.py    |  4 +++-
 src/PIL/PpmImagePlugin.py      |  4 +++-
 src/PIL/PsdImagePlugin.py      |  6 ++----
 src/PIL/QoiImagePlugin.py      |  2 +-
 src/PIL/SgiImagePlugin.py      | 15 ++++++++++++---
 src/PIL/SpiderImagePlugin.py   |  4 +++-
 src/PIL/SunImagePlugin.py      |  8 ++++++--
 src/PIL/TgaImagePlugin.py      |  4 ++--
 src/PIL/TiffImagePlugin.py     | 15 ++++++++-------
 src/PIL/WebPImagePlugin.py     |  2 +-
 src/PIL/XVThumbImagePlugin.py  |  6 +++++-
 src/PIL/XbmImagePlugin.py      |  2 +-
 src/PIL/XpmImagePlugin.py      |  4 +++-
 38 files changed, 153 insertions(+), 78 deletions(-)

diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py
index a8bd798c12d..0985dce9cf1 100644
--- a/Tests/test_imagefile.py
+++ b/Tests/test_imagefile.py
@@ -238,7 +238,9 @@ def _open(self) -> None:
         self.rawmode = "RGBA"
         self._mode = "RGBA"
         self._size = (200, 200)
-        self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)]
+        self.tile = [
+            ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)
+        ]
 
 
 class CodecsTest:
@@ -268,7 +270,7 @@ def test_extents_none(self) -> None:
         buf = BytesIO(b"\x00" * 255)
 
         im = MockImageFile(buf)
-        im.tile = [("MOCK", None, 32, None)]
+        im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
 
         im.load()
 
@@ -281,12 +283,12 @@ def test_negsize(self) -> None:
         buf = BytesIO(b"\x00" * 255)
 
         im = MockImageFile(buf)
-        im.tile = [("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
+        im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, -10, yoff + ysize), 32, None)]
 
         with pytest.raises(ValueError):
             im.load()
 
-        im.tile = [("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)]
+        im.tile = [ImageFile._Tile("MOCK", (xoff, yoff, xoff + xsize, -10), 32, None)]
         with pytest.raises(ValueError):
             im.load()
 
@@ -294,12 +296,20 @@ def test_oversize(self) -> None:
         buf = BytesIO(b"\x00" * 255)
 
         im = MockImageFile(buf)
-        im.tile = [("MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None)]
+        im.tile = [
+            ImageFile._Tile(
+                "MOCK", (xoff, yoff, xoff + xsize + 100, yoff + ysize), 32, None
+            )
+        ]
 
         with pytest.raises(ValueError):
             im.load()
 
-        im.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None)]
+        im.tile = [
+            ImageFile._Tile(
+                "MOCK", (xoff, yoff, xoff + xsize, yoff + ysize + 100), 32, None
+            )
+        ]
         with pytest.raises(ValueError):
             im.load()
 
@@ -336,7 +346,7 @@ def test_extents_none(self) -> None:
         buf = BytesIO(b"\x00" * 255)
 
         im = MockImageFile(buf)
-        im.tile = [("MOCK", None, 32, None)]
+        im.tile = [ImageFile._Tile("MOCK", None, 32, None)]
 
         fp = BytesIO()
         ImageFile._save(im, fp, [ImageFile._Tile("MOCK", None, 0, "RGB")])
diff --git a/docs/example/DdsImagePlugin.py b/docs/example/DdsImagePlugin.py
index caa852b1fe1..d8f53607697 100644
--- a/docs/example/DdsImagePlugin.py
+++ b/docs/example/DdsImagePlugin.py
@@ -246,7 +246,9 @@ def _open(self) -> None:
             msg = f"Unimplemented pixel format {repr(fourcc)}"
             raise NotImplementedError(msg)
 
-        self.tile = [(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
+        self.tile = [
+            ImageFile._Tile(self.decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))
+        ]
 
     def load_seek(self, pos: int) -> None:
         pass
diff --git a/src/PIL/BlpImagePlugin.py b/src/PIL/BlpImagePlugin.py
index 1112276dc6e..8e29898dfc8 100644
--- a/src/PIL/BlpImagePlugin.py
+++ b/src/PIL/BlpImagePlugin.py
@@ -273,7 +273,7 @@ def _open(self) -> None:
             raise BLPFormatError(msg)
 
         self._mode = "RGBA" if self._blp_alpha_depth else "RGB"
-        self.tile = [(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
+        self.tile = [ImageFile._Tile(decoder, (0, 0) + self.size, 0, (self.mode, 0, 1))]
 
 
 class _BLPBaseDecoder(ImageFile.PyDecoder):
@@ -372,7 +372,10 @@ def _decode_jpeg_stream(self) -> None:
         Image._decompression_bomb_check(image.size)
         if image.mode == "CMYK":
             decoder_name, extents, offset, args = image.tile[0]
-            image.tile = [(decoder_name, extents, offset, (args[0], "CMYK"))]
+            assert isinstance(args, tuple)
+            image.tile = [
+                ImageFile._Tile(decoder_name, extents, offset, (args[0], "CMYK"))
+            ]
         r, g, b = image.convert("RGB").split()
         reversed_image = Image.merge("RGB", (b, g, r))
         self.set_as_raw(reversed_image.tobytes())
diff --git a/src/PIL/BmpImagePlugin.py b/src/PIL/BmpImagePlugin.py
index de441c6b5c2..e70856ffbc1 100644
--- a/src/PIL/BmpImagePlugin.py
+++ b/src/PIL/BmpImagePlugin.py
@@ -296,7 +296,7 @@ def _bitmap(self, header: int = 0, offset: int = 0) -> None:
             args.append(((file_info["width"] * file_info["bits"] + 31) >> 3) & (~3))
         args.append(file_info["direction"])
         self.tile = [
-            (
+            ImageFile._Tile(
                 decoder_name,
                 (0, 0, file_info["width"], file_info["height"]),
                 offset or self.fp.tell(),
diff --git a/src/PIL/CurImagePlugin.py b/src/PIL/CurImagePlugin.py
index 85e2145e766..c4be0cecaf8 100644
--- a/src/PIL/CurImagePlugin.py
+++ b/src/PIL/CurImagePlugin.py
@@ -17,7 +17,7 @@
 #
 from __future__ import annotations
 
-from . import BmpImagePlugin, Image
+from . import BmpImagePlugin, Image, ImageFile
 from ._binary import i16le as i16
 from ._binary import i32le as i32
 
@@ -64,7 +64,7 @@ def _open(self) -> None:
         # patch up the bitmap height
         self._size = self.size[0], self.size[1] // 2
         d, e, o, a = self.tile[0]
-        self.tile[0] = d, (0, 0) + self.size, o, a
+        self.tile[0] = ImageFile._Tile(d, (0, 0) + self.size, o, a)
 
 
 #
diff --git a/src/PIL/DdsImagePlugin.py b/src/PIL/DdsImagePlugin.py
index a57e4aea2d5..6315324701a 100644
--- a/src/PIL/DdsImagePlugin.py
+++ b/src/PIL/DdsImagePlugin.py
@@ -367,7 +367,7 @@ def _open(self) -> None:
                 mask_count = 3
 
             masks = struct.unpack(f"<{mask_count}I", header.read(mask_count * 4))
-            self.tile = [("dds_rgb", extents, 0, (bitcount, masks))]
+            self.tile = [ImageFile._Tile("dds_rgb", extents, 0, (bitcount, masks))]
             return
         elif pfflags & DDPF.LUMINANCE:
             if bitcount == 8:
diff --git a/src/PIL/FitsImagePlugin.py b/src/PIL/FitsImagePlugin.py
index 4846054b1e4..abacc5e67f2 100644
--- a/src/PIL/FitsImagePlugin.py
+++ b/src/PIL/FitsImagePlugin.py
@@ -67,7 +67,7 @@ def _open(self) -> None:
             raise ValueError(msg)
 
         offset += self.fp.tell() - 80
-        self.tile = [(decoder_name, (0, 0) + self.size, offset, args)]
+        self.tile = [ImageFile._Tile(decoder_name, (0, 0) + self.size, offset, args)]
 
     def _get_size(
         self, headers: dict[bytes, bytes], prefix: bytes
diff --git a/src/PIL/FliImagePlugin.py b/src/PIL/FliImagePlugin.py
index 52d1fce3167..666390be9ee 100644
--- a/src/PIL/FliImagePlugin.py
+++ b/src/PIL/FliImagePlugin.py
@@ -159,7 +159,7 @@ def _seek(self, frame: int) -> None:
         framesize = i32(s)
 
         self.decodermaxblock = framesize
-        self.tile = [("fli", (0, 0) + self.size, self.__offset, None)]
+        self.tile = [ImageFile._Tile("fli", (0, 0) + self.size, self.__offset, None)]
 
         self.__offset += framesize
 
diff --git a/src/PIL/FpxImagePlugin.py b/src/PIL/FpxImagePlugin.py
index 64d48635e8b..8fef51076b4 100644
--- a/src/PIL/FpxImagePlugin.py
+++ b/src/PIL/FpxImagePlugin.py
@@ -166,7 +166,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
 
             if compression == 0:
                 self.tile.append(
-                    (
+                    ImageFile._Tile(
                         "raw",
                         (x, y, x1, y1),
                         i32(s, i) + 28,
@@ -177,7 +177,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
             elif compression == 1:
                 # FIXME: the fill decoder is not implemented
                 self.tile.append(
-                    (
+                    ImageFile._Tile(
                         "fill",
                         (x, y, x1, y1),
                         i32(s, i) + 28,
@@ -205,7 +205,7 @@ def _open_subimage(self, index: int = 1, subimage: int = 0) -> None:
                     jpegmode = rawmode
 
                 self.tile.append(
-                    (
+                    ImageFile._Tile(
                         "jpeg",
                         (x, y, x1, y1),
                         i32(s, i) + 28,
diff --git a/src/PIL/FtexImagePlugin.py b/src/PIL/FtexImagePlugin.py
index 5acbb4912f7..ddb469bc332 100644
--- a/src/PIL/FtexImagePlugin.py
+++ b/src/PIL/FtexImagePlugin.py
@@ -93,9 +93,9 @@ def _open(self) -> None:
 
         if format == Format.DXT1:
             self._mode = "RGBA"
-            self.tile = [("bcn", (0, 0) + self.size, 0, 1)]
+            self.tile = [ImageFile._Tile("bcn", (0, 0) + self.size, 0, (1,))]
         elif format == Format.UNCOMPRESSED:
-            self.tile = [("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
+            self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, ("RGB", 0, 1))]
         else:
             msg = f"Invalid texture compression format: {repr(format)}"
             raise ValueError(msg)
diff --git a/src/PIL/GdImageFile.py b/src/PIL/GdImageFile.py
index 88b87a22cd6..f1b4969f2c4 100644
--- a/src/PIL/GdImageFile.py
+++ b/src/PIL/GdImageFile.py
@@ -72,7 +72,7 @@ def _open(self) -> None:
         )
 
         self.tile = [
-            (
+            ImageFile._Tile(
                 "raw",
                 (0, 0) + self.size,
                 7 + true_color_offset + 4 + 256 * 4,
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
index fa8137fb935..22539925fee 100644
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -407,7 +407,7 @@ def _rgb(color: int) -> tuple[int, int, int]:
                 elif self.mode not in ("RGB", "RGBA"):
                     transparency = frame_transparency
             self.tile = [
-                (
+                ImageFile._Tile(
                     "gif",
                     (x0, y0, x1, y1),
                     self.__offset,
diff --git a/src/PIL/IcoImagePlugin.py b/src/PIL/IcoImagePlugin.py
index d0605b75a46..144fe0b6321 100644
--- a/src/PIL/IcoImagePlugin.py
+++ b/src/PIL/IcoImagePlugin.py
@@ -228,7 +228,7 @@ def frame(self, idx: int) -> Image.Image:
             # change tile dimension to only encompass XOR image
             im._size = (im.size[0], int(im.size[1] / 2))
             d, e, o, a = im.tile[0]
-            im.tile[0] = d, (0, 0) + im.size, o, a
+            im.tile[0] = ImageFile._Tile(d, (0, 0) + im.size, o, a)
 
             # figure out where AND mask image starts
             if header.bpp == 32:
diff --git a/src/PIL/ImImagePlugin.py b/src/PIL/ImImagePlugin.py
index b9416508987..f9f47348c66 100644
--- a/src/PIL/ImImagePlugin.py
+++ b/src/PIL/ImImagePlugin.py
@@ -253,7 +253,11 @@ def _open(self) -> None:
                 # use bit decoder (if necessary)
                 bits = int(self.rawmode[2:])
                 if bits not in [8, 16, 32]:
-                    self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))]
+                    self.tile = [
+                        ImageFile._Tile(
+                            "bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1)
+                        )
+                    ]
                     return
             except ValueError:
                 pass
@@ -263,13 +267,17 @@ def _open(self) -> None:
             # ever stumbled upon such a file ;-)
             size = self.size[0] * self.size[1]
             self.tile = [
-                ("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
-                ("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
-                ("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)),
+                ImageFile._Tile("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
+                ImageFile._Tile("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
+                ImageFile._Tile(
+                    "raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)
+                ),
             ]
         else:
             # LabEye/IFUNC files
-            self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
+            self.tile = [
+                ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
+            ]
 
     @property
     def n_frames(self) -> int:
@@ -295,7 +303,9 @@ def seek(self, frame: int) -> None:
 
         self.fp = self._fp
 
-        self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]
+        self.tile = [
+            ImageFile._Tile("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))
+        ]
 
     def tell(self) -> int:
         return self.frame
diff --git a/src/PIL/Image.py b/src/PIL/Image.py
index 95b4c64eef4..04f73870a29 100644
--- a/src/PIL/Image.py
+++ b/src/PIL/Image.py
@@ -3641,7 +3641,10 @@ def merge(mode: str, bands: Sequence[Image]) -> Image:
 
 def register_open(
     id: str,
-    factory: Callable[[IO[bytes], str | bytes], ImageFile.ImageFile],
+    factory: (
+        Callable[[IO[bytes], str | bytes], ImageFile.ImageFile]
+        | type[ImageFile.ImageFile]
+    ),
     accept: Callable[[bytes], bool | str] | None = None,
 ) -> None:
     """
diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index a1c33f21974..fce1591ef5d 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -34,12 +34,15 @@
 import os
 import struct
 import sys
-from typing import IO, Any, NamedTuple
+from typing import IO, TYPE_CHECKING, Any, NamedTuple
 
 from . import Image
 from ._deprecate import deprecate
 from ._util import is_path
 
+if TYPE_CHECKING:
+    from ._typing import StrOrBytesPath
+
 MAXBLOCK = 65536
 
 SAFEBLOCK = 1024 * 1024
@@ -107,32 +110,34 @@ class _Tile(NamedTuple):
 class ImageFile(Image.Image):
     """Base class for image file format handlers."""
 
-    def __init__(self, fp=None, filename=None):
+    def __init__(
+        self, fp: StrOrBytesPath | IO[bytes] | None = None, filename: str | bytes | None = None
+    ) -> None:
         super().__init__()
 
         self._min_frame = 0
 
-        self.custom_mimetype = None
+        self.custom_mimetype: str | None = None
 
-        self.tile = None
+        self.tile: list[_Tile] = []
         """ A list of tile descriptors, or ``None`` """
 
         self.readonly = 1  # until we know better
 
-        self.decoderconfig = ()
+        self.decoderconfig: tuple[Any, ...] = ()
         self.decodermaxblock = MAXBLOCK
 
         if is_path(fp):
             # filename
             self.fp = open(fp, "rb")
-            self.filename = fp
+            self.filename = os.path.realpath(os.fspath(fp))
             self._exclusive_fp = True
         else:
             # stream
             self.fp = fp
-            self.filename = filename
+            self.filename = filename if filename is not None else ""
             # can be overridden
-            self._exclusive_fp = None
+            self._exclusive_fp = False
 
         try:
             try:
@@ -155,6 +160,9 @@ def __init__(self, fp=None, filename=None):
                 self.fp.close()
             raise
 
+    def _open(self) -> None:
+        pass
+
     def get_format_mimetype(self) -> str | None:
         if self.custom_mimetype:
             return self.custom_mimetype
@@ -178,7 +186,7 @@ def verify(self) -> None:
     def load(self) -> Image.core.PixelAccess | None:
         """Load image data based on tile list"""
 
-        if self.tile is None:
+        if not self.tile and self._im is None:
             msg = "cannot load this image"
             raise OSError(msg)
 
@@ -214,6 +222,7 @@ def load(self) -> Image.core.PixelAccess | None:
                 args = (args, 0, 1)
             if (
                 decoder_name == "raw"
+                and isinstance(args, tuple)
                 and len(args) >= 3
                 and args[0] == self.mode
                 and args[0] in Image._MAPMODES
diff --git a/src/PIL/ImtImagePlugin.py b/src/PIL/ImtImagePlugin.py
index abb3fb762e7..594c56513cd 100644
--- a/src/PIL/ImtImagePlugin.py
+++ b/src/PIL/ImtImagePlugin.py
@@ -58,7 +58,7 @@ def _open(self) -> None:
             if s == b"\x0C":
                 # image data begins
                 self.tile = [
-                    (
+                    ImageFile._Tile(
                         "raw",
                         (0, 0) + self.size,
                         self.fp.tell() - len(buffer),
diff --git a/src/PIL/IptcImagePlugin.py b/src/PIL/IptcImagePlugin.py
index a8cd38218f1..60ab7c83f37 100644
--- a/src/PIL/IptcImagePlugin.py
+++ b/src/PIL/IptcImagePlugin.py
@@ -147,7 +147,9 @@ def _open(self) -> None:
 
         # tile
         if tag == (8, 10):
-            self.tile = [("iptc", (0, 0) + self.size, offset, compression)]
+            self.tile = [
+                ImageFile._Tile("iptc", (0, 0) + self.size, offset, compression)
+            ]
 
     def load(self) -> Image.core.PixelAccess | None:
         if len(self.tile) != 1 or self.tile[0][0] != "iptc":
diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py
index 02c2e48cfb4..cfbf7846093 100644
--- a/src/PIL/Jpeg2KImagePlugin.py
+++ b/src/PIL/Jpeg2KImagePlugin.py
@@ -286,7 +286,7 @@ def _open(self) -> None:
                 length = -1
 
         self.tile = [
-            (
+            ImageFile._Tile(
                 "jpeg2k",
                 (0, 0) + self.size,
                 0,
@@ -338,8 +338,9 @@ def load(self) -> Image.core.PixelAccess | None:
 
             # Update the reduce and layers settings
             t = self.tile[0]
+            assert isinstance(t[3], tuple)
             t3 = (t[3][0], self._reduce, self.layers, t[3][3], t[3][4])
-            self.tile = [(t[0], (0, 0) + self.size, t[2], t3)]
+            self.tile = [ImageFile._Tile(t[0], (0, 0) + self.size, t[2], t3)]
 
         return ImageFile.ImageFile.load(self)
 
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index bd4539be420..1f54b7e92a2 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -372,7 +372,9 @@ def _open(self) -> None:
                     rawmode = self.mode
                     if self.mode == "CMYK":
                         rawmode = "CMYK;I"  # assume adobe conventions
-                    self.tile = [("jpeg", (0, 0) + self.size, 0, (rawmode, ""))]
+                    self.tile = [
+                        ImageFile._Tile("jpeg", (0, 0) + self.size, 0, (rawmode, ""))
+                    ]
                     # self.__offset = self.fp.tell()
                     break
                 s = self.fp.read(1)
@@ -423,6 +425,7 @@ def draft(
         scale = 1
         original_size = self.size
 
+        assert isinstance(a, tuple)
         if a[0] == "RGB" and mode in ["L", "YCbCr"]:
             self._mode = mode
             a = mode, ""
@@ -432,6 +435,7 @@ def draft(
             for s in [8, 4, 2, 1]:
                 if scale >= s:
                     break
+            assert e is not None
             e = (
                 e[0],
                 e[1],
@@ -441,7 +445,7 @@ def draft(
             self._size = ((self.size[0] + s - 1) // s, (self.size[1] + s - 1) // s)
             scale = s
 
-        self.tile = [(d, e, o, a)]
+        self.tile = [ImageFile._Tile(d, e, o, a)]
         self.decoderconfig = (scale, 0)
 
         box = (0, 0, original_size[0] / scale, original_size[1] / scale)
diff --git a/src/PIL/McIdasImagePlugin.py b/src/PIL/McIdasImagePlugin.py
index 27972236c0a..5dd031be369 100644
--- a/src/PIL/McIdasImagePlugin.py
+++ b/src/PIL/McIdasImagePlugin.py
@@ -67,7 +67,9 @@ def _open(self) -> None:
         offset = w[34] + w[15]
         stride = w[15] + w[10] * w[11] * w[14]
 
-        self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))]
+        self.tile = [
+            ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride, 1))
+        ]
 
 
 # --------------------------------------------------------------------
diff --git a/src/PIL/MpoImagePlugin.py b/src/PIL/MpoImagePlugin.py
index 5ed9f56a164..71f89a09a85 100644
--- a/src/PIL/MpoImagePlugin.py
+++ b/src/PIL/MpoImagePlugin.py
@@ -26,6 +26,7 @@
 
 from . import (
     Image,
+    ImageFile,
     ImageSequence,
     JpegImagePlugin,
     TiffImagePlugin,
@@ -145,7 +146,9 @@ def seek(self, frame: int) -> None:
         if self.info.get("exif") != original_exif:
             self._reload_exif()
 
-        self.tile = [("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])]
+        self.tile = [
+            ImageFile._Tile("jpeg", (0, 0) + self.size, self.offset, self.tile[0][-1])
+        ]
         self.__frame = frame
 
     def tell(self) -> int:
diff --git a/src/PIL/MspImagePlugin.py b/src/PIL/MspImagePlugin.py
index 40e5fa435a8..c9260325499 100644
--- a/src/PIL/MspImagePlugin.py
+++ b/src/PIL/MspImagePlugin.py
@@ -70,9 +70,9 @@ def _open(self) -> None:
         self._size = i16(s, 4), i16(s, 6)
 
         if s[:4] == b"DanM":
-            self.tile = [("raw", (0, 0) + self.size, 32, ("1", 0, 1))]
+            self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 32, ("1", 0, 1))]
         else:
-            self.tile = [("MSP", (0, 0) + self.size, 32, None)]
+            self.tile = [ImageFile._Tile("MSP", (0, 0) + self.size, 32, None)]
 
 
 class MspDecoder(ImageFile.PyDecoder):
diff --git a/src/PIL/PcdImagePlugin.py b/src/PIL/PcdImagePlugin.py
index d9970405c7a..e8ea800a42c 100644
--- a/src/PIL/PcdImagePlugin.py
+++ b/src/PIL/PcdImagePlugin.py
@@ -47,7 +47,7 @@ def _open(self) -> None:
 
         self._mode = "RGB"
         self._size = 768, 512  # FIXME: not correct for rotated images!
-        self.tile = [("pcd", (0, 0) + self.size, 96 * 2048, None)]
+        self.tile = [ImageFile._Tile("pcd", (0, 0) + self.size, 96 * 2048, None)]
 
     def load_end(self) -> None:
         if self.tile_post_rotate:
diff --git a/src/PIL/PcxImagePlugin.py b/src/PIL/PcxImagePlugin.py
index c3c9dd3f137..8445d5cc740 100644
--- a/src/PIL/PcxImagePlugin.py
+++ b/src/PIL/PcxImagePlugin.py
@@ -128,7 +128,9 @@ def _open(self) -> None:
         bbox = (0, 0) + self.size
         logger.debug("size: %sx%s", *self.size)
 
-        self.tile = [("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))]
+        self.tile = [
+            ImageFile._Tile("pcx", bbox, self.fp.tell(), (rawmode, planes * stride))
+        ]
 
 
 # --------------------------------------------------------------------
diff --git a/src/PIL/PixarImagePlugin.py b/src/PIL/PixarImagePlugin.py
index 887b6568bf7..36f565f1c20 100644
--- a/src/PIL/PixarImagePlugin.py
+++ b/src/PIL/PixarImagePlugin.py
@@ -61,7 +61,9 @@ def _open(self) -> None:
         # FIXME: to be continued...
 
         # create tile descriptor (assuming "dumped")
-        self.tile = [("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))]
+        self.tile = [
+            ImageFile._Tile("raw", (0, 0) + self.size, 1024, (self.mode, 0, 1))
+        ]
 
 
 #
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 7bdaa9fe740..e4d745e67ff 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -151,7 +151,9 @@ def _open(self) -> None:
                     decoder_name = "ppm"
 
             args = rawmode if decoder_name == "raw" else (rawmode, maxval)
-        self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]
+        self.tile = [
+            ImageFile._Tile(decoder_name, (0, 0) + self.size, self.fp.tell(), args)
+        ]
 
 
 #
diff --git a/src/PIL/PsdImagePlugin.py b/src/PIL/PsdImagePlugin.py
index cc99cd6d8cb..8ff5e39088a 100644
--- a/src/PIL/PsdImagePlugin.py
+++ b/src/PIL/PsdImagePlugin.py
@@ -277,8 +277,8 @@ def read(size: int) -> bytes:
 
 def _maketile(
     file: IO[bytes], mode: str, bbox: tuple[int, int, int, int], channels: int
-) -> list[ImageFile._Tile] | None:
-    tiles = None
+) -> list[ImageFile._Tile]:
+    tiles = []
     read = file.read
 
     compression = i16(read(2))
@@ -291,7 +291,6 @@ def _maketile(
     if compression == 0:
         #
         # raw compression
-        tiles = []
         for channel in range(channels):
             layer = mode[channel]
             if mode == "CMYK":
@@ -303,7 +302,6 @@ def _maketile(
         #
         # packbits compression
         i = 0
-        tiles = []
         bytecount = read(channels * ysize * 2)
         offset = file.tell()
         for channel in range(channels):
diff --git a/src/PIL/QoiImagePlugin.py b/src/PIL/QoiImagePlugin.py
index c81d381fb6c..eaf03140291 100644
--- a/src/PIL/QoiImagePlugin.py
+++ b/src/PIL/QoiImagePlugin.py
@@ -32,7 +32,7 @@ def _open(self) -> None:
         self._mode = "RGB" if channels == 3 else "RGBA"
 
         self.fp.seek(1, os.SEEK_CUR)  # colorspace
-        self.tile = [("qoi", (0, 0) + self._size, self.fp.tell(), None)]
+        self.tile = [ImageFile._Tile("qoi", (0, 0) + self._size, self.fp.tell(), None)]
 
 
 class QoiDecoder(ImageFile.PyDecoder):
diff --git a/src/PIL/SgiImagePlugin.py b/src/PIL/SgiImagePlugin.py
index 50d97910932..2c205a926be 100644
--- a/src/PIL/SgiImagePlugin.py
+++ b/src/PIL/SgiImagePlugin.py
@@ -109,19 +109,28 @@ def _open(self) -> None:
             pagesize = xsize * ysize * bpc
             if bpc == 2:
                 self.tile = [
-                    ("SGI16", (0, 0) + self.size, headlen, (self.mode, 0, orientation))
+                    ImageFile._Tile(
+                        "SGI16",
+                        (0, 0) + self.size,
+                        headlen,
+                        (self.mode, 0, orientation),
+                    )
                 ]
             else:
                 self.tile = []
                 offset = headlen
                 for layer in self.mode:
                     self.tile.append(
-                        ("raw", (0, 0) + self.size, offset, (layer, 0, orientation))
+                        ImageFile._Tile(
+                            "raw", (0, 0) + self.size, offset, (layer, 0, orientation)
+                        )
                     )
                     offset += pagesize
         elif compression == 1:
             self.tile = [
-                ("sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc))
+                ImageFile._Tile(
+                    "sgi_rle", (0, 0) + self.size, headlen, (rawmode, orientation, bpc)
+                )
             ]
 
 
diff --git a/src/PIL/SpiderImagePlugin.py b/src/PIL/SpiderImagePlugin.py
index 7045ab566de..075073f9fe3 100644
--- a/src/PIL/SpiderImagePlugin.py
+++ b/src/PIL/SpiderImagePlugin.py
@@ -154,7 +154,9 @@ def _open(self) -> None:
             self.rawmode = "F;32F"
         self._mode = "F"
 
-        self.tile = [("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))]
+        self.tile = [
+            ImageFile._Tile("raw", (0, 0) + self.size, offset, (self.rawmode, 0, 1))
+        ]
         self._fp = self.fp  # FIXME: hack
 
     @property
diff --git a/src/PIL/SunImagePlugin.py b/src/PIL/SunImagePlugin.py
index 4e098474ab9..8912379ea3e 100644
--- a/src/PIL/SunImagePlugin.py
+++ b/src/PIL/SunImagePlugin.py
@@ -124,9 +124,13 @@ def _open(self) -> None:
         # (https://www.fileformat.info/format/sunraster/egff.htm)
 
         if file_type in (0, 1, 3, 4, 5):
-            self.tile = [("raw", (0, 0) + self.size, offset, (rawmode, stride))]
+            self.tile = [
+                ImageFile._Tile("raw", (0, 0) + self.size, offset, (rawmode, stride))
+            ]
         elif file_type == 2:
-            self.tile = [("sun_rle", (0, 0) + self.size, offset, rawmode)]
+            self.tile = [
+                ImageFile._Tile("sun_rle", (0, 0) + self.size, offset, rawmode)
+            ]
         else:
             msg = "Unsupported Sun Raster file type"
             raise SyntaxError(msg)
diff --git a/src/PIL/TgaImagePlugin.py b/src/PIL/TgaImagePlugin.py
index 5aea363c064..90d5b5cf4ee 100644
--- a/src/PIL/TgaImagePlugin.py
+++ b/src/PIL/TgaImagePlugin.py
@@ -137,7 +137,7 @@ def _open(self) -> None:
             if imagetype & 8:
                 # compressed
                 self.tile = [
-                    (
+                    ImageFile._Tile(
                         "tga_rle",
                         (0, 0) + self.size,
                         self.fp.tell(),
@@ -146,7 +146,7 @@ def _open(self) -> None:
                 ]
             else:
                 self.tile = [
-                    (
+                    ImageFile._Tile(
                         "raw",
                         (0, 0) + self.size,
                         self.fp.tell(),
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index ab23356d4f3..d05c08489a3 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1298,7 +1298,7 @@ def _load_libtiff(self) -> Image.core.PixelAccess | None:
         # (self._compression, (extents tuple),
         #   0, (rawmode, self._compression, fp))
         extents = self.tile[0][1]
-        args = list(self.tile[0][3])
+        args = self.tile[0][3]
 
         # To be nice on memory footprint, if there's a
         # file descriptor, use that instead of reading
@@ -1316,11 +1316,12 @@ def _load_libtiff(self) -> Image.core.PixelAccess | None:
             fp = False
 
         if fp:
-            args[2] = fp
+            assert isinstance(args, tuple)
+            args_list = list(args)
+            args_list[2] = fp
+            args = tuple(args_list)
 
-        decoder = Image._getdecoder(
-            self.mode, "libtiff", tuple(args), self.decoderconfig
-        )
+        decoder = Image._getdecoder(self.mode, "libtiff", args, self.decoderconfig)
         try:
             decoder.setimage(self.im, extents)
         except ValueError as e:
@@ -1538,7 +1539,7 @@ def _setup(self) -> None:
             # Offset in the tile tuple is 0, we go from 0,0 to
             # w,h, and we only do this once -- eds
             a = (rawmode, self._compression, False, self.tag_v2.offset)
-            self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a))
+            self.tile.append(ImageFile._Tile("libtiff", (0, 0, xsize, ysize), 0, a))
 
         elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:
             # striped image
@@ -1571,7 +1572,7 @@ def _setup(self) -> None:
 
                 args = (tile_rawmode, int(stride), 1)
                 self.tile.append(
-                    (
+                    ImageFile._Tile(
                         self._compression,
                         (x, y, min(x + w, xsize), min(y + h, ysize)),
                         offset,
diff --git a/src/PIL/WebPImagePlugin.py b/src/PIL/WebPImagePlugin.py
index fc8f4fc2edc..f8d6168bafd 100644
--- a/src/PIL/WebPImagePlugin.py
+++ b/src/PIL/WebPImagePlugin.py
@@ -142,7 +142,7 @@ def load(self) -> Image.core.PixelAccess | None:
             if self.fp and self._exclusive_fp:
                 self.fp.close()
             self.fp = BytesIO(data)
-            self.tile = [("raw", (0, 0) + self.size, 0, self.rawmode)]
+            self.tile = [ImageFile._Tile("raw", (0, 0) + self.size, 0, self.rawmode)]
 
         return super().load()
 
diff --git a/src/PIL/XVThumbImagePlugin.py b/src/PIL/XVThumbImagePlugin.py
index c84adaca215..5d1f201a454 100644
--- a/src/PIL/XVThumbImagePlugin.py
+++ b/src/PIL/XVThumbImagePlugin.py
@@ -73,7 +73,11 @@ def _open(self) -> None:
 
         self.palette = ImagePalette.raw("RGB", PALETTE)
 
-        self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1))]
+        self.tile = [
+            ImageFile._Tile(
+                "raw", (0, 0) + self.size, self.fp.tell(), (self.mode, 0, 1)
+            )
+        ]
 
 
 # --------------------------------------------------------------------
diff --git a/src/PIL/XbmImagePlugin.py b/src/PIL/XbmImagePlugin.py
index 6c2e32804e3..f3d490a840f 100644
--- a/src/PIL/XbmImagePlugin.py
+++ b/src/PIL/XbmImagePlugin.py
@@ -67,7 +67,7 @@ def _open(self) -> None:
         self._mode = "1"
         self._size = xsize, ysize
 
-        self.tile = [("xbm", (0, 0) + self.size, m.end(), None)]
+        self.tile = [ImageFile._Tile("xbm", (0, 0) + self.size, m.end(), None)]
 
 
 def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
diff --git a/src/PIL/XpmImagePlugin.py b/src/PIL/XpmImagePlugin.py
index 8d56331e6aa..1fc6c0c39d5 100644
--- a/src/PIL/XpmImagePlugin.py
+++ b/src/PIL/XpmImagePlugin.py
@@ -101,7 +101,9 @@ def _open(self) -> None:
         self._mode = "P"
         self.palette = ImagePalette.raw("RGB", b"".join(palette))
 
-        self.tile = [("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))]
+        self.tile = [
+            ImageFile._Tile("raw", (0, 0) + self.size, self.fp.tell(), ("P", 0, 1))
+        ]
 
     def load_read(self, read_bytes: int) -> bytes:
         #

From 8699f13a74f162552fa70644bfdbe9705f87f080 Mon Sep 17 00:00:00 2001
From: Andrew Murray <radarhere@users.noreply.github.com>
Date: Thu, 29 Aug 2024 22:15:43 +1000
Subject: [PATCH 2/2] Require fp parameter

---
 src/PIL/ImageFile.py       | 6 +++---
 src/PIL/JpegImagePlugin.py | 2 +-
 src/PIL/TiffImagePlugin.py | 2 +-
 3 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/PIL/ImageFile.py b/src/PIL/ImageFile.py
index fce1591ef5d..69fbafd3968 100644
--- a/src/PIL/ImageFile.py
+++ b/src/PIL/ImageFile.py
@@ -34,7 +34,7 @@
 import os
 import struct
 import sys
-from typing import IO, TYPE_CHECKING, Any, NamedTuple
+from typing import IO, TYPE_CHECKING, Any, NamedTuple, cast
 
 from . import Image
 from ._deprecate import deprecate
@@ -111,7 +111,7 @@ class ImageFile(Image.Image):
     """Base class for image file format handlers."""
 
     def __init__(
-        self, fp: StrOrBytesPath | IO[bytes] | None = None, filename: str | bytes | None = None
+        self, fp: StrOrBytesPath | IO[bytes], filename: str | bytes | None = None
     ) -> None:
         super().__init__()
 
@@ -134,7 +134,7 @@ def __init__(
             self._exclusive_fp = True
         else:
             # stream
-            self.fp = fp
+            self.fp = cast(IO[bytes], fp)
             self.filename = filename if filename is not None else ""
             # can be overridden
             self._exclusive_fp = False
diff --git a/src/PIL/JpegImagePlugin.py b/src/PIL/JpegImagePlugin.py
index 1f54b7e92a2..e3372a1c2a6 100644
--- a/src/PIL/JpegImagePlugin.py
+++ b/src/PIL/JpegImagePlugin.py
@@ -848,7 +848,7 @@ def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
 ##
 # Factory for making JPEG and MPO instances
 def jpeg_factory(
-    fp: IO[bytes] | None = None, filename: str | bytes | None = None
+    fp: IO[bytes], filename: str | bytes | None = None
 ) -> JpegImageFile | MpoImageFile:
     im = JpegImageFile(fp, filename)
     try:
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index d05c08489a3..ad898dc8e1b 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -1126,7 +1126,7 @@ class TiffImageFile(ImageFile.ImageFile):
 
     def __init__(
         self,
-        fp: StrOrBytesPath | IO[bytes] | None = None,
+        fp: StrOrBytesPath | IO[bytes],
         filename: str | bytes | None = None,
     ) -> None:
         self.tag_v2: ImageFileDirectory_v2
