Merge branch 'python-pillow:main' into p2pa_images_conversion

This commit is contained in:
Davide Consalvo 2022-05-27 12:37:43 +02:00 committed by GitHub
commit 84da70988f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 103 additions and 30 deletions

View File

@ -5,6 +5,15 @@ Changelog (Pillow)
9.2.0 (unreleased)
------------------
- Improve transparency handling when saving GIF images #6176
[radarhere]
- Do not update GIF frame position until local image is found #6219
[radarhere]
- Netscape GIF extension belongs after the global color table #6211
[radarhere]
- Only write GIF comments at the beginning of the file #6300
[raygard, radarhere]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -354,16 +354,23 @@ def test_seek_rewind():
assert_image_equal(im, expected)
def test_n_frames():
for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]:
# Test is_animated before n_frames
with Image.open(path) as im:
assert im.is_animated == (n_frames != 1)
@pytest.mark.parametrize(
"path, n_frames",
(
(TEST_GIF, 1),
("Tests/images/comment_after_last_frame.gif", 2),
("Tests/images/iss634.gif", 42),
),
)
def test_n_frames(path, n_frames):
# Test is_animated before n_frames
with Image.open(path) as im:
assert im.is_animated == (n_frames != 1)
# Test is_animated after n_frames
with Image.open(path) as im:
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
# Test is_animated after n_frames
with Image.open(path) as im:
assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1)
def test_no_change():
@ -632,7 +639,8 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0)
def test_transparency_in_second_frame():
def test_transparency_in_second_frame(tmp_path):
out = str(tmp_path / "temp.gif")
with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0
@ -642,6 +650,14 @@ def test_transparency_in_second_frame():
assert_image_equal_tofile(im, "Tests/images/different_transparency_merged.png")
im.save(out, save_all=True)
with Image.open(out) as reread:
reread.seek(reread.tell() + 1)
assert_image_equal_tofile(
reread, "Tests/images/different_transparency_merged.png"
)
def test_no_transparency_in_second_frame():
with Image.open("Tests/images/iss634.gif") as img:
@ -653,6 +669,22 @@ def test_no_transparency_in_second_frame():
assert img.histogram()[255] == 0
def test_remapped_transparency(tmp_path):
out = str(tmp_path / "temp.gif")
im = Image.new("P", (1, 2))
im2 = im.copy()
# Add transparency at a higher index
# so that it will be optimized to a lower index
im.putpixel((0, 1), 5)
im.info["transparency"] = 5
im.save(out, save_all=True, append_images=[im2])
with Image.open(out) as reloaded:
assert reloaded.info["transparency"] == reloaded.getpixel((0, 1))
def test_duration(tmp_path):
duration = 1000
@ -772,9 +804,16 @@ def test_number_of_loops(tmp_path):
im = Image.new("L", (100, 100), "#000")
im.save(out, loop=number_of_loops)
with Image.open(out) as reread:
assert reread.info["loop"] == number_of_loops
# Check that even if a subsequent GIF frame has the number of loops specified,
# only the value from the first frame is used
with Image.open("Tests/images/duplicate_number_of_loops.gif") as im:
assert im.info["loop"] == 2
im.seek(1)
assert im.info["loop"] == 2
def test_background(tmp_path):
out = str(tmp_path / "temp.gif")

View File

@ -609,6 +609,20 @@ class TestImage:
with pytest.raises(ValueError):
im.remap_palette(None)
def test_remap_palette_transparency(self):
im = Image.new("P", (1, 2))
im.putpixel((0, 1), 1)
im.info["transparency"] = 0
im_remapped = im.remap_palette([1, 0])
assert im_remapped.info["transparency"] == 1
# Test unused transparency
im.info["transparency"] = 2
im_remapped = im.remap_palette([1, 0])
assert "transparency" not in im_remapped.info
def test__new(self):
im = hopper("RGB")
im_p = hopper("P")

View File

@ -156,7 +156,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
it will loop forever.
**comment**
May not be present. A comment about the image.
May not be present. A comment about the image. This is the last comment found
before the current frame's image.
**extension**
May not be present. Contains application specific information.

View File

@ -185,8 +185,6 @@ class GifImageFile(ImageFile.ImageFile):
if not s or s == b";":
raise EOFError
self.__frame = frame
self.tile = []
palette = None
@ -244,7 +242,7 @@ class GifImageFile(ImageFile.ImageFile):
info["comment"] = comment
s = None
continue
elif s[0] == 255:
elif s[0] == 255 and frame == 0:
#
# application extension
#
@ -252,7 +250,7 @@ class GifImageFile(ImageFile.ImageFile):
if block[:11] == b"NETSCAPE2.0":
block = self.data()
if len(block) >= 3 and block[0] == 1:
info["loop"] = i16(block, 1)
self.info["loop"] = i16(block, 1)
while self.data():
pass
@ -291,6 +289,8 @@ class GifImageFile(ImageFile.ImageFile):
if interlace is None:
# self._fp = None
raise EOFError
self.__frame = frame
if not update_image:
return
@ -399,7 +399,7 @@ class GifImageFile(ImageFile.ImageFile):
if info.get("comment"):
self.info["comment"] = info["comment"]
for k in ["duration", "extension", "loop"]:
for k in ["duration", "extension"]:
if k in info:
self.info[k] = info[k]
elif k in self.info:
@ -574,10 +574,14 @@ def _write_multiple_frames(im, fp, palette):
im_frame = _normalize_mode(im_frame.copy())
if frame_count == 0:
for k, v in im_frame.info.items():
if k == "transparency":
continue
im.encoderinfo.setdefault(k, v)
im_frame = _normalize_palette(im_frame, palette, im.encoderinfo)
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"])
if isinstance(duration, (list, tuple)):
encoderinfo["duration"] = duration[frame_count]
elif duration is None and "duration" in im_frame.info:
@ -716,18 +720,6 @@ def _write_local_header(fp, im, offset, flags):
+ o8(0)
)
if "loop" in im.encoderinfo:
number_of_loops = im.encoderinfo["loop"]
fp.write(
b"!"
+ o8(255) # extension intro
+ o8(11)
+ b"NETSCAPE2.0"
+ o8(3)
+ o8(1)
+ o16(number_of_loops) # number of loops
+ o8(0)
)
include_color_table = im.encoderinfo.get("include_color_table")
if include_color_table:
palette_bytes = _get_palette_bytes(im)
@ -933,6 +925,17 @@ def _get_global_header(im, info):
# Global Color Table
_get_header_palette(palette_bytes),
]
if "loop" in info:
header.append(
b"!"
+ o8(255) # extension intro
+ o8(11)
+ b"NETSCAPE2.0"
+ o8(3)
+ o8(1)
+ o16(info["loop"]) # number of loops
+ o8(0)
)
if info.get("comment"):
comment_block = b"!" + o8(254) # extension intro

View File

@ -1909,6 +1909,13 @@ class Image:
m_im.putpalette(new_palette_bytes)
m_im.palette = ImagePalette.ImagePalette("RGB", palette=palette_bytes)
if "transparency" in self.info:
try:
m_im.info["transparency"] = dest_map.index(self.info["transparency"])
except ValueError:
if "transparency" in m_im.info:
del m_im.info["transparency"]
return m_im
def _get_safe_box(self, size, resample, box):