目录
介绍
几乎所有照片和智能手机相机都使用EXIF标准来存储有关图像的信息。EXIF数据由标签列表组成,每个标签存储有关图像的信息。例如,日期和时间是在拍摄图像时或拍摄图像的GPS位置时存储的。使用特殊的EXIF编辑器,可以在拍摄照片后添加说明文字。
本文介绍如何使用C#库在JPEG图像文件中读取和写入EXIF标签。尽管EXIF标记也可以存储在TIFF图像文件中,但此处未考虑。
背景
有许多用于读取JPEG文件的EXIF标签的第三方库,.NET Framework还提供了用于访问EXIF标签的.NET类。但是大多数第三方库都无法将EXIF标签写入JPEG文件。在.NET Framework中,存在用于读取和写入EXIF标记的类。但是它们有很多开销,因此它们非常慢并且不是无损的,即,在加载JPEG图像时未压缩JPEG图像,而当使用新的或更改的EXIF标签保存JPEG图像时,将对其进行新的压缩。因此,我决定开发一个名为CompactExifLib的新库。
例如,从400张JPEG照片中读取了拍摄日期的EXIF标签,并测量了以毫秒为单位的时间,并将其记录在下表中:
WPF类BitmapMetadata | 库CompactExifLib | 速度因数 |
1774毫秒 | 40.2毫秒 | 44.1 |
在第一列中,使用.NET Framework的WPF类BitmapMetadata读取EXIF标记,在第二列中使用库CompactExifLib。如您所见,该库CompactExifLib比.NET Framework的库快44倍以上。
CompactExifLib库的好处:
- 速度非常快,因为可以使用基本文件读写方法直接访问EXIF标记
- 无损图像保存。保存EXIF标签时,JPEG图像矩阵完全不变。
- 完全用C#编写,不需要DLL
- 可以与Windows窗体和WPF应用程序一起使用
- 可与.NET Framework 4.0中的许多.NET版本一起使用
演示应用程序
在下载包中,有一个演示应用程序,其中列出了JPEG图像文件的所有EXIF标签。
使用代码
读写标签
该CompactExifLib库由一个单个的.cs文件组成。因此,使用该库非常容易,只需将文件ExifData.cs添加到您的项目中,然后使用using命令插入名称空间CompactExifLib。该库中的主要类是保存图像文件的完整EXIF数据的ExifData类。例如,要读取照片“c:\temp\testimage.jpg”的日期,可以使用以下代码:
using CompactExifLib;
...
ExifData TestExif;
DateTime DateTaken;
try
{
TestExif = new ExifData(@"c:\temp\testimage.jpg");
if (TestExif.GetTagValue(ExifTag.DateTimeOriginal, out DateTaken))
{
// The date taken is now available in variable "DateTaken"
}
}
catch
{
// Error occurred while reading image file
}
ExifData构造函数的声明:
public ExifData(string FileNameWithPath, ExifLoadOptions Options = 0);
并从指定的图像文件加载EXIF数据。如果文件没有EXIF数据块,则返回一个空块。如果加载失败,则会引发异常。可能的原因是:
- 该文件不存在。
- 对文件的访问被拒绝。
- 该文件包含非法内容,例如,它不是有效的JPEG文件。
该ExifData构造函数将图像文件的EXIF数据完全复制在内存中,并立即关闭该文件。然后,对EXIF数据的存储副本执行所有读取和写入访问。当将EXIF数据写回图片文件时,必须调用ExifData的Save方法:
public void Save(string DestFileNameWithPath = null,
ExifSaveOptions SaveOptions = ExifSaveOptions.None);
如果参数NewFileNameWithPath为null或省略,则该Save方法将覆盖现有的图像文件。也可以通过在参数NewFileNameWithPath中传递文件名来将图像另存为新文件名。如果无法保存文件,该Save方法将引发异常。可能的原因是:
- 该文件被写保护。
- 对文件的访问被拒绝。
- 该文件不再可用,例如已被删除或已删除该卷。
- EXIF数据太大。EXIF数据的最大大小为65526字节。
关于加载和保存EXIF数据以及使用流的可能性的更详细的描述可以在下面的“加载和保存EXIF数据”部分中找到。
在以下示例代码中,更改了上一示例中图像的拍摄日期,然后将EXIF数据写回到图像文件中。
DateTaken.AddHours(2); // Add 2 hours to the time stamp
TestExif.SetTagValue(Exifag.DateTimeOriginal, DateTaken);
try
{
TestExif.Save();
}
catch
{
// Error occurred while writing image file
}
标签ID和图像文件目录(IFD)
标签由称为标签ID的16位值定义。以下示例代码显示了一些标签ID定义。
public enum ExifTagId
{
...
Orientation = 0x0112,
ImageDescription = 0x010E,
DateTimeOriginal = 0x9003,
...
}
但是标签ID不足以指定标签。另外,必须指定IFD。EXIF标签分为几个部分,称为图像文件目录(IFD)。如果要读取或写入标签,则必须指定正确的IFD。EXIF标准[EXIF2.32]中定义了应将哪个IFD用于标签。为了指定IFD,可以使用以下常量。
public enum ExifIfd
{
PrimaryData = 0,
PrivateData = 1,
GpsInfoData = 2,
Interoperability = 3,
ThumbnailData = 4
}
IFD PrimaryData是EXIF数据的主要IFD,它包含基本图像数据,而IFD PrivateData包含其他图像数据。IFD GpsInfoData存储拍摄图像的位置的GPS数据。Interoperability供内部使用,并且ThumbnailData存储缩略图的EXIF数据,缩略图是小的预览图像。
为了使指定EXIF标签更容易,这里有一些组合常量,它们的值的高16位包含IFD,低16位包含标签ID。
public enum ExifTag
{
...
Orientation = (ExifIfd.PrimaryData << 16) | ExifTagId.Orientation,
ImageDescription = (ExifIfd.PrimaryData << 16) | ExifTagId.ImageDescription,
DateTimeOriginal = (ExifIfd.PrivateData << 16) | ExifTagId.DateTimeOriginal,
...
}
这里,常量Orientation和ImageDescription定义标记存储在IFD PrimaryData中,而常量DateTimeOriginal定义了标记在IFD PrivateData中。此外,存在用于创建类型ExifTag的值以及用于从此类值中提取IFD和标签ID的方法。这些方法在“其他有用的方法”部分中描述。
标签类型
每个EXIF标签都有一个类型,并且在此库中,标签类型由enum类型ExifTagType指定:
public enum ExifTagType
{
Byte = 1,
Ascii = 2,
UShort = 3,
ULong = 4,
URational = 5,
Undefined = 7,
SLong = 9,
SRational = 10
}
下表描述了标签类型:
ExifTagType类型常量 | 正式类型名称 | 描述 |
Byte | BYTE | 无符号8位整数值的数组 |
UShort | SHORT | 无符号16位整数值的数组 |
ULong | LONG | 无符号32位整数值的数组 |
SLong | SLONG | 有符号的32位整数值的数组 |
URational | RATIONAL | 无符号64位有理数数组。分子和分母都被编码为无符号的32位数字,并且分子首先存储。 |
SRational | SRATIONAL | 有符号的64位有理数数组。分子和分母都被编码为带符号的32位数字,并且分子首先存储。 |
Ascii | ASCII | 8位字符数组 |
Undefined | UNDEFINED | 8位值的数组。内容的解释在相应的标签中定义。 |
SByte | SBYTE | 有符号的 8 位整数值数组。仅适用于此库定义且不完全支持的TIFF图像。 |
SShort | SSHORT | 带符号的 16 位整数值数组。仅适用于此库定义且不完全支持的 TIFF 图像。 |
Float | FLOAT | 32 位浮点值数组。仅适用于此库定义且不完全支持的 TIFF 图像。 |
Double | DOUBLE | 64 位浮点值数组。仅适用于此库定义且不完全支持的 TIFF 图像。 |
通常,标签不仅可以存储一个值,还可以存储多个值的数组。
整数
可以使用以下重载方法读取存储整数的标记:
public bool GetTagValue(ExifTag TagSpec, out int Value, int Index = 0);
public bool GetTagValue(ExifTag TagSpec, out uint Value, int Index = 0);
在第一个参数TagSpec中,指定标签ID和标签的IFD。在第二个参数Value中,标签内容作为整数值返回。第三个参数Index指定要读取的值的数组索引,索引0表示标签的第一个值。如果操作成功,则返回值为true,如果发生错误,则返回值为false。在以下情况下会发生错误:
- 标签不存在。
- 标签类型不是类型ExifTagType.Byte,.UShort,.ULong或.SLong之一。
- 该参数Index指定标签数据之外的数组元素。
- 如果参数Value的类型为int:标签类型为ExifTagType.ULong,并且标签中存储的数字大于或等于0x80000000。
- 如果参数Value的类型为uint:标签类型为ExifTagType.SLong,并且标签中存储的数字为负数。
要将整数写到标签中,有一些重载的SetTagValue方法:
public bool SetTagValue(ExifTag TagSpec, int Value, ExifTagType TagType, int Index = 0);
public bool SetTagValue(ExifTag TagSpec, uint Value, ExifTagType TagType, int Index = 0);
如果标签不存在,则由SetTagValue自动创建。因此,必须在第三个参数中指定标签类型TagType。在这里,有效的标签类型是ExifTagType.Byte,.UShort,.ULong和.SLong。第四个参数Index指定要写入的值的数组索引。如有必要,标签存储器将自动扩大,以便可以将值存储在指定的索引中。如果重新分配了标签存储器,则将当前标签内容复制到新存储器中。返回值通知在以下情况下可能发生的错误:
- 指定的标签类型无效。
- 参数Value中的数字超出参数TagType中指定的标签类型的范围。
该方法SetTagValue仅写入EXIF数据的内部存储器副本,而不写入图片文件。因此,此方法永远不会引发异常。
在以下示例中,将写入和读取图像方向标签。根据EXIF标准[EXIF2.32]的定义,此标记应为16位SHORT类型的值,该值与常量ExifTagType.UShort相对应。
ExifData TestExif;
int ImageOrientation;
...
TestExif.SetTagValue(ExifTag.Orientation, 6, ExifTagType.UShort);
TestExif.GetTagValue(ExifTag.Orientation, out ImageOrientation);
使用图像方向标签,可以定义顺时针旋转90、180或270度,并定义图像矩阵的反射,前提是图像查看器在绘制图像时考虑使用此EXIF标签。例如,此标签的值6定义顺时针旋转90度。
数组标签
大多数标签仅包含一个值,但是有些标签存储多个值,即值的数组。可以使用已知方法GetTagValue和SetTagValue读取和写入数组并且该参数Index指定从零开始的数组索引。除此之外,还有一些用于读取和写入标签的数组元素数量(=值计数)的方法。该GetTagValueCount方法可以读取值计数:
public bool GetTagValueCount(ExifTag TagSpec, out int ValueCount);
在参数ValueCount中,返回标签的数组元素的数量。如果标签存在,则返回值为true。如果标签不存在,ValueCount则为0,返回值为false。要设置标签的值计数,有两种可用的重载方法SetTagValueCount:
public bool SetTagValueCount(ExifTag TagSpec, int ValueCount);
public bool SetTagValueCount(ExifTag TagSpec, int ValueCount, ExifTagType TagType);
仅当标记已存在并且在这种情况下返回true时,才可以使用第一种方法。否则,该方法将失败并返回false。相反,如果第二种方法尚不存在,则会创建该标记。因此,必须指定标签类型。如果当前分配的内存很小,则这两种方法都会重新分配标签的内部存储器。然后将当前标签内容复制到新标签存储器中。使用第二种方法,还可以更改现有标签的标签类型。但是,如果这样做,则以后必须覆盖整个标签存储器,或者必须确保标签内容与新标签类型兼容。
以下示例显示了如何读取标签“SubjectArea”,该标签应根据EXIF标准包含2、3或4个值。
ExifData TestExif;
...
TestExif.GetTagValueCount(ExifTag.SubjectArea, out int c);
int[] v = new int[c];
for (int i = 0; i < v.Length; i++)
{
TestExif.GetTagValue(ExifTag.SubjectArea, out v[i], i);
}
字符串
Strings被编码为字符数组,并且以下标记类型用于string编码:
- ExifTagType.Ascii:这是用于编码的默认标记类型string。string的结尾是一个null字符。
- ExifTagType.Undefined:某些string标签使用这种类型编码。不存在终止null字符。
- ExifTagType.Byte:Microsoft定义了带有16位Unicode字符的特殊unicode string标签,这些标签存储在字节数组中。string将以Unicode null字符终止。
为了以strings读写标签,有一些重载的方法GetTagValue和SetTagValue可用:
public bool GetTagValue(ExifTag TagSpec, out string Value, StrCoding Coding);
public bool SetTagValue(ExifTag TagSpec, string Value, StrCoding Coding);
该方法GetTagValue读取string标记,并删除所有终止null字符(如果存在)。如果标签类型为ExifTagType.Ascii或ExifTagType.Byte,则该方法SetTagValue将编写一个string标签并添加一个终止null字符。
当操作成功的时候类型bool的返回值是true,否则为false。如果读取标签且标签不存在或标签类型不正确,则会发生错误。
由于string EXIF标准中定义了几种编码,因此您必须查看string特定EXIF标签使用哪种编码和标签类型。最后一个参数Coding参数指定应用于读取或写入标签的代码页和标签类型。此参数是的enum类型StrCoding和该类型的下表中列出的常量:
类型为StrCoding的常量 | 描述 | 预期标签类型 |
Utf8 | Unicode代码页UTF8。基本代码是US ASCII代码页,特殊字符使用128、255之间的代码点编码为两个,三个或四个字节。 | ExifTagType.Ascii |
UsAscii | 美国ASCII代码页,每个字符一个字节,并且代码点从0到127。读取字符串时,所有从128到255的非法代码点都将设置为问号。 | ExifTagType.Ascii |
UsAscii_Undef | 与UsAscii相同,除了标签类型不同。 | ExifTagType.Undefined |
WestEuropeanWin | Windows的西欧代码页1252。基本是美国ASCII字符集,特殊字符在128至255的代码点中被编码为单个字节。 注意:.NET Core应用程序中的代码页1252不可用,并且在尝试使用时会引发异常。但是您可以安装一个NuGet包来扩展可用的代码页。 | ExifTagType.Ascii |
Utf16Le_Byte | Unicode代码页UTF 16 LE(小端),每个字符两个或四个字节。即使将EXIF块编码为BE(大端),字节顺序也始终为LE。 | ExifTagType.Byte |
IdCode_Utf16 | 字符串前面有一个8字节的ID码。ID代码定义了字符串编码,此库支持ID代码“Default”,“Ascii”和“ Unicode”。 读取标签: | ExifTagType.Undefined |
IdCode_UsAscii | 在string读取和写入美国ASCII代码页,除非该ID码为“Unicode”读取的时候。 | ExifTagType.Undefined |
IdCode_WestEu | 在string读取和写入1252西欧代码页,除非该ID码为“Unicode”读取的时候。 | ExifTagType.Undefined |
包含所有EXIF标签及其对应标签类型的非官方表格以及字符串编码的说明可在[EXIV2]中找到。下表列出了一些流行的EXIF字符串标签:
EXIF标签 | 参数“Coding”的可能值 | 说明 |
ImageDescription | Utf8, UsAscii, WestEuropeanWin |
|
Copyright | Utf8, UsAscii, WestEuropeanWin |
|
Artist | Utf8, UsAscii, WestEuropeanWin |
|
Make | Utf8, UsAscii |
|
Model | Utf8, UsAscii |
|
DateTime | Utf8, UsAscii |
|
DateTimeOriginal | Utf8, UsAscii |
|
DateTimeDigitized | Utf8, UsAscii |
|
ExifVersion | UsAscii_Undef |
|
FlashPixVersion | UsAscii_Undef |
|
UserComment | IdCode_Utf16, IdCode_UsAscii, IdCode_WestEu |
|
XpTitle | Utf16Le_Byte | 由微软定义 |
XpComment | Utf16Le_Byte | 由微软定义 |
XpAuthor | Utf16Le_Byte | 由微软定义 |
XpKeywords | Utf16Le_Byte | 由微软定义 |
XpSubject | Utf16Le_Byte | 由微软定义 |
也许您想知道哪种string编码应用于带有几种可能的编码(如标记“ImageDescription”)的标记。根据EXIF标准,仅ExifTagType.Ascii类型的string标签中允许使用US-ASCII字符(代码点0到127),但是几乎所有用于编辑EXIF标签的工具都使用扩展的代码页,例如Utf8或WestEuropeanWin用于编写标签。不幸的是,没有通用的方法来确定使用哪个代码页对标签进行编码,但是Utf8可以用作默认方法。这是由照片的相机,如标签“手写标签Make”和“ Model”不使用特殊字符。如果不使用像重音字符这样的特殊字符,编码页Utf8, UsAscii和WestEuropeanWin甚至是相同的,像标签“Make”和“Model”这样的照片相机手写的string标签不使用特殊字符。
Microsoft定义的string标签不是官方EXIF标准的一部分,这些标签的名称以字母“Xp”开头。
在以下示例中,将写入和读取一些string标签。
ExifData TestExif;
string s;
...
TestExif.SetTagValue(ExifTag.ImageDescription, "Smiley ☺", StrCoding.Utf8);
TestExif.GetTagValue(ExifTag.ImageDescription, out s, StrCoding.Utf8);
TestExif.SetTagValue(ExifTag.UserComment, "Comment Ω", StrCoding.IdCode_Utf16);
TestExif.GetTagValue(ExifTag.UserComment, out s, StrCoding.IdCode_Utf16);
TestExif.SetTagValue(ExifTag.ExifVersion, "1234", StrCoding.UsAscii_Undef);
TestExif.GetTagValue(ExifTag.ExifVersion, out s, StrCoding.UsAscii_Undef);
TestExif.SetTagValue(ExifTag.XpTitle, "Title Σ", StrCoding.Utf16Le_Byte);
TestExif.GetTagValue(ExifTag.XpTitle, out s, StrCoding.Utf16Le_Byte);
有理数
一些标签包含小数,这些小数被编码为两个连续的32位整数,即分子和分母。有带符号和无符号有理数,请参见标签类型ExifTagType.SRational和ExifTagType.URational。在库struct ExifRational中定义了,可以同时存储有符号和无符号有理数。
public struct ExifRational
{
public uint Numer, Denom;
public bool Sign; // true = Negative number or negative zero
public ExifRational(int _Numer, int _Denom)
{
...
}
public ExifRational(uint _Numer, uint _Denom, bool _Sign = false)
{
...
}
...
}
为了读写有理数标记,可以使用以下重载方法:
public bool GetTagValue(ExifTag TagSpec, out ExifRational Value, int Index = 0);
public bool SetTagValue(ExifTag TagSpec, ExifRational Value, ExifTagType TagType, int Index = 0);
该方法GetTagValue需要类型为ExifTagType.SRational或ExifTagType.URational的标记,否则它将失败。使用SetTagValue编写标签时,参数TagType可以是ExifTagType.SRational或ExifTagType.URational。例如,当您尝试编写负有理数并将参数TagType设置为ExifTagType.URational时,将实现范围检查,该方法将失败并返回false。
以下示例显示了如何编写和读取有理数。
ExifData TestExif;
ExifRational r1, r2;
...
r1 = new ExifRational(1637, 1000);
TestExif.SetTagValue(ExifTag.ExposureTime, r1, ExifTagType.URational);
TestExif.GetTagValue(ExifTag.ExposureTime, out r2);
此处,图像的曝光时间设置为1637/1000 = 1.637秒。根据EXIF标准[EXIF2.32]的定义,标记类型RATIONAL必须与常量ExifTagType.URational相对应。
为了在十进制数和有理数之间转换数字,可以使用以下ExifRational方法:
public static decimal ToDecimal(ExifRational Value);
public static ExifRational FromDecimal(decimal Value);
在上面的示例中,可以使用以下值1.637对变量r1进行初始化:
r1 = ExifRational.FromDecimal(1.637m);
日期和时间
在EXIF数据中,有以下用于存储日期的标签:
- DateTime:图像上次更改的日期
- DateTimeOriginal:拍摄图像的日期
- DateTimeDigitized:图像数字化的日期
- GpsDateStamp:卫星的GPS日期
所有日期都以string标记类型存储为ExifTagType.Ascii。使用以下方法,可以使用DateTime struct来访问这些标签:
public bool GetTagValue(ExifTag TagSpec, out DateTime Value,
ExifDateFormat Format = ExifDateFormat.DateAndTime);
public bool SetTagValue(ExifTag TagSpec, DateTime Value,
ExifDateFormat Format = ExifDateFormat.DateAndTime);
有两种日期格式,最后一个参数Format指定要使用的格式:
- ExifDateFormat.DateAndTime:存在日期和时间,并用空格字符分隔,例如“2019:12:22 15:23:47”。此格式用于三个“DateTimeXXX”标签。
- ExifDateFormat.DateOnly:仅显示日期,例如“2019:12:22”。此格式用于标签“GpsDateStamp”。附加标签中提供了GPS一天中的时间,请参见下文。
这三个DateTimeXXX标签的精度为1秒。一些相机还会额外写入EXIF标签SubsecTimeXXX,这些标签可以提供几分之一秒的时间。使用以下ExifData方法,您可以轻松访问这些标签,因为在DateTime struct
中处理了不到一秒的时间。所有时间均为当地时区。
方法 | 描述 | EXIF标签 |
public bool GetDateTaken(out DateTime Value); | 以1毫秒的精度获取日期。 | DateTimeOriginal, |
public bool SetDateTaken(DateTime Value); | 以1毫秒的精度设置日期。 | DateTimeOriginal, |
public void RemoveDateTaken(); | 删除日期的标签。 | DateTimeOriginal, |
public bool GetDateDigitized(out DateTime Value); | 以1毫秒的精度获取数字化的日期。 | DateTimeDigitized, |
public bool SetDateDigitized(DateTime Value); | 设置日期的数字化精度为1毫秒。 | DateTimeDigitized, |
public void RemoveDateDigitized(); | 删除数字化日期的标签。 | DateTimeDigitized, |
public bool GetDateChanged(out DateTime Value); | 获取日期更改的精度为1毫秒。 | DateTime, |
public bool SetDateChanged(DateTime Value); | 设置日期以1毫秒的精度更改。 | DateTime, |
public void RemoveDateChanged(); | 删除更改日期的标签。 | DateTime, |
因为已知的EXIF标签GpsDateStamp仅提供日期,所以还有另一个EXIF标签GpsTimeStamp可以提供UTC时区的时间。在此标签中,如果照相相机已将其记录下来,则可能还会有几分之一秒的时间。下表中的ExifData方法可以访问两个EXIF标记:
方法 | 描述 | EXIF标签 |
public bool GetGpsDateTimeStamp(out DateTime Value); | 获取UTC时区的GPS日期和时间戳。 | GpsDateStamp, |
public bool SetGpsDateTimeStamp(DateTime Value); | 在UTC时区中设置GPS日期和时间戳。 | GpsDateStamp, |
public void RemoveGpsDateTimeStamp(); | 删除GPS日期和时间戳标签。 | GpsDateStamp, |
原始数据和字节顺序
也可以不做任何解释就获得标签的原始数据字节。为此,该方法GetTagRawData是可用的。
public bool GetTagRawData(ExifTag TagSpec, out ExifTagType TagType, out int ValueCount,
out byte[] RawData)
由IFD和标签ID组成的 EXIF 标签在第一个参数TagSpec中传递。标签数据通过以下参数返回:
- TagType:标签的类型。
- ValueCount:存储在标签中的值的数量。请记住,EXIF标记通常是一个值数组,而不仅仅是单个值,因此ValueCount是数组元素的数量。仅当单个标签值的大小为1个字节(标签类型ExifTagType.Byte,Ascii和Undefined)时,返回ValueCount的数字也是原始数据字节数。例如,对于标签类型ExifTagType.UShort,原始数据字节数为2*ValueCount。
- RawData: 带有标签原始数据字节的数组。原始数据字节数为RawData.Length.
- 方法返回值:true=标签数据读取成功,false=标签不存在。
原始数据字节的解释取决于EXIF数据的字节顺序。EXIF数据可以以Little Endian (LE) 或 Big Endian (BE) 格式存储。大多数相机和图像处理工具以Little Endian格式写入EXIF数据,但也有一些相机和工具使用Big Endian格式。字节顺序是类型ExifTagType.UShort,SShort,ULong,SLong,URational,SRational,Float和Double的所有标签很重要。此外,还有一些类型Undefined的标签需要单独处理字节顺序。在这个库中,当前EXIF块的字节顺序可以由ExifData属性ByteOrder确定:
public enum ExifByteOrder { LittleEndian, BigEndian };
public ExifByteOrder ByteOrder { get; }
在ByteOrder属性中,该值ExifByteOrder.LittleEndian表示先存储多字节值的低字节,例如将16位十六进制值1A34存储为字节序列34 1A。对于设置ExifByteOrder.BigEndian,字节序列将为1A 34。为了更容易地从字节数组中读取16位或32位整数值,有两种ExifData方法可用:
public ushort ExifReadUInt16(byte[] Data, int StartIndex);
public uint ExifReadUInt32(byte[] Data, int StartIndex);
读取标签数据的字节顺序取自属性ByteOrder。
还有第二个重载方法GetTagRawData可以返回原始数据而不将它们复制到新数组:
public bool GetTagRawData(ExifTag TagSpec, out ExifTagType TagType, out int ValueCount,
out byte[] RawData, out int RawDataIndex);
原始数据字节在第四个参数RawData中返回,但该数组也可能包含不属于指定标签的数据!在这里,第一个原始数据字节存储在RawData[RawDataIndex]中,最后一个原始数据字节存储在RawData[RawDataIndex + GetTagByteCount(TagType, ValueCount) - 1]中。您可以通过ExifData静态方法GetTagByteCount确定原始数据字节数:
public static int GetTagByteCount(ExifTagType TagType, int ValueCount);
此方法返回指定标签类型和值计数的标签所需的原始数据字节数。访问数组RawData时,应考虑以下事项:
- 此方法的调用者不得更改数组RawData。
- 将新数据写入指定的EXIF标签后,不应再使用该数组RawData。
- 如果标签不存在,RawData则为null,方法的返回值为false。
为了写入标签的原始数据字节,可以使用该SetTagRawData方法。
public bool SetTagRawData(ExifTag TagSpec, ExifTagType TagType, int ValueCount, byte[] RawData,
int RawDataIndex = 0);
原始数据在参数RawData中传递,参数RawDataIndex指定存储原始数据第一个字节的数组索引。请注意,此方法不会复制原始数据,因此调用此方法后一定不要更改包含原始数据的数组。传递给SetTagRawData的原始数据字节数由参数TagType和ValueCount(=标签的数组元素数)隐式定义。您可以通过已知方法GetTagByteCount确定原始数据字节数。
要将16位或32位整数值写入字节数组,可以使用以下ExifData方法:
public void ExifWriteUInt16(byte[] Data, int StartIndex, ushort Value);
public void ExifWriteUInt32(byte[] Data, int StartIndex, uint Value);
移除标签
要删除标签,可以使用以下方法:
public bool RemoveTag(ExifTag TagSpec);
public bool RemoveAllTagsFromIfd(ExifIfd Ifd);
public void RemoveAllTags();
方法RemoveTag删除单个标签,方法RemoveAllTagsFromIfd从特定的IFD删除所有标签,并且方法RemoveAllTags从图像文件删除所有标签,因此EXIF块此后为空。如果ThumbnailData删除了IFD ,则缩略图也将自动删除。在TIFF图像中,不可能删除所有EXIF标记,因为在IFD PrimaryData中存在包含内部图像结构数据的标记。因此,如果应用于TIFF图像,这些方法不会删除TIFF内部EXIF标记、用于IPTC块的EXIF标记和用于XMP块的EXIF标记。
加载和保存EXIF数据
要从图像加载EXIF数据,有两个可用的ExifData
构造函数:
public ExifData(string FileNameWithPath, ExifLoadOptions Options = 0);
public ExifData(Stream ImageStream, ExifLoadOptions Options = 0);
第一个构造函数是已知的,它从第一个参数FileNameWithPath
中传递的JPEG或TIFF文件加载EXIF数据。使用第二个构造函数,您可以从流中加载EXIF数据。流位置必须在图像数据的开头,并且流必须是可查找的。当构造函数返回时,流不会关闭。因此,如果您现在不再需要该流,则必须调用Stream
方法Dispose
。使用第二个参数Options
可以加载一个空的EXIF块,即忽略图像文件的EXIF块。为此,将此参数的值设置为 ExifLoadOptions.CreateEmptyBlock
。
使用第一种Save
方法可以将EXIF数据保存在文件中:
public void Save(string DestFileNameWithPath = null, ExifSaveOptions SaveOptions = 0);
在第一个参数DestFileNameWithPath
中,指定了保存EXIF数据的文件名。如果将此参数设置为null
,则将EXIF数据写入原始图像文件。严格来说,是先创建一个带有新EXIF数据的临时文件,然后删除原始图像文件,最后将临时文件重命名为原始文件名。所以覆盖是安全的。请注意,从中加载EXIF数据的原始图像文件仍然必须可用。此外,该方法Save
只能在第一个以文件名作为参数的ExifData
构造函数用于加载EXIF数据时使用。目前不使用第二个参数SaveOptions
。
使用第二种Save
方法可以将EXIF数据保存在流中:
public void Save(Stream SourceStream, Stream DestStream, ExifSaveOptions SaveOptions = 0);
第一个参数SourceStream
应该是流,从中加载原始EXIF数据。SourceStream
的流位置必须在图像数据的开头并且SourceStream
必须是可查找的。第二个参数DestStream
指定新图像数据应写入的流。
除了EXIF数据,还有其他方法可以指定图像文件中的元数据。例如,如果您使用Windows 10的资源管理器在JPEG文件中写入元数据,则会写入EXIF数据和XMP数据。您可以使用以下方法检测和删除此类替代描述块:
public bool ImageFileBlockExists(ImageFileBlock BlockType);
public void RemoveImageFileBlock(ImageFileBlock Block);
public enum ImageFileBlock
{
Unknown = 0,
Exif = 1,
Iptc = 2,
Xmp = 3,
JpegComment = 4
};
流行的替代元数据块是IPTC和XMP块。在JPEG图像中,还有可能包含任意文本的JPEG注释块。
在TIFF图像中有一些细节:
- IPTC和XMP块不像在JPEG图像中那样存储在自己的文件块中,而是存储在特定的EXIF标签中。
- 删除EXIF块不会删除TIFF内部EXIF标签、IPTC块的EXIF标签和XMP块的EXIF标签。
GPS数据
如果相机将GPS数据写入图像,则可以使用IFD“GPS信息数据”中的EXIF标签访问它们。为了使访问GPS位置的经度和纬度更容易,该结构GeoCoordinate可用:
public struct GeoCoordinate
{
public decimal Degree; // Integer number: 0 ≤ Degree ≤ 90 (for latitudes)
// or 180 (for longitudes)
public decimal Minute; // Integer number: 0 ≤ Minute < 60
public decimal Second; // Fraction number: 0 ≤ Second < 60
public char CardinalPoint; // For latitudes: 'N' or 'S'; for longitudes: 'E' or 'W'
...
}
地理坐标以度、角分、角秒和基点以经典表示形式存储。也可以使用以下GeoCoordinate方法将地理坐标的经典表示形式转换为带符号的单个十进制值:
public static decimal ToDecimal(GeoCoordinate Value);
public static GeoCoordinate FromDecimal(decimal Value, bool IsLatitude);
十进制值的符号表示基点。以下是这两种表示形式中纬度的一些示例值:
46°51'2.3948“ N = + 46.850665°
46°51'2.3948” S = -46.850665°
大多数GPS值都分为两个EXIF标签。为了能够更轻松地访问它们,下表列出了访问GPS标签的ExifData方法。也可以访问GPS日期和时间戳,请参阅“日期和时间”部分。
方法 | 描述 | EXIF标签 |
public bool GetGpsLongitude(out GeoCoordinate Value); | 获取GPS经度。 | GpsLongitude, |
public bool SetGpsLongitude(GeoCoordinate Value); | 设置GPS经度。 | GpsLongitude, |
public void RemoveGpsLongitude(); | 删除GPS经度标签。 | GpsLongitude, |
public bool GetGpsLatitude(out GeoCoordinate Value); | 获取GPS纬度。 | GpsLatitude, |
public bool SetGpsLatitude(GeoCoordinate Value); | 设置GPS纬度。 | GpsLatitude, |
public void RemoveGpsLatitude(); | 删除GPS纬度标签。 | GpsLatitude, |
public bool GetGpsAltitude(out decimal Value); | 获取与海平面有关的高度(以米为单位)。正值表示“海平面以上”,负值表示“海平面以下”。 | GpsAltitude, |
public bool SetGpsAltitude(decimal Value); | 设置与海平面有关的以米为单位的高度。 | GpsAltitude, |
public void RemoveGpsAltitude(); | 删除GPS高度标签。 | GpsAltitude, |
还有更多可用的GPS标签。为了从图像中删除所有GPS标签,您可以使用ExifData方法RemoveAllTagsFromIfd:
ExifData TestExif;
...
TestExif.RemoveAllTagsFromIfd(ExifIfd.GpsInfoData);
如果要检查是否有可用的GPS标签,可以使用以下ExifData方法IfdExists:
if (TestExif.IfdExists(ExifIfd.GpsInfoData)) ...
缩图图片
缩略图图像是JPEG图像的小预览图像。缩略图图像存储在EXIF数据中,并且由于完整的EXIF数据只能具有65535字节,因此缩略图图像的大小受到限制。以下方法ThumbnailImageExists检查缩略图是否存在:
public bool ThumbnailImageExists();
为了读取缩略图图像,该方法GetThumbnailImage可用:
public bool GetThumbnailImage(out byte[] ThumbnailData, out int ThumbnailIndex,
out int ThumbnailByteCount);
在第一个参数ThumbnailData中,将给出带有缩略图的数组。该数组可能包含更多数据,调用者不得更改它。缩略图的第一个字节存储在数组索引处,该索引在第二个参数中返回ThumbnailIndex。缩略图的大小在最后一个参数中返回ThumbnailByteCount。如果未定义缩略图,则返回值为false,数组ThumbnailData为null。
方法SetThumbnailImage设置一个新的缩略图,该缩略图在参数ThumbnailData中指定为数组。该方法不会复制该数组,因此您不应在调用此SetThumbnailImage方法后更改此数组。
public bool SetThumbnailImage(byte[] ThumbnailData, int ThumbnailIndex = 0,
int ThumbnailByteCount = -1);
第二个参数ThumbnailIndex指定缩略图开始的数组索引。第三个参数ThumbnailByteCount指定缩略图的字节数,如果将此参数设置为-1,则将数组ThumbnailData的剩余长度指定为字节数。
使用方法RemoveThumbnailImage,您可以删除缩略图。
public void RemoveThumbnailImage(bool RemoveAlsoThumbnailTags);
如果参数RemoveAlsoThumbnailTags设置为true,则还会删除IFD缩略图数据中的所有标签。
JPEG 和 TIFF 文件之间的差异
JPEG格式 | TIFF格式 |
EXIF块是可选的,可以删除。如果移除图像的外观不会改变,除了EXIF标签“方向”可能会改变图像旋转。 | EXIF块是必不可少的,因为有些EXIF标签包含内部图像数据。更改或删除这些内部EXIF标签会损坏图像! |
只能存储一张图像和一张缩略图。 | 单个文件中可能存储了多个图像。这也称为多页TIFF文件。 |
EXIF块的大小限制为65526字节。 | 理论上,EXIF块的大小可以达到4GB,但在这个库中的限制是2GB。 |
其他有用的方法
检查是否存在EXIF标记或IFD:
public bool TagExists(ExifTag TagSpec);
public bool IfdExists(ExifIfd Ifd);
获取EXIF标记的类型:
public bool GetTagType(ExifTag TagSpec, out ExifTagType TagType);
枚举IFD的所有标签。可以在演示应用程序中找到一个示例。
public bool InitTagEnumeration(ExifIfd Ifd);
public bool EnumerateNextTag(out ExifTag TagSpec);
根据IFD和标签ID创建EXIF标签规范:
public static ExifTag ComposeTagSpec(ExifIfd Ifd, ExifTagId TagId);
从EXIF标签规范中获取IFD或标签ID:
public static ExifIfd ExtractIfd(ExifTag TagSpec);
public static ExifTagId ExtractTagId(ExifTag TagSpec);
用另一个图像文件的EXIF数据替换所有EXIF标签和缩略图图像。复制新的EXIF数据之前,将删除所有现有标签和当前对象的缩略图。
public void ReplaceAllTagsBy(ExifData SourceExifData);
ReplaceAllTagsBy
方法首先删除当前ExifData
对象中的所有现有标记和缩略图图像。然后,参数SourceExifData
中的对象的EXIF标记被复制到当前的ExifData
对象。如果此方法用于TIFF图像,则TIFF内部EXIF标记保持不变,即它们既不会被复制也不会被删除。
参考
ID | 描述 | 链接 |
[EXIF2.32] | 官方EXIF规范V 2.32 | http://cipa.jp/std/documents/download_e.html?DC-008-Translation-2019-E |
[EXIV2] | 带有EXIF标签的非正式表格 |
https://www.codeproject.com/Articles/5251929/CompactExifLib-Access-to-EXIF-Tags-in-JPEG-Files