System.Drawing.Image 内置了读取EXIF 属性项(PropertyItem )的能力------但从**.NET6** 起,System.Drawing.Common 仅在Windows上受支持。
在不引入第三方库的情况下,手动实现了一个简单的拍摄日期(DateTimeOriginal )解析器,主要是为了在Unity中使用,测试可运行的实现如下:
cs
/// <summary>
/// 简单的EXIF解析器,仅支持JPEG格式,且只读取DateTimeOriginal。
/// </summary>
private static class ExifReader
{
public static DateTime? GetDateTimeOriginal(string filePath, string fileName)
{
using var fs = File.OpenRead(filePath);
using var reader = new BinaryReader(fs);
// 验证JPEG SOI段标记:0xFF 0xD8(2字节)
if (reader.ReadByte() != 0xFF || reader.ReadByte() != 0xD8)
{
return null;
}
// 遍历JPEG段,找到APP1段标记:0xFF 0xE1(2字节)
while (fs.Position < fs.Length)
{
if (reader.ReadByte() != 0xFF)
{
continue;
}
var marker = reader.ReadByte();
// 找到APP1段,其中就是EXIF数据
if (marker == 0xE1)
{
// 读取长度(2字节)
var segLen = ExifReader.ReadBigEndianUInt16(reader);
// segLen包含长度字节,减去后是EXIF长度
var exifData = reader.ReadBytes(segLen - 2);
return ExifReader.ParseExifDateTimeOriginal(exifData, fileName);
}
// 跳过其他段
if (marker == 0xDA)
{
// SOS之后是压缩数据,不再有段
break;
}
var len = ExifReader.ReadBigEndianUInt16(reader);
fs.Position += len - 2;
}
return null;
}
/// <summary>
/// 简化处理:直接搜索DateTimeOriginal的Tag值。
/// 完整实现需要:解析TIFF IFD结构,此处略。
/// </summary>
private static DateTime? ParseExifDateTimeOriginal(byte[] data, string fileName)
{
// EXIF头 + TIFF头 = 14字节
// EXIF头:
// Exif\0\0 (6字节)
// TIFF头:
// 字节序标记II/MM(2字节)
// TIFF魔数0x002A(2字节)
// 第一个IFD的偏移(4字节),从data[10]开始,相对于TIFF头data[6]的偏移
if (data.Length < 14 || data[0] != 'E' || data[1] != 'x' || data[2] != 'i' || data[3] != 'f')
{
// 不合法的EXIF头
return null;
}
// data[6..7]: II小端, MM大端
bool isLittleEndian;
if (data[6] == 0x49 && data[7] == 0x49)
{
isLittleEndian = true;
}
else if (data[6] == 0x4D && data[7] == 0x4D)
{
isLittleEndian = false;
}
else
{
// 不合法的TIFF头
return null;
}
// IFD结构:[条目数 2字节][条目1 12字节][条目2 12字节]...[下一个IFD偏移 4字节]
// 条目结构:Tag(2) + Type(2) + Count(4) + ValueOffset(4)
// IFD每个条目的最大访问索引是i + 11,找到某个条目时,必须保证可以访问data[i + 11]
var len = data.Length - 11;
// 按顺序盲搜DateTimeOriginal的Tag(0x9003),这里并没有按照IFD结构搜索Tag
for (var i = 0; i < len; ++i)
{
// 搜索Tag的字节序,取决于TIFF头的大小端标识
var found = isLittleEndian ? (data[i] == 0x03 && data[i + 1] == 0x90)
: (data[i] == 0x90 && data[i + 1] == 0x03);
if (found == false)
{
continue;
}
// DateTimeOriginal的Type固定是2(ASCII 字符串)
var isTypeASCII = isLittleEndian ? data[i + 2] == 0x02 && data[i + 3] == 0x00
: data[i + 2] == 0x00 && data[i + 3] == 0x02;
if (isTypeASCII == false)
{
continue;
}
var count = ExifReader.ReadInt32(data, i + 4, isLittleEndian);
// count固定是20,因为"YYYY:MM:DD HH:MM:SS\0"刚好就是20字节
if (count != 20)
{
Debug.LogError($"Image = {fileName}, count = {count}");
continue;
}
var offset = ExifReader.ReadInt32(data, i + 8, isLittleEndian);
// offset是相对于,TIFF头data[6]的偏移
var absOffset = 6 + offset;
if (absOffset + count <= data.Length)
{
string rawDateTime;
try
{
rawDateTime = Encoding.ASCII.GetString(data, absOffset, count).TrimEnd('\0');
}
catch(Exception e)
{
Debug.LogError($"{e.Message}, Image = {fileName}");
return null;
}
return ExifReader.TryParseDateTime(rawDateTime);
}
}
return null;
}
private static ushort ReadBigEndianUInt16(BinaryReader reader)
{
var high = reader.ReadByte();
var low = reader.ReadByte();
return (ushort) ((high << 8) | low);
}
private static int ReadInt32(byte[] data, int offset, bool isLittleEndian)
{
var span = data.AsSpan(offset);
return isLittleEndian ? BinaryPrimitives.ReadInt32LittleEndian(span) : BinaryPrimitives.ReadInt32BigEndian(span);
}
private static DateTime? TryParseDateTime(string rawDateTime)
{
if (DateTime.TryParseExact(rawDateTime, "yyyy:MM:dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt))
{
return dt;
}
return null;
}
}