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.