Repository: python-pillow/Pillow

Issue:
A frame is *ignored* when a GIF is saved with a completely transparent frame included
### What did you do?
I tried to save a gif file with a completely transparent frame.

### What did you expect to happen?
![expected](https://user-images.githubusercontent.com/62872159/206250174-2ac104fc-cd02-4da7-af72-213df161de3d.gif)

### What actually happened?
![example](https://user-images.githubusercontent.com/62872159/206251051-d40c78b8-2159-4a45-89c2-199683232134.gif)
You can see that the last empty frame does not appear. Instead, the duration of the previous frame is delayed.

### What are your OS, Python and Pillow versions?

* OS: I tried both Windows 10, MacOS, and Raspbian
* Python: 3.10.4
* Pillow: 9.3.0

<!--
Please include **code** that reproduces the issue and whenever possible, an **image** that demonstrates the issue. Please upload images to GitHub, not to third-party file hosting sites. If necessary, add the image to a zip or tar archive.

The best reproductions are self-contained scripts with minimal dependencies. If you are using a framework such as Plone, Django, or Buildout, try to replicate the issue just using Pillow.
-->

Note that I came up with a simple code rather than my original code to keep this simple.
```python
from PIL import Image, ImageDraw

image1 = Image.new("RGBA", (100, 100), "#00000000")
image2 = Image.new("RGBA", (100, 100), "#00000000")
image3 = Image.new("RGBA", (100, 100), "#00000000")

draw = ImageDraw.Draw(image2)
draw.rectangle((25, 25, 75, 75), fill = "#FF0000FF")

image1.save("example.gif", format="GIF", save_all=True, disposal=2, loop=0, duration=1000, append_images=[image2, image3])
```

This also happens when a completely black (#000000) image is appended, regardless of the transparency. 
```python
from PIL import Image, ImageDraw

image1 = Image.new("RGBA", (100, 100), "#00000000")
image2 = Image.new("RGBA", (100, 100), "#00000000")
image3 = Image.new("RGBA", (100, 100), "#00000000")

draw = ImageDraw.Draw(image2)
draw.rectangle((25, 25, 75, 75), fill = "#FF0000FF")

draw = ImageDraw.Draw(image3)
draw.rectangle((25, 25, 75, 75), fill = "#000000FF")

image1.save("example.gif", format="GIF", save_all=True, disposal=2, loop=0, duration=1000, append_images=[image2, image3])
```

*edit
I checked that "white" (#FFFFFF00) transparent images are normally appended to the image. 

Example code:
```python
from PIL import Image, ImageDraw

image1 = Image.new("RGBA", (100, 100), "#FFFFFF00")
image2 = Image.new("RGBA", (100, 100), "#FFFFFF00")
image3 = Image.new("RGBA", (100, 100), "#FFFFFF00")

draw = ImageDraw.Draw(image2)
draw.rectangle((25, 25, 75, 75), fill = "#FF0000FF")

image1.save("example.gif", format="GIF", save_all=True, disposal=2, loop=0, duration=1000, append_images=[image2, image3])
```
This will produce the wanted result. But anyways, I think this still is an issue.


Discussion:
I've created PR #6787 to resolve this.

Resolving patch:
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -565,6 +565,16 @@ def _write_single_frame(im, fp, palette):
     fp.write(b"\0")  # end of image data
 
 
+def _getbbox(base_im, im_frame):
+    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"), base_im.convert("RGB")
+        )
+    return delta.getbbox()
+
+
 def _write_multiple_frames(im, fp, palette):
 
     duration = im.encoderinfo.get("duration")
@@ -598,6 +608,12 @@ def _write_multiple_frames(im, fp, palette):
             if im_frames:
                 # delta frame
                 previous = im_frames[-1]
+                bbox = _getbbox(previous["im"], im_frame)
+                if not bbox:
+                    # This frame is identical to the previous frame
+                    if encoderinfo.get("duration"):
+                        previous["encoderinfo"]["duration"] += encoderinfo["duration"]
+                    continue
                 if encoderinfo.get("disposal") == 2:
                     if background_im is None:
                         color = im.encoderinfo.get(
@@ -606,21 +622,7 @@ def _write_multiple_frames(im, fp, palette):
                         background = _get_background(im_frame, color)
                         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"), base_im.convert("RGB")
-                    )
-                bbox = delta.getbbox()
-                if not bbox:
-                    # This frame is identical to the previous frame
-                    if encoderinfo.get("duration"):
-                        previous["encoderinfo"]["duration"] += encoderinfo["duration"]
-                    continue
+                    bbox = _getbbox(background_im, im_frame)
             else:
                 bbox = None
             im_frames.append({"im": im_frame, "bbox": bbox, "encoderinfo": encoderinfo})


Test patch:
diff --git a/Tests/test_file_gif.py b/Tests/test_file_gif.py
--- a/Tests/test_file_gif.py
+++ b/Tests/test_file_gif.py
@@ -677,6 +677,24 @@ def test_dispose2_background(tmp_path):
         assert im.getpixel((0, 0)) == (255, 0, 0)
 
 
+def test_dispose2_background_frame(tmp_path):
+    out = str(tmp_path / "temp.gif")
+
+    im_list = [Image.new("RGBA", (1, 20))]
+
+    different_frame = Image.new("RGBA", (1, 20))
+    different_frame.putpixel((0, 10), (255, 0, 0, 255))
+    im_list.append(different_frame)
+
+    # Frame that matches the background
+    im_list.append(Image.new("RGBA", (1, 20)))
+
+    im_list[0].save(out, save_all=True, append_images=im_list[1:], disposal=2)
+
+    with Image.open(out) as im:
+        assert im.n_frames == 3
+
+
 def test_transparency_in_second_frame(tmp_path):
     out = str(tmp_path / "temp.gif")
     with Image.open("Tests/images/different_transparency.gif") as im: