← All articles · May 6, 2026 · pixel.hjlabs.in · Powered by image2cpp

ESP-IDF LVGL Image Conversion: PNG to C Array for LVGL v9

LVGL is the de-facto graphics framework for ESP-IDF, RP2040, STM32, and any MCU big enough to run a real UI toolkit. It has its own image format, its own struct (lv_img_dsc_t), and its own online converter. This post explains where image2cpp fits, where it doesn't, and how to convert PNGs for LVGL v9 in an ESP-IDF project.

The LVGL image descriptor

An LVGL image is not a raw byte array — it's an lv_img_dsc_t struct that wraps the bytes plus metadata:

typedef struct {
    lv_image_header_t header;  // width, height, color format, stride
    uint32_t data_size;        // total bytes in data
    const uint8_t *data;       // pointer to pixel data
    const void *reserved;
} lv_image_dsc_t;

You can't just paste image2cpp output and pass it to lv_image_set_src() — the API expects the full struct. But the bytes inside are the same RGB565 / RGB888 / 1-bit data image2cpp emits.

Color formats LVGL v9 understands

LVGL formatimage2cpp equivalentBytes/pixel
LV_COLOR_FORMAT_L88-bit grayscale1
LV_COLOR_FORMAT_I8 (indexed)(palette + indices, manual)1 + palette
LV_COLOR_FORMAT_RGB56516-bit RGB565 (Arduino)2
LV_COLOR_FORMAT_RGB565A8RGB565 + 1-byte alpha3
LV_COLOR_FORMAT_ARGB8888(no direct image2cpp output)4
LV_COLOR_FORMAT_RGB88824-bit RGB3
LV_COLOR_FORMAT_A1 / A2 / A4 / A81/2/4/8-bit alpha mask0.125 / 0.25 / 0.5 / 1

Path 1: the LVGL online converter (easy)

LVGL ships an official converter at lvgl.io/tools/imageconverter. Drop in a PNG, pick the LVGL version (v9), pick the color format, get a .c file with the full lv_img_dsc_t declaration ready to #include. This is the path of least resistance for LVGL projects.

Path 2: image2cpp + manual struct wrap

If you want consistency across LVGL and non-LVGL parts of your codebase, use image2cpp for the bytes and wrap manually:

#include "lvgl.h"

// 1. paste image2cpp RGB565 output as plain bytes
static const uint8_t my_img_data [] LV_ATTRIBUTE_LARGE_CONST = {
    0x00, 0x00, 0xFF, 0xFF, /* ... w*h*2 bytes ... */
};

// 2. wrap in an LVGL descriptor
const lv_image_dsc_t my_img = {
    .header = {
        .magic = LV_IMAGE_HEADER_MAGIC,
        .cf = LV_COLOR_FORMAT_RGB565,
        .w = 64,
        .h = 64,
        .stride = 64 * 2
    },
    .data_size = 64 * 64 * 2,
    .data = my_img_data
};

// 3. show on screen
void create_ui(lv_obj_t *parent) {
    lv_obj_t *img = lv_image_create(parent);
    lv_image_set_src(img, &my_img);
}

The LV_ATTRIBUTE_LARGE_CONST macro

LVGL uses LV_ATTRIBUTE_LARGE_CONST to place image data in flash. On ESP-IDF this expands to attributes that ensure flash placement; on AVR it expands to PROGMEM. Always use it for image arrays in LVGL code — portable and correct.

ESP-IDF integration

In main/CMakeLists.txt, add the image source file:

idf_component_register(
    SRCS "main.c" "ui.c" "images/my_img.c"
    INCLUDE_DIRS "."
    REQUIRES "lvgl"
)

For larger asset libraries, glob:

file(GLOB IMG_SOURCES "images/*.c")
idf_component_register(
    SRCS "main.c" "ui.c" ${IMG_SOURCES}
    ...
)

Color format and CONFIG_LV_COLOR_DEPTH

Your LVGL build is configured for one color depth (typically 16-bit on MCUs, 32-bit on bigger SoCs). Image color format must be compatible. Check CONFIG_LV_COLOR_DEPTH in menuconfig:

Mixing depths works but LVGL converts at draw time — slower. Match the format to your build for best performance.

Loading from filesystem (LittleFS / SPIFFS)

For large images, embed in flash via the LVGL filesystem layer instead of as a C array. Save the binary as e.g. logo.bin in your spiffs/ partition, then:

lv_image_set_src(img, "S:/spiffs/logo.bin");

The "S:" prefix routes to the SPIFFS driver. Saves compile time, allows OTA image updates without reflashing firmware, but adds 5-50ms decode latency.

Built-in PNG / JPG decoders

LVGL v9 ships with optional libraries for runtime PNG (lv_libs_png) and JPG (lv_libs_tjpgd) decoding. Enable in menuconfig under "LVGL configuration → 3rd-party libraries". Then you can:

lv_image_set_src(img, "S:/spiffs/photo.png");

Tradeoff: decode time (50-300ms for typical images), heap usage during decode (~3-5x image size), but huge flash savings if you have many photos.

Image transforms (rotate, scale, recolor)

LVGL can transform images at draw time without re-encoding:

lv_image_set_rotation(img, 450);   // 45 degrees (in 0.1 deg units)
lv_image_set_scale(img, 384);      // 1.5x (256 = 1x)
lv_image_set_recolor(img, lv_color_hex(0xff0000));  // tint red

These cost CPU at draw time but no extra flash. For real-time animation, expect a frame-rate hit.

Common pitfalls

image2cpp vs LVGL converter: when to use which

Use image2cpp whenUse LVGL converter when
Cross-platform (Arduino + LVGL + framebuf)LVGL-only project
You want raw bytes for any consumerYou want zero-config struct output
Custom dithering / thresholdStandard color formats
Faster iteration (in-browser)Single source of truth for LVGL assets

Bottom line

For pure LVGL projects, the official LVGL converter is fastest. For cross-platform codebases or anywhere you want full control, generate raw bytes with image2cpp and wrap them in lv_img_dsc_t manually. Both produce identical pixel output. Read our Arduino tutorial for the fundamentals.

For LVGL UI screenshot tooling, og.hjlabs.in handles your project's GitHub README graphics; fmt.hjlabs.in is handy for debugging JSON config dumps from ESP-IDF.

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