Linux_应用篇(17) FrameBuffer 应用编程

本章学习 Linux 下的 Framebuffer 应用编程, 通过对本章内容的学习, 大家将会了解到 Framebuffer 设备究竟是什么?以及如何编写应用程序来操控 FrameBuffer 设备。

本章将会讨论如下主题。

⚫ 什么是 Framebuffer 设备?

⚫ LCD 显示的基本原理;

⚫ 使用存储映射 I/O 方式编写 LCD 应用程序。

⚫ 在 LCD 上打点、画线;

⚫ BMP 图片格式详解;

⚫ 在 LCD 上显示图片

什么是 FrameBuffer

Frame 是帧的意思, buffer 是缓冲的意思,所以 Framebuffer 就是帧缓冲, 这意味着 Framebuffer 就是一块内存,里面保存着一帧图像。帧缓冲(framebuffer)是 Linux 系统中的一种显示驱动接口,它将显示设备(譬如 LCD) 进行抽象、 屏蔽了不同显示设备硬件的实现,对应用层抽象为一块显示内存(显存),它允许上层应用程序直接对显示缓冲区进行读写操作,而用户不必关心物理显存的位置等具体细节,这些都由Framebuffer 设备驱动来完成。所以在 Linux 系统中,显示设备被称为 FrameBuffer 设备(帧缓冲设备),所以 LCD 显示屏自然而言就是 FrameBuffer 设备。 FrameBuffer 设备对应的设备文件为/dev/fbX(X 为数字, 0、 1、 2、 3 等) , Linux下可支持多个 FrameBuffer 设备,最多可达 32 个,分别为/dev/fb0 到/dev/fb31, 开发板出厂系统中, /dev/fb0设备节点便是 LCD 屏。

应用程序读写/dev/fbX 就相当于读写显示设备的显示缓冲区(显存),譬如 LCD 的分辨率是 800*480,每一个像素点的颜色用 24 位(譬如 RGB888)来表示,那么这个显示缓冲区的大小就是 800 x 480 x 24 / 8 =1152000 个字节。 譬如执行下面这条命令将 LCD 清屏,也就是将其填充为黑色(假设 LCD 对应的设备节点是/dev/fb0,分辨率为 800*480, RGB888 格式):

cpp 复制代码
dd if=/dev/zero of=/dev/fb0 bs=1024 count=1125

这条命令的作用就是将 1125x1024 个字节数据全部写入到 LCD 显存中,并且这些数据都是 0x0。

LCD 应用编程介绍

本小节介绍如何对 FrameBuffer 设备(譬如 LCD)进行应用编程, 通过上面的介绍,相信大家应该已经知道如何操作 LCD 显示设备了,应用程序通过对 LCD 设备节点/dev/fb0(假设 LCD 对应的设备节点是/dev/fb0)进行 I/O 操作即可实现对 LCD 的显示控制,实质就相当于读写了 LCD 的显存,而显存是 LCD 的显示缓冲区, LCD 硬件会从显存中读取数据显示到 LCD 液晶面板上。在应用程序中,操作/dev/fbX 的一般步骤如下:

①、首先打开/dev/fbX 设备文件。

②、 使用 ioctl()函数获取到当前显示设备的参数信息,譬如屏幕的分辨率大小、像素格式,根据屏幕参数计算显示缓冲区的大小。

③、通过存储映射 I/O 方式将屏幕的显示缓冲区映射到用户空间(mmap)。

④、映射成功后就可以直接读写屏幕的显示缓冲区,进行绘图或图片显示等操作了。

⑤、完成显示后, 调用 munmap()取消映射、并调用 close()关闭设备文件。

从上面介绍的操作步骤来看, LCD 的应用编程还是非常简单的,这些知识点都是在前面的入门篇中给大家介绍过。

使用 ioctl()获取屏幕参数信息

当打开 LCD 设备文件之后,需要先获取到 LCD 屏幕的参数信息,譬如 LCD 的 X 轴分辨率、 Y 轴分辨率以及像素格式等信息,通过这些参数计算出 LCD 显示缓冲区的大小。

通 过 ioctl() 函 数 来 获 取 屏 幕 参 数 信 息 , 对 于 Framebuffer 设 备 来 说 , 常 用 的 request 包 括FBIOGET_VSCREENINFO、 FBIOPUT_VSCREENINFO、 FBIOGET_FSCREENINFO。

⚫ FBIOGET_VSCREENINFO: 表示获取 FrameBuffer 设备的可变参数信息,可变参数信息使用 struct fb_var_screeninfo 结 构 体 来 描 述 , 所 以 此 时 ioctl() 需 要 有 第 三 个 参 数 , 它 是 一 个 struct fb_var_screeninfo *指针,指向 struct fb_var_screeninfo 类型对象, 调用 ioctl()会将 LCD 屏的可变参数信息保存在 struct fb_var_screeninfo 类型对象中,如下所示:

cpp 复制代码
struct fb_var_screeninfo fb_var;
ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);

⚫ FBIOPUT_VSCREENINFO: 表示设置 FrameBuffer 设备的可变参数信息,既然是可变参数,那说明应用层可对其进行修改、重新配置,当然前提条件是底层驱动支持这些参数的动态调整,譬如在我们的 Windows 系统中,用户可以修改屏幕的显示分辨率,这就是一种动态调整。同样此时 ioctl()需要有第三个参数, 也是一个 struct fb_var_screeninfo *指针,指向 struct fb_var_screeninfo 类型对象, 表示用 struct fb_var_screeninfo 对象中填充的数据设置 LCD, 如下所示:

cpp 复制代码
struct fb_var_screeninfo fb_var = {0};
/* 对 fb_var 进行数据填充 */
......
......
/* 设置可变参数信息 */
ioctl(fd, FBIOPUT_VSCREENINFO, &fb_var);

⚫ FBIOGET_FSCREENINFO: 表示获取 FrameBuffer 设备的固定参数信息,既然是固定参数,那就意味着应用程序不可修改。固定参数信息使用struct fb_fix_screeninfo结构体来描述,所以此时ioctl()需要有第三个参数,它是一个 struct fb_fix_screeninfo *指针,指向 struct fb_fix_screeninfo 类型对象,调用 ioctl()会将 LCD 的固定参数信息保存在 struct fb_fix_screeninfo 对象中,如下所示:

cpp 复制代码
struct fb_fix_screeninfo fb_fix;
ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);

上 面 所 提 到 的 三 个 宏 定 义 FBIOGET_VSCREENINFO 、 FBIOPUT_VSCREENINFO 、FBIOGET_FSCREENINFO 以及 2 个数据结构 struct fb_var_screeninfo 和 struct fb_fix_screeninfo 都定义在<linux/fb.h>头文件中,所以在我们的应用程序中需要包含该头文件。

cpp 复制代码
#define FBIOGET_VSCREENINFO 0x4600
#define FBIOPUT_VSCREENINFO 0x4601
#define FBIOGET_FSCREENINFO 0x4602


struct fb_var_screeninfo {
    __u32 xres; /* 可视区域,一行有多少个像素点, X 分辨率 */
    __u32 yres; /* 可视区域,一列有多少个像素点, Y 分辨率 */
    __u32 xres_virtual; /* 虚拟区域,一行有多少个像素点 */
    __u32 yres_virtual; /* 虚拟区域,一列有多少个像素点 */
    __u32 xoffset; /* 虚拟到可见屏幕之间的行偏移 */
    __u32 yoffset; /* 虚拟到可见屏幕之间的列偏移 */
    __u32 bits_per_pixel; /* 每个像素点使用多少个 bit 来描述,也就是像素深度 bpp */
    __u32 grayscale; /* =0 表示彩色, =1 表示灰度, >1 表示 FOURCC 颜色 */
    /* 用于描述 R、 G、 B 三种颜色分量分别用多少位来表示以及它们各自的偏移量 */
    struct fb_bitfield red; /* Red 颜色分量色域偏移 */
    struct fb_bitfield green; /* Green 颜色分量色域偏移 */
    struct fb_bitfield blue; /* Blue 颜色分量色域偏移 */
    struct fb_bitfield transp; /* 透明度分量色域偏移 */
    __u32 nonstd; /* nonstd 等于 0,表示标准像素格式;不等于 0 则表示非标准像素格式 */
    __u32 activate;
    __u32 height; /* 用来描述 LCD 屏显示图像的高度(以毫米为单位) */
    __u32 width; /* 用来描述 LCD 屏显示图像的宽度(以毫米为单位) */
    __u32 accel_flags;
    /* 以下这些变量表示时序参数 */
    __u32 pixclock; /* pixel clock in ps (pico seconds) */
    __u32 left_margin; /* time from sync to picture */
    __u32 right_margin; /* time from picture to sync */
    __u32 upper_margin; /* time from sync to picture */
    __u32 lower_margin;
    __u32 hsync_len; /* length of horizontal sync */
    __u32 vsync_len; /* length of vertical sync */
    __u32 sync; /* see FB_SYNC_* */
    __u32 vmode; /* see FB_VMODE_* */
    __u32 rotate; /* angle we rotate counter clockwise */
    __u32 colorspace; /* colorspace for FOURCC-based modes */
    __u32 reserved[4]; /* Reserved for future compatibility */
};

通过 xres、 yres 获取到屏幕的水平分辨率和垂直分辨率, bits_per_pixel 表示像素深度 bpp,即每一个像素点使用多少个 bit 位来描述它的颜色,通过 xres * yres * bits_per_pixel / 8 计算可得到整个显示缓存区的大小。red、 green、 blue 描述了 RGB 颜色值中 R、 G、 B 三种颜色通道分别使用多少 bit 来表示以及它们各自的偏移量,通过 red、 green、 blue 变量可知道 LCD 的 RGB 像素格式,譬如是 RGB888 还是 RGB565,亦或者是 BGR888、 BGR565 等。 struct fb_bitfield 结构体如下所示:

cpp 复制代码
struct fb_bitfield {
    __u32 offset; /* 偏移量 */
    __u32 length; /* 长度 */
    __u32 msb_right; /* != 0 : Most significant bit is right */
};

struct fb_fix_screeninfo 结构体

cpp 复制代码
struct fb_fix_screeninfo {
    char id[16]; /* 字符串形式的标识符 */
    unsigned long smem_start; /* 显存的起始地址(物理地址) */
    __u32 smem_len; /* 显存的长度 */
    __u32 type;
    __u32 type_aux;
    __u32 visual;
    __u16 xpanstep;
    __u16 ypanstep;
    __u16 ywrapstep;
    __u32 line_length; /* 一行的字节数 */
    unsigned long mmio_start; /* Start of Memory Mapped I/O(physical address) */
    __u32 mmio_len; /* Length of Memory Mapped I/O */
    __u32 accel; /* Indicate to driver which specific chip/card we have */
    __u16 capabilities;
    __u16 reserved[2];
};

smem_start 表示显存的起始地址,这是一个物理地址,当然在应用层无法直接使用; smem_len 表示显存的长度,这个长度并不一定等于 LCD 实际的显存大小。 line_length 表示屏幕的一行像素点有多少个字节,通常可以使用 line_length * yres 来得到屏幕显示缓冲区的大小。通过上面介绍,接下来我们编写一个示例代码,获取 LCD 屏幕的参数信息,示例代码如下所示:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>

int main(int argc, char *argv[])
{
    struct fb_fix_screeninfo fb_fix;
    struct fb_var_screeninfo fb_var;
    int fd;

    /* 打开 framebuffer 设备 */
    if (0 > (fd = open("/dev/fb0", O_WRONLY))) {
        perror("open error");
        exit(-1);
    }

    /* 获取参数信息 */
    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);

    printf("分辨率: %d*%d\n"
        "像素深度 bpp: %d\n"
        "一行的字节数: %d\n"
        "像素格式: R<%d %d> G<%d %d> B<%d %d>\n",
        fb_var.xres, fb_var.yres, fb_var.bits_per_pixel,
        fb_fix.line_length,
        fb_var.red.offset, fb_var.red.length,
        fb_var.green.offset, fb_var.green.length,
        fb_var.blue.offset, fb_var.blue.length);

    /* 关闭设备文件退出程序 */
    close(fd);
    exit(0);
}

首先打开 LCD 设备文件,开发板出厂系统, LCD 对应的设备文件为/dev/fb0;打开设备文件之后得到文件描述符 fd,接着使用 ioctl()函数获取 LCD 的可变参数信息和固定参数信息,并将这些信息打印出来。在测试之前,需将 LCD 屏通过软排线连接到开发板(掉电情况下连接),连接好之后启动开发板。使用交叉编译工具编译上述示例代码,将编译得到的可执行文件拷贝到开发板 Linux 系统的用户家目录下,并直接运行它,如下所示:

笔者使用的是 7 寸 800*480 RGB 屏, 与上图打印显示的分辨率 800*480 是相符的;像素深度为 16,也就意味着一个像素点的颜色值将使用 16bit(也就是 2 个字节) 来表示;一行的字节数为 1600,一行共有 800个像素点,每个像素点使用 16bit 来描述,一共就是 800*16/8=1600 个字节数据,这也是没问题的。打印出像素格式为 R<11 5> G<5 6> B<0 5>, 分别表示 R、 G、 B 三种颜色分量对应的偏移量和长度,第一个数字表示偏移量,第二个参数为长度, 从打印的结果可知, 16bit 颜色值中高 5 位表示 R 颜色通道、中

间 6 位表示 G 颜色通道、低 5 位表示 B 颜色通道, 所以这是一个 RGB565 格式的显示设备。

使用 mmap()将显示缓冲区映射到用户空间

在前面小节中给大家介绍了存储映射 I/O 这种高级 I/O 方式,它的一个非常经典的使用场景便是用在 Framebuffer 应用编程中。 通过 mmap()将显示器的显示缓冲区(显存)映射到进程的地址空间中,这样应用程序便可直接对显示缓冲区进行读写操作。为什么这里需要使用存储映射 I/O 这种方式呢? 其实使用普通的 I/O 方式(譬如直接 read、 write)也是可以的, 只是, 当数据量比较大时,普通 I/O 方式效率较低。 假设某一显示器的分辨率为 1920 * 1080,像素格式为 ARGB8888,针对该显示器,刷一帧图像的数据量为 1920 x 1080 x 32 / 8 = 8294400 个字节(约等于 8MB),这还只是一帧的图像数据,而对于显示器来说,显示的图像往往是动态改变的,意味着图像数据会被不断更新。在这种情况下, 数据量是比较庞大的, 使用普通 I/O 方式必然导致效率低下,所以才会采用存储映射I/O 方式。

LCD应用编程基本操作

本小节编写应用程序,在 LCD 上实现画点、画线、画矩形等基本 LCD 操作,示例代码如下所示:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>

#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})

static int width; //LCD X 分辨率
static int height; //LCD Y 分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
/********************************************************************
* 函数名称: lcd_draw_point
* 功能描述: 打点
* 输入参数: x, y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
{
    unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
    /* 对传入参数的校验 */
    if (x >= width)
        x = width - 1;
    if (y >= height)
        y = height - 1;
    /* 填充颜色 */
    screen_base[y * width + x] = rgb565_color;
}
/********************************************************************
* 函数名称: lcd_draw_line
* 功能描述: 画线(水平或垂直线)
* 输入参数: x, y, dir, length, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_line(unsigned int x, unsigned int y, int dir, unsigned int length, unsigned int color)
{
    unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
    unsigned int end;
    unsigned long temp;

    /* 对传入参数的校验 */
    if (x >= width)
        x = width - 1;
    if (y >= height)
        y = height - 1;
    /* 填充颜色 */
    temp = y * width + x;//定位到起点
    if (dir) { //水平线
        end = x + length - 1;
        if (end >= width)
            end = width - 1;
        for ( ; x <= end; x++, temp++)
            screen_base[temp] = rgb565_color;
    } else { //垂直线
        end = y + length - 1;
        if (end >= height)
            end = height - 1;
        for ( ; y <= end; y++, temp += width)
            screen_base[temp] = rgb565_color;
    }
}
/********************************************************************
* 函数名称: lcd_draw_rectangle
* 功能描述: 画矩形
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x, unsigned int start_y, unsigned int end_y, unsigned int color)
{
    int x_len = end_x - start_x + 1;
    int y_len = end_y - start_y - 1;
    lcd_draw_line(start_x, start_y, 1, x_len, color);//上边
    lcd_draw_line(start_x, end_y, 1, x_len, color); //下边
    lcd_draw_line(start_x, start_y + 1, 0, y_len, color);//左边
    lcd_draw_line(end_x, start_y + 1, 0, y_len, color);//右边
}
/********************************************************************
* 函数名称: lcd_fill
* 功能描述: 将一个矩形区域填充为参数 color 所指定的颜色
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_fill(unsigned int start_x, unsigned int end_x, unsigned int start_y, unsigned int end_y, unsigned int color)
{
    unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
    unsigned long temp;
    unsigned int x;
    /* 对传入参数的校验 */
    if (end_x >= width)
        end_x = width - 1;
    if (end_y >= height)
        end_y = height - 1;
    /* 填充颜色 */
    temp = start_y * width; //定位到起点行首
    for ( ; start_y <= end_y; start_y++, temp+=width) {
        for (x = start_x; x <= end_x; x++)
            screen_base[temp + x] = rgb565_color;
    }
}

int main(int argc, char *argv[])
{
    struct fb_fix_screeninfo fb_fix;
    struct fb_var_screeninfo fb_var;
    unsigned int screen_size;
    int fd;

    /* 打开 framebuffer 设备 */
    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    /* 获取参数信息 */
    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
    screen_size = fb_fix.line_length * fb_var.yres;
    width = fb_var.xres;
    height = fb_var.yres;

    /* 将显示缓冲区映射到进程地址空间 */
    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == (void *)screen_base) {
        perror("mmap error");
        close(fd);
        exit(EXIT_FAILURE);
    }

    /* 画正方形方块 */
    int w = height * 0.25;//方块的宽度为 1/4 屏幕高度
    lcd_fill(0, width-1, 0, height-1, 0x0); //清屏(屏幕显示黑色)
    lcd_fill(0, w, 0, w, 0xFF0000); //红色方块
    lcd_fill(width-w, width-1, 0, w, 0xFF00); //绿色方块
    lcd_fill(0, w, height-w, height-1, 0xFF); //蓝色方块
    lcd_fill(width-w, width-1, height-w, height-1, 0xFFFF00);//黄色方块

    /* 画线: 十字交叉线 */
    lcd_draw_line(0, height * 0.5, 1, width, 0xFFFFFF);//白色线
    lcd_draw_line(width * 0.5, 0, 0, height, 0xFFFFFF);//白色线

    /* 画矩形 */
    unsigned int s_x, s_y, e_x, e_y;
    s_x = 0.25 * width;
    s_y = w;
    e_x = width - s_x;
    e_y = height - s_y;
    for ( ; (s_x <= e_x) && (s_y <= e_y); s_x+=5, s_y+=5, e_x-=5, e_y-=5)
        lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);

    /* 退出 */
    munmap(screen_base, screen_size); //取消映射
    close(fd); //关闭文件
    exit(EXIT_SUCCESS); //退出进程
}

在示例代码中定义了一个宏 argb8888_to_rgb565,用于实现将 unsigned int 类型的颜色(也就是ARGB8888 颜色)转换为 RGB565 颜色。程序中自定义了 4 个函数:

lcd_draw_point: 用于实现画点、打点操作,参数 x 和 y 指定像素点的位置,参数 color 表示颜色。

lcd_draw_line: 用于实现画线操作,参数 x 和 y 指定线的起始位置;参数 dir 表示方向,水平方向(dir!=0)还是垂直方向(dir=0),不支持斜线画法,画斜线需要一些算法去操作,这不是本章内容需要去关注的知识点;参数 length 表示线的长度,以像素为单位;参数 color 表示线条的颜色。

lcd_draw_rectangle: 用于实现画矩形操作,参数 start_x 和 start_y 指定矩形左上角的位置;参数 end_x和 end_y 指定矩形右下角的位置;参数 color 指定矩形 4 个边的线条颜色。

lcd_fill: 将一个指定的矩形区域填充为参数 color 指定的颜色,参数 start_x 和 start_y 指定矩形左上角的位置;参数 end_x 和 end_y 指定矩形右下角的位置;参数 color 指定矩形区域填充的颜色。

具体代码的实现各位读者自己去看, 非常简单,来看下 main()中做了哪些事情:

⚫ 首先调用 open()打开 LCD 设备文件得到文件描述符 fd;

⚫ 接着使用 ioctl 函数获取 LCD 的可变参数信息和固定参数信息,通过得到的信息计算 LCD 显存大小、得到 LCD 屏幕的分辨率, ALPHA/Mini I.MX6U 开发板出厂系统将 LCD 实现为一个 RGB565 显示设备,所以程序中自定义的 4 个函数在操作 LCD 像素点时、都是以 RGB565的格式写入颜色值。

⚫ 接着使用 mmap 建立映射;

⚫ 映射成功之后就可以在应用层直接操作 LCD 显存了,调用自定义的函数在 LCD 上画线、画矩形、画方块;

⚫ 操作完成之后,调用 munmap 取消映射,调用 close 关闭 LCD 设备文件,退出程序。

编译应用程序,将编译得到的可执行文件拷贝到开发板 Linux 系统的用户家目录下,执行应用程序:

LCD 应用编程显示 BMP 图片

本小节介绍如何在 LCD 上显示一张 BMP 图片,在编写程序之前, 首先需要对 BMP 格式图片进行简单地介绍。

BMP 图像介绍

我们常用的图片格式有很多,一般最常用的有三种: JPEG(或 JPG)、 PNG、 BMP 和 GIF。其中 JPEG(或 JPG)、 PNG 以及 BMP 都是静态图片,而 GIF 则可以实现动态图片。在本小节实验中,我们选择使用BMP 图片格式。BMP(全称 Bitmap)是 Window 操作系统中的标准图像文件格式,文件后缀名为".bmp",使用非常广。它采用位映射存储格式,除了图像深度可选以外,图像数据没有进行任何压缩,因此, BMP 图像文件所占用的空间很大,但是没有失真、 并且解析 BMP 图像简单。BMP 文件的图像深度可选 1bit、 4bit、 8bit、 16bit、 24bit 以及 32bit, 典型的 BMP 图像文件由四部分组成:

①、 BMP 文件头(BMP file header),它包含 BMP 文件的格式、大小、 位图数据的偏移量等信息;

②、位图信息头(bitmap information) ,它包含位图信息头大小、 图像的尺寸、 图像大小、 位平面数、压缩方式以及颜色索引等信息;

③、调色板(color palette),这部分是可选的,如果使用索引来表示图像, 调色板就是索引与其对应颜色的映射表;

④、位图数据(bitmap data),也就是图像数据。

BMP 文件头、位图信息头、调色板和位图数据, 总结如下表所示:

|----------------------------|----------------|----------------------------------------------|
| 数据段名称 | 大小(Byte) | 说明 |
| bmp 文件头 (bmp file header) | 14 | 包含 BMP 文件的格式、大小、到位图数据的 偏移量等信息 |
| 位图信息头 (bitmap information) | 通常为 40 或 56 字节 | 包含位图信息头大小、 图像的尺寸、图像大 小、位平面数、 压缩方式以及颜色索引等信 息; |
| 调色板 (color palette) | 由颜色索引数决定 | 可选,如果使用索引来表示图像的颜色, 则调 色板就是索引与其对应颜色的映射表; |
| 位图数据 (bitmap data) | 由图像尺寸决定 | 图像数据 |

一般常见的图像都是以 16 位(R、 G、 B 三种颜色分别使用 5bit、 6bit、 5bit 来表示)、 24 位(R、 G、B 三种颜色都使用 8bit 来表示) 色图像为主,我们称这样的图像为真彩色图像, 真彩色图像是不需要调色板的,即位图信息头后面紧跟的就是位图数据了。对某些 BMP 位图文件说并非如此, 譬如 16 色位图、 256 色位图,它们需要使用到调色板,具体调色板如何使用,我们不关心,本节我们将会以 16 位色(RGB565) BMP 图像为例。以一张 16 位 BMP 图像为例(如何得到 16 位色 BMP 图像,后面向大家介绍) ,首先在 Windows 下查看图片的属性,如下所示:

可以看到该图片的分辨率为 800*480,位深度为 16bit,每个像素点使用 16 位表示,也就是 RGB565。为了向大家介绍 BMP 文件结构,接下来使用十六进制查看工具将 image.bmp 文件打开,文件头部分的内容如下所示:

一、 bmp 文件头

Windows 下为 bmp 文件头定义了如下结构体:

cpp 复制代码
typedef struct tagBITMAPFILEHEADER
{
    UINT16 bfType;
    DWORD bfSize;
    UINT16 bfReserved1;
    UINT16 bfReserved2;
    DWORD bfOffBits;
} BITMAPFILEHEADER;

|-------------|------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------|
| 变量名 | 地址偏移 | 大小 | 作用 |
| bfType | 00H | 2 bytes | 说明 bmp 文件的类型,可取值为: ①BM -- Windows ②BA -- OS/2 Bitmap Array ③CI -- OS/2 Color Icon ④CP -- OS/2 Color Pointer ⑤IC -- OS/2 Icon ⑥PT -- OS/2 Pointer |
| bfSize | 02H | 4 bytes | 说明该文件的大小,以字节为单位。 |
| bfReserved1 | 06H | 2 bytes | 保留字段,必须设置为 0。 |
| bfReserved2 | 08H | 2 bytes | 保留字段,必须设置为 0。 |
| bfOffBits | 0AH | 4 bytes | 说明从文件起始位置到图像数据之间的字节偏移量。 这个参数非常有用,因为位图信息头和调色板的长度 会根据不同的情况而变化,所以我们可以用这个偏移 量迅速从文件中找到图像数据的偏移地址。 |

从上面的描述信息,再来对照文件数据:

00~01H: 0x42、 0x4D 对应的 ASCII 字符分别为为 B、 M,表示这是 Windows 所支持的位图格式,该字段必须是"BM"才是 Windows 位图文件。

02~05H: 对应于文件大小, 0x000BB848=768072 字节,与 image.bmp 文件大小是相符的。

06~09H: 保留字段。

0A~0D: 0x00000046=70,即从文件头部开始到位图数据需要偏移 70 个字节。

bmp 文件头的大小固定为 14 个字节。

二、 位图信息头

同样, Windows 下为位图信息头定义了如下结构体:

cpp 复制代码
typedef struct tagBITMAPINFOHEADER {
    DWORD biSize;
    LONG biWidth;
    LONG biHeight;
    WORD biPlanes;
    WORD biBitCount;
    DWORD biCompression;
    DWORD biSizeImage;
    LONG biXPelsPerMeter;
    LONG biYPelsPerMeter;
    DWORD biClrUsed;
    DWORD biClrImportant;
} BITMAPINFOHEADER;

结构体中每一个成员说明如下:

|-----------------|------|---------|-------------------------------------------------------------------------------------------------------------------------------------------------|
| 变量名 | 地址偏移 | 大小 | 作用 |
| biSize | 0EH | 4 bytes | 位图信息头大小。 |
| biWidth | 12H | 4 bytes | 图像的宽度,以像素为单位。 |
| biHeight | 16H | 4 bytes | 图像的高度,以像素为单位。 注意,这个值除了用于描述图像的高度之外, 它还有另外一个用途,用于指明该图像是倒向 的位图、还是正向的位图。 如果该值是一个正数,说明是倒向的位图;如 果该值是一个负数,则说明是正向的位图。 一般情况下, BMP 图像都是倒向的位图,也就 是该值是一个正数。 |
| biPlanes | 1AH | 2 bytes | 色彩平面数,该值总被设置为 1。 |
| biBitCount | 1CH | 2 bytes | 像素深度,指明一个像素点需要多少个 bit 数据 来描述,其值可为 1、 4、 8、 16、 24、 32 |
| biCompression | 1EH | 4 bytes | 说明图像数据的压缩类型,取值范围如下: ①0 -- RGB 方式 ②1 -- 8bpp 的 RLE 方式,只用于 8bit 位图 ③2 -- 4bpp 的 RLE 方式, 只用于 4bit 位图 ④3 -- Bit-fields 方式 ⑤4 -- 仅用于打印机 ⑥5 -- 仅用于打印机 |
| biSizeImage | 22H | 4 bytes | 说明图像的大小,以字节为单位,当压缩类型 为 BI_RGB 时,可设置为 0。 |
| biXPelsPerMeter | 26H | 4 bytes | 水平分辨率,用像素/米来表示,有符号整数。 |
| biYPelsPerMeter | 2AH | 4 bytes | 垂直分辨率,用像素/米来表示,有符号整数。 |
| biClrUsed | 2EH | 4 bytes | 说明位图实际使用的彩色表中的颜色索引数。 |
| biClrImportant | 32H | 4 bytes | 说明对图像显示有重要影响的颜色索引的数 目,如果是 0,则表示都重要。 |

从上面的描述信息,再来对照文件数据:

0E~11H: 0x00000038=56,这说明这个位图信息头的大小为 56 个字节。

12~15H: 0x00000320=800,图像宽度为 800 个像素,与文件属性一致。

16~19H: 0x000001E0=480,图像高度为 480 个像素,与文件属性一致;这个数是一个正数,说明是一个倒向的位图,什么是正向的位图、什么是倒向的位图, 说的是图像数据的排列问题; 如果是正向的位图,图像数据是按照图像的左上角到右下角方式排列的,水平方向从左到右,垂直方向从上到下。倒向的位图,图像数据则是按照图像的左下角到右上角方式排列的,水平方向依然从左到右,垂直方向改为从下到上。

1A~1BH: 0x0001=1,这个值总为 1。

1C~1DH: 0x0010=16,表示每个像素占 16 个 bit。

1E~21H: 0x00000003=3, bit-fileds 方式。

22~25H: 0x000BB802=768002,图像的大小,注意图像的大小并不是 BMP 文件的大小,而是图像数据的大小。

26~29H: 0x00000EC2=3778,水平分辨率为 3778 像素/米。

2A~2DH: 0x00000EC2=3778,垂直分辨率为 3778 像素/米。

2E~31H: 0x00000000=0,本位图未使用调色板。

32~35H: 0x00000000=0。

只有压缩方式选项被设置为 bit-fileds(0x3) 时, 位图信息头的大小才会等于 56 字节,否则,为 40 字节。 56 个字节相比于 40 个字节,多出了 16 个字节, 那么多出的 16 个字节数据描述了什么信息呢? 稍后再给大家介绍。

三、 调色板

调色板是单色、 16 色、 256 色位图图像文件所持有的,如果是 16 位、 24 位以及 32 位位图文件,则 BMP文件组成部分中不包含调色板,关于调色板这里不过多介绍,有兴趣可以自己去了解。

四、 位图数据

位图数据其实就是图像的数据, 对于 24 位位图,使用 3 个字节数据来表示一个像素点的颜色,对于 16位位图,使用 2 个字节数据来表示一个像素点的颜色,同理, 32 位位图则使用 4 个字节来描述。BMP 位图分为正向的位图和倒向的位图,主要区别在于图像数据存储的排列方式,前面已经给大家解释的比较清楚了, 如下如所示(左边对应的是正向位图,右边对应的则是倒向位图) :

所以正向位图先存储图像的第一行数据,从左到右依次存放,接着存放第二行,依次这样;而倒向位图,则先存储图像的最后一行(倒数第一行)数据,也是从左到右依次存放,接着倒数二行,依次这样。

RGB 和 Bit-Fields

当图像中引用的色彩超过 256 种时,就需要 16bpp 或更高 bpp 的位图(24 位、 32 位)。调色板不适合bpp 较大的位图,因此 16bpp 及以上的位图都不使用调色板,不使用调色板的位图图像有两种编码格式:RGB 和 Bit-Fields(下称 BF) 。

RGB 编码格式是一种均分的思想, 使 Red、 Green、 Blue 三种颜色信息容量一样大,譬如 24bpp-RGB,它通常只有这一种编码格式,在 24bits 中,低 8 位表示 Blue 分量;中 8 为表示 Green 分量;高 8 位表示 Red分量。

而在 32bpp-RGB 中,低 24 位的编码方式与 24bpp 位图相同,最高 8 位用来表示透明度 Alpha 分量。32bpp 的位图尺寸太大,一般只有在图像处理的中间过程中使用。对于需要半透过效果的图像,更好的选择是 PNG 格式。

BF 编码格式与 RGB 不同,它利用位域操作,人为地确定 RGB 三分量所包含的信息容量。 位图信息头介绍中提及到,当压缩方式选项置为 BF 时, 位图信息头大小比平时多出 16 字节, 这 16 个字节实际上是 4个 32bit 的位域掩码, 按照先后顺序,它们分别是 R、 G、 B、 A 四个分量的位域掩码, 当然如果没有 Alpha分量,则 Alpha 掩码没有实际意义。位域掩码的作用是指出 R、 G、 B 三种颜色信息容量的大小,分别使用多少个 bit 数据来表示,以及三种颜色分量的位置偏移量。譬如对于 16 位色的 RGB565 图像,通常使用 BF 编码格式,同样这也是 BF 编码格式最著名和最普遍的应用之一, 它的 R、 G 和 B 分量的位域掩码分别是 0xF800、 0x07E0 和 0x001F,也就是 R 通道使用 2 个字节中的高 5 位表示, G 通道使用 2 个字节中的中间 6 位表示。而 B 通道则使用 2个字节中的最低 5 位表示, 如下图所示:

如何得到 16 位色 RGB565 格式 BMP 图像?

在 Windows 下我们转换得到的 BMP 位图通常是 24 位色的 RGB888 格式图像,那如何得到 RGB565 格式 BMP 位图呢?当然这个方法很多,这里笔者向大家介绍一种方法就是通过 Photoshop 软件来得到 RGB565格式的 BMP 位图。首先,找一张图片,图片格式无所谓,只要Photoshop软件能打开即可;确定图片之后,我们启动Photoshop软件,并且使用 Photoshop 软件打开这张图片, 打开之后点击菜单栏中的文件--->存储为,接着出现如下界面:

在这个界面中,首先选择文件保存的路径,然后设置文件名以及文件格式,选择文件格式为 BMP 格式,之后点击保存,如下:

点击选择 16 位色图,接着点击高级模式按钮:

点击选择 RGB565,接着点击确定按钮即可,这样就可得到 16 位色 RGB565 格式的 BMP 图像。

在 LCD 上显示 BMP 图像

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>

/**** BMP 文件头数据结构 ****/
typedef struct {
    unsigned char type[2]; //文件类型
    unsigned int size; //文件大小
    unsigned short reserved1; //保留字段 1
    unsigned short reserved2; //保留字段 2
    unsigned int offset; //到位图数据的偏移量
} __attribute__ ((packed)) bmp_file_header;

/**** 位图信息头数据结构 ****/
typedef struct {
    unsigned int size; //位图信息头大小
    int width; //图像宽度
    int height; //图像高度
    unsigned short planes; //位面数
    unsigned short bpp; //像素深度
    unsigned int compression; //压缩方式
    unsigned int image_size; //图像大小
    int x_pels_per_meter; //像素/米
    int y_pels_per_meter; //像素/米
    unsigned int clr_used;
    unsigned int clr_omportant;
} __attribute__ ((packed)) bmp_info_header;

/**** 静态全局变量 ****/
static int width; //LCD X 分辨率
static int height; //LCD Y 分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
static unsigned long line_length; //LCD 一行的长度(字节为单位)
/********************************************************************
* 函数名称: show_bmp_image
* 功能描述: 在 LCD 上显示指定的 BMP 图片
* 输入参数: 文件路径
* 返 回 值: 成功返回 0, 失败返回-1
********************************************************************/
static int show_bmp_image(const char *path)
{
    bmp_file_header file_h;
    bmp_info_header info_h;
    unsigned short *line_buf = NULL; //行缓冲区
    unsigned long line_bytes; //BMP 图像一行的字节的大小
    unsigned int min_h, min_bytes;
    int fd = -1;
    int j;

    /* 打开文件 */
    if (0 > (fd = open(path, O_RDONLY))) {
        perror("open error");
        return -1;
    }

    /* 读取 BMP 文件头 */
    if (sizeof(bmp_file_header) != read(fd, &file_h, sizeof(bmp_file_header))) {
        perror("read error");
        close(fd);
        return -1;
    }

    if (0 != memcmp(file_h.type, "BM", 2)) {
        fprintf(stderr, "it's not a BMP file\n");
        close(fd);
        return -1;
    }

    /* 读取位图信息头 */
    if (sizeof(bmp_info_header) != read(fd, &info_h, sizeof(bmp_info_header))) {
        perror("read error");
        close(fd);
        return -1;
    }

    /* 打印信息 */
    printf("文件大小: %d\n"
    "位图数据的偏移量: %d\n"
    "位图信息头大小: %d\n"
    "图像分辨率: %d*%d\n"
    "像素深度: %d\n", file_h.size, file_h.offset,
    info_h.size, info_h.width, info_h.height,
    info_h.bpp);

    /* 将文件读写位置移动到图像数据开始处 */
    if (-1 == lseek(fd, file_h.offset, SEEK_SET)) {
        perror("lseek error");
        close(fd);
        return -1;
    }

    /* 申请一个 buf、暂存 bmp 图像的一行数据 */
    line_bytes = info_h.width * info_h.bpp / 8;
    line_buf = malloc(line_bytes);
    if (NULL == line_buf) {
        fprintf(stderr, "malloc error\n");
        close(fd);
        return -1;
    }

    if (line_length > line_bytes)
        min_bytes = line_bytes;
    else
        min_bytes = line_length;

/**** 读取图像数据显示到 LCD ****/
    if (0 < info_h.height) {//倒向位图
        if (info_h.height > height) {
            min_h = height;
            lseek(fd, (info_h.height - height) * line_bytes, SEEK_CUR);
            screen_base += width * (height - 1); //定位到屏幕左下角位置
        } else {
            min_h = info_h.height;
            screen_base += width * (info_h.height - 1);
        }

        for (j = min_h; j > 0; screen_base -= width, j--) {
            read(fd, line_buf, line_bytes); //读取出图像数据
            memcpy(screen_base, line_buf, min_bytes);//刷入 LCD 显存
        }
    } else { //正向位图
        int temp = 0 - info_h.height; //负数转成正数
        if (temp > height)
            min_h = height;
        else
            min_h = temp;
        for (j = 0; j < min_h; j++, screen_base += width) {
            read(fd, line_buf, line_bytes);
            memcpy(screen_base, line_buf, min_bytes);
        }
    }
    /* 关闭文件、函数返回 */
    close(fd);
    free(line_buf);
    return 0;
}

int main(int argc, char *argv[])
{
    struct fb_fix_screeninfo fb_fix;
    struct fb_var_screeninfo fb_var;
    unsigned int screen_size;
    int fd;

    /* 传参校验 */
    if (2 != argc) {
        fprintf(stderr, "usage: %s <bmp_file>\n", argv[0]);
        exit(-1);
    }

    /* 打开 framebuffer 设备 */
    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    /* 获取参数信息 */
    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
    screen_size = fb_fix.line_length * fb_var.yres;
    line_length = fb_fix.line_length;
    width = fb_var.xres;
    height = fb_var.yres;

    /* 将显示缓冲区映射到进程地址空间 */
    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == (void *)screen_base) {
        perror("mmap error");
        close(fd);
        exit(EXIT_FAILURE);
    }

    /* 显示 BMP 图片 */
    memset(screen_base, 0xFF, screen_size);
    show_bmp_image(argv[1]);

    /* 退出 */
    munmap(screen_base, screen_size); //取消映射
    close(fd); //关闭文件
    exit(EXIT_SUCCESS); //退出进程
}

代码中有两个自定义结构体 bmp_file_header 和 bmp_info_header,描述 bmp 文件头的数据结构bmp_file_header、以及描述位图信息头的数据结构 bmp_info_header。

当执行程序时候,需要传入参数,指定一个 bmp 文件。 main()函数中会调用 show_bmp_image()函数在LCD 上显示 bmp 图像, show_bmp_image()函数的参数为 bmp 文件路径,在 show_bmp_image()函数中首先会打开指定路径的 bmp 文件,得到对应的文件描述符 fd,接着调用 read()函数读取 bmp 文件头和位图信息头。获取到信息之后使用 printf 将其打印出来,接着使用 lseek()函数将文件的读写位置移动到图像数据起始位置处,也就是 bmp_file_header 结构体中的 offset 变量指定的地址偏移量。通过 info_h.height 判断该 BMP 位图是正向的位图还是倒向的位图,它们的处理方式不一样,这些代码自己去看,关于本示例代码就介绍这么多,接下来使用交叉编译工具编译上述示例代码,将编译得到的可执行文件 testApp 以及测试使用的 bmp 图像文件拷贝到开发板 Linux 系统的用户家目录下:

LCD 应用编程显示 jpeg 图片

我们常用的图片格式有很多,一般最常用的有三种: JPEG(或 JPG)、 PNG、 BMP。上一章给大家介绍了如何在 LCD 上显示 BMP 图片,详细介绍了 BMP 图像的格式; BMP 图像虽然没有失真、并且解析简单,但是由于图像数据没有进行任何压缩,因此, BMP 图像文件所占用的存储空间很大,不适合存储在磁盘设备中。而 JPEG(或 JPG)、 PNG 则是经过压缩处理的图像格式,将图像数据进行压缩编码,大大降低了图像文件的大小, 适合存储在磁盘设备中, 所以很常用。 本节我们就来学习如何在 LCD 屏上显示 jpeg 图像,下一节将向大家介绍如何在 LCD 屏上显示 png 图像。

本节将会讨论如下主题。

⚫ JPEG 简介;

⚫ libjpeg 库简介;

⚫ libjpeg 库移植;

⚫ 使用 libjpeg 库函数对 JPEG 图像进行解码;

JPEG 简介

JPEG(Joint Photographic Experts Group)是由国际标准组织为静态图像所建立的第一个国际数字图像压缩标准,也是至今一直在使用的、应用最广的图像压缩标准。JPEG 由于可以提供有损压缩,因此压缩比可以达到其他传统压缩算法无法比拟的程度; JPEG 虽然是有损压缩,但这个损失的部分是人的视觉不容易察觉到的部分,它充分利用了人眼对计算机色彩中的高频信息部分不敏感的特点,来大大节省了需要处理的数据信息。JPEG 压缩文件通常以.jpg 或.jpeg 作为文件后缀名, 关于 JPEG 压缩标准就给大家介绍这么多,这些内容都是笔者从网络上截取下来的,对此感兴趣的读者可以自行从网络上查阅这些信息。

libjpeg 简介

JPEG 压缩标准使用了一套压缩算法对原始图像数据进行了压缩得到.jpg 或.jpeg 图像文件,如果想要在LCD 上显示.jpg 或.jpeg 图像文件,则需要对其进行解压缩、以得到图像的原始数据,譬如 RGB 数据。既然压缩过程使用了算法,那对.jpg 或.jpeg 图像文件进行解压同样也需要算法来处理,当然,笔者并不会教大家如何编写解压算法, 这些算法的实现也是很复杂的,笔者肯定不会,自然教不了大家!但是,我们可以使用别人写好的库、调用别人写好的库函数来解压.jpg 或.jpeg 图像文件,也就是本小节要向大家介绍的 libjpeg 库。libjpeg 是一个完全用 C 语言编写的函数库,包含了 JPEG 解码(解压缩) 、 JPEG 编码(创建压缩) 和其他的 JPEG 功能的实现。 可以使用 libjpeg 库对.jpg 或.jpeg 压缩文件进行解压或者生成.jpg 或.jpeg 压缩文件。libjpeg 是一个开源 C 语言库,我们获取到它的源代码。

libjpeg 移植

下载源码包

首先,打开 http://www.ijg.org/files/链接地址,如下所示:

这里我们选择一个适中的版本,笔者以 v9b 为例,对应的文件名为 jpegsrc.v9b.tar.gz,点击该文件即可下载。

编译源码

将 jpegsrc.v9b.tar.gz 压缩包文件拷贝到 Ubuntu 系统用户家目录下,执行命令解压:

解压成功之后会生成 jpeg-9b 文件夹,也就是 libjpeg 源码文件夹。编译之前,在家目录下的 tools 文件夹中创建一个名为 jpeg 的文件夹,该目录作为 libjpeg 库的安装目录,

进入到 libjpeg 源码目录 jpeg-9b 中,该目录下包含的内容如下所示:

接下来对 libjpeg 源码进行交叉编译,跟编译 tslib 时步骤一样,包含三个步骤:

⚫ 配置工程;

⚫ 编译工程;

⚫ 安装;

一套流程下来非常地快!没有任何难点。在此之前,先对交叉编译工具的环境进行初始化,使用 source执行交叉编译工具安装目录下的 environment-setup-cortexa7hf-neon-poky-linux-gnueabi 脚本文件(如果已经初始化过了,那就不用再进行初始化了):

cpp 复制代码
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

执行下面这条命令对 libjpeg 工程进行配置:

cpp 复制代码
./configure --host=arm-poky-linux-gnueabi --prefix=/home/dt/tools/jpeg/

大家可以执行./configure --help 查看它的配置选项以及含义, --host 选项用于指定交叉编译得到的库文件是运行在哪个平台,通常将--host 设置为交叉编译器名称的前缀,譬如 arm-poky-linux-gnueabi-gcc 前缀就是 arm-poky-linux-gnueabi; --prefix 选项则用于指定库文件的安装路径, 将家目录下的 tools/jpeg 目录作为libjpeg 的安装目录。

接着执行 make 命令编译工程:

编译完成之后,执行命令安装 libjpeg:

安装目录下的文件夹介绍

与 tslib 库安装目录下的包含的文件夹基本相同(除了没有 etc 目录) , bin 目录下包含一些测试工具;include 目录下包含头文件; lib 目录下包含动态链接库文件。

进入到 include 目录下:

在这个目录下包含了 4 个头文件,在应用程序中,我们只需包含 jpeglib.h 头文件即可!进入到 lib 目录下:

libjpeg.so 和 libjpeg.so.9 都是符号链接,指向 libjpeg.so.9.2.0。

移植到开发板

进入到 libjpeg 安装目录下,将 bin 目录下的所有测试工具拷贝到开发板 Linux 系统/usr/bin 目录;将 lib目录下的所有库文件拷贝到开发板 Linux 系统/usr/lib 目录。拷贝 lib 目录下的库文件时,需要注意符号链接的问题, 不能破坏原有的符号链接; 可以将 lib 目录下的所有文件打包成压缩包的形式。接着执行 libjpeg 提供的测试工具,看看我们移植成没:

djpeg 是编译 libjpeg 源码得到的测试工具(在 libjpeg 安装目录下的 lib 目录中) ,当执行命令之后,能够成功打印出这些信息就表示我们的移植成功了!

libjpeg 使用说明

libjpeg 提供 JPEG 解码、 JPEG 编码和其他的 JPEG 功能的实现, 本小节我们只给大家介绍如何使用libjpeg 提供的库函数对.jpg/.jpeg 进行解码(解压),得到 RGB 数据。

首先,使用 libjpeg 库需要在我们的应用程序中包含它的头文件 jpeglib.h,该头文件包含了一些结构体数据结构以及 API 接口的申明。 先来看看解码操作的过程:

⑴、创建 jpeg 解码对象;

⑵、指定解码数据源;

⑶、 读取图像信息;

⑷、设置解码参数;

⑸、开始解码;

⑹、读取解码后的数据;

⑺、解码完毕;

⑻、释放/销毁解码对象。

以上便是整个解码操作的过程, 用 libjpeg 库解码 jpeg 数据的时候,最重要的一个数据结构为 structjpeg_decompress_struct 结构体, 该数据结构记录着 jpeg 数据的详细信息, 也保存着解码之后输出数据的详细信息。 除此之外, 还需要定义一个用于处理错误的对象, 错误处理对象是一个 struct jpeg_error_mgr 结构体变量。

cpp 复制代码
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;

以上就定义了 JPEG 解码对象和错误处理对象。

错误处理

使用 libjpeg 库函数的时候难免会产生错误,所以我们在使用 libjpeg 解码之前,首先要做好错误处理。在 libjpeg 库中,实现了默认错误处理函数,当错误发生时, 譬如如果内存不足、文件格式不对等, 则会 libjpeg实现的默认错误处理函数, 默认错误处理函数将会调用 exit()结束束整个进程;当然,我们可以修改错误处理的方式, libjpeg 提供了接口让用户可以注册一个自定义错误处理函数。错误处理对象使用 struct jpeg_error_mgr 结构体描述,该结构体内容如下所示:

cpp 复制代码
/* Error handler object */
struct jpeg_error_mgr {
/* Error exit handler: does not return to caller */
JMETHOD(noreturn_t, error_exit, (j_common_ptr cinfo));
/* Conditionally emit a trace or warning message */
JMETHOD(void, emit_message, (j_common_ptr cinfo, int msg_level));
/* Routine that actually outputs a trace or error message */
JMETHOD(void, output_message, (j_common_ptr cinfo));
/* Format a message string for the most recent JPEG error or message */
JMETHOD(void, format_message, (j_common_ptr cinfo, char * buffer));
#define JMSG_LENGTH_MAX 200 /* recommended size of format_message buffer */
/* Reset error state variables at start of a new image */
JMETHOD(void, reset_error_mgr, (j_common_ptr cinfo));
/* The message ID code and any parameters are saved here.
* A message can have one string parameter or up to 8 int parameters.
*/
int msg_code;
#define JMSG_STR_PARM_MAX 80
union {
int i[8];
char s[JMSG_STR_PARM_MAX];
} msg_parm;
/* Standard state variables for error facility */
int trace_level; /* max msg_level that will be displayed */
/* For recoverable corrupt-data errors, we emit a warning message,
* but keep going unless emit_message chooses to abort. emit_message
* should count warnings in num_warnings. The surrounding application
* can check for bad data by seeing if num_warnings is nonzero at the
* end of processing.
*/
long num_warnings; /* number of corrupt-data warnings */
/* These fields point to the table(s) of error message strings.
* An application can change the table pointer to switch to a different
* message list (typically, to change the language in which errors are
* reported). Some applications may wish to add additional error codes
* that will be handled by the JPEG library error mechanism; the second
* table pointer is used for this purpose.
*
* First table includes all errors generated by JPEG library itself.
* Error code 0 is reserved for a "no such error string" message.
*/
const char * const * jpeg_message_table; /* Library errors */
int last_jpeg_message; /* Table contains strings 0..last_jpeg_message */
/* Second table can be added by application (see cjpeg/djpeg for example).
* It contains strings numbered first_addon_message..last_addon_message.
*/
const char * const * addon_message_table; /* Non-library errors */
int first_addon_message; /* code for first string in addon table */
int last_addon_message; /* code for last string in addon table */
};

error_exit 函数指针便指向了错误处理函数。使用 libjpeg 库函数 jpeg_std_error()会将 libjpeg 错误处理设置为默认处理方式。如下所示:

cpp 复制代码
//初始化错误处理对象、并将其与解压对象绑定
cinfo.err = jpeg_std_error(&jerr);
//如果我们要修改默认的错误处理函数,可这样操作:
void my_error_exit(struct jpeg_decompress_struct *cinfo)
{
/* ... */
}
cinfo.err.error_exit = my_error_exit;
创建解码对象

要使用 libjpeg 解码 jpeg 数据,这步是必须要做的。

cpp 复制代码
jpeg_create_decompress(&cinfo);

在创建解码对象之后,如果解码结束或者解码出错时,需要调用 jpeg_destroy_decompress 销毁/释放解码对象,否则将会内存泄漏。

设置数据源

也就是设置需要进行解码的 jpeg 文件,使用 jpeg_stdio_src()函数设置数据源:

cpp 复制代码
FILE *jpeg_file = NULL;
//打开.jpeg/.jpg 图像文件
jpeg_file = fopen("./image.jpg", "r"); //只读方式打开
if (NULL == jpeg_file) {
    perror("fopen error");
    return -1;
}
//指定图像文件
jpeg_stdio_src(&cinfo, jpeg_file);

待解码的 jpeg 文件使用标准 I/O 方式 fopen 将其打开。 除此之外, jpeg 数据源还可以来自内存中、而不一定的是文件流

读取 jpeg 文件的头信息

这个和创建解码对象一样,是必须要调用的,是约定,没什么好说的。 因为在解码之前,需要读取 jpeg文件的头部信息,以获取该文件的信息,这些获取到的信息会直接赋值给 cinfo 对象的某些成员变量。

cpp 复制代码
jpeg_read_header(&cinfo, TRUE);

调用 jpeg_read_header()后,可以得到 jpeg 图像的一些信息,譬如 jpeg 图像的宽度、高度、 颜色通道数以及 colorspace 等,这些信息会赋值给 cinfo 对象中的相应成员变量,如下所示:

cpp 复制代码
cinfo.image_width //jpeg 图像宽度
cinfo.image_height //jpeg 图像高度
cinfo.num_components //颜色通道数
cinfo.jpeg_color_space //jpeg 图像的颜色空间

支持的颜色包括如下几种:

cpp 复制代码
/* Known color spaces. */
typedef enum {
    JCS_UNKNOWN, /* error/unspecified */
    JCS_GRAYSCALE, /* monochrome */
    JCS_RGB, /* red/green/blue, standard RGB (sRGB) */
    JCS_YCbCr, /* Y/Cb/Cr (also known as YUV), standard YCC */
    JCS_CMYK, /* C/M/Y/K */
    JCS_YCCK, /* Y/Cb/Cr/K */
    JCS_BG_RGB, /* big gamut red/green/blue, bg-sRGB */
    JCS_BG_YCC /* big gamut Y/Cb/Cr, bg-sYCC */
} J_COLOR_SPACE;
设置解码处理参数

在进行解码之前,我们可以对一些解码参数进行设置, 这些参数都有一个默认值,调用jpeg_read_header()函数后,这些参数被设置成相应的默认值。直接对 cinfo 对象的成员变量进行修改即可,这里介绍两个比较有代表性的解码处理参数:

⚫ 输出的颜色(cinfo.out_color_space): 默认配置为 RGB 颜色,也就是 JCS_RGB;

⚫ 图像缩放操作(cinfo.scale_num 和 cinfo.scale_denom): libjpeg 可以设置解码出来的图像的大小,也就是与原图的比例。使用 scale_num 和 scale_denom 两个参数,解出来的图像大小就是scale_num/scale_denom, JPEG 当前仅支持 1/1、 1/2、 1/4、 和 1/8 这几种缩小比例。 默认是 1/1,也就是保持原图大小。譬如要将输出图像设置为原图的 1/2 大小,可进行如下设置:

cpp 复制代码
cinfo.scale_num=1;
cinfo.scale_denom=2;
开始解码

经过前面的参数设置,我们可以开始解码了,调用 jpeg_start_decompress()函数:

cpp 复制代码
jpeg_start_decompress(&cinfo);

在完成解压缩操作后 ,会将解压后的图像信息填充至 cinfo 结构中。譬如 ,输出图像宽度cinfo.output_width,输出图像高度 cinfo.output_height,每个像素中的颜色通道数 cinfo.output_components(比如灰度为 1,全彩色 RGB888 为 3)等。一般情况下,这些参数是在 jpeg_start_decompress 后才被填充到 cinfo 中的,如果希望在调用

jpeg_start_decompress 之前就获得这些参数,可以通过调用 jpeg_calc_output_dimensions()的方法来实现。

读取数据

接下来就可以读取解码后的数据了, 数据是按照行读取的, 解码后的数据按照从左到右、 从上到下的顺序存储,每个像素点对应的各颜色或灰度通道数据是依次存储, 譬如一个 24-bit RGB 真彩色的图像中,一行的数据存储模式为 B,G,R,B,G,R,B,G,R,...。libjpeg 默认解码得到的图像数据是 BGR888 格式,即 R 颜色在低 8 位、而 B 颜色在高 8 位。 可以定义一个 BGR888 颜色类型,如下所示:

cpp 复制代码
typedef struct bgr888_color {
    unsigned char red;
    unsigned char green;
    unsigned char blue;
} __attribute__ ((packed)) bgr888_t;

每次读取一行数据, 计算每行数据需要的空间大小,比如 RGB 图像就是宽度× 3(24-bit RGB 真彩色一个像素 3 个字节) ,灰度图就是宽度× 1(一个像素 1 个字节)。

cpp 复制代码
bgr888_t *line_buf = malloc(cinfo.output_width * cinfo.output_components);

以上我们分配了一个行缓冲区,它的大小为 cinfo.output_width * cinfo.output_components,也就是输出图像的宽度乘上每一个像素的字节大小。 我们除了使用 malloc 分配缓冲区外,还可以使用 libjpeg 的内存管理器来分配缓冲区,这个不再介绍!缓冲区分配好之后,接着可以调用 jpeg_read_scanlines()来读取数据, jpeg_read_scanlines()可以指定一次

读多少行,但是目前该函数还只能支持一次只读 1 行;函数如下所示:

cpp 复制代码
jpeg_read_scanlines(&cinfo, &buf, 1);

1 表示每次读取的行数,通常都是将其设置为 1。cinfo.output_scanline 表示接下来要读取的行对应的索引值, 初始化为 0(表示第一行)、 1 表示第二行等,每读取一行数据,该变量就会加 1,所以我们可以通过下面这种循环方式依次读取解码后的所有数据:

cpp 复制代码
while(cinfo.output_scanline < cinfo.output_height)
{
    jpeg_read_scanlines(&cinfo, buffer, 1);
    //do something
}
结束解码

解码完毕之后调用 jpeg_finish_decompress()函数:

cpp 复制代码
jpeg_finish_decompress(&cinfo);
释放/销毁解码对象

当解码完成之后,我们需要调用 jpeg_destroy_decompress()函数销毁/释放解码对象:

cpp 复制代码
jpeg_destroy_decompress(&cinfo);

libjpeg 应用编程

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <jpeglib.h>

typedef struct bgr888_color {
unsigned char red;
unsigned char green;
unsigned char blue;
} __attribute__ ((packed)) bgr888_t;

static int width; //LCD X 分辨率
static int height; //LCD Y 分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
static unsigned long line_length; //LCD 一行的长度(字节为单位)
static unsigned int bpp; //像素深度 bpp

static int show_jpeg_image(const char *path)
{
    struct jpeg_decompress_struct cinfo;
    struct jpeg_error_mgr jerr;
    FILE *jpeg_file = NULL;
    bgr888_t *jpeg_line_buf = NULL; //行缓冲区:用于存储从 jpeg 文件中解压出来的一行图像数据
    unsigned short *fb_line_buf = NULL; //行缓冲区:用于存储写入到 LCD 显存的一行数据
    unsigned int min_h, min_w;
    unsigned int valid_bytes;
    int i;

    //绑定默认错误处理函数
    cinfo.err = jpeg_std_error(&jerr);

    //打开.jpeg/.jpg 图像文件
    jpeg_file = fopen(path, "r"); //只读方式打开
    if (NULL == jpeg_file) {
        perror("fopen error");
        return -1;
    }

    //创建 JPEG 解码对象
    jpeg_create_decompress(&cinfo);

    //指定图像文件
    jpeg_stdio_src(&cinfo, jpeg_file);

    //读取图像信息
    jpeg_read_header(&cinfo, TRUE);
    printf("jpeg 图像大小: %d*%d\n", cinfo.image_width, cinfo.image_height);

    //设置解码参数
    cinfo.out_color_space = JCS_RGB;//默认就是 JCS_RGB
    //cinfo.scale_num = 1;
    //cinfo.scale_denom = 2;

    //开始解码图像
    jpeg_start_decompress(&cinfo);

    //为缓冲区分配内存空间
    jpeg_line_buf = malloc(cinfo.output_components * cinfo.output_width);
    fb_line_buf = malloc(line_length);

    //判断图像和 LCD 屏哪个的分辨率更低
    if (cinfo.output_width > width)
        min_w = width;
    else
        min_w = cinfo.output_width;
    if (cinfo.output_height > height)
        min_h = height;
    else
        min_h = cinfo.output_height;

    //读取数据
    valid_bytes = min_w * bpp / 8;//一行的有效字节数 表示真正写入到 LCD 显存的一行数据的大小
    while (cinfo.output_scanline < min_h) {
        jpeg_read_scanlines(&cinfo, (unsigned char **)&jpeg_line_buf, 1);//每次读取一行数据
        //将读取到的 BGR888 数据转为 RGB565
        for (i = 0; i < min_w; i++)
            fb_line_buf[i] = ((jpeg_line_buf[i].red & 0xF8) << 8) |
                             ((jpeg_line_buf[i].green & 0xFC) << 3) |
                             ((jpeg_line_buf[i].blue & 0xF8) >> 3);
        memcpy(screen_base, fb_line_buf, valid_bytes);
        screen_base += width;//+width 定位到 LCD 下一行显存地址的起点
    }

    //解码完成
    jpeg_finish_decompress(&cinfo); //完成解码
    jpeg_destroy_decompress(&cinfo);//销毁 JPEG 解码对象、释放资源

    //关闭文件、释放内存
    fclose(jpeg_file);
    free(fb_line_buf);
    free(jpeg_line_buf);
    return 0;
}

int main(int argc, char *argv[])
{
    struct fb_fix_screeninfo fb_fix;
    struct fb_var_screeninfo fb_var;
    unsigned int screen_size;
    int fd;

    /* 传参校验 */
    if (2 != argc) {
        fprintf(stderr, "usage: %s <jpeg_file>\n", argv[0]);
        exit(-1);
    }

    /* 打开 framebuffer 设备 */
    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    /* 获取参数信息 */
    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
    line_length = fb_fix.line_length;
    bpp = fb_var.bits_per_pixel;
    screen_size = line_length * fb_var.yres;
    width = fb_var.xres;
    height = fb_var.yres;

    /* 将显示缓冲区映射到进程地址空间 */
    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == (void *)screen_base) {
        perror("mmap error");
        close(fd);
        exit(EXIT_FAILURE);
    }

    /* 显示 JPEG图片 */
    memset(screen_base, 0xFF, screen_size);
    show_jpeg_image(argv[1]);

    /* 退出 */
    munmap(screen_base, screen_size); //取消映射
    close(fd); //关闭文件
    exit(EXIT_SUCCESS); //退出进程
}

在 while 循环中,通过 jpeg_read_scanlines()每次读取一行数据,注意, jpeg_read_scanlines()函数的第二个参数是一个 unsigned char **类型指针。读取到数据之后,需要将其转为 RGB565 格式,因为我们这个开发板出厂系统, LCD 是 RGB565 格式的显示设备。编译上述代码:

bash 复制代码
${CC} -o testApp testApp.c -I /home/dt/tools/jpeg/include -L /home/dt/tools/jpeg/lib -ljpeg

LCD 应用编程显示 PNG 图片

上一章介绍了如何使用 libjpeg 库对 jpeg 图像进行解码、并显示到 LCD 屏上,除了 jpeg 图像之外, png图像也很常见,那本节我们就来学习如何对 png 图像进行解码、并显示到 LCD 屏上。

本节将会讨论如下主题。

⚫ PNG 简介;

⚫ libpng 库简介;

⚫ libpng 库移植;

⚫ 使用 libpng 库函数对 PNG 图像进行解码;

PNG 简介

PNG(便携式网络图形格式 PortableNetwork Graphic Format, 简称 PNG) 是一种采用无损压缩算法的位图格式,其设计目的是试图替代 GIF 和 TIFF 文件,同时增加一些 GIF 文件所不具备的特性。 PNG 使用从LZ77 派生的无损数据压缩算法,它压缩比高,生成文件体积小,并且支持透明效果,所以被广泛使用。

特点

⚫ 无损压缩: PNG 文件采用 LZ77 算法的派生算法进行压缩,其结果是获得高的压缩比,不损失数据。它利用特殊的编码方法标记重复出现的数据,因而对图像的颜色没有影响,也不可能产生颜色的损失,这样就可以重复保存而不降低图像质量。

⚫ 体积小: 在保证图片清晰、逼真、不失真的前提下, PNG 使用从 LZ77 派生的无损数据压缩算法,它压缩比高,生成文件体积小;

⚫ 索引彩色模式: PNG-8 格式与 GIF 图像类似,同样采用 8 位调色板将 RGB 彩色图像转换为索引彩色图像。图像中保存的不再是各个像素的彩色信息,而是从图像中挑选出来的具有代表性的颜色编号,每一编号对应一种颜色, 图像的数据量也因此减少,这对彩色图像的传播非常有利。

⚫ 更优化的网络传输显示: PNG 图像在浏览器上采用流式浏览,即使经过交错处理的图像会在完全下载之前提供浏览者一个基本的图像内容,然后再逐渐清晰起来。它允许连续读出和写入图像数据,这个特性很适合于在通信过程中显示和生成图像。

⚫ 支持透明效果: PNG 可以为原图像定义 256 个透明层次,使得彩色图像的边缘能与任何背景平滑地融合,从而彻底地消除锯齿边缘。这种功能是 GIF 和 JPEG 没有的。

关于 PNG 格式就介绍这么多。

libpng 简介

对于 png 图像,我们可以使用 libpng 库对其进行解码,跟 libjpeg 一样,它也是一套免费、开源的 C 语言函数库,支持对 png 图像文件解码、编码等功能。

zlib 移植

zlib 其实是一套包含了数据压缩算法的函式库,此函数库为自由软件, 是一套免费、开源的 C 语言函数库,所以我们可以获取到它源代码。libpng 依赖于 zlib 库, 所以要想移植 libpng 先得移植 zlib 库才可以, zlib 也好、 libpng 也好,其实移植过程非常简单,无非就是下载源码、编译源码这样的一些工作,那本小节就向大家介绍如何移植 zlib。

下载源码包

我们可以进入到 https://www.zlib.net/fossils/这个链接地址下载 zlib 源码包:

往下翻,找到一个合适的版本, 这里我们就选择 1.2.10 版本的 zlib。

编译源码

将下载的 zlib-1.2.10.tar.gz 压缩文件拷贝到 Ubuntu 系统的用户家目录下,然后将其解压开:

cpp 复制代码
tar -xzf zlib-1.2.10.tar.gz

解压之后就会得到 zlib-1.2.10 文件夹,这就是 zlib 的源代码目录。在编译 zlib 之前,我们先在 tools 目录下创建一个名为 zlib 的文件夹,作为 zlib 库的安装目录。接着我们进入到 zlib 的源码目录 zlib-1.2.10。在此之前,先对交叉编译工具的环境进行初始化,使用 source 执行交叉编译工具安装目录下的environment-setup-cortexa7hf-neon-poky-linux-gnueabi 脚本文件(如果已经初始化过了,那就不用再进行初始化了):

cpp 复制代码
source /opt/fsl-imx-x11/4.1.15-2.1.0/environment-setup-cortexa7hf-neon-poky-linux-gnueabi

执行下面这条命令对 zlib 工程进行配置:

cpp 复制代码
./configure --prefix=/home/dt/tools/zlib/

--prefix 选项指定 zlib 库的安装目录,将家目录下的 tools/zlib 作为 zlib 库的安装目录。

配置完成之后,直接 make 编译

编译完成之后,接着执行 make install 安装即可!

移植到开发板

进入到 zlib 安装目录下,将 lib 目录下的所有动态链接库文件拷贝到开发板 Linux 系统/usr/lib 目录;注意在拷贝之前,需要先将出厂系统中原有的 zlib 库文件删除, 在开发板 Linux 系统下执行命令:

cpp 复制代码
rm -rf /usr/lib/libz.* /lib/libz.*

删除之后,再将我们编译得到的 zlib 库文件拷贝到开发板/usr/lib 目录, 拷贝库文件时,需要注意符号链接的问题,不能破坏原有的符号链接。

拷贝过去之后,开发板/usr/lib 目录下就应该存在这些库文件,如下所示:

libpng 移植

下载源码包

首先下载 libpng 源码包,进入 https://github.com/glennrp/libpng/releases 链接地址,如下:

编译源码

将下载的 libpng-1.6.35.tar.gz 压缩包文件拷贝到 Ubuntu 系统的用户家目录下,接着将其解压,解压之后得到 libpng-1.6.35 文件夹,这便是 libpng 的源码目录。在编译 libpng 之前,先在 tools 目录下创建一个名为 png 的文件夹,作为 libpng 库的安装目录,接着我们进入到 libpng 源码目录下,配置、编译、安装,一套流程下来就 OK 了!在此之前,先对交叉编译工具的环境进行初始化,使用 source 执行交叉编译工具安装目录下的environment-setup-cortexa7hf-neon-poky-linux-gnueabi 脚本文件(如果已经初始化过了,那就不用再进行初始化了),libpng 依赖于 zlib 库,前面我们已经将 zlib 库编译成功了,但是我们得告知编译器 zlib 库的安装目录,这样编译器才能找到 zlib 的库文件以及头文件,编译 libpng 的时才不会报错。执行下面这三条命令,将 zlib 库安装目录下的 include 和 lib 路径导出到环境变量:

bash 复制代码
export LDFLAGS="${LDFLAGS} -L/home/dt/tools/zlib/lib"
export CFLAGS="${CFLAGS} -I/home/dt/tools/zlib/include"
export CPPFLAGS="${CPPFLAGS} -I/home/dt/tools/zlib/include"

接着执行下面这条命令对 libpng 源码工程进行配置:

bash 复制代码
./configure --prefix=/home/dt/tools/png --host=arm-poky-linux-gnueabi

--prefix 选项指定 libpng 的安装目录,将家目录下的 tools/png 作为 libpng 的安装目录。

接着执行 make 进行编译

最后执行 make install 安装即可

移植到开发板

进入到 libpng 安装目录,将 bin 目录下的所有测试工具拷贝到开发板 Linux 系统/usr/bin 目录;将 lib 目录下的所有库文件拷贝到 Linux 系统/usr/lib 目录,注意在拷贝之前,先将开发板出厂系统中已经移植好的libpng 库文件删除,删除之后,再将编译得到的 libpng 库文件拷贝到开发板/usr/lib 目录,拷贝库文件时,需要注意符号链接的问题,不能破坏原有的符号链接。拷贝过去之后,开发板/usr/lib 目录下就应该存在这些库文件,如下所示:

libpng 使用说明

本小节向大家简单地介绍如何使用 libpng 对 png 图像进行解码, libpng 除了解码功能之外,还包含编码功能,也就是创建 png 压缩文件,当然,这个笔者就不再介绍了。 libpng 官方提供一份非常详细地使用文档,笔者也是参考了这份文档给大家进行介绍的,这份文档的链接地址如下:

bash 复制代码
http://www.libpng.org/pub/png/libpng-1.4.0-manual.pdf
http://www.libpng.org/pub/png/libpng-manual.txt

这两份文档的内容是一样的,第一份是 pdf 文档、第二份是 txt 文档,如果大家想更加深入的了解、学习,那么可以查阅这份文档。

libpng 的数据结构

首先,使用 libpng 库需要包含它的头文件<png.h>。 png.h 头文件中包含了 API、数据结构的申明, libpng中有两个很重要的数据结构体: png_struct 和 png_info。

png_struct 作为 libpng 库函数内部使用的一个数据结构体,除了作为传递给每个 libpng 库函数调用的第一个变量外,在大多数情况下不会被用户所使用。 使用 libpng 之前, 需要创建一个 png_struct 对象并对其进行初始化操作,该对象由 libpng 库内部使用,调用 libpng 库函数时, 通常需要把这个对象作为参数传入。png_info 数据结构体描述了 png 图像的信息, 在以前旧的版本中,用户可以直接访问 png_info 对象中的成员, 譬如查看图像的宽、高、像素深度、 修改解码参数等; 然而,这往往会导致出现一些问题,因此新的版本中专门开发了一组 png_info 对象的访问接口: get 方法 png_get_XXX 和 set 方法 png_set_XXX, 建议大家通过 API 来访问这些成员。

创建和初始化 png_struct 对象

首先第一步是创建 png_struct 对象、并对其进行初始化操作,使用 png_create_read_struct()函数创建一个 png_struct 对象、并完成初始化操作, read 表示我们需要创建的是一个用于 png 解码的 png_struct 对象;同理可以使用 png_create_write_struct()创建一个用于 png 编码的 png_struct 对象。png_create_read_struct 函数原型如下所示:

cpp 复制代码
png_structp png_create_read_struct(png_const_charp user_png_ver, png_voidp error_ptr, png_error_ptr error_fn, png_error_ptr warn_fn);

它的是返回值是一个 png_structp 指针,指向一个 png_struct 对象;所以 png_create_read_struct()函数创建 png_struct 对象之后,会返回一个指针给调用者,该指针指向所创建的 png_struct 对象。 但如果创建对象失败,则会返回 NULL,所以调用者可以通过判断返回值是否为 NULL 来确定 png_create_read_struct()函数执行是否成功!该函数有 4 个参数, 第一个参数 user_png_ver 指的是 libpng 的版本信息,通常将其设置为PNG_LIBPNG_VER_STRING,这是 png.h 头文件中定义的一个宏,其内容便是 libpng 的版本号信息,如下:

cpp 复制代码
#define PNG_LIBPNG_VER_STRING "1.6.35"

创建、初始化 png_struct 对象时, 调用者可以指定自定义的错误处理函数和自定义的警告处理函数,通过参数 error_fn 指向自定义的错误处理函数、通过参数 warn_fn 指向自定义的警告处理函数, 而参数 error_ptr表示传递给这些函数所使用的数据结构的指针; 当然也可将它们设置为 NULL,表示使用 libpng 默认的错误处理函数以及警告函数。 使用示例如下:

cpp 复制代码
png_structp png_ptr = NULL;
png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png_ptr)
    return -1;
创建和初始化 png_info 对象

png_info数据结构体描述了png图像的信息,同样也需要创建png_info对象,调用png_create_info_struct()函数创建一个 png_info 对象,其函数原型如下所示:

cpp 复制代码
png_infop png_create_info_struct(png_const_structrp png_ptr);

该函数返回一个 png_infop 指针,指向一个 png_info 对象,所以 png_create_info_struct()函数创建 png_info对象之后,会将它的指针返回给调用者;如果创建失败,则会返回 NULL,所以调用者可以通过判断返回值是否为 NULL 来确定函数调用是否成功!该函数有一个参数,需要传入一个png_struct对象的指针,内部会将它们之间建立关联,当销毁png_struct对象时、也可将 png_info 对象销毁。 使用示例如下:

cpp 复制代码
png_infop info_ptr = NULL;
info_ptr = png_create_info_struct(png_const_structrp png_ptr);
if (NULL == info_ptr) {
    png_destroy_read_struct(&png_ptr, NULL, NULL);
    return -1;
}

png_destroy_read_struct()函数用于销毁 png_struct 对象的函数,后面再给大家介绍。

设置错误返回点

调用 png_create_read_struct()函数创建 png_struct 对象时,调用者可以指定一个自定义的错误处理函数,当 libpng 工作发生错误时,它就会执行这个错误处理函数;但如果调用者并未指定自定义的错误处理函数,那么 libpng 将会使用默认的错误处理函数,其实默认的错误处理函数会执行一个跳转动作,跳转到程序中的某一个位置,我们把这个位置称为错误返回点。这样,当调用者未指定自定义错误处理函数时, 当 libpng 遇到错误时, 它会执行默认错误处理函数,而默认错误处理函数会跳转到错误返回点,通常这个错误返回点就是在我们程序中的某个位置,我们期望libpng 发生错误时能够回到我们的程序中, 为什么要这样做呢?因为发生错误时不能直接终止退出,而需要执行释放、销毁等清理工作,譬如前面创建的 png_struct 和 png_info 对象,需要销毁,避免内存泄漏。那如何在我们的程序中设置错误返回点呢? 在此之前,笔者需要向大家介绍两个库函数: setjmp 和longjmp。
setjmp 和 longjmp

在 C 语言中,在一个函数中执行跳转,我们可以使用 goto 语句,笔者也经常使用 goto 语句,尤其是在开发驱动程序时;但 goto 语句只能在一个函数内部进行跳转,不能跨越函数,譬如从 func1()函数跳转到func2()函数,如果想要实现这种跨越函数间的跳转,在 Linux 下,我们可以使用库函数 setjmp 和 longjmp。setjmp 函数用于设置跳转点,也就是跳转位置; longjmp 执行跳转,那么它会跳转到 setjmp 函数所设置的跳转点,来看看这两个函数的原型:

cpp 复制代码
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

可以看到 setjmp 和 longjmp 函数都有一个 env 参数,这是一个 jmp_buf 类型的参数, jmp_buf 是一种特殊类型,当调用 setjmp()时,它会把当前进程环境的各种信息保存到 env 参数中,而调用 longjmp()也必须指定相同的参数,这样才可跳转到 setjmp 所设置的跳转点。从编程角度来看,调用 longjmp()函数后,看起来就和第二次调用 setjmp()返回时完全一样,可以通过检查 setjmp()函数的返回值,来区分 setjmp()是初次调用返回还是第二次"返回",初始调用返回值为 0,后续"伪"返回的返回值为 longjmp()调用中参数 val 所指定的任意值,通过对 val 参数使用不同的值,可以区分出程序中跳转到同一位置的多个不同的起跳位置。所以,通常情况下,调用 longjmp()时,不会将参数 val 设置为 0,这样将会导致无法区分 setjmp()是初次返回还是后续的"伪"返回,这里大家要注意!好,那么关于这两个函数就向大家介绍这么多,我们来看一个例子:

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

static jmp_buf buf;

static void hello(void)
{
    printf("hello world!\n");
    longjmp(buf,1);
    printf("Nice to meet you!\n");
}

int main(void)
{
    if(0 == setjmp(buf)) {
        printf("First return\n");
        hello();
    }
    else
        printf("Second return\n");

    exit(0);
}

我们直接在 Ubuntu 系统下编译运行,运行结果如下所示:

libpng 设置错误返回点

libpng 库默认也使用 setjmp/longjmp 这两个库函数组合来处理发生错误时的跳转,当 libpng 遇到错误时,执行默认错误处理函数,默认错误处理函数会调用 longjmp()来进行跳转,所以我们需要使用 setjmp()来为 libpng 设置一个错误返回点。设置方法如下:

cpp 复制代码
/* 设置错误返回点 */
if (setjmp(png_jmpbuf(png_ptr))) {
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    return -1;
}

png_jmpbuf()函数可以获取到 png_struct 对象中的 jmp_buf 变量,那么后续 libpng 库调用 longjmp 执行跳转时也是使用这个变量。 我们可以在错误返回点执行一些清理工作。

指定数据源

也就是指定需要进行解码的 png 图像,通常可以使用多种方式来指定数据源,譬如文件输入流、内存中的数据流等,这里笔者以文件输入流为例。libpng 提供了 png_init_io()函数, png_init_io()可以指定数据源,该数据源以文件输入流的方式提供,来看看函数原型:

cpp 复制代码
png_init_io(png_structrp png_ptr, png_FILE_p fp);

第一个参数是 png_ptr,指向 png_struct 对象;而第二个参数 fp 则是一个 png_FILE_p 类型指针,其实就是标准 I/O 中的 FILE *指针。所以由此可知,我们需要先使用 fopen()函数将 png 文件打开,然后得到指向该文件的 FILE *类型指针。

cpp 复制代码
使用示例如下:
FILE *png_file = NULL;
/* 打开 png 文件 */
png_file = fopen("image.png", "r"); //以只读方式打开
if (NULL == png_file) {
    perror("fopen error");
    return -1;
}
/* 指定数据源 */
png_init_io(png_ptr, png_file);
读取 png 图像数据并解码

从 png 文件中读取数据并进行解码,将解码后的图像数据存放在内存中,待用户读取。 关于这一步的操作, libpng 提供了两种方式去处理: high-level 接口处理和 low-level 接口处理。其实 high-level 只是对 lowlevel 方式进行了一个封装,使用 high-level 接口非常方便只需一个函数即可,但缺点是灵活性不高、被限定了;而 low-level 接口恰好相反, 灵活性高、但需要用户调用多个 API; 所以具体使用哪种方式要看你的需求。

high-level 接口

通常在满足以下两个条件时使用 high-level 接口:

⚫ 用户的内存空间足够大, 可以一次性存放整个 png 文件解码后的数据;

⚫ 数据输出格式限定为 libpng 预定义的数据转换格式。

在满足以上两个条件时, 可以使用 high-level 接口, libpng 预定义数据转换类型包括:

|----------------------------|------------------------------------------------|
| libpng 预定义转换类型 | 说明 |
| PNG_TRANSFORM_IDENTITY | No transformation |
| PNG_TRANSFORM_STRIP_16 | Strip 16-bit samples to 8 bits |
| PNG_TRANSFORM_STRIP_ALPHA | Discard the alpha channel |
| PNG_TRANSFORM_PACKING | Expand 1, 2 and 4-bit samples to bytes |
| PNG_TRANSFORM_PACKSWAP | Change order of packed pixels to LSB first |
| PNG_TRANSFORM_EXPAND | Perform set_expand() |
| PNG_TRANSFORM_INVERT_MONO | Invert monochrome images |
| PNG_TRANSFORM_SHIFT | Normalize pixels to the sBIT depth |
| PNG_TRANSFORM_BGR | Flip RGB to BGR, RGBA to BGRA |
| PNG_TRANSFORM_SWAP_ALPHA | Flip RGBA to ARGB or GA to AG |
| PNG_TRANSFORM_INVERT_ALPHA | Change alpha from opacity to transparency |
| PNG_TRANSFORM_SWAP_ENDIAN | Byte-swap 16-bit samples |
| PNG_TRANSFORM_GRAY_TO_RGB | Expand grayscale samples to RGB (or GA to RGBA |

这些转换当中,还不包括背景颜色设置(透明图) 、伽马变换、抖动和填充物等,使用 high-level 接口只能使用以上这些预定义的转换类型,而其它的配置则保持默认。

high-level 接口只需要使用一个函数 png_read_png(),调用该函数将一次性把整个 png 文件的图像数据解码出来、将解码后的数据存放在内存中,如下所示:

cpp 复制代码
png_read_png(png_structrp png_ptr, png_inforp info_ptr, int transforms, png_voidp params);

第一个参数 png_ptr 为指向 png_struct 对象的指针,第二个参数 info_ptr 为指向 png_info 对象的指针;而第三个参数 transforms 为整型参数, 取值为上表所列出的 libpng 预定义的数据转换类型,可以使用 or(C语言的或 | 运算符)组合多个转换类型。 使用示例如下:

cpp 复制代码
png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);

该函数相当于调用一系列 low-level 函数(下文将会介绍) , 调用顺序如下所示:

⑴、 调用 png_read_info 函数获得 png 图像信息;

⑵、根据参数 transforms 所指定的转换类型对数据输出转换格式进行设置;

⑶、 调用 png_read_image 一次性把整个 png 文件的图像数据解码出来、并将解码后的数据存放在内存中。

⑷、 调用 png_read_end 结束解码。
low-level 接口

使用 low-level 接口,需要用户将函数 png_read_png()所做的事情一步一步执行:

a)、读取 png 图像的信息

首先我们要调用 png_read_info()函数获取 png 图像的信息:

png_read_info(png_ptr, info_ptr);

该函数会把 png 图像的信息读入到 info_ptr 指向的 png_info 对象中。

b)、查询图像的信息

前面提到 png_read_info()函数会把 png 图像的信息读入到 png_info 对象中,接下来我们可以调用 libpng提供的 API 查询这些信息。

cpp 复制代码
unsigned int width = png_get_image_width(png_ptr, info_ptr); //获取 png 图像的宽度
unsigned int height = png_get_image_height(png_ptr, info_ptr); //获取 png 图像的高度
unsigned char depth = png_get_bit_depth(png_ptr, info_ptr); //获取 png 图像的位深度
unsigned char color_type = png_get_color_type(png_ptr, info_ptr); //获取 png 图像的颜色类型

color type 在 png.h 头文件中定义,如下所示:

cpp 复制代码
/* These describe the color_type field in png_info. */
/* color type masks */
#define PNG_COLOR_MASK_PALETTE 1
#define PNG_COLOR_MASK_COLOR 2
#define PNG_COLOR_MASK_ALPHA 4
/* color types. Note that not all combinations are legal */
#define PNG_COLOR_TYPE_GRAY 0
#define PNG_COLOR_TYPE_PALETTE (PNG_COLOR_MASK_COLOR | PNG_COLOR_MASK_PALETTE)
#define PNG_COLOR_TYPE_RGB (PNG_COLOR_MASK_COLOR)
#define PNG_COLOR_TYPE_RGB_ALPHA (PNG_COLOR_MASK_COLOR |
PNG_COLOR_MASK_ALPHA)
#define PNG_COLOR_TYPE_GRAY_ALPHA (PNG_COLOR_MASK_ALPHA)
/* aliases */
#define PNG_COLOR_TYPE_RGBA PNG_COLOR_TYPE_RGB_ALPHA
#define PNG_COLOR_TYPE_GA PNG_COLOR_TYPE_GRAY_ALPHA

c)、设置解码输出参数(转换参数)

这步非常重要,用户可以指定数据输出转换的格式,比如 RGB888, BGR888、 ARGB8888 等数据输出格式, libpng 提供了很多 set 方法(png_set_xxxxx 函数)来实现这些设置, 例如如下代码:

cpp 复制代码
unsigned char depth = png_get_bit_depth(png_ptr, info_ptr);
unsigned char color_type = png_get_color_type(png_ptr, info_ptr);
if (16 == depth)
png_set_strip_16(png_ptr); //将 16 位深度转为 8 位深度
if (8 > depth)
png_set_expand(png_ptr); //如果位深小于 8,则扩展为 24-bit RGB

if (PNG_COLOR_TYPE_GRAY_ALPHA == color_type)
png_set_gray_to_rgb(png_ptr); //如果是灰度图,则转为 RGB

关于这些函数的作用和使用方法,大家可以打开 libpng 的头文件 png.h 进行查看,每个函数它都有相应的注释信息以及参数列表。 如上我们列举了几个 png_set_xxx 转换函数, 这种转换函数还很多, 这里便不再一一进行介绍,具体请查看 libpng 的使用手册以了解他们的作用。虽然 libpng 提供了很多转换函数, 可以调用它们对数据的输出格式进行设置, 但是用户的需求是往往无限的,很多输出格式 libpng 并不是原生支持的, 譬如 YUV565、 RGB565、 YUYV 等,为了解决这样的问题, libpng 允许用户设置自定义转换函数,可以让用户注册自定义转换函数给 libpng 库, libpng 库对输出数据进行转换时,会调用用户注册的自定义转换函数进行转换。调用者通过 png_set_read_user_transform_fn()函数向 libpng 注册一个自定义转换函数, 另外调用者还可以通过 png_set_user_transform_info()函数告诉 libpng 自定义转换函数的用户自定义数据结构和输出数据的详细信息,比如颜色深度、 颜色通道(channel)等等。关于这些内容,大家自己去查阅 libpng 的使用帮助文档。

d)、 更新 png 数据的详细信息

经过前面的设置之后, 信息肯定会有一些变化, 我们需要调用 png_read_update_info 函数更新信息:

cpp 复制代码
png_read_update_info(png_ptr, info_ptr);

该函数将会更新保存在 info_ptr 指向的 png_info 对象中的图像信息。

e)、读取 png 数据并解码

前面设置完成之后, 接下来便可对 png 文件的数据进行解码了。调用 png_read_image()函数可以一次性把整个 png 文件的图像数据解码出来、并将解码后的数据存放在用户提供的内存区域中, 使用示例如下:

cpp 复制代码
png_read_image(png_ptr, row_pointers);

该函数无返回值,参数 png_ptr 指向 png_struct 对象;第二个参数 row_pointers 是一个 png_bytepp 类型的指针变量,也就是 unsigned char **,是一个指针数组,如下所示:

cpp 复制代码
png_bytep row_pointers[height];

调用该函数,需要调用者提供足够大的内存空间,可以保存整个图像的数据,这个内存空间的大小通常是解码后数据的总大小;调用者分配内存空间后, 需要传入指向每一行的指针数组,如下所示:

cpp 复制代码
png_bytep row_pointers[height] = {0};
size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);//获取每一行数据的字节大小
int row;
/* 为每一行数据分配一个缓冲区 */
for (row = 0; row < height; row++)
row_pointers[row] = png_malloc(png_ptr, rowbytes);
png_read_image(png_ptr, row_pointers);

Tips: png_malloc()函数是 libpng 提供的一个 API,其实就等价于库函数 malloc。

除了 png_read_image()函数之外, 我们也可以调用 png_read_rows()一次解码 1 行或多行数据、并将解码后的数据存放在用于提供的内存区域中,譬如:

cpp 复制代码
size_t rowbytes = png_get_rowbytes(png_ptr, info_ptr);//获取每一行数据的字节大小
png_bytep row_buf = png_malloc(png_ptr, rowbytes);//分配分缓冲、用于存储一行数据
int row;
for (row = 0; row < height; row++) {

png_read_rows(png_ptr, &row_buf, NULL, 1);//每次读取、解码一行数据(最后一个数字 1 表示每次 1 行)
/* 对这一行数据进行处理: 譬如刷入 LCD 显存进行显示 */
do_something();
}

png_read_rows 会自动跳转处理下一行数据。

由此可知,在 low-level 接口,调用 png_read_image()或 png_read_rows()函数都需要向 libpng 提供用于存放数据的内存区域。但是在 high-level 接口中,调用 png_read_png()时我们并不需要自己分配缓冲区,png_read_png()函数内部会自动分配一块缓冲区,那我们如何获取到它分配的缓冲区呢?通过 png_get_rows()函数得到,下小节介绍。

f)、 png_read_end()结束读取、解码

当整个 png 文件的数据已经读取、解码完成之后,我们可以调用 png_read_end()结束,代码如下:

cpp 复制代码
png_read_end(png_ptr, info_ptr);
读取解码后的数据

解码完成之后,我们便可以去获取解码后的数据了,要么那它们做进一步的处理、要么直接刷入显存显示到 LCD 上; 对于 low-level 方式, 存放图像数据的缓冲区是由调用者分配的, 所以直接从缓冲区中获取数据即可!对于 high-level 方式,存放图像数据的缓冲区是由 png_read_png()函数内部所分配的,并将缓冲区与png_struct 对象之间建立了关联,我们可以通过 png_get_rows()函数获取到指向每一行数据缓冲区的指针数组,如下所示:

cpp 复制代码
png_bytepp row_pointers = NULL;

row_pointers = png_get_rows(png_ptr, info_ptr);//获取到指向每一行数据缓冲区的指针数组

当我们销毁 png_struct 对象时,由 png_read_png()所分配的缓冲区也会被释放归还给操作系统。

结束销毁对象

调用 png_destroy_read_struct()销毁 png_struct 对象,该函数原型如下所示:

cpp 复制代码
void png_destroy_read_struct(png_structpp png_ptr_ptr, png_infopp info_ptr_ptr, png_infopp end_info_ptr_ptr);

使用方法如下:

cpp 复制代码
png_destroy_read_struct(png_ptr, info_ptr, NULL);

libpng 应用编程

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <png.h>

static int width; //LCD X 分辨率
static int height; //LCD Y 分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
static unsigned long line_length; //LCD 一行的长度(字节为单位)
static unsigned int bpp; //像素深度 bpp

static int show_png_image(const char *path)
{
    png_structp png_ptr = NULL;
    png_infop info_ptr = NULL;
    FILE *png_file = NULL;
    unsigned short *fb_line_buf = NULL; //行缓冲区:用于存储写入到 LCD 显存的一行数据
    unsigned int min_h, min_w;
    unsigned int valid_bytes;
    unsigned int image_h, image_w;
    png_bytepp row_pointers = NULL;
    int i, j, k;

    /* 打开 png 文件 */
    png_file = fopen(path, "r"); //以只读方式打开
        if (NULL == png_file) {
        perror("fopen error");
        return -1;
    }

    /* 分配和初始化 png_ptr、 info_ptr */
    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png_ptr) {
        fclose(png_file);
        return -1;
    }

    info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr) {
        png_destroy_read_struct(&png_ptr, NULL, NULL);
        fclose(png_file);
        return -1;
    }

    /* 设置错误返回点 */
    if (setjmp(png_jmpbuf(png_ptr))) {
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        fclose(png_file);
        return -1;
    }

    /* 指定数据源 */
    png_init_io(png_ptr, png_file);

    /* 读取 png 文件 */
    png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_ALPHA, NULL);
    image_h = png_get_image_height(png_ptr, info_ptr);
    image_w = png_get_image_width(png_ptr, info_ptr);
    printf("分辨率: %d*%d\n", image_w, image_h);

    /* 判断是不是 RGB888 */
    if ((8 != png_get_bit_depth(png_ptr, info_ptr)) &&
        (PNG_COLOR_TYPE_RGB != png_get_color_type(png_ptr, info_ptr))) {
        printf("Error: Not 8bit depth or not RGB color");
        png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
        fclose(png_file);
        return -1;
    }

    /* 判断图像和 LCD 屏那个的分辨率更低 */
    if (image_w > width)
        min_w = width;
    else
        min_w = image_w;
    if (image_h > height)
        min_h = height;
    else
        min_h = image_h;
    valid_bytes = min_w * bpp / 8;

    /* 读取解码后的数据 */
    fb_line_buf = malloc(valid_bytes);
    row_pointers = png_get_rows(png_ptr, info_ptr);//获取数据
    unsigned int temp = min_w * 3; //RGB888 一个像素 3 个 bit 位
    for(i = 0; i < min_h; i++) {
        // RGB888 转为 RGB565
        for(j = k = 0; j < temp; j += 3, k++)
            fb_line_buf[k] = ((row_pointers[i][j] & 0xF8) << 8) |
                             ((row_pointers[i][j+1] & 0xFC) << 3) |
                             ((row_pointers[i][j+2] & 0xF8) >> 3);
        memcpy(screen_base, fb_line_buf, valid_bytes);//将一行数据刷入显存
        screen_base += width; //定位到显存下一行
    }

    /* 结束、销毁/释放内存 */
    png_destroy_read_struct(&png_ptr, &info_ptr, NULL);
    free(fb_line_buf);
    fclose(png_file);
    return 0;
}

int main(int argc, char *argv[])
{
    struct fb_fix_screeninfo fb_fix;
    struct fb_var_screeninfo fb_var;
    unsigned int screen_size;
    int fd;

    /* 传参校验 */
    if (2 != argc) {
        fprintf(stderr, "usage: %s <png_file>\n", argv[0]);
        exit(-1);
    }

    /* 打开 framebuffer 设备 */
    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    /* 获取参数信息 */
    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
    line_length = fb_fix.line_length;
    bpp = fb_var.bits_per_pixel;
    screen_size = line_length * fb_var.yres;
    width = fb_var.xres;
    height = fb_var.yres;

    /* 将显示缓冲区映射到进程地址空间 */
    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == (void *)screen_base) {
        perror("mmap error");
        close(fd);
        exit(EXIT_FAILURE);
    }

    /* 显示 PNG 图片 */
    memset(screen_base, 0xFF, screen_size);//屏幕刷白
    show_png_image(argv[1]);

    /* 退出 */
    munmap(screen_base, screen_size); //取消映射
    close(fd); //关闭文件
    exit(EXIT_SUCCESS); //退出进程
}

编译:

bash 复制代码
${CC} -o testApp testApp.c -I/home/dt/tools/png/include -L/home/dt/tools/png/lib -L/home/dt/tools/zlib/lib -lpng -lz

LCD 横屏切换为竖屏

首先给大家普及一个基本的知识点,这种横屏、竖屏的切换与驱动程序无关,是应用层需要去解决的一个问题!本章将会讨论如下主题。

⚫ 横屏显示如何切换为竖屏显示;

⚫ 编写代码验证;

横屏显示如何切换为竖屏显示

开发板配套使用的这些 LCD 屏都是横屏显示的,包括正点原子 4.3 寸 480*272、 4.3 寸 800*480、 7 寸800*480、 7 寸 1024*600 以及 10.1 寸 1280*800 等这些 RGB LCD 屏; LCD 屏正向放置情况下(以 800*480分辨率为例) ,它的左上角就是坐标(0, 0)、左下角坐标是(0, 480-1)、右上角坐标是(800-1, 0)、右下角坐标是(800-1, 480-1),如下所示

这是硬件上固定的,它是一种不可修改的硬件属性,譬如你不能对 LCD 硬件进行配置,将屏幕左下角设置为起点(0, 0),这是不可以的; 像素点的排列顺序是从左到右、从上到下, 我们对 LCD 上不同像素点进行操作时,需要找到该像素点对应的显存地址,同样也是基于这种标准来的; 假设显存基地址为(unsignedchar *)base, 那么定位一个(x, y)坐标像素点对应的地址的公式为 base + (y * width + x) * pix_bytes,其中pix_bytes 表示一个像素点使用 pix_bytes 个字节来描述。示意图如下所示:

上图已经很直观、明了的说明了 LCD 屏上各个像素点与显存空间的对应关系。但是在很多的应用场合中,往往需要以竖屏的方式来显示画面,譬如手机就是一个很好的例子,相信大家的手机都是竖屏方式显示的;甚至还有一些电子产品既能支持横屏也能支持竖屏显示,当然这是针对应用程序而言。那我们的应用程序中如何将 LCD 屏修改为竖屏显示呢?其实原理上非常简单,我相信大家都可以想到,譬如在应用程序中将左下角作为起点(0, 0),那么左上角对应就是(480-1, 0)、右下角对应就是(0, 800-1)、右上角对应就是(480-1, 800-1),如下图所示:

以上便是竖屏显示情况下,其中的一种坐标分布情况,当然这是应用程序认为的一种坐标分布,对于LCD 硬件来说, 实际物理上的起点坐标依然没有变化。那么在上图中竖屏这种情况下, 应用程序的坐标对应的像素点,它的显存地址就不能使用 base + (y *width + x) * pix_bytes 公式进行计算了;譬如上图竖屏方式下,起点坐标(0, 0)对应的实际物理坐标是(0, 480-1),同理它的显存地址也是通过实际物理坐标(0, 480-1)这个坐标计算而来、而不是通过(0, 0)计算。在上图中竖屏方式下,应用程序的(x, y)坐标点对应的显存地址可通过如下公式进行计算:

cpp 复制代码
base + ((height - 1- x) * width + y)) * pix_bytes;

公式中的 x 和 y 分别表示竖屏方式下的(x, y)坐标,当然这个公式仅适用于上图这种竖屏方式;你也可以把图旋转 180 度倒过来,同样也是竖屏,这种情况就不能用上面这条公式了。公式推导非常简单,没什么可解释的。

示例代码
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <linux/fb.h>

#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})

static int lcd_width; //LCD X 分辨率
static int lcd_height; //LCD Y 分辨率
static int lcd_max_y; //LCD Y 坐标最大值
static int user_width; //竖屏模式下 X 分辨率
static int user_height; //竖屏模式下 Y 分辨率
static unsigned short *screen_base = NULL; //映射后的显存基地址
/********************************************************************
* 函数名称: lcd_draw_point
* 功能描述: 打点
* 输入参数: x, y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_point(unsigned int x, unsigned int y, unsigned int color)
{
    unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
    /* 对传入参数的校验 */
    if (x >= user_width)
        x = user_width - 1;
    if (y >= user_height)
        y = user_height - 1;
    /* 填充颜色 */
    screen_base[(lcd_max_y-x) * lcd_width + y] = rgb565_color;
}
/********************************************************************
* 函数名称: lcd_draw_line
* 功能描述: 画线(水平或垂直线)
* 输入参数: x, y, dir, length, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_line(unsigned int x, unsigned int y, int dir,
unsigned int length, unsigned int color)
{
    unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
    unsigned int end;
    unsigned long temp;
    /* 对传入参数的校验 */
    if (x >= user_width)
        x = user_width - 1;
    if (y >= user_height)
        y = user_height - 1;
    /* 填充颜色 */
    temp = (lcd_max_y-x) * lcd_width + y;
    if (dir) { //水平线
        end = x + length - 1;
        if (end >= user_width)
        end = user_width - 1;
        for ( ; x <= end; x++, temp -= lcd_width)
        screen_base[temp] = rgb565_color;
    }
    else { //垂直线
        end = y + length - 1;
        if (end >= user_height)
            end = user_height - 1;
        for ( ; y <= end; y++, temp++)
            screen_base[temp] = rgb565_color;
    }
}
/********************************************************************
* 函数名称: lcd_draw_rectangle
* 功能描述: 画矩形
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_draw_rectangle(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y, unsigned int color)
{
    int x_len = end_x - start_x + 1;
    int y_len = end_y - start_y - 1;
    lcd_draw_line(start_x, start_y, 1, x_len, color);//上边
    lcd_draw_line(start_x, end_y, 1, x_len, color); //下边
    lcd_draw_line(start_x, start_y + 1, 0, y_len, color);//左边
    lcd_draw_line(end_x, start_y + 1, 0, y_len, color);//右边
}
/********************************************************************
* 函数名称: lcd_fill
* 功能描述: 将一个矩形区域填充为参数 color 所指定的颜色
* 输入参数: start_x, end_x, start_y, end_y, color
* 返 回 值: 无
********************************************************************/
static void lcd_fill(unsigned int start_x, unsigned int end_x,
unsigned int start_y, unsigned int end_y, unsigned int color)
{
    unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
    unsigned long temp;
    unsigned long step_size_count;
    int x;

    /* 对传入参数的校验 */
    if (end_x >= user_width)
        end_x = user_width - 1;
    if (end_y >= user_height)
        end_y = user_height - 1;
    /* 填充颜色 */
    temp = (lcd_max_y-start_x) * lcd_width + start_y;
    for ( ; start_y <= end_y; start_y++, temp++) {
        step_size_count = 0;
        for (x = start_x; x <= end_x; x++, step_size_count += lcd_width)
            screen_base[temp - step_size_count] = rgb565_color;
    }
}

int main(int argc, char *argv[])
{
    struct fb_fix_screeninfo fb_fix;
    struct fb_var_screeninfo fb_var;
    unsigned int screen_size;
    int fd;

    /* 打开 framebuffer 设备 */
    if (0 > (fd = open("/dev/fb0", O_RDWR))) {
        perror("open error");
        exit(EXIT_FAILURE);
    }

    /* 获取参数信息 */
    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
    screen_size = fb_fix.line_length * fb_var.yres;
    lcd_width = fb_var.xres;
    lcd_height = fb_var.yres;
    lcd_max_y = lcd_height - 1;
    user_width = fb_var.yres;
    user_height = fb_var.xres;

    /* 将显示缓冲区映射到进程地址空间 */
    screen_base = mmap(NULL, screen_size, PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == (void *)screen_base) {
        perror("mmap error");
        close(fd);
        exit(EXIT_FAILURE);
    }

    /* 画正方形方块 */
    int w = user_height * 0.25;//方块的宽度为 1/4 屏幕高度
    lcd_fill(0, user_width-1, 0, user_height-1, 0x0); //清屏(屏幕显示黑色)
    lcd_fill(0, w, 0, w, 0xFF0000); //红色方块
    lcd_fill(user_width-w, user_width-1, 0, w, 0xFF00); //绿色方块
    lcd_fill(0, w, user_height-w, user_height-1, 0xFF); //蓝色方块
    lcd_fill(user_width-w, user_width-1, user_height-w, user_height-1, 0xFFFF00);//黄色方块
/* 画线: 十字交叉线 */
    lcd_draw_line(0, user_height * 0.5, 1, user_width, 0xFFFFFF);//白色水平线
    lcd_draw_line(user_width * 0.5, 0, 0, user_height, 0xFFFFFF);//白色垂直线
    /* 画矩形 */
    unsigned int s_x, s_y, e_x, e_y;
    s_x = 0.25 * user_width;
    s_y = w;
    e_x = user_width - s_x;
    e_y = user_height - s_y;
    for ( ; (s_x <= e_x) && (s_y <= e_y);
    s_x+=5, s_y+=5, e_x-=5, e_y-=5)
    lcd_draw_rectangle(s_x, e_x, s_y, e_y, 0xFFFFFF);
    /* 退出 */
    munmap(screen_base, screen_size); //取消映射
    close(fd); //关闭文件
    exit(EXIT_SUCCESS); //退出进程
}

LCD 应用编程显示字符

前面几个章节向大家介绍了如何在 LCD 屏上显示图像, 本章我们就来学习下,如何在 LCD 屏上显示字符,譬如数字、字母以及中文字符等!本章将会讨论如下主题。

⚫ 使用原始的方式:自己取模显示字符

⚫ 使用 freetype 访问字体文件;

⚫ freetype 简介;

⚫ freetype 移植;

⚫ freetype 的使用介绍。

freetype 简介

FreeType 一个完全免费(开源)的软件字体引擎库,设计小巧、高效、高度可定制且可移植,它提供了统一的接口来访问多种不同格式的字体文件。 它提供了一个简单、易于使用且统一的接口来访问字体文件的内容,从而大大简化了这些任务。请注意,"FreeType"也称为"FreeType 2",以区别于旧的、已弃用的"FreeType 1"库, Freetype 1库已经不再维护和支持了。

freetype 移植

本小节我们来移植 FreeType,将 FreeType 移植到我们的开发板根文件系统中

下载 FreeType 源码

开发板出厂系统中, FreeType 的版本为 2.6,这个版本稍微有点低,我们选择移植 2.8 版本的 FreeType。进入到 https://download.savannah.gnu.org/releases/freetype/链接地址,如下所示:

交叉编译 FreeType 源码

将下载好的 freetype-2.8.tar.gz 压缩文件拷贝到 Ubuntu 系统的用户家目录下,在 tools 目录下创建一个名为 freetype 的目录,把它作为 FreeType 的安装目录,执行命令将 freetype-2.8.tar.gz 解压开来,进入到 freetype-2.8 目录,老规矩,同样是三部曲:配置、编译、安装。首先对交叉编译工具的环境进行初始化,前面章节内容已经提过很多次了,使用交叉编译器之前,必须要对其环境进行初始化(如果当前终端已经初始化过了,则无需再次进行初始化)。FreeType 库基于模块化设计,意味着我们可以对其进行裁剪,将不需要的功能模块从配置中移除,减小库文件的体积;除此之外, FreeType 还支持很多配置选项, 如果大家想要对 FreeType 做一些自定义配置或者对其进行裁剪,可以参考 FreeType 源码目录下 docs/CUSTOMIZE 文档,该文件对此有比较详细的说明,建议大家看一看,如果有需求的话。 docs 目录下还有其它很多的说明文档, 也都可以读一读。

这里我们简单地配置一下,打开 include/freetype/config/ftoption.h 文件,如下所示:

bash 复制代码
vim include/freetype/config/ftoption.h

该文件定义了很多的配置宏,我们可以选择使能或禁用这些配置选项,具体配置哪些功能,大家自己去研究,每一个配置宏都有详细地解释说明。 这里我们打开以下两个配置宏:

cpp 复制代码
#define FT_CONFIG_OPTION_SYSTEM_ZLIB
#define FT_CONFIG_OPTION_USE_PNG

大家找到这两个宏,默认情况下,这两个都被注释掉了,所以是没有使能的; 把这两个宏的注释去掉,使能这两个配置宏。

第一个配置宏表示使用系统安装的 zlib 库,因为 FreeType 支持 Gzip 压缩文件,会使用到 zlib 库, zlib之前我们移植好了;第二个配置宏表示支持 PNG bitmap 位图,因为 FreeType 可以加载 PNG 格式的彩色位图字形,需要依赖于 libpng 库,这个库前面我们也是移植好了。配置好之后,保存、退出 ftoption.h 文件,接着执行如下命令对 FreeType 工程源码进行配置:

cpp 复制代码
./configure --prefix=/home/dt/tools/freetype/ --host=arm-poky-linux-gnueabi --with-zlib=yes --with-bzip2=no --with-png=yes --with-harfbuzz=no ZLIB_CFLAGS="-I/home/dt/tools/zlib/include -L/home/dt/tools/zlib/lib" ZLIB_LIBS=-lz LIBPNG_CFLAGS="-I/home/dt/tools/png/include -L/home/dt/tools/png/lib" LIBPNG_LIBS=-lpng

这个配置命令很长,简单地提一下,具体的细节大家可以执行"./configure --help"查看配置帮助信息。--prefix 选项指定 FreeType 库的安装目录; --host 选项设置为交叉编译器名称的前缀,这两个选项前面几个章节内容都已经给大家详细地解释过。

--with-zlib=yes 表示使用 zlib;

--with-bzip2=no 表示不使用 bzip2 库;

--with-png=yes 表示使用 libpng 库;

--with-harfbuzz=no 表示不使用 harfbuzz 库。

ZLIB_CFLAGS 选项用于指定 zlib 的头文件路径和库文件路径,根据实际安装路径填写;

ZLIB_LIBS 选项指定链接的 zlib 库的名称;

LIBPNG_CFLAGS 选项用于指定 libpng 的头文件路径和库文件路径,根据实际安装路径填写;

LIBPNG_LIBS 选项用于指定链接的 libpng 库的名称。

配置完成之后接着执行 make 编译,编译完成之后执行 make install 安装即可!

如果要使用 FreeType 库,我们需要在应用程序源码中包含 include/freetype2 目录下的 ft2build.h 头文件,除此之外,还需要包含另一个头文件 FT_FREETYPE_H,这是一个用宏定义的头文件,其实就是include/freetype2/freetype/freetype.h 头文件。

所以,在我们的应用程序一般是这样写:

cpp 复制代码
#include <ft2build.h>
#include FT_FREETYPE_H
移植到开发板

接下来将编译得到的动态链接库文件拷贝到开发板 Linux 系统/usr/lib 目录,在拷贝之前,需将/usr/lib 目录下原有的 FreeType 库文件删除掉,执行下面这条命令:

cpp 复制代码
rm -rf /usr/lib/libfreetype.*

删除之后,再将我们编译得到的库文件拷贝到开发板/usr/lib 目录下,也就是 FreeType 安装目录 lib 目录下的所有库文件,拷贝的时候注意符号链接的问题。拷贝完成之后,如下所示:

freetype 库的使用

整个移植工作完成之后,接着简单地介绍下 FreeType 库的使用, FreeType 库支持的功能很多、提供给用户的库函数也很多,所以笔者肯定不会给大家细聊! 以介绍性为主。

FreeType 官方也提供了详细地使用帮助文档,以下便是这些文档的链接地址:

cpp 复制代码
https://www.freetype.org/freetype2/docs/tutorial/step1.html
https://www.freetype.org/freetype2/docs/tutorial/step2.html
https://www.freetype.org/freetype2/docs/reference/index.html

以下这个链接是一份中文参考文档,大家可以看一下,笔者也不知道是哪位作者编写的,写的非常详细!

cpp 复制代码
https://www.doc88.com/p-7178359224563.html?r=1

在正式介绍 FreeType 库使用之前,需要先了解几个涉及到的概念:
字形(glyph)

字符图像就叫做字形,一个字符能够有多种不同的字形,可以理解为字形就是字符的一种书写风格,譬如宋体的汉字"国"与微软雅黑的汉字"国",它们的字形是不同的,也就是它们书写风格是不同;宋体的"国"与微软雅黑的"国"就是两种不同的字形。
字形索引

在字体文件中,通过字形索引找到对应的字形, 而字形索引是由字符编码转换而来的, 譬如 ASCII 编码、 GB2312 编码、 BIG5 编码、 GBK 编码以及国际标准字符集使用的 Unicode 编码等。 对于字符编码,如果还有不了解读者,建议自行查阅相关的书籍。
像素点(pixel)、点(point)以及 dpi

像素点大家都知道,譬如 LCD 分辨率为 800*480,那就表示 LCD 水平方向有 800 个像素点、垂直方向有 480 个像素点,所以此 LCD 一共有 800*480 个像素点。像素点这个概念大家都很熟悉了, 也就不再多说; 我们再来看下"点"的概念, 点(point)是一种简单地物理单位,在数字印刷中,一个点(point)等于 1/72 英寸(1 英寸等于 25.4 毫米)。

除此之外,还有一个 dpi 的概念, dpi(dots per inch)表示每英寸的像素点数,譬如 300*400dpi 表示在水平方向,每英寸有 300 个像素点、在垂直方向上每英寸有 400 个像素点。 通过点数和 dpi 可以计算出像素点数,公式如下:

像素点数 = 点数 * dpi / 72

譬如,假设某一显示设备水平方向 dpi 为 300,已知水平方向的点数为 50,那么像素点数的计算方式为:

50 * 300 / 72 = 208

所以可以算出像素点数为 208, 因为后面会用到这些概念,所以先给大家简单地说明一下。
字形的布局

以下两张图清晰地描述了字形布局的情况:分为水平布局和垂直布局,来描述布局情况, 以下这两张图都是从官方的文档中截取过来的。水平方向书写文字使用水平布局方式,绝大部分情况下我们一般都是在水平方向上书写文字;垂直方向书写文字使用垂直布局方式,对于汉字来说,垂直方向书写也是比较常见的,很有代表性的就是对联、还有很多古书文字的写法, 也都是采用这种垂直书写。

⚫ 基准线、原点

从图中可以看到,不管是水平布局还是垂直布局,图中都可以找到一个 origin 原点, 经过原点的水平线(X 轴)和垂直线(Y 轴)称为基准线,笔者将其称为水平基线和垂直基线。对于水平布局, 垂直基线在字形的左边,垂直基线简单地放置在字形上,通过图中所标注的度量数据确定与基线的位置关系。对于垂直布局,水平基线在字形的上方,字形在垂直基线上居中放置,同样也是通过图中所标注的度量数据确定与基线的位置关系。原点、基准线可以用于定位字形,水平布局和垂直布局使用不同的约束来放置字形。

⚫ 字形的宽度和高度

每一个字形都有自己的宽度和高度,图中使用 width(宽)和 height(高)来表示, width 描述了字形轮廓的最左边到最右边的距离;而 height 描述了字形轮廓的最上边到最下边的距离。同一种书写风格,不同字符所对应的字形,它们的宽高是不一定相等的,譬如大写 A 和小写 a,宽度和高度明显是不同的;但有些字符的字形宽度和高度是相同的,这个与具体的字符有关!

⚫ bearingX 和 bearingY

bearingX 表示从垂直基线到字形轮廓最左边的距离。对于水平布局来说, 字形在垂直基线的右侧,所以bearingX 是一个正数;而对于垂直布局来说, 字形在垂直基线上居中放置,所以字形轮廓的最左边通常是在垂直基线的左侧,所以 bearingX 是一个负数。bearingY 则表示从水平基线到字形轮廓最上边的距离。 对于垂直布局来说, bearingY 是一个正数,字形处于水平基线的下方;而对于水平布局来说, 如果字形轮廓的最上边在水平基线的上方,则 bearingY 是一个正数、相反则是一个负数。

⚫ xMin/xMax、 yMin/yMax

xMin 表示字形轮廓最左边的位置, xMax 则表示字形轮廓最右边的位置; yMin 表示字形轮廓最下边的位置, yMax 则表示字形轮廓最上边的位置,通过这 4 个位置可以构成一个字形的边界框(bounding box,bbox),当然这是一个假象的框子,它尽可能紧密的装入字形。

⚫ advance

advance 则表示步进宽度, 相邻两个原点位置的距离(字间距)。如果是水平布局,则表示相邻的两个原点在水平方向上的距离(advanceX) ,也就是相邻两条垂直基线之间的距离;同理,如果是垂直布局,则表示相邻的两个原点在垂直方向上的距离(advanceY) ,也就是相邻两条水平基线之间的距离。以上所提到的这些参数都很重要,大家一定要理解这些参数所表示的意义, 绘制字符时,需要以这些参数作为参考值进行对齐显示。

使用 FreeType 访问字体文件, 可以从字体文件中获取到字形的位图数据,位图数据存储在一个 buffer中, buffer 大小为字形的宽*高个字节(字形边界框的宽*高个字节) ,也就是图width*height 个字节大小, 每一个点使用一个字节来表示,当数组中该点对应的数值等于 0,表示该点不填充颜色;当数值大于 0,表示该点需要填充颜色。
字符显示时如何对齐?

平时我们使用文本编辑器编写文字的时候, 这些字符都是对齐显示的; 譬如在一行文本中, 即使包含大小写的英文字母、标点符号、汉字等这些字符, 这一行字符显示在屏幕上时、 都是对齐显示的; 这里说的对齐是按照标准规范进行对齐, 譬如逗号", "显示时是靠近下边的、而不是靠近上边显示;双引号""显示时是靠近上边的、而不是居中显示; 这就是笔者认为的字符显示时的对齐规范, 你可以认为每一个字符,它都有对应的一个显示规范,是靠近上边显示呢、还是靠近下边显示亦或者是靠近中间显示呢等。

那我们如何保证对齐显示呢?其实就是通过水平基线和垂直基线,如下图所示:

不同字符对应的字形,水平基线到字形轮廓最上边的距离都是不一样的,譬如图中水平基线到"A"和"a"轮廓最上边的距离明显是不一样的;除此之外,有些字形的轮廓最下边已经在水平基线之下、而有些字形的轮廓最下边却又在水平基线之上。分析完水平基线之后,我们再来看看垂直基线,每一个字形的垂直基线到字形轮廓最左边的距离也都是不一样的,譬如"韩"和"a", 很明显、它们各自的垂直基线到字形轮廓最左边的距离是不一样的。水平基线可以作为垂直方向(上下方向)上对齐显示的基准线,而垂直基线可以作为水平方向(左右方向)上对齐显示的基准线;对于水平布局来说, 相邻两条垂直基线的距离就是字间距或者叫步进宽度;从一个字形的原点加上一个步进宽度就到了下一个字形的原点。当我们要在屏幕上画字形的时候,首先要定位到字形的左上角位置, 从左上角开始,依次从左到右、从上到下, 字形显示的宽度就是字形的宽度 width、字符显示的高度就是字形的高度 height。 那如何找到左上角的位置,这个很简单,通过 bearingY 和 bearingX 便可确定。譬如我们将(100, 100)这个位置作为原点,那么,字符显示位置的左上角便是(100+bearingX, 100-bearingY)。以上就给大家介绍关于字符对齐的问题,大家一定要理解这些内容,如果你理解不了上面的内容,后面的示例代码你可能就看不懂。

初始化 FreeType 库

在使用 FreeType 库函数之前,需要对 FreeType 库进行初始化操作,使用 FT_Init_FreeType()函数完成初始化操作。在调用该函数之前,我们需要定义一个 FT_Library 类型变量, 调用 FT_Init_FreeType()函数时将该变量的指针作为参数传递进去;使用示例如下所示:

cpp 复制代码
FT_Library library;
FT_Error error;
error = FT_Init_FreeType(&library);
if (error)
fprintf(stderr, "Error: failed to initialize FreeType library object\n");

FT_Init_FreeType 完成以下操作:

⚫ 它创建了一个 FreeType 库对象,并将 library 作为库对象的句柄。

⚫ FT_Init_FreeType()调用成功返回 0;失败将返回一个非零值错误码。

加载 face 对象

应用程序通过调用 FT_New_Face()函数创建一个新的 face 对象, 其实就是加载字体文件, 为啥叫 face(脸),应该是一种抽象的说法! 一个 face 对象描述了一个特定的字体样式和风格,譬如"Times New Roman Regular"和"Times New Roman Italic"对应两种不同的 face。调用 FT_New_Face()函数前,我们需要定义一个 FT_Face 类型变量,使用示例如下所示:

cpp 复制代码
FT_Library library; //库对象的句柄
FT_Face face; //face 对象的句柄
FT_Error error;
FT_Init_FreeType(&library);
error = FT_New_Face(library, "/usr/share/fonts/font.ttf", 0, &face);
if (error) {
/* 发生错误、进行相关处理 */
}

FT_New_Face()函数原型如下所示:

cpp 复制代码
FT_Error FT_New_Face(FT_Library library, const char *filepathname, FT_Long face_index, FT_Face *aface);

函数参数以及返回值说明如下:

library: 一个 FreeType 库对象的句柄, face 对象从中建立;

filepathname: 字库文件路径名(一个标准的 C 字符串);

face_index: 某些字体格式允许把几个字体 face 嵌入到同一个文件中,这个索引指示了你想加载的 face,其实就是一个下标,如果这个值太大,函数将会返回一个错误,通常把它设置为 0 即可!想要知道一个字体文件中包含了多少个 face,只要简单地加载它的第一个 face(把 face_index 设置为 0),函数调用成功返回后, face->num_faces 的值就指示出了有多少个 face 嵌入在该字体文件中。

aface: 一个指向新建 face 对象的指针,当失败时其值被设置为 NULL。

返回值: 调用成功返回 0;失败将返回一个非零值的错误码。

设置字体大小

设置字体的大小有两种方式: FT_Set_Char_Size()和 FT_Set_Pixel_Sizes()。
FT_Set_Pixel_Sizes()函数

调用 FT_Set_Pixel_Sizes()函数设置字体的宽度和高度,以像素为单位,使用示例如下所示:

cpp 复制代码
FT_Set_Pixel_Sizes(face, 50, 50);

第一个参数传入 face 句柄;第二个参数和第三个参数分别指示字体的宽度和高度,以像素为单位; 需要注意的是, 我们可以将宽度或高度中的任意一个参数设置为 0,那么意味着设置为 0 的参数将会与另一个参数保持相等,如下所示:

cpp 复制代码
FT_Set_Pixel_Sizes(face, 50, 0);

上面调用 FT_Set_Pixel_Sizes()函数时,将字体高度设置为 0, 也就意味着字体高度将自动等于字体宽度50。
FT_Set_Char_Size()函数

调用 FT_Set_Char_Size()函数设置字体大小, 示例如下所示,假设在一个 300x300dpi 的设备上把字体大小设置为 16pt:

cpp 复制代码
error = FT_Set_Char_Size(
    face,//face 对象的句柄
    16*64, //以 1/64 点为单位的字体宽度
    16*64, //以 1/64 点为单位的字体高度
    300, //水平方向上每英寸的像素点数
    300); //垂直方向上每英寸的像素点数

说明:

⚫ 字体的宽度和高度并不是以像素为单位,而是以 1/64 点(point) 为单位表示(也就是 26.6 固定浮点格式) ,一个点是一个 1/72 英寸的距离。

⚫ 同样也可将宽度或高度其中之一设置为 0, 那么意味着设置为 0 的参数将会与另一个参数保持相等。

⚫ dpi 参数设置为 0 时,表示使用默认值 72dpi。

示例代码
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <sys/mman.h>
#include <linux/fb.h>
#include <math.h> //数学库函数头文件
#include <wchar.h>
#include <ft2build.h>
#include FT_FREETYPE_H

#define FB_DEV "/dev/fb0" //LCD 设备节点
#define argb8888_to_rgb565(color) ({ \
unsigned int temp = (color); \
((temp & 0xF80000UL) >> 8) | \
((temp & 0xFC00UL) >> 5) | \
((temp & 0xF8UL) >> 3); \
})

static unsigned int width; //LCD 宽度
static unsigned int height; //LCD 高度
static unsigned short *screen_base = NULL;//LCD 显存基地址 RGB565
static unsigned long screen_size;
static int fd = -1;
static FT_Library library;
static FT_Face face;

static int fb_dev_init(void)
{
    struct fb_var_screeninfo fb_var = {0};
    struct fb_fix_screeninfo fb_fix = {0};
    /* 打开 framebuffer 设备 */
    fd = open(FB_DEV, O_RDWR);
    if (0 > fd) {
        fprintf(stderr, "open error: %s: %s\n", FB_DEV, strerror(errno));
        return -1;
    }

    /* 获取 framebuffer 设备信息 */
    ioctl(fd, FBIOGET_VSCREENINFO, &fb_var);
    ioctl(fd, FBIOGET_FSCREENINFO, &fb_fix);
    screen_size = fb_fix.line_length * fb_var.yres;
    width = fb_var.xres;
    height = fb_var.yres;

    /* 内存映射 */
    screen_base = mmap(NULL, screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    if (MAP_FAILED == (void *)screen_base) {
        perror("mmap error");
        close(fd);
        return -1;
    }

    /* LCD 背景刷成黑色 */
    memset(screen_base, 0xFF, screen_size);
    return 0;
}

static int freetype_init(const char *font, int angle)
{
    FT_Error error;
    FT_Vector pen;
    FT_Matrix matrix;
    float rad; //旋转角度
    /* FreeType 初始化 */
    FT_Init_FreeType(&library);

    /* 加载 face 对象 */
    error = FT_New_Face(library, font, 0, &face);
    if (error) {
        fprintf(stderr, "FT_New_Face error: %d\n", error);
        exit(EXIT_FAILURE);
    }

    /* 原点坐标 */
    pen.x = 0 * 64;
    pen.y = 0 * 64; //原点设置为(0, 0)

    /* 2x2 矩阵初始化 */
    rad = (1.0 * angle / 180) * M_PI; //(角度转换为弧度) M_PI 是圆周率
    #if 0 //非水平方向
    matrix.xx = (FT_Fixed)( cos(rad) * 0x10000L);
    matrix.xy = (FT_Fixed)(-sin(rad) * 0x10000L);
    matrix.yx = (FT_Fixed)( sin(rad) * 0x10000L);
    matrix.yy = (FT_Fixed)( cos(rad) * 0x10000L);
    #endif
    #if 1 //斜体 水平方向显示的
    matrix.xx = (FT_Fixed)( cos(rad) * 0x10000L);
    matrix.xy = (FT_Fixed)( sin(rad) * 0x10000L);
    matrix.yx = (FT_Fixed)( 0 * 0x10000L);
    matrix.yy = (FT_Fixed)( 1 * 0x10000L);
    #endif

    /* 设置 */
    FT_Set_Transform(face, &matrix, &pen);
    FT_Set_Pixel_Sizes(face, 50, 0); //设置字体大小
    return 0;
}

static void lcd_draw_character(int x, int y, const wchar_t *str, unsigned int color)
{
    unsigned short rgb565_color = argb8888_to_rgb565(color);//得到 RGB565 颜色值
    FT_GlyphSlot slot = face->glyph;
    size_t len = wcslen(str); //计算字符的个数
    long int temp;
    int n;
    int i, j, p, q;
    int max_x, max_y, start_y, start_x;

    // 循环加载各个字符
    for (n = 0; n < len; n++) {
        // 加载字形、转换得到位图数据
        if (FT_Load_Char(face, str[n], FT_LOAD_RENDER))
            continue;
        start_y = y - slot->bitmap_top; //计算字形轮廓上边 y 坐标起点位置 注意是减去 bitmap_top
        if (0 > start_y) {//如果为负数 如何处理??
            q = -start_y;
            temp = 0;
            j = 0;
        }
        else { // 正数又该如何处理??
            q = 0;
            temp = width * start_y;
            j = start_y;
        }

        max_y = start_y + slot->bitmap.rows;//计算字形轮廓下边 y 坐标结束位置
        if (max_y > (int)height)
            max_y = height;
        for (; j < max_y; j++, q++, temp += width) {
            start_x = x + slot->bitmap_left; //起点位置要加上左边空余部分长度
            if (0 > start_x) {
                p = -start_x;
                i = 0;
            }
            else {
                p = 0;
                i = start_x;
            }
            max_x = start_x + slot->bitmap.width;
            if (max_x > (int)width)
                max_x = width;
            for (; i < max_x; i++, p++) {
                // 如果数据不为 0,则表示需要填充颜色
                if (slot->bitmap.buffer[q * slot->bitmap.width + p])
                screen_base[temp + i] = rgb565_color;
            }
        }
        //调整到下一个字形的原点
        x += slot->advance.x / 64; //26.6 固定浮点格式
        y -= slot->advance.y / 64;
    }
}

int main(int argc, char *argv[])
{
    /* LCD 初始化 */
    if (fb_dev_init())
    exit(EXIT_FAILURE);

    /* freetype 初始化 */
    if (freetype_init(argv[1], atoi(argv[2])))
    exit(EXIT_FAILURE);

    /* 在 LCD 上显示中文 */
    int y = height * 0.25;
    lcd_draw_character(50, 100, L"路漫漫其修远兮,吾将上下而求索", 0x000000);
    lcd_draw_character(50, y+100, L"莫愁前路无知己,天下谁人不识君", 0x9900FF);
    lcd_draw_character(50, 2*y+100, L"君不见黄河之水天上来,奔流到海不复回", 0xFF0099);
    lcd_draw_character(50, 3*y+100, L"君不见高堂明镜悲白发,朝如青丝暮成雪", 0x9932CC);

    /* 退出程序 */
    FT_Done_Face(face);
    FT_Done_FreeType(library);
    munmap(screen_base, screen_size);
    close(fd);
    exit(EXIT_SUCCESS);
}

编译方法:

${CC} -o testApp testApp.c -I/home/dt/tools/freetype/include/freetype2 -L/home/dt/tools/freetype/lib -lfreetype -L/home/dt/tools/zlib/lib -lz -L/home/dt/tools/png/lib -lpng -lm

相关推荐
Cloud孙文波几秒前
解决GPU 显存未能完全释放
linux·运维·服务器
Deryck_德瑞克13 分钟前
centos中查看服务的日志
linux·运维·centos
鸽芷咕13 分钟前
『亚马逊云科技产品测评』程序员最值得拥有的第一台专属服务器 “亚马逊EC2实例“
linux·运维·服务器·科技·个人博客
少陽君17 分钟前
omyzsh安装插件zsh-syntax-highlighting
linux
云雨歇26 分钟前
Linux驱动开发笔记(十三)Sysfs文件系统
linux·驱动开发·笔记
曲江涛42 分钟前
【JS问题】require相对路径引入模块
linux·运维·服务器
weixin_403160531 小时前
Ubuntu+Apache2 搭建Gerrit 环境
linux·运维·ubuntu
YRr YRr1 小时前
ubuntu wget使用方法(ubuntu20.04)
linux·运维·ubuntu
只恨天高1 小时前
SpringBoot-在配置文件中使用Profile
java·linux·spring boot
李三不四1 小时前
redis修改密码
linux·redis