(请保留-> 作者: 罗冰 luobing4365_CSDN博客-UEFI开发,汇编语言探索,嵌入式开发领域博主)
最近大部分时间都在研究怎么调试了。原计划的图像显示系列,落后不少进度。
之前介绍的BMP和PCX格式,都是无损压缩格式。今天想研究下有损压缩的明星—Jpeg格式。
本来是准备了四种格式的研究:BMP、PCX、JPEG和ICO。因为时间原因,ICO就不再继续编程了。如果未来有需要,到时再把它补上吧。
这篇博客是图像显示系列的最后一篇了。
1 图像开源库
JPEG的格式相对比较复杂,涉及到很多数学运算。即便看懂格式以及算法,重新完全自己编写,要在这几天完成,也不是很现实。
最好的做法当然是借助开源库。 最近因为某个案子,我一直在研究CxImage。各种格式都能支持,并且代码都开源了,非常适合学习。
图1 CxImage演示
不过有个问题,Cximage是C++写的,用了各种的Class。如果想移植到UEFI下来,可能要费很多功夫。虽然也可以让UEFI支持C++的方式来减少工作量(《UEFI原理与编程》中介绍过方法),也不是短短几天能搞定的。
最好还是用C语言写的Jpeg库,比较方便移植。
在github上找到了云风的ffjpeg,使用mingw32编译的库,非常合适。作者也是我天朝程序员一枚,在此表示感谢^_^。下载地址为:
GitHub - rockcarry/ffjpeg: a simple jpeg codec.
2 JPEG格式简介
网上有很多关于这方面的文章,可以找来看看。也可以直接找它的标准文档《The JPEG Still Picture Compression Standard》仔细读读,我敢保证,会很头疼的。
我们常说的JPEG图像,实际上是JFIF(JPEG File Interchange Format),版本号为1.02。这是由Eric Hamilton于1992年9月提出的,目前使用最为广泛。此外还有TIFF JPEG等格式,这种比较复杂。比如专利申请的时候,那些审查意见通知书,都是用tif格式发过来的。日常使用中我只见过专利局使用,其他大部分都是JFIF格式。
JPEG采用大端存储,与平常x86常用的小端存储不同,读取数据的时候要注意这点。
JPEG的压缩过程,大概可以总结为:色彩空间转换->缩减取样->离散余弦变换(DCT)->量化->熵编码。
JPEG的图像并非采用RGB或者CMYK模式,而是采用YCbCr模式。这是因为,人眼对亮度的敏感度要高于对色彩的敏感度。基于此特性,在压缩图像时,将亮度和颜色分开处理。亮度部分不做太多改变,对颜色进行压缩处理,这样就算图像损失了部分细节,也不太容易捕捉到。
RGB与YCbCr模式之间的转换关系如下:
R = Y+1.042(Cr-128)
G = Y-0.3414(Cb-128)-0.71414(Cr-128)
B = Y+1.772(Cb-128)
Y = 0.299R + 0.587G + 0.114 B
Cb = – 0.1687R – 0.3313G + 0.5B + 128
Cr = 0.5R – 0.4187G – 0.0813B + 128
其余步骤的转换,比如DCT等,都有相应的公式。这里不再列出讨论了,移植代码过程中只需要知道有这个过程,了解对应的函数即可。
编程时需要重点关注的是其文件结构。JPEG的每个标记都是由2个字节组成,第一个字节固定为0xFF。每个标记前还可以添加数目不限的0xFF填充字节。下面是常用的八个标记:
1) SOI 0xD8 图像开始
2) APP0 0xE0 JFIF应用资料块
3) APPn 0xE1 – 0xEF 其他的应用资料块(n, 1~15)
4) DQT 0xDB 量化表
5) SOF0 0xC0 帧开始
6) DHT 0xC4 霍夫曼(Huffman)表
7) SOS 0xDA 扫描线开始
8) EOI 0xD9 图像结束
详细的信息如下:
图2 JPEG文件格式
这么短篇幅的介绍不可能把文件格式说清楚,但足够让我们移植代码了。
3 使用main()入口函数
在ffjpeg的代码中,大量使用了C标准库函数。为了移植,UEFI代码也必须使用C的标准库函数,因此只能使用main()作为入口。
把显示PCX图像的例子稍微修改下就可以了,步骤如下:
1) Luo2.c中,将INTN EFIAPI ShellAppMain(IN UINTN Argc, IN CHAR16 **Argv)改为int main(IN int Argc, IN char **Argv);
2) 在Luo2.inf中[Packages]段添加StdLib/StdLib.dec;在[LibraryClasses]段添加LibC LibStdio;
3) 在Common.h中添加需要的头文件,包括stdio.h、string.h、stdlib.h和wchar.h。
编译会发现提示错误,主要都是因char型字符串与Unicode字符串不一致导致的。 添加库文件
在Option ROM的开发中,只能用UefiMain()作为入口,以上这些库函数都不能使用,需要类似功能时只能自己写。这次是为了简化编程,否则大部分的代码都得重写,比较浪费时间。
对Luo2.c编译错误的地方,使用新的函数转换一下,兼容之前的逻辑即可。这样就把main()作为入口的框架搭建起来了,能比较方便地移植jpeg开源代码。
4 代码移植与演示
代码移植的工作量其实不大,主要将Jpeg解压缩需要的代码移植过来即可。
ffjpeg中提供了演示的代码,将jpeg图像转换为bmp图像。直接跟踪代码,把需要的函数一个个拷贝过来即可。函数内部变量全部都不用动,只是类型为BYTE、WORD和DWORD的变量,都改为UINT8、UINT16、UINT32就可以了。
图3 代码移植
代码移植到之前图像处理的文件Pictures.c中,相关的数据结构定义在Pictures.h中。运行效果如下:
图4 运行效果(显示JPEG图像)
5 调试小技巧
这次其实没有太多代码的编写,主要都是在做移植的工作,测试用的代码不过40来行,移植和编程总共花了我2个小时左右。
虽然完成很快,可惜代码没按照我想象的工作。
剩下的两天时间,我都在调试。正好使用了上一章节的windbg的调试环境,能够很方便的去看中间过程。
UDK的开发环境中,编译选项是打开了优化开关的。所以,在windbg下去看函数内部变量,其实…不一定能看到。虽然可以进行源代码调试,但会发现C语言的代码和汇编代码怎么也对应不起来,有时会让人比较迷惑,还不如直接看汇编代码。
这些锅都应该由编译器的优化来背。 解决方法很简单,在/conf中,修改文件tools_def.txt中相应的编译参数就可以了,把优化相关的都关掉:
图5 关闭编译优化选项
编译之后的APP,从原来的77K增大到了129K。按照上一篇博客的方法搭建调试环境,可以发现所有局部变量都能清晰的观察了:
图6 详细的变量观察
至于困扰我两天的BUG,在经过各种探索之后,我发现:不优化的程序,存储的数据总是会有两个字节的偏差;优化后的程序,内部数据乱得一塌糊涂,连图像大小都错了。一个个函数排查,也没有发现异常的地方。
这很像是数据对齐引起的问题,看下头文件,果然忘记了加限定了。加上语句#pragma pack(1),问题解决了。
这时间浪费得真有点冤。
Gitee地址:https://gitee.com/luobing4365/uefi-explorer
项目代码位于:/19 Display JPEG 下