Repository: python-pillow/Pillow

Issue:
Ability to set duration in gifmaker script
Add ability to set `duration` in the [gifmaker](https://github.com/python-pillow/Pillow/blob/54173d2c67bc8ed8953b56e67d4719676f43a073/Scripts/gifmaker.py) script.

From http://pillow.readthedocs.org/en/latest/handbook/image-file-formats.html#gif:

> **duration**
> Time between frames in an animation (in milliseconds).



Discussion:
Depends on #1085 

Still an issue?


Resolving patch:
diff --git a/PIL/GifImagePlugin.py b/PIL/GifImagePlugin.py
--- a/PIL/GifImagePlugin.py
+++ b/PIL/GifImagePlugin.py
@@ -292,8 +292,24 @@ def _save(im, fp, filename):
     for s in header:
         fp.write(s)
 
-    flags = 0
+    # local image header
+    get_local_header(fp, im)
+
+    im_out.encoderconfig = (8, get_interlace(im))
+    ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0,
+                                  RAWMODE[im_out.mode])])
+
+    fp.write(b"\0")  # end of image data
+
+    fp.write(b";")  # end of file
+
+    try:
+        fp.flush()
+    except:
+        pass
 
+
+def get_interlace(im):
     try:
         interlace = im.encoderinfo["interlace"]
     except KeyError:
@@ -303,9 +319,11 @@ def _save(im, fp, filename):
     if min(im.size) < 16:
         interlace = 0
 
-    if interlace:
-        flags = flags | 64
+    return interlace
+
 
+def get_local_header(fp, im, offset=(0, 0)):
+    transparent_color_exists = False
     try:
         transparency = im.encoderinfo["transparency"]
     except KeyError:
@@ -314,46 +332,60 @@ def _save(im, fp, filename):
         transparency = int(transparency)
         # optimize the block away if transparent color is not used
         transparent_color_exists = True
-        # adjust the transparency index after optimize
-        if used_palette_colors is not None and len(used_palette_colors) < 256:
-            for i in range(len(used_palette_colors)):
-                if used_palette_colors[i] == transparency:
-                    transparency = i
-                    transparent_color_exists = True
-                    break
-                else:
-                    transparent_color_exists = False
-
-        # transparency extension block
-        if transparent_color_exists:
-            fp.write(b"!" +
-                     o8(249) +              # extension intro
-                     o8(4) +                # length
-                     o8(1) +                # transparency info present
-                     o16(0) +               # duration
-                     o8(transparency) +     # transparency index
-                     o8(0))
-
-    # local image header
-    fp.write(b"," +
-             o16(0) + o16(0) +          # bounding box
-             o16(im.size[0]) +          # size
-             o16(im.size[1]) +
-             o8(flags) +                # flags
-             o8(8))                     # bits
 
-    im_out.encoderconfig = (8, interlace)
-    ImageFile._save(im_out, fp, [("gif", (0, 0)+im.size, 0,
-                                  RAWMODE[im_out.mode])])
+        if _get_optimize(im, im.encoderinfo):
+            used_palette_colors = _get_used_palette_colors(im)
+
+            # adjust the transparency index after optimize
+            if len(used_palette_colors) < 256:
+                for i in range(len(used_palette_colors)):
+                    if used_palette_colors[i] == transparency:
+                        transparency = i
+                        transparent_color_exists = True
+                        break
+                    else:
+                        transparent_color_exists = False
+
+    if "duration" in im.encoderinfo:
+        duration = int(im.encoderinfo["duration"] / 10)
+    else:
+        duration = 0
+    if transparent_color_exists or duration != 0:
+        transparency_flag = 1 if transparent_color_exists else 0
+        if not transparent_color_exists:
+            transparency = 0
+
+        fp.write(b"!" +
+                 o8(249) +                # extension intro
+                 o8(4) +                  # length
+                 o8(transparency_flag) +  # transparency info present
+                 o16(duration) +          # duration
+                 o8(transparency) +       # transparency index
+                 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))
 
-    fp.write(b"\0")  # end of image data
+    flags = 0
 
-    fp.write(b";")  # end of file
+    if get_interlace(im):
+        flags = flags | 64
 
-    try:
-        fp.flush()
-    except:
-        pass
+    fp.write(b"," +
+             o16(offset[0]) +             # offset
+             o16(offset[1]) +
+             o16(im.size[0]) +            # size
+             o16(im.size[1]) +
+             o8(flags) +                  # flags
+             o8(8))                       # bits
 
 
 def _save_netpbm(im, fp, filename):
@@ -405,11 +437,26 @@ def _save_netpbm(im, fp, filename):
 # --------------------------------------------------------------------
 # GIF utilities
 
+def _get_optimize(im, info):
+    return im.mode in ("P", "L") and info and info.get("optimize", 0)
+
+
+def _get_used_palette_colors(im):
+    used_palette_colors = []
+
+    # check which colors are used
+    i = 0
+    for count in im.histogram():
+        if count:
+            used_palette_colors.append(i)
+        i += 1
+
+    return used_palette_colors
+
+
 def getheader(im, palette=None, info=None):
     """Return a list of strings representing a GIF header"""
 
-    optimize = info and info.get("optimize", 0)
-
     # Header Block
     # http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp
     header = [
@@ -431,15 +478,8 @@ def getheader(im, palette=None, info=None):
 
     used_palette_colors = palette_bytes = None
 
-    if im.mode in ("P", "L") and optimize:
-        used_palette_colors = []
-
-        # check which colors are used
-        i = 0
-        for count in im.histogram():
-            if count:
-                used_palette_colors.append(i)
-            i += 1
+    if _get_optimize(im, info):
+        used_palette_colors = _get_used_palette_colors(im)
 
         # create the new palette if not every color is used
         if len(used_palette_colors) < 256:
@@ -510,13 +550,7 @@ def write(self, data):
         im.encoderinfo = params
 
         # local image header
-        fp.write(b"," +
-                 o16(offset[0]) +       # offset
-                 o16(offset[1]) +
-                 o16(im.size[0]) +      # size
-                 o16(im.size[1]) +
-                 o8(0) +                # flags
-                 o8(8))                 # bits
+        get_local_header(fp, im, offset)
 
         ImageFile._save(im, fp, [("gif", (0, 0)+im.size, 0, RAWMODE[im.mode])])
 
diff --git a/Scripts/gifmaker.py b/Scripts/gifmaker.py
--- a/Scripts/gifmaker.py
+++ b/Scripts/gifmaker.py
@@ -72,8 +72,8 @@ def makedelta(fp, sequence):
 
     for im in sequence:
 
-        #
-        # FIXME: write graphics control block before each frame
+        # To specify duration, add the time in milliseconds to getdata(),
+        # e.g. getdata(im, duration=1000)
 
         if not previous:
 


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
@@ -169,6 +169,33 @@ def test_iss634(self):
         # first frame
         self.assertEqual(img.histogram()[img.info['transparency']], 0)
 
+    def test_duration(self):
+        duration = 1000
+
+        out = self.tempfile('temp.gif')
+        fp = open(out, "wb")
+        im = Image.new('L', (100, 100), '#000')
+        for s in GifImagePlugin.getheader(im)[0] + GifImagePlugin.getdata(im, duration=duration):
+            fp.write(s)
+        fp.write(b";")
+        fp.close()
+        reread = Image.open(out)
+
+        self.assertEqual(reread.info['duration'], duration)
+
+    def test_number_of_loops(self):
+        number_of_loops = 2
+
+        out = self.tempfile('temp.gif')
+        fp = open(out, "wb")
+        im = Image.new('L', (100, 100), '#000')
+        for s in GifImagePlugin.getheader(im)[0] + GifImagePlugin.getdata(im, loop=number_of_loops):
+            fp.write(s)
+        fp.write(b";")
+        fp.close()
+        reread = Image.open(out)
+
+        self.assertEqual(reread.info['loop'], number_of_loops)
 
 if __name__ == '__main__':
     unittest.main()