diff --git a/src/PIL/Image.py b/src/PIL/Image.py index eb3dd5a36f9..ae8634539e8 100644 --- a/src/PIL/Image.py +++ b/src/PIL/Image.py @@ -2052,7 +2052,11 @@ def putpalette( msg = "illegal image mode" raise ValueError(msg) if isinstance(data, ImagePalette.ImagePalette): - palette = ImagePalette.raw(data.rawmode, data.palette) + if data.rawmode is not None: + palette = ImagePalette.raw(data.rawmode, data.palette) + else: + palette = ImagePalette.ImagePalette(palette=data.palette) + palette.dirty = 1 else: if not isinstance(data, bytes): data = bytes(data) diff --git a/src/PIL/ImageDraw2.py b/src/PIL/ImageDraw2.py index 1ad239382cb..58302f95050 100644 --- a/src/PIL/ImageDraw2.py +++ b/src/PIL/ImageDraw2.py @@ -167,7 +167,7 @@ def polygon(self, xy: Coords, *options: Any) -> None: """ self.render("polygon", xy, *options) - def rectangle(self, xy: Coords, *options) -> None: + def rectangle(self, xy: Coords, *options: Any) -> None: """ Draws a rectangle. diff --git a/src/PIL/ImageFont.py b/src/PIL/ImageFont.py index 24490348c32..2ab65bfefe4 100644 --- a/src/PIL/ImageFont.py +++ b/src/PIL/ImageFont.py @@ -269,12 +269,12 @@ def load_from_bytes(f) -> None: else: load_from_bytes(font) - def __getstate__(self): + def __getstate__(self) -> list[Any]: return [self.path, self.size, self.index, self.encoding, self.layout_engine] - def __setstate__(self, state): + def __setstate__(self, state: list[Any]) -> None: path, size, index, encoding, layout_engine = state - self.__init__(path, size, index, encoding, layout_engine) + FreeTypeFont.__init__(self, path, size, index, encoding, layout_engine) def getname(self) -> tuple[str | None, str | None]: """ diff --git a/src/PIL/ImagePalette.py b/src/PIL/ImagePalette.py index 8ccecbd07ca..183f85526d2 100644 --- a/src/PIL/ImagePalette.py +++ b/src/PIL/ImagePalette.py @@ -208,7 +208,7 @@ def save(self, fp: str | IO[str]) -> None: # Internal -def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: +def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette: palette = ImagePalette() palette.rawmode = rawmode palette.palette = data diff --git a/src/PIL/Jpeg2KImagePlugin.py b/src/PIL/Jpeg2KImagePlugin.py index eeec41686ca..ef9107f0073 100644 --- a/src/PIL/Jpeg2KImagePlugin.py +++ b/src/PIL/Jpeg2KImagePlugin.py @@ -324,7 +324,7 @@ def reduce(self): return self._reduce or super().reduce @reduce.setter - def reduce(self, value): + def reduce(self, value: int) -> None: self._reduce = value def load(self) -> Image.core.PixelAccess | None: diff --git a/src/PIL/PdfImagePlugin.py b/src/PIL/PdfImagePlugin.py index e0f732199bc..7fc1108bb84 100644 --- a/src/PIL/PdfImagePlugin.py +++ b/src/PIL/PdfImagePlugin.py @@ -25,7 +25,7 @@ import math import os import time -from typing import IO +from typing import IO, Any from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features @@ -48,7 +48,12 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None: # (Internal) Image save plugin for the PDF format. -def _write_image(im, filename, existing_pdf, image_refs): +def _write_image( + im: Image.Image, + filename: str | bytes, + existing_pdf: PdfParser.PdfParser, + image_refs: list[PdfParser.IndirectReference], +) -> tuple[PdfParser.IndirectReference, str]: # FIXME: Should replace ASCIIHexDecode with RunLengthDecode # (packbits) or LZWDecode (tiff/lzw compression). Note that # PDF 1.2 also supports Flatedecode (zip compression). @@ -61,10 +66,10 @@ def _write_image(im, filename, existing_pdf, image_refs): width, height = im.size - dict_obj = {"BitsPerComponent": 8} + dict_obj: dict[str, Any] = {"BitsPerComponent": 8} if im.mode == "1": if features.check("libtiff"): - filter = "CCITTFaxDecode" + decode_filter = "CCITTFaxDecode" dict_obj["BitsPerComponent"] = 1 params = PdfParser.PdfArray( [ @@ -79,22 +84,23 @@ def _write_image(im, filename, existing_pdf, image_refs): ] ) else: - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "L": - filter = "DCTDecode" + decode_filter = "DCTDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") procset = "ImageB" # grayscale elif im.mode == "LA": - filter = "JPXDecode" + decode_filter = "JPXDecode" # params = f"<< /Predictor 15 /Columns {width-2} >>" procset = "ImageB" # grayscale dict_obj["SMaskInData"] = 1 elif im.mode == "P": - filter = "ASCIIHexDecode" + decode_filter = "ASCIIHexDecode" palette = im.getpalette() + assert palette is not None dict_obj["ColorSpace"] = [ PdfParser.PdfName("Indexed"), PdfParser.PdfName("DeviceRGB"), @@ -110,15 +116,15 @@ def _write_image(im, filename, existing_pdf, image_refs): image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0] dict_obj["SMask"] = image_ref elif im.mode == "RGB": - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") procset = "ImageC" # color images elif im.mode == "RGBA": - filter = "JPXDecode" + decode_filter = "JPXDecode" procset = "ImageC" # color images dict_obj["SMaskInData"] = 1 elif im.mode == "CMYK": - filter = "DCTDecode" + decode_filter = "DCTDecode" dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") procset = "ImageC" # color images decode = [1, 0, 1, 0, 1, 0, 1, 0] @@ -131,9 +137,9 @@ def _write_image(im, filename, existing_pdf, image_refs): op = io.BytesIO() - if filter == "ASCIIHexDecode": + if decode_filter == "ASCIIHexDecode": ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) - elif filter == "CCITTFaxDecode": + elif decode_filter == "CCITTFaxDecode": im.save( op, "TIFF", @@ -141,21 +147,22 @@ def _write_image(im, filename, existing_pdf, image_refs): # use a single strip strip_size=math.ceil(width / 8) * height, ) - elif filter == "DCTDecode": + elif decode_filter == "DCTDecode": Image.SAVE["JPEG"](im, op, filename) - elif filter == "JPXDecode": + elif decode_filter == "JPXDecode": del dict_obj["BitsPerComponent"] Image.SAVE["JPEG2000"](im, op, filename) else: - msg = f"unsupported PDF filter ({filter})" + msg = f"unsupported PDF filter ({decode_filter})" raise ValueError(msg) stream = op.getvalue() - if filter == "CCITTFaxDecode": + filter: PdfParser.PdfArray | PdfParser.PdfName + if decode_filter == "CCITTFaxDecode": stream = stream[8:] - filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) + filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)]) else: - filter = PdfParser.PdfName(filter) + filter = PdfParser.PdfName(decode_filter) image_ref = image_refs.pop(0) existing_pdf.write_obj( diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py index 318e9825ca8..dd013a0cf45 100644 --- a/src/PIL/TiffImagePlugin.py +++ b/src/PIL/TiffImagePlugin.py @@ -47,16 +47,18 @@ import os import struct import warnings -from collections.abc import MutableMapping +from collections.abc import Iterator, MutableMapping from fractions import Fraction from numbers import Number, Rational -from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn +from typing import IO, TYPE_CHECKING, Any, Callable, NoReturn, cast from . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags from ._binary import i16be as i16 from ._binary import i32be as i32 from ._binary import o8 from ._deprecate import deprecate +from ._typing import StrOrBytesPath +from ._util import is_path from .TiffTags import TYPES logger = logging.getLogger(__name__) @@ -313,7 +315,7 @@ def _limit_signed_rational(val, max_val, min_val): _write_dispatch = {} -def _delegate(op): +def _delegate(op: str): def delegate(self, *args): return getattr(self._val, op)(*args) @@ -334,7 +336,9 @@ class IFDRational(Rational): __slots__ = ("_numerator", "_denominator", "_val") - def __init__(self, value, denominator: int = 1) -> None: + def __init__( + self, value: float | Fraction | IFDRational, denominator: int = 1 + ) -> None: """ :param value: either an integer numerator, a float/rational/other number, or an IFDRational @@ -358,18 +362,20 @@ def __init__(self, value, denominator: int = 1) -> None: self._val = float("nan") elif denominator == 1: self._val = Fraction(value) + elif int(value) == value: + self._val = Fraction(int(value), denominator) else: - self._val = Fraction(value, denominator) + self._val = Fraction(value / denominator) @property def numerator(self): return self._numerator @property - def denominator(self): + def denominator(self) -> int: return self._denominator - def limit_rational(self, max_denominator): + def limit_rational(self, max_denominator: int) -> tuple[float, int]: """ :param max_denominator: Integer, the maximum denominator value @@ -379,6 +385,7 @@ def limit_rational(self, max_denominator): if self.denominator == 0: return self.numerator, self.denominator + assert isinstance(self._val, Fraction) f = self._val.limit_denominator(max_denominator) return f.numerator, f.denominator @@ -396,14 +403,15 @@ def __eq__(self, other: object) -> bool: val = float(val) return val == other - def __getstate__(self): + def __getstate__(self) -> list[float | Fraction]: return [self._val, self._numerator, self._denominator] - def __setstate__(self, state): + def __setstate__(self, state: list[float | Fraction]) -> None: IFDRational.__init__(self, 0) _val, _numerator, _denominator = state self._val = _val self._numerator = _numerator + assert isinstance(_denominator, int) self._denominator = _denominator """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', @@ -730,13 +738,13 @@ def __delitem__(self, tag: int) -> None: self._tags_v1.pop(tag, None) self._tagdata.pop(tag, None) - def __iter__(self): + def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v2)) def _unpack(self, fmt: str, data: bytes): return struct.unpack(self._endian + fmt, data) - def _pack(self, fmt: str, *values): + def _pack(self, fmt: str, *values) -> bytes: return struct.pack(self._endian + fmt, *values) list( @@ -787,7 +795,7 @@ def write_string(self, value: str | bytes | int) -> bytes: def load_rational(self, data, legacy_api: bool = True): vals = self._unpack(f"{len(data) // 4}L", data) - def combine(a, b): + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -814,7 +822,7 @@ def write_undefined(self, value: bytes | int | IFDRational) -> bytes: def load_signed_rational(self, data: bytes, legacy_api: bool = True): vals = self._unpack(f"{len(data) // 4}l", data) - def combine(a, b): + def combine(a: int, b: int) -> tuple[int, int] | IFDRational: return (a, b) if legacy_api else IFDRational(a, b) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) @@ -903,11 +911,11 @@ def load(self, fp): warnings.warn(str(msg)) return - def tobytes(self, offset=0): + def tobytes(self, offset: int = 0) -> bytes: # FIXME What about tagdata? result = self._pack("H", len(self._tags_v2)) - entries = [] + entries: list[tuple[int, int, int, bytes, bytes]] = [] offset = offset + len(result) + len(self._tags_v2) * 12 + 4 stripoffsets = None @@ -916,7 +924,7 @@ def tobytes(self, offset=0): for tag, value in sorted(self._tags_v2.items()): if tag == STRIPOFFSETS: stripoffsets = len(entries) - typ = self.tagtype.get(tag) + typ = self.tagtype[tag] logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) is_ifd = typ == TiffTags.LONG and isinstance(value, dict) if is_ifd: @@ -1072,7 +1080,7 @@ def __contains__(self, tag: object) -> bool: def __len__(self) -> int: return len(set(self._tagdata) | set(self._tags_v1)) - def __iter__(self): + def __iter__(self) -> Iterator[int]: return iter(set(self._tagdata) | set(self._tags_v1)) def __setitem__(self, tag: int, value) -> None: @@ -1943,17 +1951,18 @@ class AppendingTiffWriter: 521, # JPEGACTables } - def __init__(self, fn, new: bool = False) -> None: - if hasattr(fn, "read"): - self.f = fn - self.close_fp = False - else: + def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None: + self.f: IO[bytes] + if is_path(fn): self.name = fn self.close_fp = True try: self.f = open(fn, "w+b" if new else "r+b") except OSError: self.f = open(fn, "w+b") + else: + self.f = cast(IO[bytes], fn) + self.close_fp = False self.beginning = self.f.tell() self.setup() @@ -1961,7 +1970,7 @@ def setup(self) -> None: # Reset everything. self.f.seek(self.beginning, os.SEEK_SET) - self.whereToWriteNewIFDOffset = None + self.whereToWriteNewIFDOffset: int | None = None self.offsetOfNewPage = 0 self.IIMM = iimm = self.f.read(4) @@ -2000,6 +2009,7 @@ def finalize(self) -> None: ifd_offset = self.readLong() ifd_offset += self.offsetOfNewPage + assert self.whereToWriteNewIFDOffset is not None self.f.seek(self.whereToWriteNewIFDOffset) self.writeLong(ifd_offset) self.f.seek(ifd_offset) @@ -2020,7 +2030,7 @@ def __exit__(self, *args: object) -> None: def tell(self) -> int: return self.f.tell() - self.offsetOfNewPage - def seek(self, offset: int, whence=io.SEEK_SET) -> int: + def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: if whence == os.SEEK_SET: offset += self.offsetOfNewPage @@ -2111,7 +2121,6 @@ def fixIFD(self) -> None: field_size = self.fieldSizes[field_type] total_size = field_size * count is_local = total_size <= 4 - offset: int | None if not is_local: offset = self.readLong() + self.offsetOfNewPage self.rewriteLastLong(offset) @@ -2131,8 +2140,6 @@ def fixIFD(self) -> None: ) self.f.seek(cur_pos) - offset = cur_pos = None - elif is_local: # skip the locally stored value that is not an offset self.f.seek(4, os.SEEK_CUR)