mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 17:36:18 +03:00
Added type hints
This commit is contained in:
parent
6944e9e183
commit
01529d8b09
|
@ -57,6 +57,9 @@ class TestImageWinDib:
|
||||||
# Assert
|
# Assert
|
||||||
assert dib.size == (128, 128)
|
assert dib.size == (128, 128)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ImageWin.Dib(mode)
|
||||||
|
|
||||||
def test_dib_paste(self) -> None:
|
def test_dib_paste(self) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
|
@ -65,7 +65,7 @@ def has_ghostscript() -> bool:
|
||||||
return gs_binary is not False
|
return gs_binary is not False
|
||||||
|
|
||||||
|
|
||||||
def Ghostscript(tile, size, fp, scale=1, transparency=False):
|
def Ghostscript(tile, size, fp, scale=1, transparency: bool = False) -> Image.Image:
|
||||||
"""Render an image using Ghostscript"""
|
"""Render an image using Ghostscript"""
|
||||||
global gs_binary
|
global gs_binary
|
||||||
if not has_ghostscript():
|
if not has_ghostscript():
|
||||||
|
|
|
@ -25,7 +25,7 @@ from __future__ import annotations
|
||||||
import warnings
|
import warnings
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from math import ceil, log
|
from math import ceil, log
|
||||||
from typing import IO
|
from typing import IO, NamedTuple
|
||||||
|
|
||||||
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
|
from . import BmpImagePlugin, Image, ImageFile, PngImagePlugin
|
||||||
from ._binary import i16le as i16
|
from ._binary import i16le as i16
|
||||||
|
@ -119,8 +119,22 @@ def _accept(prefix: bytes) -> bool:
|
||||||
return prefix[:4] == _MAGIC
|
return prefix[:4] == _MAGIC
|
||||||
|
|
||||||
|
|
||||||
|
class IconHeader(NamedTuple):
|
||||||
|
width: int
|
||||||
|
height: int
|
||||||
|
nb_color: int
|
||||||
|
reserved: int
|
||||||
|
planes: int
|
||||||
|
bpp: int
|
||||||
|
size: int
|
||||||
|
offset: int
|
||||||
|
dim: tuple[int, int]
|
||||||
|
square: int
|
||||||
|
color_depth: int
|
||||||
|
|
||||||
|
|
||||||
class IcoFile:
|
class IcoFile:
|
||||||
def __init__(self, buf) -> None:
|
def __init__(self, buf: IO[bytes]) -> None:
|
||||||
"""
|
"""
|
||||||
Parse image from file-like object containing ico file data
|
Parse image from file-like object containing ico file data
|
||||||
"""
|
"""
|
||||||
|
@ -141,51 +155,44 @@ class IcoFile:
|
||||||
for i in range(self.nb_items):
|
for i in range(self.nb_items):
|
||||||
s = buf.read(16)
|
s = buf.read(16)
|
||||||
|
|
||||||
icon_header = {
|
|
||||||
"width": s[0],
|
|
||||||
"height": s[1],
|
|
||||||
"nb_color": s[2], # No. of colors in image (0 if >=8bpp)
|
|
||||||
"reserved": s[3],
|
|
||||||
"planes": i16(s, 4),
|
|
||||||
"bpp": i16(s, 6),
|
|
||||||
"size": i32(s, 8),
|
|
||||||
"offset": i32(s, 12),
|
|
||||||
}
|
|
||||||
|
|
||||||
# See Wikipedia
|
# See Wikipedia
|
||||||
for j in ("width", "height"):
|
width = s[0] or 256
|
||||||
if not icon_header[j]:
|
height = s[1] or 256
|
||||||
icon_header[j] = 256
|
|
||||||
|
|
||||||
# See Wikipedia notes about color depth.
|
# No. of colors in image (0 if >=8bpp)
|
||||||
# We need this just to differ images with equal sizes
|
nb_color = s[2]
|
||||||
icon_header["color_depth"] = (
|
bpp = i16(s, 6)
|
||||||
icon_header["bpp"]
|
icon_header = IconHeader(
|
||||||
or (
|
width=width,
|
||||||
icon_header["nb_color"] != 0
|
height=height,
|
||||||
and ceil(log(icon_header["nb_color"], 2))
|
nb_color=nb_color,
|
||||||
)
|
reserved=s[3],
|
||||||
or 256
|
planes=i16(s, 4),
|
||||||
|
bpp=i16(s, 6),
|
||||||
|
size=i32(s, 8),
|
||||||
|
offset=i32(s, 12),
|
||||||
|
dim=(width, height),
|
||||||
|
square=width * height,
|
||||||
|
# See Wikipedia notes about color depth.
|
||||||
|
# We need this just to differ images with equal sizes
|
||||||
|
color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256,
|
||||||
)
|
)
|
||||||
|
|
||||||
icon_header["dim"] = (icon_header["width"], icon_header["height"])
|
|
||||||
icon_header["square"] = icon_header["width"] * icon_header["height"]
|
|
||||||
|
|
||||||
self.entry.append(icon_header)
|
self.entry.append(icon_header)
|
||||||
|
|
||||||
self.entry = sorted(self.entry, key=lambda x: x["color_depth"])
|
self.entry = sorted(self.entry, key=lambda x: x.color_depth)
|
||||||
# ICO images are usually squares
|
# ICO images are usually squares
|
||||||
self.entry = sorted(self.entry, key=lambda x: x["square"], reverse=True)
|
self.entry = sorted(self.entry, key=lambda x: x.square, reverse=True)
|
||||||
|
|
||||||
def sizes(self) -> set[tuple[int, int]]:
|
def sizes(self) -> set[tuple[int, int]]:
|
||||||
"""
|
"""
|
||||||
Get a list of all available icon sizes and color depths.
|
Get a set of all available icon sizes and color depths.
|
||||||
"""
|
"""
|
||||||
return {(h["width"], h["height"]) for h in self.entry}
|
return {(h.width, h.height) for h in self.entry}
|
||||||
|
|
||||||
def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
|
def getentryindex(self, size: tuple[int, int], bpp: int | bool = False) -> int:
|
||||||
for i, h in enumerate(self.entry):
|
for i, h in enumerate(self.entry):
|
||||||
if size == h["dim"] and (bpp is False or bpp == h["color_depth"]):
|
if size == h.dim and (bpp is False or bpp == h.color_depth):
|
||||||
return i
|
return i
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
@ -202,9 +209,9 @@ class IcoFile:
|
||||||
|
|
||||||
header = self.entry[idx]
|
header = self.entry[idx]
|
||||||
|
|
||||||
self.buf.seek(header["offset"])
|
self.buf.seek(header.offset)
|
||||||
data = self.buf.read(8)
|
data = self.buf.read(8)
|
||||||
self.buf.seek(header["offset"])
|
self.buf.seek(header.offset)
|
||||||
|
|
||||||
im: Image.Image
|
im: Image.Image
|
||||||
if data[:8] == PngImagePlugin._MAGIC:
|
if data[:8] == PngImagePlugin._MAGIC:
|
||||||
|
@ -222,8 +229,7 @@ class IcoFile:
|
||||||
im.tile[0] = d, (0, 0) + im.size, o, a
|
im.tile[0] = d, (0, 0) + im.size, o, a
|
||||||
|
|
||||||
# figure out where AND mask image starts
|
# figure out where AND mask image starts
|
||||||
bpp = header["bpp"]
|
if 32 == header.bpp:
|
||||||
if 32 == bpp:
|
|
||||||
# 32-bit color depth icon image allows semitransparent areas
|
# 32-bit color depth icon image allows semitransparent areas
|
||||||
# PIL's DIB format ignores transparency bits, recover them.
|
# PIL's DIB format ignores transparency bits, recover them.
|
||||||
# The DIB is packed in BGRX byte order where X is the alpha
|
# The DIB is packed in BGRX byte order where X is the alpha
|
||||||
|
@ -253,7 +259,7 @@ class IcoFile:
|
||||||
# padded row size * height / bits per char
|
# padded row size * height / bits per char
|
||||||
|
|
||||||
total_bytes = int((w * im.size[1]) / 8)
|
total_bytes = int((w * im.size[1]) / 8)
|
||||||
and_mask_offset = header["offset"] + header["size"] - total_bytes
|
and_mask_offset = header.offset + header.size - total_bytes
|
||||||
|
|
||||||
self.buf.seek(and_mask_offset)
|
self.buf.seek(and_mask_offset)
|
||||||
mask_data = self.buf.read(total_bytes)
|
mask_data = self.buf.read(total_bytes)
|
||||||
|
@ -307,7 +313,7 @@ class IcoImageFile(ImageFile.ImageFile):
|
||||||
def _open(self) -> None:
|
def _open(self) -> None:
|
||||||
self.ico = IcoFile(self.fp)
|
self.ico = IcoFile(self.fp)
|
||||||
self.info["sizes"] = self.ico.sizes()
|
self.info["sizes"] = self.ico.sizes()
|
||||||
self.size = self.ico.entry[0]["dim"]
|
self.size = self.ico.entry[0].dim
|
||||||
self.load()
|
self.load()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -3286,7 +3286,7 @@ def fromarray(obj: SupportsArrayInterface, mode: str | None = None) -> Image:
|
||||||
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
|
return frombuffer(mode, size, obj, "raw", rawmode, 0, 1)
|
||||||
|
|
||||||
|
|
||||||
def fromqimage(im):
|
def fromqimage(im) -> ImageFile.ImageFile:
|
||||||
"""Creates an image instance from a QImage image"""
|
"""Creates an image instance from a QImage image"""
|
||||||
from . import ImageQt
|
from . import ImageQt
|
||||||
|
|
||||||
|
@ -3296,7 +3296,7 @@ def fromqimage(im):
|
||||||
return ImageQt.fromqimage(im)
|
return ImageQt.fromqimage(im)
|
||||||
|
|
||||||
|
|
||||||
def fromqpixmap(im):
|
def fromqpixmap(im) -> ImageFile.ImageFile:
|
||||||
"""Creates an image instance from a QPixmap image"""
|
"""Creates an image instance from a QPixmap image"""
|
||||||
from . import ImageQt
|
from . import ImageQt
|
||||||
|
|
||||||
|
@ -3867,7 +3867,7 @@ class Exif(_ExifBase):
|
||||||
# returns a dict with any single item tuples/lists as individual values
|
# returns a dict with any single item tuples/lists as individual values
|
||||||
return {k: self._fixup(v) for k, v in src_dict.items()}
|
return {k: self._fixup(v) for k, v in src_dict.items()}
|
||||||
|
|
||||||
def _get_ifd_dict(self, offset, group=None):
|
def _get_ifd_dict(self, offset: int, group=None):
|
||||||
try:
|
try:
|
||||||
# an offset pointer to the location of the nested embedded IFD.
|
# an offset pointer to the location of the nested embedded IFD.
|
||||||
# It should be a long, but may be corrupted.
|
# It should be a long, but may be corrupted.
|
||||||
|
@ -3881,7 +3881,7 @@ class Exif(_ExifBase):
|
||||||
info.load(self.fp)
|
info.load(self.fp)
|
||||||
return self._fixup_dict(info)
|
return self._fixup_dict(info)
|
||||||
|
|
||||||
def _get_head(self):
|
def _get_head(self) -> bytes:
|
||||||
version = b"\x2B" if self.bigtiff else b"\x2A"
|
version = b"\x2B" if self.bigtiff else b"\x2A"
|
||||||
if self.endian == "<":
|
if self.endian == "<":
|
||||||
head = b"II" + version + b"\x00" + o32le(8)
|
head = b"II" + version + b"\x00" + o32le(8)
|
||||||
|
@ -4102,16 +4102,16 @@ class Exif(_ExifBase):
|
||||||
keys.update(self._info)
|
keys.update(self._info)
|
||||||
return len(keys)
|
return len(keys)
|
||||||
|
|
||||||
def __getitem__(self, tag):
|
def __getitem__(self, tag: int):
|
||||||
if self._info is not None and tag not in self._data and tag in self._info:
|
if self._info is not None and tag not in self._data and tag in self._info:
|
||||||
self._data[tag] = self._fixup(self._info[tag])
|
self._data[tag] = self._fixup(self._info[tag])
|
||||||
del self._info[tag]
|
del self._info[tag]
|
||||||
return self._data[tag]
|
return self._data[tag]
|
||||||
|
|
||||||
def __contains__(self, tag) -> bool:
|
def __contains__(self, tag: object) -> bool:
|
||||||
return tag in self._data or (self._info is not None and tag in self._info)
|
return tag in self._data or (self._info is not None and tag in self._info)
|
||||||
|
|
||||||
def __setitem__(self, tag, value) -> None:
|
def __setitem__(self, tag: int, value) -> None:
|
||||||
if self._info is not None and tag in self._info:
|
if self._info is not None and tag in self._info:
|
||||||
del self._info[tag]
|
del self._info[tag]
|
||||||
self._data[tag] = value
|
self._data[tag] = value
|
||||||
|
|
|
@ -553,7 +553,9 @@ def _save(im, fp, tile, bufsize: int = 0) -> None:
|
||||||
fp.flush()
|
fp.flush()
|
||||||
|
|
||||||
|
|
||||||
def _encode_tile(im, fp, tile: list[_Tile], bufsize: int, fh, exc=None) -> None:
|
def _encode_tile(
|
||||||
|
im, fp: IO[bytes], tile: list[_Tile], bufsize: int, fh, exc=None
|
||||||
|
) -> None:
|
||||||
for encoder_name, extents, offset, args in tile:
|
for encoder_name, extents, offset, args in tile:
|
||||||
if offset > 0:
|
if offset > 0:
|
||||||
fp.seek(offset)
|
fp.seek(offset)
|
||||||
|
@ -653,7 +655,7 @@ class PyCodec:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def setfd(self, fd) -> None:
|
def setfd(self, fd: IO[bytes]) -> None:
|
||||||
"""
|
"""
|
||||||
Called from ImageFile to set the Python file-like object
|
Called from ImageFile to set the Python file-like object
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,14 @@ from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Callable
|
from typing import TYPE_CHECKING, Callable
|
||||||
|
|
||||||
from . import Image
|
from . import Image
|
||||||
from ._util import is_path
|
from ._util import is_path
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import ImageFile
|
||||||
|
|
||||||
qt_version: str | None
|
qt_version: str | None
|
||||||
qt_versions = [
|
qt_versions = [
|
||||||
["6", "PyQt6"],
|
["6", "PyQt6"],
|
||||||
|
@ -90,11 +93,11 @@ def fromqimage(im):
|
||||||
return Image.open(b)
|
return Image.open(b)
|
||||||
|
|
||||||
|
|
||||||
def fromqpixmap(im):
|
def fromqpixmap(im) -> ImageFile.ImageFile:
|
||||||
return fromqimage(im)
|
return fromqimage(im)
|
||||||
|
|
||||||
|
|
||||||
def align8to32(bytes, width, mode):
|
def align8to32(bytes: bytes, width: int, mode: str) -> bytes:
|
||||||
"""
|
"""
|
||||||
converts each scanline of data from 8 bit to 32 bit aligned
|
converts each scanline of data from 8 bit to 32 bit aligned
|
||||||
"""
|
"""
|
||||||
|
@ -172,7 +175,7 @@ def _toqclass_helper(im):
|
||||||
if qt_is_installed:
|
if qt_is_installed:
|
||||||
|
|
||||||
class ImageQt(QImage):
|
class ImageQt(QImage):
|
||||||
def __init__(self, im):
|
def __init__(self, im) -> None:
|
||||||
"""
|
"""
|
||||||
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
|
An PIL image wrapper for Qt. This is a subclass of PyQt's QImage
|
||||||
class.
|
class.
|
||||||
|
|
|
@ -70,11 +70,14 @@ class Dib:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, image: Image.Image | str, size: tuple[int, int] | list[int] | None = None
|
self, image: Image.Image | str, size: tuple[int, int] | None = None
|
||||||
) -> None:
|
) -> None:
|
||||||
if isinstance(image, str):
|
if isinstance(image, str):
|
||||||
mode = image
|
mode = image
|
||||||
image = ""
|
image = ""
|
||||||
|
if size is None:
|
||||||
|
msg = "If first argument is mode, size is required"
|
||||||
|
raise ValueError(msg)
|
||||||
else:
|
else:
|
||||||
mode = image.mode
|
mode = image.mode
|
||||||
size = image.size
|
size = image.size
|
||||||
|
@ -105,7 +108,12 @@ class Dib:
|
||||||
result = self.image.expose(handle)
|
result = self.image.expose(handle)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def draw(self, handle, dst, src=None):
|
def draw(
|
||||||
|
self,
|
||||||
|
handle,
|
||||||
|
dst: tuple[int, int, int, int],
|
||||||
|
src: tuple[int, int, int, int] | None = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Same as expose, but allows you to specify where to draw the image, and
|
Same as expose, but allows you to specify where to draw the image, and
|
||||||
what part of it to draw.
|
what part of it to draw.
|
||||||
|
@ -115,7 +123,7 @@ class Dib:
|
||||||
the destination have different sizes, the image is resized as
|
the destination have different sizes, the image is resized as
|
||||||
necessary.
|
necessary.
|
||||||
"""
|
"""
|
||||||
if not src:
|
if src is None:
|
||||||
src = (0, 0) + self.size
|
src = (0, 0) + self.size
|
||||||
if isinstance(handle, HWND):
|
if isinstance(handle, HWND):
|
||||||
dc = self.image.getdc(handle)
|
dc = self.image.getdc(handle)
|
||||||
|
@ -202,22 +210,22 @@ class Window:
|
||||||
title, self.__dispatcher, width or 0, height or 0
|
title, self.__dispatcher, width or 0, height or 0
|
||||||
)
|
)
|
||||||
|
|
||||||
def __dispatcher(self, action, *args):
|
def __dispatcher(self, action: str, *args):
|
||||||
return getattr(self, f"ui_handle_{action}")(*args)
|
return getattr(self, f"ui_handle_{action}")(*args)
|
||||||
|
|
||||||
def ui_handle_clear(self, dc, x0, y0, x1, y1):
|
def ui_handle_clear(self, dc, x0, y0, x1, y1) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def ui_handle_damage(self, x0, y0, x1, y1):
|
def ui_handle_damage(self, x0, y0, x1, y1) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def ui_handle_destroy(self) -> None:
|
def ui_handle_destroy(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def ui_handle_repair(self, dc, x0, y0, x1, y1):
|
def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def ui_handle_resize(self, width, height):
|
def ui_handle_resize(self, width, height) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def mainloop(self) -> None:
|
def mainloop(self) -> None:
|
||||||
|
@ -227,12 +235,12 @@ class Window:
|
||||||
class ImageWindow(Window):
|
class ImageWindow(Window):
|
||||||
"""Create an image window which displays the given image."""
|
"""Create an image window which displays the given image."""
|
||||||
|
|
||||||
def __init__(self, image, title="PIL"):
|
def __init__(self, image, title: str = "PIL") -> None:
|
||||||
if not isinstance(image, Dib):
|
if not isinstance(image, Dib):
|
||||||
image = Dib(image)
|
image = Dib(image)
|
||||||
self.image = image
|
self.image = image
|
||||||
width, height = image.size
|
width, height = image.size
|
||||||
super().__init__(title, width=width, height=height)
|
super().__init__(title, width=width, height=height)
|
||||||
|
|
||||||
def ui_handle_repair(self, dc, x0, y0, x1, y1):
|
def ui_handle_repair(self, dc, x0, y0, x1, y1) -> None:
|
||||||
self.image.draw(dc, (x0, y0, x1, y1))
|
self.image.draw(dc, (x0, y0, x1, y1))
|
||||||
|
|
|
@ -18,6 +18,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Sequence
|
from collections.abc import Sequence
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
from . import Image, ImageFile
|
from . import Image, ImageFile
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
|
@ -184,7 +185,7 @@ Image.register_open(IptcImageFile.format, IptcImageFile)
|
||||||
Image.register_extension(IptcImageFile.format, ".iim")
|
Image.register_extension(IptcImageFile.format, ".iim")
|
||||||
|
|
||||||
|
|
||||||
def getiptcinfo(im):
|
def getiptcinfo(im: ImageFile.ImageFile):
|
||||||
"""
|
"""
|
||||||
Get IPTC information from TIFF, JPEG, or IPTC file.
|
Get IPTC information from TIFF, JPEG, or IPTC file.
|
||||||
|
|
||||||
|
@ -221,16 +222,17 @@ def getiptcinfo(im):
|
||||||
class FakeImage:
|
class FakeImage:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
im = FakeImage()
|
fake_im = FakeImage()
|
||||||
im.__class__ = IptcImageFile
|
fake_im.__class__ = IptcImageFile # type: ignore[assignment]
|
||||||
|
iptc_im = cast(IptcImageFile, fake_im)
|
||||||
|
|
||||||
# parse the IPTC information chunk
|
# parse the IPTC information chunk
|
||||||
im.info = {}
|
iptc_im.info = {}
|
||||||
im.fp = BytesIO(data)
|
iptc_im.fp = BytesIO(data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
im._open()
|
iptc_im._open()
|
||||||
except (IndexError, KeyError):
|
except (IndexError, KeyError):
|
||||||
pass # expected failure
|
pass # expected failure
|
||||||
|
|
||||||
return im.info
|
return iptc_im.info
|
||||||
|
|
|
@ -685,7 +685,11 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
subsampling = get_sampling(im)
|
subsampling = get_sampling(im)
|
||||||
|
|
||||||
def validate_qtables(qtables):
|
def validate_qtables(
|
||||||
|
qtables: (
|
||||||
|
str | tuple[list[int], ...] | list[list[int]] | dict[int, list[int]] | None
|
||||||
|
)
|
||||||
|
) -> list[list[int]] | None:
|
||||||
if qtables is None:
|
if qtables is None:
|
||||||
return qtables
|
return qtables
|
||||||
if isinstance(qtables, str):
|
if isinstance(qtables, str):
|
||||||
|
@ -715,12 +719,12 @@ def _save(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
if len(table) != 64:
|
if len(table) != 64:
|
||||||
msg = "Invalid quantization table"
|
msg = "Invalid quantization table"
|
||||||
raise TypeError(msg)
|
raise TypeError(msg)
|
||||||
table = array.array("H", table)
|
table_array = array.array("H", table)
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
msg = "Invalid quantization table"
|
msg = "Invalid quantization table"
|
||||||
raise ValueError(msg) from e
|
raise ValueError(msg) from e
|
||||||
else:
|
else:
|
||||||
qtables[idx] = list(table)
|
qtables[idx] = list(table_array)
|
||||||
return qtables
|
return qtables
|
||||||
|
|
||||||
if qtables == "keep":
|
if qtables == "keep":
|
||||||
|
|
|
@ -39,7 +39,7 @@ import struct
|
||||||
import warnings
|
import warnings
|
||||||
import zlib
|
import zlib
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import IO, TYPE_CHECKING, Any, NoReturn
|
from typing import IO, TYPE_CHECKING, Any, NamedTuple, NoReturn
|
||||||
|
|
||||||
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
||||||
from ._binary import i16be as i16
|
from ._binary import i16be as i16
|
||||||
|
@ -1126,7 +1126,21 @@ class _fdat:
|
||||||
self.seq_num += 1
|
self.seq_num += 1
|
||||||
|
|
||||||
|
|
||||||
def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_images):
|
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],
|
||||||
|
chunk,
|
||||||
|
mode: str,
|
||||||
|
rawmode: str,
|
||||||
|
default_image: Image.Image | None,
|
||||||
|
append_images: list[Image.Image],
|
||||||
|
) -> Image.Image | None:
|
||||||
duration = im.encoderinfo.get("duration")
|
duration = im.encoderinfo.get("duration")
|
||||||
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
|
loop = im.encoderinfo.get("loop", im.info.get("loop", 0))
|
||||||
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
|
disposal = im.encoderinfo.get("disposal", im.info.get("disposal", Disposal.OP_NONE))
|
||||||
|
@ -1137,7 +1151,7 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
|
||||||
else:
|
else:
|
||||||
chain = itertools.chain([im], append_images)
|
chain = itertools.chain([im], append_images)
|
||||||
|
|
||||||
im_frames = []
|
im_frames: list[_Frame] = []
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
for im_seq in chain:
|
for im_seq in chain:
|
||||||
for im_frame in ImageSequence.Iterator(im_seq):
|
for im_frame in ImageSequence.Iterator(im_seq):
|
||||||
|
@ -1158,24 +1172,24 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
|
||||||
|
|
||||||
if im_frames:
|
if im_frames:
|
||||||
previous = im_frames[-1]
|
previous = im_frames[-1]
|
||||||
prev_disposal = previous["encoderinfo"].get("disposal")
|
prev_disposal = previous.encoderinfo.get("disposal")
|
||||||
prev_blend = previous["encoderinfo"].get("blend")
|
prev_blend = previous.encoderinfo.get("blend")
|
||||||
if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
|
if prev_disposal == Disposal.OP_PREVIOUS and len(im_frames) < 2:
|
||||||
prev_disposal = Disposal.OP_BACKGROUND
|
prev_disposal = Disposal.OP_BACKGROUND
|
||||||
|
|
||||||
if prev_disposal == Disposal.OP_BACKGROUND:
|
if prev_disposal == Disposal.OP_BACKGROUND:
|
||||||
base_im = previous["im"].copy()
|
base_im = previous.im.copy()
|
||||||
dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
|
dispose = Image.core.fill("RGBA", im.size, (0, 0, 0, 0))
|
||||||
bbox = previous["bbox"]
|
bbox = previous.bbox
|
||||||
if bbox:
|
if bbox:
|
||||||
dispose = dispose.crop(bbox)
|
dispose = dispose.crop(bbox)
|
||||||
else:
|
else:
|
||||||
bbox = (0, 0) + im.size
|
bbox = (0, 0) + im.size
|
||||||
base_im.paste(dispose, bbox)
|
base_im.paste(dispose, bbox)
|
||||||
elif prev_disposal == Disposal.OP_PREVIOUS:
|
elif prev_disposal == Disposal.OP_PREVIOUS:
|
||||||
base_im = im_frames[-2]["im"]
|
base_im = im_frames[-2].im
|
||||||
else:
|
else:
|
||||||
base_im = previous["im"]
|
base_im = previous.im
|
||||||
delta = ImageChops.subtract_modulo(
|
delta = ImageChops.subtract_modulo(
|
||||||
im_frame.convert("RGBA"), base_im.convert("RGBA")
|
im_frame.convert("RGBA"), base_im.convert("RGBA")
|
||||||
)
|
)
|
||||||
|
@ -1186,14 +1200,14 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
|
||||||
and prev_blend == encoderinfo.get("blend")
|
and prev_blend == encoderinfo.get("blend")
|
||||||
and "duration" in encoderinfo
|
and "duration" in encoderinfo
|
||||||
):
|
):
|
||||||
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
|
previous.encoderinfo["duration"] += encoderinfo["duration"]
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
bbox = None
|
bbox = None
|
||||||
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
|
im_frames.append(_Frame(im_frame, bbox, encoderinfo))
|
||||||
|
|
||||||
if len(im_frames) == 1 and not default_image:
|
if len(im_frames) == 1 and not default_image:
|
||||||
return im_frames[0]["im"]
|
return im_frames[0].im
|
||||||
|
|
||||||
# animation control
|
# animation control
|
||||||
chunk(
|
chunk(
|
||||||
|
@ -1211,14 +1225,14 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
|
||||||
|
|
||||||
seq_num = 0
|
seq_num = 0
|
||||||
for frame, frame_data in enumerate(im_frames):
|
for frame, frame_data in enumerate(im_frames):
|
||||||
im_frame = frame_data["im"]
|
im_frame = frame_data.im
|
||||||
if not frame_data["bbox"]:
|
if not frame_data.bbox:
|
||||||
bbox = (0, 0) + im_frame.size
|
bbox = (0, 0) + im_frame.size
|
||||||
else:
|
else:
|
||||||
bbox = frame_data["bbox"]
|
bbox = frame_data.bbox
|
||||||
im_frame = im_frame.crop(bbox)
|
im_frame = im_frame.crop(bbox)
|
||||||
size = im_frame.size
|
size = im_frame.size
|
||||||
encoderinfo = frame_data["encoderinfo"]
|
encoderinfo = frame_data.encoderinfo
|
||||||
frame_duration = int(round(encoderinfo.get("duration", 0)))
|
frame_duration = int(round(encoderinfo.get("duration", 0)))
|
||||||
frame_disposal = encoderinfo.get("disposal", disposal)
|
frame_disposal = encoderinfo.get("disposal", disposal)
|
||||||
frame_blend = encoderinfo.get("blend", blend)
|
frame_blend = encoderinfo.get("blend", blend)
|
||||||
|
@ -1253,6 +1267,7 @@ def _write_multiple_frames(im, fp, chunk, mode, rawmode, default_image, append_i
|
||||||
[("zip", (0, 0) + im_frame.size, 0, rawmode)],
|
[("zip", (0, 0) + im_frame.size, 0, rawmode)],
|
||||||
)
|
)
|
||||||
seq_num = fdat_chunks.seq_num
|
seq_num = fdat_chunks.seq_num
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
|
@ -1437,12 +1452,15 @@ def _save(
|
||||||
exif = exif[6:]
|
exif = exif[6:]
|
||||||
chunk(fp, b"eXIf", exif)
|
chunk(fp, b"eXIf", exif)
|
||||||
|
|
||||||
|
single_im: Image.Image | None = im
|
||||||
if save_all:
|
if save_all:
|
||||||
im = _write_multiple_frames(
|
single_im = _write_multiple_frames(
|
||||||
im, fp, chunk, mode, rawmode, default_image, append_images
|
im, fp, chunk, mode, rawmode, default_image, append_images
|
||||||
)
|
)
|
||||||
if im:
|
if single_im:
|
||||||
ImageFile._save(im, _idat(fp, chunk), [("zip", (0, 0) + im.size, 0, rawmode)])
|
ImageFile._save(
|
||||||
|
single_im, _idat(fp, chunk), [("zip", (0, 0) + single_im.size, 0, rawmode)]
|
||||||
|
)
|
||||||
|
|
||||||
if info:
|
if info:
|
||||||
for info_chunk in info.chunks:
|
for info_chunk in info.chunks:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user