Added type hints

This commit is contained in:
Andrew Murray 2024-07-29 23:46:07 +10:00
parent 7b8a031ec1
commit 6420f73613
7 changed files with 71 additions and 53 deletions

View File

@ -2052,7 +2052,11 @@ class Image:
msg = "illegal image mode" msg = "illegal image mode"
raise ValueError(msg) raise ValueError(msg)
if isinstance(data, ImagePalette.ImagePalette): 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: else:
if not isinstance(data, bytes): if not isinstance(data, bytes):
data = bytes(data) data = bytes(data)

View File

@ -167,7 +167,7 @@ class Draw:
""" """
self.render("polygon", xy, *options) self.render("polygon", xy, *options)
def rectangle(self, xy: Coords, *options) -> None: def rectangle(self, xy: Coords, *options: Any) -> None:
""" """
Draws a rectangle. Draws a rectangle.

View File

@ -269,12 +269,12 @@ class FreeTypeFont:
else: else:
load_from_bytes(font) load_from_bytes(font)
def __getstate__(self): def __getstate__(self) -> list[Any]:
return [self.path, self.size, self.index, self.encoding, self.layout_engine] 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 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]: def getname(self) -> tuple[str | None, str | None]:
""" """

View File

@ -208,7 +208,7 @@ class ImagePalette:
# Internal # Internal
def raw(rawmode, data: Sequence[int] | bytes | bytearray) -> ImagePalette: def raw(rawmode: str, data: Sequence[int] | bytes | bytearray) -> ImagePalette:
palette = ImagePalette() palette = ImagePalette()
palette.rawmode = rawmode palette.rawmode = rawmode
palette.palette = data palette.palette = data

View File

@ -324,7 +324,7 @@ class Jpeg2KImageFile(ImageFile.ImageFile):
return self._reduce or super().reduce return self._reduce or super().reduce
@reduce.setter @reduce.setter
def reduce(self, value): def reduce(self, value: int) -> None:
self._reduce = value self._reduce = value
def load(self) -> Image.core.PixelAccess | None: def load(self) -> Image.core.PixelAccess | None:

View File

@ -25,7 +25,7 @@ import io
import math import math
import os import os
import time import time
from typing import IO from typing import IO, Any
from . import Image, ImageFile, ImageSequence, PdfParser, __version__, features 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. # (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 # FIXME: Should replace ASCIIHexDecode with RunLengthDecode
# (packbits) or LZWDecode (tiff/lzw compression). Note that # (packbits) or LZWDecode (tiff/lzw compression). Note that
# PDF 1.2 also supports Flatedecode (zip compression). # 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 width, height = im.size
dict_obj = {"BitsPerComponent": 8} dict_obj: dict[str, Any] = {"BitsPerComponent": 8}
if im.mode == "1": if im.mode == "1":
if features.check("libtiff"): if features.check("libtiff"):
filter = "CCITTFaxDecode" decode_filter = "CCITTFaxDecode"
dict_obj["BitsPerComponent"] = 1 dict_obj["BitsPerComponent"] = 1
params = PdfParser.PdfArray( params = PdfParser.PdfArray(
[ [
@ -79,22 +84,23 @@ def _write_image(im, filename, existing_pdf, image_refs):
] ]
) )
else: else:
filter = "DCTDecode" decode_filter = "DCTDecode"
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
procset = "ImageB" # grayscale procset = "ImageB" # grayscale
elif im.mode == "L": elif im.mode == "L":
filter = "DCTDecode" decode_filter = "DCTDecode"
# params = f"<< /Predictor 15 /Columns {width-2} >>" # params = f"<< /Predictor 15 /Columns {width-2} >>"
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray") dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceGray")
procset = "ImageB" # grayscale procset = "ImageB" # grayscale
elif im.mode == "LA": elif im.mode == "LA":
filter = "JPXDecode" decode_filter = "JPXDecode"
# params = f"<< /Predictor 15 /Columns {width-2} >>" # params = f"<< /Predictor 15 /Columns {width-2} >>"
procset = "ImageB" # grayscale procset = "ImageB" # grayscale
dict_obj["SMaskInData"] = 1 dict_obj["SMaskInData"] = 1
elif im.mode == "P": elif im.mode == "P":
filter = "ASCIIHexDecode" decode_filter = "ASCIIHexDecode"
palette = im.getpalette() palette = im.getpalette()
assert palette is not None
dict_obj["ColorSpace"] = [ dict_obj["ColorSpace"] = [
PdfParser.PdfName("Indexed"), PdfParser.PdfName("Indexed"),
PdfParser.PdfName("DeviceRGB"), 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] image_ref = _write_image(smask, filename, existing_pdf, image_refs)[0]
dict_obj["SMask"] = image_ref dict_obj["SMask"] = image_ref
elif im.mode == "RGB": elif im.mode == "RGB":
filter = "DCTDecode" decode_filter = "DCTDecode"
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB") dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceRGB")
procset = "ImageC" # color images procset = "ImageC" # color images
elif im.mode == "RGBA": elif im.mode == "RGBA":
filter = "JPXDecode" decode_filter = "JPXDecode"
procset = "ImageC" # color images procset = "ImageC" # color images
dict_obj["SMaskInData"] = 1 dict_obj["SMaskInData"] = 1
elif im.mode == "CMYK": elif im.mode == "CMYK":
filter = "DCTDecode" decode_filter = "DCTDecode"
dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK") dict_obj["ColorSpace"] = PdfParser.PdfName("DeviceCMYK")
procset = "ImageC" # color images procset = "ImageC" # color images
decode = [1, 0, 1, 0, 1, 0, 1, 0] 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() op = io.BytesIO()
if filter == "ASCIIHexDecode": if decode_filter == "ASCIIHexDecode":
ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)]) ImageFile._save(im, op, [("hex", (0, 0) + im.size, 0, im.mode)])
elif filter == "CCITTFaxDecode": elif decode_filter == "CCITTFaxDecode":
im.save( im.save(
op, op,
"TIFF", "TIFF",
@ -141,21 +147,22 @@ def _write_image(im, filename, existing_pdf, image_refs):
# use a single strip # use a single strip
strip_size=math.ceil(width / 8) * height, strip_size=math.ceil(width / 8) * height,
) )
elif filter == "DCTDecode": elif decode_filter == "DCTDecode":
Image.SAVE["JPEG"](im, op, filename) Image.SAVE["JPEG"](im, op, filename)
elif filter == "JPXDecode": elif decode_filter == "JPXDecode":
del dict_obj["BitsPerComponent"] del dict_obj["BitsPerComponent"]
Image.SAVE["JPEG2000"](im, op, filename) Image.SAVE["JPEG2000"](im, op, filename)
else: else:
msg = f"unsupported PDF filter ({filter})" msg = f"unsupported PDF filter ({decode_filter})"
raise ValueError(msg) raise ValueError(msg)
stream = op.getvalue() stream = op.getvalue()
if filter == "CCITTFaxDecode": filter: PdfParser.PdfArray | PdfParser.PdfName
if decode_filter == "CCITTFaxDecode":
stream = stream[8:] stream = stream[8:]
filter = PdfParser.PdfArray([PdfParser.PdfName(filter)]) filter = PdfParser.PdfArray([PdfParser.PdfName(decode_filter)])
else: else:
filter = PdfParser.PdfName(filter) filter = PdfParser.PdfName(decode_filter)
image_ref = image_refs.pop(0) image_ref = image_refs.pop(0)
existing_pdf.write_obj( existing_pdf.write_obj(

View File

@ -47,16 +47,18 @@ import math
import os import os
import struct import struct
import warnings import warnings
from collections.abc import MutableMapping from collections.abc import Iterator, MutableMapping
from fractions import Fraction from fractions import Fraction
from numbers import Number, Rational 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 . import ExifTags, Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16 from ._binary import i16be as i16
from ._binary import i32be as i32 from ._binary import i32be as i32
from ._binary import o8 from ._binary import o8
from ._deprecate import deprecate from ._deprecate import deprecate
from ._typing import StrOrBytesPath
from ._util import is_path
from .TiffTags import TYPES from .TiffTags import TYPES
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -313,7 +315,7 @@ _load_dispatch = {}
_write_dispatch = {} _write_dispatch = {}
def _delegate(op): def _delegate(op: str):
def delegate(self, *args): def delegate(self, *args):
return getattr(self._val, op)(*args) return getattr(self._val, op)(*args)
@ -334,7 +336,9 @@ class IFDRational(Rational):
__slots__ = ("_numerator", "_denominator", "_val") __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 :param value: either an integer numerator, a
float/rational/other number, or an IFDRational float/rational/other number, or an IFDRational
@ -358,18 +362,20 @@ class IFDRational(Rational):
self._val = float("nan") self._val = float("nan")
elif denominator == 1: elif denominator == 1:
self._val = Fraction(value) self._val = Fraction(value)
elif int(value) == value:
self._val = Fraction(int(value), denominator)
else: else:
self._val = Fraction(value, denominator) self._val = Fraction(value / denominator)
@property @property
def numerator(self): def numerator(self):
return self._numerator return self._numerator
@property @property
def denominator(self): def denominator(self) -> int:
return self._denominator 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 :param max_denominator: Integer, the maximum denominator value
@ -379,6 +385,7 @@ class IFDRational(Rational):
if self.denominator == 0: if self.denominator == 0:
return self.numerator, self.denominator return self.numerator, self.denominator
assert isinstance(self._val, Fraction)
f = self._val.limit_denominator(max_denominator) f = self._val.limit_denominator(max_denominator)
return f.numerator, f.denominator return f.numerator, f.denominator
@ -396,14 +403,15 @@ class IFDRational(Rational):
val = float(val) val = float(val)
return val == other return val == other
def __getstate__(self): def __getstate__(self) -> list[float | Fraction]:
return [self._val, self._numerator, self._denominator] return [self._val, self._numerator, self._denominator]
def __setstate__(self, state): def __setstate__(self, state: list[float | Fraction]) -> None:
IFDRational.__init__(self, 0) IFDRational.__init__(self, 0)
_val, _numerator, _denominator = state _val, _numerator, _denominator = state
self._val = _val self._val = _val
self._numerator = _numerator self._numerator = _numerator
assert isinstance(_denominator, int)
self._denominator = _denominator self._denominator = _denominator
""" a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul', """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
@ -730,13 +738,13 @@ class ImageFileDirectory_v2(_IFDv2Base):
self._tags_v1.pop(tag, None) self._tags_v1.pop(tag, None)
self._tagdata.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)) return iter(set(self._tagdata) | set(self._tags_v2))
def _unpack(self, fmt: str, data: bytes): def _unpack(self, fmt: str, data: bytes):
return struct.unpack(self._endian + fmt, data) 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) return struct.pack(self._endian + fmt, *values)
list( list(
@ -787,7 +795,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def load_rational(self, data, legacy_api: bool = True): def load_rational(self, data, legacy_api: bool = True):
vals = self._unpack(f"{len(data) // 4}L", data) 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 (a, b) if legacy_api else IFDRational(a, b)
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
@ -814,7 +822,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
def load_signed_rational(self, data: bytes, legacy_api: bool = True): def load_signed_rational(self, data: bytes, legacy_api: bool = True):
vals = self._unpack(f"{len(data) // 4}l", data) 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 (a, b) if legacy_api else IFDRational(a, b)
return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2])) return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))
@ -903,11 +911,11 @@ class ImageFileDirectory_v2(_IFDv2Base):
warnings.warn(str(msg)) warnings.warn(str(msg))
return return
def tobytes(self, offset=0): def tobytes(self, offset: int = 0) -> bytes:
# FIXME What about tagdata? # FIXME What about tagdata?
result = self._pack("H", len(self._tags_v2)) 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 offset = offset + len(result) + len(self._tags_v2) * 12 + 4
stripoffsets = None stripoffsets = None
@ -916,7 +924,7 @@ class ImageFileDirectory_v2(_IFDv2Base):
for tag, value in sorted(self._tags_v2.items()): for tag, value in sorted(self._tags_v2.items()):
if tag == STRIPOFFSETS: if tag == STRIPOFFSETS:
stripoffsets = len(entries) stripoffsets = len(entries)
typ = self.tagtype.get(tag) typ = self.tagtype[tag]
logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value)) logger.debug("Tag %s, Type: %s, Value: %s", tag, typ, repr(value))
is_ifd = typ == TiffTags.LONG and isinstance(value, dict) is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
if is_ifd: if is_ifd:
@ -1072,7 +1080,7 @@ class ImageFileDirectory_v1(ImageFileDirectory_v2):
def __len__(self) -> int: def __len__(self) -> int:
return len(set(self._tagdata) | set(self._tags_v1)) 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)) return iter(set(self._tagdata) | set(self._tags_v1))
def __setitem__(self, tag: int, value) -> None: def __setitem__(self, tag: int, value) -> None:
@ -1943,17 +1951,18 @@ class AppendingTiffWriter:
521, # JPEGACTables 521, # JPEGACTables
} }
def __init__(self, fn, new: bool = False) -> None: def __init__(self, fn: StrOrBytesPath | IO[bytes], new: bool = False) -> None:
if hasattr(fn, "read"): self.f: IO[bytes]
self.f = fn if is_path(fn):
self.close_fp = False
else:
self.name = fn self.name = fn
self.close_fp = True self.close_fp = True
try: try:
self.f = open(fn, "w+b" if new else "r+b") self.f = open(fn, "w+b" if new else "r+b")
except OSError: except OSError:
self.f = open(fn, "w+b") self.f = open(fn, "w+b")
else:
self.f = cast(IO[bytes], fn)
self.close_fp = False
self.beginning = self.f.tell() self.beginning = self.f.tell()
self.setup() self.setup()
@ -1961,7 +1970,7 @@ class AppendingTiffWriter:
# Reset everything. # Reset everything.
self.f.seek(self.beginning, os.SEEK_SET) self.f.seek(self.beginning, os.SEEK_SET)
self.whereToWriteNewIFDOffset = None self.whereToWriteNewIFDOffset: int | None = None
self.offsetOfNewPage = 0 self.offsetOfNewPage = 0
self.IIMM = iimm = self.f.read(4) self.IIMM = iimm = self.f.read(4)
@ -2000,6 +2009,7 @@ class AppendingTiffWriter:
ifd_offset = self.readLong() ifd_offset = self.readLong()
ifd_offset += self.offsetOfNewPage ifd_offset += self.offsetOfNewPage
assert self.whereToWriteNewIFDOffset is not None
self.f.seek(self.whereToWriteNewIFDOffset) self.f.seek(self.whereToWriteNewIFDOffset)
self.writeLong(ifd_offset) self.writeLong(ifd_offset)
self.f.seek(ifd_offset) self.f.seek(ifd_offset)
@ -2020,7 +2030,7 @@ class AppendingTiffWriter:
def tell(self) -> int: def tell(self) -> int:
return self.f.tell() - self.offsetOfNewPage 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: if whence == os.SEEK_SET:
offset += self.offsetOfNewPage offset += self.offsetOfNewPage
@ -2111,7 +2121,6 @@ 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() + self.offsetOfNewPage offset = self.readLong() + self.offsetOfNewPage
self.rewriteLastLong(offset) self.rewriteLastLong(offset)
@ -2131,8 +2140,6 @@ class AppendingTiffWriter:
) )
self.f.seek(cur_pos) self.f.seek(cur_pos)
offset = cur_pos = None
elif is_local: elif is_local:
# skip the locally stored value that is not an offset # skip the locally stored value that is not an offset
self.f.seek(4, os.SEEK_CUR) self.f.seek(4, os.SEEK_CUR)