mirror of
				https://github.com/python-pillow/Pillow.git
				synced 2025-10-31 16:07:30 +03:00 
			
		
		
		
	
						commit
						f19e07b58c
					
				|  | @ -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 |  | ||||||
| 
 | 
 | ||||||
|  |             # No. of colors in image (0 if >=8bpp) | ||||||
|  |             nb_color = s[2] | ||||||
|  |             bpp = i16(s, 6) | ||||||
|  |             icon_header = IconHeader( | ||||||
|  |                 width=width, | ||||||
|  |                 height=height, | ||||||
|  |                 nb_color=nb_color, | ||||||
|  |                 reserved=s[3], | ||||||
|  |                 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. |                 # See Wikipedia notes about color depth. | ||||||
|                 # We need this just to differ images with equal sizes |                 # We need this just to differ images with equal sizes | ||||||
|             icon_header["color_depth"] = ( |                 color_depth=bpp or (nb_color != 0 and ceil(log(nb_color, 2))) or 256, | ||||||
|                 icon_header["bpp"] |  | ||||||
|                 or ( |  | ||||||
|                     icon_header["nb_color"] != 0 |  | ||||||
|                     and ceil(log(icon_header["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 header.bpp == 32: | ||||||
|             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