Merge branch 'main' into type_hint_tests

This commit is contained in:
Andrew Murray 2024-06-18 23:01:12 +10:00
commit 8d8852d744
55 changed files with 429 additions and 306 deletions

View File

@ -35,7 +35,7 @@ install:
- curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip - curl -fsSL -o nasm-win64.zip https://raw.githubusercontent.com/python-pillow/pillow-depends/main/nasm-2.16.03-win64.zip
- 7z x nasm-win64.zip -oc:\ - 7z x nasm-win64.zip -oc:\
- choco install ghostscript --version=10.3.1 - choco install ghostscript --version=10.3.1
- path c:\nasm-2.16.03;C:\Program Files\gs\gs10.00.0\bin;%PATH% - path c:\nasm-2.16.03;C:\Program Files\gs\gs10.03.1\bin;%PATH%
- cd c:\pillow\winbuild\ - cd c:\pillow\winbuild\
- ps: | - ps: |
c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\ c:\python38\python.exe c:\pillow\winbuild\build_prepare.py -v --depends=C:\pillow-depends\

View File

@ -1 +1 @@
cibuildwheel==2.18.1 cibuildwheel==2.19.1

View File

@ -7,11 +7,15 @@ brew install \
ghostscript \ ghostscript \
libimagequant \ libimagequant \
libjpeg \ libjpeg \
libraqm \
libtiff \ libtiff \
little-cms2 \ little-cms2 \
openjpeg \ openjpeg \
webp webp
if [[ "$ImageOS" == "macos13" ]]; then
brew install --ignore-dependencies libraqm
else
brew install libraqm
fi
export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig" export PKG_CONFIG_PATH="/usr/local/opt/openblas/lib/pkgconfig"
# TODO Update condition when cffi supports 3.13 # TODO Update condition when cffi supports 3.13

View File

@ -3,7 +3,7 @@ version: 2
formats: [pdf] formats: [pdf]
build: build:
os: ubuntu-22.04 os: ubuntu-lts-latest
tools: tools:
python: "3" python: "3"
jobs: jobs:

View File

@ -5,6 +5,12 @@ Changelog (Pillow)
10.4.0 (unreleased) 10.4.0 (unreleased)
------------------- -------------------
- Accept 't' suffix for libtiff version #8126, #8129
[radarhere]
- Deprecate ImageDraw.getdraw hints parameter #8124
[radarhere, hugovk]
- Added ImageDraw circle() #8085 - Added ImageDraw circle() #8085
[void4, hugovk, radarhere] [void4, hugovk, radarhere]

View File

@ -38,7 +38,9 @@ def test_version() -> None:
assert function(name) == version assert function(name) == version
if name != "PIL": if name != "PIL":
if name == "zlib" and version is not None: if name == "zlib" and version is not None:
version = version.replace(".zlib-ng", "") version = re.sub(".zlib-ng$", "", version)
elif name == "libtiff" and version is not None:
version = re.sub("t$", "", version)
assert version is None or re.search(r"\d+(\.\d+)*$", version) assert version is None or re.search(r"\d+(\.\d+)*$", version)
for module in features.modules: for module in features.modules:

View File

@ -53,6 +53,7 @@ def test_closed_file() -> None:
def test_seek_after_close() -> None: def test_seek_after_close() -> None:
im = Image.open("Tests/images/iss634.gif") im = Image.open("Tests/images/iss634.gif")
assert isinstance(im, GifImagePlugin.GifImageFile)
im.load() im.load()
im.close() im.close()
@ -377,7 +378,8 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
img = img.convert("RGB") img = img.convert("RGB")
tempfile = str(tmp_path / "temp.gif") tempfile = str(tmp_path / "temp.gif")
GifImagePlugin._save_netpbm(img, 0, tempfile) b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded: with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("RGB"), 0) assert_image_similar(img, reloaded.convert("RGB"), 0)
@ -388,7 +390,8 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
img = img.convert("L") img = img.convert("L")
tempfile = str(tmp_path / "temp.gif") tempfile = str(tmp_path / "temp.gif")
GifImagePlugin._save_netpbm(img, 0, tempfile) b = BytesIO()
GifImagePlugin._save_netpbm(img, b, tempfile)
with Image.open(tempfile) as reloaded: with Image.open(tempfile) as reloaded:
assert_image_similar(img, reloaded.convert("L"), 0) assert_image_similar(img, reloaded.convert("L"), 0)
@ -648,7 +651,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
assert rgb_img.getpixel((50, 50)) == circle assert rgb_img.getpixel((50, 50)) == circle
# Check that frame transparency wasn't added unnecessarily # Check that frame transparency wasn't added unnecessarily
assert img._frame_transparency is None assert getattr(img, "_frame_transparency") is None
def test_dispose2_diff(tmp_path: Path) -> None: def test_dispose2_diff(tmp_path: Path) -> None:

View File

@ -54,7 +54,7 @@ class TestFileLibTiff(LibTiffTestCase):
def test_version(self) -> None: def test_version(self) -> None:
version = features.version_codec("libtiff") version = features.version_codec("libtiff")
assert version is not None assert version is not None
assert re.search(r"\d+\.\d+\.\d+$", version) assert re.search(r"\d+\.\d+\.\d+t?$", version)
def test_g4_tiff(self, tmp_path: Path) -> None: def test_g4_tiff(self, tmp_path: Path) -> None:
"""Test the ordinary file path load path""" """Test the ordinary file path load path"""

View File

@ -1628,3 +1628,8 @@ def test_incorrectly_ordered_coordinates(xy: tuple[int, int, int, int]) -> None:
draw.rectangle(xy) draw.rectangle(xy)
with pytest.raises(ValueError): with pytest.raises(ValueError):
draw.rounded_rectangle(xy) draw.rounded_rectangle(xy)
def test_getdraw():
with pytest.warns(DeprecationWarning):
ImageDraw.getdraw(None, [])

View File

@ -115,6 +115,13 @@ Support for LibTIFF earlier than 4
Support for LibTIFF earlier than version 4 has been deprecated. Support for LibTIFF earlier than version 4 has been deprecated.
Upgrade to a newer version of LibTIFF instead. Upgrade to a newer version of LibTIFF instead.
ImageDraw.getdraw hints parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. deprecated:: 10.4.0
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
Removed features Removed features
---------------- ----------------

View File

@ -34,6 +34,11 @@ Support for LibTIFF earlier than 4
Support for LibTIFF earlier than version 4 has been deprecated. Support for LibTIFF earlier than version 4 has been deprecated.
Upgrade to a newer version of LibTIFF instead. Upgrade to a newer version of LibTIFF instead.
ImageDraw.getdraw hints parameter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``hints`` parameter in :py:meth:`~PIL.ImageDraw.getdraw()` has been deprecated.
API Changes API Changes
=========== ===========

View File

@ -37,7 +37,9 @@ IMAGEQUANT_ROOT = None
JPEG2K_ROOT = None JPEG2K_ROOT = None
JPEG_ROOT = None JPEG_ROOT = None
LCMS_ROOT = None LCMS_ROOT = None
RAQM_ROOT = None
TIFF_ROOT = None TIFF_ROOT = None
WEBP_ROOT = None
ZLIB_ROOT = None ZLIB_ROOT = None
FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ FUZZING_BUILD = "LIB_FUZZING_ENGINE" in os.environ
@ -459,6 +461,8 @@ class pil_build_ext(build_ext):
"FREETYPE_ROOT": "freetype2", "FREETYPE_ROOT": "freetype2",
"HARFBUZZ_ROOT": "harfbuzz", "HARFBUZZ_ROOT": "harfbuzz",
"FRIBIDI_ROOT": "fribidi", "FRIBIDI_ROOT": "fribidi",
"RAQM_ROOT": "raqm",
"WEBP_ROOT": "libwebp",
"LCMS_ROOT": "lcms2", "LCMS_ROOT": "lcms2",
"IMAGEQUANT_ROOT": "libimagequant", "IMAGEQUANT_ROOT": "libimagequant",
}.items(): }.items():

View File

@ -31,6 +31,7 @@ BLP files come in many different flavours:
from __future__ import annotations from __future__ import annotations
import abc
import os import os
import struct import struct
from enum import IntEnum from enum import IntEnum
@ -60,7 +61,9 @@ def unpack_565(i: int) -> tuple[int, int, int]:
return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3 return ((i >> 11) & 0x1F) << 3, ((i >> 5) & 0x3F) << 2, (i & 0x1F) << 3
def decode_dxt1(data, alpha=False): def decode_dxt1(
data: bytes, alpha: bool = False
) -> tuple[bytearray, bytearray, bytearray, bytearray]:
""" """
input: one "row" of data (i.e. will produce 4*width pixels) input: one "row" of data (i.e. will produce 4*width pixels)
""" """
@ -68,9 +71,9 @@ def decode_dxt1(data, alpha=False):
blocks = len(data) // 8 # number of blocks in row blocks = len(data) // 8 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray()) ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks): for block_index in range(blocks):
# Decode next 8-byte block. # Decode next 8-byte block.
idx = block * 8 idx = block_index * 8
color0, color1, bits = struct.unpack_from("<HHI", data, idx) color0, color1, bits = struct.unpack_from("<HHI", data, idx)
r0, g0, b0 = unpack_565(color0) r0, g0, b0 = unpack_565(color0)
@ -115,7 +118,7 @@ def decode_dxt1(data, alpha=False):
return ret return ret
def decode_dxt3(data): def decode_dxt3(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
""" """
input: one "row" of data (i.e. will produce 4*width pixels) input: one "row" of data (i.e. will produce 4*width pixels)
""" """
@ -123,8 +126,8 @@ def decode_dxt3(data):
blocks = len(data) // 16 # number of blocks in row blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray()) ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks): for block_index in range(blocks):
idx = block * 16 idx = block_index * 16
block = data[idx : idx + 16] block = data[idx : idx + 16]
# Decode next 16-byte block. # Decode next 16-byte block.
bits = struct.unpack_from("<8B", block) bits = struct.unpack_from("<8B", block)
@ -168,7 +171,7 @@ def decode_dxt3(data):
return ret return ret
def decode_dxt5(data): def decode_dxt5(data: bytes) -> tuple[bytearray, bytearray, bytearray, bytearray]:
""" """
input: one "row" of data (i.e. will produce 4 * width pixels) input: one "row" of data (i.e. will produce 4 * width pixels)
""" """
@ -176,8 +179,8 @@ def decode_dxt5(data):
blocks = len(data) // 16 # number of blocks in row blocks = len(data) // 16 # number of blocks in row
ret = (bytearray(), bytearray(), bytearray(), bytearray()) ret = (bytearray(), bytearray(), bytearray(), bytearray())
for block in range(blocks): for block_index in range(blocks):
idx = block * 16 idx = block_index * 16
block = data[idx : idx + 16] block = data[idx : idx + 16]
# Decode next 16-byte block. # Decode next 16-byte block.
a0, a1 = struct.unpack_from("<BB", block) a0, a1 = struct.unpack_from("<BB", block)
@ -276,7 +279,7 @@ class BlpImageFile(ImageFile.ImageFile):
class _BLPBaseDecoder(ImageFile.PyDecoder): class _BLPBaseDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
try: try:
self._read_blp_header() self._read_blp_header()
self._load() self._load()
@ -285,6 +288,10 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
raise OSError(msg) from e raise OSError(msg) from e
return -1, 0 return -1, 0
@abc.abstractmethod
def _load(self) -> None:
pass
def _read_blp_header(self) -> None: def _read_blp_header(self) -> None:
assert self.fd is not None assert self.fd is not None
self.fd.seek(4) self.fd.seek(4)
@ -318,7 +325,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
ret.append((b, g, r, a)) ret.append((b, g, r, a))
return ret return ret
def _read_bgra(self, palette): def _read_bgra(self, palette: list[tuple[int, int, int, int]]) -> bytearray:
data = bytearray() data = bytearray()
_data = BytesIO(self._safe_read(self._blp_lengths[0])) _data = BytesIO(self._safe_read(self._blp_lengths[0]))
while True: while True:
@ -327,7 +334,7 @@ class _BLPBaseDecoder(ImageFile.PyDecoder):
except struct.error: except struct.error:
break break
b, g, r, a = palette[offset] b, g, r, a = palette[offset]
d = (r, g, b) d: tuple[int, ...] = (r, g, b)
if self._blp_alpha_depth: if self._blp_alpha_depth:
d += (a,) d += (a,)
data.extend(d) data.extend(d)
@ -431,7 +438,7 @@ class BLPEncoder(ImageFile.PyEncoder):
data += b"\x00" * 4 data += b"\x00" * 4
return data return data
def encode(self, bufsize): def encode(self, bufsize: int) -> tuple[int, int, bytes]:
palette_data = self._write_palette() palette_data = self._write_palette()
offset = 20 + 16 * 4 * 2 + len(palette_data) offset = 20 + 16 * 4 * 2 + len(palette_data)
@ -449,7 +456,7 @@ class BLPEncoder(ImageFile.PyEncoder):
return len(data), 0, data return len(data), 0, data
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode != "P": if im.mode != "P":
msg = "Unsupported BLP image mode" msg = "Unsupported BLP image mode"
raise ValueError(msg) raise ValueError(msg)

View File

@ -301,7 +301,8 @@ class BmpImageFile(ImageFile.ImageFile):
class BmpRleDecoder(ImageFile.PyDecoder): class BmpRleDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
rle4 = self.args[1] rle4 = self.args[1]
data = bytearray() data = bytearray()
x = 0 x = 0
@ -395,12 +396,12 @@ SAVE = {
} }
def _dib_save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _dib_save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, False) _save(im, fp, filename, False)
def _save( def _save(
im: Image.Image, fp: IO[bytes], filename: str, bitmap_header: bool = True im: Image.Image, fp: IO[bytes], filename: str | bytes, bitmap_header: bool = True
) -> None: ) -> None:
try: try:
rawmode, bits, colors = SAVE[im.mode] rawmode, bits, colors = SAVE[im.mode]

View File

@ -60,7 +60,7 @@ class BufrStubImageFile(ImageFile.StubImageFile):
return _handler return _handler
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"): if _handler is None or not hasattr(_handler, "save"):
msg = "BUFR save handler not installed" msg = "BUFR save handler not installed"
raise OSError(msg) raise OSError(msg)

View File

@ -480,7 +480,8 @@ class DdsImageFile(ImageFile.ImageFile):
class DdsRgbDecoder(ImageFile.PyDecoder): class DdsRgbDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
bitcount, masks = self.args bitcount, masks = self.args
# Some masks will be padded with zeros, e.g. R 0b11 G 0b1100 # Some masks will be padded with zeros, e.g. R 0b11 G 0b1100
@ -511,7 +512,7 @@ class DdsRgbDecoder(ImageFile.PyDecoder):
return -1, 0 return -1, 0
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode not in ("RGB", "RGBA", "L", "LA"): if im.mode not in ("RGB", "RGBA", "L", "LA"):
msg = f"cannot write mode {im.mode} as DDS" msg = f"cannot write mode {im.mode} as DDS"
raise OSError(msg) raise OSError(msg)

View File

@ -27,6 +27,7 @@ import re
import subprocess import subprocess
import sys import sys
import tempfile import tempfile
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i32le as i32 from ._binary import i32le as i32
@ -236,7 +237,7 @@ class EpsImageFile(ImageFile.ImageFile):
msg = 'EPS header missing "%%BoundingBox" comment' msg = 'EPS header missing "%%BoundingBox" comment'
raise SyntaxError(msg) raise SyntaxError(msg)
def _read_comment(s): def _read_comment(s: str) -> bool:
nonlocal reading_trailer_comments nonlocal reading_trailer_comments
try: try:
m = split.match(s) m = split.match(s)
@ -244,27 +245,25 @@ class EpsImageFile(ImageFile.ImageFile):
msg = "not an EPS file" msg = "not an EPS file"
raise SyntaxError(msg) from e raise SyntaxError(msg) from e
if m: if not m:
k, v = m.group(1, 2) return False
self.info[k] = v
if k == "BoundingBox": k, v = m.group(1, 2)
if v == "(atend)": self.info[k] = v
reading_trailer_comments = True if k == "BoundingBox":
elif not self._size or ( if v == "(atend)":
trailer_reached and reading_trailer_comments reading_trailer_comments = True
): elif not self._size or (trailer_reached and reading_trailer_comments):
try: try:
# Note: The DSC spec says that BoundingBox # Note: The DSC spec says that BoundingBox
# fields should be integers, but some drivers # fields should be integers, but some drivers
# put floating point values there anyway. # put floating point values there anyway.
box = [int(float(i)) for i in v.split()] box = [int(float(i)) for i in v.split()]
self._size = box[2] - box[0], box[3] - box[1] self._size = box[2] - box[0], box[3] - box[1]
self.tile = [ self.tile = [("eps", (0, 0) + self.size, offset, (length, box))]
("eps", (0, 0) + self.size, offset, (length, box)) except Exception:
] pass
except Exception: return True
pass
return True
while True: while True:
byte = self.fp.read(1) byte = self.fp.read(1)
@ -413,7 +412,7 @@ class EpsImageFile(ImageFile.ImageFile):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _save(im, fp, filename, eps=1): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes, eps: int = 1) -> None:
"""EPS Writer for the Python Imaging Library.""" """EPS Writer for the Python Imaging Library."""
# make sure image data is available # make sure image data is available

View File

@ -122,7 +122,7 @@ class FitsImageFile(ImageFile.ImageFile):
class FitsGzipDecoder(ImageFile.PyDecoder): class FitsGzipDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None assert self.fd is not None
value = gzip.decompress(self.fd.read()) value = gzip.decompress(self.fd.read())

View File

@ -241,7 +241,7 @@ class FpxImageFile(ImageFile.ImageFile):
self.ole.close() self.ole.close()
super().close() super().close()
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.ole.close() self.ole.close()
super().__exit__() super().__exit__()

View File

@ -29,9 +29,10 @@ import itertools
import math import math
import os import os
import subprocess import subprocess
import sys
from enum import IntEnum from enum import IntEnum
from functools import cached_property from functools import cached_property
from typing import IO from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union
from . import ( from . import (
Image, Image,
@ -46,6 +47,9 @@ from ._binary import i16le as i16
from ._binary import o8 from ._binary import o8
from ._binary import o16le as o16 from ._binary import o16le as o16
if TYPE_CHECKING:
from . import _imaging
class LoadingStrategy(IntEnum): class LoadingStrategy(IntEnum):
""".. versionadded:: 9.1.0""" """.. versionadded:: 9.1.0"""
@ -118,7 +122,7 @@ class GifImageFile(ImageFile.ImageFile):
self._seek(0) # get ready to read first frame self._seek(0) # get ready to read first frame
@property @property
def n_frames(self): def n_frames(self) -> int:
if self._n_frames is None: if self._n_frames is None:
current = self.tell() current = self.tell()
try: try:
@ -163,11 +167,11 @@ class GifImageFile(ImageFile.ImageFile):
msg = "no more images in GIF file" msg = "no more images in GIF file"
raise EOFError(msg) from e raise EOFError(msg) from e
def _seek(self, frame, update_image=True): def _seek(self, frame: int, update_image: bool = True) -> None:
if frame == 0: if frame == 0:
# rewind # rewind
self.__offset = 0 self.__offset = 0
self.dispose = None self.dispose: _imaging.ImagingCore | None = None
self.__frame = -1 self.__frame = -1
self._fp.seek(self.__rewind) self._fp.seek(self.__rewind)
self.disposal_method = 0 self.disposal_method = 0
@ -195,9 +199,9 @@ class GifImageFile(ImageFile.ImageFile):
msg = "no more images in GIF file" msg = "no more images in GIF file"
raise EOFError(msg) raise EOFError(msg)
palette = None palette: ImagePalette.ImagePalette | Literal[False] | None = None
info = {} info: dict[str, Any] = {}
frame_transparency = None frame_transparency = None
interlace = None interlace = None
frame_dispose_extent = None frame_dispose_extent = None
@ -213,7 +217,7 @@ class GifImageFile(ImageFile.ImageFile):
# #
s = self.fp.read(1) s = self.fp.read(1)
block = self.data() block = self.data()
if s[0] == 249: if s[0] == 249 and block is not None:
# #
# graphic control extension # graphic control extension
# #
@ -249,14 +253,14 @@ class GifImageFile(ImageFile.ImageFile):
info["comment"] = comment info["comment"] = comment
s = None s = None
continue continue
elif s[0] == 255 and frame == 0: elif s[0] == 255 and frame == 0 and block is not None:
# #
# application extension # application extension
# #
info["extension"] = block, self.fp.tell() info["extension"] = block, self.fp.tell()
if block[:11] == b"NETSCAPE2.0": if block[:11] == b"NETSCAPE2.0":
block = self.data() block = self.data()
if len(block) >= 3 and block[0] == 1: if block and len(block) >= 3 and block[0] == 1:
self.info["loop"] = i16(block, 1) self.info["loop"] = i16(block, 1)
while self.data(): while self.data():
pass pass
@ -345,51 +349,52 @@ class GifImageFile(ImageFile.ImageFile):
else: else:
return (color, color, color) return (color, color, color)
self.dispose = None
self.dispose_extent = frame_dispose_extent self.dispose_extent = frame_dispose_extent
try: if self.dispose_extent and self.disposal_method >= 2:
if self.disposal_method < 2: try:
# do not dispose or none specified if self.disposal_method == 2:
self.dispose = None # replace with background colour
elif self.disposal_method == 2:
# replace with background colour
# only dispose the extent in this frame
x0, y0, x1, y1 = self.dispose_extent
dispose_size = (x1 - x0, y1 - y0)
Image._decompression_bomb_check(dispose_size)
# by convention, attempt to use transparency first
dispose_mode = "P"
color = self.info.get("transparency", frame_transparency)
if color is not None:
if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGBA"
color = _rgb(color) + (0,)
else:
color = self.info.get("background", 0)
if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGB"
color = _rgb(color)
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
else:
# replace with previous contents
if self.im is not None:
# only dispose the extent in this frame # only dispose the extent in this frame
self.dispose = self._crop(self.im, self.dispose_extent)
elif frame_transparency is not None:
x0, y0, x1, y1 = self.dispose_extent x0, y0, x1, y1 = self.dispose_extent
dispose_size = (x1 - x0, y1 - y0) dispose_size = (x1 - x0, y1 - y0)
Image._decompression_bomb_check(dispose_size) Image._decompression_bomb_check(dispose_size)
# by convention, attempt to use transparency first
dispose_mode = "P" dispose_mode = "P"
color = frame_transparency color = self.info.get("transparency", frame_transparency)
if self.mode in ("RGB", "RGBA"): if color is not None:
dispose_mode = "RGBA" if self.mode in ("RGB", "RGBA"):
color = _rgb(frame_transparency) + (0,) dispose_mode = "RGBA"
color = _rgb(color) + (0,)
else:
color = self.info.get("background", 0)
if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGB"
color = _rgb(color)
self.dispose = Image.core.fill(dispose_mode, dispose_size, color) self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
except AttributeError: else:
pass # replace with previous contents
if self.im is not None:
# only dispose the extent in this frame
self.dispose = self._crop(self.im, self.dispose_extent)
elif frame_transparency is not None:
x0, y0, x1, y1 = self.dispose_extent
dispose_size = (x1 - x0, y1 - y0)
Image._decompression_bomb_check(dispose_size)
dispose_mode = "P"
color = frame_transparency
if self.mode in ("RGB", "RGBA"):
dispose_mode = "RGBA"
color = _rgb(frame_transparency) + (0,)
self.dispose = Image.core.fill(
dispose_mode, dispose_size, color
)
except AttributeError:
pass
if interlace is not None: if interlace is not None:
transparency = -1 transparency = -1
@ -498,7 +503,12 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
return im.convert("L") return im.convert("L")
def _normalize_palette(im, palette, info): _Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette]
def _normalize_palette(
im: Image.Image, palette: _Palette | None, info: dict[str, Any]
) -> Image.Image:
""" """
Normalizes the palette for image. Normalizes the palette for image.
- Sets the palette to the incoming palette, if provided. - Sets the palette to the incoming palette, if provided.
@ -526,8 +536,10 @@ def _normalize_palette(im, palette, info):
source_palette = bytearray(i // 3 for i in range(768)) source_palette = bytearray(i // 3 for i in range(768))
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette) im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
used_palette_colors: list[int] | None
if palette: if palette:
used_palette_colors = [] used_palette_colors = []
assert source_palette is not None
for i in range(0, len(source_palette), 3): for i in range(0, len(source_palette), 3):
source_color = tuple(source_palette[i : i + 3]) source_color = tuple(source_palette[i : i + 3])
index = im.palette.colors.get(source_color) index = im.palette.colors.get(source_color)
@ -558,7 +570,11 @@ def _normalize_palette(im, palette, info):
return im return im
def _write_single_frame(im, fp, palette): def _write_single_frame(
im: Image.Image,
fp: IO[bytes],
palette: _Palette | None,
) -> None:
im_out = _normalize_mode(im) im_out = _normalize_mode(im)
for k, v in im_out.info.items(): for k, v in im_out.info.items():
im.encoderinfo.setdefault(k, v) im.encoderinfo.setdefault(k, v)
@ -579,7 +595,9 @@ def _write_single_frame(im, fp, palette):
fp.write(b"\0") # end of image data fp.write(b"\0") # end of image data
def _getbbox(base_im, im_frame): def _getbbox(
base_im: Image.Image, im_frame: Image.Image
) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im): if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im):
im_frame = im_frame.convert("RGBA") im_frame = im_frame.convert("RGBA")
base_im = base_im.convert("RGBA") base_im = base_im.convert("RGBA")
@ -587,12 +605,20 @@ def _getbbox(base_im, im_frame):
return delta, delta.getbbox(alpha_only=False) return delta, delta.getbbox(alpha_only=False)
def _write_multiple_frames(im, fp, palette): class _Frame(NamedTuple):
im: Image.Image
bbox: tuple[int, int, int, int] | None
encoderinfo: dict[str, Any]
def _write_multiple_frames(
im: Image.Image, fp: IO[bytes], palette: _Palette | None
) -> bool:
duration = im.encoderinfo.get("duration") duration = im.encoderinfo.get("duration")
disposal = im.encoderinfo.get("disposal", im.info.get("disposal")) disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
im_frames = [] im_frames: list[_Frame] = []
previous_im = None previous_im: Image.Image | None = None
frame_count = 0 frame_count = 0
background_im = None background_im = None
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])): for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
@ -618,24 +644,22 @@ def _write_multiple_frames(im, fp, palette):
frame_count += 1 frame_count += 1
diff_frame = None diff_frame = None
if im_frames: if im_frames and previous_im:
# delta frame # delta frame
delta, bbox = _getbbox(previous_im, im_frame) delta, bbox = _getbbox(previous_im, im_frame)
if not bbox: if not bbox:
# This frame is identical to the previous frame # This frame is identical to the previous frame
if encoderinfo.get("duration"): if encoderinfo.get("duration"):
im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[ im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
"duration"
]
continue continue
if im_frames[-1]["encoderinfo"].get("disposal") == 2: if im_frames[-1].encoderinfo.get("disposal") == 2:
if background_im is None: if background_im is None:
color = im.encoderinfo.get( color = im.encoderinfo.get(
"transparency", im.info.get("transparency", (0, 0, 0)) "transparency", im.info.get("transparency", (0, 0, 0))
) )
background = _get_background(im_frame, color) background = _get_background(im_frame, color)
background_im = Image.new("P", im_frame.size, background) background_im = Image.new("P", im_frame.size, background)
background_im.putpalette(im_frames[0]["im"].palette) background_im.putpalette(im_frames[0].im.palette)
bbox = _getbbox(background_im, im_frame)[1] bbox = _getbbox(background_im, im_frame)[1]
elif encoderinfo.get("optimize") and im_frame.mode != "1": elif encoderinfo.get("optimize") and im_frame.mode != "1":
if "transparency" not in encoderinfo: if "transparency" not in encoderinfo:
@ -681,40 +705,38 @@ def _write_multiple_frames(im, fp, palette):
else: else:
bbox = None bbox = None
previous_im = im_frame previous_im = im_frame
im_frames.append( im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))
{"im": diff_frame or im_frame, "bbox": bbox, "encoderinfo": encoderinfo}
)
if len(im_frames) == 1: if len(im_frames) == 1:
if "duration" in im.encoderinfo: if "duration" in im.encoderinfo:
# Since multiple frames will not be written, use the combined duration # Since multiple frames will not be written, use the combined duration
im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"] im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
return return False
for frame_data in im_frames: for frame_data in im_frames:
im_frame = frame_data["im"] im_frame = frame_data.im
if not frame_data["bbox"]: if not frame_data.bbox:
# global header # global header
for s in _get_global_header(im_frame, frame_data["encoderinfo"]): for s in _get_global_header(im_frame, frame_data.encoderinfo):
fp.write(s) fp.write(s)
offset = (0, 0) offset = (0, 0)
else: else:
# compress difference # compress difference
if not palette: if not palette:
frame_data["encoderinfo"]["include_color_table"] = True frame_data.encoderinfo["include_color_table"] = True
im_frame = im_frame.crop(frame_data["bbox"]) im_frame = im_frame.crop(frame_data.bbox)
offset = frame_data["bbox"][:2] offset = frame_data.bbox[:2]
_write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"]) _write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
return True return True
def _save_all(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True) _save(im, fp, filename, save_all=True)
def _save( def _save(
im: Image.Image, fp: IO[bytes], filename: str, save_all: bool = False im: Image.Image, fp: IO[bytes], filename: str | bytes, save_all: bool = False
) -> None: ) -> None:
# header # header
if "palette" in im.encoderinfo or "palette" in im.info: if "palette" in im.encoderinfo or "palette" in im.info:
@ -742,7 +764,9 @@ def get_interlace(im: Image.Image) -> int:
return interlace return interlace
def _write_local_header(fp, im, offset, flags): def _write_local_header(
fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int
) -> None:
try: try:
transparency = im.encoderinfo["transparency"] transparency = im.encoderinfo["transparency"]
except KeyError: except KeyError:
@ -790,7 +814,7 @@ def _write_local_header(fp, im, offset, flags):
fp.write(o8(8)) # bits fp.write(o8(8)) # bits
def _save_netpbm(im, fp, filename): def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# Unused by default. # Unused by default.
# To use, uncomment the register_save call at the end of the file. # To use, uncomment the register_save call at the end of the file.
# #
@ -821,6 +845,7 @@ def _save_netpbm(im, fp, filename):
) )
# Allow ppmquant to receive SIGPIPE if ppmtogif exits # Allow ppmquant to receive SIGPIPE if ppmtogif exits
assert quant_proc.stdout is not None
quant_proc.stdout.close() quant_proc.stdout.close()
retcode = quant_proc.wait() retcode = quant_proc.wait()
@ -842,7 +867,7 @@ def _save_netpbm(im, fp, filename):
_FORCE_OPTIMIZE = False _FORCE_OPTIMIZE = False
def _get_optimize(im, info): def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
""" """
Palette optimization is a potentially expensive operation. Palette optimization is a potentially expensive operation.
@ -886,6 +911,7 @@ def _get_optimize(im, info):
and current_palette_size > 2 and current_palette_size > 2
): ):
return used_palette_colors return used_palette_colors
return None
def _get_color_table_size(palette_bytes: bytes) -> int: def _get_color_table_size(palette_bytes: bytes) -> int:
@ -926,7 +952,10 @@ def _get_palette_bytes(im: Image.Image) -> bytes:
return im.palette.palette if im.palette else b"" return im.palette.palette if im.palette else b""
def _get_background(im, info_background): def _get_background(
im: Image.Image,
info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,
) -> int:
background = 0 background = 0
if info_background: if info_background:
if isinstance(info_background, tuple): if isinstance(info_background, tuple):
@ -949,7 +978,7 @@ def _get_background(im, info_background):
return background return background
def _get_global_header(im, info): def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
"""Return a list of strings representing a GIF header""" """Return a list of strings representing a GIF header"""
# Header Block # Header Block
@ -1011,7 +1040,12 @@ def _get_global_header(im, info):
return header return header
def _write_frame_data(fp, im_frame, offset, params): def _write_frame_data(
fp: IO[bytes],
im_frame: Image.Image,
offset: tuple[int, int],
params: dict[str, Any],
) -> None:
try: try:
im_frame.encoderinfo = params im_frame.encoderinfo = params
@ -1031,7 +1065,9 @@ def _write_frame_data(fp, im_frame, offset, params):
# Legacy GIF utilities # Legacy GIF utilities
def getheader(im, palette=None, info=None): def getheader(
im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None
) -> tuple[list[bytes], list[int] | None]:
""" """
Legacy Method to get Gif data from image. Legacy Method to get Gif data from image.
@ -1043,11 +1079,11 @@ def getheader(im, palette=None, info=None):
:returns: tuple of(list of header items, optimized palette) :returns: tuple of(list of header items, optimized palette)
""" """
used_palette_colors = _get_optimize(im, info)
if info is None: if info is None:
info = {} info = {}
used_palette_colors = _get_optimize(im, info)
if "background" not in info and "background" in im.info: if "background" not in info and "background" in im.info:
info["background"] = im.info["background"] info["background"] = im.info["background"]
@ -1059,7 +1095,9 @@ def getheader(im, palette=None, info=None):
return header, used_palette_colors return header, used_palette_colors
def getdata(im, offset=(0, 0), **params): def getdata(
im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
) -> list[bytes]:
""" """
Legacy Method Legacy Method
@ -1076,12 +1114,23 @@ def getdata(im, offset=(0, 0), **params):
:returns: List of bytes containing GIF encoded frame data :returns: List of bytes containing GIF encoded frame data
""" """
from io import BytesIO
class Collector: class Collector(BytesIO):
data = [] data = []
def write(self, data): if sys.version_info >= (3, 12):
self.data.append(data) from collections.abc import Buffer
def write(self, data: Buffer) -> int:
self.data.append(data)
return len(data)
else:
def write(self, data: Any) -> int:
self.data.append(data)
return len(data)
im.load() # make sure raster data is available im.load() # make sure raster data is available

View File

@ -21,6 +21,7 @@ See the GIMP distribution for more information.)
from __future__ import annotations from __future__ import annotations
from math import log, pi, sin, sqrt from math import log, pi, sin, sqrt
from typing import IO, Callable
from ._binary import o8 from ._binary import o8
@ -28,7 +29,7 @@ EPSILON = 1e-10
"""""" # Enable auto-doc for data member """""" # Enable auto-doc for data member
def linear(middle, pos): def linear(middle: float, pos: float) -> float:
if pos <= middle: if pos <= middle:
if middle < EPSILON: if middle < EPSILON:
return 0.0 return 0.0
@ -43,19 +44,19 @@ def linear(middle, pos):
return 0.5 + 0.5 * pos / middle return 0.5 + 0.5 * pos / middle
def curved(middle, pos): def curved(middle: float, pos: float) -> float:
return pos ** (log(0.5) / log(max(middle, EPSILON))) return pos ** (log(0.5) / log(max(middle, EPSILON)))
def sine(middle, pos): def sine(middle: float, pos: float) -> float:
return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0 return (sin((-pi / 2.0) + pi * linear(middle, pos)) + 1.0) / 2.0
def sphere_increasing(middle, pos): def sphere_increasing(middle: float, pos: float) -> float:
return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2) return sqrt(1.0 - (linear(middle, pos) - 1.0) ** 2)
def sphere_decreasing(middle, pos): def sphere_decreasing(middle: float, pos: float) -> float:
return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2) return 1.0 - sqrt(1.0 - linear(middle, pos) ** 2)
@ -64,9 +65,22 @@ SEGMENTS = [linear, curved, sine, sphere_increasing, sphere_decreasing]
class GradientFile: class GradientFile:
gradient = None gradient: (
list[
tuple[
float,
float,
float,
list[float],
list[float],
Callable[[float, float], float],
]
]
| None
) = None
def getpalette(self, entries=256): def getpalette(self, entries: int = 256) -> tuple[bytes, str]:
assert self.gradient is not None
palette = [] palette = []
ix = 0 ix = 0
@ -101,7 +115,7 @@ class GradientFile:
class GimpGradientFile(GradientFile): class GimpGradientFile(GradientFile):
"""File handler for GIMP's gradient format.""" """File handler for GIMP's gradient format."""
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
if fp.readline()[:13] != b"GIMP Gradient": if fp.readline()[:13] != b"GIMP Gradient":
msg = "not a GIMP gradient file" msg = "not a GIMP gradient file"
raise SyntaxError(msg) raise SyntaxError(msg)
@ -114,7 +128,7 @@ class GimpGradientFile(GradientFile):
count = int(line) count = int(line)
gradient = [] self.gradient = []
for i in range(count): for i in range(count):
s = fp.readline().split() s = fp.readline().split()
@ -132,6 +146,4 @@ class GimpGradientFile(GradientFile):
msg = "cannot handle HSV colour space" msg = "cannot handle HSV colour space"
raise OSError(msg) raise OSError(msg)
gradient.append((x0, x1, xm, rgb0, rgb1, segment)) self.gradient.append((x0, x1, xm, rgb0, rgb1, segment))
self.gradient = gradient

View File

@ -60,7 +60,7 @@ class GribStubImageFile(ImageFile.StubImageFile):
return _handler return _handler
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"): if _handler is None or not hasattr(_handler, "save"):
msg = "GRIB save handler not installed" msg = "GRIB save handler not installed"
raise OSError(msg) raise OSError(msg)

View File

@ -60,7 +60,7 @@ class HDF5StubImageFile(ImageFile.StubImageFile):
return _handler return _handler
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"): if _handler is None or not hasattr(_handler, "save"):
msg = "HDF5 save handler not installed" msg = "HDF5 save handler not installed"
raise OSError(msg) raise OSError(msg)

View File

@ -22,6 +22,7 @@ import io
import os import os
import struct import struct
import sys import sys
from typing import IO
from . import Image, ImageFile, PngImagePlugin, features from . import Image, ImageFile, PngImagePlugin, features
@ -312,7 +313,7 @@ class IcnsImageFile(ImageFile.ImageFile):
return px return px
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
""" """
Saves the image as a series of PNG files, Saves the image as a series of PNG files,
that are then combined into a .icns file. that are then combined into a .icns file.
@ -346,29 +347,27 @@ def _save(im, fp, filename):
entries = [] entries = []
for type, size in sizes.items(): for type, size in sizes.items():
stream = size_streams[size] stream = size_streams[size]
entries.append( entries.append((type, HEADERSIZE + len(stream), stream))
{"type": type, "size": HEADERSIZE + len(stream), "stream": stream}
)
# Header # Header
fp.write(MAGIC) fp.write(MAGIC)
file_length = HEADERSIZE # Header file_length = HEADERSIZE # Header
file_length += HEADERSIZE + 8 * len(entries) # TOC file_length += HEADERSIZE + 8 * len(entries) # TOC
file_length += sum(entry["size"] for entry in entries) file_length += sum(entry[1] for entry in entries)
fp.write(struct.pack(">i", file_length)) fp.write(struct.pack(">i", file_length))
# TOC # TOC
fp.write(b"TOC ") fp.write(b"TOC ")
fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE)) fp.write(struct.pack(">i", HEADERSIZE + len(entries) * HEADERSIZE))
for entry in entries: for entry in entries:
fp.write(entry["type"]) fp.write(entry[0])
fp.write(struct.pack(">i", entry["size"])) fp.write(struct.pack(">i", entry[1]))
# Data # Data
for entry in entries: for entry in entries:
fp.write(entry["type"]) fp.write(entry[0])
fp.write(struct.pack(">i", entry["size"])) fp.write(struct.pack(">i", entry[1]))
fp.write(entry["stream"]) fp.write(entry[2])
if hasattr(fp, "flush"): if hasattr(fp, "flush"):
fp.flush() fp.flush()

View File

@ -40,7 +40,7 @@ from ._binary import o32le as o32
_MAGIC = b"\0\0\1\0" _MAGIC = b"\0\0\1\0"
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
fp.write(_MAGIC) # (2+2) fp.write(_MAGIC) # (2+2)
bmp = im.encoderinfo.get("bitmap_format") == "bmp" bmp = im.encoderinfo.get("bitmap_format") == "bmp"
sizes = im.encoderinfo.get( sizes = im.encoderinfo.get(

View File

@ -326,7 +326,7 @@ SAVE = {
} }
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
image_type, rawmode = SAVE[im.mode] image_type, rawmode = SAVE[im.mode]
except KeyError as e: except KeyError as e:
@ -341,6 +341,8 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
# or: SyntaxError("not an IM file") # or: SyntaxError("not an IM file")
# 8 characters are used for "Name: " and "\r\n" # 8 characters are used for "Name: " and "\r\n"
# Keep just the filename, ditch the potentially overlong path # Keep just the filename, ditch the potentially overlong path
if isinstance(filename, bytes):
filename = filename.decode("ascii")
name, ext = os.path.splitext(os.path.basename(filename)) name, ext = os.path.splitext(os.path.basename(filename))
name = "".join([name[: 92 - len(ext)], ext]) name = "".join([name[: 92 - len(ext)], ext])

View File

@ -41,7 +41,7 @@ import warnings
from collections.abc import Callable, MutableMapping from collections.abc import Callable, MutableMapping
from enum import IntEnum from enum import IntEnum
from types import ModuleType from types import ModuleType
from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, Tuple, cast
# VERSION was removed in Pillow 6.0.0. # VERSION was removed in Pillow 6.0.0.
# PILLOW_VERSION was removed in Pillow 9.0.0. # PILLOW_VERSION was removed in Pillow 9.0.0.
@ -626,7 +626,7 @@ class Image:
self.load() self.load()
def _dump( def _dump(
self, file: str | None = None, format: str | None = None, **options self, file: str | None = None, format: str | None = None, **options: Any
) -> str: ) -> str:
suffix = "" suffix = ""
if format: if format:
@ -649,10 +649,12 @@ class Image:
return filename return filename
def __eq__(self, other): def __eq__(self, other: object) -> bool:
if self.__class__ is not other.__class__:
return False
assert isinstance(other, Image)
return ( return (
self.__class__ is other.__class__ self.mode == other.mode
and self.mode == other.mode
and self.size == other.size and self.size == other.size
and self.info == other.info and self.info == other.info
and self.getpalette() == other.getpalette() and self.getpalette() == other.getpalette()
@ -1365,7 +1367,7 @@ class Image:
""" """
return ImageMode.getmode(self.mode).bands return ImageMode.getmode(self.mode).bands
def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]: def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None:
""" """
Calculates the bounding box of the non-zero regions in the Calculates the bounding box of the non-zero regions in the
image. image.
@ -2470,7 +2472,7 @@ class Image:
save_all = params.pop("save_all", False) save_all = params.pop("save_all", False)
self.encoderinfo = params self.encoderinfo = params
self.encoderconfig = () self.encoderconfig: tuple[Any, ...] = ()
preinit() preinit()
@ -2965,7 +2967,7 @@ class ImageTransformHandler:
# Debugging # Debugging
def _wedge(): def _wedge() -> Image:
"""Create grayscale wedge (for debugging only)""" """Create grayscale wedge (for debugging only)"""
return Image()._new(core.wedge("L")) return Image()._new(core.wedge("L"))
@ -3027,12 +3029,18 @@ def new(
color = ImageColor.getcolor(color, mode) color = ImageColor.getcolor(color, mode)
im = Image() im = Image()
if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]: if (
# RGB or RGBA value for a P image mode == "P"
from . import ImagePalette and isinstance(color, (list, tuple))
and all(isinstance(i, int) for i in color)
):
color_ints: tuple[int, ...] = cast(Tuple[int, ...], tuple(color))
if len(color_ints) == 3 or len(color_ints) == 4:
# RGB or RGBA value for a P image
from . import ImagePalette
im.palette = ImagePalette.ImagePalette() im.palette = ImagePalette.ImagePalette()
color = im.palette.getcolor(color) color = im.palette.getcolor(color_ints)
return im._new(core.fill(mode, size, color)) return im._new(core.fill(mode, size, color))
@ -3566,7 +3574,9 @@ def register_mime(id: str, mimetype: str) -> None:
MIME[id.upper()] = mimetype MIME[id.upper()] = mimetype
def register_save(id: str, driver) -> None: def register_save(
id: str, driver: Callable[[Image, IO[bytes], str | bytes], None]
) -> None:
""" """
Registers an image save function. This function should not be Registers an image save function. This function should not be
used in application code. used in application code.
@ -3577,7 +3587,9 @@ def register_save(id: str, driver) -> None:
SAVE[id.upper()] = driver SAVE[id.upper()] = driver
def register_save_all(id: str, driver) -> None: def register_save_all(
id: str, driver: Callable[[Image, IO[bytes], str | bytes], None]
) -> None:
""" """
Registers an image function to save all the frames Registers an image function to save all the frames
of a multiframe format. This function should not be of a multiframe format. This function should not be
@ -3651,7 +3663,7 @@ def register_encoder(name: str, encoder: type[ImageFile.PyEncoder]) -> None:
# Simple display support. # Simple display support.
def _show(image, **options) -> None: def _show(image: Image, **options: Any) -> None:
from . import ImageShow from . import ImageShow
ImageShow.show(image, **options) ImageShow.show(image, **options)
@ -3661,7 +3673,9 @@ def _show(image, **options) -> None:
# Effects # Effects
def effect_mandelbrot(size, extent, quality): def effect_mandelbrot(
size: tuple[int, int], extent: tuple[int, int, int, int], quality: int
) -> Image:
""" """
Generate a Mandelbrot set covering the given extent. Generate a Mandelbrot set covering the given extent.

View File

@ -37,6 +37,7 @@ import struct
from typing import TYPE_CHECKING, AnyStr, Sequence, cast from typing import TYPE_CHECKING, AnyStr, Sequence, cast
from . import Image, ImageColor from . import Image, ImageColor
from ._deprecate import deprecate
from ._typing import Coords from ._typing import Coords
""" """
@ -219,7 +220,9 @@ class ImageDraw:
# This is a straight line, so no joint is required # This is a straight line, so no joint is required
continue continue
def coord_at_angle(coord, angle): def coord_at_angle(
coord: Sequence[float], angle: float
) -> tuple[float, float]:
x, y = coord x, y = coord
angle -= 90 angle -= 90
distance = width / 2 - 1 distance = width / 2 - 1
@ -902,26 +905,17 @@ except AttributeError:
def getdraw(im=None, hints=None): def getdraw(im=None, hints=None):
""" """
(Experimental) A more advanced 2D drawing interface for PIL images,
based on the WCK interface.
:param im: The image to draw in. :param im: The image to draw in.
:param hints: An optional list of hints. :param hints: An optional list of hints. Deprecated.
:returns: A (drawing context, drawing resource factory) tuple. :returns: A (drawing context, drawing resource factory) tuple.
""" """
# FIXME: this needs more work! if hints is not None:
# FIXME: come up with a better 'hints' scheme. deprecate("'hints' parameter", 12)
handler = None from . import ImageDraw2
if not hints or "nicest" in hints:
try:
from . import _imagingagg as handler
except ImportError:
pass
if handler is None:
from . import ImageDraw2 as handler
if im: if im:
im = handler.Draw(im) im = ImageDraw2.Draw(im)
return im, handler return im, ImageDraw2
def floodfill( def floodfill(
@ -1109,11 +1103,13 @@ def _compute_regular_polygon_vertices(
return [_compute_polygon_vertex(angle) for angle in angles] return [_compute_polygon_vertex(angle) for angle in angles]
def _color_diff(color1, color2: float | tuple[int, ...]) -> float: def _color_diff(
color1: float | tuple[int, ...], color2: float | tuple[int, ...]
) -> float:
""" """
Uses 1-norm distance to calculate difference between two values. Uses 1-norm distance to calculate difference between two values.
""" """
if isinstance(color2, tuple): first = color1 if isinstance(color1, tuple) else (color1,)
return sum(abs(color1[i] - color2[i]) for i in range(0, len(color2))) second = color2 if isinstance(color2, tuple) else (color2,)
else:
return abs(color1 - color2) return sum(abs(first[i] - second[i]) for i in range(0, len(second)))

View File

@ -30,7 +30,7 @@ from . import Image, ImageColor, ImageDraw, ImageFont, ImagePath
class Pen: class Pen:
"""Stores an outline color and width.""" """Stores an outline color and width."""
def __init__(self, color, width=1, opacity=255): def __init__(self, color: str, width: int = 1, opacity: int = 255) -> None:
self.color = ImageColor.getrgb(color) self.color = ImageColor.getrgb(color)
self.width = width self.width = width
@ -38,7 +38,7 @@ class Pen:
class Brush: class Brush:
"""Stores a fill color""" """Stores a fill color"""
def __init__(self, color, opacity=255): def __init__(self, color: str, opacity: int = 255) -> None:
self.color = ImageColor.getrgb(color) self.color = ImageColor.getrgb(color)
@ -63,7 +63,7 @@ class Draw:
self.image = image self.image = image
self.transform = None self.transform = None
def flush(self): def flush(self) -> Image.Image:
return self.image return self.image
def render(self, op, xy, pen, brush=None): def render(self, op, xy, pen, brush=None):

View File

@ -487,7 +487,7 @@ class Parser:
def __enter__(self): def __enter__(self):
return self return self
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.close() self.close()
def close(self): def close(self):
@ -763,7 +763,7 @@ class PyEncoder(PyCodec):
def pushes_fd(self): def pushes_fd(self):
return self._pushes_fd return self._pushes_fd
def encode(self, bufsize): def encode(self, bufsize: int) -> tuple[int, int, bytes]:
""" """
Override to perform the encoding process. Override to perform the encoding process.

View File

@ -497,7 +497,7 @@ def expand(
color = _color(fill, image.mode) color = _color(fill, image.mode)
if image.palette: if image.palette:
palette = ImagePalette.ImagePalette(palette=image.getpalette()) palette = ImagePalette.ImagePalette(palette=image.getpalette())
if isinstance(color, tuple): if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
color = palette.getcolor(color) color = palette.getcolor(color)
else: else:
palette = None palette = None

View File

@ -18,10 +18,13 @@
from __future__ import annotations from __future__ import annotations
import array import array
from typing import IO, Sequence from typing import IO, TYPE_CHECKING, Sequence
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
if TYPE_CHECKING:
from . import Image
class ImagePalette: class ImagePalette:
""" """
@ -128,7 +131,11 @@ class ImagePalette:
raise ValueError(msg) from e raise ValueError(msg) from e
return index return index
def getcolor(self, color, image=None) -> int: def getcolor(
self,
color: tuple[int, int, int] | tuple[int, int, int, int],
image: Image.Image | None = None,
) -> int:
"""Given an rgb tuple, allocate palette entry. """Given an rgb tuple, allocate palette entry.
.. warning:: This method is experimental. .. warning:: This method is experimental.
@ -163,7 +170,7 @@ class ImagePalette:
self.dirty = 1 self.dirty = 1
return index return index
else: else:
msg = f"unknown color specifier: {repr(color)}" msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable]
raise ValueError(msg) raise ValueError(msg)
def save(self, fp: str | IO[str]) -> None: def save(self, fp: str | IO[str]) -> None:

View File

@ -37,7 +37,7 @@ from . import Image
_pilbitmap_ok = None _pilbitmap_ok = None
def _pilbitmap_check(): def _pilbitmap_check() -> int:
global _pilbitmap_ok global _pilbitmap_ok
if _pilbitmap_ok is None: if _pilbitmap_ok is None:
try: try:
@ -162,7 +162,7 @@ class PhotoImage:
""" """
return self.__size[1] return self.__size[1]
def paste(self, im): def paste(self, im: Image.Image) -> None:
""" """
Paste a PIL image into the photo image. Note that this can Paste a PIL image into the photo image. Note that this can
be very slow if the photo image is displayed. be very slow if the photo image is displayed.
@ -254,7 +254,7 @@ class BitmapImage:
return str(self.__photo) return str(self.__photo)
def getimage(photo): def getimage(photo: PhotoImage) -> Image.Image:
"""Copies the contents of a PhotoImage to a PIL image memory.""" """Copies the contents of a PhotoImage to a PIL image memory."""
im = Image.new("RGBA", (photo.width(), photo.height())) im = Image.new("RGBA", (photo.width(), photo.height()))
block = im.im block = im.im

View File

@ -18,6 +18,7 @@ from __future__ import annotations
import io import io
import os import os
import struct import struct
from typing import IO, Tuple, cast
from . import Image, ImageFile, ImagePalette, _binary from . import Image, ImageFile, ImagePalette, _binary
@ -58,7 +59,7 @@ class BoxReader:
self.remaining_in_box -= num_bytes self.remaining_in_box -= num_bytes
return data return data
def read_fields(self, field_format): def read_fields(self, field_format: str) -> tuple[int | bytes, ...]:
size = struct.calcsize(field_format) size = struct.calcsize(field_format)
data = self._read_bytes(size) data = self._read_bytes(size)
return struct.unpack(field_format, data) return struct.unpack(field_format, data)
@ -81,9 +82,9 @@ class BoxReader:
self.remaining_in_box = -1 self.remaining_in_box = -1
# Read the length and type of the next box # Read the length and type of the next box
lbox, tbox = self.read_fields(">I4s") lbox, tbox = cast(Tuple[int, bytes], self.read_fields(">I4s"))
if lbox == 1: if lbox == 1:
lbox = self.read_fields(">Q")[0] lbox = cast(int, self.read_fields(">Q")[0])
hlen = 16 hlen = 16
else: else:
hlen = 8 hlen = 8
@ -126,12 +127,13 @@ def _parse_codestream(fp):
return size, mode return size, mode
def _res_to_dpi(num, denom, exp): def _res_to_dpi(num: int, denom: int, exp: int) -> float | None:
"""Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution, """Convert JPEG2000's (numerator, denominator, exponent-base-10) resolution,
calculated as (num / denom) * 10^exp and stored in dots per meter, calculated as (num / denom) * 10^exp and stored in dots per meter,
to floating-point dots per inch.""" to floating-point dots per inch."""
if denom != 0: if denom == 0:
return (254 * num * (10**exp)) / (10000 * denom) return None
return (254 * num * (10**exp)) / (10000 * denom)
def _parse_jp2_header(fp): def _parse_jp2_header(fp):
@ -328,11 +330,13 @@ def _accept(prefix: bytes) -> bool:
# Save support # Save support
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# Get the keyword arguments # Get the keyword arguments
info = im.encoderinfo info = im.encoderinfo
if filename.endswith(".j2k") or info.get("no_jp2", False): if isinstance(filename, str):
filename = filename.encode()
if filename.endswith(b".j2k") or info.get("no_jp2", False):
kind = "j2k" kind = "j2k"
else: else:
kind = "jp2" kind = "jp2"

View File

@ -42,7 +42,7 @@ import subprocess
import sys import sys
import tempfile import tempfile
import warnings import warnings
from typing import Any from typing import IO, Any
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -644,7 +644,7 @@ def get_sampling(im):
return samplings.get(sampling, -1) return samplings.get(sampling, -1)
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.width == 0 or im.height == 0: if im.width == 0 or im.height == 0:
msg = "cannot write empty image as JPEG" msg = "cannot write empty image as JPEG"
raise ValueError(msg) raise ValueError(msg)
@ -827,7 +827,7 @@ def _save(im, fp, filename):
ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize) ImageFile._save(im, fp, [("jpeg", (0, 0) + im.size, 0, rawmode)], bufsize)
def _save_cjpeg(im, fp, filename): def _save_cjpeg(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# ALTERNATIVE: handle JPEGs via the IJG command line utilities. # ALTERNATIVE: handle JPEGs via the IJG command line utilities.
tempfile = im._dump() tempfile = im._dump()
subprocess.check_call(["cjpeg", "-outfile", filename, tempfile]) subprocess.check_call(["cjpeg", "-outfile", filename, tempfile])

View File

@ -93,7 +93,7 @@ class MicImageFile(TiffImagePlugin.TiffImageFile):
self.ole.close() self.ole.close()
super().close() super().close()
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.__fp.close() self.__fp.close()
self.ole.close() self.ole.close()
super().__exit__() super().__exit__()

View File

@ -33,23 +33,18 @@ from . import (
from ._binary import o32le from ._binary import o32le
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
JpegImagePlugin._save(im, fp, filename) JpegImagePlugin._save(im, fp, filename)
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
append_images = im.encoderinfo.get("append_images", []) append_images = im.encoderinfo.get("append_images", [])
if not append_images: if not append_images and not getattr(im, "is_animated", False):
try: _save(im, fp, filename)
animated = im.is_animated return
except AttributeError:
animated = False
if not animated:
_save(im, fp, filename)
return
mpf_offset = 28 mpf_offset = 28
offsets = [] offsets: list[int] = []
for imSequence in itertools.chain([im], append_images): for imSequence in itertools.chain([im], append_images):
for im_frame in ImageSequence.Iterator(imSequence): for im_frame in ImageSequence.Iterator(imSequence):
if not offsets: if not offsets:

View File

@ -164,7 +164,7 @@ Image.register_decoder("MSP", MspDecoder)
# write MSP files (uncompressed only) # write MSP files (uncompressed only)
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode != "1": if im.mode != "1":
msg = f"cannot write mode {im.mode} as MSP" msg = f"cannot write mode {im.mode} as MSP"
raise OSError(msg) raise OSError(msg)

View File

@ -138,7 +138,7 @@ class PSDraw:
sx = x / im.size[0] sx = x / im.size[0]
sy = y / im.size[1] sy = y / im.size[1]
self.fp.write(b"%f %f scale\n" % (sx, sy)) self.fp.write(b"%f %f scale\n" % (sx, sy))
EpsImagePlugin._save(im, self.fp, None, 0) EpsImagePlugin._save(im, self.fp, "", 0)
self.fp.write(b"\ngrestore\n") self.fp.write(b"\ngrestore\n")

View File

@ -14,6 +14,8 @@
# #
from __future__ import annotations from __future__ import annotations
from typing import IO
from ._binary import o8 from ._binary import o8
@ -22,8 +24,8 @@ class PaletteFile:
rawmode = "RGB" rawmode = "RGB"
def __init__(self, fp): def __init__(self, fp: IO[bytes]) -> None:
self.palette = [(i, i, i) for i in range(256)] palette = [o8(i) * 3 for i in range(256)]
while True: while True:
s = fp.readline() s = fp.readline()
@ -44,9 +46,9 @@ class PaletteFile:
g = b = r g = b = r
if 0 <= i <= 255: if 0 <= i <= 255:
self.palette[i] = o8(r) + o8(g) + o8(b) palette[i] = o8(r) + o8(g) + o8(b)
self.palette = b"".join(self.palette) self.palette = b"".join(palette)
def getpalette(self) -> tuple[bytes, str]: def getpalette(self) -> tuple[bytes, str]:
return self.palette, self.rawmode return self.palette, self.rawmode

View File

@ -8,6 +8,8 @@
## ##
from __future__ import annotations from __future__ import annotations
from typing import IO
from . import Image, ImageFile from . import Image, ImageFile
from ._binary import o8 from ._binary import o8
from ._binary import o16be as o16b from ._binary import o16be as o16b
@ -82,10 +84,10 @@ _Palm8BitColormapValues = (
# so build a prototype image to be used for palette resampling # so build a prototype image to be used for palette resampling
def build_prototype_image(): def build_prototype_image() -> Image.Image:
image = Image.new("L", (1, len(_Palm8BitColormapValues))) image = Image.new("L", (1, len(_Palm8BitColormapValues)))
image.putdata(list(range(len(_Palm8BitColormapValues)))) image.putdata(list(range(len(_Palm8BitColormapValues))))
palettedata = () palettedata: tuple[int, ...] = ()
for colormapValue in _Palm8BitColormapValues: for colormapValue in _Palm8BitColormapValues:
palettedata += colormapValue palettedata += colormapValue
palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues)) palettedata += (0, 0, 0) * (256 - len(_Palm8BitColormapValues))
@ -112,7 +114,7 @@ _COMPRESSION_TYPES = {"none": 0xFF, "rle": 0x01, "scanline": 0x00}
# (Internal) Image save plugin for the Palm format. # (Internal) Image save plugin for the Palm format.
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "P": if im.mode == "P":
# we assume this is a color Palm image with the standard colormap, # we assume this is a color Palm image with the standard colormap,
# unless the "info" dict has a "custom-colormap" field # unless the "info" dict has a "custom-colormap" field
@ -141,7 +143,7 @@ def _save(im, fp, filename):
raise OSError(msg) raise OSError(msg)
# we ignore the palette here # we ignore the palette here
im.mode = "P" im._mode = "P"
rawmode = f"P;{bpp}" rawmode = f"P;{bpp}"
version = 1 version = 1

View File

@ -144,7 +144,7 @@ SAVE = {
} }
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
version, bits, planes, rawmode = SAVE[im.mode] version, bits, planes, rawmode = SAVE[im.mode]
except KeyError as e: except KeyError as e:

View File

@ -40,7 +40,7 @@ from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features
# 5. page contents # 5. page contents
def _save_all(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True) _save(im, fp, filename, save_all=True)

View File

@ -76,7 +76,7 @@ class PdfFormatError(RuntimeError):
pass pass
def check_format_condition(condition, error_message): def check_format_condition(condition: bool, error_message: str) -> None:
if not condition: if not condition:
raise PdfFormatError(error_message) raise PdfFormatError(error_message)
@ -93,12 +93,11 @@ class IndirectReference(IndirectReferenceTuple):
def __bytes__(self) -> bytes: def __bytes__(self) -> bytes:
return self.__str__().encode("us-ascii") return self.__str__().encode("us-ascii")
def __eq__(self, other): def __eq__(self, other: object) -> bool:
return ( if self.__class__ is not other.__class__:
other.__class__ is self.__class__ return False
and other.object_id == self.object_id assert isinstance(other, IndirectReference)
and other.generation == self.generation return other.object_id == self.object_id and other.generation == self.generation
)
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return not (self == other)
@ -405,9 +404,8 @@ class PdfParser:
def __enter__(self) -> PdfParser: def __enter__(self) -> PdfParser:
return self return self
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, *args: object) -> None:
self.close() self.close()
return False # do not suppress exceptions
def start_writing(self) -> None: def start_writing(self) -> None:
self.close_buf() self.close_buf()

View File

@ -178,7 +178,7 @@ class ChunkStream:
def __enter__(self) -> ChunkStream: def __enter__(self) -> ChunkStream:
return self return self
def __exit__(self, *args): def __exit__(self, *args: object) -> None:
self.close() self.close()
def close(self) -> None: def close(self) -> None:
@ -1234,7 +1234,7 @@ def _write_multiple_frames(im, fp, chunk, rawmode, default_image, append_images)
seq_num = fdat_chunks.seq_num seq_num = fdat_chunks.seq_num
def _save_all(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
_save(im, fp, filename, save_all=True) _save(im, fp, filename, save_all=True)

View File

@ -328,7 +328,7 @@ class PpmDecoder(ImageFile.PyDecoder):
# -------------------------------------------------------------------- # --------------------------------------------------------------------
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode == "1": if im.mode == "1":
rawmode, head = "1;I", b"P4" rawmode, head = "1;I", b"P4"
elif im.mode == "L": elif im.mode == "L":

View File

@ -37,6 +37,8 @@ class QoiImageFile(ImageFile.ImageFile):
class QoiDecoder(ImageFile.PyDecoder): class QoiDecoder(ImageFile.PyDecoder):
_pulls_fd = True _pulls_fd = True
_previous_pixel: bytes | bytearray | None = None
_previously_seen_pixels: dict[int, bytes | bytearray] = {}
def _add_to_previous_pixels(self, value: bytes | bytearray) -> None: def _add_to_previous_pixels(self, value: bytes | bytearray) -> None:
self._previous_pixel = value self._previous_pixel = value
@ -45,9 +47,10 @@ class QoiDecoder(ImageFile.PyDecoder):
hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64 hash_value = (r * 3 + g * 5 + b * 7 + a * 11) % 64
self._previously_seen_pixels[hash_value] = value self._previously_seen_pixels[hash_value] = value
def decode(self, buffer): def decode(self, buffer: bytes) -> tuple[int, int]:
assert self.fd is not None
self._previously_seen_pixels = {} self._previously_seen_pixels = {}
self._previous_pixel = None
self._add_to_previous_pixels(bytearray((0, 0, 0, 255))) self._add_to_previous_pixels(bytearray((0, 0, 0, 255)))
data = bytearray() data = bytearray()
@ -55,7 +58,8 @@ class QoiDecoder(ImageFile.PyDecoder):
dest_length = self.state.xsize * self.state.ysize * bands dest_length = self.state.xsize * self.state.ysize * bands
while len(data) < dest_length: while len(data) < dest_length:
byte = self.fd.read(1)[0] byte = self.fd.read(1)[0]
if byte == 0b11111110: # QOI_OP_RGB value: bytes | bytearray
if byte == 0b11111110 and self._previous_pixel: # QOI_OP_RGB
value = bytearray(self.fd.read(3)) + self._previous_pixel[3:] value = bytearray(self.fd.read(3)) + self._previous_pixel[3:]
elif byte == 0b11111111: # QOI_OP_RGBA elif byte == 0b11111111: # QOI_OP_RGBA
value = self.fd.read(4) value = self.fd.read(4)
@ -66,7 +70,7 @@ class QoiDecoder(ImageFile.PyDecoder):
value = self._previously_seen_pixels.get( value = self._previously_seen_pixels.get(
op_index, bytearray((0, 0, 0, 0)) op_index, bytearray((0, 0, 0, 0))
) )
elif op == 1: # QOI_OP_DIFF elif op == 1 and self._previous_pixel: # QOI_OP_DIFF
value = bytearray( value = bytearray(
( (
(self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2) (self._previous_pixel[0] + ((byte & 0b00110000) >> 4) - 2)
@ -77,7 +81,7 @@ class QoiDecoder(ImageFile.PyDecoder):
self._previous_pixel[3], self._previous_pixel[3],
) )
) )
elif op == 2: # QOI_OP_LUMA elif op == 2 and self._previous_pixel: # QOI_OP_LUMA
second_byte = self.fd.read(1)[0] second_byte = self.fd.read(1)[0]
diff_green = (byte & 0b00111111) - 32 diff_green = (byte & 0b00111111) - 32
diff_red = ((second_byte & 0b11110000) >> 4) - 8 diff_red = ((second_byte & 0b11110000) >> 4) - 8
@ -90,7 +94,7 @@ class QoiDecoder(ImageFile.PyDecoder):
) )
) )
value += self._previous_pixel[3:] value += self._previous_pixel[3:]
elif op == 3: # QOI_OP_RUN elif op == 3 and self._previous_pixel: # QOI_OP_RUN
run_length = (byte & 0b00111111) + 1 run_length = (byte & 0b00111111) + 1
value = self._previous_pixel value = self._previous_pixel
if bands == 3: if bands == 3:

View File

@ -125,7 +125,7 @@ class SgiImageFile(ImageFile.ImageFile):
] ]
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode not in {"RGB", "RGBA", "L"}: if im.mode not in {"RGB", "RGBA", "L"}:
msg = "Unsupported SGI image mode" msg = "Unsupported SGI image mode"
raise ValueError(msg) raise ValueError(msg)
@ -171,8 +171,9 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
# Maximum Byte value (255 = 8bits per pixel) # Maximum Byte value (255 = 8bits per pixel)
pinmax = 255 pinmax = 255
# Image name (79 characters max, truncated below in write) # Image name (79 characters max, truncated below in write)
filename = os.path.basename(filename) img_name = os.path.splitext(os.path.basename(filename))[0]
img_name = os.path.splitext(filename)[0].encode("ascii", "ignore") if isinstance(img_name, str):
img_name = img_name.encode("ascii", "ignore")
# Standard representation of pixel in the file # Standard representation of pixel in the file
colormap = 0 colormap = 0
fp.write(struct.pack(">h", magic_number)) fp.write(struct.pack(">h", magic_number))

View File

@ -263,7 +263,7 @@ def makeSpiderHeader(im: Image.Image) -> list[bytes]:
return [struct.pack("f", v) for v in hdr] return [struct.pack("f", v) for v in hdr]
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode[0] != "F": if im.mode[0] != "F":
im = im.convert("F") im = im.convert("F")
@ -279,9 +279,10 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))]) ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, 1))])
def _save_spider(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save_spider(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
# get the filename extension and register it with Image # get the filename extension and register it with Image
ext = os.path.splitext(filename)[1] filename_ext = os.path.splitext(filename)[1]
ext = filename_ext.decode() if isinstance(filename_ext, bytes) else filename_ext
Image.register_extension(SpiderImageFile.format, ext) Image.register_extension(SpiderImageFile.format, ext)
_save(im, fp, filename) _save(im, fp, filename)

View File

@ -16,7 +16,6 @@
from __future__ import annotations from __future__ import annotations
import io import io
from types import TracebackType
from . import ContainerIO from . import ContainerIO
@ -61,12 +60,7 @@ class TarIO(ContainerIO.ContainerIO[bytes]):
def __enter__(self) -> TarIO: def __enter__(self) -> TarIO:
return self return self
def __exit__( def __exit__(self, *args: object) -> None:
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
self.close() self.close()
def close(self) -> None: def close(self) -> None:

View File

@ -178,7 +178,7 @@ SAVE = {
} }
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
try: try:
rawmode, bits, colormaptype, imagetype = SAVE[im.mode] rawmode, bits, colormaptype, imagetype = SAVE[im.mode]
except KeyError as e: except KeyError as e:

View File

@ -50,7 +50,7 @@ import warnings
from collections.abc import MutableMapping from collections.abc import MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational from numbers import Number, Rational
from typing import TYPE_CHECKING, Any, Callable from typing import IO, TYPE_CHECKING, Any, Callable
from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
@ -387,7 +387,7 @@ class IFDRational(Rational):
def __hash__(self): def __hash__(self):
return self._val.__hash__() return self._val.__hash__()
def __eq__(self, other): def __eq__(self, other: object) -> bool:
val = self._val val = self._val
if isinstance(other, IFDRational): if isinstance(other, IFDRational):
other = other._val other = other._val
@ -717,7 +717,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
# Unspec'd, and length > 1 # Unspec'd, and length > 1
dest[tag] = values dest[tag] = values
def __delitem__(self, tag): def __delitem__(self, tag: int) -> None:
self._tags_v2.pop(tag, None) self._tags_v2.pop(tag, None)
self._tags_v1.pop(tag, None) self._tags_v1.pop(tag, None)
self._tagdata.pop(tag, None) self._tagdata.pop(tag, None)
@ -1106,7 +1106,7 @@ class TiffImageFile(ImageFile.ImageFile):
super().__init__(fp, filename) super().__init__(fp, filename)
def _open(self): def _open(self) -> None:
"""Open the first image in a TIFF file""" """Open the first image in a TIFF file"""
# Header # Header
@ -1123,8 +1123,8 @@ class TiffImageFile(ImageFile.ImageFile):
self.__first = self.__next = self.tag_v2.next self.__first = self.__next = self.tag_v2.next
self.__frame = -1 self.__frame = -1
self._fp = self.fp self._fp = self.fp
self._frame_pos = [] self._frame_pos: list[int] = []
self._n_frames = None self._n_frames: int | None = None
logger.debug("*** TiffImageFile._open ***") logger.debug("*** TiffImageFile._open ***")
logger.debug("- __first: %s", self.__first) logger.debug("- __first: %s", self.__first)
@ -1998,10 +1998,9 @@ class AppendingTiffWriter:
def __enter__(self) -> AppendingTiffWriter: def __enter__(self) -> AppendingTiffWriter:
return self return self
def __exit__(self, exc_type, exc_value, traceback): def __exit__(self, *args: object) -> None:
if self.close_fp: if self.close_fp:
self.close() self.close()
return False
def tell(self) -> int: def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage return self.f.tell() - self.offsetOfNewPage
@ -2043,42 +2042,42 @@ class AppendingTiffWriter:
def write(self, data): def write(self, data):
return self.f.write(data) return self.f.write(data)
def readShort(self): def readShort(self) -> int:
(value,) = struct.unpack(self.shortFmt, self.f.read(2)) (value,) = struct.unpack(self.shortFmt, self.f.read(2))
return value return value
def readLong(self): def readLong(self) -> int:
(value,) = struct.unpack(self.longFmt, self.f.read(4)) (value,) = struct.unpack(self.longFmt, self.f.read(4))
return value return value
def rewriteLastShortToLong(self, value): def rewriteLastShortToLong(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg) raise RuntimeError(msg)
def rewriteLastShort(self, value): def rewriteLastShort(self, value: int) -> None:
self.f.seek(-2, os.SEEK_CUR) self.f.seek(-2, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2: if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2" msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg) raise RuntimeError(msg)
def rewriteLastLong(self, value): def rewriteLastLong(self, value: int) -> None:
self.f.seek(-4, os.SEEK_CUR) self.f.seek(-4, os.SEEK_CUR)
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
raise RuntimeError(msg) raise RuntimeError(msg)
def writeShort(self, value): def writeShort(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.shortFmt, value)) bytes_written = self.f.write(struct.pack(self.shortFmt, value))
if bytes_written is not None and bytes_written != 2: if bytes_written is not None and bytes_written != 2:
msg = f"wrote only {bytes_written} bytes but wanted 2" msg = f"wrote only {bytes_written} bytes but wanted 2"
raise RuntimeError(msg) raise RuntimeError(msg)
def writeLong(self, value): def writeLong(self, value: int) -> None:
bytes_written = self.f.write(struct.pack(self.longFmt, value)) bytes_written = self.f.write(struct.pack(self.longFmt, value))
if bytes_written is not None and bytes_written != 4: if bytes_written is not None and bytes_written != 4:
msg = f"wrote only {bytes_written} bytes but wanted 4" msg = f"wrote only {bytes_written} bytes but wanted 4"
@ -2097,9 +2096,9 @@ class AppendingTiffWriter:
field_size = self.fieldSizes[field_type] field_size = self.fieldSizes[field_type]
total_size = field_size * count total_size = field_size * count
is_local = total_size <= 4 is_local = total_size <= 4
offset: int | None
if not is_local: if not is_local:
offset = self.readLong() offset = self.readLong() + self.offsetOfNewPage
offset += self.offsetOfNewPage
self.rewriteLastLong(offset) self.rewriteLastLong(offset)
if tag in self.Tags: if tag in self.Tags:
@ -2149,7 +2148,7 @@ class AppendingTiffWriter:
self.rewriteLastLong(offset) self.rewriteLastLong(offset)
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
encoderconfig = im.encoderconfig encoderconfig = im.encoderconfig
append_images = list(encoderinfo.get("append_images", [])) append_images = list(encoderinfo.get("append_images", []))

View File

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from io import BytesIO from io import BytesIO
from typing import Any from typing import IO, Any
from . import Image, ImageFile from . import Image, ImageFile
@ -182,7 +182,7 @@ class WebPImageFile(ImageFile.ImageFile):
return self.__logical_frame return self.__logical_frame
def _save_all(im, fp, filename): def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
append_images = list(encoderinfo.get("append_images", [])) append_images = list(encoderinfo.get("append_images", []))
@ -195,7 +195,7 @@ def _save_all(im, fp, filename):
_save(im, fp, filename) _save(im, fp, filename)
return return
background = (0, 0, 0, 0) background: int | tuple[int, ...] = (0, 0, 0, 0)
if "background" in encoderinfo: if "background" in encoderinfo:
background = encoderinfo["background"] background = encoderinfo["background"]
elif "background" in im.info: elif "background" in im.info:
@ -325,7 +325,7 @@ def _save_all(im, fp, filename):
fp.write(data) fp.write(data)
def _save(im, fp, filename): def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
lossless = im.encoderinfo.get("lossless", False) lossless = im.encoderinfo.get("lossless", False)
quality = im.encoderinfo.get("quality", 80) quality = im.encoderinfo.get("quality", 80)
alpha_quality = im.encoderinfo.get("alpha_quality", 100) alpha_quality = im.encoderinfo.get("alpha_quality", 100)

View File

@ -163,7 +163,7 @@ class WmfStubImageFile(ImageFile.StubImageFile):
return super().load() return super().load()
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if _handler is None or not hasattr(_handler, "save"): if _handler is None or not hasattr(_handler, "save"):
msg = "WMF save handler not installed" msg = "WMF save handler not installed"
raise OSError(msg) raise OSError(msg)

View File

@ -70,7 +70,7 @@ class XbmImageFile(ImageFile.ImageFile):
self.tile = [("xbm", (0, 0) + self.size, m.end(), None)] self.tile = [("xbm", (0, 0) + self.size, m.end(), None)]
def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None: def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
if im.mode != "1": if im.mode != "1":
msg = f"cannot write mode {im.mode} as XBM" msg = f"cannot write mode {im.mode} as XBM"
raise OSError(msg) raise OSError(msg)