GIF decode/encode extension
This is a uLisp extension for decoding and encoding GIF images.
Updates
11th February 2026: Added a note about memory requirements, and added an extra example serving a GIF calculated from a function to an HTTP request.
Functions
It provides the following functions:
decode-gif function
Syntax: (decode-gif stream [function])
Decodes a GIF file from the stream and plots it, or calls a function for each pixel.

If a function is provided it should be of the form:
(defun fn (x y colours) ... )
and it can be used to plot the colour index at x,y.
encode-gif function
Syntax: (encode-gif stream xsize ysize colours [function] [noclear])
Encodes a GIF file by reading the display, posterising the 16-bit colour to appropriate colour indexes for the specified number of colours, and outputs the encoding to stream.

If a function is provided it is called for each point, and should be of the form:
(defun fn (x y colours) ... )
It should return a colour index appropriate for the specified number of colours. You can call posterise in this function if you want to create the colour index from a 16-bit colour.
Setting noclear to t can give a smaller file size for images with repetitive content.
posterise function
Syntax: (posterise col565 colours)
Converts a 16-bit colour in RGB 565 format to a colour index in the specified number of colours by posterising it, and returns it.
For example, to find the colour index for red with an 8-colour table:
(posterise #xf800 8) 4
The following colour tables are supported:
| Colours | Colour table |
| 2 | Black and white |
| 4 | Black, green, red, white |
| 8 | Black, blue, green, cyan, red, magenta, yellow, white |
| 16 | Every combination of 2 red levels, 3 green levels, and 2 blue levels. |
| 32 | Every combination of 3 red levels, 3 green levels, and 3 blue levels. |
| 64 | Every combination of 4 red levels, 4 green levels, and 4 blue levels. |
| 128 | Every combination of 5 red levels, 5 green levels, and 5 blue levels. |
| 256 | Every combination of 6 red levels, 7 green levels, and 6 blue levels. |
Installing the GIF extension
- Download the GIF decode/encode extension file and save it with the name GIFDecodeEncode.ino.
- Put it in the same folder as your uLisp source file.
- In the main uLisp source file uncomment the lines:
#define gfxsupport #define extensions
- Compile uLisp and upload it to your board.
The decode-gif, encode-gif, and posterise functions will then be added to the built-in functions in uLisp, together with their documentation.
For information about the Extensions File feature of uLisp see: Adding your own functions.
Memory requirements
The GIF extension requires about 13060 bytes of memory, which is equivalent to about 1632 uLisp objects. On some platforms you may need to reduce the value of WORKSPACE by this amount to avoid an out of memory error.
The symptom is an error such as:
collect2: error: ld returned 1 exit status
Adjust the workspace #define by subtracting 1632; for example:
#define WORKSPACESIZE (9216-1632)
Graphics applications
Between them these functions allow you to implement a variety of different applications illustrated by the following examples:
- Display an image from an SD card
- Control how an image is displayed
- Save a screendump to an SD card
- Encode an image defined by a function and save it to SD card
- Encode with an alternative encoding
- Display the palette for a given number of colours
- Encode an image with fewer colours
- Load an image from the web
- Capture a screen display over the web
- Return a GIF generated from a function over the web
Get all the examples in a single file here: GIF decode/encode examples.
Here are the examples:
Display an image from an SD card
(with-sd-card (str "JapanCat.gif") (decode-gif str))
This simply loads an image from the SD card and displays it:

This is a 320x240 256-colour GIF created in an external image program with a size of about 60Kbytes.
Control how an image is displayed
By default decode-gif displays the image at the top left corner of the screen. If you want more control over how the GIF is displayed you can provide a function as the third argument. This should be of the form fn(x y colours).
For example, this displays the same image flipped left to right:
(with-sd-card (str "JapanCat.gif") (decode-gif str #'(lambda (x y c) (draw-pixel (- 319 x) y c))))
Here's the result:

Save a screendump to an SD card
The following program saves the image on the display to an SD card with the specified filename:
(defun screendump (filename &optional (colours 256))
(bind (width height) (display-size)
(with-sd-card (str filename 2)
(encode-gif str width height colours))))
It calls encode-gif to encode the image as a GIF with the specified number of colours, which defaults to 256.
Encode an image defined by a function and save it to SD card
If you give a function as the last argument to encode-gif, the function is called for each x,y coordinate of the image, and the value it returns determines the colour index of that pixel.
In the following example the function ellipses calculates a function at each point (xx, yy) that draws coloured ellipses:
(defun ellipses (xx yy colours)
(bind (width height) (display-size)
(let* ((x (- xx (truncate width 2)))
(y (- yy (truncate height 2)))
(f (truncate (+ (* x (+ x y)) (* y y)) 17)))
(mod f colours))))
(defun test2 ()
(bind (width height) (display-size)
(let ((colours 64))
(with-sd-card (str "ellipses.gif" 2)
(encode-gif str width height colours ellipses)))))
The resulting data is written to the SD card. You can display the image from the SD card with the command:
(with-sd-card (str "ellipses.gif") (decode-gif str))
Here's the result of the above call:

The ellipses function can be used to generate detailed images of any specified size for testing the GIF encoder.
Encode with an alternative encoding
The encode-gif function allows you to specify a noclear option allowed by the GIF specification that reuses the translation table once it's full, rather than clearing and restarting it. This can give smaller images if they contain repetitive content, like these ellipse patterns. Here's the same call with noclear set to t:
(defun test2 ()
(bind (width height) (display-size)
(let ((colours 64))
(with-sd-card (str "ellipse2.gif" 2)
(encode-gif str width height colours ellipses t)))))
The image ellipse2.gif looks identical but is 67065 bytes rather than 68417 bytes.
Display the palette for a given number of colours
The encode-gif function automatically chooses a palette for a specified number of colours, with the colours optimally spread out in the colour space.
The following function generates a test card showing the palette for a given number of colours and saves it to an SD card:
(defun palette (colours)
(bind (xsize ysize) (display-size)
(let* ((p (- (integer-length colours) 2))
(xn (ash 1 (truncate (+ p 2) 2)))
(yn (ash 1 (truncate (+ p 1) 2)))
(h (truncate xsize xn))
(v (truncate ysize yn))
(filename (format nil "P~a.gif" colours)))
(with-sd-card (str filename 2)
(encode-gif str xsize ysize colours
#'(lambda (x y colours)
(+ (* (truncate y v) xn) (truncate x h))))))))
It saves a file to the SD card called Pn where n is the number of colours.
For example, to generate an image showing the 256 colour palette, evaluate:
(palette 256)
You can then display the image from the SD card with the command:
(with-sd-card (str "P256.GIF") (decode-gif str))
The image shows a block for each of the 256 colours:

For comparison, here is the palette for 8 colours:
Encode an image with fewer colours
The following example shows the effect of posterising an image to a smaller palette, in this case from 256 colours to 64 colours. It loads the image JapanCat.gif, encodes it to a 64-colour GIF by reading each pixel on the screen and writing it to the file Cat2.gif, and then loads the image Cat2.gif back to the screen:
(defun test3 ()
(bind (width height) (display-size)
(with-sd-card (str "JapanCat.gif") (decode-gif str))
(with-sd-card (str "Cat2.gif" 2) (encode-gif str width height 64))
(with-sd-card (str "Cat2.gif") (decode-gif str))))
Here's the result:

Encoding it with 64 colours reduces the size from 68Kbytes to about 22Kbytes.
Graphics and Wi-Fi applications
The following examples use the GIF decode/encode extension in conjunction with the Wi-Fi extensions. They are based on the Wi-Fi examples.
Load an image from the web
The following example loads a GIF image from the web; the example I've used is a 240x135 256-colour GIF from this site at the following URL:
http://www.ulisp.com/pictures/3j/japancat240x135.gif.
I ran it on the Adafruit ESP32-S2 TFT Feather which has PSRAM. You could also use the TTGO T-Display, but you will need to reduce the #define WORKSPACESIZE setting to allow for the arrays needed by the GIF decode/encode extension file.
First log into wi-fi with your network name and password, such as:
(wifi-connect "Geronimo" "secret99")
Define the following function:
(defun display-pic ()
(with-client (s "www.ulisp.com" 80)
(format s "GET /pictures/3j/japancat240x135.gif HTTP/1.0~a~%" #\return)
(format s "Host: www.ulisp.com~a~%" #\return)
(format s "Connection: close~a~%" #\return)
(format s "~a~%" #\return)
(loop (when (= (length (read-line s)) 1) (return))) ; Skip headers
(decode-gif s)))
The format statements terminate each line with an extra #\return character, which is required by the HTTP protocol; if you forget to include these it won't work.
Then run it by typing:
(display-pic)
It should connect to www.ulisp.com, fetch the file, decode it by running decode-gif, and display:

Capture a screen display over the web
This next example demonstrates encoding a screen display as a GIF and serving it over a network to a request from a web browser.
To demonstrate the idea the following program clock displays the time on the 240x135 display on the Adafruit ESP32-S2 TFT Feather:
(defun clock ()
(wifi-server)
(format t "Connect your web browser to ~a~%" (wifi-localip))
(bind (xs ys) (display-size)
(let ((lastminute -1))
; Wait for time
(loop (when (get-time) (return)))
; Update time
(loop
(bind (year month day hour minute &rest others) (get-time)
(unless (= minute lastminute)
(fill-screen)
(with-gfx (str)
(set-text-size 8) (set-cursor 0 40) (set-text-color #xf81f)
(format str "~a:~2,'0d" hour minute))
(setq lastminute minute)))
; Check for web client
(serve-gif)
(delay 10000)))))
This calls (wifi-server) to start a web server. It then calls serve-gif to check whether a client is connected, and if so, serve the GIF file by calling encode-gif:
(defun serve-gif ()
(bind (xs ys) (display-size)
(with-client (s)
(loop (when (= (length (read-line s)) 1) (return)))
(format s "HTTP/1.1 200 OK~a~%" #\return)
(format s "Content-Type: image/gif~a~%" #\return)
(format s "Connection: close~a~%" #\return)
(format s "~a~%" #\return)
(encode-gif s xs ys 8))))
To run this, log into Wi-Fi with your network name and password, such as:
(wifi-connect "Geronimo" "secret99")
Then run:
(clock)
This repeatedly updates the time and calls serve-gif.
In serve-gif the call to with-client returns nil if there's no web browser trying to connect; otherwise it encodes the screen as a GIF and returns the data as the response:

Note: There was a problem on some platforms that calling (wifi-server) a second time caused the web server to fail. This is fixed in uLisp Release 4.9a.
Return a GIF generated from a function over the web
For the following example you don't even need a display. It calculates an image from a function and returns it to an HTTP request. The example was run on an Adafruit PyPortal.
The following function calculates an integer value from 0 to (1- colours) by evaluating the function cos(x)+cos(y), with suitable scaling:
(defun plot (x y colours)
(truncate
(* (1- colours)
(/ (+ 2
(cos (/ (- x (/ 320 2)) 20))
(cos (/ (- y (/ 240 2)) 20))) 4))))
This is encoded as a GIF by passing it as the last parameter to an encode-gif call, and returned in response to an HTTP request by the function 3dplot:
(defun 3dplot ()
(wifi-server)
(format t "Connect your web browser to ~a~%" (wifi-localip))
(loop
(with-client (s)
(loop (when (= (length (read-line s)) 1) (return)))
(format s "HTTP/1.1 200 OK~a~%" #\return)
(format s "Content-Type: image/gif~a~%" #\return)
(format s "Connection: close~a~%" #\return)
(format s "~a~%" #\return)
(encode-gif s 320 240 64 plot))
(delay 10000)))
To run it, log into wi-fi with your network name and password, such as:
(wifi-connect "Geronimo" "secret99")
Then run:
(3dplot)
This calls (wifi-server) to start a web server. It then repeatedly calls with-client which returns nil if there's no web browser trying to connect. Otherwise it calls the function for each point, encodes the result as a GIF, and returns the data as the response. It will display the following 64-colour 320x240 image in a web browser:
See the note above about (web-server).
About the GIF decode/encode extension
The GIF image format has several advantages over other formats for microcontroller-based boards. It is relatively simple to implement, doesn't have large RAM requirements, and can give very compact images, especially for diagrams and screen displays. It's also well supported by image editors.
Decoding a GIF image
The GIF format treats the image as a linear stream of pixel colour values. The LZW compression it uses works by finding a sequence of pixels it has already encountered, and it encodes each sequence as a variable-length code of from 3 to 12 bits. These codes are then output as a continuous stream of bits.
Decoding a GIF requires the decoder to divide the input stream into codes containing the correct number of bits. These codes are then looked up in a 4096-element table, to translate them into the appropriate sequence of one or more pixels represented by that code.
It would be very memory intensive if we had to store the complete pixel sequence corresponding to every code in the table. Fortunately, every sequence is equivalent to an earlier sequence, extended by the addition of one pixel. We can therefore represent the sequence as a pointer to the earlier sequence in the table, plus the additional pixel. Each entry in the translation table therefore needs to store a 12-bit pointer to an earlier entry in the table, plus an 8-bit byte for the colour of the additional pixel in the sequence.
The clever thing about LZW compression is that the lookup table doesn't need to be included with the GIF image, because it can be reconstructed dynamically as the code values are read in from the input stream.
Encoding an image
Encoding a 16-bit colour image is harder than decoding because you need to decide how to allocate the colours to the maximum of 256 colours available in the colour table. This extension uses a simple posterising routine that creates a balanced colour table, and also makes it easy to convert each 16-bit colour to a colour index, or back again.
Restrictions
This decoder supports GIF87a or GIF89a format GIFs with from 2 to 256 colours. It ignores inapplicable application extensions, such as the Adobe XMP extension inserted by Photoshop.
It doesn't support animated GIFs, local colour tables, or interlaced/progressive GIFs, but should work with standard GIFs created by a range of image editors.
History
These extensions are a development of several GIF decoders I've written over the years.
To test the algorithms I first wrote a GIF decoder in Lisp: A minimal GIF decoder on Lispology.
This led to a GIF decoder written in C running on an AVR128DA28 microcontroller: Minimal GIF decoder on Technoblogy.
I also wrote a version of the GIF image decoder in Lisp to run on a PicoCalc: GIF image decoder on uLisp.
This latest version is written in C as a uLisp extension for fast performance, and includes an encoder so you can GIF-encode an image from a screen or function.
