Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create GIF deltas from background colour of GIF frames if disposal mode is 2 #3708

Merged
merged 23 commits into from Jun 29, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
ae19409
Allow correct delta generation for GIFs with disposal 2 (Fixes #3665)
sircinnamon Mar 8, 2019
1f6d1be
Ensure disposal key exists before checking
sircinnamon Mar 8, 2019
4a2be2a
Create gif frame delta by subtracting 0x0 image if disposal is mode 2
sircinnamon Mar 8, 2019
c73da62
Ensure disposal key exists before checking
sircinnamon Mar 8, 2019
3b1a1fb
Create background image for calculating gif deltas
sircinnamon Mar 11, 2019
215cdfd
Merge branch 'master' of https://github.com/sircinnamon/Pillow
sircinnamon Mar 11, 2019
3b74281
Fix line lengths and init background out of loop
sircinnamon Mar 11, 2019
583d731
Fix line indents for linting
sircinnamon Mar 11, 2019
c57bfb9
Merge branch 'master' of https://github.com/python-pillow/Pillow
sircinnamon Mar 11, 2019
96c5a4c
Add test for disposal mode 2 gifs
sircinnamon Mar 14, 2019
8a36a15
Force include colour table for disposal=2 gifs and pad colour table t…
sircinnamon Mar 14, 2019
53cfd19
Check encoder info for disposal mode
sircinnamon Mar 14, 2019
85a07bb
Linting changes
sircinnamon Mar 14, 2019
0b630e0
Test that background colours read are equal to saved colours
radarhere Mar 14, 2019
aa5874d
Merge pull request #1 from radarhere/gif_test
sircinnamon Mar 14, 2019
4b2746f
Remove disposal 2 duplicate frame exemption and add true delta test
sircinnamon Mar 22, 2019
ad70fc7
Linting changes
sircinnamon Mar 22, 2019
7443e6d
Clean up disposal flag check
sircinnamon Apr 9, 2019
5fb36d2
Merge branch 'master' of https://github.com/python-pillow/Pillow
sircinnamon Apr 9, 2019
c3e982e
Merge branch 'master' into master
radarhere Jun 29, 2019
3e4db05
Removed code not required by tests
radarhere Jun 29, 2019
97c15a2
Corrected color table size calculation
radarhere Jun 29, 2019
90d3d37
Do not presume that the background color index is 0
radarhere Jun 29, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
101 changes: 99 additions & 2 deletions Tests/test_file_gif.py
@@ -1,6 +1,6 @@
from .helper import unittest, PillowTestCase, hopper, netpbm_available

from PIL import Image, ImagePalette, GifImagePlugin
from PIL import Image, ImagePalette, GifImagePlugin, ImageDraw

from io import BytesIO

Expand Down Expand Up @@ -59,7 +59,7 @@ def test_bilevel(optimize):
return len(test_file.getvalue())

self.assertEqual(test_grayscale(0), 800)
self.assertEqual(test_grayscale(1), 38)
self.assertEqual(test_grayscale(1), 44)
self.assertEqual(test_bilevel(0), 800)
self.assertEqual(test_bilevel(1), 800)

Expand Down Expand Up @@ -318,6 +318,103 @@ def test_save_dispose(self):
img.seek(img.tell() + 1)
self.assertEqual(img.disposal_method, i + 1)

def test_dispose2_palette(self):
out = self.tempfile("temp.gif")

# 4 backgrounds: White, Grey, Black, Red
circles = [(255, 255, 255), (153, 153, 153), (0, 0, 0), (255, 0, 0)]

im_list = []
for circle in circles:
img = Image.new("RGB", (100, 100), (255, 0, 0))

# Red circle in center of each frame
d = ImageDraw.Draw(img)
d.ellipse([(40, 40), (60, 60)], fill=circle)

im_list.append(img)

im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)

img = Image.open(out)

for i, circle in enumerate(circles):
img.seek(i)
rgb_img = img.convert("RGB")

# Check top left pixel matches background
self.assertEqual(rgb_img.getpixel((0, 0)), (255, 0, 0))

# Center remains red every frame
self.assertEqual(rgb_img.getpixel((50, 50)), circle)

def test_dispose2_diff(self):
out = self.tempfile("temp.gif")

# 4 frames: red/blue, red/red, blue/blue, red/blue
circles = [
((255, 0, 0, 255), (0, 0, 255, 255)),
((255, 0, 0, 255), (255, 0, 0, 255)),
((0, 0, 255, 255), (0, 0, 255, 255)),
((255, 0, 0, 255), (0, 0, 255, 255)),
]

im_list = []
for i in range(len(circles)):
# Transparent BG
img = Image.new("RGBA", (100, 100), (255, 255, 255, 0))

# Two circles per frame
d = ImageDraw.Draw(img)
d.ellipse([(0, 30), (40, 70)], fill=circles[i][0])
d.ellipse([(60, 30), (100, 70)], fill=circles[i][1])

im_list.append(img)

im_list[0].save(
out, save_all=True, append_images=im_list[1:], disposal=2, transparency=0
)

img = Image.open(out)

for i, colours in enumerate(circles):
img.seek(i)
rgb_img = img.convert("RGBA")

# Check left circle is correct colour
self.assertEqual(rgb_img.getpixel((20, 50)), colours[0])

# Check right circle is correct colour
self.assertEqual(rgb_img.getpixel((80, 50)), colours[1])

# Check BG is correct colour
self.assertEqual(rgb_img.getpixel((1, 1)), (255, 255, 255, 0))

def test_dispose2_background(self):
out = self.tempfile("temp.gif")

im_list = []

im = Image.new("P", (100, 100))
d = ImageDraw.Draw(im)
d.rectangle([(50, 0), (100, 100)], fill="#f00")
d.rectangle([(0, 0), (50, 100)], fill="#0f0")
im_list.append(im)

im = Image.new("P", (100, 100))
d = ImageDraw.Draw(im)
d.rectangle([(0, 0), (100, 50)], fill="#f00")
d.rectangle([(0, 50), (100, 100)], fill="#0f0")
im_list.append(im)

im_list[0].save(
out, save_all=True, append_images=im_list[1:], disposal=[0, 2], background=1
)

im = Image.open(out)
im.seek(1)
self.assertEqual(im.getpixel((0, 0)), 0)

def test_iss634(self):
img = Image.open("Tests/images/iss634.gif")
# seek to the second frame
Expand Down
49 changes: 34 additions & 15 deletions src/PIL/GifImagePlugin.py
Expand Up @@ -426,6 +426,7 @@ def _write_multiple_frames(im, fp, palette):

im_frames = []
frame_count = 0
background_im = None
for imSequence in itertools.chain([im], im.encoderinfo.get("append_images", [])):
for im_frame in ImageSequence.Iterator(imSequence):
# a copy is required here since seek can still mutate the image
Expand All @@ -445,11 +446,22 @@ def _write_multiple_frames(im, fp, palette):
if im_frames:
# delta frame
previous = im_frames[-1]
if _get_palette_bytes(im_frame) == _get_palette_bytes(previous["im"]):
delta = ImageChops.subtract_modulo(im_frame, previous["im"])
if encoderinfo.get("disposal") == 2:
if background_im is None:
background = _get_background(
im,
im.encoderinfo.get("background", im.info.get("background")),
)
background_im = Image.new("P", im_frame.size, background)
background_im.putpalette(im_frames[0]["im"].palette)
base_im = background_im
else:
base_im = previous["im"]
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("RGB"), previous["im"].convert("RGB")
im_frame.convert("RGB"), base_im.convert("RGB")
)
bbox = delta.getbbox()
if not bbox:
Expand Down Expand Up @@ -683,10 +695,12 @@ def _get_color_table_size(palette_bytes):
# calculate the palette size for the header
import math

color_table_size = int(math.ceil(math.log(len(palette_bytes) // 3, 2))) - 1
if color_table_size < 0:
color_table_size = 0
return color_table_size
if not palette_bytes:
return 0
elif len(palette_bytes) < 9:
return 1
else:
return int(math.ceil(math.log(len(palette_bytes) // 3, 2))) - 1


def _get_header_palette(palette_bytes):
Expand Down Expand Up @@ -717,6 +731,18 @@ def _get_palette_bytes(im):
return im.palette.palette


def _get_background(im, infoBackground):
background = 0
if infoBackground:
background = infoBackground
if isinstance(background, tuple):
# WebPImagePlugin stores an RGBA value in info["background"]
# So it must be converted to the same format as GifImagePlugin's
# info["background"] - a global color table index
background = im.palette.getcolor(background)
return background


def _get_global_header(im, info):
"""Return a list of strings representing a GIF header"""

Expand All @@ -736,14 +762,7 @@ def _get_global_header(im, info):
if im.info.get("version") == b"89a":
version = b"89a"

background = 0
if "background" in info:
background = info["background"]
if isinstance(background, tuple):
# WebPImagePlugin stores an RGBA value in info["background"]
# So it must be converted to the same format as GifImagePlugin's
# info["background"] - a global color table index
background = im.palette.getcolor(background)
background = _get_background(im, info.get("background"))

palette_bytes = _get_palette_bytes(im)
color_table_size = _get_color_table_size(palette_bytes)
Expand Down