The Graphics Interchange Format (GIF) was initially developed in 1987 as a solution for efficiently compressing and transmitting images over slow internet connections, which were prevalent during that era. Over time, the GIF89a specification has become an established standard within the World Wide Web Consortium (W3C).

A GIF file is composed of a series of data blocks, with the first two blocks having a fixed length and format. Subsequent blocks possess variable lengths and are self-descriptive, featuring a byte that identifies the block type, followed by a payload length byte, and finally the payload itself.

The accompanying railroad diagram illustrates the various types of blocks and their potential positions within the file. Each valid block sequence corresponds to a distinct path following the arrows. Notably, the central section of the diagram can be repeated indefinitely.

For a detailed explanation of each block, you can check out this excellent resource.

For our project’s requirements, the implementation of the GIF specification in Elixir is relatively straightforward.

defmodule GIF do
  def screen_descriptor(width, height, resolution) do
    <<width :: little-16,
      height :: little-16,
      1 :: 1,             # Global colour table flag
      resolution :: 3,    # Colour resolution
      0 :: 1,             # Sort flag
      resolution :: 3,    # Size of global colour table
      0,                  # Background colour index
      0,                  # Pixel aspect ratio
    >>
  end

  def color_table do
    <<0x00, 0x00, 0x00,   # Black
      0xFF, 0xFF, 0xFF,   # White
    >>
  end

  def graphic_control_ext(disposal \\ 0, delay \\ 0) do
    <<0x21,               # Extension introducer
      0xF9,               # Graphic control label
      0x04,               # Length of block
      0 :: 3,             # Reserved for future use
      disposal :: 3,      # Disposal method
      0 :: 1,             # User input flag
      0 :: 1,             # Transparent colour flag
      delay :: little-16, # Animation delay as a multiples of 10ms
      0,                  # Transparent colour index
      0,                  # Block terminator
    >>
  end

  def image_descriptor(left \\ 0, top \\ 0, width, height) do
    <<0x2C,               # Image seperator
      left :: little-16,
      top :: little-16,
      width :: little-16,
      height :: little-16,
      0 :: 1,             # Local colour table flag
      0 :: 1,             # Interlace flag
      0 :: 1,             # Sort flag
      0 :: 2,             # Reserved for future use
      0 :: 3,             # Size of local colour table
    >>
  end

  def application_extension(loop) do
    <<0x21,                # Extension introducer
      0xFF,                # Application extension label
      11,                  # Length of application block
      "NETSCAPE2.0",       # Extension name
      3,                   # Length of data sub block
      1,
      loop :: little-16,   # Loop counter
      0,                   # Data sub block terminator
    >>
  end
end

For our need, we need to create a base with header, screen_descriptor, color_table, and application_extension and loop to generate each frame from image_data.

base = "GIF89a" <>
  GIF.screen_descriptor(width, height, 0) <>
  GIF.color_table() <>
  GIF.application_extension(0)

frame = GIF.graphic_control_ext(1, 100) <>
  GIF.image_descriptor(width, height) <>
  image_data

To serve the image through the web server, we can send the initial frame as a chunked response, and send the subsequent frames every second as they are generated.

defmodule WebServer do
  def init(req, state) do
    req =
      :cowboy_req.stream_reply(
        200,
        %{
          "Content-Type" => "image/gif",
          "connection" => "keep-alive",
          "content-transfer-encoding" => "binary",
          "expires" => "0",
          "Cache-Control" => "no-cache, no-store, no-transform"
        },
        req
      )

    data = GifProducer.subscribe()
    :cowboy_req.stream_body(data, :nofin, req)

    {:cowboy_loop, req, state}
  end

  def info(msg, req, state) when is_binary(msg) do
    :cowboy_req.stream_body(msg, :nofin, req)
    {:ok, req, state}
  end

You can check out the full source code on Github.