硬解jpeg的exif信息

Posted by 阿伦的博客 | Alun's Blog on March 19, 2024

当前有很多库能硬解jpeg图像信息,但如果是硬解呢,不依赖其他图形库,只通过c++标准库进行读取可行吗。

jpeg的结构

首先,我们可以参考Description of Exif file format这篇文章和JPG这篇文章,可以看出jpeg的结构基本如下: jpeg文件结构 另一份exif的数据详细解析 Exif值解析 从这份表单可以看出,这个图片的exif信息还是很多的。但是!当我使用代码强行提取的时候,发现提取到的exif信息远小于表单上的数据!这是因为一部分图片拥有的exif信息是不完全的。

#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
#include <sstream>
#include <string>

struct ExifData {
    std::string cameraModel;
    std::string creationDate;
    int rating;
    std::string description;
    int xResolution;
    int yResolution;
    int resolutionUnit; // 1 = no-unit, 2 = inch, 3 = cm
};



// 查找和提取 Exif 数据
std::string findAndExtractExifData(const std::vector<unsigned char>& imageData) {
    // 查找 Exif 数据的开始
    std::vector<unsigned char> startMarker(2) ;
     startMarker[0] = 0xFF;
     startMarker[1] = 0xE1;
    auto startIter = std::search(imageData.begin(), imageData.end(), startMarker.begin(), startMarker.end());
    if (startIter == imageData.end()) {
        std::cout << "未找到 Exif 数据的开始" << std::endl;
        return "";
    }

    // 获取 Exif 数据的长度
    auto exifLengthIter = startIter + 2;
    unsigned short exifLength = (static_cast<unsigned short>(*(exifLengthIter)) << 8) | static_cast<unsigned short>(*(exifLengthIter + 1));
    std::cout << "Exif 数据长度: " << exifLength << std::endl;

    // 提取 Exif 数据
    std::vector<unsigned char> exifData(exifLength);
    std::copy(startIter, startIter + exifLength, exifData.begin());

    // 返回 Exif 数据的字符串形式
    return std::string(reinterpret_cast<char*>(exifData.data()), exifData.size());
}

int main() {
    // 从 JPEG 图像文件中读取数据
    std::ifstream file("your_image_path/image.jpg", std::ios::binary | std::ios::ate);
    if (!file.is_open()) {
        std::cerr << "无法打开图像文件" << std::endl;
        return 1;
    }

    std::streamsize fileSize = file.tellg();
    file.seekg(0, std::ios::beg);

    std::vector<unsigned char> imageData(fileSize);
    if (!file.read(reinterpret_cast<char*>(imageData.data()), fileSize)) {
        std::cerr << "无法读取图像文件" << std::endl;
        return 1;
    }

    // 查找并提取 Exif 数据
    std::string exifData = findAndExtractExifData(imageData);

    // 输出 Exif 数据
    std::cout << exifData << std::endl;

    return 0;
}

输出的值如下

Exif 数据长度: 2905 �� Yhttp://ns.adobe.com/xap/1.0/<?xpacket begin=”” id=”W5M0MpCehiHzreSzNTczkc9d”?>

NIKON D810 Ver.1.01 2018:11:20 10:40:31 0 300 300 2

<?xpacket end=”w

其中能读出的数据有:

根节点是 , 其中包含了 3 个命名空间: xmlns:x="adobe:ns:meta/": Adobe 自定义的 XMP 元数据命名空间 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#": RDF (Resource Description Framework) 语法命名空间 xmlns:xmp="http://ns.adobe.com/xap/1.0/": XMP 核心属性命名空间 xmlns:dc="http://purl.org/dc/elements/1.1/": Dublin Core 元数据属性命名空间 xmlns:tiff="http://ns.adobe.com/tiff/1.0/": TIFF 图像元数据属性命名空间

节点中,我们可以获取到:

: 创建该图像的工具,是 "NIKON D810 Ver.1.01" : 图像的创建日期,是 "2018:11:20 10:40:31" : 图像的评分,是 "0" : 图像的水平分辨率,是 "300" : 图像的垂直分辨率,是 "300" : 分辨率的单位,是 "2" (代表英寸) 这个时候,细心的朋友可能发现了,我们的有效信息一般是由 ***<?xpacket begin=*** 开始而由 ***<?xpacket end=*** 结束(事实上在结束标志前还有一大片空数据)因此实际结束位应该是一定数量的空数据或者 ***<?xpacket end=*** 知道这个信息就好办了,我们就可以用一个函数来硬解图片exif数据 ```c++ #include #include #include #include #include std::string extractExifData(const std::string& filename) { std::ifstream file(filename, std::ios::binary); if (!file) { std::cout << "无法打开文件: " << filename << std::endl; return ""; } std::vector imageData((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); std::string exifData; std::string startMarker = "<?xpacket begin="; std::string endMarker = " "; size_t startPos = std::string(imageData.begin(), imageData.end()).find(startMarker); if (startPos != std::string::npos) { size_t endPos = std::string(imageData.begin() + startPos, imageData.end()).find(endMarker); if (endPos != std::string::npos) { endPos += startPos; exifData = std::string(imageData.begin() + startPos, imageData.begin() + endPos + endMarker.length() + 3); } } return exifData; } ``` 此时我们能获取到各种类型图片的内嵌xmp信息,其中就有我们需要的部分信息,只要对其中数据进行提取就可以获得数据 ![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b1ca8fe8a01fa1ec3d482a2983bdd136.png) 读取的值其实还不是exif图,而是内嵌的xmp文件,为妾这样读取效率太慢,这个时候,我们就可以思考有没有更佳优异的读取exif信息的方式了。 这个时候我们就要理解一个概念叫IFD,这个数据结构长12位由2位tag,2位type,4位count和4位offset组成,其中,tag是标签名,每个tag(2位16进制数)都会对应一个标签,比如0x010f就是make标签,代表相机制造商信息,具体的标签名对应列表参考这个文档[Exif2.3标签名](https://exiftool.org/TagNames/EXIF.html)。 此时我们就可以正确读取了,以EXIF格式的jpeg为例,首先我们读取前两位是否为0xFF 0xD8,然后读取四位看是0xE0还是0xE1,如果是0xE0则是APP0区域,位JFIF空间,如果是0xE1则是APP1区域为Exif空间 ![Exif和Jfif对比](https://i-blog.csdnimg.cn/blog_migrate/a2b7fb4ab64f46b37edb068458f75656.png) 因此,如果是JFIF,提取数据就比较简单了,不需要考虑大小端啥的,按照[维基百科-JPEG文件交换格式](https://zh.wikipedia.org/wiki/JPEG%E6%96%87%E4%BB%B6%E4%BA%A4%E6%8D%A2%E6%A0%BC%E5%BC%8F) JFIF文件结构如下 ![JFIF文件结构](https://i-blog.csdnimg.cn/blog_migrate/75caf4d0257425300a645de8aec4c95e.png) APP0标记段如下 ![APP0](https://i-blog.csdnimg.cn/blog_migrate/e2af9a0e57e25fd57c718253124921a9.png) 单纯读取JFIF信息就可以只读EXIF空格后的几位 而读取EXIF数据则复杂得多。 首先,用文件指针读取第一二位,确定是否为FFD8,如果是则下一步,否输出不是jpeg文件,然后读取第三四位看是FFE0还是FFE1,如果是FFE0则为JFIF格式,如果为FFE1则为EXIF格式,如果是EXIF存储格式,则读取第13和14位,看是否为II或者MM如果是II则为小端存储,MM则为大端存储,然后读取其21位和22位(此时开始读取需要遵守之前读取的大小端规则)确定总体tag数量比如刚刚读取到II,现在读取到0D00则有13个tag然后开始将数据写入IFD中。 进行完逻辑之后,我们就读取到了第一个非常有用的数据IFD( Image File Directory),然后再针对性进行解码,下面是一个示例 ```c #include <i386/endian.h> #include #include #include #include #include using namespace std; // 字节序枚举 enum ByteOrder { LITTLE, BIG }; enum DataType { Byte = 1, Ascii = 2, Short = 3, Long = 4, Rational = 5, Undefined = 7, SignedRational = 10, Float = 11, Double = 12 }; // IFD结构体 struct IFD { unsigned short tagName; unsigned short type; unsigned int count; unsigned int offset; }; // 读取2个字节 unsigned short readShort(ifstream& file, ByteOrder byteOrder) { unsigned char bytes[2]; file.read(reinterpret_cast<char*>(bytes), 2); if (byteOrder == LITTLE) { return (bytes[1] << 8) + bytes[0]; } else { return (bytes[0] << 8) + bytes[1]; } } // 读取4个字节 unsigned int readLong(ifstream& file, ByteOrder byteOrder) { unsigned char bytes[4]; file.read(reinterpret_cast<char*>(bytes), 4); if (byteOrder == LITTLE) { return (bytes[3] << 24) + (bytes[2] << 16) + (bytes[1] << 8) + bytes[0]; } else { return (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3]; } } // 解析IFD结构体中的标记名称和标记值 void parseIFDValue(ifstream& file, ByteOrder byteOrder, const IFD& ifd, const map<unsigned short, string>& tagNames) { // 输出标记名称 if (tagNames.find(ifd.tagName) != tagNames.end()) { cout << "标记名称: " << tagNames.at(ifd.tagName); } else { cout << "未知标记: 0x" << hex << ifd.tagName; } // 根据类型解析标记值 switch (ifd.type) { case Ascii: { streampos currentPos = file.tellg(); file.seekg(ifd.offset, ios::beg); vector buffer(ifd.count); file.read(buffer.data(), ifd.count); string value(buffer.begin(), buffer.end()); cout << " 标记值: " << value << endl; file.seekg(currentPos); } break; case Short: { if (ifd.count == 1) { cout << " 标记值: " << ifd.offset << endl; } else { streampos currentPos = file.tellg(); cout << " 标记值 "; file.seekg(ifd.offset, ios::beg); for (unsigned int i = 0; i < ifd.count; i++) { unsigned short value = readShort(file, byteOrder); cout<< value << endl; } cout << endl; file.seekg(currentPos); } } break; case Long: { if (ifd.count == 1) { cout << " 标记值: " << ifd.offset << endl; } else { streampos currentPos = file.tellg(); file.seekg(ifd.offset, ios::beg); cout << " 标记值 "; for (unsigned int i = 0; i < ifd.count; i++) { unsigned int value = readLong(file, byteOrder); cout << value ; } cout << endl; file.seekg(currentPos); } } break; // 其他类型的处理... default: cout << "未处理的数据类型: " << ifd.type << endl; break; } cout << endl; } // 解析EXIF元数据 void parseExif(const string& filename) { ifstream file(filename, ios::binary); if (!file) { cout << "无法打开文件: " << filename << endl; return; } // 读取前两个字节,确定是否为JPEG文件 unsigned short marker = readShort(file, BIG); if (marker != 0xFFD8) { cout << "不是JPEG文件" << endl; return; } // 读取下一个标记 marker = readShort(file, BIG); if (marker != 0xFFE0 && marker != 0xFFE1) { cout << "不是JFIF或EXIF格式" << endl; return; } // 如果是EXIF格式,读取字节序和标记数量 ByteOrder byteOrder; unsigned int ifdOffset = 0; if (marker == 0xFFE1) { file.seekg(8, ios::cur); // 跳过EXIF头 unsigned short byteOrderMarker = readShort(file, BIG); cout << "字节序: " << byteOrderMarker << endl; if (byteOrderMarker == 0x4949) { byteOrder = LITTLE; } else if (byteOrderMarker == 0x4D4D) { byteOrder = BIG; } else { cout << "无效的字节序" << endl; return; } file.seekg(2, ios::cur); // 跳过42 ifdOffset = readLong(file, byteOrder) + 12; } // 读取IFD file.seekg(ifdOffset, ios::beg); unsigned short tagCount = readShort(file, byteOrder); cout << "标记数量: " << tagCount << endl; // 定义标记名称映射 map<unsigned short, string> tagNames; tagNames[0x010E] = "ImageDescription"; tagNames[0x013B] = "Artist"; tagNames[0x010F] = "Make"; tagNames[0x0110] = "Model"; tagNames[0x0112] = "Orientation"; tagNames[0x011A] = "XResolution"; tagNames[0x011B] = "YResolution"; tagNames[0x0128] = "ResolutionUnit"; tagNames[0x0131] = "Software"; tagNames[0x0132] = "DateTime"; tagNames[0x0213] = "YCbCrPositioning"; tagNames[0x8769] = "ExifOffset"; tagNames[0x8298] = "Copyright"; tagNames[0x829A] = "ExposureTime"; tagNames[0x829D] = "FNumber"; tagNames[0x8822] = "ExposureProgram"; tagNames[0x8825] = "GPSInfo"; tagNames[0x8827] = "ISOSpeedRatings"; tagNames[0x9000] = "ExifVersion"; tagNames[0x9003] = "DateTimeOriginal"; tagNames[0x9004] = "DateTimeDigitized"; tagNames[0x9204] = "ExposureBiasValue"; tagNames[0x9205] = "MaxApertureValue"; tagNames[0x9207] = "MeteringMode"; tagNames[0x9208] = "Lightsource"; tagNames[0x9209] = "Flash"; tagNames[0x920A] = "FocalLength"; tagNames[0x927C] = "MakerNote"; tagNames[0x9286] = "UserComment"; tagNames[0xA000] = "FlashPixVersion"; tagNames[0xA001] = "ColorSpace"; tagNames[0xA002] = "ExifImageWidth"; tagNames[0xA003] = "ExifImageLength"; tagNames[0xA433] = "LensMake"; tagNames[0xA434] = "LensModel"; vector ifds(tagCount); // 读取IFD结构体 for (int i = 0; i < tagCount; i++) { ifds[i].tagName = readShort(file, byteOrder); ifds[i].type = readShort(file, byteOrder); ifds[i].count = readLong(file, byteOrder); ifds[i].offset = readLong(file, byteOrder) + 12; // // 输出IFD信息 // if (tagNames.find(ifds[i].tagName) != tagNames.end()) { // cout << "标记名称: " << tagNames[ifds[i].tagName]; // } else { // cout << "未知标记: 0x" << hex << ifds[i].tagName; // } // cout << " 类型: " << ifds[i].type ; // cout << " 数量: " << ifds[i].count; // cout << " 偏移量: " << ifds[i].offset << endl; // cout << endl; parseIFDValue(file, byteOrder, ifds[i], tagNames); } file.close(); } ``` 解析完成就可以拿到exif数据啦 ![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/8fdb23043fe8389a104d8c4105cf282b.png) 等等,这样我们只解析了一个区块的exif信息,要完整解析还得再获取一个偏移量获得下一区块的位置,而且如果偏移量少于tagcout*12的情况,原偏移量所在位置就是值位置,也就是说,如果偏移量为1,那么值就为1