Merge pull request #8319 from radarhere/type_hint

Added type hints
This commit is contained in:
Andrew Murray 2024-08-24 23:54:19 +10:00 committed by GitHub
commit 4721c31b19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 133 additions and 87 deletions

View File

@ -51,7 +51,7 @@ build_script:
test_script: test_script:
- cd c:\pillow - cd c:\pillow
- '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml numpy olefile pyroma' - '%PYTHON%\%EXECUTABLE% -m pip install pytest pytest-cov pytest-timeout defusedxml ipython numpy olefile pyroma'
- c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE% - c:\"Program Files (x86)"\"Windows Kits"\10\Debuggers\x86\gflags.exe /p /enable %PYTHON%\%EXECUTABLE%
- '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"' - '%PYTHON%\%EXECUTABLE% -c "from PIL import Image"'
- '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests' - '%PYTHON%\%EXECUTABLE% -m pytest -vx --cov PIL --cov Tests --cov-report term --cov-report xml Tests'

View File

@ -30,6 +30,7 @@ python3 -m pip install --upgrade pip
python3 -m pip install --upgrade wheel python3 -m pip install --upgrade wheel
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov

View File

@ -23,6 +23,7 @@ export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
python3 -m pip install coverage python3 -m pip install coverage
python3 -m pip install defusedxml python3 -m pip install defusedxml
python3 -m pip install ipython
python3 -m pip install olefile python3 -m pip install olefile
python3 -m pip install -U pytest python3 -m pip install -U pytest
python3 -m pip install -U pytest-cov python3 -m pip install -U pytest-cov

View File

@ -74,6 +74,7 @@ jobs:
perl perl
python3${{ matrix.python-minor-version }}-cython python3${{ matrix.python-minor-version }}-cython
python3${{ matrix.python-minor-version }}-devel python3${{ matrix.python-minor-version }}-devel
python3${{ matrix.python-minor-version }}-ipython
python3${{ matrix.python-minor-version }}-numpy python3${{ matrix.python-minor-version }}-numpy
python3${{ matrix.python-minor-version }}-sip python3${{ matrix.python-minor-version }}-sip
python3${{ matrix.python-minor-version }}-tkinter python3${{ matrix.python-minor-version }}-tkinter

View File

@ -684,6 +684,13 @@ class TestFileTiff:
with Image.open(outfile) as reloaded: with Image.open(outfile) as reloaded:
assert_image_equal_tofile(reloaded, infile) assert_image_equal_tofile(reloaded, infile)
def test_invalid_tiled_dimensions(self) -> None:
with open("Tests/images/tiff_tiled_planar_raw.tif", "rb") as fp:
data = fp.read()
b = BytesIO(data[:144] + b"\x02" + data[145:])
with pytest.raises(ValueError):
Image.open(b)
@pytest.mark.parametrize("mode", ("P", "PA")) @pytest.mark.parametrize("mode", ("P", "PA"))
def test_palette(self, mode: str, tmp_path: Path) -> None: def test_palette(self, mode: str, tmp_path: Path) -> None:
outfile = str(tmp_path / "temp.tif") outfile = str(tmp_path / "temp.tif")

View File

@ -42,6 +42,12 @@ try:
except ImportError: except ImportError:
ElementTree = None ElementTree = None
PrettyPrinter: type | None
try:
from IPython.lib.pretty import PrettyPrinter
except ImportError:
PrettyPrinter = None
# Deprecation helper # Deprecation helper
def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image: def helper_image_new(mode: str, size: tuple[int, int]) -> Image.Image:
@ -91,16 +97,15 @@ class TestImage:
# with pytest.raises(MemoryError): # with pytest.raises(MemoryError):
# Image.new("L", (1000000, 1000000)) # Image.new("L", (1000000, 1000000))
@pytest.mark.skipif(PrettyPrinter is None, reason="IPython is not installed")
def test_repr_pretty(self) -> None: def test_repr_pretty(self) -> None:
class Pretty:
def text(self, text: str) -> None:
self.pretty_output = text
im = Image.new("L", (100, 100)) im = Image.new("L", (100, 100))
p = Pretty() output = io.StringIO()
assert PrettyPrinter is not None
p = PrettyPrinter(output)
im._repr_pretty_(p, False) im._repr_pretty_(p, False)
assert p.pretty_output == "<PIL.Image.Image image mode=L size=100x100>" assert output.getvalue() == "<PIL.Image.Image image mode=L size=100x100>"
def test_open_formats(self) -> None: def test_open_formats(self) -> None:
PNGFILE = "Tests/images/hopper.png" PNGFILE = "Tests/images/hopper.png"

View File

@ -412,9 +412,8 @@ class TestPyEncoder(CodecsTest):
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_pyfd() encoder.encode_to_pyfd()
fh = BytesIO()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
encoder.encode_to_file(fh, 0) encoder.encode_to_file(0, 0)
def test_zero_height(self) -> None: def test_zero_height(self) -> None:
with pytest.raises(UnidentifiedImageError): with pytest.raises(UnidentifiedImageError):

View File

@ -5,8 +5,6 @@ import sys
from io import BytesIO from io import BytesIO
from pathlib import Path from pathlib import Path
import pytest
from PIL import Image, PSDraw from PIL import Image, PSDraw
@ -49,15 +47,14 @@ def test_draw_postscript(tmp_path: Path) -> None:
assert os.path.getsize(tempfile) > 0 assert os.path.getsize(tempfile) > 0
@pytest.mark.parametrize("buffer", (True, False)) def test_stdout() -> None:
def test_stdout(buffer: bool) -> None:
# Temporarily redirect stdout # Temporarily redirect stdout
old_stdout = sys.stdout old_stdout = sys.stdout
class MyStdOut: class MyStdOut:
buffer = BytesIO() buffer = BytesIO()
mystdout: MyStdOut | BytesIO = MyStdOut() if buffer else BytesIO() mystdout = MyStdOut()
sys.stdout = mystdout sys.stdout = mystdout
@ -67,6 +64,4 @@ def test_stdout(buffer: bool) -> None:
# Reset stdout # Reset stdout
sys.stdout = old_stdout sys.stdout = old_stdout
if isinstance(mystdout, MyStdOut): assert mystdout.buffer.getvalue() != b""
mystdout = mystdout.buffer
assert mystdout.getvalue() != b""

View File

@ -362,6 +362,7 @@ Classes
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
.. autoclass:: PIL.Image.ImagePointHandler .. autoclass:: PIL.Image.ImagePointHandler
.. autoclass:: PIL.Image.ImagePointTransform
.. autoclass:: PIL.Image.ImageTransformHandler .. autoclass:: PIL.Image.ImageTransformHandler
Protocols Protocols

View File

@ -319,11 +319,11 @@ class IcoImageFile(ImageFile.ImageFile):
self.load() self.load()
@property @property
def size(self): def size(self) -> tuple[int, int]:
return self._size return self._size
@size.setter @size.setter
def size(self, value): def size(self, value: tuple[int, int]) -> None:
if value not in self.info["sizes"]: if value not in self.info["sizes"]:
msg = "This is not one of the allowed sizes of this image" msg = "This is not one of the allowed sizes of this image"
raise ValueError(msg) raise ValueError(msg)

View File

@ -221,6 +221,8 @@ if TYPE_CHECKING:
import mmap import mmap
from xml.etree.ElementTree import Element from xml.etree.ElementTree import Element
from IPython.lib.pretty import PrettyPrinter
from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin from . import ImageFile, ImageFilter, ImagePalette, TiffImagePlugin
from ._typing import NumpyArray, StrOrBytesPath, TypeGuard from ._typing import NumpyArray, StrOrBytesPath, TypeGuard
ID: list[str] = [] ID: list[str] = []
@ -468,43 +470,53 @@ def _getencoder(
# Simple expression analyzer # Simple expression analyzer
class _E: class ImagePointTransform:
"""
Used with :py:meth:`~PIL.Image.Image.point` for single band images with more than
8 bits, this represents an affine transformation, where the value is multiplied by
``scale`` and ``offset`` is added.
"""
def __init__(self, scale: float, offset: float) -> None: def __init__(self, scale: float, offset: float) -> None:
self.scale = scale self.scale = scale
self.offset = offset self.offset = offset
def __neg__(self) -> _E: def __neg__(self) -> ImagePointTransform:
return _E(-self.scale, -self.offset) return ImagePointTransform(-self.scale, -self.offset)
def __add__(self, other: _E | float) -> _E: def __add__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, _E): if isinstance(other, ImagePointTransform):
return _E(self.scale + other.scale, self.offset + other.offset) return ImagePointTransform(
return _E(self.scale, self.offset + other) self.scale + other.scale, self.offset + other.offset
)
return ImagePointTransform(self.scale, self.offset + other)
__radd__ = __add__ __radd__ = __add__
def __sub__(self, other: _E | float) -> _E: def __sub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return self + -other return self + -other
def __rsub__(self, other: _E | float) -> _E: def __rsub__(self, other: ImagePointTransform | float) -> ImagePointTransform:
return other + -self return other + -self
def __mul__(self, other: _E | float) -> _E: def __mul__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, _E): if isinstance(other, ImagePointTransform):
return NotImplemented return NotImplemented
return _E(self.scale * other, self.offset * other) return ImagePointTransform(self.scale * other, self.offset * other)
__rmul__ = __mul__ __rmul__ = __mul__
def __truediv__(self, other: _E | float) -> _E: def __truediv__(self, other: ImagePointTransform | float) -> ImagePointTransform:
if isinstance(other, _E): if isinstance(other, ImagePointTransform):
return NotImplemented return NotImplemented
return _E(self.scale / other, self.offset / other) return ImagePointTransform(self.scale / other, self.offset / other)
def _getscaleoffset(expr) -> tuple[float, float]: def _getscaleoffset(
a = expr(_E(1, 0)) expr: Callable[[ImagePointTransform], ImagePointTransform | float]
return (a.scale, a.offset) if isinstance(a, _E) else (0, a) ) -> tuple[float, float]:
a = expr(ImagePointTransform(1, 0))
return (a.scale, a.offset) if isinstance(a, ImagePointTransform) else (0, a)
# -------------------------------------------------------------------- # --------------------------------------------------------------------
@ -677,7 +689,7 @@ class Image:
id(self), id(self),
) )
def _repr_pretty_(self, p, cycle: bool) -> None: def _repr_pretty_(self, p: PrettyPrinter, cycle: bool) -> None:
"""IPython plain text display support""" """IPython plain text display support"""
# Same as __repr__ but without unpredictable id(self), # Same as __repr__ but without unpredictable id(self),
@ -1880,7 +1892,13 @@ class Image:
def point( def point(
self, self,
lut: Sequence[float] | NumpyArray | Callable[[int], float] | ImagePointHandler, lut: (
Sequence[float]
| NumpyArray
| Callable[[int], float]
| Callable[[ImagePointTransform], ImagePointTransform | float]
| ImagePointHandler
),
mode: str | None = None, mode: str | None = None,
) -> Image: ) -> Image:
""" """
@ -1897,7 +1915,7 @@ class Image:
object:: object::
class Example(Image.ImagePointHandler): class Example(Image.ImagePointHandler):
def point(self, data): def point(self, im: Image) -> Image:
# Return result # Return result
:param mode: Output mode (default is same as input). This can only be used if :param mode: Output mode (default is same as input). This can only be used if
the source image has mode "L" or "P", and the output has mode "1" or the the source image has mode "L" or "P", and the output has mode "1" or the
@ -1916,10 +1934,10 @@ class Image:
# check if the function can be used with point_transform # check if the function can be used with point_transform
# UNDONE wiredfool -- I think this prevents us from ever doing # UNDONE wiredfool -- I think this prevents us from ever doing
# a gamma function point transform on > 8bit images. # a gamma function point transform on > 8bit images.
scale, offset = _getscaleoffset(lut) scale, offset = _getscaleoffset(lut) # type: ignore[arg-type]
return self._new(self.im.point_transform(scale, offset)) return self._new(self.im.point_transform(scale, offset))
# for other modes, convert the function to a table # for other modes, convert the function to a table
flatLut = [lut(i) for i in range(256)] * self.im.bands flatLut = [lut(i) for i in range(256)] * self.im.bands # type: ignore[arg-type]
else: else:
flatLut = lut flatLut = lut
@ -2855,11 +2873,11 @@ class Image:
self, self,
box: tuple[int, int, int, int], box: tuple[int, int, int, int],
image: Image, image: Image,
method, method: Transform,
data, data: Sequence[float],
resample: int = Resampling.NEAREST, resample: int = Resampling.NEAREST,
fill: bool = True, fill: bool = True,
): ) -> None:
w = box[2] - box[0] w = box[2] - box[0]
h = box[3] - box[1] h = box[3] - box[1]
@ -3994,15 +4012,19 @@ class Exif(_ExifBase):
ifd[tag] = value ifd[tag] = value
return b"Exif\x00\x00" + head + ifd.tobytes(offset) return b"Exif\x00\x00" + head + ifd.tobytes(offset)
def get_ifd(self, tag): def get_ifd(self, tag: int) -> dict[int, Any]:
if tag not in self._ifds: if tag not in self._ifds:
if tag == ExifTags.IFD.IFD1: if tag == ExifTags.IFD.IFD1:
if self._info is not None and self._info.next != 0: if self._info is not None and self._info.next != 0:
self._ifds[tag] = self._get_ifd_dict(self._info.next) ifd = self._get_ifd_dict(self._info.next)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]: elif tag in [ExifTags.IFD.Exif, ExifTags.IFD.GPSInfo]:
offset = self._hidden_data.get(tag, self.get(tag)) offset = self._hidden_data.get(tag, self.get(tag))
if offset is not None: if offset is not None:
self._ifds[tag] = self._get_ifd_dict(offset, tag) ifd = self._get_ifd_dict(offset, tag)
if ifd is not None:
self._ifds[tag] = ifd
elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]: elif tag in [ExifTags.IFD.Interop, ExifTags.IFD.Makernote]:
if ExifTags.IFD.Exif not in self._ifds: if ExifTags.IFD.Exif not in self._ifds:
self.get_ifd(ExifTags.IFD.Exif) self.get_ifd(ExifTags.IFD.Exif)
@ -4059,7 +4081,9 @@ class Exif(_ExifBase):
(offset,) = struct.unpack(">L", data) (offset,) = struct.unpack(">L", data)
self.fp.seek(offset) self.fp.seek(offset)
camerainfo = {"ModelID": self.fp.read(4)} camerainfo: dict[str, int | bytes] = {
"ModelID": self.fp.read(4)
}
self.fp.read(4) self.fp.read(4)
# Seconds since 2000 # Seconds since 2000
@ -4075,16 +4099,18 @@ class Exif(_ExifBase):
][1] ][1]
camerainfo["Parallax"] = handler( camerainfo["Parallax"] = handler(
ImageFileDirectory_v2(), parallax, False ImageFileDirectory_v2(), parallax, False
) )[0]
self.fp.read(4) self.fp.read(4)
camerainfo["Category"] = self.fp.read(2) camerainfo["Category"] = self.fp.read(2)
makernote = {0x1101: dict(self._fixup_dict(camerainfo))} makernote = {0x1101: camerainfo}
self._ifds[tag] = makernote self._ifds[tag] = makernote
else: else:
# Interop # Interop
self._ifds[tag] = self._get_ifd_dict(tag_data, tag) ifd = self._get_ifd_dict(tag_data, tag)
if ifd is not None:
self._ifds[tag] = ifd
ifd = self._ifds.get(tag, {}) ifd = self._ifds.get(tag, {})
if tag == ExifTags.IFD.Exif and self._hidden_data: if tag == ExifTags.IFD.Exif and self._hidden_data:
ifd = { ifd = {

View File

@ -31,6 +31,7 @@ from __future__ import annotations
import abc import abc
import io import io
import itertools import itertools
import os
import struct import struct
import sys import sys
from typing import IO, Any, NamedTuple from typing import IO, Any, NamedTuple
@ -555,7 +556,7 @@ def _encode_tile(
fp: IO[bytes], fp: IO[bytes],
tile: list[_Tile], tile: list[_Tile],
bufsize: int, bufsize: int,
fh, fh: int | None,
exc: BaseException | None = None, exc: BaseException | None = None,
) -> None: ) -> None:
for encoder_name, extents, offset, args in tile: for encoder_name, extents, offset, args in tile:
@ -577,6 +578,7 @@ def _encode_tile(
break break
else: else:
# slight speedup: compress to real file object # slight speedup: compress to real file object
assert fh is not None
errcode = encoder.encode_to_file(fh, bufsize) errcode = encoder.encode_to_file(fh, bufsize)
if errcode < 0: if errcode < 0:
raise _get_oserror(errcode, encoder=True) from exc raise _get_oserror(errcode, encoder=True) from exc
@ -801,7 +803,7 @@ class PyEncoder(PyCodec):
self.fd.write(data) self.fd.write(data)
return bytes_consumed, errcode return bytes_consumed, errcode
def encode_to_file(self, fh: IO[bytes], bufsize: int) -> int: def encode_to_file(self, fh: int, bufsize: int) -> int:
""" """
:param fh: File handle. :param fh: File handle.
:param bufsize: Buffer size. :param bufsize: Buffer size.
@ -814,5 +816,5 @@ class PyEncoder(PyCodec):
while errcode == 0: while errcode == 0:
status, errcode, buf = self.encode(bufsize) status, errcode, buf = self.encode(bufsize)
if status > 0: if status > 0:
fh.write(buf[status:]) os.write(fh, buf[status:])
return errcode return errcode

View File

@ -214,7 +214,7 @@ def getiptcinfo(
# as 4-byte integers, so we cannot use the get method...) # as 4-byte integers, so we cannot use the get method...)
try: try:
data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK] data = im.tag_v2[TiffImagePlugin.IPTC_NAA_CHUNK]
except (AttributeError, KeyError): except KeyError:
pass pass
if data is None: if data is None:

View File

@ -17,7 +17,7 @@
from __future__ import annotations from __future__ import annotations
import sys import sys
from typing import TYPE_CHECKING from typing import IO, TYPE_CHECKING
from . import EpsImagePlugin from . import EpsImagePlugin
@ -28,15 +28,12 @@ from . import EpsImagePlugin
class PSDraw: class PSDraw:
""" """
Sets up printing to the given file. If ``fp`` is omitted, Sets up printing to the given file. If ``fp`` is omitted,
``sys.stdout.buffer`` or ``sys.stdout`` is assumed. ``sys.stdout.buffer`` is assumed.
""" """
def __init__(self, fp=None): def __init__(self, fp: IO[bytes] | None = None) -> None:
if not fp: if not fp:
try:
fp = sys.stdout.buffer fp = sys.stdout.buffer
except AttributeError:
fp = sys.stdout
self.fp = fp self.fp = fp
def begin_document(self, id: str | None = None) -> None: def begin_document(self, id: str | None = None) -> None:

View File

@ -624,12 +624,12 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._tagdata: dict[int, bytes] = {} self._tagdata: dict[int, bytes] = {}
self.tagtype = {} # added 2008-06-05 by Florian Hoech self.tagtype = {} # added 2008-06-05 by Florian Hoech
self._next = None self._next = None
self._offset = None self._offset: int | None = None
def __str__(self) -> str: def __str__(self) -> str:
return str(dict(self)) return str(dict(self))
def named(self): def named(self) -> dict[str, Any]:
""" """
:returns: dict of name|key: value :returns: dict of name|key: value
@ -643,7 +643,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def __len__(self) -> int: def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v2)) return len(set(self._tagdata) | set(self._tags_v2))
def __getitem__(self, tag): def __getitem__(self, tag: int) -> Any:
if tag not in self._tags_v2: # unpack on the fly if tag not in self._tags_v2: # unpack on the fly
data = self._tagdata[tag] data = self._tagdata[tag]
typ = self.tagtype[tag] typ = self.tagtype[tag]
@ -855,7 +855,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
raise OSError(msg) raise OSError(msg)
return ret return ret
def load(self, fp): def load(self, fp: IO[bytes]) -> None:
self.reset() self.reset()
self._offset = fp.tell() self._offset = fp.tell()
@ -1098,7 +1098,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
for legacy_api in (False, True): for legacy_api in (False, True):
self._setitem(tag, value, legacy_api) self._setitem(tag, value, legacy_api)
def __getitem__(self, tag): def __getitem__(self, tag: int) -> Any:
if tag not in self._tags_v1: # unpack on the fly if tag not in self._tags_v1: # unpack on the fly
data = self._tagdata[tag] data = self._tagdata[tag]
typ = self.tagtype[tag] typ = self.tagtype[tag]
@ -1124,11 +1124,15 @@ class TiffImageFile(ImageFile.ImageFile):
format_description = "Adobe TIFF" format_description = "Adobe TIFF"
_close_exclusive_fp_after_loading = False _close_exclusive_fp_after_loading = False
def __init__(self, fp=None, filename=None): def __init__(
self.tag_v2 = None self,
fp: StrOrBytesPath | IO[bytes] | None = None,
filename: str | bytes | None = None,
) -> None:
self.tag_v2: ImageFileDirectory_v2
""" Image file directory (tag dictionary) """ """ Image file directory (tag dictionary) """
self.tag = None self.tag: ImageFileDirectory_v1
""" Legacy tag entries """ """ Legacy tag entries """
super().__init__(fp, filename) super().__init__(fp, filename)
@ -1143,9 +1147,6 @@ class TiffImageFile(ImageFile.ImageFile):
self.tag_v2 = ImageFileDirectory_v2(ifh) self.tag_v2 = ImageFileDirectory_v2(ifh)
# legacy IFD entries will be filled in later
self.ifd: ImageFileDirectory_v1 | None = None
# setup frame pointers # setup frame pointers
self.__first = self.__next = self.tag_v2.next self.__first = self.__next = self.tag_v2.next
self.__frame = -1 self.__frame = -1
@ -1396,8 +1397,11 @@ class TiffImageFile(ImageFile.ImageFile):
logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING)) logger.debug("- YCbCr subsampling: %s", self.tag_v2.get(YCBCRSUBSAMPLING))
# size # size
xsize = int(self.tag_v2.get(IMAGEWIDTH)) xsize = self.tag_v2.get(IMAGEWIDTH)
ysize = int(self.tag_v2.get(IMAGELENGTH)) ysize = self.tag_v2.get(IMAGELENGTH)
if not isinstance(xsize, int) or not isinstance(ysize, int):
msg = "Invalid dimensions"
raise ValueError(msg)
self._size = xsize, ysize self._size = xsize, ysize
logger.debug("- size: %s", self.size) logger.debug("- size: %s", self.size)
@ -1545,8 +1549,12 @@ class TiffImageFile(ImageFile.ImageFile):
else: else:
# tiled image # tiled image
offsets = self.tag_v2[TILEOFFSETS] offsets = self.tag_v2[TILEOFFSETS]
w = self.tag_v2.get(TILEWIDTH) tilewidth = self.tag_v2.get(TILEWIDTH)
h = self.tag_v2.get(TILELENGTH) h = self.tag_v2.get(TILELENGTH)
if not isinstance(tilewidth, int) or not isinstance(h, int):
msg = "Invalid tile dimensions"
raise ValueError(msg)
w = tilewidth
for offset in offsets: for offset in offsets:
if x + w > xsize: if x + w > xsize:
@ -1624,7 +1632,7 @@ SAVE_INFO = {
} }
def _save(im, fp, filename): def _save(im: Image.Image, fp, filename: str | bytes) -> None:
try: try:
rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode] rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
except KeyError as e: except KeyError as e:
@ -1760,6 +1768,7 @@ def _save(im, fp, filename):
if im.mode == "1": if im.mode == "1":
inverted_im = im.copy() inverted_im = im.copy()
px = inverted_im.load() px = inverted_im.load()
if px is not None:
for y in range(inverted_im.height): for y in range(inverted_im.height):
for x in range(inverted_im.width): for x in range(inverted_im.width):
px[x, y] = 0 if px[x, y] == 255 else 255 px[x, y] = 0 if px[x, y] == 255 else 255
@ -1805,11 +1814,11 @@ def _save(im, fp, filename):
ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1) ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)
if im.mode == "YCbCr": if im.mode == "YCbCr":
for tag, value in { for tag, default_value in {
YCBCRSUBSAMPLING: (1, 1), YCBCRSUBSAMPLING: (1, 1),
REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255), REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
}.items(): }.items():
ifd.setdefault(tag, value) ifd.setdefault(tag, default_value)
blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS] blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
if libtiff: if libtiff:
@ -1852,7 +1861,7 @@ def _save(im, fp, filename):
] ]
# bits per sample is a single short in the tiff directory, not a list. # bits per sample is a single short in the tiff directory, not a list.
atts = {BITSPERSAMPLE: bits[0]} atts: dict[int, Any] = {BITSPERSAMPLE: bits[0]}
# Merge the ones that we have with (optional) more bits from # Merge the ones that we have with (optional) more bits from
# the original file, e.g x,y resolution so that we can # the original file, e.g x,y resolution so that we can
# save(load('')) == original file. # save(load('')) == original file.
@ -1923,13 +1932,15 @@ def _save(im, fp, filename):
offset = ifd.save(fp) offset = ifd.save(fp)
ImageFile._save( ImageFile._save(
im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))] im,
fp,
[ImageFile._Tile("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))],
) )
# -- helper for multi-page save -- # -- helper for multi-page save --
if "_debug_multipage" in encoderinfo: if "_debug_multipage" in encoderinfo:
# just to access o32 and o16 (using correct byte order) # just to access o32 and o16 (using correct byte order)
im._debug_multipage = ifd setattr(im, "_debug_multipage", ifd)
class AppendingTiffWriter: class AppendingTiffWriter: