mirror of
https://github.com/python-pillow/Pillow.git
synced 2025-01-14 11:26:27 +03:00
Fill identical pixels with transparency in subsequent frames
This commit is contained in:
parent
7070feccb7
commit
55c5587437
|
@ -217,6 +217,27 @@ def test_optimize_if_palette_can_be_reduced_by_half():
|
||||||
assert len(reloaded.palette.palette) // 3 == colors
|
assert len(reloaded.palette.palette) // 3 == colors
|
||||||
|
|
||||||
|
|
||||||
|
def test_full_palette_second_frame(tmp_path):
|
||||||
|
out = str(tmp_path / "temp.gif")
|
||||||
|
im = Image.new("P", (1, 256))
|
||||||
|
|
||||||
|
full_palette_im = Image.new("P", (1, 256))
|
||||||
|
for i in range(256):
|
||||||
|
full_palette_im.putpixel((0, i), i)
|
||||||
|
full_palette_im.palette = ImagePalette.ImagePalette(
|
||||||
|
"RGB", bytearray(i // 3 for i in range(768))
|
||||||
|
)
|
||||||
|
full_palette_im.palette.dirty = 1
|
||||||
|
|
||||||
|
im.save(out, save_all=True, append_images=[full_palette_im])
|
||||||
|
|
||||||
|
with Image.open(out) as reloaded:
|
||||||
|
reloaded.seek(1)
|
||||||
|
|
||||||
|
for i in range(256):
|
||||||
|
reloaded.getpixel((0, i)) == i
|
||||||
|
|
||||||
|
|
||||||
def test_roundtrip(tmp_path):
|
def test_roundtrip(tmp_path):
|
||||||
out = str(tmp_path / "temp.gif")
|
out = str(tmp_path / "temp.gif")
|
||||||
im = hopper()
|
im = hopper()
|
||||||
|
|
|
@ -266,13 +266,14 @@ following options are available::
|
||||||
:py:class:`PIL.ImagePalette.ImagePalette` object.
|
:py:class:`PIL.ImagePalette.ImagePalette` object.
|
||||||
|
|
||||||
**optimize**
|
**optimize**
|
||||||
Whether to attempt to compress the palette by eliminating unused colors.
|
Whether to attempt to compress the palette by eliminating unused colors
|
||||||
|
(this is only useful if the palette can be compressed to the next smaller
|
||||||
|
power of 2 elements) and whether to mark all pixels that are not new in the
|
||||||
|
next frame as transparent.
|
||||||
|
|
||||||
This is attempted by default, unless a palette is specified as an option or
|
This is attempted by default, unless a palette is specified as an option or
|
||||||
as part of the first image's :py:attr:`~PIL.Image.Image.info` dictionary.
|
as part of the first image's :py:attr:`~PIL.Image.Image.info` dictionary.
|
||||||
|
|
||||||
This is only useful if the palette can be compressed to the next smaller
|
|
||||||
power of 2 elements.
|
|
||||||
|
|
||||||
Note that if the image you are saving comes from an existing GIF, it may have
|
Note that if the image you are saving comes from an existing GIF, it may have
|
||||||
the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary.
|
the following properties in its :py:attr:`~PIL.Image.Image.info` dictionary.
|
||||||
For these options, if you do not pass them in, they will default to
|
For these options, if you do not pass them in, they will default to
|
||||||
|
|
|
@ -30,7 +30,15 @@ import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
from . import Image, ImageChops, ImageFile, ImagePalette, ImageSequence
|
from . import (
|
||||||
|
Image,
|
||||||
|
ImageChops,
|
||||||
|
ImageFile,
|
||||||
|
ImageMath,
|
||||||
|
ImageOps,
|
||||||
|
ImagePalette,
|
||||||
|
ImageSequence,
|
||||||
|
)
|
||||||
from ._binary import i16le as i16
|
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
|
||||||
|
@ -534,7 +542,15 @@ def _normalize_palette(im, palette, info):
|
||||||
else:
|
else:
|
||||||
used_palette_colors = _get_optimize(im, info)
|
used_palette_colors = _get_optimize(im, info)
|
||||||
if used_palette_colors is not None:
|
if used_palette_colors is not None:
|
||||||
return im.remap_palette(used_palette_colors, source_palette)
|
im = im.remap_palette(used_palette_colors, source_palette)
|
||||||
|
if "transparency" in info:
|
||||||
|
try:
|
||||||
|
info["transparency"] = used_palette_colors.index(
|
||||||
|
info["transparency"]
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
del info["transparency"]
|
||||||
|
return im
|
||||||
|
|
||||||
im.palette.palette = source_palette
|
im.palette.palette = source_palette
|
||||||
return im
|
return im
|
||||||
|
@ -562,13 +578,11 @@ def _write_single_frame(im, fp, palette):
|
||||||
|
|
||||||
|
|
||||||
def _getbbox(base_im, im_frame):
|
def _getbbox(base_im, im_frame):
|
||||||
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")
|
||||||
|
base_im = base_im.convert("RGBA")
|
||||||
delta = ImageChops.subtract_modulo(im_frame, base_im)
|
delta = ImageChops.subtract_modulo(im_frame, base_im)
|
||||||
else:
|
return delta, delta.getbbox(alpha_only=False)
|
||||||
delta = ImageChops.subtract_modulo(
|
|
||||||
im_frame.convert("RGBA"), base_im.convert("RGBA")
|
|
||||||
)
|
|
||||||
return delta.getbbox(alpha_only=False)
|
|
||||||
|
|
||||||
|
|
||||||
def _write_multiple_frames(im, fp, palette):
|
def _write_multiple_frames(im, fp, palette):
|
||||||
|
@ -576,6 +590,7 @@ def _write_multiple_frames(im, fp, palette):
|
||||||
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
|
disposal = im.encoderinfo.get("disposal", im.info.get("disposal"))
|
||||||
|
|
||||||
im_frames = []
|
im_frames = []
|
||||||
|
previous_im = 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", [])):
|
||||||
|
@ -589,9 +604,9 @@ def _write_multiple_frames(im, fp, palette):
|
||||||
im.encoderinfo.setdefault(k, v)
|
im.encoderinfo.setdefault(k, v)
|
||||||
|
|
||||||
encoderinfo = im.encoderinfo.copy()
|
encoderinfo = im.encoderinfo.copy()
|
||||||
im_frame = _normalize_palette(im_frame, palette, encoderinfo)
|
|
||||||
if "transparency" in im_frame.info:
|
if "transparency" in im_frame.info:
|
||||||
encoderinfo.setdefault("transparency", im_frame.info["transparency"])
|
encoderinfo.setdefault("transparency", im_frame.info["transparency"])
|
||||||
|
im_frame = _normalize_palette(im_frame, palette, encoderinfo)
|
||||||
if isinstance(duration, (list, tuple)):
|
if isinstance(duration, (list, tuple)):
|
||||||
encoderinfo["duration"] = duration[frame_count]
|
encoderinfo["duration"] = duration[frame_count]
|
||||||
elif duration is None and "duration" in im_frame.info:
|
elif duration is None and "duration" in im_frame.info:
|
||||||
|
@ -600,14 +615,16 @@ def _write_multiple_frames(im, fp, palette):
|
||||||
encoderinfo["disposal"] = disposal[frame_count]
|
encoderinfo["disposal"] = disposal[frame_count]
|
||||||
frame_count += 1
|
frame_count += 1
|
||||||
|
|
||||||
|
diff_frame = None
|
||||||
if im_frames:
|
if im_frames:
|
||||||
# delta frame
|
# delta frame
|
||||||
previous = im_frames[-1]
|
delta, bbox = _getbbox(previous_im, im_frame)
|
||||||
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"):
|
||||||
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
|
im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[
|
||||||
|
"duration"
|
||||||
|
]
|
||||||
continue
|
continue
|
||||||
if encoderinfo.get("disposal") == 2:
|
if encoderinfo.get("disposal") == 2:
|
||||||
if background_im is None:
|
if background_im is None:
|
||||||
|
@ -617,10 +634,44 @@ def _write_multiple_frames(im, fp, palette):
|
||||||
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)
|
delta, bbox = _getbbox(background_im, im_frame)
|
||||||
|
if encoderinfo.get("optimize") and im_frame.mode != "1":
|
||||||
|
if "transparency" not in encoderinfo:
|
||||||
|
try:
|
||||||
|
encoderinfo[
|
||||||
|
"transparency"
|
||||||
|
] = im_frame.palette._new_color_index(im_frame)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if "transparency" in encoderinfo:
|
||||||
|
# When the delta is zero, fill the image with transparency
|
||||||
|
diff_frame = im_frame.copy()
|
||||||
|
fill = Image.new(
|
||||||
|
"P", diff_frame.size, encoderinfo["transparency"]
|
||||||
|
)
|
||||||
|
if delta.mode == "RGBA":
|
||||||
|
r, g, b, a = delta.split()
|
||||||
|
mask = ImageMath.eval(
|
||||||
|
"convert(max(max(max(r, g), b), a) * 255, '1')",
|
||||||
|
r=r,
|
||||||
|
g=g,
|
||||||
|
b=b,
|
||||||
|
a=a,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if delta.mode == "P":
|
||||||
|
# Convert to L without considering palette
|
||||||
|
delta_l = Image.new("L", delta.size)
|
||||||
|
delta_l.putdata(delta.getdata())
|
||||||
|
delta = delta_l
|
||||||
|
mask = ImageMath.eval("convert(im * 255, '1')", im=delta)
|
||||||
|
diff_frame.paste(fill, mask=ImageOps.invert(mask))
|
||||||
else:
|
else:
|
||||||
bbox = None
|
bbox = None
|
||||||
im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})
|
previous_im = im_frame
|
||||||
|
im_frames.append(
|
||||||
|
{"im": diff_frame or im_frame, "bbox": bbox, "encoderinfo": encoderinfo}
|
||||||
|
)
|
||||||
|
|
||||||
if len(im_frames) > 1:
|
if len(im_frames) > 1:
|
||||||
for frame_data in im_frames:
|
for frame_data in im_frames:
|
||||||
|
@ -678,22 +729,10 @@ def get_interlace(im):
|
||||||
|
|
||||||
|
|
||||||
def _write_local_header(fp, im, offset, flags):
|
def _write_local_header(fp, im, offset, flags):
|
||||||
transparent_color_exists = False
|
|
||||||
try:
|
try:
|
||||||
transparency = int(im.encoderinfo["transparency"])
|
transparency = im.encoderinfo["transparency"]
|
||||||
except (KeyError, ValueError):
|
except KeyError:
|
||||||
pass
|
transparency = None
|
||||||
else:
|
|
||||||
# optimize the block away if transparent color is not used
|
|
||||||
transparent_color_exists = True
|
|
||||||
|
|
||||||
used_palette_colors = _get_optimize(im, im.encoderinfo)
|
|
||||||
if used_palette_colors is not None:
|
|
||||||
# adjust the transparency index after optimize
|
|
||||||
try:
|
|
||||||
transparency = used_palette_colors.index(transparency)
|
|
||||||
except ValueError:
|
|
||||||
transparent_color_exists = False
|
|
||||||
|
|
||||||
if "duration" in im.encoderinfo:
|
if "duration" in im.encoderinfo:
|
||||||
duration = int(im.encoderinfo["duration"] / 10)
|
duration = int(im.encoderinfo["duration"] / 10)
|
||||||
|
@ -702,11 +741,9 @@ def _write_local_header(fp, im, offset, flags):
|
||||||
|
|
||||||
disposal = int(im.encoderinfo.get("disposal", 0))
|
disposal = int(im.encoderinfo.get("disposal", 0))
|
||||||
|
|
||||||
if transparent_color_exists or duration != 0 or disposal:
|
if transparency is not None or duration != 0 or disposal:
|
||||||
packed_flag = 1 if transparent_color_exists else 0
|
packed_flag = 1 if transparency is not None else 0
|
||||||
packed_flag |= disposal << 2
|
packed_flag |= disposal << 2
|
||||||
if not transparent_color_exists:
|
|
||||||
transparency = 0
|
|
||||||
|
|
||||||
fp.write(
|
fp.write(
|
||||||
b"!"
|
b"!"
|
||||||
|
@ -714,7 +751,7 @@ def _write_local_header(fp, im, offset, flags):
|
||||||
+ o8(4) # length
|
+ o8(4) # length
|
||||||
+ o8(packed_flag) # packed fields
|
+ o8(packed_flag) # packed fields
|
||||||
+ o16(duration) # duration
|
+ o16(duration) # duration
|
||||||
+ o8(transparency) # transparency index
|
+ o8(transparency or 0) # transparency index
|
||||||
+ o8(0)
|
+ o8(0)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -802,7 +839,7 @@ def _get_optimize(im, info):
|
||||||
:param info: encoderinfo
|
:param info: encoderinfo
|
||||||
:returns: list of indexes of palette entries in use, or None
|
:returns: list of indexes of palette entries in use, or None
|
||||||
"""
|
"""
|
||||||
if im.mode in ("P", "L") and info and info.get("optimize", 0):
|
if im.mode in ("P", "L") and info and info.get("optimize"):
|
||||||
# Potentially expensive operation.
|
# Potentially expensive operation.
|
||||||
|
|
||||||
# The palette saves 3 bytes per color not used, but palette
|
# The palette saves 3 bytes per color not used, but palette
|
||||||
|
|
|
@ -102,6 +102,30 @@ class ImagePalette:
|
||||||
# Declare tostring as an alias for tobytes
|
# Declare tostring as an alias for tobytes
|
||||||
tostring = tobytes
|
tostring = tobytes
|
||||||
|
|
||||||
|
def _new_color_index(self, image=None, e=None):
|
||||||
|
if not isinstance(self.palette, bytearray):
|
||||||
|
self._palette = bytearray(self.palette)
|
||||||
|
index = len(self.palette) // 3
|
||||||
|
special_colors = ()
|
||||||
|
if image:
|
||||||
|
special_colors = (
|
||||||
|
image.info.get("background"),
|
||||||
|
image.info.get("transparency"),
|
||||||
|
)
|
||||||
|
while index in special_colors:
|
||||||
|
index += 1
|
||||||
|
if index >= 256:
|
||||||
|
if image:
|
||||||
|
# Search for an unused index
|
||||||
|
for i, count in reversed(list(enumerate(image.histogram()))):
|
||||||
|
if count == 0 and i not in special_colors:
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
if index >= 256:
|
||||||
|
msg = "cannot allocate more than 256 colors"
|
||||||
|
raise ValueError(msg) from e
|
||||||
|
return index
|
||||||
|
|
||||||
def getcolor(self, color, image=None):
|
def getcolor(self, color, image=None):
|
||||||
"""Given an rgb tuple, allocate palette entry.
|
"""Given an rgb tuple, allocate palette entry.
|
||||||
|
|
||||||
|
@ -124,27 +148,7 @@ class ImagePalette:
|
||||||
return self.colors[color]
|
return self.colors[color]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
# allocate new color slot
|
# allocate new color slot
|
||||||
if not isinstance(self.palette, bytearray):
|
index = self._new_color_index(image, e)
|
||||||
self._palette = bytearray(self.palette)
|
|
||||||
index = len(self.palette) // 3
|
|
||||||
special_colors = ()
|
|
||||||
if image:
|
|
||||||
special_colors = (
|
|
||||||
image.info.get("background"),
|
|
||||||
image.info.get("transparency"),
|
|
||||||
)
|
|
||||||
while index in special_colors:
|
|
||||||
index += 1
|
|
||||||
if index >= 256:
|
|
||||||
if image:
|
|
||||||
# Search for an unused index
|
|
||||||
for i, count in reversed(list(enumerate(image.histogram()))):
|
|
||||||
if count == 0 and i not in special_colors:
|
|
||||||
index = i
|
|
||||||
break
|
|
||||||
if index >= 256:
|
|
||||||
msg = "cannot allocate more than 256 colors"
|
|
||||||
raise ValueError(msg) from e
|
|
||||||
self.colors[color] = index
|
self.colors[color] = index
|
||||||
if index * 3 < len(self.palette):
|
if index * 3 < len(self.palette):
|
||||||
self._palette = (
|
self._palette = (
|
||||||
|
|
Loading…
Reference in New Issue
Block a user