|
|
|
@ -11,97 +11,122 @@ namespace ImageProcessorCore |
|
|
|
using System.Linq; |
|
|
|
using System.Text; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Reads and parses EXIF data from a byte array
|
|
|
|
/// </summary>
|
|
|
|
internal sealed class ExifReader |
|
|
|
{ |
|
|
|
private delegate TDataType ConverterMethod<TDataType>(byte[] data); |
|
|
|
|
|
|
|
private byte[] data; |
|
|
|
private Collection<ExifTag> invalidTags = new Collection<ExifTag>(); |
|
|
|
private uint index; |
|
|
|
private readonly Collection<ExifTag> invalidTags = new Collection<ExifTag>(); |
|
|
|
private byte[] exifData; |
|
|
|
private uint currentIndex; |
|
|
|
private bool isLittleEndian; |
|
|
|
private uint exifOffset; |
|
|
|
private uint gpsOffset; |
|
|
|
private uint startIndex; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the thumbnail length in the byte stream
|
|
|
|
/// </summary>
|
|
|
|
public uint ThumbnailLength |
|
|
|
{ |
|
|
|
get; |
|
|
|
private set; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the thumbnail offset position in the byte stream
|
|
|
|
/// </summary>
|
|
|
|
public uint ThumbnailOffset |
|
|
|
{ |
|
|
|
get; |
|
|
|
private set; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets the remaining length.
|
|
|
|
/// </summary>
|
|
|
|
private int RemainingLength |
|
|
|
{ |
|
|
|
get |
|
|
|
{ |
|
|
|
if (this.index >= this.data.Length) |
|
|
|
if (this.currentIndex >= this.exifData.Length) |
|
|
|
{ |
|
|
|
return 0; |
|
|
|
} |
|
|
|
|
|
|
|
return this.data.Length - (int)this.index; |
|
|
|
return this.exifData.Length - (int)this.currentIndex; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Reads and returns the collection of EXIF values.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="data">The data.</param>
|
|
|
|
/// <returns>
|
|
|
|
/// The <see cref="Collection{ExifValue}"/>.
|
|
|
|
/// </returns>
|
|
|
|
public Collection<ExifValue> Read(byte[] data) |
|
|
|
{ |
|
|
|
Collection<ExifValue> result = new Collection<ExifValue>(); |
|
|
|
|
|
|
|
this.data = data; |
|
|
|
this.exifData = data; |
|
|
|
|
|
|
|
if (GetString(4) == "Exif") |
|
|
|
if (this.GetString(4) == "Exif") |
|
|
|
{ |
|
|
|
if (GetShort() != 0) |
|
|
|
if (this.GetShort() != 0) |
|
|
|
{ |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
this.startIndex = 6; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
this.index = 0; |
|
|
|
this.currentIndex = 0; |
|
|
|
} |
|
|
|
|
|
|
|
this.isLittleEndian = GetString(2) == "II"; |
|
|
|
this.isLittleEndian = this.GetString(2) == "II"; |
|
|
|
|
|
|
|
if (GetShort() != 0x002A) |
|
|
|
if (this.GetShort() != 0x002A) |
|
|
|
{ |
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
uint ifdOffset = GetLong(); |
|
|
|
AddValues(result, ifdOffset); |
|
|
|
uint ifdOffset = this.GetLong(); |
|
|
|
this.AddValues(result, ifdOffset); |
|
|
|
|
|
|
|
uint thumbnailOffset = GetLong(); |
|
|
|
GetThumbnail(thumbnailOffset); |
|
|
|
uint thumbnailOffset = this.GetLong(); |
|
|
|
this.GetThumbnail(thumbnailOffset); |
|
|
|
|
|
|
|
if (this.exifOffset != 0) |
|
|
|
AddValues(result, this.exifOffset); |
|
|
|
{ |
|
|
|
this.AddValues(result, this.exifOffset); |
|
|
|
} |
|
|
|
|
|
|
|
if (this.gpsOffset != 0) |
|
|
|
AddValues(result, this.gpsOffset); |
|
|
|
{ |
|
|
|
this.AddValues(result, this.gpsOffset); |
|
|
|
} |
|
|
|
|
|
|
|
return result; |
|
|
|
} |
|
|
|
|
|
|
|
public IEnumerable<ExifTag> InvalidTags |
|
|
|
{ |
|
|
|
get |
|
|
|
{ |
|
|
|
return this.invalidTags; |
|
|
|
} |
|
|
|
} |
|
|
|
public IEnumerable<ExifTag> InvalidTags => this.invalidTags; |
|
|
|
|
|
|
|
private void AddValues(Collection<ExifValue> values, uint index) |
|
|
|
{ |
|
|
|
this.index = this.startIndex + index; |
|
|
|
this.currentIndex = this.startIndex + index; |
|
|
|
ushort count = GetShort(); |
|
|
|
|
|
|
|
for (ushort i = 0; i < count; i++) |
|
|
|
{ |
|
|
|
ExifValue value = CreateValue(); |
|
|
|
if (value == null) |
|
|
|
{ |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
bool duplicate = false; |
|
|
|
foreach (ExifValue val in values) |
|
|
|
@ -114,27 +139,37 @@ namespace ImageProcessorCore |
|
|
|
} |
|
|
|
|
|
|
|
if (duplicate) |
|
|
|
{ |
|
|
|
continue; |
|
|
|
} |
|
|
|
|
|
|
|
if (value.Tag == ExifTag.SubIFDOffset) |
|
|
|
{ |
|
|
|
if (value.DataType == ExifDataType.Long) |
|
|
|
{ |
|
|
|
this.exifOffset = (uint)value.Value; |
|
|
|
} |
|
|
|
} |
|
|
|
else if (value.Tag == ExifTag.GPSIFDOffset) |
|
|
|
{ |
|
|
|
if (value.DataType == ExifDataType.Long) |
|
|
|
{ |
|
|
|
this.gpsOffset = (uint)value.Value; |
|
|
|
} |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
values.Add(value); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private object ConvertValue(ExifDataType dataType, byte[] data, uint numberOfComponents) |
|
|
|
{ |
|
|
|
if (data == null || data.Length == 0) |
|
|
|
{ |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
switch (dataType) |
|
|
|
{ |
|
|
|
@ -144,93 +179,120 @@ namespace ImageProcessorCore |
|
|
|
return ToString(data); |
|
|
|
case ExifDataType.Byte: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
{ |
|
|
|
return ToByte(data); |
|
|
|
else |
|
|
|
return data; |
|
|
|
} |
|
|
|
|
|
|
|
return data; |
|
|
|
case ExifDataType.DoubleFloat: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
return ToDouble(data); |
|
|
|
else |
|
|
|
return ToArray(dataType, data, ToDouble); |
|
|
|
{ |
|
|
|
return this.ToDouble(data); |
|
|
|
} |
|
|
|
|
|
|
|
return ToArray(dataType, data, this.ToDouble); |
|
|
|
case ExifDataType.Long: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
return ToLong(data); |
|
|
|
else |
|
|
|
return ToArray(dataType, data, ToLong); |
|
|
|
{ |
|
|
|
return this.ToLong(data); |
|
|
|
} |
|
|
|
|
|
|
|
return ToArray(dataType, data, ToLong); |
|
|
|
case ExifDataType.Rational: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
return ToRational(data); |
|
|
|
else |
|
|
|
return ToArray(dataType, data, ToRational); |
|
|
|
{ |
|
|
|
return this.ToRational(data); |
|
|
|
} |
|
|
|
|
|
|
|
return ToArray(dataType, data, this.ToRational); |
|
|
|
case ExifDataType.Short: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
return ToShort(data); |
|
|
|
else |
|
|
|
return ToArray(dataType, data, ToShort); |
|
|
|
{ |
|
|
|
return this.ToShort(data); |
|
|
|
} |
|
|
|
|
|
|
|
return ToArray(dataType, data, this.ToShort); |
|
|
|
case ExifDataType.SignedByte: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
return ToSignedByte(data); |
|
|
|
else |
|
|
|
return ToArray(dataType, data, ToSignedByte); |
|
|
|
{ |
|
|
|
return this.ToSignedByte(data); |
|
|
|
} |
|
|
|
|
|
|
|
return ToArray(dataType, data, this.ToSignedByte); |
|
|
|
case ExifDataType.SignedLong: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
return ToSignedLong(data); |
|
|
|
else |
|
|
|
return ToArray(dataType, data, ToSignedLong); |
|
|
|
{ |
|
|
|
return this.ToSignedLong(data); |
|
|
|
} |
|
|
|
|
|
|
|
return ToArray(dataType, data, this.ToSignedLong); |
|
|
|
case ExifDataType.SignedRational: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
return ToSignedRational(data); |
|
|
|
else |
|
|
|
return ToArray(dataType, data, ToSignedRational); |
|
|
|
{ |
|
|
|
return this.ToSignedRational(data); |
|
|
|
} |
|
|
|
|
|
|
|
return ToArray(dataType, data, this.ToSignedRational); |
|
|
|
case ExifDataType.SignedShort: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
return ToSignedShort(data); |
|
|
|
else |
|
|
|
return ToArray(dataType, data, ToSignedShort); |
|
|
|
{ |
|
|
|
return this.ToSignedShort(data); |
|
|
|
} |
|
|
|
|
|
|
|
return ToArray(dataType, data, this.ToSignedShort); |
|
|
|
case ExifDataType.SingleFloat: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
{ |
|
|
|
return ToSingle(data); |
|
|
|
else |
|
|
|
return ToArray(dataType, data, ToSingle); |
|
|
|
} |
|
|
|
|
|
|
|
return ToArray(dataType, data, ToSingle); |
|
|
|
case ExifDataType.Undefined: |
|
|
|
if (numberOfComponents == 1) |
|
|
|
{ |
|
|
|
return ToByte(data); |
|
|
|
else |
|
|
|
return data; |
|
|
|
} |
|
|
|
|
|
|
|
return data; |
|
|
|
default: |
|
|
|
throw new NotImplementedException(); |
|
|
|
throw new NotSupportedException(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private ExifValue CreateValue() |
|
|
|
{ |
|
|
|
if (RemainingLength < 12) |
|
|
|
{ |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
ExifTag tag = ToEnum(GetShort(), ExifTag.Unknown); |
|
|
|
ExifDataType dataType = ToEnum(GetShort(), ExifDataType.Unknown); |
|
|
|
object value = null; |
|
|
|
ExifTag tag = ToEnum(this.GetShort(), ExifTag.Unknown); |
|
|
|
ExifDataType dataType = ToEnum(this.GetShort(), ExifDataType.Unknown); |
|
|
|
object value; |
|
|
|
|
|
|
|
if (dataType == ExifDataType.Unknown) |
|
|
|
return new ExifValue(tag, dataType, value, false); |
|
|
|
{ |
|
|
|
return new ExifValue(tag, dataType, null, false); |
|
|
|
} |
|
|
|
|
|
|
|
uint numberOfComponents = (uint)GetLong(); |
|
|
|
uint numberOfComponents = this.GetLong(); |
|
|
|
|
|
|
|
uint size = numberOfComponents * ExifValue.GetSize(dataType); |
|
|
|
byte[] data = GetBytes(4); |
|
|
|
|
|
|
|
if (size > 4) |
|
|
|
{ |
|
|
|
uint oldIndex = this.index; |
|
|
|
this.index = ToLong(data) + this.startIndex; |
|
|
|
uint oldIndex = this.currentIndex; |
|
|
|
this.currentIndex = ToLong(data) + this.startIndex; |
|
|
|
if (RemainingLength < size) |
|
|
|
{ |
|
|
|
this.invalidTags.Add(tag); |
|
|
|
this.index = oldIndex; |
|
|
|
this.currentIndex = oldIndex; |
|
|
|
return null; |
|
|
|
} |
|
|
|
value = ConvertValue(dataType, GetBytes(size), numberOfComponents); |
|
|
|
this.index = oldIndex; |
|
|
|
|
|
|
|
value = ConvertValue(dataType, this.GetBytes(size), numberOfComponents); |
|
|
|
this.currentIndex = oldIndex; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
@ -246,19 +308,23 @@ namespace ImageProcessorCore |
|
|
|
{ |
|
|
|
TEnum enumValue = (TEnum)(object)value; |
|
|
|
if (Enum.GetValues(typeof(TEnum)).Cast<TEnum>().Any(v => v.Equals(enumValue))) |
|
|
|
{ |
|
|
|
return enumValue; |
|
|
|
} |
|
|
|
|
|
|
|
return defaultValue; |
|
|
|
} |
|
|
|
|
|
|
|
private byte[] GetBytes(uint length) |
|
|
|
{ |
|
|
|
if (this.index + length > (uint)this.data.Length) |
|
|
|
if (this.currentIndex + length > (uint)this.exifData.Length) |
|
|
|
{ |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
byte[] data = new byte[length]; |
|
|
|
Array.Copy(this.data, (int)this.index, data, 0, (int)length); |
|
|
|
this.index += length; |
|
|
|
Array.Copy(this.exifData, (int)this.currentIndex, data, 0, (int)length); |
|
|
|
this.currentIndex += length; |
|
|
|
|
|
|
|
return data; |
|
|
|
} |
|
|
|
@ -286,9 +352,13 @@ namespace ImageProcessorCore |
|
|
|
foreach (ExifValue value in values) |
|
|
|
{ |
|
|
|
if (value.Tag == ExifTag.JPEGInterchangeFormat && (value.DataType == ExifDataType.Long)) |
|
|
|
{ |
|
|
|
ThumbnailOffset = (uint)value.Value + this.startIndex; |
|
|
|
} |
|
|
|
else if (value.Tag == ExifTag.JPEGInterchangeFormatLength && value.DataType == ExifDataType.Long) |
|
|
|
{ |
|
|
|
ThumbnailLength = (uint)value.Value; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -319,7 +389,9 @@ namespace ImageProcessorCore |
|
|
|
private double ToDouble(byte[] data) |
|
|
|
{ |
|
|
|
if (!ValidateArray(data, 8)) |
|
|
|
{ |
|
|
|
return default(double); |
|
|
|
} |
|
|
|
|
|
|
|
return BitConverter.ToDouble(data, 0); |
|
|
|
} |
|
|
|
@ -327,7 +399,9 @@ namespace ImageProcessorCore |
|
|
|
private uint ToLong(byte[] data) |
|
|
|
{ |
|
|
|
if (!ValidateArray(data, 4)) |
|
|
|
{ |
|
|
|
return default(uint); |
|
|
|
} |
|
|
|
|
|
|
|
return BitConverter.ToUInt32(data, 0); |
|
|
|
} |
|
|
|
@ -336,7 +410,9 @@ namespace ImageProcessorCore |
|
|
|
{ |
|
|
|
|
|
|
|
if (!ValidateArray(data, 2)) |
|
|
|
{ |
|
|
|
return default(ushort); |
|
|
|
} |
|
|
|
|
|
|
|
return BitConverter.ToUInt16(data, 0); |
|
|
|
} |
|
|
|
@ -344,7 +420,9 @@ namespace ImageProcessorCore |
|
|
|
private float ToSingle(byte[] data) |
|
|
|
{ |
|
|
|
if (!ValidateArray(data, 4)) |
|
|
|
{ |
|
|
|
return default(float); |
|
|
|
} |
|
|
|
|
|
|
|
return BitConverter.ToSingle(data, 0); |
|
|
|
} |
|
|
|
@ -354,7 +432,9 @@ namespace ImageProcessorCore |
|
|
|
string result = Encoding.UTF8.GetString(data, 0, data.Length); |
|
|
|
int nullCharIndex = result.IndexOf('\0'); |
|
|
|
if (nullCharIndex != -1) |
|
|
|
{ |
|
|
|
result = result.Substring(0, nullCharIndex); |
|
|
|
} |
|
|
|
|
|
|
|
return result; |
|
|
|
} |
|
|
|
@ -362,11 +442,14 @@ namespace ImageProcessorCore |
|
|
|
private double ToRational(byte[] data) |
|
|
|
{ |
|
|
|
if (!ValidateArray(data, 8, 4)) |
|
|
|
{ |
|
|
|
return default(double); |
|
|
|
} |
|
|
|
|
|
|
|
uint numerator = BitConverter.ToUInt32(data, 0); |
|
|
|
uint denominator = BitConverter.ToUInt32(data, 4); |
|
|
|
|
|
|
|
// TODO: investigate the possibility of a Rational struct
|
|
|
|
return numerator / (double)denominator; |
|
|
|
} |
|
|
|
|
|
|
|
@ -378,7 +461,9 @@ namespace ImageProcessorCore |
|
|
|
private int ToSignedLong(byte[] data) |
|
|
|
{ |
|
|
|
if (!ValidateArray(data, 4)) |
|
|
|
{ |
|
|
|
return default(int); |
|
|
|
} |
|
|
|
|
|
|
|
return BitConverter.ToInt32(data, 0); |
|
|
|
} |
|
|
|
@ -386,7 +471,9 @@ namespace ImageProcessorCore |
|
|
|
private double ToSignedRational(byte[] data) |
|
|
|
{ |
|
|
|
if (!ValidateArray(data, 8, 4)) |
|
|
|
{ |
|
|
|
return default(double); |
|
|
|
} |
|
|
|
|
|
|
|
int numerator = BitConverter.ToInt32(data, 0); |
|
|
|
int denominator = BitConverter.ToInt32(data, 4); |
|
|
|
@ -397,7 +484,9 @@ namespace ImageProcessorCore |
|
|
|
private short ToSignedShort(byte[] data) |
|
|
|
{ |
|
|
|
if (!ValidateArray(data, 2)) |
|
|
|
{ |
|
|
|
return default(short); |
|
|
|
} |
|
|
|
|
|
|
|
return BitConverter.ToInt16(data, 0); |
|
|
|
} |
|
|
|
@ -410,10 +499,14 @@ namespace ImageProcessorCore |
|
|
|
private bool ValidateArray(byte[] data, int size, int stepSize) |
|
|
|
{ |
|
|
|
if (data == null || data.Length < size) |
|
|
|
{ |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
if (this.isLittleEndian == BitConverter.IsLittleEndian) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
for (int i = 0; i < data.Length; i += stepSize) |
|
|
|
{ |
|
|
|
|