← All articles · April 30, 2026 · pixel.hjlabs.in · Powered by image2cpp

MicroPython Bitmap Display: Convert Images for ESP32/RP2040

MicroPython is the easy on-ramp to embedded development — no compile cycle, REPL on serial, batteries included. But the bitmap drawing path differs from Arduino. This post walks the MicroPython workflow: convert with image2cpp, embed as Python bytes, blit with the framebuf module on ESP32, RP2040 Pico, or any board running MicroPython.

The framebuf module: your only friend

MicroPython's built-in framebuf module is the universal pixel pusher. It owns a buffer of bytes, knows the format (mono / RGB565 / GS4 etc), and exposes blit(), fill(), text(), pixel(). Display drivers (ssd1306.py, st7789.py) inherit from FrameBuffer — so what you learn here applies to every display.

Convert with image2cpp — same as Arduino

The bytes are identical. Open pixel.hjlabs.in/converter, generate the array, but ignore the C++ wrapper — you only need the hex bytes. Use the "raw bytes" output if available, or copy the array contents and convert to Python.

From C array to Python bytes — one substitution

image2cpp gives you something like:

const unsigned char myImg [] PROGMEM = {
  0x00, 0xFF, 0x80, 0x42, /* ... */
};

In Python, that becomes:

my_img = bytearray((
  0x00, 0xFF, 0x80, 0x42,  # ... same hex values
))

Or for a slightly cleaner form:

my_img = b'\x00\xff\x80\x42'  # ... continuing

Wrap bytes as a FrameBuffer

import framebuf

# 32x32 monochrome bitmap (32*32/8 = 128 bytes)
my_img = bytearray((
  0xff, 0xff, 0xff, 0xff,  # row 0
  0x80, 0x00, 0x00, 0x01,  # row 1
  # ... 32 rows total
))

fb = framebuf.FrameBuffer(my_img, 32, 32, framebuf.MONO_HLSB)

The format constants:

ConstantMeaningimage2cpp setting
MONO_VLSB1-bit, vertical, LSB firstVertical, LSB
MONO_HLSB1-bit, horizontal, LSB first (XBM)XBM
MONO_HMSB1-bit, horizontal, MSB firstHorizontal MSB (Adafruit-compatible)
RGB56516-bit color (2 bytes/pixel)Arduino RGB565
GS2_HMSB / GS4_HMSB / GS82/4/8-bit grayscaleCustom (less common)

Display: SSD1306 on ESP32

Wire SSD1306 to ESP32 standard I2C (SDA=GPIO21, SCL=GPIO22). Install the ssd1306.py driver via mpremote mip install ssd1306 or copy from MicroPython's repo to the device.

from machine import Pin, I2C
import ssd1306
import framebuf

i2c = I2C(0, sda=Pin(21), scl=Pin(22))
oled = ssd1306.SSD1306_I2C(128, 64, i2c)

logo_bytes = bytearray((
  # 128x64 = 1024 bytes from image2cpp
))
logo = framebuf.FrameBuffer(logo_bytes, 128, 64, framebuf.MONO_HMSB)

oled.fill(0)
oled.blit(logo, 0, 0)
oled.show()

Display: ST7789 on RP2040 Pico

For color images on a Pico, use Russ Hughes's excellent st7789_mpy driver (built into MicroPython firmware for many displays). Convert the image as RGB565 in image2cpp, then:

from machine import Pin, SPI
import st7789

spi = SPI(0, baudrate=40_000_000, sck=Pin(2), mosi=Pin(3))
tft = st7789.ST7789(spi, 240, 320,
  reset=Pin(15, Pin.OUT), cs=Pin(5, Pin.OUT),
  dc=Pin(4, Pin.OUT))
tft.init()

# RGB565 bytes for 64x64 sprite (8192 bytes)
sprite = bytearray((
  # ...
))
tft.blit_buffer(sprite, 100, 80, 64, 64)

The blit_buffer method on st7789_mpy takes raw RGB565 bytes directly — the same bytes image2cpp emits.

Loading from filesystem instead of inlining

Hardcoding 8KB of hex into a .py file works but is ugly. Better: save the bytes as a binary file and load at runtime:

# conversion (one-time, on PC)
# Run image2cpp, copy hex array, then in Python:
import struct
hex_values = [0x00, 0xFF, /* ... */]
with open('logo.bin', 'wb') as f:
    f.write(bytes(hex_values))
# Upload logo.bin to the board with mpremote
# at runtime, on the board
with open('logo.bin', 'rb') as f:
    logo_bytes = f.read()
fb = framebuf.FrameBuffer(bytearray(logo_bytes), 128, 64, framebuf.MONO_HMSB)
oled.blit(fb, 0, 0)
oled.show()

Drawing GIF / animation frames

For sprite animation, store one bytes object per frame, advance with time.ticks_ms():

import time
frames = [frame0_bytes, frame1_bytes, frame2_bytes]
fbs = [framebuf.FrameBuffer(bytearray(f), 32, 32, framebuf.MONO_HMSB) for f in frames]

i = 0
last = time.ticks_ms()
while True:
    if time.ticks_diff(time.ticks_ms(), last) > 100:
        oled.fill(0)
        oled.blit(fbs[i], 48, 16)
        oled.show()
        i = (i + 1) % len(fbs)
        last = time.ticks_ms()

Memory constraints on RP2040 / ESP32

MicroPython on RP2040 leaves ~150KB heap free. ESP32 has ~100KB. A 320x240 RGB565 image is 153KB — that won't fit in heap on either. Strategies:

Common pitfalls

Bottom line

MicroPython's framebuf consumes the same bytes as Adafruit_GFX — just wrap them in a FrameBuffer with the matching format constant. image2cpp generates the bytes, you copy-paste, you ship. Read our Arduino OLED tutorial for the C++ side — concepts transfer 1:1.

Streaming JSON from your MicroPython device? fmt.hjlabs.in formats and validates JSON in the browser.

Try image2cpp now — free, in browser

Drop in a PNG, JPG or BMP. Get a paste-ready C++ array for Arduino, ESP32 or RP2040. 100% client-side. Learn more about image2cpp or jump straight to the tool.

Open the converter →

More from the blog