Skip to content

Commit

Permalink
Merge branch 'python-pillow:main' into p2pa_images_conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
RedShy committed May 27, 2022
2 parents 9a14be8 + 8b84e4c commit 84da709
Show file tree
Hide file tree
Showing 8 changed files with 103 additions and 30 deletions.
9 changes: 9 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
Binary file added Tests/images/comment_after_last_frame.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Tests/images/duplicate_number_of_loops.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 50 additions & 11 deletions Tests/test_file_gif.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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

Expand All @@ -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:
Expand All @@ -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

Expand Down Expand Up @@ -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")
Expand Down
14 changes: 14 additions & 0 deletions Tests/test_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,20 @@ def test_remap_palette(self):
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")
Expand Down
3 changes: 2 additions & 1 deletion docs/handbook/image-file-formats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
39 changes: 21 additions & 18 deletions src/PIL/GifImagePlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,6 @@ def _seek(self, frame, update_image=True):
if not s or s == b";":
raise EOFError

self.__frame = frame

self.tile = []

palette = None
Expand Down Expand Up @@ -244,15 +242,15 @@ def _seek(self, frame, update_image=True):
info["comment"] = comment
s = None
continue
elif s[0] == 255:
elif s[0] == 255 and frame == 0:
#
# application extension
#
info["extension"] = block, self.fp.tell()
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

Expand Down Expand Up @@ -291,6 +289,8 @@ def _seek(self, frame, update_image=True):
if interlace is None:
# self._fp = None
raise EOFError

self.__frame = frame
if not update_image:
return

Expand Down Expand Up @@ -399,7 +399,7 @@ def _rgb(color):

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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions src/PIL/Image.py
Original file line number Diff line number Diff line change
Expand Up @@ -1909,6 +1909,13 @@ def remap_palette(self, dest_map, source_palette=None):
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):
Expand Down

0 comments on commit 84da709

Please sign in to comment.