前段时间写过一篇 FFmpeg 获取 JPG 图片旋转信息等 Exif 信息。但有些时候,我们并不想,或者暂时不想解码图片,而只想获取图片的宽高等相关信息。而上篇文章中提到的使用 FFmpeg 解码后获取的图片旋转信息的方法无疑要增加图像解码过程,看起来好像稍微慢了些。因此,我们有必要找个方法避免解码后才能获得图片信息。
ffprobe
这里有个疑问,感觉当我们使用 ffprobe 命令行 ffprobe -i xxx.jpg -show_streams -show_format 查看该图片的信息时,ffprobe 是能正常展示这张 jpg 图片的宽高的,但是从解码过程来看,整个过程中的 AVStream->codecpar 中的 width height 却都是 0。包括,FFmpeg 解析 png 时也是。因此,有必要看一下 ffprobe 的源码,看一下它是如何正确地打印出图片的宽高的。
JFIF & EXIF
参考这篇文章 JPEG文件格式 JFIF & Exif 可知,世界上有两种 jpg 图片文件格式头,一种是较为简单的 JFIF 格式,仅存储了图像精度、高度、宽度、颜色分量数及颜色分量信息;另一种是比较复杂的 EXIF 格式,存储了各种各样的信息,包括了从相机制造商到光圈大小、图像旋转信息、拍摄时间等。
对于 JFIF 格式的 JPG,其实我们看一下 JPEG Start of Frame marker 的结构,也能自己通过读文件的方法读取出该 JPG 图片的宽高。
但是对于 EXIF 格式的头部信息,则略显复杂。因此,我找了下现有的开源库,发现了 CExif 这个类。
Cexif 类
这个类是我从 CSDN 下载的,从代码上的注释来看,应该是个意大利程序员开发的。我从 GitHub 上找了下,没有相关的 repository,于是自己创建了个:CExif。
这个类的用法很简单,创建一个 Cexif 实例,把 JPG 文件的句柄 FILE * 作为参数传递给类成员函数 bool DecodeExif ,然后即可访问类的 public 成员结构体变量 m_exifinfo,可以用 m_exifinfo->Orientation 获取 JPG 图像的方向信息,进而通过方向定义解析出 JPG 图片的旋转信息。
具体方法如下:
Cexif c;
FILE *f = fopen(filename.c_str(), "rb");
if (!f) {
AVLOGE("Error open file %s, errno %d.", filename.c_str(), errno);
return false;
}
c.DecodeExif(f);
fclose(f);
width_ = c.m_exifinfo->Width;
height_ = c.m_exifinfo->Height;
switch (c.m_exifinfo->Orientation) {
case 1:
case 2:
rotation_ = 0;
break;
case 3:
case 4:
rotation_ = 180;
break;
case 5:
case 8:
rotation_ = 270;
break;
case 6:
case 7:
rotation_ = 90;
break;
default:
rotation_ = 0;
break;
}
再快一些
在 Linux 上使用 vim 查看一张 jpg 图片(当然也可以是任何文件),然后输入 :%!xxd 即可以十六进制查看该图片文件的具体字节信息。如下图所示: 通过使用 Windows 正常图片浏览器打开该图片获得它的宽高。通过读 Cexif 的源码也能分析到。一个 JPG 图片的最开头的三个字节是 0x FF D8 FF,再后面的 E1 标识了它是使用 APP1 marker,也就是使用 Exif Attribute Information 来记录相关信息的。还有些 JPG 的第四个字节可能是 0xC0,也就是 SOFn(Start of Frame) 节,也记录了图片的宽高。
先看 Exif 格式中,紧随着的 0x0C07 标识了该 EXIF 信息节的长度是十六进制数 C07 对应的十进制,即 3079 个字节那么大。于是 Cexif 在读到文件的第四个字节是 E1 后,会再去读两个字节,来判断 Exif 节的长度 len,然后接着读取 len - 2 个字节,因为这个 len 其实是包括了 0x0C07 这两个字节在内的。
再往下读,四个字节,0x 45 78 69 66 对应的 ASCII 码刚好是 Exif 这四个字符。再往后一些的 0x4d4d 对应于 ASCII 码 MM ,即 motorola 存放顺序;如果对应于 II 则是 Intel 存放顺序。略过一些,当读到 0x0112 时,则表示读到了 Orientation 信息,不同的信息用 Tag 表示,后面的 0x0003 表示了标识信息 Orientation 的数据格式 Format,再往后读4个字节,则是其内容个数 Components。
通过 Cexif 可以,Format 主要有以下几种:
name | SBYTE | BYTE | USHORT | ULONG | URATIONAL | SRATIONAL | SSHORT | SLONG |
---|
enumvalue | 6 | 1 | 3 | 4 | 5 | 10 | 8 | 9 | type | signed char * | unsigned char * | 16u | 32u | 32s/32s | 32s/32s | signed short | 32s | nb_bytes | 1 | 1 | 2 | 4 | 8 | 8 | 2 | 4 |
因此,Orientation 对应的 format 3 的意思是它是个 ushort,要读 2 个字节。而它的 components 是 1,也就是只有一个信息。于是往后再读两个字节刚好读到 0x0006 ,即对应于这张 JPG 图片的 Orientation 信息是 6。
同理,从 Cexif 的源码可知,0xa003 表示图像高度,用 4 也就是 4 个字节的 32 位无符号表示,有 1 个,再往后读 4 个字节,即 0x08dc ,即 2268。0xa002 表示图像宽度,不再赘述。
然而,从 Cexif 的源码中并未看到有读取上述图像高度和图像宽度的处理。通过断点发现 Cexif 在读完 Exif 节信息后,还在不停地继续向后读文件。即 EXIF.cpp 45 行的 for(;;) 循环。但是它读到的内容,能处理的并不多,即命中 cpp 105 行那个 marker 的 case 并不多。因此,可以在这里做一个加速,先判断是否是后面的 case,再决定是否要将数据读出来 (fread )。在遇到并不能处理的数据块时,可以直接跳过 (fseek ) 相应的内存大小。因此,我对 cpp 中这部分代码做了如下优化,这里,我们需要的就只有 EXIF 来读取图片方向信息,SOF 来读取图片宽高,以及 SOS 来确保这个 for(;;) 循环能在读到图像数据之前停下来:
lh = fgetc(hFile);
ll = fgetc(hFile);
itemlen = (lh << 8) | ll;
if (itemlen < 2) {
strcpy(m_szLastError, "invalid marker");
return 0;
}
if (marker != M_EXIF && marker != M_SOS && (marker < M_SOF0 || marker > M_SOF15)) {
fseek(hFile, itemlen - 2, SEEK_CUR);
continue;
}
Sections[SectionsRead].Size = itemlen;
此外,由于我们只需要从 EXIF 信息中读取 Orientation 信息,即 case TAG_ORIENTATION: ,因此对于其他 case 也可以用 #if 0 #endif 来控制不编译。
|