Repository: python-pillow/Pillow

Issue:
GIF - use single colour table for all frames
### What did you do?

Attempted to normalise a sequence of `P` images to use the same colour palette in an attempt to output a smaller GIF file that uses a common palette across all frames.

The complete code is here - https://github.com/pimoroni/mlx90640-library/blob/master/python/rgb-to-gif.py - but the relevant snippet can be found below:

```python
    if len(frames) > 1:
        # Generate an image with all frames, and crunch it to ADAPTIVE to form a master colour palette
        master = Image.new('RGB', (32, 24 * len(frames)))
        for index, image in enumerate(frames):
            master.paste(image, (0, 24 * index))
        master = master.convert('P', dither=False, palette=Image.ADAPTIVE, colors=256)

        for index, image in enumerate(frames):
            image = image.convert('P', dither=False, palette=master.palette)
            # image = image.quantize(method=3, palette=master)
            image = image.transpose(Image.ROTATE_270).transpose(Image.FLIP_LEFT_RIGHT)
            image = image.resize(OUTPUT_SIZE, Image.NEAREST)
            frames[index] = image

        filename = 'mlx90640-{}.gif'.format(
            datetime.now().strftime("%Y-%m-%d-%H-%M-%S"))
print("Saving {} with {} frames.".format(filename, len(frames)))

        frames[0].save(
            filename,
            save_all=True,
            append_images=frames[1:],
            duration=1000 // fps,
            loop=0,
            include_color_table=True,
            optimize=True,
palette=master.palette.getdata())
```

### What did you expect to happen?

For the GIF format save method to understand that I was sharing a colour palette across frames, and export accordingly. There doesn't seem to be a way - that I can identify - to communicate this.

### What actually happened?

Result a ~1.19MiB GIF:

![mlx90640-2019-04-11-16-34-25](https://user-images.githubusercontent.com/1746967/55971201-e471b980-5c78-11e9-9663-a7018e19b4f3.gif)

Versus a ~263.4KiB GIF - This is a ~70-80% size saving just running the result through ezgif (https://ezgif.com/optimize) optimisation and picking "Use single color table for all frames":

![ezgif-4-917f7c76321e](https://user-images.githubusercontent.com/1746967/55971210-e9366d80-5c78-11e9-84f0-12011907e932.gif)

Unless I am very much misunderstanding what ezgif is doing in this case, no other optimisation was performed.

### What are your OS, Python and Pillow versions?

* OS: Linux, Raspbia
* Python: 3.4.9
* Pillow: PIL.PILLOW_VERSION == 5.4.1



Discussion:
Taking your 1.2mb image, and running
```python
from PIL import Image
im = Image.open('input.gif')
im.save('output.gif', save_all=True)
```
Before #5291, the output is 1.3mb. After, it is 320kb.

So we're not using a single color table yet, but that is a step towards solving your underlying problem.

Resolving patch:
diff --git a/src/PIL/GifImagePlugin.py b/src/PIL/GifImagePlugin.py
--- a/src/PIL/GifImagePlugin.py
+++ b/src/PIL/GifImagePlugin.py
@@ -414,9 +414,26 @@ def _normalize_palette(im, palette, info):
             source_palette = bytearray(i // 3 for i in range(768))
         im.palette = ImagePalette.ImagePalette("RGB", palette=source_palette)
 
-    used_palette_colors = _get_optimize(im, info)
-    if used_palette_colors is not None:
-        return im.remap_palette(used_palette_colors, source_palette)
+    if palette:
+        used_palette_colors = []
+        for i in range(0, len(source_palette), 3):
+            source_color = tuple(source_palette[i : i + 3])
+            try:
+                index = im.palette.colors[source_color]
+            except KeyError:
+                index = None
+            used_palette_colors.append(index)
+        for i, index in enumerate(used_palette_colors):
+            if index is None:
+                for j in range(len(used_palette_colors)):
+                    if j not in used_palette_colors:
+                        used_palette_colors[i] = j
+                        break
+        im = im.remap_palette(used_palette_colors)
+    else:
+        used_palette_colors = _get_optimize(im, info)
+        if used_palette_colors is not None:
+            return im.remap_palette(used_palette_colors, source_palette)
 
     im.palette.palette = source_palette
     return im
@@ -507,7 +524,8 @@ def _write_multiple_frames(im, fp, palette):
                 offset = (0, 0)
             else:
                 # compress difference
-                frame_data["encoderinfo"]["include_color_table"] = True
+                if not palette:
+                    frame_data["encoderinfo"]["include_color_table"] = True
 
                 im_frame = im_frame.crop(frame_data["bbox"])
                 offset = frame_data["bbox"][:2]


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
@@ -821,6 +821,29 @@ def test_palette_save_P(tmp_path):
         assert_image_equal(reloaded, im)
 
 
+def test_palette_save_all_P(tmp_path):
+    frames = []
+    colors = ((255, 0, 0), (0, 255, 0))
+    for color in colors:
+        frame = Image.new("P", (100, 100))
+        frame.putpalette(color)
+        frames.append(frame)
+
+    out = str(tmp_path / "temp.gif")
+    frames[0].save(
+        out, save_all=True, palette=[255, 0, 0, 0, 255, 0], append_images=frames[1:]
+    )
+
+    with Image.open(out) as im:
+        # Assert that the frames are correct, and each frame has the same palette
+        assert_image_equal(im.convert("RGB"), frames[0].convert("RGB"))
+        assert im.palette.palette == im.global_palette.palette
+
+        im.seek(1)
+        assert_image_equal(im.convert("RGB"), frames[1].convert("RGB"))
+        assert im.palette.palette == im.global_palette.palette
+
+
 def test_palette_save_ImagePalette(tmp_path):
     # Pass in a different palette, as an ImagePalette.ImagePalette
     # effectively the same as test_palette_save_P