CompactExifLib:访问JPEG文件中的EXIF标签

目录

介绍

背景

演示应用程序

使用代码

读写标签

标签ID和图像文件目录(IFD)

标签类型

整数

数组标签

字符串

有理数

日期和时间

原始数据和字节顺序

移除标签

加载和保存EXIF数据

 GPS数据

缩图图片

JPEG 和 TIFF 文件之间的差异

其他有用的方法

参考


介绍

几乎所有照片和智能手机相机都使用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的新库。

例如,从400JPEG照片中读取了拍摄日期的EXIF标签,并测量了以毫秒为单位的时间,并将其记录在下表中:

WPFBitmapMetadata

CompactExifLib

速度因数

1774毫秒

40.2毫秒

44.1

在第一列中,使用.NET FrameworkWPFBitmapMetadata读取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数据写回图片文件时,必须调用ExifDataSave方法:

public void Save(string DestFileNameWithPath = null, 
              ExifSaveOptions SaveOptions = ExifSaveOptions.None);

如果参数NewFileNameWithPathnull或省略,则该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

标签由称为标签ID16位值定义。以下示例代码显示了一些标签ID定义。

public enum ExifTagId
{
  ...
  Orientation = 0x0112,
  ImageDescription = 0x010E,
  DateTimeOriginal = 0x9003,
  ...
}

但是标签ID不足以指定标签。另外,必须指定IFDEXIF标签分为几个部分,称为图像文件目录(IFD)。如果要读取或写入标签,则必须指定正确的IFDEXIF标准[EXIF2.32]中定义了应将哪个IFD用于标签。为了指定IFD,可以使用以下常量。

public enum ExifIfd
{
  PrimaryData = 0,
  PrivateData = 1,
  GpsInfoData = 2,
  Interoperability = 3,
  ThumbnailData = 4
}

IFD PrimaryDataEXIF数据的主要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,
  ...
}

这里,常量OrientationImageDescription定义标记存储在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]的定义,此标记应为16SHORT类型的值,该值与常量ExifTagType.UShort相对应。

ExifData TestExif;
int ImageOrientation;

...

TestExif.SetTagValue(ExifTag.Orientation, 6, ExifTagType.UShort);
TestExif.GetTagValue(ExifTag.Orientation, out ImageOrientation);

使用图像方向标签,可以定义顺时针旋转90180270度,并定义图像矩阵的反射,前提是图像查看器在绘制图像时考虑使用此EXIF标签。例如,此标签的值6定义顺时针旋转90度。

数组标签

大多数标签仅包含一个值,但是有些标签存储多个值,即值的数组。可以使用已知方法GetTagValueSetTagValue读取和写入数组并且该参数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标准包含234个值。

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:这是用于编码的默认标记类型stringstring的结尾是一个null字符。
  • ExifTagType.Undefined:某些string标签使用这种类型编码。不存在终止null字符。
  • ExifTagType.ByteMicrosoft定义了带有16Unicode字符的特殊unicode string标签,这些标签存储在字节数组中。string将以Unicode null字符终止。

为了以strings读写标签,有一些重载的方法GetTagValueSetTagValue可用:

public bool GetTagValue(ExifTag TagSpec, out string Value, StrCoding Coding);

public bool SetTagValue(ExifTag TagSpec, string Value, StrCoding Coding);

该方法GetTagValue读取string标记,并删除所有终止null字符(如果存在)。如果标签类型为ExifTagType.AsciiExifTagType.Byte,则该方法SetTagValue将编写一个string标签并添加一个终止null字符。

当操作成功的时候类型bool的返回值是true,否则为false。如果读取标签且标签不存在或标签类型不正确,则会发生错误。

由于string EXIF标准中定义了几种编码,因此您必须查看string特定EXIF标签使用哪种编码和标签类型。最后一个参数Coding参数指定应用于读取或写入标签的代码页和标签类型。此参数是的enum类型StrCoding和该类型的下表中列出的常量:

类型为StrCoding的常量

描述

预期标签类型

Utf8

Unicode代码页UTF8。基本代码是US ASCII代码页,特殊字符使用128255之间的代码点编码为两个,三个或四个字节。

ExifTagType.Ascii

UsAscii

美国ASCII代码页,每个字符一个字节,并且代码点从0127。读取字符串时,所有从128255的非法代码点都将设置为问号。

ExifTagType.Ascii

UsAscii_Undef

UsAscii相同,除了标签类型不同。

ExifTagType.Undefined

WestEuropeanWin

Windows的西欧代码页1252。基本是美国ASCII字符集,特殊字符在128255的代码点中被编码为单个字节。

注意.NET Core应用程序中的代码页1252不可用,并且在尝试使用时会引发异常。但是您可以安装一个NuGet包来扩展可用的代码页。

ExifTagType.Ascii

Utf16Le_Byte

Unicode代码页UTF 16 LE(小端),每个字符两个或四个字节。即使将EXIF块编码为BE(大端),字节顺序也始终为LE

ExifTagType.Byte

IdCode_Utf16

字符串前面有一个8字节的ID码。ID代码定义了字符串编码,此库支持ID代码DefaultAscii“ Unicode

读取标签:
如果ID代码为DefaultAsciistring则在US ASCII代码页中读取。如果ID代码为Unicode,则将string其读取为UTF 16字符串。根据EXIF块的字节顺序,UTF 16字节顺序为LE(小端)或BE(大端)。
写入标签:
ID代码设置为Unicode,并且将string其写入UTF 16 LEUTF 16 BE代码页中。

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字符(代码点0127),但是几乎所有用于编辑EXIF标签的工具都使用扩展的代码页,例如Utf8WestEuropeanWin用于编写标签。不幸的是,没有通用的方法来确定使用哪个代码页对标签进行编码,但是Utf8可以用作默认方法。这是由照片的相机,如标签手写标签Make“ Model不使用特殊字符。如果不使用像重音字符这样的特殊字符,编码页Utf8, UsAsciiWestEuropeanWin甚至是相同的,像标签MakeModel这样的照片相机手写的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.SRationalExifTagType.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.SRationalExifTagType.URational的标记,否则它将失败。使用SetTagValue编写标签时,参数TagType可以是ExifTagType.SRationalExifTagType.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
SubsecTimeOriginal

public bool SetDateTaken(DateTime Value);

1毫秒的精度设置日期。

DateTimeOriginal
SubsecTimeOriginal

public void RemoveDateTaken();

删除日期的标签。

DateTimeOriginal
SubsecTimeOriginal

public bool GetDateDigitized(out DateTime Value);

1毫秒的精度获取数字化的日期。

DateTimeDigitized
SubsecTimeDigitized

public bool SetDateDigitized(DateTime Value);

设置日期的数字化精度为1毫秒。

DateTimeDigitized
SubsecTimeDigitized

public void RemoveDateDigitized();

删除数字化日期的标签。

DateTimeDigitized
SubsecTimeDigitized

public bool GetDateChanged(out DateTime Value);

获取日期更改的精度为1毫秒。

DateTime
SubsecTime

public bool SetDateChanged(DateTime Value);

设置日期以1毫秒的精度更改。

DateTime
SubsecTime

public void RemoveDateChanged();

删除更改日期的标签。

DateTime
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)

IFD和标签ID组成的 EXIF 标签在第一个参数TagSpec中传递。标签数据通过以下参数返回:

  • TagType:标签的类型。
  • ValueCount:存储在标签中的值的数量。请记住,EXIF标记通常是一个值数组,而不仅仅是单个值,因此ValueCount是数组元素的数量。仅当单个标签值的大小为1个字节(标签类型ExifTagType.Byte,AsciiUndefined)时,返回ValueCount的数字也是原始数据字节数。例如,对于标签类型ExifTagType.UShort,原始数据字节数为2*ValueCount
  • RawData: 带有标签原始数据字节的数组。原始数据字节数为RawData.Length.
  • 方法返回值:true=标签数据读取成功,false=标签不存在。

原始数据字节的解释取决于EXIF数据的字节顺序。EXIF数据可以以Little Endian (LE) Big Endian (BE) 格式存储。大多数相机和图像处理工具以Little Endian格式写入EXIF数据,但也有一些相机和工具使用Big Endian格式。字节顺序是类型ExifTagType.UShortSShortULongSLongURationalSRationalFloatDouble的所有标签很重要。此外,还有一些类型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的原始数据字节数由参数TagTypeValueCount=标签的数组元素数)隐式定义。您可以通过已知方法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中传递的JPEGTIFF文件加载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
};

流行的替代元数据块是IPTCXMP块。在JPEG图像中,还有可能包含任意文本的JPEG注释块。

TIFF图像中有一些细节:

  • IPTCXMP块不像在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
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图像的小预览图像。缩略图图像存储在EXIF数据中,并且由于完整的EXIF数据只能具有65535字节,因此缩略图图像的大小受到限制。以下方法ThumbnailImageExists检查缩略图是否存在:

public bool ThumbnailImageExists();

为了读取缩略图图像,该方法GetThumbnailImage可用:

public bool GetThumbnailImage(out byte[] ThumbnailData, out int ThumbnailIndex,
                              out int ThumbnailByteCount);

在第一个参数ThumbnailData中,将给出带有缩略图的数组。该数组可能包含更多数据,调用者不得更改它。缩略图的第一个字节存储在数组索引处,该索引在第二个参数中返回ThumbnailIndex。缩略图的大小在最后一个参数中返回ThumbnailByteCount。如果未定义缩略图,则返回值为false,数组ThumbnailDatanull

方法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.exiv2.org/tags.html

https://www.codeproject.com/Articles/5251929/CompactExifLib-Access-to-EXIF-Tags-in-JPEG-Files

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值