当前有很多库能硬解jpeg图像信息,但如果是硬解呢,不依赖其他图形库,只通过c++标准库进行读取可行吗。
jpeg的结构
首先,我们可以参考Description of Exif file format这篇文章和JPG这篇文章,可以看出jpeg的结构基本如下:
另一份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"?>
<x:xmpmeta xmlns:x=“adobe:ns:meta/” x:xmptk=“XMP Core 6.0.0”>
<rdf:RDF xmlns:rdf=“http://www.w3.org/1999/02/22-rdf-syntax-ns#”>
<rdf:Description rdf:about=“”
xmlns:xmp=“http://ns.adobe.com/xap/1.0/”
xmlns:dc=“http://purl.org/dc/elements/1.1/”
xmlns:tiff=“http://ns.adobe.com/tiff/1.0/”>
xmp:CreatorToolNIKON D810 Ver.1.01 </xmp:CreatorTool>
xmp:CreateDate2018:11:20 10:40:31</xmp:CreateDate>
xmp:Rating0</xmp:Rating>
tiff:XResolution300</tiff:XResolution>
tiff:YResolution300</tiff:YResolution>
tiff:ResolutionUnit2</tiff:ResolutionUnit>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta> <?xpacket end="w
其中能读出的数据有:
根节点是 <x:xmpmeta>, 其中包含了 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 图像元数据属性命名空间
在 rdf:Description 节点中,我们可以获取到:
xmp:CreatorTool: 创建该图像的工具,是 “NIKON D810 Ver.1.01”
xmp:CreateDate: 图像的创建日期,是 “2018:11:20 10:40:31”
xmp:Rating: 图像的评分,是 “0”
tiff:XResolution: 图像的水平分辨率,是 “300”
tiff:YResolution: 图像的垂直分辨率,是 “300”
tiff:ResolutionUnit: 分辨率的单位,是 “2” (代表英寸)
这个时候,细心的朋友可能发现了,我们的有效信息一般是由 <?xpacket begin= 开始而由 <?xpacket end= 结束(事实上在结束标志前还有一大片空数据)因此实际结束位应该是一定数量的空数据或者 <?xpacket end= 知道这个信息就好办了,我们就可以用一个函数来硬解图片exif数据
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <unordered_map>
std::string extractExifData(const std::string& filename) {
std::ifstream file(filename, std::ios::binary);
if (!file) {
std::cout << "无法打开文件: " << filename << std::endl;
return "";
}
std::vector<char> imageData((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
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信息,其中就有我们需要的部分信息,只要对其中数据进行提取就可以获得数据
读取的值其实还不是exif图,而是内嵌的xmp文件,为妾这样读取效率太慢,这个时候,我们就可以思考有没有更佳优异的读取exif信息的方式了。
这个时候我们就要理解一个概念叫IFD,这个数据结构长12位由2位tag,2位type,4位count和4位offset组成,其中,tag是标签名,每个tag(2位16进制数)都会对应一个标签,比如0x010f就是make标签,代表相机制造商信息,具体的标签名对应列表参考这个文档Exif2.3标签名。
此时我们就可以正确读取了,以EXIF格式的jpeg为例,首先我们读取前两位是否为0xFF 0xD8,然后读取四位看是0xE0还是0xE1,如果是0xE0则是APP0区域,位JFIF空间,如果是0xE1则是APP1区域为Exif空间
因此,如果是JFIF,提取数据就比较简单了,不需要考虑大小端啥的,按照维基百科-JPEG文件交换格式
JFIF文件结构如下
APP0标记段如下
单纯读取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),然后再针对性进行解码,下面是一个示例
#include <i386/endian.h>
#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
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<char> 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<IFD> 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数据啦
等等,这样我们只解析了一个区块的exif信息,要完整解析还得再获取一个偏移量获得下一区块的位置,而且如果偏移量少于tagcout*12的情况,原偏移量所在位置就是值位置,也就是说,如果偏移量为1,那么值就为1