wolfgang ziegler


„make stuff and blog about it“

Making an E-Paper Picture Frame

November 19, 2023

The Hardware

More than a year ago, I bought this 7 color e-ink display, tested it and somehow forgot about it again. Then just recently, I remembered about it and decided to make it into a digital picture frame that would display our family photos on my working desk.

I also have a stash of unused Raspberry Pis lying around, so I put one of those into use and connected it to the display.

An unused picture frame was also readily available, so it was mostly a matter of putting the existing pieces together.

The backboard of the frame needed a cut-out to make room for the Raspberry Pi Zero, but that was all the "modifications" I had to make. Even the passe-partout matched the dimensions of the screen already.

The back of the frame

Part List

Here's what I used.

  • Raspberry Pi Zero: Actually any Raspberry Pi will work. The Zero is great however, as it does not take up a lot of space.
  • Pimoroni ePaper HAT: This is the exact model that I used.
  • Power supply: Micro USB, 5V, 2A.
  • Picture Frame: e.g. IKEA Ribba

The Software

In the past I blogged about setting up a Raspberry Pi for headless operation. Since then, things have become even easier and all these settings (e.g. ssh) are just a click away when using the Raspberry Pi Imager tool.

The Raspberry Pi Imager tool

The actual actual code driving the display is a small Python application using Pimoroni's inky library.

A global instance of inky is used for that.

inky = auto(ask_user=True, verbose=True)

The main application then simply calls the helper function display_next_image and sleeps for 15 minutes. This means, each 15 minutes we update the image that gets displayed.

while True:
  display_next_image()
  time.sleep(timedelta(minutes=15).total_seconds())

The implementation of display_next_image looks like this:

def display_next_image():
  image = get_next_image()
  image = rotate_image(image)
  image = scale_image(image)
  inky.set_image(image, saturation=0.5)
  inky.show()

The first function that's called is get_next_image. Here's where your mileage may vary and you have to decide where to retrieve the image from. Whether this image is already on a folder on the Raspberry Pi's SD card or gets downloaded from somewhere else, does not actually matter. The only important thing is that the Image type from Python's Pillow library is used. The methods below rely on that.

Next, rotate_image makes sure that the image is rotated correctly (by looking at its EXIF tags).

def rotate_image(image):
  # Check if the image has EXIF data
  if hasattr(image, '_getexif'):
      exif = image._getexif()
      if exif is not None:
          # Look for the EXIF orientation tag
          for tag, orientation in ExifTags.TAGS.items():
              if orientation == 'Orientation':
                  break

          # Check if the orientation tag exists in the EXIF data
          if tag in exif:
              # Get the actual orientation value
              orientation_value = exif[tag]

              # Determine if rotation is needed based on the orientation value
              if orientation_value == 1:
                  # No rotation needed (normal orientation)
                  pass
              elif orientation_value == 3:
                  # Rotate 180 degrees
                  image = image.rotate(180, expand=True)
              elif orientation_value == 6:
                  # Rotate 270 degrees
                  image = image.rotate(270, expand=True)
              elif orientation_value == 8:
                  # Rotate 90 degrees
                  image = image.rotate(90, expand=True)
  return image

Then, the image is scaled to match the dimensions of the e-paper display.

def scale_image(image):
  aspect_ratio = image.width / image.height
  target_width, target_height = inky.resolution
  if aspect_ratio > (target_width / target_height):
    new_width = target_width
    new_height = int(target_width / aspect_ratio)
  else:
    new_height = target_height
    new_width = int(target_height * aspect_ratio)
  image = image.resize((new_width, new_height), Image.ANTIALIAS)
  padded_image = Image.new('RGB', (target_width, target_height), (255, 255, 255))
  left_padding = (target_width - new_width) // 2
  top_padding = (target_height - new_height) // 2
  padded_image.paste(image, (left_padding, top_padding))
  return padded_image;

And finally, the Image gets displayed by callinginky.set_image() and inky.show().

Automatically run the script

To have this script run on startup automatically, I added one line to /etc/rc.local just before exit 0.

...
python3 /home/pi/dev/framepi/main.py &
exit 0

The Finished Picture Frame

I must say I am really happy with the result. This picture frame is running flawlessly for weeks now and the display quality is much better than I had expected.

The finished picture frame

The update interval of 15 minutes also turned out to be a good choice for me. It's often enough to see some variation but not too often as the update animations would be a distraction.