<rss version="2.0">
  <channel>
    <title>Gif on LLBBL Blog</title>
    <link>https://llbbl.blog/categories/gif/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Tue, 25 Aug 2026 10:00:00 -0500</lastBuildDate>
    
    <item>
      <title>Looping Is Not in the GIF Spec</title>
      <link>https://llbbl.blog/2026/08/25/looping-is-not-in-the.html</link>
      <pubDate>Tue, 25 Aug 2026 10:00:00 -0500</pubDate>
      
      <guid>http://llbbl.micro.blog/2026/08/25/looping-is-not-in-the.html</guid>
      <description>&lt;p&gt;The GIF89a specification defines the signature, the color tables, the LZW encoder, the four interlacing passes, transparency, and the per-frame delay. It does not define animation looping. Download it and search: the word &amp;ldquo;loop&amp;rdquo; does not appear in the document once.&lt;/p&gt;
&lt;p&gt;Every GIF you have ever watched repeat did so because of a 19-byte block that Netscape made up in 1995 and nobody ever standardized.&lt;/p&gt;
&lt;p&gt;I saved the same eight frames twice, once with a loop count and once without:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;loop.gif      1969 bytes
noloop.gif    1950 bytes
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Nineteen bytes. That is the entire difference between an animation and a slideshow that stops.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-nineteen-bytes&#34;&gt;The Nineteen Bytes&lt;/h2&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ grep -abo NETSCAPE loop.gif
28:NETSCAPE
$ grep -abo NETSCAPE noloop.gif
$
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;At byte 25, three bytes before that match, the block starts:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;0x21        extension introducer
0xFF        application extension label
0x0B        block size: 11
&amp;#34;NETSCAPE&amp;#34;  8 bytes
&amp;#34;2.0&amp;#34;       3 bytes
0x03        sub-block size: 3
0x01        sub-block ID: loop count
0x0000      loop count, little-endian; zero means forever
0x00        block terminator
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The GIF89a spec does define a generic Application Extension, in section 26: a container where a vendor can stash eleven bytes of identifier and whatever payload they like, which decoders that don&amp;rsquo;t recognize the identifier must skip. That was the extension point Netscape used, and the feature won so completely that it is now the only reason the format still exists.&lt;/p&gt;
&lt;p&gt;Section 14 of the same document, well before it gets there, tells you not to do this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This approach is recommended in favor of using Application Extensions, which become overhead for all other applications that do not process them.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The spec authors saw the vendor-extension move coming, wrote down that it was a bad idea, and then shipped the mechanism anyway. Six years later a browser vendor used it to define the format&amp;rsquo;s most famous feature.&lt;/p&gt;
&lt;p&gt;There is no RFC for this. There is no W3C note. There is no erratum to GIF89a. The &lt;a href=&#34;https://wiki.whatwg.org/wiki/GIF&#34;&gt;WHATWG&amp;rsquo;s own wiki page for GIF&lt;/a&gt; does not document the block either; it links out to a third-party page that reverse-engineered it. Every GIF encoder in the world implements a format feature whose only description is somebody else&amp;rsquo;s notes.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-header-and-the-ghost-of-1995&#34;&gt;The Header, and the Ghost of 1995&lt;/h2&gt;
&lt;p&gt;The first sixteen bytes tell you most of what the file is:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ xxd -g 1 -l 32 loop.gif
00000000: 47 49 46 38 39 61 c8 00 64 00 81 00 00 ff 5a 28  GIF89a..d.....Z(
00000010: 14 14 1e 00 00 00 00 00 00 21 ff 0b 4e 45 54 53  .........!..NETS
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;GIF89a&lt;/code&gt;, then &lt;code&gt;c8 00&lt;/code&gt; and &lt;code&gt;64 00&lt;/code&gt;: 200 by 100, little-endian, which is the opposite of what JPEG, PNG, and MP4 all chose. Then a packed byte, &lt;code&gt;0x81&lt;/code&gt;, whose low three bits say the global color table holds 2^(1+1) colors. Four colors, twelve bytes, and then at byte 25 the Netscape block starts immediately.&lt;/p&gt;
&lt;p&gt;The per-frame timing lives in a different block, the Graphic Control Extension, and it has a problem:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;GCE at byte 44:  delay field = 8 (hundredths of a second)
GCE at byte 386: delay field = 8 (hundredths of a second)
8 Graphic Control Extensions total
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Hundredths of a second. Two bytes, unsigned, little-endian. The finest interval the format can express is 10 milliseconds, so the theoretical ceiling is 100fps and there is no way to say 60fps evenly.&lt;/p&gt;
&lt;p&gt;It gets worse in practice. Browsers treat a delay of 0 or 1 as 10, so asking for 100fps gets you 10fps. &lt;a href=&#34;https://bugzilla.mozilla.org/show_bug.cgi?id=232822&#34;&gt;Firefox has done this since at least 2004&lt;/a&gt;, and the reason in the source comments is compatibility with how Netscape behaved. A delay of 2 is the practical floor, which puts the real maximum at 50fps.&lt;/p&gt;
&lt;p&gt;So the animation feature was defined by Netscape, and the animation timing is still bounded by an emulation of Netscape&amp;rsquo;s bugs, thirty years after Netscape.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;lzw-is-just-worse&#34;&gt;LZW Is Just Worse&lt;/h2&gt;
&lt;p&gt;Here is the part that makes GIF&amp;rsquo;s survival strange.&lt;/p&gt;
&lt;p&gt;I took a 512x512 image with 151,112 distinct colors and reduced it to a 256-color adaptive palette. Then I saved those exact same quantized pixels two ways:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;pal.gif      59198 bytes    (LZW)
pal.png      29154 bytes    (DEFLATE)
true.png     64106 bytes    (DEFLATE, all 151,112 colors, lossless)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Identical pixels, identical palette. PNG is less than half the size. And full-color lossless PNG, with every one of those 151,112 colors intact, is only 8% bigger than the GIF that threw 150,856 of them away.&lt;/p&gt;
&lt;p&gt;LZW builds a dictionary of repeated index sequences with codes that grow from 2 bits to a hard ceiling of 12. DEFLATE does LZ77 plus Huffman coding and has no such ceiling. On anything that isn&amp;rsquo;t flat-color line art, it is not close.&lt;/p&gt;
&lt;p&gt;To be fair to the format, GIF is not always the loser. My eight-frame animation of a flat orange circle on a flat dark background came out:&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;anim.gif     1969 bytes
anim.png     2170 bytes   (APNG)
anim.webp    4392 bytes
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;On a handful of frames of solid color, GIF&amp;rsquo;s per-frame overhead is small and LZW does fine. That is the shape of content GIF was designed for in 1987, when it was moving graphics over a 2400-baud modem, and it is still competitive there. It is just that nobody uses GIF for that anymore. They use it for compressed video of a person reacting to something, which is close to the worst possible input for a 256-color palette and a 12-bit dictionary.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;the-format-png-was-built-to-replace&#34;&gt;The Format PNG Was Built To Replace&lt;/h2&gt;
&lt;p&gt;At the end of December 1994, CompuServe and Unisys jointly announced that software reading or writing GIF would need a license for &lt;a href=&#34;https://patents.google.com/patent/US4558302A/en&#34;&gt;US Patent 4,558,302&lt;/a&gt;, Terry Welch&amp;rsquo;s LZW patent, granted in 1985.&lt;/p&gt;
&lt;p&gt;The response was immediate. PNG went from nothing to a frozen &lt;a href=&#34;https://www.w3.org/TR/PNG-History.html&#34;&gt;ninth draft on 7 March 1995&lt;/a&gt;, roughly ten weeks later. It is a better format on essentially every axis: better compression, 24-bit color, an alpha channel instead of one transparent index, and no patent. The League for Programming Freedom ran a &amp;ldquo;Burn All GIFs&amp;rdquo; campaign. Everyone agreed GIF should die.&lt;/p&gt;
&lt;p&gt;The patent expired on 20 June 2003, and by then the argument had been over for years. PNG had won the still-image half completely.&lt;/p&gt;
&lt;p&gt;It lost the other half because it did not have animation, and by the time APNG existed the web had already decided that a looping image was a GIF. The format that PNG was created to replace survives entirely on the one capability PNG shipped without, which the GIF specification also does not have, and which exists only because a browser vendor stuffed it into a generic extension slot and shipped it.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;what-to-do-about-it&#34;&gt;What To Do About It&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stop using GIF for video.&lt;/strong&gt; A short MP4 or a WebM is smaller by an order of magnitude, plays with hardware decoding, and does not have a 256-color palette. Every major platform silently transcodes your GIF uploads to video already.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use PNG for anything still.&lt;/strong&gt; There is no case in 2026 where a static GIF is the right answer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you must ship GIF, quantize deliberately.&lt;/strong&gt; Pick the palette yourself rather than letting the encoder guess, and dither on purpose. The default adaptive palette is rarely the best 256 colors for your image.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Never set a frame delay below 2.&lt;/strong&gt; You will get 10fps instead of the 50 or 100 you asked for, and it will look like a bug in your code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Check for the loop block if animations mysteriously play once.&lt;/strong&gt; &lt;code&gt;grep -abo NETSCAPE file.gif&lt;/code&gt;. Plenty of encoders omit it by default, including Pillow unless you pass &lt;code&gt;loop=0&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every format in this series has had a gap between what the specification says and what implementations do. GIF is the case where the gap swallowed the format. The spec describes a still-image format with optional frame timing. What the world actually uses is that spec plus one undocumented vendor block, and the vendor has been gone since 2003.&lt;/p&gt;
&lt;h2 id=&#34;sources&#34;&gt;Sources&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.w3.org/Graphics/GIF/spec-gif89a.txt&#34;&gt;GIF89a specification&lt;/a&gt; — CompuServe, 31 July 1989; the Application Extension block is section 26&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://wiki.whatwg.org/wiki/GIF&#34;&gt;WHATWG Wiki: GIF&lt;/a&gt; — where the web platform&amp;rsquo;s own wiki sends you for the looping extension, which is off-site&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://patents.google.com/patent/US4558302A/en&#34;&gt;US Patent 4,558,302&lt;/a&gt; — Welch, granted 1985, expired 20 June 2003&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.w3.org/TR/PNG-History.html&#34;&gt;PNG specification history&lt;/a&gt; — the ninth draft froze on 7 March 1995&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://bugzilla.mozilla.org/show_bug.cgi?id=232822&#34;&gt;Mozilla bug 232822&lt;/a&gt; — animated GIF frame delays of 10ms are slowed to 100ms&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://www.matthewflickinger.com/lab/whatsinagif/lzw_image_data.asp&#34;&gt;What&amp;rsquo;s In A GIF&lt;/a&gt; — the clearest walkthrough of GIF&amp;rsquo;s LZW bit packing anywhere&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at &lt;a href=&#34;https://micro.blog/llbbl?remote_follow=1&#34;&gt;@logan@llbbl.blog&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
</description>
    </item>
    
  </channel>
</rss>