ElectronicZoologyfield notes from the garage
Display • ESP32

How to display custom
images on the GC9A01

Display: GC9A01 - 240x240 round IPS TFT
Board: ESP32 Dev Board (38-pin)
Tools: Python + Pillow, Arduino IDE
GC9A01 Display Series — Step 2 of 3 — Your first image
✓ Confirmed Working
This guide assumes your GC9A01 is already wired and working. If not, start with the How to wire the GC9A01 round display with ESP32 →

What You Need

Software

Install on Arch Linux:

Copy and paste into terminal

sudo pacman -S python python-pillow

Other systems:

Copy and paste into terminal

pip3 install Pillow

How It Works

The GC9A01 speaks RGB565 - each pixel is a 16-bit number encoding red (5 bits), green (6 bits), and blue (5 bits). Standard image formats like PNG and JPEG can't be sent directly - they need to be decoded and converted first.

The process is: resize your image to 240x240, run a Python script to convert every pixel to its RGB565 value, and write the result out as a C header file. The sketch includes that header and blits the pixel array straight to the display in one call.

Why not decode on the device? Decoding JPEG or PNG on an ESP32 is possible but slow and complex. Pre-converting to raw RGB565 on your computer means the sketch is trivial - just a memcpy to the display. A 240x240 image is 115KB in flash, well within the ESP32's 4MB.

Prepare Your Image

The converter will resize any image to 240x240 automatically, but you'll get better results doing it yourself first - it lets you control how it's cropped and centred.

In GIMP

  1. Open your image
  2. Image → Scale Image - scale so the shortest side is 240px
  3. Image → Canvas Size - set to 240x240 and centre the layer
  4. Image → Flatten Image - merges transparency to a solid background
  5. File → Export As - save as .png
Transparency The display has no concept of transparent pixels - every pixel must have a colour. Flatten your image before exporting so transparent areas become a solid colour (black by default in GIMP, or set the background colour first).
Round display, square image The GC9A01 panel is physically round but the pixel grid is 240x240 square. The display hardware clips the corners - anything outside the circle just isn't visible. You don't need to manually cut your image into a circle shape.

Convert to RGB565

Save this script as image_to_rgb565.py anywhere on your computer, then run it once per image. How to save a script →

#!/usr/bin/env python3
"""
Convert an image to an RGB565 C header for Arduino_GFX_Library.

Usage:
    python3 image_to_rgb565.py <image> <varname>

Example:
    python3 image_to_rgb565.py myface.png myface

Outputs: myface.h in the same folder as the image.
The image will be resized to 240x240 automatically.
"""

import sys
from pathlib import Path
from PIL import Image

def convert(img_path, varname):
    img = Image.open(img_path).convert("RGB")
    img = img.resize((240, 240), Image.LANCZOS)
    raw = img.tobytes()
    pixels = [(raw[i], raw[i+1], raw[i+2]) for i in range(0, len(raw), 3)]

    out_path = Path(img_path).parent / f"{varname}.h"
    with open(out_path, "w") as f:
        f.write(f"// {Path(img_path).name} -> 240x240 RGB565\n")
        f.write(f"#pragma once\n")
        f.write(f"const uint16_t {varname}[57600] = {{\n  ")

        for i, (r, g, b) in enumerate(pixels):
            val = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
            f.write(f"0x{val:04X}")
            if i < len(pixels) - 1:
                f.write(", ")
            if (i + 1) % 12 == 0:
                f.write("\n  ")

        f.write("\n};\n")

    print(f"Done -> {out_path}  (240x240, {240*240*2} bytes)")

if __name__ == "__main__":
    if len(sys.argv) != 3:
        print("Usage: python3 image_to_rgb565.py <image> <varname>")
        sys.exit(1)
    convert(sys.argv[1], sys.argv[2])

Run it

Replace the highlighted parts with your own values:

Copy and paste into terminal

python3 image_to_rgb565.py myface.png myface

python3 image_to_rgb565.py myface.png myface

e.g. python3 image_to_rgb565.py ~/Downloads/thomasFaces/thomas1.png thomas1

This produces myface.h in the same folder as the image. Move it into your Arduino sketch folder alongside the .ino file.

  • myface.png - path to your image, e.g. ~/Downloads/thomasFaces/thomas1.png
  • myface - a name you choose for this image - no spaces, no special characters
What does the name do?

Say you have an image in your photos folder called thomas1.png. You give the script thomas1 as the name. It creates a file called thomas1.h next to your image, and inside that file all the pixel data is stored in a variable also called thomas1. That variable name is what you put into the sketch - so the sketch knows what to draw.

The script uses the name for two things:

  1. The output filename - thomas1 becomes thomas1.h
  2. The variable name declared inside that file - const uint16_t thomas1[57600] = {...}

Call it whatever makes sense for your image. No spaces, no special characters.

The Sketch

Place myface.h in the same folder as the sketch. The top two lines are the only ones you change - update both myface references to match your varname.

Arduino IDE showing the two lines to change - the include and define at the top of the sketch
Make sure the .h file appears as a tab in Arduino IDE, and the name in #include and #define both match exactly.
/*
 * We stand on the shoulders of giants when we build
 * with knowledge gained from others' efforts.
 * That doesn't make us giants. Be humble.
 * Create with care. Open source is the way.
 *
 * GC9A01 Custom Image Display
 * ---------------------------
 * Displays a custom RGB565 image on a GC9A01 240x240
 * round IPS TFT display via SPI using Arduino_GFX_Library.
 * Image is pre-converted from PNG using image_to_rgb565.py.
 *
 * Board:   ESP32 Dev Board (38-pin)
 * Library: Arduino_GFX_Library by Moon On Our Nation
 *
 * Wiring:
 *   VCC -> 3.3V or 5V   GND -> GND
 *   SCL -> GPIO 18       SDA -> GPIO 23
 *   DC  -> GPIO 27       CS  -> GPIO 5
 *   RST -> GPIO 4
 *
 * Open source - MIT Licence
 * Full guide:
 * https://electroniczoology.com/guides/how-to-display-images-gc9a01-esp32
 *
 * Electronic Zoology - field notes from the garage
 */

#include <Arduino_GFX_Library.h>
#include "myface.h"   // <- change myface to your varname
#define IMAGE myface  // <- change myface to match

#define TFT_DC  27
#define TFT_CS   5
#define TFT_RST  4

Arduino_DataBus *bus = new Arduino_HWSPI(TFT_DC, TFT_CS);
Arduino_GFX *gfx = new Arduino_GC9A01(bus, TFT_RST, 0, true);

void setup() {
  Serial.begin(115200);
  gfx->begin();
  gfx->fillScreen(0x0000);
  gfx->draw16bitRGBBitmap(0, 0, (uint16_t*)IMAGE, 240, 240);
}

void loop() {}
Source image - Thomas face PNG
Source image
Result on the GC9A01 round display
On the display
Want to cycle through multiple images? See How to cycle images on the GC9A01 with batch conversion →

Troubleshooting

Wrong colours - image looks like a negative or has a red/blue swap

  • RGB565 byte order matters - try swapping the bytes in the converter
  • In image_to_rgb565.py, change the val line to: val = ((val & 0xFF) << 8) | ((val >> 8) & 0xFF)
  • This byte-swaps each pixel to big-endian order which some display configs expect

Compile error - array too large / out of memory

  • On ESP32, const arrays are automatically stored in flash by the compiler - the 115KB array will not overflow RAM - see How ESP32 RAM works → to learn more
  • If you see this error, confirm your board is set to an ESP32 variant in Arduino IDE - AVR boards (Uno, Nano) do not have this behaviour and would need PROGMEM

Image draws but is shifted, tiled, or has garbage at edges

  • The image must be exactly 240x240 - check with python3 -c "from PIL import Image; print(Image.open('myface.png').size)"
  • Rerun the converter after confirming the source image dimensions

Transparent areas showing as black or wrong colour

  • Flatten the image in GIMP before exporting - Image → Flatten Image
  • Set your desired background colour before flattening
  • The converter uses .convert("RGB") which drops the alpha channel - transparent pixels become black unless you flatten first

Display is blank after flashing

  • Check the #include "myface.h" filename matches exactly - Arduino IDE is case sensitive on Linux
  • Confirm the .h file is in the same folder as the .ino file
  • Open Serial Monitor - if nothing appears, the sketch may have crashed on boot
Have a smaller round display? See How to use the GC9D01 160x160 round TFT with ESP32 - a different chip with a different driver.
Need a simpler display for status readouts? See How to use the SSD1306 OLED display with ESP32 - two wires, no colour, very low power.