mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-10 19:56:47 +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
|
||||
|
||||
|
||||
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):
|
||||
out = str(tmp_path / "temp.gif")
|
||||
im = hopper()
|
||||
|
|
|
@ -266,13 +266,14 @@ following options are available::
|
|||
:py:class:`PIL.ImagePalette.ImagePalette` object.
|
||||
|
||||
**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
|
||||
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
|
||||
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
|
||||
|
|
|
@ -30,7 +30,15 @@ import os
|
|||
import subprocess
|
||||
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 o8
|
||||
from ._binary import o16le as o16
|
||||
|
@ -534,7 +542,15 @@ def _normalize_palette(im, palette, info):
|
|||
else:
|
||||
used_palette_colors = _get_optimize(im, info)
|
||||
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
|
||||
return im
|
||||
|
@ -562,13 +578,11 @@ def _write_single_frame(im, fp, palette):
|
|||
|
||||
|
||||
def _getbbox(base_im, im_frame):
|
||||
if _get_palette_bytes(im_frame) == _get_palette_bytes(base_im):
|
||||
delta = ImageChops.subtract_modulo(im_frame, base_im)
|
||||
else:
|
||||
delta = ImageChops.subtract_modulo(
|
||||
im_frame.convert("RGBA"), base_im.convert("RGBA")
|
||||
)
|
||||
return delta.getbbox(alpha_only=False)
|
||||
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)
|
||||
return delta, delta.getbbox(alpha_only=False)
|
||||
|
||||
|
||||
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"))
|
||||
|
||||
im_frames = []
|
||||
previous_im = None
|
||||
frame_count = 0
|
||||
background_im = None
|
||||
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)
|
||||
|
||||
encoderinfo = im.encoderinfo.copy()
|
||||
im_frame = _normalize_palette(im_frame, palette, encoderinfo)
|
||||
if "transparency" in im_frame.info:
|
||||
encoderinfo.setdefault("transparency", im_frame.info["transparency"])
|
||||
im_frame = _normalize_palette(im_frame, palette, encoderinfo)
|
||||
if isinstance(duration, (list, tuple)):
|
||||
encoderinfo["duration"] = duration[frame_count]
|
||||
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]
|
||||
frame_count += 1
|
||||
|
||||
diff_frame = None
|
||||
if im_frames:
|
||||
# delta frame
|
||||
previous = im_frames[-1]
|
||||
bbox = _getbbox(previous["im"], im_frame)
|
||||
delta, bbox = _getbbox(previous_im, im_frame)
|
||||
if not bbox:
|
||||
# This frame is identical to the previous frame
|
||||
if encoderinfo.get("duration"):
|
||||
previous["encoderinfo"]["duration"] += encoderinfo["duration"]
|
||||
im_frames[-1]["encoderinfo"]["duration"] += encoderinfo[
|
||||
"duration"
|
||||
]
|
||||
continue
|
||||
if encoderinfo.get("disposal") == 2:
|
||||
if background_im is None:
|
||||
|
@ -617,10 +634,44 @@ def _write_multiple_frames(im, fp, palette):
|
|||
background = _get_background(im_frame, color)
|
||||
background_im = Image.new("P", im_frame.size, background)
|
||||
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:
|
||||
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:
|
||||
for frame_data in im_frames:
|
||||
|
@ -678,22 +729,10 @@ def get_interlace(im):
|
|||
|
||||
|
||||
def _write_local_header(fp, im, offset, flags):
|
||||
transparent_color_exists = False
|
||||
try:
|
||||
transparency = int(im.encoderinfo["transparency"])
|
||||
except (KeyError, ValueError):
|
||||
pass
|
||||
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
|
||||
transparency = im.encoderinfo["transparency"]
|
||||
except KeyError:
|
||||
transparency = None
|
||||
|
||||
if "duration" in im.encoderinfo:
|
||||
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))
|
||||
|
||||
if transparent_color_exists or duration != 0 or disposal:
|
||||
packed_flag = 1 if transparent_color_exists else 0
|
||||
if transparency is not None or duration != 0 or disposal:
|
||||
packed_flag = 1 if transparency is not None else 0
|
||||
packed_flag |= disposal << 2
|
||||
if not transparent_color_exists:
|
||||
transparency = 0
|
||||
|
||||
fp.write(
|
||||
b"!"
|
||||
|
@ -714,7 +751,7 @@ def _write_local_header(fp, im, offset, flags):
|
|||
+ o8(4) # length
|
||||
+ o8(packed_flag) # packed fields
|
||||
+ o16(duration) # duration
|
||||
+ o8(transparency) # transparency index
|
||||
+ o8(transparency or 0) # transparency index
|
||||
+ o8(0)
|
||||
)
|
||||
|
||||
|
@ -802,7 +839,7 @@ def _get_optimize(im, info):
|
|||
:param info: encoderinfo
|
||||
: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.
|
||||
|
||||
# The palette saves 3 bytes per color not used, but palette
|
||||
|
|
|
@ -102,6 +102,30 @@ class ImagePalette:
|
|||
# Declare tostring as an alias for 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):
|
||||
"""Given an rgb tuple, allocate palette entry.
|
||||
|
||||
|
@ -124,27 +148,7 @@ class ImagePalette:
|
|||
return self.colors[color]
|
||||
except KeyError as e:
|
||||
# allocate new color slot
|
||||
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
|
||||
index = self._new_color_index(image, e)
|
||||
self.colors[color] = index
|
||||
if index * 3 < len(self.palette):
|
||||
self._palette = (
|
||||
|
|
Loading…
Reference in New Issue
Block a user