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) 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 - Only write GIF comments at the beginning of the file #6300
[raygard, radarhere] [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) assert_image_equal(im, expected)
def test_n_frames(): @pytest.mark.parametrize(
for path, n_frames in [[TEST_GIF, 1], ["Tests/images/iss634.gif", 42]]: "path, n_frames",
# Test is_animated before n_frames (
with Image.open(path) as im: (TEST_GIF, 1),
assert im.is_animated == (n_frames != 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 # Test is_animated after n_frames
with Image.open(path) as im: with Image.open(path) as im:
assert im.n_frames == n_frames assert im.n_frames == n_frames
assert im.is_animated == (n_frames != 1) assert im.is_animated == (n_frames != 1)
def test_no_change(): def test_no_change():
@ -632,7 +639,8 @@ def test_dispose2_background(tmp_path):
assert im.getpixel((0, 0)) == (255, 0, 0) 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: with Image.open("Tests/images/different_transparency.gif") as im:
assert im.info["transparency"] == 0 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") 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(): def test_no_transparency_in_second_frame():
with Image.open("Tests/images/iss634.gif") as img: 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 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): def test_duration(tmp_path):
duration = 1000 duration = 1000
@ -772,9 +804,16 @@ def test_number_of_loops(tmp_path):
im = Image.new("L", (100, 100), "#000") im = Image.new("L", (100, 100), "#000")
im.save(out, loop=number_of_loops) im.save(out, loop=number_of_loops)
with Image.open(out) as reread: with Image.open(out) as reread:
assert reread.info["loop"] == number_of_loops 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): def test_background(tmp_path):
out = str(tmp_path / "temp.gif") out = str(tmp_path / "temp.gif")

View File

@ -609,6 +609,20 @@ class TestImage:
with pytest.raises(ValueError): with pytest.raises(ValueError):
im.remap_palette(None) 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): def test__new(self):
im = hopper("RGB") im = hopper("RGB")
im_p = hopper("P") im_p = hopper("P")

View File

@ -156,7 +156,8 @@ The :py:meth:`~PIL.Image.open` method sets the following
it will loop forever. it will loop forever.
**comment** **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** **extension**
May not be present. Contains application specific information. May not be present. Contains application specific information.

View File

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

View File

@ -1909,6 +1909,13 @@ class Image:
m_im.putpalette(new_palette_bytes) m_im.putpalette(new_palette_bytes)
m_im.palette = ImagePalette.ImagePalette("RGB", palette=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 return m_im
def _get_safe_box(self, size, resample, box): def _get_safe_box(self, size, resample, box):