mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-12-25 01:16:16 +03:00
Merge pull request #8128 from radarhere/type_hint_gif
This commit is contained in:
commit
ecf3a986ef
|
@ -53,6 +53,7 @@ def test_closed_file() -> None:
|
||||||
|
|
||||||
def test_seek_after_close() -> None:
|
def test_seek_after_close() -> None:
|
||||||
im = Image.open("Tests/images/iss634.gif")
|
im = Image.open("Tests/images/iss634.gif")
|
||||||
|
assert isinstance(im, GifImagePlugin.GifImageFile)
|
||||||
im.load()
|
im.load()
|
||||||
im.close()
|
im.close()
|
||||||
|
|
||||||
|
@ -377,7 +378,8 @@ def test_save_netpbm_bmp_mode(tmp_path: Path) -> None:
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
|
|
||||||
tempfile = str(tmp_path / "temp.gif")
|
tempfile = str(tmp_path / "temp.gif")
|
||||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
b = BytesIO()
|
||||||
|
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||||
with Image.open(tempfile) as reloaded:
|
with Image.open(tempfile) as reloaded:
|
||||||
assert_image_similar(img, reloaded.convert("RGB"), 0)
|
assert_image_similar(img, reloaded.convert("RGB"), 0)
|
||||||
|
|
||||||
|
@ -388,7 +390,8 @@ def test_save_netpbm_l_mode(tmp_path: Path) -> None:
|
||||||
img = img.convert("L")
|
img = img.convert("L")
|
||||||
|
|
||||||
tempfile = str(tmp_path / "temp.gif")
|
tempfile = str(tmp_path / "temp.gif")
|
||||||
GifImagePlugin._save_netpbm(img, 0, tempfile)
|
b = BytesIO()
|
||||||
|
GifImagePlugin._save_netpbm(img, b, tempfile)
|
||||||
with Image.open(tempfile) as reloaded:
|
with Image.open(tempfile) as reloaded:
|
||||||
assert_image_similar(img, reloaded.convert("L"), 0)
|
assert_image_similar(img, reloaded.convert("L"), 0)
|
||||||
|
|
||||||
|
@ -648,7 +651,7 @@ def test_dispose2_palette(tmp_path: Path) -> None:
|
||||||
assert rgb_img.getpixel((50, 50)) == circle
|
assert rgb_img.getpixel((50, 50)) == circle
|
||||||
|
|
||||||
# Check that frame transparency wasn't added unnecessarily
|
# Check that frame transparency wasn't added unnecessarily
|
||||||
assert img._frame_transparency is None
|
assert getattr(img, "_frame_transparency") is None
|
||||||
|
|
||||||
|
|
||||||
def test_dispose2_diff(tmp_path: Path) -> None:
|
def test_dispose2_diff(tmp_path: Path) -> None:
|
||||||
|
|
|
@ -29,9 +29,10 @@ import itertools
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import IO
|
from typing import IO, TYPE_CHECKING, Any, List, Literal, NamedTuple, Union
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
Image,
|
Image,
|
||||||
|
@ -46,6 +47,9 @@ from ._binary import i16le as i16
|
||||||
from ._binary import o8
|
from ._binary import o8
|
||||||
from ._binary import o16le as o16
|
from ._binary import o16le as o16
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import _imaging
|
||||||
|
|
||||||
|
|
||||||
class LoadingStrategy(IntEnum):
|
class LoadingStrategy(IntEnum):
|
||||||
""".. versionadded:: 9.1.0"""
|
""".. versionadded:: 9.1.0"""
|
||||||
|
@ -118,7 +122,7 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
self._seek(0) # get ready to read first frame
|
self._seek(0) # get ready to read first frame
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def n_frames(self):
|
def n_frames(self) -> int:
|
||||||
if self._n_frames is None:
|
if self._n_frames is None:
|
||||||
current = self.tell()
|
current = self.tell()
|
||||||
try:
|
try:
|
||||||
|
@ -163,11 +167,11 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
msg = "no more images in GIF file"
|
msg = "no more images in GIF file"
|
||||||
raise EOFError(msg) from e
|
raise EOFError(msg) from e
|
||||||
|
|
||||||
def _seek(self, frame, update_image=True):
|
def _seek(self, frame: int, update_image: bool = True) -> None:
|
||||||
if frame == 0:
|
if frame == 0:
|
||||||
# rewind
|
# rewind
|
||||||
self.__offset = 0
|
self.__offset = 0
|
||||||
self.dispose = None
|
self.dispose: _imaging.ImagingCore | None = None
|
||||||
self.__frame = -1
|
self.__frame = -1
|
||||||
self._fp.seek(self.__rewind)
|
self._fp.seek(self.__rewind)
|
||||||
self.disposal_method = 0
|
self.disposal_method = 0
|
||||||
|
@ -195,9 +199,9 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
msg = "no more images in GIF file"
|
msg = "no more images in GIF file"
|
||||||
raise EOFError(msg)
|
raise EOFError(msg)
|
||||||
|
|
||||||
palette = None
|
palette: ImagePalette.ImagePalette | Literal[False] | None = None
|
||||||
|
|
||||||
info = {}
|
info: dict[str, Any] = {}
|
||||||
frame_transparency = None
|
frame_transparency = None
|
||||||
interlace = None
|
interlace = None
|
||||||
frame_dispose_extent = None
|
frame_dispose_extent = None
|
||||||
|
@ -213,7 +217,7 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
#
|
#
|
||||||
s = self.fp.read(1)
|
s = self.fp.read(1)
|
||||||
block = self.data()
|
block = self.data()
|
||||||
if s[0] == 249:
|
if s[0] == 249 and block is not None:
|
||||||
#
|
#
|
||||||
# graphic control extension
|
# graphic control extension
|
||||||
#
|
#
|
||||||
|
@ -249,14 +253,14 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
info["comment"] = comment
|
info["comment"] = comment
|
||||||
s = None
|
s = None
|
||||||
continue
|
continue
|
||||||
elif s[0] == 255 and frame == 0:
|
elif s[0] == 255 and frame == 0 and block is not None:
|
||||||
#
|
#
|
||||||
# application extension
|
# application extension
|
||||||
#
|
#
|
||||||
info["extension"] = block, self.fp.tell()
|
info["extension"] = block, self.fp.tell()
|
||||||
if block[:11] == b"NETSCAPE2.0":
|
if block[:11] == b"NETSCAPE2.0":
|
||||||
block = self.data()
|
block = self.data()
|
||||||
if len(block) >= 3 and block[0] == 1:
|
if block and len(block) >= 3 and block[0] == 1:
|
||||||
self.info["loop"] = i16(block, 1)
|
self.info["loop"] = i16(block, 1)
|
||||||
while self.data():
|
while self.data():
|
||||||
pass
|
pass
|
||||||
|
@ -345,51 +349,52 @@ class GifImageFile(ImageFile.ImageFile):
|
||||||
else:
|
else:
|
||||||
return (color, color, color)
|
return (color, color, color)
|
||||||
|
|
||||||
|
self.dispose = None
|
||||||
self.dispose_extent = frame_dispose_extent
|
self.dispose_extent = frame_dispose_extent
|
||||||
try:
|
if self.dispose_extent and self.disposal_method >= 2:
|
||||||
if self.disposal_method < 2:
|
try:
|
||||||
# do not dispose or none specified
|
if self.disposal_method == 2:
|
||||||
self.dispose = None
|
# replace with background colour
|
||||||
elif self.disposal_method == 2:
|
|
||||||
# replace with background colour
|
|
||||||
|
|
||||||
# only dispose the extent in this frame
|
|
||||||
x0, y0, x1, y1 = self.dispose_extent
|
|
||||||
dispose_size = (x1 - x0, y1 - y0)
|
|
||||||
|
|
||||||
Image._decompression_bomb_check(dispose_size)
|
|
||||||
|
|
||||||
# by convention, attempt to use transparency first
|
|
||||||
dispose_mode = "P"
|
|
||||||
color = self.info.get("transparency", frame_transparency)
|
|
||||||
if color is not None:
|
|
||||||
if self.mode in ("RGB", "RGBA"):
|
|
||||||
dispose_mode = "RGBA"
|
|
||||||
color = _rgb(color) + (0,)
|
|
||||||
else:
|
|
||||||
color = self.info.get("background", 0)
|
|
||||||
if self.mode in ("RGB", "RGBA"):
|
|
||||||
dispose_mode = "RGB"
|
|
||||||
color = _rgb(color)
|
|
||||||
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
|
||||||
else:
|
|
||||||
# replace with previous contents
|
|
||||||
if self.im is not None:
|
|
||||||
# only dispose the extent in this frame
|
# only dispose the extent in this frame
|
||||||
self.dispose = self._crop(self.im, self.dispose_extent)
|
|
||||||
elif frame_transparency is not None:
|
|
||||||
x0, y0, x1, y1 = self.dispose_extent
|
x0, y0, x1, y1 = self.dispose_extent
|
||||||
dispose_size = (x1 - x0, y1 - y0)
|
dispose_size = (x1 - x0, y1 - y0)
|
||||||
|
|
||||||
Image._decompression_bomb_check(dispose_size)
|
Image._decompression_bomb_check(dispose_size)
|
||||||
|
|
||||||
|
# by convention, attempt to use transparency first
|
||||||
dispose_mode = "P"
|
dispose_mode = "P"
|
||||||
color = frame_transparency
|
color = self.info.get("transparency", frame_transparency)
|
||||||
if self.mode in ("RGB", "RGBA"):
|
if color is not None:
|
||||||
dispose_mode = "RGBA"
|
if self.mode in ("RGB", "RGBA"):
|
||||||
color = _rgb(frame_transparency) + (0,)
|
dispose_mode = "RGBA"
|
||||||
|
color = _rgb(color) + (0,)
|
||||||
|
else:
|
||||||
|
color = self.info.get("background", 0)
|
||||||
|
if self.mode in ("RGB", "RGBA"):
|
||||||
|
dispose_mode = "RGB"
|
||||||
|
color = _rgb(color)
|
||||||
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
self.dispose = Image.core.fill(dispose_mode, dispose_size, color)
|
||||||
except AttributeError:
|
else:
|
||||||
pass
|
# replace with previous contents
|
||||||
|
if self.im is not None:
|
||||||
|
# only dispose the extent in this frame
|
||||||
|
self.dispose = self._crop(self.im, self.dispose_extent)
|
||||||
|
elif frame_transparency is not None:
|
||||||
|
x0, y0, x1, y1 = self.dispose_extent
|
||||||
|
dispose_size = (x1 - x0, y1 - y0)
|
||||||
|
|
||||||
|
Image._decompression_bomb_check(dispose_size)
|
||||||
|
dispose_mode = "P"
|
||||||
|
color = frame_transparency
|
||||||
|
if self.mode in ("RGB", "RGBA"):
|
||||||
|
dispose_mode = "RGBA"
|
||||||
|
color = _rgb(frame_transparency) + (0,)
|
||||||
|
self.dispose = Image.core.fill(
|
||||||
|
dispose_mode, dispose_size, color
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
if interlace is not None:
|
if interlace is not None:
|
||||||
transparency = -1
|
transparency = -1
|
||||||
|
@ -498,7 +503,12 @@ def _normalize_mode(im: Image.Image) -> Image.Image:
|
||||||
return im.convert("L")
|
return im.convert("L")
|
||||||
|
|
||||||
|
|
||||||
def _normalize_palette(im, palette, info):
|
_Palette = Union[bytes, bytearray, List[int], ImagePalette.ImagePalette]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_palette(
|
||||||
|
im: Image.Image, palette: _Palette | None, info: dict[str, Any]
|
||||||
|
) -> Image.Image:
|
||||||
"""
|
"""
|
||||||
Normalizes the palette for image.
|
Normalizes the palette for image.
|
||||||
- Sets the palette to the incoming palette, if provided.
|
- Sets the palette to the incoming palette, if provided.
|
||||||
|
@ -526,8 +536,10 @@ def _normalize_palette(im, palette, info):
|
||||||
source_palette = bytearray(i // 3 for i in range(768))
|
source_palette = bytearray(i // 3 for i in range(768))
|
||||||
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
|
im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
|
||||||
|
|
||||||
|
used_palette_colors: list[int] | None
|
||||||
if palette:
|
if palette:
|
||||||
used_palette_colors = []
|
used_palette_colors = []
|
||||||
|
assert source_palette is not None
|
||||||
for i in range(0, len(source_palette), 3):
|
for i in range(0, len(source_palette), 3):
|
||||||
source_color = tuple(source_palette[i : i + 3])
|
source_color = tuple(source_palette[i : i + 3])
|
||||||
index = im.palette.colors.get(source_color)
|
index = im.palette.colors.get(source_color)
|
||||||
|
@ -561,7 +573,7 @@ def _normalize_palette(im, palette, info):
|
||||||
def _write_single_frame(
|
def _write_single_frame(
|
||||||
im: Image.Image,
|
im: Image.Image,
|
||||||
fp: IO[bytes],
|
fp: IO[bytes],
|
||||||
palette: bytes | bytearray | list[int] | ImagePalette.ImagePalette,
|
palette: _Palette | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
im_out = _normalize_mode(im)
|
im_out = _normalize_mode(im)
|
||||||
for k, v in im_out.info.items():
|
for k, v in im_out.info.items():
|
||||||
|
@ -585,7 +597,7 @@ def _write_single_frame(
|
||||||
|
|
||||||
def _getbbox(
|
def _getbbox(
|
||||||
base_im: Image.Image, im_frame: Image.Image
|
base_im: Image.Image, im_frame: Image.Image
|
||||||
) -> tuple[Image.Image, tuple[int, int, int, int]]:
|
) -> tuple[Image.Image, tuple[int, int, int, int] | None]:
|
||||||
if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im):
|
if _get_palette_bytes(im_frame) != _get_palette_bytes(base_im):
|
||||||
im_frame = im_frame.convert("RGBA")
|
im_frame = im_frame.convert("RGBA")
|
||||||
base_im = base_im.convert("RGBA")
|
base_im = base_im.convert("RGBA")
|
||||||
|
@ -593,12 +605,20 @@ def _getbbox(
|
||||||
return delta, delta.getbbox(alpha_only=False)
|
return delta, delta.getbbox(alpha_only=False)
|
||||||
|
|
||||||
|
|
||||||
def _write_multiple_frames(im, fp, palette):
|
class _Frame(NamedTuple):
|
||||||
|
im: Image.Image
|
||||||
|
bbox: tuple[int, int, int, int] | None
|
||||||
|
encoderinfo: dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
def _write_multiple_frames(
|
||||||
|
im: Image.Image, fp: IO[bytes], palette: _Palette | None
|
||||||
|
) -> bool:
|
||||||
duration = im.encoderinfo.get("duration")
|
duration = im.encoderinfo.get("duration")
|
||||||
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
|
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
|
||||||
|
|
||||||
im_frames = []
|
im_frames: list[_Frame] = []
|
||||||
previous_im = None
|
previous_im: Image.Image | None = None
|
||||||
frame_count = 0
|
frame_count = 0
|
||||||
background_im = None
|
background_im = None
|
||||||
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
|
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
|
||||||
|
@ -624,24 +644,22 @@ def _write_multiple_frames(im, fp, palette):
|
||||||
frame_count += 1
|
frame_count += 1
|
||||||
|
|
||||||
diff_frame = None
|
diff_frame = None
|
||||||
if im_frames:
|
if im_frames and previous_im:
|
||||||
# delta frame
|
# delta frame
|
||||||
delta, bbox = _getbbox(previous_im, im_frame)
|
delta, bbox = _getbbox(previous_im, im_frame)
|
||||||
if not bbox:
|
if not bbox:
|
||||||
# This frame is identical to the previous frame
|
# This frame is identical to the previous frame
|
||||||
if encoderinfo.get("duration"):
|
if encoderinfo.get("duration"):
|
||||||
im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[
|
im_frames[-1].encoderinfo["duration"] += encoderinfo["duration"]
|
||||||
"duration"
|
|
||||||
]
|
|
||||||
continue
|
continue
|
||||||
if im_frames[-1]["encoderinfo"].get("disposal") == 2:
|
if im_frames[-1].encoderinfo.get("disposal") == 2:
|
||||||
if background_im is None:
|
if background_im is None:
|
||||||
color = im.encoderinfo.get(
|
color = im.encoderinfo.get(
|
||||||
"transparency", im.info.get("transparency", (0, 0, 0))
|
"transparency", im.info.get("transparency", (0, 0, 0))
|
||||||
)
|
)
|
||||||
background = _get_background(im_frame, color)
|
background = _get_background(im_frame, color)
|
||||||
background_im = Image.new("P", im_frame.size, background)
|
background_im = Image.new("P", im_frame.size, background)
|
||||||
background_im.putpalette(im_frames[0]["im"].palette)
|
background_im.putpalette(im_frames[0].im.palette)
|
||||||
bbox = _getbbox(background_im, im_frame)[1]
|
bbox = _getbbox(background_im, im_frame)[1]
|
||||||
elif encoderinfo.get("optimize") and im_frame.mode != "1":
|
elif encoderinfo.get("optimize") and im_frame.mode != "1":
|
||||||
if "transparency" not in encoderinfo:
|
if "transparency" not in encoderinfo:
|
||||||
|
@ -687,31 +705,29 @@ def _write_multiple_frames(im, fp, palette):
|
||||||
else:
|
else:
|
||||||
bbox = None
|
bbox = None
|
||||||
previous_im = im_frame
|
previous_im = im_frame
|
||||||
im_frames.append(
|
im_frames.append(_Frame(diff_frame or im_frame, bbox, encoderinfo))
|
||||||
{"im": diff_frame or im_frame, "bbox": bbox, "encoderinfo": encoderinfo}
|
|
||||||
)
|
|
||||||
|
|
||||||
if len(im_frames) == 1:
|
if len(im_frames) == 1:
|
||||||
if "duration" in im.encoderinfo:
|
if "duration" in im.encoderinfo:
|
||||||
# Since multiple frames will not be written, use the combined duration
|
# Since multiple frames will not be written, use the combined duration
|
||||||
im.encoderinfo["duration"] = im_frames[0]["encoderinfo"]["duration"]
|
im.encoderinfo["duration"] = im_frames[0].encoderinfo["duration"]
|
||||||
return
|
return False
|
||||||
|
|
||||||
for frame_data in im_frames:
|
for frame_data in im_frames:
|
||||||
im_frame = frame_data["im"]
|
im_frame = frame_data.im
|
||||||
if not frame_data["bbox"]:
|
if not frame_data.bbox:
|
||||||
# global header
|
# global header
|
||||||
for s in _get_global_header(im_frame, frame_data["encoderinfo"]):
|
for s in _get_global_header(im_frame, frame_data.encoderinfo):
|
||||||
fp.write(s)
|
fp.write(s)
|
||||||
offset = (0, 0)
|
offset = (0, 0)
|
||||||
else:
|
else:
|
||||||
# compress difference
|
# compress difference
|
||||||
if not palette:
|
if not palette:
|
||||||
frame_data["encoderinfo"]["include_color_table"] = True
|
frame_data.encoderinfo["include_color_table"] = True
|
||||||
|
|
||||||
im_frame = im_frame.crop(frame_data["bbox"])
|
im_frame = im_frame.crop(frame_data.bbox)
|
||||||
offset = frame_data["bbox"][:2]
|
offset = frame_data.bbox[:2]
|
||||||
_write_frame_data(fp, im_frame, offset, frame_data["encoderinfo"])
|
_write_frame_data(fp, im_frame, offset, frame_data.encoderinfo)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -748,7 +764,9 @@ def get_interlace(im: Image.Image) -> int:
|
||||||
return interlace
|
return interlace
|
||||||
|
|
||||||
|
|
||||||
def _write_local_header(fp, im, offset, flags):
|
def _write_local_header(
|
||||||
|
fp: IO[bytes], im: Image.Image, offset: tuple[int, int], flags: int
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
transparency = im.encoderinfo["transparency"]
|
transparency = im.encoderinfo["transparency"]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -849,7 +867,7 @@ def _save_netpbm(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
|
||||||
_FORCE_OPTIMIZE = False
|
_FORCE_OPTIMIZE = False
|
||||||
|
|
||||||
|
|
||||||
def _get_optimize(im, info):
|
def _get_optimize(im: Image.Image, info: dict[str, Any]) -> list[int] | None:
|
||||||
"""
|
"""
|
||||||
Palette optimization is a potentially expensive operation.
|
Palette optimization is a potentially expensive operation.
|
||||||
|
|
||||||
|
@ -893,6 +911,7 @@ def _get_optimize(im, info):
|
||||||
and current_palette_size > 2
|
and current_palette_size > 2
|
||||||
):
|
):
|
||||||
return used_palette_colors
|
return used_palette_colors
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _get_color_table_size(palette_bytes: bytes) -> int:
|
def _get_color_table_size(palette_bytes: bytes) -> int:
|
||||||
|
@ -933,7 +952,10 @@ def _get_palette_bytes(im: Image.Image) -> bytes:
|
||||||
return im.palette.palette if im.palette else b""
|
return im.palette.palette if im.palette else b""
|
||||||
|
|
||||||
|
|
||||||
def _get_background(im, info_background):
|
def _get_background(
|
||||||
|
im: Image.Image,
|
||||||
|
info_background: int | tuple[int, int, int] | tuple[int, int, int, int] | None,
|
||||||
|
) -> int:
|
||||||
background = 0
|
background = 0
|
||||||
if info_background:
|
if info_background:
|
||||||
if isinstance(info_background, tuple):
|
if isinstance(info_background, tuple):
|
||||||
|
@ -956,7 +978,7 @@ def _get_background(im, info_background):
|
||||||
return background
|
return background
|
||||||
|
|
||||||
|
|
||||||
def _get_global_header(im, info):
|
def _get_global_header(im: Image.Image, info: dict[str, Any]) -> list[bytes]:
|
||||||
"""Return a list of strings representing a GIF header"""
|
"""Return a list of strings representing a GIF header"""
|
||||||
|
|
||||||
# Header Block
|
# Header Block
|
||||||
|
@ -1018,7 +1040,12 @@ def _get_global_header(im, info):
|
||||||
return header
|
return header
|
||||||
|
|
||||||
|
|
||||||
def _write_frame_data(fp, im_frame, offset, params):
|
def _write_frame_data(
|
||||||
|
fp: IO[bytes],
|
||||||
|
im_frame: Image.Image,
|
||||||
|
offset: tuple[int, int],
|
||||||
|
params: dict[str, Any],
|
||||||
|
) -> None:
|
||||||
try:
|
try:
|
||||||
im_frame.encoderinfo = params
|
im_frame.encoderinfo = params
|
||||||
|
|
||||||
|
@ -1038,7 +1065,9 @@ def _write_frame_data(fp, im_frame, offset, params):
|
||||||
# Legacy GIF utilities
|
# Legacy GIF utilities
|
||||||
|
|
||||||
|
|
||||||
def getheader(im, palette=None, info=None):
|
def getheader(
|
||||||
|
im: Image.Image, palette: _Palette | None = None, info: dict[str, Any] | None = None
|
||||||
|
) -> tuple[list[bytes], list[int] | None]:
|
||||||
"""
|
"""
|
||||||
Legacy Method to get Gif data from image.
|
Legacy Method to get Gif data from image.
|
||||||
|
|
||||||
|
@ -1050,11 +1079,11 @@ def getheader(im, palette=None, info=None):
|
||||||
:returns: tuple of(list of header items, optimized palette)
|
:returns: tuple of(list of header items, optimized palette)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
used_palette_colors = _get_optimize(im, info)
|
|
||||||
|
|
||||||
if info is None:
|
if info is None:
|
||||||
info = {}
|
info = {}
|
||||||
|
|
||||||
|
used_palette_colors = _get_optimize(im, info)
|
||||||
|
|
||||||
if "background" not in info and "background" in im.info:
|
if "background" not in info and "background" in im.info:
|
||||||
info["background"] = im.info["background"]
|
info["background"] = im.info["background"]
|
||||||
|
|
||||||
|
@ -1066,7 +1095,9 @@ def getheader(im, palette=None, info=None):
|
||||||
return header, used_palette_colors
|
return header, used_palette_colors
|
||||||
|
|
||||||
|
|
||||||
def getdata(im, offset=(0, 0), **params):
|
def getdata(
|
||||||
|
im: Image.Image, offset: tuple[int, int] = (0, 0), **params: Any
|
||||||
|
) -> list[bytes]:
|
||||||
"""
|
"""
|
||||||
Legacy Method
|
Legacy Method
|
||||||
|
|
||||||
|
@ -1083,12 +1114,23 @@ def getdata(im, offset=(0, 0), **params):
|
||||||
:returns: List of bytes containing GIF encoded frame data
|
:returns: List of bytes containing GIF encoded frame data
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
class Collector:
|
class Collector(BytesIO):
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
def write(self, data: bytes) -> None:
|
if sys.version_info >= (3, 12):
|
||||||
self.data.append(data)
|
from collections.abc import Buffer
|
||||||
|
|
||||||
|
def write(self, data: Buffer) -> int:
|
||||||
|
self.data.append(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def write(self, data: Any) -> int:
|
||||||
|
self.data.append(data)
|
||||||
|
return len(data)
|
||||||
|
|
||||||
im.load() # make sure raster data is available
|
im.load() # make sure raster data is available
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ import warnings
|
||||||
from collections.abc import Callable, MutableMapping
|
from collections.abc import Callable, MutableMapping
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, cast
|
from typing import IO, TYPE_CHECKING, Any, Literal, Protocol, Sequence, Tuple, cast
|
||||||
|
|
||||||
# VERSION was removed in Pillow 6.0.0.
|
# VERSION was removed in Pillow 6.0.0.
|
||||||
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
# PILLOW_VERSION was removed in Pillow 9.0.0.
|
||||||
|
@ -1367,7 +1367,7 @@ class Image:
|
||||||
"""
|
"""
|
||||||
return ImageMode.getmode(self.mode).bands
|
return ImageMode.getmode(self.mode).bands
|
||||||
|
|
||||||
def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int]:
|
def getbbox(self, *, alpha_only: bool = True) -> tuple[int, int, int, int] | None:
|
||||||
"""
|
"""
|
||||||
Calculates the bounding box of the non-zero regions in the
|
Calculates the bounding box of the non-zero regions in the
|
||||||
image.
|
image.
|
||||||
|
@ -3029,12 +3029,18 @@ def new(
|
||||||
color = ImageColor.getcolor(color, mode)
|
color = ImageColor.getcolor(color, mode)
|
||||||
|
|
||||||
im = Image()
|
im = Image()
|
||||||
if mode == "P" and isinstance(color, (list, tuple)) and len(color) in [3, 4]:
|
if (
|
||||||
# RGB or RGBA value for a P image
|
mode == "P"
|
||||||
from . import ImagePalette
|
and isinstance(color, (list, tuple))
|
||||||
|
and all(isinstance(i, int) for i in color)
|
||||||
|
):
|
||||||
|
color_ints: tuple[int, ...] = cast(Tuple[int, ...], tuple(color))
|
||||||
|
if len(color_ints) == 3 or len(color_ints) == 4:
|
||||||
|
# RGB or RGBA value for a P image
|
||||||
|
from . import ImagePalette
|
||||||
|
|
||||||
im.palette = ImagePalette.ImagePalette()
|
im.palette = ImagePalette.ImagePalette()
|
||||||
color = im.palette.getcolor(color)
|
color = im.palette.getcolor(color_ints)
|
||||||
return im._new(core.fill(mode, size, color))
|
return im._new(core.fill(mode, size, color))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -497,7 +497,7 @@ def expand(
|
||||||
color = _color(fill, image.mode)
|
color = _color(fill, image.mode)
|
||||||
if image.palette:
|
if image.palette:
|
||||||
palette = ImagePalette.ImagePalette(palette=image.getpalette())
|
palette = ImagePalette.ImagePalette(palette=image.getpalette())
|
||||||
if isinstance(color, tuple):
|
if isinstance(color, tuple) and (len(color) == 3 or len(color) == 4):
|
||||||
color = palette.getcolor(color)
|
color = palette.getcolor(color)
|
||||||
else:
|
else:
|
||||||
palette = None
|
palette = None
|
||||||
|
|
|
@ -18,10 +18,13 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import array
|
import array
|
||||||
from typing import IO, Sequence
|
from typing import IO, TYPE_CHECKING, Sequence
|
||||||
|
|
||||||
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
from . import GimpGradientFile, GimpPaletteFile, ImageColor, PaletteFile
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from . import Image
|
||||||
|
|
||||||
|
|
||||||
class ImagePalette:
|
class ImagePalette:
|
||||||
"""
|
"""
|
||||||
|
@ -128,7 +131,11 @@ class ImagePalette:
|
||||||
raise ValueError(msg) from e
|
raise ValueError(msg) from e
|
||||||
return index
|
return index
|
||||||
|
|
||||||
def getcolor(self, color, image=None) -> int:
|
def getcolor(
|
||||||
|
self,
|
||||||
|
color: tuple[int, int, int] | tuple[int, int, int, int],
|
||||||
|
image: Image.Image | None = None,
|
||||||
|
) -> int:
|
||||||
"""Given an rgb tuple, allocate palette entry.
|
"""Given an rgb tuple, allocate palette entry.
|
||||||
|
|
||||||
.. warning:: This method is experimental.
|
.. warning:: This method is experimental.
|
||||||
|
@ -163,7 +170,7 @@ class ImagePalette:
|
||||||
self.dirty = 1
|
self.dirty = 1
|
||||||
return index
|
return index
|
||||||
else:
|
else:
|
||||||
msg = f"unknown color specifier: {repr(color)}"
|
msg = f"unknown color specifier: {repr(color)}" # type: ignore[unreachable]
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
def save(self, fp: str | IO[str]) -> None:
|
def save(self, fp: str | IO[str]) -> None:
|
||||||
|
|
Loading…
Reference in New Issue
Block a user