CompactExifLib:访问 JPEG、TIFF 和 PNG 文件中的 EXIF 标签






4.98/5 (52投票s)
用于读取和写入 JPEG、TIFF 和 PNG 图像文件 EXIF 标签的 C# 库
引言
EXIF 标准几乎被所有照片和智能手机相机用于在图像中存储元数据。EXIF 数据包含一个标签列表,每个标签存储有关图像的一小部分信息。例如,照片拍摄的日期和时间或照片拍摄的 GPS 位置。本文将介绍如何使用 C# 库读取和写入 JPEG、TIFF 和 PNG 图像文件中的 EXIF 标签。
背景
有许多第三方库可以读取图像文件的 EXIF 标签,.NET Framework 也提供了访问 EXIF 标签的 .NET 类。但是,大多数第三方库无法将 EXIF 标签写入图像文件。.NET Framework 中有用于读取和写入 EXIF 标签的类。但它们的开销很大,因此速度非常慢,并且它们不是无损的,即图像在加载时未压缩,在保存新或更改的 EXIF 标签时会重新压缩。因此,我决定开发一个名为 CompactExifLib
的新库。
例如,从 400 张 JPEG 照片中读取了拍摄日期 EXIF 标签,并测量了毫秒时间,记录在下表中
WPF 类 BitmapMetadata | 库 CompactExifLib | 速度因子 |
2730 毫秒 | 50 毫秒 | 54.6 |
第一列使用了 .NET Framework 的 WPF 类 BitmapMetadata
来读取 EXIF 标签,第二列使用了 CompactExifLib
库。如您所见,CompactExifLib
库比 .NET Framework 的库快 50 倍以上。
CompactExifLib
库的优点
- 速度极快,因为 EXIF 标签是通过基本的文件读写方法直接访问的。
- 无损图像保存。保存 EXIF 标签时,图像矩阵完全不变。
- 完全用 C# 编写,无需 DLL。
- 可与 Windows Forms 和 WPF 应用程序一起使用。
- 适用于 .NET Framework 4.0 及更高版本的所有 .NET 版本。
演示应用程序
在第一个下载包中,有一个演示应用程序,列出了图像文件的所有 EXIF 标签。CompactExifLib
库也包含在内,可在文件 ExifData.cs 中找到。
选择图像文件后,您可以看到该图像的所有 EXIF 标签。
示例应用程序“SuperPhotoView”
SuperPhotoView 是一个演示 CompactExifLib
用法的较大应用程序。您可以在第二个下载包中找到此应用程序的二进制文件。
在此应用程序中,您可以选择一个包含图像文件的文件夹,并在数据表中显示这些图像的最重要 EXIF 标签。可以编辑 EXIF 标签并将其保存到图像文件中。也可以一次性为多个图像执行此操作。此外,还可以以幻灯片放映的形式查看图像,并在每张图像的底部显示 EXIF 标签“图像描述”。
Using the Code
读取和写入标签
CompactExifLib
库包含一个 .cs 文件。因此,使用该库非常方便,只需将文件 ExifData.cs 添加到您的项目中,然后使用 using
命令插入命名空间 CompactExifLib
。该库中的主类是 ExifData
类,它包含图像文件的完整 EXIF 数据。例如,要读取照片“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、TIFF 或 PNG 文件。
ExifData
构造函数会将图像文件的 EXIF 数据完全复制到内存中,并立即关闭文件。然后,所有读取和写入操作都在 EXIF 数据内存副本上执行。要将 EXIF 数据写回图像文件,必须调用 ExifData
方法 Save
public void Save(string DestFileNameWithPath = null,
ExifSaveOptions SaveOptions = ExifSaveOptions.None);
如果第一个参数 DestFileNameWithPath
为 null
或省略,则 Save
方法会覆盖现有图像文件。通过在 DestFileNameWithPath
参数中传递文件名,也可以将图像保存为新文件。如果文件无法保存,Save
方法将抛出异常。可能的原因是
- 文件被写保护。
- 文件访问被拒绝。
- 文件不再可用,例如,已被删除或卷已被移除。
- EXIF 数据过大。JPEG 中的最大 EXIF 数据大小为 65526 字节,TIFF 和 PNG 中的最大 EXIF 数据大小为 2 GB。
有关加载和保存 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)
标签由一个 16 位值定义,称为标签 ID。以下示例代码显示了一些标签 ID 定义。
public enum ExifTagId
{
...
Orientation = 0x0112,
ImageDescription = 0x010E,
DateTimeOriginal = 0x9003,
...
}
但是,仅凭标签 ID 无法指定一个标签。此外,还必须指定 IFD。EXIF 标签被划分为几个部分,称为图像文件目录 (IFD)。如果要读取或写入标签,则必须指定正确的 IFD。哪个 IFD 应用于某个标签,在 EXIF 标准 [EXIF2.32] 中有定义。为了指定 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 标签,存在组合常量,它们包含 IFD(在上 16 位)和标签 ID(在值的下 16 位)。
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 标签都有一个类型,在此库中,标签类型由 ExifTagType
枚举类型指定
public enum ExifTagType
{
Byte = 1,
Ascii = 2,
UShort = 3,
ULong = 4,
URational = 5,
SByte = 6, // Only for TIFFs
Undefined = 7,
SShort = 8, // Only for TIFFs
SLong = 9,
SRational = 10,
Float = 11, // Only for TIFFs
Double = 12 // Only for TIFFs
}
下表描述了标签类型
ExifTagType 类型的常量 | 官方类型名称 | 描述 |
字节型 | 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 | 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
。相反,第二个方法会在标签不存在时创建它。因此,必须指定标签类型。两种方法都会在标签内部内存不足时重新分配内存。然后将当前标签内容复制到新标签内存中。使用第二个方法时,还可以更改现有标签的标签类型。但是,如果要这样做,则必须随后覆盖整个标签内存,或者确保标签内容与新标签类型兼容。
以下示例演示了如何读取根据 EXIF 标准应包含 2、3 或 4 个值的“SubjectArea
”标签。
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);
}
字符串
String
被编码为字符数组,以下标签类型用于编码 string
ExifTagType.Ascii
:这是编码string
的默认标签类型。string
以null
字符终止。ExifTagType.Undefined
:一些string
标签使用此类型进行编码。不存在终止null
字符。ExifTagType.Byte
:Microsoft 定义了特殊的 Unicodestring
标签,使用 16 位 Unicode 字符,存储在字节数组中。string
以 Unicodenull
字符终止。
要将标签读取和写入为 string
,可以使用重载方法 GetTagValue
和 SetTagValue
public bool GetTagValue(ExifTag TagSpec, out string Value, StrCoding Coding);
public bool SetTagValue(ExifTag TagSpec, string Value, StrCoding Coding);
GetTagValue
方法读取 string
标签并删除所有终止 null
字符(如果存在)。SetTagValue
方法写入 string
标签,如果标签类型是 ExifTagType.Ascii
或 ExifTagType.Byte
,则添加终止 null
字符。
类型为 bool
的返回值在操作成功时为 true
,否则为 false
。如果读取的标签不存在或标签类型不正确,则会发生错误。
由于 EXIF 标准定义了几种 string
编码,您必须查看对于特定 EXIF 标签使用的是哪种 string
编码和标签类型。最后一个参数 Coding
指定用于读取或写入标签的代码页和标签类型。此参数是 StrCoding
枚举类型,该类型的常量列在该表中
StrCoding 类型的常量 | 描述 | 预期标签类型 |
Utf8 | Unicode 代码页 UTF 8。基础是 US ASCII 代码页,特殊字符使用 128 至 255 的码位进行编码,使用两个、三个或四个字节。 |
|
UsAscii | US ASCII 代码页,每个字符一个字节,码位从 0 到 127。读取字符串时,128 到 255 之间的所有非法码位都设置为问号。 | ExifTagType.Ascii |
UsAscii_Undef | 与 UsAscii 相同,只是标签类型不同。 | ExifTagType.Undefined |
WestEuropeanWin | Windows 的西欧代码页 1252。基础是 US ASCII 字符集,特殊字符在 128 到 255 的码位中以单个字节编码。 注意:代码页 1252 在 .NET Core 应用程序中不可用,尝试使用它会抛出异常。但是,您可以安装 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 | 除非读取时 ID 代码为“
Unicode ”,否则字符串以 US ASCII 代码页读取和写入。 | ExifTagType.Undefined |
IdCode_WestEu | 除非读取时 ID 代码为“Unicode ”,否则字符串以西欧代码页 1252 读取和写入。 | ExifTagType.Undefined |
可以在 [EXIV2] 找到一个包含所有 EXIF 标签及其对应标签类型和字符串编码描述的非官方表格。下表列出了一些流行的 EXIF 字符串标签
EXIF 标签 | 参数“Coding”的可能值 | 注意 |
ImageDescription | Utf8, UsAscii, WestEuropeanWin | |
Copyright | Utf8, UsAscii, WestEuropeanWin | |
Artist | Utf8, UsAscii, WestEuropeanWin | |
Make | Utf8, UsAscii | |
模型 | Utf8, UsAscii | |
软件 | Utf8, UsAscii | |
日期时间 | Utf8, UsAscii | |
DateTimeOriginal | Utf8, UsAscii | |
DateTimeDigitized | Utf8, UsAscii | |
ExifVersion | UsAscii_Undef | |
FlashPixVersion | UsAscii_Undef | |
UserComment | IdCode_Utf16, IdCode_UsAscii, IdCode_WestEu | |
XpTitle | Utf16Le_Byte | Microsoft 定义 |
XpComment | Utf16Le_Byte | Microsoft 定义 |
XpAuthor | Utf16Le_Byte | Microsoft 定义 |
XpKeywords | Utf16Le_Byte | Microsoft 定义 |
XpSubject | Utf16Le_Byte | Microsoft 定义 |
也许您想知道对于具有多种可能编码的标签(如“ImageDescription
”标签),应该使用哪种 string
编码。根据 EXIF 标准,类型为 ExifTagType.Ascii
的 string
标签只允许使用 US-ASCII 字符(码位 0 到 127),但几乎所有编辑 EXIF 标签的工具都使用扩展代码页(如 Utf
8
或 WestEuropeanWin
)来写入标签。不幸的是,没有通用的方法可以确定用于编码该标签的代码页,但 Utf8
可以用作默认值。如果不使用重音字符等特殊字符,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
。在本库中,定义了 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);
在上述示例中,使用以下方法也可以初始化变量 r1
的值为 1.637
r1 = ExifRational.FromDecimal(1.637m);
日期和时间
在 EXIF 数据中,有以下标签用于存储日期
DateTime
:图像最后修改的日期和时间。DateTimeOriginal
:图像拍摄的日期和时间。DateTimeDigitized
:图像数字化的日期和时间。GpsDateStamp
:来自卫星的 GPS 日期。
所有日期都以字符串形式存储,标签类型为 ExifTagType.Ascii
。使用以下方法,可以通过 DateTime
结构访问这些标签
public bool GetTagValue(ExifTag TagSpec, out DateTime Value,
ExifDateFormat Format = ExifDateFormat.DateAndTime);
public bool SetTagValue(ExifTag TagSpec, DateTime Value,
ExifDateFormat Format = ExifDateFormat.DateAndTime);
有两种日期格式,最后一个参数 Format
指定要使用的格式
ExifDat
eFormat.DateAndTime
:存在日期和时间,它们之间用空格分隔,例如,“2019:12:22 15:23:47
”。此格式用于三个“DateTimeXXX
”标签。ExifDateFormat.DateOnly
:仅存在日期,例如,“2019:12:22
”。此格式用于“GpsDateStamp
”标签。GPS 时间可以通过附加标签访问,见下文。
三个 DateTimeXXX
标签的精度为 1 秒。一些照片相机还写入以下 EXIF 标签,提供秒的小数部分
SubsecTime
:图像最后修改时间的秒的小数部分。SubsecTimeOriginal
:图像拍摄时间的秒的小数部分。SubsecTimeDigitized
:图像数字化时间的秒的小数部分。
使用以下 ExifData
方法,可以方便地访问这些标签,因为小数秒在 DateTime
结构中进行了处理。所有时间均在本地时区。
方法 | 描述 | EXIF 标签 |
public bool GetDateTaken(out DateTime Value);
| 获取拍摄日期,精度为 1 毫秒。 | DateTimeOriginal ,SubsecTimeOriginal |
public bool SetDateTaken(DateTime Value);
| 设置拍摄日期,精度为 1 毫秒。 | DateTimeOriginal ,SubsecTimeOriginal |
public void RemoveDateTaken();
| 删除拍摄日期标签。 | DateTimeOriginal ,SubsecTimeOriginal |
public bool GetDateDigitized(out DateTime Value);
| 获取数字化日期,精度为 1 毫秒。 |
|
public bool SetDateDigitized(DateTime Value);
| 设置数字化日期,精度为 1 毫秒。 | DateTimeDigitized ,SubsecTimeDigitized |
public void RemoveDateDigitized();
| 删除数字化日期标签。 | DateTimeDigitized ,SubsecTimeDigitized |
public bool GetDateChanged(out DateTime Value);
| 获取修改日期,精度为 1 毫秒。 | 日期时间 ,SubsecTime |
public bool SetDateChanged(DateTime Value);
| 设置修改日期,精度为 1 毫秒。 | 日期时间 ,SubsecTime |
public void RemoveDateChanged();
| 删除修改日期标签。 | 日期时间 ,SubsecTime |
由于已知的 EXIF 标签 GpsDateStamp
仅提供日期,因此还有一个 EXIF 标签 GpsTimeStamp
提供 UTC 时区的日期。如果相机记录了小数秒,此标签中也可能包含小数秒。可以通过下表中的 ExifData
方法访问这两个 EXIF 标签
方法 | 描述 | EXIF 标签 |
public bool GetGpsDateTimeStamp(out DateTime Value);
| 获取 UTC 时区的 GPS 日期和时间戳。 | GpsDateStamp ,GpsTimeStamp |
public bool SetGpsDateTimeStamp(DateTime Value);
| 设置 UTC 时区的 GPS 日期和时间戳。 | GpsDateStamp ,GpsTimeStamp |
public void RemoveGpsDateTimeStamp();
| 删除 GPS 日期和时间戳标签。 | GpsDateStamp ,GpsTimeStamp |
原始数据和字节顺序
也可以读取标签的原始数据字节,而无需任何解释。为此,可以使用 GetTagRawData
方法
public bool GetTagRawData(ExifTag TagSpec, out ExifTagType TagType, out int ValueCount,
out byte[] RawData)
在第一个参数 TagSpec
中传递由 IFD 和标签 ID 组成的 EXIF 标签。标签数据在以下参数中返回
TagType
:标签类型。ValueCount
:标签中存储的值的数量。请记住,EXIF 标签通常是值的数组,而不仅仅是一个值,因此ValueCount
是数组元素的数量。仅当单个标签值的大小为 1 字节(标签类型ExifTagType.Byte
、Ascii
和Undefined
)时,ValueCount
返回的数量也是原始数据字节的数量。例如,对于ExifTagType.UShort
类型的标签,原始数据字节的数量为2*ValueCount
。RawData
:包含标签原始数据字节的数组。原始数据字节的数量为RawData.Length
。- 方法返回值:
true
= 成功读取标签数据,false
= 标签不存在。
原始数据字节的解释取决于 EXIF 数据的字节顺序。EXIF 数据可以存储为小端序 (LE) 或大端序 (BE) 格式。大多数照相机和图像处理工具都以小端序格式写入 EXIF 数据,但有些相机和工具使用大端序格式。字节顺序对于所有类型为 ExifTagType.UShort
、SShort
、ULong
、SLong
、URational
、SRational
、Float
和 Double
的标签都很重要。此外,还有一些 Undefined
类型的标签需要单独处理字节顺序。在本库中,可以通过 ExifData
属性 ByteOrder
来确定当前 EXIF 块的字节顺序
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);
加载和保存 EXIF 数据
要从图像加载 EXIF 数据,有两个 ExifData
构造函数可用
public ExifData(string FileNameWithPath, ExifLoadOptions Options = 0);
public ExifData(Stream ImageStream, ExifLoadOptions Options = 0);
第一个构造函数是已知的,它从第一个参数 FileNameWithPath
中传递的 JPEG、TIFF 或 PNG 文件加载 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 数据的原始图像文件仍必须可用。此外,只有当使用第一个 ExifData
构造函数(以文件名作为参数)加载 EXIF 数据时,才能使用 Save
方法。第二个参数 SaveOptions
目前未使用。
可以使用第二个 Save
方法将 EXIF 数据保存到流中
public void Save(Stream SourceStream, Stream DestStream, ExifSaveOptions SaveOptions = 0);
第一个参数 SourceStream
应该是从中加载原始 EXIF 数据的流。SourceStream
的流位置必须在图像数据的开头,并且 SourceStream
必须是可查找的。第二个参数 DestStream
指定应将新图像数据写入的流。
移除标签
要移除标签,可以使用以下方法
public bool RemoveTag(ExifTag TagSpec);
public bool RemoveAllTagsFromIfd(ExifIfd Ifd);
public void RemoveAllTags();
RemoveTag
方法移除单个标签,RemoveAllTagsFromIfd
方法从特定 IFD 移除所有标签,RemoveAllTags
方法从图像文件移除所有标签,使 EXIF 块变为空。如果移除了 IFD ThumbnailData
,缩略图也会被自动移除。在 TIFF 图像中,无法移除所有 EXIF 标签,因为在 IFD PrimaryData 中存在包含内部图像数据数据的标签。因此,如果应用于 TIFF 图像,调用 RemoveAllTags
不会移除这些标签。更多信息请参见章节“替代元数据格式”。
替代元数据格式
除了 EXIF 数据之外,还有其他方法可以在图像文件中指定元数据。流行的替代元数据格式是 IPTC 和 XMP 块,它们独立于图像文件格式定义。例如,如果您使用 Windows 10 的资源管理器在 JPEG 文件中写入元数据,元数据将写入 EXIF 块,并另外写入 XMP 块。如果 XMP 块不存在,资源管理器将始终创建一个 XMP 块。
在 JPEG 图像中,存在 JPEG 注释块,该块可能包含任意文本。PNG 文件有自己的元数据格式,这些元数据存储在多个 PNG 文件块中。使用以下方法,可以检测和移除这些替代描述块
public bool ImageFileBlockExists(ImageFileBlock BlockType);
public void RemoveImageFileBlock(ImageFileBlock BlockType);
public enum ImageFileBlock
{
Unknown = 0, // Internal value, do not use
Exif = 1,
Iptc = 2,
Xmp = 3,
JpegComment = 4,
PngMetaData = 5,
PngDateChanged = 6
};
使用 RemoveImageFileBlock
方法还可以移除 EXIF 块,即从图像文件中移除所有 EXIF 标签
RemoveImageFileBlock(ImageFileBlock.Exif);
这将产生与调用相同的结果
RemoveAllTags();
但在 TIFF 图像中有一些特殊情况
- TIFF 文件没有文件块结构,而是只能存储 EXIF 标签。因此,IPTC 和 XMP 块使用特定的 EXIF 标签
ExifTag.IptcMetadata
和ExifTag.XmpMetadata
进行存储。这些标签只能用于 TIFF 文件。 - 从 TIFF 文件中移除 EXIF 块并不会真正移除所有 EXIF 标签!TIFF 内部 EXIF 标签、IPTC 块的 EXIF 标签和 XMP 块的 EXIF 标签不会被移除。
GPS 数据
如果相机将 GPS 数据写入图像,您可以使用 IFD“GPS info data”中的 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 ,GpsLongitudeRef |
public bool SetGpsLongitude(GeoCoordinate Value);
| 设置 GPS 经度。 | GpsLongitude ,GpsLongitudeRef |
public void RemoveGpsLongitude();
| 删除 GPS 经度标签。 | GpsLongitude ,GpsLongitudeRef |
public bool GetGpsLatitude(out GeoCoordinate Value);
| 获取 GPS 纬度。 | GpsLatitude ,GpsLatitudeRef |
public bool SetGpsLatitude(GeoCoordinate Value);
| 设置 GPS 纬度。 | GpsLatitude ,GpsLatitudeRef |
public void RemoveGpsLatitude();
| 删除 GPS 纬度标签。 | GpsLatitude ,GpsLatitudeRef |
public bool GetGpsAltitude(out decimal Value);
| 获取相对于海平面的高度(以米为单位)。正值表示“高于海平面”,负值表示“低于海平面”。 | GpsAltitude ,GpsAltitudeRef |
public bool SetGpsAltitude(decimal Value);
| 设置相对于海平面的高度(以米为单位)。 | GpsAltitude ,GpsAltitudeRef |
public void RemoveGpsAltitude();
| 删除 GPS 高度标签。 | GpsAltitude ,GpsAltitudeRef |
还有一些其他的 GPS 标签可用。为了移除图像中的所有 GPS 标签,可以使用 ExifData
方法 RemoveAllTagsFromIfd
ExifData TestExif;
...
TestExif.RemoveAllTagsFromIfd(ExifIfd.GpsInfoData);
如果想检查是否存在任何 GPS 标签,可以使用 ExifData
方法 IfdExists
if (TestExif.IfdExists(ExifIfd.GpsInfoData)) ...
缩略图
在 JPEG 文件中可以存储缩略图,这是一个小的预览图像。TIFF 和 PNG 文件不支持此功能。缩略图存储在 EXIF 数据中,由于 JPEG 文件中的 EXIF 块限制为 64 kB,因此缩略图的大小也有限。以下方法 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
中指定为数组。此方法不会复制数组,因此调用此方法后不应修改该数组。
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 | PNG |
EXIF 块是可选的,可以移除。如果移除,图像的外观不会改变,只有 EXIF 标签“Orientation”可能会改变图像旋转。EXIF 块非常常见且分布广泛。 | 整个文件由一个 EXIF 块或一系列多个 EXIF 块组成。因此,EXIF 块是必不可少的,不能移除。有些 EXIF 标签包含内部图像数据,更改或移除这些内部 EXIF 标签会损坏图像! | 有自己的 PNG 元数据格式,并且在后期才为 PNG 标准添加了使用 EXIF 数据的可能性。因此,EXIF 块非常罕见,只有少数工具支持 EXIF 数据。 |
可以存储一个图像。 | 单个文件中可以存储多个图像和 EXIF 块。这也被称为多页 TIFF 文件。 | 只能存储一个图像。 |
支持 EXIF 块中的缩略图。 | 不支持第一个 EXIF 块中的缩略图,但您可以将缩略图存储为第二个图像。 | 不支持 EXIF 块中的缩略图。 |
EXIF 块的大小限制为 65526 字节。 | 理论上, EXIF 块的大小可以高达 4 GB,但在本库中限制为 2 GB。 | 理论上, EXIF 块的大小可以高达 4 GB,但在本库中限制为 2 GB。 |
其他有用方法
检查 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 标签和缩略图
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.exiv2.org/tags.html |
[JPEGWiki] | JPEG 文件规范 | https://en.wikipedia.org/wiki/JPEG_File_Interchange_Format |
[TIFF6] | TIFF 文件规范 | https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFF6.pdf |
[PNGWiki] | PNG 文件规范 | https://en.wikipedia.org/wiki/Portable_Network_Graphics |
历史
版本号 | 日期 | 描述 |
1.6 | 2021-06-25 |
|
1.5 | 2020-05-30 |
|
1.4 | 2021-03-31 |
|
1.3 | 2021-03-15 |
|
1.2 | 2021-02-13 |
|
1.1 | 2020-05-16 |
|
1.0 | 2020-05-01 | 初始版本 |