试用 LVGL Pro,一套完整的工具包,助您高效构建、测试、分享和交付 UI!
LVGL
教程

使用 LVGL 的自定义解码器接口显示 PNG 图像

学习如何将 lodepng 库与 LVGL 的图像解码器接口集成,以在嵌入式系统上实时显示 PNG 图像。

加博尔·基什-瓦莫希加博尔·基什-瓦莫希14 分钟阅读

如果你在 LVGL 中使用过图像,你可能使用过在线图像转换器将图像转换为 C 数组并将其编译到代码中。然而,从 v5.2 开始,LVGL 有一个图像解码器接口,允许添加自定义解码器函数来打开和读取任何类型的图像。在本教程中,我将向你展示如何添加和使用 lodepng 库来实时显示 PNG 图像。

开始之前#

你需要知道在资源有限的嵌入式系统中打开 PNG 图像有优点和缺点。

主要优点是 PNG 图像的大小比未压缩图像小得多,因此它们占用的闪存更少。

缺点是当打开和解码图像时,整个未压缩图像需要存储在 RAM 中。此外,解码需要一些时间。

为了减少打开/解码时间,LVGL 有一个图像缓存功能。它保持最近使用的图像打开和缓冲。请记住,缓存的图像消耗 width x height x 4 bytes 的 RAM(不计算解压缩期间使用的临时缓冲区)。

在 PC 模拟器中启动 LVGL#

与嵌入式硬件相比,在 PC 上工作要快得多且容易得多。因此,为了帮助你的开发,LVGL 也被移植到 Windows、Linux 和 macOS。

如果你还没有设置 PC 模拟器环境,现在是一个很好的时机!阅读本教程:PC 模拟器设置指南

获取即用型 PNG 解码器#

如果你对实现细节不感兴趣,可以下载即用型 LVGL 的 PNG 解码器。

只需解压缩下载的文件,将其复制到项目中,并调用 png_decoder_init()。

将图像存储在闪存中#

你可以使用在线转换器将 PNG 图像转换为 C 数组,方法是选择 Raw with alpha 颜色格式,然后像这样使用它们:

/*在 lv_init() 之后调用一次*/
png_decoder_init();
 
...
 
/*创建图像对象并使用转换后的 png 文件*/
LV_IMG_DECLARE(my_image);
lv_obj_t * img_obj = lv_img_create(lv_scr_act(), NULL);
lv_img_set_src(img_obj, &my_image);
c

将图像存储为文件#

如果你使用文件,只需将文件名传递给 lv_img_set_src。请注意,这需要 POSIX 文件函数,例如 fopen、fclose、fread。如果你的系统不支持它们,你需要修改 lodepng.c/h。

/*在 lv_init() 之后调用一次*/
png_decoder_init();
 
...
 
/*创建图像对象并使用 png 文件*/
lv_obj_t * img_obj = lv_img_create(lv_scr_act(), NULL);
lv_img_set_src(img_obj, "my_image.png");
c

学习 lodepng 库#

获取 lodepng#

即使对于嵌入式系统,也有一些 PNG 解码器库。基本上,你可以选择其中任何一个,因为它们都以类似的方式工作,但是速度和性能可能会有所不同。

对于本教程,我选择了 lodepng 库。该库仅包含 2 个文件:

  • lodepng.cpp
  • lodepng.h

下载这两个文件并将它们复制到项目中,放在 main.c 文件旁边。正如你所看到的,它是一个 cpp 文件,但不用担心 - 如果你愿意,可以将其重命名为 lodepng.c。

使用 PNG 文件#

将 PNG 文件转换为纯像素数组#

要了解 lodepng 的工作原理,我们将把 PNG 图像文件解码为数组,并将此数组用作 LVGL 中的图像源。

我使用了下面的图像并将其复制到项目的根文件夹中。

使用 LVGL 解码的示例 PNG 图像
使用 LVGL 解码的示例 PNG 图像

为了简单起见,我将所有代码添加到 main.c 中。

首先,包含 lodepng:

#include "lodepng.h"
c

现在读取文件并将其内容加载到缓冲区。这应该在 lv_init() 之后:

uint32_t error;                 /*用于 png 解码器函数的返回值*/
 
/*将 PNG 文件加载到缓冲区。它仍然是压缩的(未解码)*/
unsigned char * png_data;      /*指向加载数据的指针。与原始文件相同,只是加载到 RAM 中*/
size_t png_data_size;          /*`png_data` 的字节大小*/
 
error = lodepng_load_file(&png_data, &png_data_size, "png_decoder_test.png");   /*加载文件*/
if(error) {
    printf("error %u: %s\n", error, lodepng_error_text(error));
    while(1);
}
c

现在从缓冲区解码 PNG 图像:

/*解码 PNG 图像*/
unsigned char * png_decoded;    /*将是指向解码图像的指针*/
uint32_t png_width;             /*将是解码图像的宽度*/
uint32_t png_height;            /*将是解码图像的高度*/
 
/*以 ARGB8888 格式解码加载的图像*/
error = lodepng_decode32(&png_decoded, &png_width, &png_height, png_data, png_data_size);
 
if(error) {
    printf("error %u: %s\n", error, lodepng_error_text(error));
    while(1);
}
c

在 LVGL 中使用解码的像素数组#

图像被解压缩到 png_decoded 中。我们只需要设置一个 LVGL 图像描述符并创建一个图像对象。

/*使用解码的图像为 LVGL 初始化图像描述符*/
lv_img_dsc_t png_dsc;
png_dsc.header.always_zero = 0;                      /*它必须为零*/
png_dsc.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;      /*设置颜色格式*/
png_dsc.header.w = png_width;
png_dsc.header.h = png_height;
png_dsc.data_size = png_width * png_height * 4;
png_dsc.data = png_decoded;
 
/*创建图像对象并将解码的 PNG 图像设置为其源*/
lv_obj_t * img_obj = lv_img_create(lv_scr_act(), NULL);     /*在 LVGL 中创建图像对象*/
lv_img_set_src(img_obj, &png_dsc);                          /*将图像源设置为解码的 PNG*/
lv_obj_set_drag(img_obj, true);                             /*使图像可拖动*/
 
/*为屏幕设置非白色背景颜色,以查看图像上的 alpha 是否有效*/
static lv_style_t new_style;
lv_style_copy(&new_style, lv_style_scr);
new_style.body.main_color = LV_COLOR_MAKE(0x40, 0x70, 0xAA);
lv_obj_set_style(lv_scr_act(), &new_style);
c

编译并运行后,我得到了这个:

使用 lodepng 在 LVGL 中解码的 PNG 图像
使用 lodepng 在 LVGL 中解码的 PNG 图像

使用存储在闪存中的 PNG 图像#

如果你的设备上没有文件系统,并且你想将 PNG 图像存储在闪存中以便在需要时解码:

使用在线图像转换器获取带有未压缩 PNG 文件的 C 数组。只需上传 PNG 图像,选择 Raw with alpha 颜色格式和 C array 输出格式。将此文件复制到项目中并修改解码器代码,如下所示。(你只需删除"从文件加载"部分并使用转换文件中的数据。)

LV_IMG_DECLARE(png_decoder_test);   /*声明 C 数组*/
uint32_t error;                     /*用于 png 解码器函数的返回值*/
 
/*解码 PNG 图像*/
unsigned char * png_decoded;    /*将是指向解码图像的指针*/
uint32_t png_width;             /*将是解码图像的宽度*/
uint32_t png_height;            /*将是解码图像的高度*/
 
/*以 ARGB8888 格式解码加载的图像*/
error = lodepng_decode32(&png_decoded, &png_width, &png_height, png_decoder_test.data, png_decoder_test.data_size);
 
if(error) {
    printf("error %u: %s\n", error, lodepng_error_text(error));
    while(1);
}
 
/*使用解码的图像为 LVGL 初始化图像描述符*/
... 从这里开始与上面相同 ...
c

它产生了相同的图像。

在微控制器上测试 PNG 解码器的速度#

我使用运行频率为 180 MHz、具有 2 MB 闪存和 256 kB RAM 的 STM32F429ZI 微控制器测试了 PNG 解码器的速度。测试图像的分辨率为 100 x 65。解压缩耗时 7 ms。

请注意,结果是 ARGB8888 格式,可能会转换为系统的颜色格式(例如 RGB565)。此转换需要 1 ms。

将 PNG 解码器连接到 LVGL#

LVGL 需要 4 个函数来解码自定义图像格式:

  1. decoder_info - 用于获取图像的最基本信息,如宽度、高度和颜色格式。
  2. decoder_open - 可以以两种方式工作:
    • 打开并将整个图像解码到缓冲区中并返回此缓冲区(将用于 PNG 图像)
    • 仅准备解码并返回 NULL。在这种情况下,将调用 decoder_read_line 来逐行读取图像的相关部分。
  3. decoder_read_line - 如上所述,它被调用以仅读取图像的所需行。如果图像解码器支持部分解压缩,它可以节省 RAM 和时间。
  4. decoder_close - 释放使用的资源

让我们看看如何实现这些函数来解码 PNG 图像!

解码器信息#

/**
 * 获取有关 PNG 图像的信息
 * @param src 可以是文件名或指向 C 数组的指针
 * @param header 在此处存储信息
 * @return LV_RES_OK:无错误;LV_RES_INV:无法获取信息
 */
static lv_res_t decoder_info(lv_img_decoder_t * decoder, const void * src, lv_img_header_t * header)
{
    (void) decoder; /*未使用*/
    lv_img_src_t src_type = lv_img_src_get_type(src);          /*获取源类型*/
 
    /*如果是 PNG 文件...*/
    if(src_type == LV_IMG_SRC_FILE) {
        const char * fn = src;
        if(!strcmp(&fn[strlen(fn) - 3], "png")) {              /*检查扩展名*/
 
            /* 从文件中读取宽度和高度。它们有一个常量位置:
             * [16..23]:宽度
             * [24..27]:高度
             */
            FILE* file;
            file = fopen(fn, "rb" );
            if(!file) return LV_RES_INV;
            fseek(file, 16, SEEK_SET);
            uint32_t size[2];
            fread(size, 1 , 8, file);
            fclose(file);
 
            /*将数据保存在 header 中*/
            header->always_zero = 0;
            header->cf = LV_IMG_CF_RAW_ALPHA;
            /*宽度和高度以大端格式存储,因此将它们转换为小端*/
            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;
        }
    }
    /*如果是 C 数组中的 PNG 文件...*/
    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;       /*保存颜色格式*/
        header->w = img_dsc->header.w;         /*保存颜色宽度*/
        header->h = img_dsc->header.h;         /*保存颜色高度*/
        return LV_RES_OK;
    }
 
    return LV_RES_INV;         /*如果之前没有成功,那就是一个错误*/
}
c

解码器打开#

/**
 * 打开 PNG 图像并返回解码的图像
 * @param src 可以是文件名或指向 C 数组的指针
 * @param style 图像对象的样式(现在未使用,但某些格式可能会使用它)
 * @return 指向解码图像的指针,如果失败则返回 `LV_IMG_DECODER_OPEN_FAIL`
 */
static lv_res_t decoder_open(lv_img_decoder_t * decoder, lv_img_decoder_dsc_t * dsc)
{
    (void) decoder; /*未使用*/
    uint32_t error;                 /*用于 PNG 解码器函数的返回值*/
 
    uint8_t * img_data = NULL;
 
    /*如果是 PNG 文件...*/
    if(dsc->src_type == LV_IMG_SRC_FILE) {
        const char * fn = dsc->src;
 
        if(!strcmp(&fn[strlen(fn) - 3], "png")) {              /*检查扩展名*/
 
            /*将 PNG 文件加载到缓冲区。它仍然是压缩的(未解码)*/
            unsigned char * png_data;      /*指向加载数据的指针。与原始文件相同,只是加载到 RAM 中*/
            size_t png_data_size;          /*`png_data` 的字节大小*/
 
            error = lodepng_load_file(&png_data, &png_data_size, fn);   /*加载文件*/
            if(error) {
                printf("error %u: %s\n", error, lodepng_error_text(error));
                return LV_RES_INV;
            }
 
            /*解码 PNG 图像*/
            uint32_t png_width;             /*将是解码图像的宽度*/
            uint32_t png_height;            /*将是解码图像的高度*/
 
            /*以 ARGB8888 格式解码加载的图像*/
            error = lodepng_decode32(&img_data, &png_width, &png_height, png_data, png_data_size);
            if(error) {
                printf("error %u: %s\n", error, lodepng_error_text(error));
                return LV_RES_INV;
            }
 
            /*将图像转换为系统的色深*/
            convert_color_depth(img_data,  png_width * png_height);
            dsc->img_data = img_data;
            return LV_RES_OK;     /*图像已完全解码。返回其指针*/
        }
    }
    /*如果是 C 数组中的 PNG 文件...*/
    else if(dsc->src_type == LV_IMG_SRC_VARIABLE) {
        const lv_img_dsc_t * img_dsc = dsc->src;
        uint32_t png_width;             /*未使用,仅解码器需要*/
        uint32_t png_height;            /*未使用,仅解码器需要*/
 
        /*以 ARGB8888 格式解码图像*/
        error = lodepng_decode32(&img_data, &png_width, &png_height, img_dsc->data, img_dsc->data_size);
 
        if(error) {
            return LV_RES_INV;
        }
 
        /*将图像转换为系统的色深*/
        convert_color_depth(img_data,  png_width * png_height);
 
        dsc->img_data = img_data;
        return LV_RES_OK;     /*返回其指针*/
    }
 
    return LV_RES_INV;    /*如果之前没有返回,那就失败了*/
}
c

解码器关闭#

/**
 * 释放分配的资源
 */
static void decoder_close(lv_img_decoder_t * decoder, lv_img_decoder_dsc_t * dsc)
{
    (void) decoder; /*未使用*/
    if(dsc->img_data) free((uint8_t *)dsc->img_data);
}
c

在 LVGL 中注册解码器函数#

最后,创建的函数应该在 LVGL 中注册:

lv_img_decoder_t * dec = lv_img_decoder_create();
lv_img_decoder_set_info_cb(dec, decoder_info);
lv_img_decoder_set_open_cb(dec, decoder_open);
lv_img_decoder_set_close_cb(dec, decoder_close);
c

之后,你可以将文件或 C 数组中的 PNG 图像设置为 LVGL 中图像对象的源。

结论#

你刚刚学会了如何使用 LVGL 的图像解码器接口来添加自定义图像格式。这是一个强大的功能,使你能够根据需要使用任何类型的图像。你甚至可以将图像转换为完全满足你需求的独特格式。

关于作者

加博尔·基什-瓦莫希
加博尔·基什-瓦莫希

LVGL 创始人

LVGL 的创始人和首席开发者,这是一个被全球数百万设备使用的开源嵌入式图形库。

认识博客背后的作者们

了解那些分享 LVGL 知识的优秀作者们

查看作者

订阅我们的通讯 不错过任何关于 LVGL 的新闻。我们每月最多发送 2 封邮件。

LVGL

LVGL 是最受欢迎的免费开源嵌入式图形库,支持任何 MCU、MPU 和显示类型,助您构建精美的用户界面。

我们还提供 UI 设计、实现和咨询等服务。

© 2026 LVGL。保留所有权利。
YouTubeGitHubLinkedIn