LVGL Image Converter
Turn a PNG, JPG, BMP, GIF or WebP into an LVGL C array or a .bin file. LVGL v9 and v8 color formats, palette quantisation, dithering, a live preview and a flash cost readout. Everything runs offline in your browser, nothing is uploaded.
Switching the version switches the format list too. v9 uses explicit formats (LV_COLOR_FORMAT_...), v8 uses the older names that depend on the compile time color depth (LV_IMG_CF_...).
On v8 it is this setting, not the format name, that decides how many bytes a pixel takes. Pick the value your project is built with. The generated C file carries an #if LV_COLOR_DEPTH check, so building it against the wrong depth stops with a compile error instead of rendering garbage on the panel.
Sanitised into a valid C identifier. The array becomes my_image_map and the descriptor my_image.
No image selected yet.
Add the .c file to your project and compile it. Copy the .bin file to an SD card or a LittleFS partition and LVGL reads it at runtime.
Pick an image and the usage snippet appears here.
What Is an LVGL Image Descriptor?
LVGL never takes a bare pixel array. It takes a small struct that describes one: lv_image_dsc_t in LVGL v9, lv_img_dsc_t in LVGL v8. That struct carries the color format, the width, the height, the data size and the address of the pixel array. The pointer you pass to lv_image_set_src() is the address of the struct, not of the pixels.
This LVGL image converter decodes your file in the browser, resamples it to the size you ask for, packs the bytes for the color format you pick and writes both the uint8_t array and the descriptor. From the same data it can also emit the LVGL binary file that the filesystem workflow expects.
LVGL v8 vs v9: What Changed in the Image API
Most LVGL examples on the web were written for v8 and will not compile as-is against v9. The differences fall into three groups.
1. Names
v9 dropped the img abbreviation in favour of image:
| LVGL v8 | LVGL v9 |
|---|---|
| lv_img_dsc_t | lv_image_dsc_t |
| lv_img_create() | lv_image_create() |
| lv_img_set_src() | lv_image_set_src() |
| LV_IMG_DECLARE() | LV_IMAGE_DECLARE() |
| lv_scr_act() | lv_screen_active() |
2. Color formats became explicit
In v8, LV_IMG_CF_TRUE_COLOR has no fixed meaning on its own. What it means is decided by LV_COLOR_DEPTH in lv_conf.h: two bytes per pixel in a 16 bit build, four in a 32 bit build. Drop the same array into a project configured differently and it is silently reinterpreted, which is where a large share of "my LVGL image is garbage" reports came from.
v9 removed that ambiguity. LV_COLOR_FORMAT_RGB565 is exactly two bytes per pixel in every build and LV_COLOR_FORMAT_ARGB8888 is exactly four. Color depth now only concerns the display buffer, not the images.
That is why picking v8 in this converter also asks you for LV_COLOR_DEPTH and puts an #if LV_COLOR_DEPTH check in front of the array. An array compiled at the wrong depth draws garbage without complaining, so the guard turns that into a build error you cannot miss.
3. The header struct
v9 added two fields. magic must hold LV_IMAGE_HEADER_MAGIC; it is LVGL's "is this really an image" check and leaving it out means nothing gets drawn. stride is the length of one row in bytes, not pixels, so an RGB565 image needs twice its width. In exchange, the v8 always_zero and reserved fields are gone.
/* LVGL v9 */
const lv_image_dsc_t my_image = {
.header.magic = LV_IMAGE_HEADER_MAGIC,
.header.cf = LV_COLOR_FORMAT_RGB565,
.header.w = 240,
.header.h = 240,
.header.stride = 480, /* bytes, not pixels */
.data_size = sizeof(my_image_map),
.data = my_image_map,
};
/* LVGL v8 */
const lv_img_dsc_t my_image = {
.header.cf = LV_IMG_CF_TRUE_COLOR,
.header.always_zero = 0,
.header.reserved = 0,
.header.w = 240,
.header.h = 240,
.data_size = sizeof(my_image_map),
.data = my_image_map,
};Choosing a Color Format
The format you choose is a flash budget decision. The table below is the cost of one 240x240 image in each v9 format. Since a typical ESP32 app partition is around 1.3 MB, these numbers matter quickly.
| Format (v9) | Bytes / pixel | Alpha | 240x240 cost | Typical use |
|---|---|---|---|---|
| LV_COLOR_FORMAT_RGB565 | 2 | none | 115,200 B (112.5 KB) | General purpose, native TFT format |
| LV_COLOR_FORMAT_RGB565A8 | 3 | 8 bit | 172,800 B (168.8 KB) | Icons and logos with soft edges |
| LV_COLOR_FORMAT_RGB888 | 3 | none | 172,800 B (168.8 KB) | 24 bit panels, photographs |
| LV_COLOR_FORMAT_ARGB8888 | 4 | 8 bit | 230,400 B (225 KB) | Full quality on boards with room |
| LV_COLOR_FORMAT_XRGB8888 | 4 | none | 230,400 B (225 KB) | 32 bit aligned buffers |
| LV_COLOR_FORMAT_I8 | 1 + palette | per palette entry | 58,624 B (57.3 KB) | Flat artwork with few colors |
| LV_COLOR_FORMAT_A8 | 1 | alpha only | 57,600 B (56.3 KB) | Single color mask tinted by the style |
| LV_COLOR_FORMAT_L8 | 1 | none | 57,600 B (56.3 KB) | Greyscale artwork |
On the v8 side, LV_IMG_CF_TRUE_COLOR costs the same as RGB565 in a 16 bit build and LV_IMG_CF_TRUE_COLOR_ALPHA costs three bytes per pixel. LV_IMG_CF_INDEXED_4BIT is capped at 16 colors but drops to half a byte per pixel, which still makes it the most efficient choice for icon sets and flat UI artwork.
Indexed formats and dithering
An indexed image stores its palette in front of the pixel data. Each entry is four bytes, laid out in memory as blue, green, red, alpha. This converter builds the palette with median cut, so the colors follow the actual distribution of your image instead of a fixed cube. If the source has transparent pixels, one palette slot is reserved for a fully transparent entry.
Reducing a gradient to 16 colors always produces banding. The Floyd-Steinberg dithering option scatters that error into neighbouring pixels and breaks the bands up. On flat icons dithering only adds noise, so leave it off there.
C Array or Binary File?
The C array is compiled into your firmware. It needs no filesystem, has no load latency and cannot go missing. The cost is that every image adds to the binary and changing one means a rebuild and a reflash. For icons, button artwork and logos that is the right trade.
The .bin file is copied to an SD card, SPIFFS or LittleFS and read at runtime. For large images, images that change often, or images the end user supplies, it is the only sensible option. The file starts with LVGL's header struct: 12 bytes on v9 (magic, color format, flags, width, height, stride, reserved) and 4 bytes on v8 (color format, width and height packed into a single 32 bit word), immediately followed by the pixel data.
Loading an LVGL image from an SD card
Enable a filesystem driver in lv_conf.h first, then use its drive letter. An LVGL path always begins with a letter and a colon:
/* lv_conf.h */ #define LV_USE_FS_STDIO 1 #define LV_FS_STDIO_LETTER 'A' /* v9 */ lv_obj_t * img = lv_image_create(lv_screen_active()); lv_image_set_src(img, "A:/icons/logo.bin"); /* v8, with LV_FS_POSIX_LETTER 'S' */ lv_obj_t * img = lv_img_create(lv_scr_act()); lv_img_set_src(img, "S:/icons/logo.bin");
A wrong path or an unconfigured drive letter does not raise an error, it simply draws nothing. When you are hunting for the cause, set LV_LOG_LEVEL to LV_LOG_LEVEL_WARN first; LVGL logs the failed open.
Common Problems
The image does not show at all
On v9 the usual culprit is a missing .header.magic. It is easy to lose that line when porting an old v8 array by hand, and LVGL then rejects the descriptor without drawing anything. The second cause is a wrong stride: write the row length in pixels instead of bytes and the drawing runs off the edge after the first row. The third is a parent object with zero size, so try lv_obj_center() before assuming the data is bad.
Wrong colors
Recognisable shapes with wrong colors is a byte order problem almost every time. Tick the byte order box above and regenerate. Red and blue trading places is something else: the panel is in BGR mode, and that has to be fixed in the display driver rather than in the image.
Transparent areas come out black
You picked a format without an alpha channel. RGB565 stores no transparency, so transparent pixels are composited onto the background color. For real transparency use RGB565A8 or ARGB8888 on v9, or TRUE_COLOR_ALPHA on v8. As an alternative, the v8 TRUE_COLOR_CHROMA_KEYED format paints transparent pixels pure green (#00FF00) and LVGL skips that color while drawing.
Out of memory
If the build stops with "region `FLASH' overflowed", the image does not fit in the firmware. Shrink the size first, then step the format down: RGB565 is a quarter of ARGB8888, and INDEXED_4BIT is a quarter of RGB565 again. If that is still not enough, move the image to the filesystem as a .bin so it lives on the card instead of in flash.
Frequently Asked Questions
What is an LVGL image descriptor?
LVGL does not take a bare pixel array. It takes a small struct that describes the array: lv_image_dsc_t in v9, lv_img_dsc_t in v8. The struct holds the color format, width, height, stride, data size and the address of the pixel array. What you hand to lv_image_set_src is the address of that struct, not the pixels themselves.
What changed in the LVGL image API between v8 and v9?
Three things. Naming: lv_img_dsc_t became lv_image_dsc_t, lv_img_set_src became lv_image_set_src and LV_IMG_DECLARE became LV_IMAGE_DECLARE. Color formats: the old LV_IMG_CF_TRUE_COLOR names depended on the compile time LV_COLOR_DEPTH, while v9 uses explicit formats such as LV_COLOR_FORMAT_RGB565 and LV_COLOR_FORMAT_ARGB8888 that mean the same thing in every build. Header layout: v9 added a magic field that must equal LV_IMAGE_HEADER_MAGIC and a stride field measured in bytes, and dropped the v8 always_zero and reserved fields.
Which LVGL color format should I pick?
If you do not need transparency, pick RGB565: 2 bytes per pixel and the native format of nearly every TFT panel. For an icon or logo with soft edges use RGB565A8 on v9 or TRUE_COLOR_ALPHA on v8, both 3 bytes per pixel. For flat artwork with few colors the indexed formats (I8 on v9, INDEXED_4BIT on v8) cut the size by half or more. For a single color mask that is tinted by the style, A8 or ALPHA_1BIT is the cheapest of all.
Should I use the C array or the LVGL binary file?
The C array compiles straight into flash, needs no filesystem and can never go missing, which makes it the right choice for icons and small artwork. The .bin file is copied to an SD card or to a SPIFFS or LittleFS partition and read at runtime, which is the only sensible option for large images, images that change often, or images the user supplies. The binary file starts with the LVGL header: 12 bytes on v9 and 4 bytes on v8, followed by the raw pixel data.
My LVGL image shows nothing, what is wrong?
Three causes cover most cases. On v9, a missing .header.magic = LV_IMAGE_HEADER_MAGIC makes LVGL treat the descriptor as invalid and draw nothing at all. Also on v9, a wrong stride: it is the length of one row in bytes, not pixels, so RGB565 needs twice the width. Finally the path: when you load a .bin the drive letter (A: or S:) has to be configured in lv_conf.h, otherwise lv_image_set_src fails silently.
My colors are wrong or inverted, how do I fix it?
If the shapes are recognisable but the colors are off, it is almost always byte order. LVGL writes the low byte of an RGB565 pixel first, while some SPI drivers and DMA paths expect the opposite. Tick the byte order box in this tool and regenerate the array. Red and blue swapping is a different problem: the panel is running in BGR mode and that has to be fixed in the display driver, not in the image.
What is LV_COLOR_DEPTH and why does it change the array?
On LVGL v8 the number of bytes a pixel occupies is decided by LV_COLOR_DEPTH in lv_conf.h rather than by the format name: 16 gives RGB565 in two bytes, 32 gives four bytes in blue, green, red, alpha order, and 8 (or 1) gives RGB332 in a single byte. A TRUE_COLOR array is tied to that layout, so picking v8 in this tool also means picking the depth your project is built with. The generated file carries an #if LV_COLOR_DEPTH check, so building it at another depth stops with "This image was generated for LV_COLOR_DEPTH ...". That is deliberate: a build error is far better than an image that compiles cleanly and then renders garbage. LVGL v9 does not have this problem at all, because its color formats are independent of the build configuration.
Is my image uploaded anywhere?
No. The file is read into your browser's memory, processed on a canvas and turned into a C array locally. No network request is made and nothing is sent to or stored on a server. Once the page has loaded you can go offline and it keeps working.