mirror of
https://github.com/python-pillow/Pillow.git
synced 2024-11-11 04:07:21 +03:00
Merge branch 'python-pillow:main' into p2pa_images_conversion
This commit is contained in:
commit
84da70988f
|
@ -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]
|
||||
|
||||
|
|
BIN
Tests/images/comment_after_last_frame.gif
Normal file
BIN
Tests/images/comment_after_last_frame.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
Tests/images/duplicate_number_of_loops.gif
Normal file
BIN
Tests/images/duplicate_number_of_loops.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue
Block a user