Now that we have everything set up, let's dig into the example code. To start with, we have a lot of code imports. Let's go over those:
We start with os
so that we can access a list of files in the operating system, time
for doing delays, digitalio
for button access and display pins, board
so we can see the pin definitions, and the Image
and ImageOps
modules for using Pillow. Then we import all of the displays so that we have a variety of displays to choose from. Since we're not so limited on memory on the Raspberry Pi, it works fine to just import everything.
import os import time import digitalio import board from PIL import Image, ImageOps import adafruit_rgb_display.ili9341 as ili9341 import adafruit_rgb_display.st7789 as st7789 # pylint: disable=unused-import import adafruit_rgb_display.hx8357 as hx8357 # pylint: disable=unused-import import adafruit_rgb_display.st7735 as st7735 # pylint: disable=unused-import import adafruit_rgb_display.ssd1351 as ssd1351 # pylint: disable=unused-import import adafruit_rgb_display.ssd1331 as ssd1331 # pylint: disable=unused-import
Next we have the buttons. The defaults are the most common setup, but a few of the displays vary and there is a number right next to the button. If you would like to change it, this is the correct place.
# Change to match your display BUTTON_NEXT = board.D17 BUTTON_PREVIOUS = board.D22
If you had the 1.14" Mini PiTFT, you would change it to something like the following:
BUTTON_NEXT = board.D23 BUTTON_PREVIOUS = board.D24
Next, we need to set up the pins that the display will use. We are using the defaults for the PiTFT, though you could do the same thing with a breakout board. If you have a Mini PiTFT, change reset_pin
to None
.
# Configuration for CS and DC pins (these are PiTFT defaults): cs_pin = digitalio.DigitalInOut(board.CE0) dc_pin = digitalio.DigitalInOut(board.D25) # Set this to None on the Mini PiTFT reset_pin = digitalio.DigitalInOut(board.D24)
Next, we have a convenience function for initializing the button with the internal pull-up resistor enabled.
def init_button(pin): button = digitalio.DigitalInOut(pin) button.switch_to_input() button.pull = digitalio.Pull.UP return button
Next we define a Frame
class. This probably could have been done with a named tuple, since there are no methods, but classes are very readable and it gives the flexibility to add methods. The Frame
class is used for holding the image and the duration of the frame. This allows for displaying animated Gifs with variable durations.
class Frame: def __init__(self, duration=0): self.duration = duration self.image = None
Now to get into the AnimatedGif
class, which does most of the work. The reason for going with a class is that there are a lot of shared variables that are used in functions and this modularizes everything. A good indicator of whether to use a class is if you need to use the global
keyword to modify variables.
In the __init__
function, it is mostly setting each of the internal variables to their defaults and initializing the buttons. If the width
and height
are passed in, those are used, otherwise they are extracted from the display object. We also allow the folder
name to be passed in and if that is present, we will load all the files and automatically run it.
class AnimatedGif: def __init__(self, display, width=None, height=None, folder=None): self._frame_count = 0 self._loop = 0 self._index = 0 self._duration = 0 self._gif_files = [] self._frames = [] if width is not None: self._width = width else: self._width = display.width if height is not None: self._height = height else: self._height = display.height self.display = display self.advance_button = init_button(BUTTON_NEXT) self.back_button = init_button(BUTTON_PREVIOUS) if folder is not None: self.load_files(folder) self.run()
Next, we have a pair of functions to either advance
to the next image or go back
. They use the modulus operator (%
) to see if they go beyond the end of the range allowing them to loop back.
def advance(self): self._index = (self._index + 1) % len(self._gif_files) def back(self): self._index = (self._index - 1 + len(self._gif_files)) % len(self._gif_files)
Next up is the load_files
function which will grab anything that ends in a .gif extension and then go through that list to check if it an animated Gif. Anything that is an animation is added to the self._gif_files
list. If none are found, it prints a message and exits.
def load_files(self, folder): self._gif_files = [f for f in os.listdir(folder) if f.endswith('.gif')] for gif_file in gif_files: image = Image.open(gif_file) # Only add animated Gifs if image.is_animated: self._gif_files.append(gif_file) print("Found", self._gif_files) if not self._gif_files: print("No Gif files found in current folder") exit()
The preload function is a bit more complex, so we will break that up a little bit more. It starts by opening the current image and printing out a message to the console.
def preload(self): image = Image.open(self._gif_files[self._index]) print("Loading {}...".format(self._gif_files[self._index])) #continued...
It continues loading any meta data from the animated Gif such as loop and duration info if available.
self._delay = image.info['duration'] if "loop" in image.info: self._loop = image.info['loop'] else: self._loop = 1 self._frame_count = image.n_frames self._frames.clear() #continued...
Then it loops through all the frames of the image. Inside the loop, it starts off by creating a frame object using the Frame
class and sets the duration for the frame if available, otherwise it uses the default duration for the entire image.
Next it uses the Pillow ImageOps module's pad()
function to shrink the image to fit on the screen and draws a black border where there is any extra space. To resize the image, the Nearest Neighbor method is used. To fill the entire screen and crop off parts of the image, then the fit()
function could be used instead.
Finally it adds the frame object to the frames list.
for frame in range(self._frame_count): image.seek(frame) # Create blank image for drawing. # Make sure to create image with mode 'RGB' for full color. frame_object = Frame(duration=self._duration) if "duration" in image.info: frame_object.duration = image.info['duration'] frame_object.image = ImageOps.pad(image.convert("RGB"), (self._width, self._height), method=Image.NEAREST, color=(0, 0, 0), centering=(0.5, 0.5)) self._frames.append(frame_object)
Now the play
function will load and play the current image. It starts off by calling the preload()
function. If there aren't any Gif images to play it will let you know.
Next it will start the looping of the animation and depending on the loop setting, it will either start counting down or continue indefinitely. If it gets down to 1, the looping will end and the function will return True
indicating the run function should automatically advance to the next image. In between each frame, it will check the value of the buttons, move the index accordingly, and return False
indicating that we already took care of changing the index and to just play the new index. The delay is taken care of using time.monotonic()
, which returns a float value of the elapsed time. We use it to get the start time and compare it to the current time. This allows us to take image transfer time into account.
def play(self): self.preload() # Check if we have loaded any files first if not self._gif_files: print("There are no Gif Images to Play") return False while True: for frame_object in self._frames: start_time = time.monotonic() self.display.image(frame_object.image) if not self.advance_button.value: self.advance() return False if not self.back_button.value: self.back() return False while time.monotonic() < (start_time + frame_object.duration / 1000): pass if self._loop == 1: return True if self._loop > 0: self._loop -= 1
Finally we have the run
function. Its job is to keep advancing through each of the images in the list and loop back to the start if we hit the end. It the buttons were used to change the image, then it should not automatically advance.
def run(self): while True: auto_advance = self.play() if auto_advance: self.advance()
Now that we have the classes all defined, we just need a little more setup and it's time to run it. First we initialize the display. The default display is the ili9341, but if you have a different display, feel free to comment out that one and uncomment the one you do have.
# Config for display baudrate (default max is 64mhz): BAUDRATE = 64000000 # Setup SPI bus using hardware SPI: spi = board.SPI() # Create the display: #disp = st7789.ST7789(spi, rotation=90 # 2.0" ST7789 #disp = st7789.ST7789(spi, height=240, y_offset=80, rotation=90 # 1.3", 1.54" ST7789 #disp = st7789.ST7789(spi, rotation=90, width=135, height=240, x_offset=53, y_offset=40, # 1.14" ST7789 #disp = hx8357.HX8357(spi, rotation=180, # 3.5" HX8357 #disp = st7735.ST7735R(spi, rotation=90, # 1.8" ST7735R #disp = st7735.ST7735R(spi, rotation=270, height=128, x_offset=2, y_offset=3, # 1.44" ST7735R #disp = st7735.ST7735R(spi, rotation=90, bgr=True, # 0.96" MiniTFT ST7735R #disp = ssd1351.SSD1351(spi, rotation=180, # 1.5" SSD1351 #disp = ssd1351.SSD1351(spi, height=96, y_offset=32, rotation=180, # 1.27" SSD1351 #disp = ssd1331.SSD1331(spi, rotation=180, # 0.96" SSD1331 disp = ili9341.ILI9341(spi, rotation=90, # 2.2", 2.4", 2.8", 3.2" ILI9341 cs=cs_pin, dc=dc_pin, rst=reset_pin, baudrate=BAUDRATE)
Next we have some code to swap the width and height parameters depending on the rotation. This is necessary because RGB_Display
prefers to draw images that take up the full screen.
if disp.rotation % 180 == 90: disp_height = disp.width # we swap height/width to rotate it to landscape! disp_width = disp.height else: disp_width = disp.width disp_height = disp.height
Finally we create the AnimatedGif object. By passing it the current folder
, it will automatically run.
gif_player = AnimatedGif(disp, width=disp_width, height=disp_height, folder=".")
# SPDX-FileCopyrightText: 2021 ladyada for Adafruit Industries # SPDX-License-Identifier: MIT """ Example to extract the frames and other parameters from an animated gif and then run the animation on the display. Usage: python3 rgb_display_pillow_animated_gif.py This example is for use on (Linux) computers that are using CPython with Adafruit Blinka to support CircuitPython libraries. CircuitPython does not support PIL/pillow (python imaging library)! Author(s): Melissa LeBlanc-Williams for Adafruit Industries Mike Mallett <[email protected]> """ import os import time import digitalio import board from PIL import Image, ImageOps import numpy # pylint: disable=unused-import from adafruit_rgb_display import ili9341 from adafruit_rgb_display import st7789 # pylint: disable=unused-import from adafruit_rgb_display import hx8357 # pylint: disable=unused-import from adafruit_rgb_display import st7735 # pylint: disable=unused-import from adafruit_rgb_display import ssd1351 # pylint: disable=unused-import from adafruit_rgb_display import ssd1331 # pylint: disable=unused-import # Change to match your display BUTTON_NEXT = board.D17 BUTTON_PREVIOUS = board.D22 # Configuration for CS and DC pins (these are PiTFT defaults): cs_pin = digitalio.DigitalInOut(board.CE0) dc_pin = digitalio.DigitalInOut(board.D25) # Set this to None on the Mini PiTFT reset_pin = digitalio.DigitalInOut(board.D24) def init_button(pin): button = digitalio.DigitalInOut(pin) button.switch_to_input() button.pull = digitalio.Pull.UP return button # pylint: disable=too-few-public-methods class Frame: def __init__(self, duration=0): self.duration = duration self.image = None # pylint: enable=too-few-public-methods class AnimatedGif: def __init__(self, display, width=None, height=None, folder=None): self._frame_count = 0 self._loop = 0 self._index = 0 self._duration = 0 self._gif_files = [] self._frames = [] if width is not None: self._width = width else: self._width = display.width if height is not None: self._height = height else: self._height = display.height self.display = display self.advance_button = init_button(BUTTON_NEXT) self.back_button = init_button(BUTTON_PREVIOUS) if folder is not None: self.load_files(folder) self.run() def advance(self): self._index = (self._index + 1) % len(self._gif_files) def back(self): self._index = (self._index - 1 + len(self._gif_files)) % len(self._gif_files) def load_files(self, folder): gif_files = [f for f in os.listdir(folder) if f.endswith(".gif")] for gif_file in gif_files: gif_file = os.path.join(folder, gif_file) image = Image.open(gif_file) # Only add animated Gifs if image.is_animated: self._gif_files.append(gif_file) print("Found", self._gif_files) if not self._gif_files: print("No Gif files found in current folder") exit() # pylint: disable=consider-using-sys-exit def preload(self): image = Image.open(self._gif_files[self._index]) print("Loading {}...".format(self._gif_files[self._index])) if "duration" in image.info: self._duration = image.info["duration"] else: self._duration = 0 if "loop" in image.info: self._loop = image.info["loop"] else: self._loop = 1 self._frame_count = image.n_frames self._frames.clear() for frame in range(self._frame_count): image.seek(frame) # Create blank image for drawing. # Make sure to create image with mode 'RGB' for full color. frame_object = Frame(duration=self._duration) if "duration" in image.info: frame_object.duration = image.info["duration"] frame_object.image = ImageOps.pad( # pylint: disable=no-member image.convert("RGB"), (self._width, self._height), method=Image.NEAREST, color=(0, 0, 0), centering=(0.5, 0.5), ) self._frames.append(frame_object) def play(self): self.preload() _prev_advance_btn_val = self.advance_button.value _prev_back_btn_val = self.back_button.value # Check if we have loaded any files first if not self._gif_files: print("There are no Gif Images loaded to Play") return False while True: for frame_object in self._frames: start_time = time.monotonic() self.display.image(frame_object.image) _cur_advance_btn_val = self.advance_button.value _cur_back_btn_val = self.back_button.value if not _cur_advance_btn_val and _prev_advance_btn_val: self.advance() return False if not _cur_back_btn_val and _prev_back_btn_val: self.back() return False _prev_back_btn_val = _cur_back_btn_val _prev_advance_btn_val = _cur_advance_btn_val while time.monotonic() < (start_time + frame_object.duration / 1000): pass if self._loop == 1: return True if self._loop > 0: self._loop -= 1 def run(self): while True: auto_advance = self.play() if auto_advance: self.advance() # Config for display baudrate (default max is 64mhz): BAUDRATE = 64000000 # Setup SPI bus using hardware SPI: spi = board.SPI() # pylint: disable=line-too-long # Create the display: # disp = st7789.ST7789(spi, rotation=90, # 2.0" ST7789 # disp = st7789.ST7789(spi, height=240, y_offset=80, rotation=180, # 1.3", 1.54" ST7789 # disp = st7789.ST7789(spi, rotation=90, width=135, height=240, x_offset=53, y_offset=40, # 1.14" ST7789 # disp = st7789.ST7789(spi, rotation=90, width=172, height=320, x_offset=34, # 1.47" ST7789 # disp = st7789.ST7789(spi, rotation=270, width=170, height=320, x_offset=35, # 1.9" ST7789 # disp = hx8357.HX8357(spi, rotation=180, # 3.5" HX8357 # disp = st7735.ST7735R(spi, rotation=90, # 1.8" ST7735R # disp = st7735.ST7735R(spi, rotation=270, height=128, x_offset=2, y_offset=3, # 1.44" ST7735R # disp = st7735.ST7735R(spi, rotation=90, bgr=True, width=80, # 0.96" MiniTFT Rev A ST7735R # disp = st7735.ST7735R(spi, rotation=90, invert=True, width=80, # 0.96" MiniTFT Rev B ST7735R # x_offset=26, y_offset=1, # disp = ssd1351.SSD1351(spi, rotation=180, # 1.5" SSD1351 # disp = ssd1351.SSD1351(spi, height=96, y_offset=32, rotation=180, # 1.27" SSD1351 # disp = ssd1331.SSD1331(spi, rotation=180, # 0.96" SSD1331 disp = ili9341.ILI9341( spi, rotation=90, # 2.2", 2.4", 2.8", 3.2" ILI9341 cs=cs_pin, dc=dc_pin, rst=reset_pin, baudrate=BAUDRATE, ) # pylint: enable=line-too-long if disp.rotation % 180 == 90: disp_height = disp.width # we swap height/width to rotate it to landscape! disp_width = disp.height else: disp_width = disp.width disp_height = disp.height gif_player = AnimatedGif(disp, width=disp_width, height=disp_height, folder=".")
Text editor powered by tinymce.