硬解jpeg的exif信息

当前有很多库能硬解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"?>
<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空间
Exif和Jfif对比
因此,如果是JFIF,提取数据就比较简单了,不需要考虑大小端啥的,按照维基百科-JPEG文件交换格式

JFIF文件结构如下
JFIF文件结构
APP0标记段如下
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

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值