LVGL图像导入和解码

发布于:2025-05-11 ⋅ 阅读:(25) ⋅ 点赞:(0)

LVGL版本:8.1

概述

       在LVGL中,可以导入多种不同类型的图像:

  • 经转换器生成的C语言数组,适用于页面中不常改变的固定图像。
  • 存储系统中的外部图像,比较灵活,可以通过插卡或从网络中获取,但需要配置相应的驱动和解码器,解码过程也需要占用时间和内存。
  • SYMBOL类型的文本,和label组件类似。

       其中第三种比较容易实现,第一种仅需经过官方工具生成二进制数据,再在代码中引用即可。本文重点说明第二种情况即外部图像的使用,并以PNG类型图片为例介绍LVGL图像的解码和显示。

原理框图

       图像的解析和使用可以分以下几层:

        图中的虚线框可有可无,有的仅代表多一层函数,为了更好的理解,在图上加入这些虚线框。

       对于一个通过lv_img_create()函数创建的lv_img_t图像,可以通过设置图像源进行图像导入,LVGL会先查看缓存,如果有命中则直接使用,否则进入解码环节。对于内置图像如SYMBOL和VARIABLE(即C数组)类型,可以直接使用内置的解码器,在编译时会直接合并入程序。对于外部图像格式如jpg和png,则需要添加额外的解码器,并且,由于这些外部图像通常是以文件的形式存放在文件系统中,还需要提供基本的文件操作来读取文件,如Linux可以简单使用POSIX标准的系统调用函数(open、read、write等),事实上,如果没有提供文件系统,用户也可以自定义图像的打开和读取方式,只要符合接口标准即可。

    开启配置

           以PNG类型的图像为例,需要在lv_conf.h配置文件中,打开LV_USE_PNG开关,同时要开启文件系统(根据实际所在系统进行选择),比如在这里我选择的是LV_USE_FS_POSIX,即使用POSIX标准的文件操作函数(read、write等),也是Linux的原生操作。注意这里要求填入一个前缀来区分不同系统,通常使用类似“盘符”的字母(如这里使用了'S'),因为LVGL底层打开源路径时也是比对的路径首字母,配对成功后才会使用对应的驱动来打开。

    //lv_conf.h
    /* 文件系统,如开启,请设置相应的驱动字母(或前缀) */
    #define LV_USE_FS_STDIO '\0'        /*Uses fopen, fread, etc*/
    //#define LV_FS_STDIO_PATH "/home/john/"    /*Set the working directory. If commented it will be "./" */
    
    #define LV_USE_FS_POSIX 'S'        /*Uses open, read, etc*/
    //#define LV_FS_POSIX_PATH "/home/john/"    /*Set the working directory. If commented it will be "./" */
    
    #define LV_USE_FS_WIN32 '\0'        /*Uses CreateFile, ReadFile, etc*/
    //#define LV_FS_WIN32_PATH "C:\\Users\\john\\"    /*Set the working directory. If commented it will be ".\\" */
    
    #define LV_USE_FS_FATFS '\0'        /*Uses f_open, f_read, etc*/
    
    /*PNG decoder library*/
    #define LV_USE_PNG 1

           此外,还可以增加系统的图像缓存数量LV_IMG_CACHE_DEF_SIZE,来减少不必要的解码,提高渲染效率。

    //lv_conf.h
    #define LV_IMG_CACHE_DEF_SIZE 1   //图像缓存数量,只有使用外部解码器的图像,该配置才有意义

     

    图像解码

           通过lv_img_set_src()函数设置图像源来开启LVGL解码和显示的过程,其中图像源src可以是以下类型:

    • 图像路径,通常以一个盘符为首(如'S'或'S:'),后面跟绝对路径,比如是'S/img/abc.png',注意图像要预先导入到设备对应路径上才能读取
    • LVGL规定的SYMBOL类型,也可以通过官方工具生成自定义SYMBOL;
    • C语言数组,包含图像的位置和色彩信息,可以通过官方工具将其它类型的图像转换为LVGL可识别的数据文件,并在使用处进行声明和导入。

           在这里,该函数仅用于设置图像源,并获取图像基本信息,还没有到解码部分。

    //lv_img.c
    void lv_img_set_src(lv_obj_t * obj, const void * src)
    {
        LV_ASSERT_OBJ(obj, MY_CLASS);
    
        lv_obj_invalidate(obj);
    
        lv_img_src_t src_type = lv_img_src_get_type(src);  //获取来源类型
        lv_img_t * img = (lv_img_t *)obj;
    
        /*If the new source type is unknown free the memories of the old source*/
        if(src_type == LV_IMG_SRC_UNKNOWN) {
            LV_LOG_WARN("lv_img_set_src: unknown image type");
            if(img->src_type == LV_IMG_SRC_SYMBOL || img->src_type == LV_IMG_SRC_FILE) {
                lv_mem_free((void *)img->src);
            }
            img->src      = NULL;
            img->src_type = LV_IMG_SRC_UNKNOWN;
            return;
        }
    
        lv_img_header_t header;
        lv_img_decoder_get_info(src, &header);  //通过图片来源获取对应的解码器
    
        /*Save the source*/
        if(src_type == LV_IMG_SRC_VARIABLE) {
            /*If memory was allocated because of the previous `src_type` then free it*/
            if(img->src_type == LV_IMG_SRC_FILE || img->src_type == LV_IMG_SRC_SYMBOL) {
                lv_mem_free((void *)img->src);
            }
            img->src = src;
        }
        else if(src_type == LV_IMG_SRC_FILE || src_type == LV_IMG_SRC_SYMBOL) {
            /*If the new and the old src are the same then it was only a refresh.*/
            if(img->src != src) {
                const void * old_src = NULL;
                /*If memory was allocated because of the previous `src_type` then save its pointer and free after allocation.
                 *It's important to allocate first to be sure the new data will be on a new address.
                 *Else `img_cache` wouldn't see the change in source.*/
                if(img->src_type == LV_IMG_SRC_FILE || img->src_type == LV_IMG_SRC_SYMBOL) {
                    old_src = img->src;
                }
                char * new_str = lv_mem_alloc(strlen(src) + 1);
                LV_ASSERT_MALLOC(new_str);
                if(new_str == NULL) return;
                strcpy(new_str, src);
                img->src = new_str;
    
                if(old_src) lv_mem_free((void *)old_src);
            }
        }
    
        if(src_type == LV_IMG_SRC_SYMBOL) {
            /*`lv_img_dsc_get_info` couldn't set the with and height of a font so set it here*/
            const lv_font_t * font = lv_obj_get_style_text_font(obj, LV_PART_MAIN);
            lv_coord_t letter_space = lv_obj_get_style_text_letter_space(obj, LV_PART_MAIN);
            lv_coord_t line_space = lv_obj_get_style_text_line_space(obj, LV_PART_MAIN);
            lv_point_t size;
            lv_txt_get_size(&size, src, font, letter_space, line_space, LV_COORD_MAX, LV_TEXT_FLAG_NONE);
            header.w = size.x;
            header.h = size.y;
        }
    
        img->src_type = src_type;   //图像类型(变量/符号/文件路径)
        img->w        = header.w;   //图像宽度
        img->h        = header.h;   //图像高度
        img->cf       = header.cf;   //图像色彩信息
        img->pivot.x = header.w / 2;
        img->pivot.y = header.h / 2;
    
        lv_obj_refresh_self_size(obj);
    
        /*Provide enough room for the rotated corners*/
        if(img->angle || img->zoom != LV_IMG_ZOOM_NONE) lv_obj_refresh_ext_draw_size(obj);
    
        lv_obj_invalidate(obj);   //标记无效区域,下次更新才会正式解码图片
    }

           其中lv_img_decoder_get_info()用于获取图像源的基本信息,该函数会首先遍历解码器链表,匹配合适的解码器,然后将图像的宽、高、色彩信息记录下来。

    //lv_img_decoder.c
    lv_res_t lv_img_decoder_get_info(const void * src, lv_img_header_t * header)
    {
        lv_memset_00(header, sizeof(lv_img_header_t));
    
        if(src == NULL) return LV_RES_INV;
    
        lv_img_src_t src_type = lv_img_src_get_type(src);
        if(src_type == LV_IMG_SRC_VARIABLE) {
            const lv_img_dsc_t * img_dsc = src;
            if(img_dsc->data == NULL) return LV_RES_INV;
        }
    
        lv_res_t res = LV_RES_INV;
        lv_img_decoder_t * d;
        _LV_LL_READ(&LV_GC_ROOT(_lv_img_decoder_ll), d) {  //遍历解码器链表,为图像源寻找合适的解码器
            if(d->info_cb) {
                res = d->info_cb(d, src, header);  //调用info回调,获取图像信息,header用于保存这些信息
                if(res == LV_RES_OK) break;   //返回OK,说明解码器配对成功,退出并返回
            }
        }
    
        return res;
    }

           如果定义了LV_USE_PNG,则默认注册LVGL自带的PNG解码器,其中包含获取基本信息的回调函数decoder_info()

    //lv_png.c
    static lv_res_t decoder_info(struct _lv_img_decoder_t * decoder, const void * src, lv_img_header_t * header)
    {
        (void) decoder; /*Unused*/
        lv_img_src_t src_type = lv_img_src_get_type(src);     /* 获取来源类型 */
    
        /* 检查是否是文件路径类型 */
        if(src_type == LV_IMG_SRC_FILE) {
            const char * fn = src;
            if(!strcmp(&fn[strlen(fn) - 3], "png")) {     /* 检查后缀是否为png */
    
                /* Read the width and height from the file. They have a constant location:
                * [16..23]: width
                * [24..27]: height
                */
                uint32_t size[2];
                lv_fs_file_t f;
                lv_fs_res_t res = lv_fs_open(&f, fn, LV_FS_MODE_RD);  //使用对应的驱动打开文件
                if(res != LV_FS_RES_OK) return LV_RES_INV;
                lv_fs_seek(&f, 16, LV_FS_SEEK_SET);
                uint32_t rn;
                lv_fs_read(&f, &size, 8, &rn);
                if(rn != 8) return LV_RES_INV;
                lv_fs_close(&f);
                /*Save the data in the header*/
                header->always_zero = 0;
                header->cf = LV_IMG_CF_RAW_ALPHA;
                /*The width and height are stored in Big endian format so convert them to little endian*/
                header->w = (lv_coord_t) ((size[0] & 0xff000000) >> 24) +  ((size[0] & 0x00ff0000) >> 8);
                header->h = (lv_coord_t) ((size[1] & 0xff000000) >> 24) +  ((size[1] & 0x00ff0000) >> 8);
    
                return LV_RES_OK;
            }
        }
        /*If it's a PNG file in a  C array...*/
        else if(src_type == LV_IMG_SRC_VARIABLE) {
            const lv_img_dsc_t * img_dsc = src;
            header->always_zero = 0;
            header->cf = img_dsc->header.cf;       /*Save the color format*/
            header->w = img_dsc->header.w;         /*Save the color width*/
            header->h = img_dsc->header.h;         /*Save the color height*/
            return LV_RES_OK;
        }
    
        return LV_RES_INV;         /*If didn't succeeded earlier then it's an error*/
    }

           其中lv_fs_open()函数是寻找合适的驱动来打开这个文件,我们在前面定义了LV_USE_FS_POSIX,LVGL在初始化阶段就会自动注册POSIX标准驱动,使用open、read、write等函数打开对应文件。

    //lv_fs_posix.c
    void lv_fs_posix_init(void)
    {
        static lv_fs_drv_t fs_drv; /* 驱动描述子 */
        lv_fs_drv_init(&fs_drv);
    
        fs_drv.letter = LV_USE_FS_POSIX;  //文件系统前缀,可以简单设置为'S'
        fs_drv.open_cb = fs_open;
        fs_drv.close_cb = fs_close;
        fs_drv.read_cb = fs_read;
        fs_drv.write_cb = fs_write;
        fs_drv.seek_cb = fs_seek;
        fs_drv.tell_cb = fs_tell;
    
        fs_drv.dir_close_cb = fs_dir_close;
        fs_drv.dir_open_cb = fs_dir_open;
        fs_drv.dir_read_cb = fs_dir_read;
    
        lv_fs_drv_register(&fs_drv);
    }

           到这里为止仅仅只是把图像源记录下来,直到下次更新周期的绘制阶段,才会正式解析图像。下面直接跳转到图像的绘制函数lv_draw_img_core(),这里仅截取解码图像的代码片段。

    //lv_draw_img.c
    LV_ATTRIBUTE_FAST_MEM static lv_res_t lv_img_draw_core(const lv_area_t * coords, const lv_area_t * clip_area,
                                                           const void * src,
                                                           const lv_draw_img_dsc_t * draw_dsc)
    {
        if(draw_dsc->opa <= LV_OPA_MIN) return LV_RES_OK;
    
        _lv_img_cache_entry_t * cdsc = _lv_img_cache_open(src, draw_dsc->recolor, draw_dsc->frame_id);
    
        if(cdsc == NULL) return LV_RES_INV;
    
        /*......*/
    }

           可以看到,LVGL在绘制图像前先询问缓存,即_lv_img_cache_open()函数,该函数会先遍历缓存链表,查找是否存在匹配的缓存,如匹配直接返回该缓存,否则从链表中找一个存活时间最短的缓存,用新的图像替换掉该旧缓存。

    //lv_img_cache.c
    _lv_img_cache_entry_t * _lv_img_cache_open(const void * src, lv_color_t color, int32_t frame_id)
    {
        _lv_img_cache_entry_t * cached_src = NULL;
    
    #if LV_IMG_CACHE_DEF_SIZE
        if(entry_cnt == 0) {
            LV_LOG_WARN("lv_img_cache_open: the cache size is 0");
            return NULL;
        }
    
        _lv_img_cache_entry_t * cache = LV_GC_ROOT(_lv_img_cache_array);
    
        /*Decrement all lifes. Make the entries older*/
        uint16_t i;
        for(i = 0; i < entry_cnt; i++) {
            if(cache[i].life > INT32_MIN + LV_IMG_CACHE_AGING) {
                cache[i].life -= LV_IMG_CACHE_AGING;
            }
        }
    
        for(i = 0; i < entry_cnt; i++) {
            if(color.full == cache[i].dec_dsc.color.full &&
               frame_id == cache[i].dec_dsc.frame_id &&
               lv_img_cache_match(src, cache[i].dec_dsc.src)) {  //是否命中缓存
                /* 打开一个图像也会增加图像的存活时间,因此在这里对缓存增加一个time_to_open的时间 */
                cached_src = &cache[i];
                cached_src->life += cached_src->dec_dsc.time_to_open * LV_IMG_CACHE_LIFE_GAIN;
                if(cached_src->life > LV_IMG_CACHE_LIFE_LIMIT) cached_src->life = LV_IMG_CACHE_LIFE_LIMIT;
                LV_LOG_TRACE("image source found in the cache");
                break;
            }
        }
    
        /* 命中缓存后直接返回该缓存 */
        if(cached_src) return cached_src;
    
        /* 如果没有命中,则在缓存中寻找一个合适的位置存放新图像 */
        cached_src = &cache[0];
        for(i = 1; i < entry_cnt; i++) {
            if(cache[i].life < cached_src->life) {   //寻找生存时间最短的图像
                cached_src = &cache[i];
            }
        }
    
        /* 找到被替代的缓存后,要先关闭它的解码器 */
        if(cached_src->dec_dsc.src) {
            lv_img_decoder_close(&cached_src->dec_dsc);
            LV_LOG_INFO("image draw: cache miss, close and reuse an entry");
        }
        else {
            LV_LOG_INFO("image draw: cache miss, cached to an empty entry");
        }
    #else
        cached_src = &LV_GC_ROOT(_lv_img_cache_single);
    #endif
        /* 打开一个新的解码器 */
        uint32_t t_start  = lv_tick_get();
        lv_res_t open_res = lv_img_decoder_open(&cached_src->dec_dsc, src, color, frame_id);
        if(open_res == LV_RES_INV) {
            LV_LOG_WARN("Image draw cannot open the image resource");
            lv_memset_00(cached_src, sizeof(_lv_img_cache_entry_t));
            cached_src->life = INT32_MIN; /*Make the empty entry very "weak" to force its us*/
            return NULL;
        }
    
        cached_src->life = 0;   //记录缓存后,设置存活时间从0开始
    
        /* 记录解码它的时间 */
        if(cached_src->dec_dsc.time_to_open == 0) {
            cached_src->dec_dsc.time_to_open = lv_tick_elaps(t_start);
        }
    
        if(cached_src->dec_dsc.time_to_open == 0) cached_src->dec_dsc.time_to_open = 1;
    
        return cached_src;
    }

           当没有命中缓存时,会调用lv_img_decoder_open()函数尝试使用解码器打开该图像,打开成功后,会将图像数据提取出来。

    //lv_img_decoder.c
    lv_res_t lv_img_decoder_open(lv_img_decoder_dsc_t * dsc, const void * src, lv_color_t color, int32_t frame_id)
    {
        lv_memset_00(dsc, sizeof(lv_img_decoder_dsc_t));
    
        if(src == NULL) return LV_RES_INV;
        lv_img_src_t src_type = lv_img_src_get_type(src);
        if(src_type == LV_IMG_SRC_VARIABLE) {
            const lv_img_dsc_t * img_dsc = src;
            if(img_dsc->data == NULL) return LV_RES_INV;
        }
    
        dsc->color    = color;
        dsc->src_type = src_type;
        dsc->frame_id = frame_id;
    
        if(dsc->src_type == LV_IMG_SRC_FILE) {
            size_t fnlen = strlen(src);
            dsc->src = lv_mem_alloc(fnlen + 1);
            LV_ASSERT_MALLOC(dsc->src);
            if(dsc->src == NULL) {
                LV_LOG_WARN("lv_img_decoder_open: out of memory");
                return LV_RES_INV;
            }
            strcpy((char *)dsc->src, src);
        }
        else {
            dsc->src = src;
        }
    
        lv_res_t res = LV_RES_INV;
    
        lv_img_decoder_t * decoder;
        _LV_LL_READ(&LV_GC_ROOT(_lv_img_decoder_ll), decoder) {   //遍历解码器链表
            /* info和open函数是必要的 */
            if(decoder->info_cb == NULL || decoder->open_cb == NULL) continue;
    
            res = decoder->info_cb(decoder, src, &dsc->header);
            if(res != LV_RES_OK) continue;  //返回OK,说明命中解码器
    
            dsc->decoder = decoder;
            res = decoder->open_cb(decoder, dsc);  //使用解码器打开图像文件
    
            /* 如果返回OK,表示成功打开图像文件,则返回 */
            if(res == LV_RES_OK) return res;
    
            /* 准备遍历下一个解码器节点 */
            lv_memset_00(&dsc->header, sizeof(lv_img_header_t));
    
            dsc->error_msg = NULL;
            dsc->img_data  = NULL;
            dsc->user_data = NULL;
            dsc->time_to_open = 0;
        }
    
        if(dsc->src_type == LV_IMG_SRC_FILE)
            lv_mem_free((void *)dsc->src);
    
        return res;
    }

           接下来就是使用对应解码器的open函数来获取图像,这里就不进一步说明了。可以参考官方对于不同格式图像的解码器例程代码。


    网站公告

    今日签到

    点亮在社区的每一天
    去签到