Images (lv_image)

An image can be a file or a variable which stores the bitmap itself and some metadata.

Store images

You can store images in two places

  • as a variable in internal memory (RAM or ROM)

  • as a file

Variables

Images stored internally in a variable are composed mainly of an lv_image_dsc_t structure with the following fields:

  • header:

    • cf: Color format. See below

    • w: width in pixels (<= 2048)

    • h: height in pixels (<= 2048)

    • always zero: 3 bits which need to be always zero

    • reserved: reserved for future use

  • data: pointer to an array where the image itself is stored

  • data_size: length of data in bytes

These are usually stored within a project as C files. They are linked into the resulting executable like any other constant data.

Files

To deal with files you need to add a storage Drive to LVGL. In short, a Drive is a collection of functions (open, read, close, etc.) registered in LVGL to make file operations. You can add an interface to a standard file system (FAT32 on SD card) or you create your simple file system to read data from an SPI Flash memory. In every case, a Drive is just an abstraction to read and/or write data to memory. See the File system section to learn more.

Images stored as files are not linked into the resulting executable, and must be read into RAM before being drawn. As a result, they are not as resource-friendly as images linked at compile time. However, they are easier to replace without needing to rebuild the main program.

Color formats

Various built-in color formats are supported:

The bytes of LV_COLOR_FORMAT_NATIVE images are stored in the following order.

You can store images in a Raw format to indicate that it's not encoded with one of the built-in color formats and an external Image decoder needs to be used to decode the image.

Add and use images

You can add images to LVGL in two ways:

  • using the online converter

  • manually create images

Online converter

The online Image converter is available here: https://lvgl.io/tools/imageconverter

Adding an image to LVGL via the online converter is easy.

  1. You need to select a BMP, PNG or JPG image first.

  2. Give the image a name that will be used within LVGL.

  3. Select the Color format.

  4. Select the type of image you want. Choosing a binary will generate a .bin file that must be stored separately and read using the file support. Choosing a variable will generate a standard C file that can be linked into your project.

  5. Hit the Convert button. Once the conversion is finished, your browser will automatically download the resulting file.

In the generated C arrays (variables), bitmaps for all the color depths (1, 8, 16 or 32) are included in the C file, but only the color depth that matches LV_COLOR_DEPTH in lv_conf.h will actually be linked into the resulting executable.

In the case of binary files, you need to specify the color format you want:

  • RGB332 for 8-bit color depth

  • RGB565 for 16-bit color depth

  • RGB565 Swap for 16-bit color depth (two bytes are swapped)

  • RGB888 for 32-bit color depth

Manually create an image

If you are generating an image at run-time, you can craft an image variable to display it using LVGL. For example:

uint8_t my_img_data[] = {0x00, 0x01, 0x02, ...};

static lv_image_dsc_t my_img_dsc = {
    .header.always_zero = 0,
    .header.w = 80,
    .header.h = 60,
    .data_size = 80 * 60 * LV_COLOR_DEPTH / 8,
    .header.cf = LV_COLOR_FORMAT_NATIVE,          /* Set the color format */
    .data = my_img_data,
};

Another (possibly simpler) option to create and display an image at run-time is to use the Canvas Widget.

Use images

The simplest way to use an image in LVGL is to display it with an Image (lv_image) Widget:

lv_obj_t * icon = lv_image_create(lv_screen_active(), NULL);

/* From variable */
lv_image_set_src(icon, &my_icon_dsc);

/* From file */
lv_image_set_src(icon, "S:my_icon.bin");

If the image was converted with the online converter, you should use LV_IMAGE_DECLARE(my_icon_dsc) to declare the image in the file where you want to use it.

Image decoder

As you can see in the Color formats section, LVGL supports several built-in image formats. In many cases, these will be all you need. LVGL doesn't directly support, however, generic image formats like PNG or JPG.

To handle non-built-in image formats, you need to use external libraries and attach them to LVGL via the Image decoder interface.

An image decoder consists of 4 callbacks:

  • info get some basic info about the image (width, height and color format).

  • open open an image:
    • store a decoded image

    • set it to NULL to indicate the image can be read line-by-line.

  • get_area if open didn't fully open an image this function should give back part of image as decoded data.

  • close close an opened image, free the allocated resources.

You can add any number of image decoders. When an image needs to be drawn, the library will try all the registered image decoders until it finds one which can open the image, i.e. one which knows that format.

The following formats are understood by the built-in decoder: - LV_COLOR_FORMAT_I1 - LV_COLOR_FORMAT_I2 - LV_COLOR_FORMAT_I4 - LV_COLOR_FORMAT_I8 - LV_COLOR_FORMAT_RGB888 - LV_COLOR_FORMAT_XRGB8888 - LV_COLOR_FORMAT_ARGB8888 - LV_COLOR_FORMAT_RGB565 - LV_COLOR_FORMAT_RGB565A8

Custom image formats

The easiest way to create a custom image is to use the online image converter and select Raw or Raw with alpha format. It will just take every byte of the binary file you uploaded and write it as an image "bitmap". You then need to attach an image decoder that will parse that bitmap and generate the real, renderable bitmap.

header.cf will be LV_COLOR_FORMAT_RAW, LV_COLOR_FORMAT_RAW_ALPHA accordingly. You should choose the correct format according to your needs: a fully opaque image, using an alpha channel.

After decoding, the raw formats are considered True color by the library. In other words, the image decoder must decode the Raw images to True color according to the format described in the Color formats section.

Register an image decoder

Here's an example of getting LVGL to work with PNG images.

First, you need to create a new image decoder and set some functions to open/close the PNG files. It should look like this:

/* Create a new decoder and register functions */
lv_image_decoder_t * dec = lv_image_decoder_create();
lv_image_decoder_set_info_cb(dec, decoder_info);
lv_image_decoder_set_open_cb(dec, decoder_open);
lv_image_decoder_set_get_area_cb(dec, decoder_get_area);
lv_image_decoder_set_close_cb(dec, decoder_close);


/**
 * Get info about a PNG image
 * @param decoder   pointer to the decoder where this function belongs
 * @param src       can be file name or pointer to a C array
 * @param header    image information is set in header parameter
 * @return          LV_RESULT_OK: no error; LV_RESULT_INVALID: can't get the info
 */
static lv_result_t decoder_info(lv_image_decoder_t * decoder, const void * src, lv_image_header_t * header)
{
  /* Check whether the type `src` is known by the decoder */
  if(is_png(src) == false) return LV_RESULT_INVALID;

  /* Read the PNG header and find `width` and `height` */
  ...

  header->cf = LV_COLOR_FORMAT_ARGB8888;
  header->w = width;
  header->h = height;
}

/**
 * Open a PNG image and decode it into dsc.decoded
 * @param decoder   pointer to the decoder where this function belongs
 * @param dsc       image descriptor
 * @return          LV_RESULT_OK: no error; LV_RESULT_INVALID: can't open the image
 */
static lv_result_t decoder_open(lv_image_decoder_t * decoder, lv_image_decoder_dsc_t * dsc)
{
  (void) decoder; /* Unused */

  /* Check whether the type `src` is known by the decoder */
  if(is_png(dsc->src) == false) return LV_RESULT_INVALID;

  /* Decode and store the image. If `dsc->decoded` is `NULL`, the `decoder_get_area` function will be called to get the image data line-by-line */
  dsc->decoded = my_png_decoder(dsc->src);

  /* Change the color format if decoded image format is different than original format. For PNG it's usually decoded to ARGB8888 format */
  dsc->decoded.header.cf = LV_COLOR_FORMAT_...

  /* Call a binary image decoder function if required. It's not required if `my_png_decoder` opened the image in true color format. */
  lv_result_t res = lv_bin_decoder_open(decoder, dsc);

  return res;
}

/**
 * Decode an area of image
 * @param decoder      pointer to the decoder where this function belongs
 * @param dsc          image decoder descriptor
 * @param full_area    input parameter. the full area to decode after enough subsequent calls
 * @param decoded_area input+output parameter. set the values to `LV_COORD_MIN` for the first call and to reset decoding.
 *                     the decoded area is stored here after each call.
 * @return             LV_RESULT_OK: ok; LV_RESULT_INVALID: failed or there is nothing left to decode
 */
static lv_result_t decoder_get_area(lv_image_decoder_t * decoder, lv_image_decoder_dsc_t * dsc,
                                 const lv_area_t * full_area, lv_area_t * decoded_area)
{
  /**
  * If `dsc->decoded` is always set in `decoder_open` then `decoder_get_area` does not need to be implemented.
  * If `dsc->decoded` is only sometimes set or never set in `decoder_open` then `decoder_get_area` is used to
  * incrementally decode the image by calling it repeatedly until it returns `LV_RESULT_INVALID`.
  * In the example below the image is decoded line-by-line but the decoded area can have any shape and size
  * depending on the requirements and capabilities of the image decoder.
  */

  my_decoder_data_t * my_decoder_data = dsc->user_data;

  /* if `decoded_area` has a field set to `LV_COORD_MIN` then reset decoding */
  if(decoded_area->y1 == LV_COORD_MIN) {
    decoded_area->x1 = full_area->x1;
    decoded_area->x2 = full_area->x2;
    decoded_area->y1 = full_area->y1;
    decoded_area->y2 = decoded_area->y1; /* decode line-by-line, starting with the first line */

    /* create a draw buf the size of one line */
    bool reshape_success = NULL != lv_draw_buf_reshape(my_decoder_data->partial,
                                                       dsc->decoded.header.cf,
                                                       lv_area_get_width(full_area),
                                                       1,
                                                       LV_STRIDE_AUTO);
    if(!reshape_success) {
      lv_draw_buf_destroy(my_decoder_data->partial);
      my_decoder_data->partial = lv_draw_buf_create(lv_area_get_width(full_area),
                                                    1,
                                                    dsc->decoded.header.cf,
                                                    LV_STRIDE_AUTO);

      my_png_decode_line_reset(full_area);
    }
  }
  /* otherwise decoding is already in progress. decode the next line */
  else {
    /* all lines have already been decoded. indicate completion by returning `LV_RESULT_INVALID` */
    if (decoded_area->y1 >= full_area->y2) return LV_RESULT_INVALID;
    decoded_area->y1++;
    decoded_area->y2++;
  }

  my_png_decode_line(my_decoder_data->partial);

  return LV_RESULT_OK;
}

/**
 * Close PNG image and free data
 * @param decoder   pointer to the decoder where this function belongs
 * @param dsc       image decoder descriptor
 * @return          LV_RESULT_OK: no error; LV_RESULT_INVALID: can't open the image
 */
static void decoder_close(lv_image_decoder_t * decoder, lv_image_decoder_dsc_t * dsc)
{
  /* Free all allocated data */
  my_png_cleanup();

  my_decoder_data_t * my_decoder_data = dsc->user_data;
  lv_draw_buf_destroy(my_decoder_data->partial);

  /* Call the built-in close function if the built-in open/get_area was used */
  lv_bin_decoder_close(decoder, dsc);

}

So in summary:

  • In decoder_info, you should collect some basic information about the image and store it in header.

  • In decoder_open, you should try to open the image source pointed by dsc->src. Its type is already in dsc->src_type == LV_IMG_SRC_FILE/VARIABLE. If this format/type is not supported by the decoder, return LV_RESULT_INVALID. However, if you can open the image, a pointer to the decoded image should be set in dsc->decoded. If the format is known, but you don't want to decode the entire image (e.g. no memory for it), set dsc->decoded = NULL and use decoder_get_area to get the image area pixels.

  • In decoder_close you should free all allocated resources.

  • decoder_get_area is optional. In this case you should decode the whole image In decoder_open function and store image data in dsc->decoded. Decoding the whole image requires extra memory and some computational overhead.

Manually use an image decoder

LVGL will use registered image decoders automatically if you try and draw a raw image (i.e. using the lv_image Widget) but you can use them manually as well. Create an lv_image_decoder_dsc_t variable to describe the decoding session and call lv_image_decoder_open().

The color parameter is used only with LV_COLOR_FORMAT_A1/2/4/8 images to tell color of the image.

lv_result_t res;
lv_image_decoder_dsc_t dsc;
lv_image_decoder_args_t args = { 0 }; /* Custom decoder behavior via args */
res = lv_image_decoder_open(&dsc, &my_img_dsc, &args);

if(res == LV_RESULT_OK) {
  /* Do something with `dsc->decoded`. You can copy out the decoded image by `lv_draw_buf_dup(dsc.decoded)`*/
  lv_image_decoder_close(&dsc);
}

Image post-processing

Considering that some hardware has special requirements for image formats, such as alpha premultiplication and stride alignment, most image decoders (such as PNG decoders) may not directly output image data that meets hardware requirements.

For this reason, LVGL provides a solution for image post-processing. First, call a custom post-processing function after lv_image_decoder_open to adjust the data in the image cache, and then mark the processing status in cache_entry->process_state (to avoid repeated post-processing).

See the detailed code below:

  • Stride alignment and premultiply post-processing example:

/* Define post-processing state */
typedef enum {
  IMAGE_PROCESS_STATE_NONE = 0,
  IMAGE_PROCESS_STATE_STRIDE_ALIGNED = 1 << 0,
  IMAGE_PROCESS_STATE_PREMULTIPLIED_ALPHA = 1 << 1,
} image_process_state_t;

lv_result_t my_image_post_process(lv_image_decoder_dsc_t * dsc)
{
  lv_color_format_t color_format = dsc->header.cf;
  lv_result_t res = LV_RESULT_OK;

  if(color_format == LV_COLOR_FORMAT_ARGB8888) {
    lv_cache_lock();
    lv_cache_entry_t * entry = dsc->cache_entry;

    if(!(entry->process_state & IMAGE_PROCESS_STATE_PREMULTIPLIED_ALPHA)) {
      lv_draw_buf_premultiply(dsc->decoded);
      LV_LOG_USER("premultiplied alpha OK");

      entry->process_state |= IMAGE_PROCESS_STATE_PREMULTIPLIED_ALPHA;
    }

    if(!(entry->process_state & IMAGE_PROCESS_STATE_STRIDE_ALIGNED)) {
       uint32_t stride_expect = lv_draw_buf_width_to_stride(decoded->header.w, decoded->header.cf);
       if(decoded->header.stride != stride_expect) {
           LV_LOG_WARN("Stride mismatch");
           lv_draw_buf_t * aligned = lv_draw_buf_adjust_stride(decoded, stride_expect);
           if(aligned == NULL) {
               LV_LOG_ERROR("No memory for Stride adjust.");
               return NULL;
           }

           decoded = aligned;
       }

       entry->process_state |= IMAGE_PROCESS_STATE_STRIDE_ALIGNED;
    }

alloc_failed:
    lv_cache_unlock();
  }

  return res;
}
  • GPU draw unit example:

void gpu_draw_image(lv_draw_unit_t * draw_unit, const lv_draw_image_dsc_t * draw_dsc, const lv_area_t * coords)
{
  ...
  lv_image_decoder_dsc_t decoder_dsc;
  lv_result_t res = lv_image_decoder_open(&decoder_dsc, draw_dsc->src, NULL);
  if(res != LV_RESULT_OK) {
    LV_LOG_ERROR("Failed to open image");
    return;
  }

  res = my_image_post_process(&decoder_dsc);
  if(res != LV_RESULT_OK) {
    LV_LOG_ERROR("Failed to post-process image");
    return;
  }
  ...
}

Image caching

Sometimes it takes a lot of time to open an image. Continuously decoding a PNG/JPEG image or loading images from a slow external memory would be inefficient and detrimental to the user experience.

Therefore, LVGL caches image data. Caching means some images will be left open, hence LVGL can quickly access them from dsc->decoded instead of needing to decode them again.

Of course, caching images is resource intensive as it uses more RAM to store the decoded image. LVGL tries to optimize the process as much as possible (see below), but you will still need to evaluate if this would be beneficial for your platform or not. Image caching may not be worth it if you have a deeply embedded target which decodes small images from a relatively fast storage medium.

Cache size

The size of cache (in bytes) can be defined with LV_CACHE_DEF_SIZE in lv_conf.h. The default value is 0, so no image is cached.

The size of cache can be changed at run-time with lv_cache_set_max_size(size_t size), and get with lv_cache_get_max_size().

Value of images

When you use more images than available cache size, LVGL can't cache all the images. Instead, the library will close one of the cached images to free space.

To decide which image to close, LVGL uses a measurement it previously made of how long it took to open the image. Cache entries that hold slower-to-open images are considered more valuable and are kept in the cache as long as possible.

If you want or need to override LVGL's measurement, you can manually set the weight value in the cache entry in cache_entry->weight = time_ms to give a higher or lower value. (Leave it unchanged to let LVGL control it.)

Every cache entry has a "life" value. Every time an image is opened through the cache, the life value of all entries is increased by their weight values to make them older. When a cached image is used, its usage_count value is increased to make it more alive.

If there is no more space in the cache, the entry with usage_count == 0 and lowest life value will be dropped.

Memory usage

Note that a cached image might continuously consume memory. For example, if three PNG images are cached, they will consume memory while they are open.

Therefore, it's the user's responsibility to be sure there is enough RAM to cache even the largest images at the same time.

Clean the cache

Let's say you have loaded a PNG image into a lv_image_dsc_t my_png variable and use it in an lv_image Widget. If the image is already cached and you then change the underlying PNG file, you need to notify LVGL to cache the image again. Otherwise, there is no easy way of detecting that the underlying file changed and LVGL will still draw the old image from cache.

To do this, use lv_cache_invalidate(lv_cache_find(&my_png, LV_CACHE_SRC_TYPE_PTR, 0, 0)).

Custom cache algorithm

If you want to implement your own cache algorithm, you can refer to the following code to replace the LVGL built-in cache manager:

static lv_cache_entry_t * my_cache_add_cb(size_t size)
{
  ...
}

static lv_cache_entry_t * my_cache_find_cb(const void * src, lv_cache_src_type_t src_type, uint32_t param1, uint32_t param2)
{
  ...
}

static void my_cache_invalidate_cb(lv_cache_entry_t * entry)
{
  ...
}

static const void * my_cache_get_data_cb(lv_cache_entry_t * entry)
{
  ...
}

static void my_cache_release_cb(lv_cache_entry_t * entry)
{
  ...
}

static void my_cache_set_max_size_cb(size_t new_size)
{
  ...
}

static void my_cache_empty_cb(void)
{
  ...
}

void my_cache_init(void)
{
 /* Initialize new cache manager. */
 lv_cache_manager_t my_manager;
 my_manager.add_cb = my_cache_add_cb;
 my_manager.find_cb = my_cache_find_cb;
 my_manager.invalidate_cb = my_cache_invalidate_cb;
 my_manager.get_data_cb = my_cache_get_data_cb;
 my_manager.release_cb = my_cache_release_cb;
 my_manager.set_max_size_cb = my_cache_set_max_size_cb;
 my_manager.empty_cb = my_cache_empty_cb;

 /* Replace existing cache manager with the new one. */
 lv_cache_lock();
 lv_cache_set_manager(&my_manager);
 lv_cache_unlock();
}

API

lv_api_map_v9_0.h

lv_draw_buf.h

lv_api_map_v8.h

lv_image.h

lv_draw_image.h

lv_image_dsc.h

lv_image_decoder.h

lv_image_cache.h

lv_image_header_cache.h

lv_api_map_v9_1.h

lv_obj_property_names.h

lv_types.h