Browse Source

Merge pull request #450 from dlemstra/Exif

Exif support

Former-commit-id: 21e226e781e516c4c2f028d609ddc662bb083f9d
Former-commit-id: 73a1a23ba9ebf37421b80f3890cf12a5c86fe848
Former-commit-id: 446b1ed069e6f7316f92801e629d1dfdff28ce29
af/merge-core
James Jackson-South 10 years ago
committed by GitHub
parent
commit
382da4d8b6
  1. 58
      src/ImageProcessorCore/Common/Helpers/Guard.cs
  2. 2
      src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id
  3. 39
      src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs
  4. 10
      src/ImageProcessorCore/Image/Image.cs
  5. 78
      src/ImageProcessorCore/Profiles/Exif/ExifDataType.cs
  6. 42
      src/ImageProcessorCore/Profiles/Exif/ExifParts.cs
  7. 227
      src/ImageProcessorCore/Profiles/Exif/ExifProfile.cs
  8. 426
      src/ImageProcessorCore/Profiles/Exif/ExifReader.cs
  9. 927
      src/ImageProcessorCore/Profiles/Exif/ExifTag.cs
  10. 595
      src/ImageProcessorCore/Profiles/Exif/ExifValue.cs
  11. 407
      src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs
  12. 3
      src/ImageProcessorCore/Profiles/Exif/README.md
  13. 30
      tests/ImageProcessorCore.Tests/FileTestBase.cs
  14. 38
      tests/ImageProcessorCore.Tests/Helpers/GuardTests.cs
  15. 306
      tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs
  16. 50
      tests/ImageProcessorCore.Tests/Profiles/Exif/ExifValueTests.cs
  17. 52
      tests/ImageProcessorCore.Tests/TestImages.cs

58
src/ImageProcessorCore/Common/Helpers/Guard.cs

@ -188,5 +188,63 @@ namespace ImageProcessorCore
$"Value must be greater than or equal to {min} and less than or equal to {max}.");
}
}
/// <summary>
/// Verifies, that the method parameter with specified target value is true
/// and throws an exception if it is found to be so.
/// </summary>
/// <param name="target">
/// The target value, which cannot be false.
/// </param>
/// <param name="parameterName">
/// The name of the parameter that is to be checked.
/// </param>
/// <param name="message">
/// The error message, if any to add to the exception.
/// </param>
/// <exception cref="ArgumentException">
/// <paramref name="target"/> is null
/// </exception>
public static void IsTrue(bool target, string parameterName, string message = "")
{
if (!target)
{
if (string.IsNullOrWhiteSpace(message))
{
throw new ArgumentException(parameterName, message);
}
throw new ArgumentException(parameterName);
}
}
/// <summary>
/// Verifies, that the method parameter with specified target value is false
/// and throws an exception if it is found to be so.
/// </summary>
/// <param name="target">
/// The target value, which cannot be true.
/// </param>
/// <param name="parameterName">
/// The name of the parameter that is to be checked.
/// </param>
/// <param name="message">
/// The error message, if any to add to the exception.
/// </param>
/// <exception cref="ArgumentException">
/// <paramref name="target"/> is null
/// </exception>
public static void IsFalse(bool target, string parameterName, string message = "")
{
if (target)
{
if (string.IsNullOrWhiteSpace(message))
{
throw new ArgumentException(parameterName, message);
}
throw new ArgumentException(parameterName);
}
}
}
}

2
src/ImageProcessorCore/Formats/Jpg/JpegDecoderCore.cs.REMOVED.git-id

@ -1 +1 @@
25b2ab2a0acfb874ca8ac8ae979fc6b1bc9bf466
09f4618eaade2900f7174b9ab400deb0a25bd813

39
src/ImageProcessorCore/Formats/Jpg/JpegEncoderCore.cs

@ -488,10 +488,9 @@ namespace ImageProcessorCore.Formats
int componentCount = 3;
// Write the Start Of Image marker.
double densityX = image.HorizontalResolution;
double densityY = image.VerticalResolution;
WriteApplicationHeader((short)image.HorizontalResolution, (short)image.VerticalResolution);
WriteApplicationHeader((short)densityX, (short)densityY);
WriteProfiles(image);
// Write the quantization tables.
this.WriteDQT();
@ -571,6 +570,40 @@ namespace ImageProcessorCore.Formats
this.outputStream.Write(this.buffer, 0, 4);
}
private void WriteProfiles<T, TP>(Image<T, TP> image)
where T : IPackedVector<TP>
where TP : struct
{
WriteProfile(image.ExifProfile);
}
private void WriteProfile(ExifProfile exifProfile)
{
if (exifProfile == null)
{
return;
}
byte[] data = exifProfile.ToByteArray();
if (data == null || data.Length == 0)
{
return;
}
if (data.Length > 65533)
{
throw new ImageFormatException("Exif profile size exceeds limit.");
}
this.buffer[0] = JpegConstants.Markers.XFF;
this.buffer[1] = JpegConstants.Markers.APP1; // Application Marker
this.buffer[2] = (byte)((data.Length >> 8) & 0xFF);
this.buffer[3] = (byte)(data.Length & 0xFF);
this.outputStream.Write(this.buffer, 0, 4);
this.outputStream.Write(data, 0, data.Length);
}
/// <summary>
/// Writes the Define Quantization Marker and tables.
/// </summary>

10
src/ImageProcessorCore/Image/Image.cs

@ -91,6 +91,11 @@ namespace ImageProcessorCore
this.HorizontalResolution = other.HorizontalResolution;
this.VerticalResolution = other.VerticalResolution;
this.CurrentImageFormat = other.CurrentImageFormat;
if (other.ExifProfile != null)
{
this.ExifProfile = new ExifProfile(other.ExifProfile);
}
}
/// <summary>
@ -185,6 +190,11 @@ namespace ImageProcessorCore
/// </summary>
public IImageFormat CurrentImageFormat { get; internal set; }
/// <summary>
/// Gets or sets the Exif profile.
/// </summary>
public ExifProfile ExifProfile { get; set; }
/// <inheritdoc/>
public override IPixelAccessor<T, TP> Lock()
{

78
src/ImageProcessorCore/Profiles/Exif/ExifDataType.cs

@ -0,0 +1,78 @@
// <copyright file="ExifDataType.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
///<summary>
/// Specifies exif data types.
///</summary>
public enum ExifDataType
{
/// <summary>
/// Unknown
/// </summary>
Unknown,
/// <summary>
/// Byte
/// </summary>
Byte,
/// <summary>
/// Ascii
/// </summary>
Ascii,
/// <summary>
/// Short
/// </summary>
Short,
/// <summary>
/// Long
/// </summary>
Long,
/// <summary>
/// Rational
/// </summary>
Rational,
/// <summary>
/// SignedByte
/// </summary>
SignedByte,
/// <summary>
/// Undefined
/// </summary>
Undefined,
/// <summary>
/// SignedShort
/// </summary>
SignedShort,
/// <summary>
/// SignedLong
/// </summary>
SignedLong,
/// <summary>
/// SignedRational
/// </summary>
SignedRational,
/// <summary>
/// SingleFloat
/// </summary>
SingleFloat,
/// <summary>
/// DoubleFloat
/// </summary>
DoubleFloat
}
}

42
src/ImageProcessorCore/Profiles/Exif/ExifParts.cs

@ -0,0 +1,42 @@
// <copyright file="ExifParts.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using System;
///<summary>
/// Specifies which parts will be written when the profile is added to an image.
///</summary>
[Flags]
public enum ExifParts
{
/// <summary>
/// None
/// </summary>
None = 0,
/// <summary>
/// IfdTags
/// </summary>
IfdTags = 1,
/// <summary>
/// ExifTags
/// </summary>
ExifTags = 4,
/// <summary>
/// GPSTags
/// </summary>
GPSTags = 8,
/// <summary>
/// All
/// </summary>
All = IfdTags | ExifTags | GPSTags
}
}

227
src/ImageProcessorCore/Profiles/Exif/ExifProfile.cs

@ -0,0 +1,227 @@
// <copyright file="ExifProfile.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
/// <summary>
/// Class that can be used to access an Exif profile.
/// </summary>
public sealed class ExifProfile
{
private byte[] data;
private Collection<ExifValue> values;
private List<ExifTag> invalidTags;
private int thumbnailOffset;
private int thumbnailLength;
///<summary>
/// Initializes a new instance of the <see cref="ExifProfile"/> class.
///</summary>
///<param name="data">The byte array to read the exif profile from.</param>
public ExifProfile()
: this((byte[])null)
{
}
///<summary>
/// Initializes a new instance of the <see cref="ExifProfile"/> class.
///</summary>
///<param name="data">The byte array to read the exif profile from.</param>
public ExifProfile(byte[] data)
{
Parts = ExifParts.All;
BestPrecision = false;
this.data = data;
this.invalidTags = new List<ExifTag>();
}
/// <summary>
/// Initializes a new instance of the <see cref="ExifProfile"/> class
/// by making a copy from another exif profile.
/// </summary>
/// <param name="other">The other exif profile, where the clone should be made from.</param>
/// <exception cref="ArgumentNullException"><paramref name="other"/> is null.</exception>
public ExifProfile(ExifProfile other)
{
Guard.NotNull(other, nameof(other));
Parts = other.Parts;
BestPrecision = other.BestPrecision;
this.thumbnailLength = other.thumbnailLength;
this.thumbnailOffset = other.thumbnailOffset;
this.invalidTags = new List<ExifTag>(other.invalidTags);
if (other.values != null)
{
this.values = new Collection<ExifValue>();
foreach(ExifValue value in other.values)
{
this.values.Add(new ExifValue(value));
}
}
else
{
this.data = other.data;
}
}
///<summary>
/// Specifies if rationals should be stored with the best precision possible. This is disabled
/// by default, setting this to true will have an impact on the performance.
///</summary>
public bool BestPrecision
{
get;
set;
}
///<summary>
/// Specifies which parts will be written when the profile is added to an image.
///</summary>
public ExifParts Parts
{
get;
set;
}
///<summary>
/// Returns the tags that where found but contained an invalid value.
///</summary>
public IEnumerable<ExifTag> InvalidTags
{
get
{
return this.invalidTags;
}
}
///<summary>
/// Returns the values of this exif profile.
///</summary>
public IEnumerable<ExifValue> Values
{
get
{
InitializeValues();
return this.values;
}
}
///<summary>
/// Returns the thumbnail in the exif profile when available.
///</summary>
/// <typeparam name="T">The pixel format.</typeparam>
/// <typeparam name="TP">The packed format. <example>long, float.</example></typeparam>
public Image<T, TP> CreateThumbnail<T, TP>()
where T : IPackedVector<TP>
where TP : struct
{
InitializeValues();
if (this.thumbnailOffset == 0 || this.thumbnailLength == 0)
return null;
if (this.data.Length < (this.thumbnailOffset + this.thumbnailLength))
return null;
using (MemoryStream memStream = new MemoryStream(this.data, this.thumbnailOffset, this.thumbnailLength))
{
return new Image<T, TP>(memStream);
}
}
///<summary>
/// Returns the value with the specified tag.
///</summary>
///<param name="tag">The tag of the exif value.</param>
public ExifValue GetValue(ExifTag tag)
{
foreach (ExifValue exifValue in Values)
{
if (exifValue.Tag == tag)
return exifValue;
}
return null;
}
///<summary>
/// Removes the value with the specified tag.
///</summary>
///<param name="tag">The tag of the exif value.</param>
public bool RemoveValue(ExifTag tag)
{
InitializeValues();
for (int i = 0; i < this.values.Count; i++)
{
if (this.values[i].Tag == tag)
{
this.values.RemoveAt(i);
return true;
}
}
return false;
}
///<summary>
/// Sets the value of the specified tag.
///</summary>
///<param name="tag">The tag of the exif value.</param>
///<param name="value">The value.</param>
public void SetValue(ExifTag tag, object value)
{
foreach (ExifValue exifValue in Values)
{
if (exifValue.Tag == tag)
{
exifValue.Value = value;
return;
}
}
ExifValue newExifValue = ExifValue.Create(tag, value);
this.values.Add(newExifValue);
}
///<summary>
/// Converts this instance to a byte array.
///</summary>
public byte[] ToByteArray()
{
if (this.values == null)
return data;
if (this.values.Count == 0)
return null;
ExifWriter writer = new ExifWriter(this.values, Parts, BestPrecision);
return writer.GetData();
}
private void InitializeValues()
{
if (this.values != null)
return;
if (this.data == null)
{
this.values = new Collection<ExifValue>();
return;
}
ExifReader reader = new ExifReader();
this.values = reader.Read(this.data);
this.invalidTags = new List<ExifTag>(reader.InvalidTags);
this.thumbnailOffset = (int)reader.ThumbnailOffset;
this.thumbnailLength = (int)reader.ThumbnailLength;
}
}
}

426
src/ImageProcessorCore/Profiles/Exif/ExifReader.cs

@ -0,0 +1,426 @@
// <copyright file="ExifReader.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
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 bool isLittleEndian;
private uint exifOffset;
private uint gpsOffset;
private uint startIndex;
public uint ThumbnailLength
{
get;
private set;
}
public uint ThumbnailOffset
{
get;
private set;
}
private int RemainingLength
{
get
{
if (this.index >= this.data.Length)
return 0;
return this.data.Length - (int)this.index;
}
}
public Collection<ExifValue> Read(byte[] data)
{
Collection<ExifValue> result = new Collection<ExifValue>();
this.data = data;
if (GetString(4) == "Exif")
{
if (GetShort() != 0)
return result;
this.startIndex = 6;
}
else
{
this.index = 0;
}
this.isLittleEndian = GetString(2) == "II";
if (GetShort() != 0x002A)
return result;
uint ifdOffset = GetLong();
AddValues(result, ifdOffset);
uint thumbnailOffset = GetLong();
GetThumbnail(thumbnailOffset);
if (this.exifOffset != 0)
AddValues(result, this.exifOffset);
if (this.gpsOffset != 0)
AddValues(result, this.gpsOffset);
return result;
}
public IEnumerable<ExifTag> InvalidTags
{
get
{
return this.invalidTags;
}
}
private void AddValues(Collection<ExifValue> values, uint index)
{
this.index = 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)
{
if (val.Tag == value.Tag)
{
duplicate = true;
break;
}
}
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)
{
case ExifDataType.Unknown:
return null;
case ExifDataType.Ascii:
return ToString(data);
case ExifDataType.Byte:
if (numberOfComponents == 1)
return ToByte(data);
else
return data;
case ExifDataType.DoubleFloat:
if (numberOfComponents == 1)
return ToDouble(data);
else
return ToArray(dataType, data, ToDouble);
case ExifDataType.Long:
if (numberOfComponents == 1)
return ToLong(data);
else
return ToArray(dataType, data, ToLong);
case ExifDataType.Rational:
if (numberOfComponents == 1)
return ToRational(data);
else
return ToArray(dataType, data, ToRational);
case ExifDataType.Short:
if (numberOfComponents == 1)
return ToShort(data);
else
return ToArray(dataType, data, ToShort);
case ExifDataType.SignedByte:
if (numberOfComponents == 1)
return ToSignedByte(data);
else
return ToArray(dataType, data, ToSignedByte);
case ExifDataType.SignedLong:
if (numberOfComponents == 1)
return ToSignedLong(data);
else
return ToArray(dataType, data, ToSignedLong);
case ExifDataType.SignedRational:
if (numberOfComponents == 1)
return ToSignedRational(data);
else
return ToArray(dataType, data, ToSignedRational);
case ExifDataType.SignedShort:
if (numberOfComponents == 1)
return ToSignedShort(data);
else
return ToArray(dataType, data, ToSignedShort);
case ExifDataType.SingleFloat:
if (numberOfComponents == 1)
return ToSingle(data);
else
return ToArray(dataType, data, ToSingle);
case ExifDataType.Undefined:
if (numberOfComponents == 1)
return ToByte(data);
else
return data;
default:
throw new NotImplementedException();
}
}
private ExifValue CreateValue()
{
if (RemainingLength < 12)
return null;
ExifTag tag = ToEnum(GetShort(), ExifTag.Unknown);
ExifDataType dataType = ToEnum(GetShort(), ExifDataType.Unknown);
object value = null;
if (dataType == ExifDataType.Unknown)
return new ExifValue(tag, dataType, value, false);
uint numberOfComponents = (uint)GetLong();
uint size = numberOfComponents * ExifValue.GetSize(dataType);
byte[] data = GetBytes(4);
if (size > 4)
{
uint oldIndex = this.index;
this.index = ToLong(data) + this.startIndex;
if (RemainingLength < size)
{
this.invalidTags.Add(tag);
this.index = oldIndex;
return null;
}
value = ConvertValue(dataType, GetBytes(size), numberOfComponents);
this.index = oldIndex;
}
else
{
value = ConvertValue(dataType, data, numberOfComponents);
}
bool isArray = value != null && numberOfComponents > 1;
return new ExifValue(tag, dataType, value, isArray);
}
private TEnum ToEnum<TEnum>(int value, TEnum defaultValue)
where TEnum : struct
{
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)
return null;
byte[] data = new byte[length];
Array.Copy(this.data, (int)this.index, data, 0, (int)length);
this.index += length;
return data;
}
private uint GetLong()
{
return ToLong(GetBytes(4));
}
private ushort GetShort()
{
return ToShort(GetBytes(2));
}
private string GetString(uint length)
{
return ToString(GetBytes(length));
}
private void GetThumbnail(uint offset)
{
Collection<ExifValue> values = new Collection<ExifValue>();
AddValues(values, offset);
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;
}
}
private static TDataType[] ToArray<TDataType>(ExifDataType dataType, Byte[] data,
ConverterMethod<TDataType> converter)
{
int dataTypeSize = (int)ExifValue.GetSize(dataType);
int length = data.Length / dataTypeSize;
TDataType[] result = new TDataType[length];
byte[] buffer = new byte[dataTypeSize];
for (int i = 0; i < length; i++)
{
Array.Copy(data, i * dataTypeSize, buffer, 0, dataTypeSize);
result.SetValue(converter(buffer), i);
}
return result;
}
private static byte ToByte(byte[] data)
{
return data[0];
}
private double ToDouble(byte[] data)
{
if (!ValidateArray(data, 8))
return default(double);
return BitConverter.ToDouble(data, 0);
}
private uint ToLong(byte[] data)
{
if (!ValidateArray(data, 4))
return default(uint);
return BitConverter.ToUInt32(data, 0);
}
private ushort ToShort(byte[] data)
{
if (!ValidateArray(data, 2))
return default(ushort);
return BitConverter.ToUInt16(data, 0);
}
private float ToSingle(byte[] data)
{
if (!ValidateArray(data, 4))
return default(float);
return BitConverter.ToSingle(data, 0);
}
private static string ToString(byte[] data)
{
string result = Encoding.UTF8.GetString(data, 0, data.Length);
int nullCharIndex = result.IndexOf('\0');
if (nullCharIndex != -1)
result = result.Substring(0, nullCharIndex);
return result;
}
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);
return numerator / (double)denominator;
}
private sbyte ToSignedByte(byte[] data)
{
return unchecked((sbyte)data[0]);
}
private int ToSignedLong(byte[] data)
{
if (!ValidateArray(data, 4))
return default(int);
return BitConverter.ToInt32(data, 0);
}
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);
return numerator / (double)denominator;
}
private short ToSignedShort(byte[] data)
{
if (!ValidateArray(data, 2))
return default(short);
return BitConverter.ToInt16(data, 0);
}
private bool ValidateArray(byte[] data, int size)
{
return ValidateArray(data, size, size);
}
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)
{
Array.Reverse(data, i, stepSize);
}
return true;
}
}
}

927
src/ImageProcessorCore/Profiles/Exif/ExifTag.cs

@ -0,0 +1,927 @@
// <copyright file="ExifTag.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
/// <summary>
/// All exif tags from the Exif standard 2.2
/// </summary>
public enum ExifTag
{
/// <summary>
/// Unknown
/// </summary>
Unknown = 0xFFFF,
/// <summary>
/// SubIFDOffset
/// </summary>
SubIFDOffset = 0x8769,
/// <summary>
/// GPSIFDOffset
/// </summary>
GPSIFDOffset = 0x8825,
/// <summary>
/// ImageWidth
/// </summary>
ImageWidth = 0x0100,
/// <summary>
/// ImageLength
/// </summary>
ImageLength = 0x0101,
/// <summary>
/// BitsPerSample
/// </summary>
BitsPerSample = 0x0102,
/// <summary>
/// Compression
/// </summary>
Compression = 0x0103,
/// <summary>
/// PhotometricInterpretation
/// </summary>
PhotometricInterpretation = 0x0106,
/// <summary>
/// Threshholding
/// </summary>
Threshholding = 0x0107,
/// <summary>
/// CellWidth
/// </summary>
CellWidth = 0x0108,
/// <summary>
/// CellLength
/// </summary>
CellLength = 0x0109,
/// <summary>
/// FillOrder
/// </summary>
FillOrder = 0x010A,
/// <summary>
/// ImageDescription
/// </summary>
ImageDescription = 0x010E,
/// <summary>
/// Make
/// </summary>
Make = 0x010F,
/// <summary>
/// Model
/// </summary>
Model = 0x0110,
/// <summary>
/// StripOffsets
/// </summary>
StripOffsets = 0x0111,
/// <summary>
/// Orientation
/// </summary>
Orientation = 0x0112,
/// <summary>
/// SamplesPerPixel
/// </summary>
SamplesPerPixel = 0x0115,
/// <summary>
/// RowsPerStrip
/// </summary>
RowsPerStrip = 0x0116,
/// <summary>
/// StripByteCounts
/// </summary>
StripByteCounts = 0x0117,
/// <summary>
/// MinSampleValue
/// </summary>
MinSampleValue = 0x0118,
/// <summary>
/// MaxSampleValue
/// </summary>
MaxSampleValue = 0x0119,
/// <summary>
/// XResolution
/// </summary>
XResolution = 0x011A,
/// <summary>
/// YResolution
/// </summary>
YResolution = 0x011B,
/// <summary>
/// PlanarConfiguration
/// </summary>
PlanarConfiguration = 0x011C,
/// <summary>
/// FreeOffsets
/// </summary>
FreeOffsets = 0x0120,
/// <summary>
/// FreeByteCounts
/// </summary>
FreeByteCounts = 0x0121,
/// <summary>
/// GrayResponseUnit
/// </summary>
GrayResponseUnit = 0x0122,
/// <summary>
/// GrayResponseCurve
/// </summary>
GrayResponseCurve = 0x0123,
/// <summary>
/// ResolutionUnit
/// </summary>
ResolutionUnit = 0x0128,
/// <summary>
/// Software
/// </summary>
Software = 0x0131,
/// <summary>
/// DateTime
/// </summary>
DateTime = 0x0132,
/// <summary>
/// Artist
/// </summary>
Artist = 0x013B,
/// <summary>
/// HostComputer
/// </summary>
HostComputer = 0x013C,
/// <summary>
/// ColorMap
/// </summary>
ColorMap = 0x0140,
/// <summary>
/// ExtraSamples
/// </summary>
ExtraSamples = 0x0152,
/// <summary>
/// Copyright
/// </summary>
Copyright = 0x8298,
/// <summary>
/// DocumentName
/// </summary>
DocumentName = 0x010D,
/// <summary>
/// PageName
/// </summary>
PageName = 0x011D,
/// <summary>
/// XPosition
/// </summary>
XPosition = 0x011E,
/// <summary>
/// YPosition
/// </summary>
YPosition = 0x011F,
/// <summary>
/// T4Options
/// </summary>
T4Options = 0x0124,
/// <summary>
/// T6Options
/// </summary>
T6Options = 0x0125,
/// <summary>
/// PageNumber
/// </summary>
PageNumber = 0x0129,
/// <summary>
/// TransferFunction
/// </summary>
TransferFunction = 0x012D,
/// <summary>
/// Predictor
/// </summary>
Predictor = 0x013D,
/// <summary>
/// WhitePoint
/// </summary>
WhitePoint = 0x013E,
/// <summary>
/// PrimaryChromaticities
/// </summary>
PrimaryChromaticities = 0x013F,
/// <summary>
/// HalftoneHints
/// </summary>
HalftoneHints = 0x0141,
/// <summary>
/// TileWidth
/// </summary>
TileWidth = 0x0142,
/// <summary>
/// TileLength
/// </summary>
TileLength = 0x0143,
/// <summary>
/// TileOffsets
/// </summary>
TileOffsets = 0x0144,
/// <summary>
/// TileByteCounts
/// </summary>
TileByteCounts = 0x0145,
/// <summary>
/// BadFaxLines
/// </summary>
BadFaxLines = 0x0146,
/// <summary>
/// CleanFaxData
/// </summary>
CleanFaxData = 0x0147,
/// <summary>
/// ConsecutiveBadFaxLines
/// </summary>
ConsecutiveBadFaxLines = 0x0148,
/// <summary>
/// InkSet
/// </summary>
InkSet = 0x014C,
/// <summary>
/// InkNames
/// </summary>
InkNames = 0x014D,
/// <summary>
/// NumberOfInks
/// </summary>
NumberOfInks = 0x014E,
/// <summary>
/// DotRange
/// </summary>
DotRange = 0x0150,
/// <summary>
/// TargetPrinter
/// </summary>
TargetPrinter = 0x0151,
/// <summary>
/// SampleFormat
/// </summary>
SampleFormat = 0x0153,
/// <summary>
/// SMinSampleValue
/// </summary>
SMinSampleValue = 0x0154,
/// <summary>
/// SMaxSampleValue
/// </summary>
SMaxSampleValue = 0x0155,
/// <summary>
/// TransferRange
/// </summary>
TransferRange = 0x0156,
/// <summary>
/// ClipPath
/// </summary>
ClipPath = 0x0157,
/// <summary>
/// XClipPathUnits
/// </summary>
XClipPathUnits = 0x0158,
/// <summary>
/// YClipPathUnits
/// </summary>
YClipPathUnits = 0x0159,
/// <summary>
/// Indexed
/// </summary>
Indexed = 0x015A,
/// <summary>
/// JPEGTables
/// </summary>
JPEGTables = 0x015B,
/// <summary>
/// OPIProxy
/// </summary>
OPIProxy = 0x015F,
/// <summary>
/// ProfileType
/// </summary>
ProfileType = 0x0191,
/// <summary>
/// FaxProfile
/// </summary>
FaxProfile = 0x0192,
/// <summary>
/// CodingMethods
/// </summary>
CodingMethods = 0x0193,
/// <summary>
/// VersionYear
/// </summary>
VersionYear = 0x0194,
/// <summary>
/// ModeNumber
/// </summary>
ModeNumber = 0x0195,
/// <summary>
/// Decode
/// </summary>
Decode = 0x01B1,
/// <summary>
/// DefaultImageColor
/// </summary>
DefaultImageColor = 0x01B2,
/// <summary>
/// JPEGProc
/// </summary>
JPEGProc = 0x0200,
/// <summary>
/// JPEGInterchangeFormat
/// </summary>
JPEGInterchangeFormat = 0x0201,
/// <summary>
/// JPEGInterchangeFormatLength
/// </summary>
JPEGInterchangeFormatLength = 0x0202,
/// <summary>
/// JPEGRestartInterval
/// </summary>
JPEGRestartInterval = 0x0203,
/// <summary>
/// JPEGLosslessPredictors
/// </summary>
JPEGLosslessPredictors = 0x0205,
/// <summary>
/// JPEGPointTransforms
/// </summary>
JPEGPointTransforms = 0x0206,
/// <summary>
/// JPEGQTables
/// </summary>
JPEGQTables = 0x0207,
/// <summary>
/// JPEGDCTables
/// </summary>
JPEGDCTables = 0x0208,
/// <summary>
/// JPEGACTables
/// </summary>
JPEGACTables = 0x0209,
/// <summary>
/// YCbCrCoefficients
/// </summary>
YCbCrCoefficients = 0x0211,
/// <summary>
/// YCbCrSubsampling
/// </summary>
YCbCrSubsampling = 0x0212,
/// <summary>
/// YCbCrPositioning
/// </summary>
YCbCrPositioning = 0x0213,
/// <summary>
/// ReferenceBlackWhite
/// </summary>
ReferenceBlackWhite = 0x0214,
/// <summary>
/// StripRowCounts
/// </summary>
StripRowCounts = 0x022F,
/// <summary>
/// XMP
/// </summary>
XMP = 0x02BC,
/// <summary>
/// ImageID
/// </summary>
ImageID = 0x800D,
/// <summary>
/// ImageLayer
/// </summary>
ImageLayer = 0x87AC,
/// <summary>
/// ExposureTime
/// </summary>
ExposureTime = 0x829A,
/// <summary>
/// FNumber
/// </summary>
FNumber = 0x829D,
/// <summary>
/// ExposureProgram
/// </summary>
ExposureProgram = 0x8822,
/// <summary>
/// SpectralSensitivity
/// </summary>
SpectralSensitivity = 0x8824,
/// <summary>
/// ISOSpeedRatings
/// </summary>
ISOSpeedRatings = 0x8827,
/// <summary>
/// OECF
/// </summary>
OECF = 0x8828,
/// <summary>
/// ExifVersion
/// </summary>
ExifVersion = 0x9000,
/// <summary>
/// DateTimeOriginal
/// </summary>
DateTimeOriginal = 0x9003,
/// <summary>
/// DateTimeDigitized
/// </summary>
DateTimeDigitized = 0x9004,
/// <summary>
/// ComponentsConfiguration
/// </summary>
ComponentsConfiguration = 0x9101,
/// <summary>
/// CompressedBitsPerPixel
/// </summary>
CompressedBitsPerPixel = 0x9102,
/// <summary>
/// ShutterSpeedValue
/// </summary>
ShutterSpeedValue = 0x9201,
/// <summary>
/// ApertureValue
/// </summary>
ApertureValue = 0x9202,
/// <summary>
/// BrightnessValue
/// </summary>
BrightnessValue = 0x9203,
/// <summary>
/// ExposureBiasValue
/// </summary>
ExposureBiasValue = 0x9204,
/// <summary>
/// MaxApertureValue
/// </summary>
MaxApertureValue = 0x9205,
/// <summary>
/// SubjectDistance
/// </summary>
SubjectDistance = 0x9206,
/// <summary>
/// MeteringMode
/// </summary>
MeteringMode = 0x9207,
/// <summary>
/// LightSource
/// </summary>
LightSource = 0x9208,
/// <summary>
/// Flash
/// </summary>
Flash = 0x9209,
/// <summary>
/// FocalLength
/// </summary>
FocalLength = 0x920A,
/// <summary>
/// SubjectArea
/// </summary>
SubjectArea = 0x9214,
/// <summary>
/// MakerNote
/// </summary>
MakerNote = 0x927C,
/// <summary>
/// UserComment
/// </summary>
UserComment = 0x9286,
/// <summary>
/// SubsecTime
/// </summary>
SubsecTime = 0x9290,
/// <summary>
/// SubsecTimeOriginal
/// </summary>
SubsecTimeOriginal = 0x9291,
/// <summary>
/// SubsecTimeDigitized
/// </summary>
SubsecTimeDigitized = 0x9292,
/// <summary>
/// FlashpixVersion
/// </summary>
FlashpixVersion = 0xA000,
/// <summary>
/// ColorSpace
/// </summary>
ColorSpace = 0xA001,
/// <summary>
/// PixelXDimension
/// </summary>
PixelXDimension = 0xA002,
/// <summary>
/// PixelYDimension
/// </summary>
PixelYDimension = 0xA003,
/// <summary>
/// RelatedSoundFile
/// </summary>
RelatedSoundFile = 0xA004,
/// <summary>
/// FlashEnergy
/// </summary>
FlashEnergy = 0xA20B,
/// <summary>
/// SpatialFrequencyResponse
/// </summary>
SpatialFrequencyResponse = 0xA20C,
/// <summary>
/// FocalPlaneXResolution
/// </summary>
FocalPlaneXResolution = 0xA20E,
/// <summary>
/// FocalPlaneYResolution
/// </summary>
FocalPlaneYResolution = 0xA20F,
/// <summary>
/// FocalPlaneResolutionUnit
/// </summary>
FocalPlaneResolutionUnit = 0xA210,
/// <summary>
/// SubjectLocation
/// </summary>
SubjectLocation = 0xA214,
/// <summary>
/// ExposureIndex
/// </summary>
ExposureIndex = 0xA215,
/// <summary>
/// SensingMethod
/// </summary>
SensingMethod = 0xA217,
/// <summary>
/// FileSource
/// </summary>
FileSource = 0xA300,
/// <summary>
/// SceneType
/// </summary>
SceneType = 0xA301,
/// <summary>
/// CFAPattern
/// </summary>
CFAPattern = 0xA302,
/// <summary>
/// CustomRendered
/// </summary>
CustomRendered = 0xA401,
/// <summary>
/// ExposureMode
/// </summary>
ExposureMode = 0xA402,
/// <summary>
/// WhiteBalance
/// </summary>
WhiteBalance = 0xA403,
/// <summary>
/// DigitalZoomRatio
/// </summary>
DigitalZoomRatio = 0xA404,
/// <summary>
/// FocalLengthIn35mmFilm
/// </summary>
FocalLengthIn35mmFilm = 0xA405,
/// <summary>
/// SceneCaptureType
/// </summary>
SceneCaptureType = 0xA406,
/// <summary>
/// GainControl
/// </summary>
GainControl = 0xA407,
/// <summary>
/// Contrast
/// </summary>
Contrast = 0xA408,
/// <summary>
/// Saturation
/// </summary>
Saturation = 0xA409,
/// <summary>
/// Sharpness
/// </summary>
Sharpness = 0xA40A,
/// <summary>
/// DeviceSettingDescription
/// </summary>
DeviceSettingDescription = 0xA40B,
/// <summary>
/// SubjectDistanceRange
/// </summary>
SubjectDistanceRange = 0xA40C,
/// <summary>
/// ImageUniqueID
/// </summary>
ImageUniqueID = 0xA420,
/// <summary>
/// GPSVersionID
/// </summary>
GPSVersionID = 0x0000,
/// <summary>
/// GPSLatitudeRef
/// </summary>
GPSLatitudeRef = 0x0001,
/// <summary>
/// GPSLatitude
/// </summary>
GPSLatitude = 0x0002,
/// <summary>
/// GPSLongitudeRef
/// </summary>
GPSLongitudeRef = 0x0003,
/// <summary>
/// GPSLongitude
/// </summary>
GPSLongitude = 0x0004,
/// <summary>
/// GPSAltitudeRef
/// </summary>
GPSAltitudeRef = 0x0005,
/// <summary>
/// GPSAltitude
/// </summary>
GPSAltitude = 0x0006,
/// <summary>
/// GPSTimestamp
/// </summary>
GPSTimestamp = 0x0007,
/// <summary>
/// GPSSatellites
/// </summary>
GPSSatellites = 0x0008,
/// <summary>
/// GPSStatus
/// </summary>
GPSStatus = 0x0009,
/// <summary>
/// GPSMeasureMode
/// </summary>
GPSMeasureMode = 0x000A,
/// <summary>
/// GPSDOP
/// </summary>
GPSDOP = 0x000B,
/// <summary>
/// GPSSpeedRef
/// </summary>
GPSSpeedRef = 0x000C,
/// <summary>
/// GPSSpeed
/// </summary>
GPSSpeed = 0x000D,
/// <summary>
/// GPSTrackRef
/// </summary>
GPSTrackRef = 0x000E,
/// <summary>
/// GPSTrack
/// </summary>
GPSTrack = 0x000F,
/// <summary>
/// GPSImgDirectionRef
/// </summary>
GPSImgDirectionRef = 0x0010,
/// <summary>
/// GPSImgDirection
/// </summary>
GPSImgDirection = 0x0011,
/// <summary>
/// GPSMapDatum
/// </summary>
GPSMapDatum = 0x0012,
/// <summary>
/// GPSDestLatitudeRef
/// </summary>
GPSDestLatitudeRef = 0x0013,
/// <summary>
/// GPSDestLatitude
/// </summary>
GPSDestLatitude = 0x0014,
/// <summary>
/// GPSDestLongitudeRef
/// </summary>
GPSDestLongitudeRef = 0x0015,
/// <summary>
/// GPSDestLongitude
/// </summary>
GPSDestLongitude = 0x0016,
/// <summary>
/// GPSDestBearingRef
/// </summary>
GPSDestBearingRef = 0x0017,
/// <summary>
/// GPSDestBearing
/// </summary>
GPSDestBearing = 0x0018,
/// <summary>
/// GPSDestDistanceRef
/// </summary>
GPSDestDistanceRef = 0x0019,
/// <summary>
/// GPSDestDistance
/// </summary>
GPSDestDistance = 0x001A,
/// <summary>
/// GPSProcessingMethod
/// </summary>
GPSProcessingMethod = 0x001B,
/// <summary>
/// GPSAreaInformation
/// </summary>
GPSAreaInformation = 0x001C,
/// <summary>
/// GPSDateStamp
/// </summary>
GPSDateStamp = 0x001D,
/// <summary>
/// GPSDifferential
/// </summary>
GPSDifferential = 0x001E
}
}

595
src/ImageProcessorCore/Profiles/Exif/ExifValue.cs

@ -0,0 +1,595 @@
// <copyright file="ExifValue.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using System;
using System.Globalization;
using System.Text;
/// <summary>
/// A value of the exif profile.
/// </summary>
public sealed class ExifValue : IEquatable<ExifValue>
{
private object value;
/// <summary>
/// Initializes a new instance of the <see cref="ExifValue"/> class
/// by making a copy from another exif value.
/// </summary>
/// <param name="other">The other exif value, where the clone should be made from.</param>
/// <exception cref="ArgumentNullException"><paramref name="other"/> is null.</exception>
public ExifValue(ExifValue other)
{
Guard.NotNull(other, nameof(other));
DataType = other.DataType;
IsArray = other.IsArray;
Tag = other.Tag;
if (!other.IsArray)
{
value = other.value;
}
else
{
Array array = (Array)other.value;
value = array.Clone();
}
}
/// <summary>
/// The data type of the exif value.
/// </summary>
public ExifDataType DataType
{
get;
private set;
}
/// <summary>
/// Returns true if the value is an array.
/// </summary>
public bool IsArray
{
get;
private set;
}
/// <summary>
/// The tag of the exif value.
/// </summary>
public ExifTag Tag
{
get;
private set;
}
/// <summary>
/// The value.
/// </summary>
public object Value
{
get
{
return this.value;
}
set
{
CheckValue(value);
this.value = value;
}
}
/// <summary>
/// Determines whether the specified ExifValue instances are considered equal.
/// </summary>
/// <param name="left">The first ExifValue to compare.</param>
/// <param name="right"> The second ExifValue to compare.</param>
/// <returns></returns>
public static bool operator ==(ExifValue left, ExifValue right)
{
return Equals(left, right);
}
/// <summary>
/// Determines whether the specified ExifValue instances are not considered equal.
/// </summary>
/// <param name="left">The first ExifValue to compare.</param>
/// <param name="right"> The second ExifValue to compare.</param>
/// <returns></returns>
public static bool operator !=(ExifValue left, ExifValue right)
{
return !Equals(left, right);
}
///<summary>
/// Determines whether the specified object is equal to the current exif value.
///</summary>
///<param name="obj">The object to compare this exif value with.</param>
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
return true;
return Equals(obj as ExifValue);
}
///<summary>
/// Determines whether the specified exif value is equal to the current exif value.
///</summary>
///<param name="other">The exif value to compare this exif value with.</param>
public bool Equals(ExifValue other)
{
if (ReferenceEquals(other, null))
return false;
if (ReferenceEquals(this, other))
return true;
return
Tag == other.Tag &&
DataType == other.DataType &&
object.Equals(this.value, other.value);
}
///<summary>
/// Serves as a hash of this type.
///</summary>
public override int GetHashCode()
{
int hashCode = Tag.GetHashCode() ^ DataType.GetHashCode();
return this.value != null ? hashCode ^ this.value.GetHashCode() : hashCode;
}
///<summary>
/// Returns a string that represents the current value.
///</summary>
public override string ToString()
{
if (this.value == null)
return null;
if (DataType == ExifDataType.Ascii)
return (string)this.value;
if (!IsArray)
return ToString(this.value);
StringBuilder sb = new StringBuilder();
foreach (object value in (Array)this.value)
{
sb.Append(ToString(value));
sb.Append(" ");
}
return sb.ToString();
}
internal bool HasValue
{
get
{
if (this.value == null)
return false;
if (DataType == ExifDataType.Ascii)
return ((string)this.value).Length > 0;
return true;
}
}
internal int Length
{
get
{
if (this.value == null)
return 4;
int size = (int)(GetSize(DataType) * NumberOfComponents);
return size < 4 ? 4 : size;
}
}
internal int NumberOfComponents
{
get
{
if (DataType == ExifDataType.Ascii)
return Encoding.UTF8.GetBytes((string)this.value).Length;
if (IsArray)
return ((Array)this.value).Length;
return 1;
}
}
internal ExifValue(ExifTag tag, ExifDataType dataType, bool isArray)
{
Tag = tag;
DataType = dataType;
IsArray = isArray;
if (dataType == ExifDataType.Ascii)
IsArray = false;
}
internal ExifValue(ExifTag tag, ExifDataType dataType, object value, bool isArray)
: this(tag, dataType, isArray)
{
this.value = value;
}
internal static ExifValue Create(ExifTag tag, object value)
{
Guard.IsFalse(tag == ExifTag.Unknown, nameof(tag), "Invalid Tag");
ExifValue exifValue = null;
Type type = value != null ? value.GetType() : null;
if (type != null && type.IsArray)
type = type.GetElementType();
switch (tag)
{
case ExifTag.ImageDescription:
case ExifTag.Make:
case ExifTag.Model:
case ExifTag.Software:
case ExifTag.DateTime:
case ExifTag.Artist:
case ExifTag.HostComputer:
case ExifTag.Copyright:
case ExifTag.DocumentName:
case ExifTag.PageName:
case ExifTag.InkNames:
case ExifTag.TargetPrinter:
case ExifTag.ImageID:
case ExifTag.SpectralSensitivity:
case ExifTag.DateTimeOriginal:
case ExifTag.DateTimeDigitized:
case ExifTag.SubsecTime:
case ExifTag.SubsecTimeOriginal:
case ExifTag.SubsecTimeDigitized:
case ExifTag.RelatedSoundFile:
case ExifTag.ImageUniqueID:
case ExifTag.GPSLatitudeRef:
case ExifTag.GPSLongitudeRef:
case ExifTag.GPSSatellites:
case ExifTag.GPSStatus:
case ExifTag.GPSMeasureMode:
case ExifTag.GPSSpeedRef:
case ExifTag.GPSTrackRef:
case ExifTag.GPSImgDirectionRef:
case ExifTag.GPSMapDatum:
case ExifTag.GPSDestLatitudeRef:
case ExifTag.GPSDestLongitudeRef:
case ExifTag.GPSDestBearingRef:
case ExifTag.GPSDestDistanceRef:
case ExifTag.GPSDateStamp:
exifValue = new ExifValue(tag, ExifDataType.Ascii, true);
break;
case ExifTag.ClipPath:
case ExifTag.VersionYear:
case ExifTag.XMP:
case ExifTag.GPSVersionID:
exifValue = new ExifValue(tag, ExifDataType.Byte, true);
break;
case ExifTag.FaxProfile:
case ExifTag.ModeNumber:
case ExifTag.GPSAltitudeRef:
exifValue = new ExifValue(tag, ExifDataType.Byte, false);
break;
case ExifTag.FreeOffsets:
case ExifTag.FreeByteCounts:
case ExifTag.TileOffsets:
case ExifTag.SMinSampleValue:
case ExifTag.SMaxSampleValue:
case ExifTag.JPEGQTables:
case ExifTag.JPEGDCTables:
case ExifTag.JPEGACTables:
case ExifTag.StripRowCounts:
exifValue = new ExifValue(tag, ExifDataType.Long, true);
break;
case ExifTag.SubIFDOffset:
case ExifTag.GPSIFDOffset:
case ExifTag.T4Options:
case ExifTag.T6Options:
case ExifTag.XClipPathUnits:
case ExifTag.YClipPathUnits:
case ExifTag.ProfileType:
case ExifTag.CodingMethods:
case ExifTag.JPEGInterchangeFormat:
case ExifTag.JPEGInterchangeFormatLength:
exifValue = new ExifValue(tag, ExifDataType.Long, false);
break;
case ExifTag.WhitePoint:
case ExifTag.PrimaryChromaticities:
case ExifTag.YCbCrCoefficients:
case ExifTag.ReferenceBlackWhite:
case ExifTag.GPSLatitude:
case ExifTag.GPSLongitude:
case ExifTag.GPSTimestamp:
case ExifTag.GPSDestLatitude:
case ExifTag.GPSDestLongitude:
exifValue = new ExifValue(tag, ExifDataType.Rational, true);
break;
case ExifTag.XPosition:
case ExifTag.YPosition:
case ExifTag.XResolution:
case ExifTag.YResolution:
case ExifTag.ExposureTime:
case ExifTag.FNumber:
case ExifTag.CompressedBitsPerPixel:
case ExifTag.ApertureValue:
case ExifTag.MaxApertureValue:
case ExifTag.SubjectDistance:
case ExifTag.FocalLength:
case ExifTag.FlashEnergy:
case ExifTag.FocalPlaneXResolution:
case ExifTag.FocalPlaneYResolution:
case ExifTag.ExposureIndex:
case ExifTag.DigitalZoomRatio:
case ExifTag.GPSAltitude:
case ExifTag.GPSDOP:
case ExifTag.GPSSpeed:
case ExifTag.GPSTrack:
case ExifTag.GPSImgDirection:
case ExifTag.GPSDestBearing:
case ExifTag.GPSDestDistance:
exifValue = new ExifValue(tag, ExifDataType.Rational, false);
break;
case ExifTag.BitsPerSample:
case ExifTag.MinSampleValue:
case ExifTag.MaxSampleValue:
case ExifTag.GrayResponseCurve:
case ExifTag.ColorMap:
case ExifTag.ExtraSamples:
case ExifTag.PageNumber:
case ExifTag.TransferFunction:
case ExifTag.Predictor:
case ExifTag.HalftoneHints:
case ExifTag.SampleFormat:
case ExifTag.TransferRange:
case ExifTag.DefaultImageColor:
case ExifTag.JPEGLosslessPredictors:
case ExifTag.JPEGPointTransforms:
case ExifTag.YCbCrSubsampling:
case ExifTag.ISOSpeedRatings:
case ExifTag.SubjectArea:
case ExifTag.SubjectLocation:
exifValue = new ExifValue(tag, ExifDataType.Short, true);
break;
case ExifTag.Compression:
case ExifTag.PhotometricInterpretation:
case ExifTag.Threshholding:
case ExifTag.CellWidth:
case ExifTag.CellLength:
case ExifTag.FillOrder:
case ExifTag.Orientation:
case ExifTag.SamplesPerPixel:
case ExifTag.PlanarConfiguration:
case ExifTag.GrayResponseUnit:
case ExifTag.ResolutionUnit:
case ExifTag.CleanFaxData:
case ExifTag.InkSet:
case ExifTag.NumberOfInks:
case ExifTag.DotRange:
case ExifTag.Indexed:
case ExifTag.OPIProxy:
case ExifTag.JPEGProc:
case ExifTag.JPEGRestartInterval:
case ExifTag.YCbCrPositioning:
case ExifTag.ExposureProgram:
case ExifTag.MeteringMode:
case ExifTag.LightSource:
case ExifTag.Flash:
case ExifTag.ColorSpace:
case ExifTag.FocalPlaneResolutionUnit:
case ExifTag.SensingMethod:
case ExifTag.CustomRendered:
case ExifTag.ExposureMode:
case ExifTag.WhiteBalance:
case ExifTag.FocalLengthIn35mmFilm:
case ExifTag.SceneCaptureType:
case ExifTag.GainControl:
case ExifTag.Contrast:
case ExifTag.Saturation:
case ExifTag.Sharpness:
case ExifTag.SubjectDistanceRange:
case ExifTag.GPSDifferential:
exifValue = new ExifValue(tag, ExifDataType.Short, false);
break;
case ExifTag.Decode:
exifValue = new ExifValue(tag, ExifDataType.SignedRational, true);
break;
case ExifTag.ShutterSpeedValue:
case ExifTag.BrightnessValue:
case ExifTag.ExposureBiasValue:
exifValue = new ExifValue(tag, ExifDataType.SignedRational, false);
break;
case ExifTag.JPEGTables:
case ExifTag.OECF:
case ExifTag.ExifVersion:
case ExifTag.ComponentsConfiguration:
case ExifTag.MakerNote:
case ExifTag.UserComment:
case ExifTag.FlashpixVersion:
case ExifTag.SpatialFrequencyResponse:
case ExifTag.CFAPattern:
case ExifTag.DeviceSettingDescription:
case ExifTag.GPSProcessingMethod:
case ExifTag.GPSAreaInformation:
exifValue = new ExifValue(tag, ExifDataType.Undefined, true);
break;
case ExifTag.FileSource:
case ExifTag.SceneType:
exifValue = new ExifValue(tag, ExifDataType.Undefined, false);
break;
case ExifTag.StripOffsets:
case ExifTag.TileByteCounts:
case ExifTag.ImageLayer:
exifValue = CreateNumber(tag, type, true);
break;
case ExifTag.ImageWidth:
case ExifTag.ImageLength:
case ExifTag.TileWidth:
case ExifTag.TileLength:
case ExifTag.BadFaxLines:
case ExifTag.ConsecutiveBadFaxLines:
case ExifTag.PixelXDimension:
case ExifTag.PixelYDimension:
exifValue = CreateNumber(tag, type, false);
break;
default:
throw new NotImplementedException();
}
exifValue.Value = value;
return exifValue;
}
internal static uint GetSize(ExifDataType dataType)
{
switch (dataType)
{
case ExifDataType.Ascii:
case ExifDataType.Byte:
case ExifDataType.SignedByte:
case ExifDataType.Undefined:
return 1;
case ExifDataType.Short:
case ExifDataType.SignedShort:
return 2;
case ExifDataType.Long:
case ExifDataType.SignedLong:
case ExifDataType.SingleFloat:
return 4;
case ExifDataType.DoubleFloat:
case ExifDataType.Rational:
case ExifDataType.SignedRational:
return 8;
default:
throw new NotImplementedException(dataType.ToString());
}
}
private void CheckValue(object value)
{
if (value == null)
return;
Type type = value.GetType();
if (DataType == ExifDataType.Ascii)
{
Guard.IsTrue(type == typeof(string), nameof(value), "Value should be a string.");
return;
}
if (type.IsArray)
{
Guard.IsTrue(IsArray, nameof(value), "Value should not be an array.");
type = type.GetElementType();
}
else
{
Guard.IsFalse(IsArray, nameof(value), "Value should not be an array.");
}
switch (DataType)
{
case ExifDataType.Byte:
Guard.IsTrue(type == typeof(byte), nameof(value), $"Value should be a byte{(IsArray ? " array." : ".")}");
break;
case ExifDataType.DoubleFloat:
case ExifDataType.Rational:
case ExifDataType.SignedRational:
Guard.IsTrue(type == typeof(double), nameof(value), $"Value should be a double{(IsArray ? " array." : ".")}");
break;
case ExifDataType.Long:
Guard.IsTrue(type == typeof(uint), nameof(value), $"Value should be an unsigned int{(IsArray ? " array." : ".")}");
break;
case ExifDataType.Short:
Guard.IsTrue(type == typeof(ushort), nameof(value), $"Value should be an unsigned short{(IsArray ? " array." : ".")}");
break;
case ExifDataType.SignedByte:
Guard.IsTrue(type == typeof(sbyte), nameof(value), $"Value should be a signed byte{(IsArray ? " array." : ".")}");
break;
case ExifDataType.SignedLong:
Guard.IsTrue(type == typeof(int), nameof(value), $"Value should be an int{(IsArray ? " array." : ".")}");
break;
case ExifDataType.SignedShort:
Guard.IsTrue(type == typeof(short), nameof(value), $"Value should be a short{(IsArray ? " array." : ".")}");
break;
case ExifDataType.SingleFloat:
Guard.IsTrue(type == typeof(float), nameof(value), $"Value should be a float{(IsArray ? " array." : ".")}");
break;
case ExifDataType.Undefined:
Guard.IsTrue(type == typeof(byte), nameof(value), "Value should be a byte array.");
break;
default:
throw new NotImplementedException();
}
}
private static ExifValue CreateNumber(ExifTag tag, Type type, bool isArray)
{
if (type == null || type == typeof(ushort))
return new ExifValue(tag, ExifDataType.Short, isArray);
else if (type == typeof(short))
return new ExifValue(tag, ExifDataType.SignedShort, isArray);
else if (type == typeof(uint))
return new ExifValue(tag, ExifDataType.Long, isArray);
else
return new ExifValue(tag, ExifDataType.SignedLong, isArray);
}
private string ToString(object value)
{
switch (DataType)
{
case ExifDataType.Ascii:
return (string)value;
case ExifDataType.Byte:
return ((byte)value).ToString("X2", CultureInfo.InvariantCulture);
case ExifDataType.DoubleFloat:
return ((double)value).ToString(CultureInfo.InvariantCulture);
case ExifDataType.Long:
return ((uint)value).ToString(CultureInfo.InvariantCulture);
case ExifDataType.Rational:
return ((double)value).ToString(CultureInfo.InvariantCulture);
case ExifDataType.Short:
return ((ushort)value).ToString(CultureInfo.InvariantCulture);
case ExifDataType.SignedByte:
return ((sbyte)value).ToString("X2", CultureInfo.InvariantCulture);
case ExifDataType.SignedLong:
return ((int)value).ToString(CultureInfo.InvariantCulture);
case ExifDataType.SignedRational:
return ((double)value).ToString(CultureInfo.InvariantCulture);
case ExifDataType.SignedShort:
return ((short)value).ToString(CultureInfo.InvariantCulture);
case ExifDataType.SingleFloat:
return ((float)value).ToString(CultureInfo.InvariantCulture);
case ExifDataType.Undefined:
return ((byte)value).ToString("X2", CultureInfo.InvariantCulture);
default:
throw new NotImplementedException();
}
}
}
}

407
src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs

@ -0,0 +1,407 @@
// <copyright file="ExifWriter.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
internal sealed class ExifWriter
{
private static readonly ExifTag[] IfdTags = new ExifTag[93]
{
ExifTag.ImageWidth, ExifTag.ImageLength, ExifTag.BitsPerSample, ExifTag.Compression,
ExifTag.PhotometricInterpretation, ExifTag.Threshholding, ExifTag.CellWidth,
ExifTag.CellLength, ExifTag.FillOrder,ExifTag.ImageDescription, ExifTag.Make,
ExifTag.Model, ExifTag.StripOffsets, ExifTag.Orientation, ExifTag.SamplesPerPixel,
ExifTag.RowsPerStrip, ExifTag.StripByteCounts, ExifTag.MinSampleValue,
ExifTag.MaxSampleValue, ExifTag.XResolution, ExifTag.YResolution,
ExifTag.PlanarConfiguration, ExifTag.FreeOffsets, ExifTag.FreeByteCounts,
ExifTag.GrayResponseUnit, ExifTag.GrayResponseCurve, ExifTag.ResolutionUnit,
ExifTag.Software, ExifTag.DateTime, ExifTag.Artist, ExifTag.HostComputer,
ExifTag.ColorMap, ExifTag.ExtraSamples, ExifTag.Copyright, ExifTag.DocumentName,
ExifTag.PageName, ExifTag.XPosition, ExifTag.YPosition, ExifTag.T4Options,
ExifTag.T6Options, ExifTag.PageNumber, ExifTag.TransferFunction, ExifTag.Predictor,
ExifTag.WhitePoint, ExifTag.PrimaryChromaticities, ExifTag.HalftoneHints,
ExifTag.TileWidth, ExifTag.TileLength, ExifTag.TileOffsets, ExifTag.TileByteCounts,
ExifTag.BadFaxLines, ExifTag.CleanFaxData, ExifTag.ConsecutiveBadFaxLines,
ExifTag.InkSet, ExifTag.InkNames, ExifTag.NumberOfInks, ExifTag.DotRange,
ExifTag.TargetPrinter, ExifTag.SampleFormat, ExifTag.SMinSampleValue,
ExifTag.SMaxSampleValue, ExifTag.TransferRange, ExifTag.ClipPath,
ExifTag.XClipPathUnits, ExifTag.YClipPathUnits, ExifTag.Indexed, ExifTag.JPEGTables,
ExifTag.OPIProxy, ExifTag.ProfileType, ExifTag.FaxProfile, ExifTag.CodingMethods,
ExifTag.VersionYear, ExifTag.ModeNumber, ExifTag.Decode, ExifTag.DefaultImageColor,
ExifTag.JPEGProc, ExifTag.JPEGInterchangeFormat, ExifTag.JPEGInterchangeFormatLength,
ExifTag.JPEGRestartInterval, ExifTag.JPEGLosslessPredictors,
ExifTag.JPEGPointTransforms, ExifTag.JPEGQTables, ExifTag.JPEGDCTables,
ExifTag.JPEGACTables, ExifTag.YCbCrCoefficients, ExifTag.YCbCrSubsampling,
ExifTag.YCbCrSubsampling, ExifTag.YCbCrPositioning, ExifTag.ReferenceBlackWhite,
ExifTag.StripRowCounts, ExifTag.XMP, ExifTag.ImageID, ExifTag.ImageLayer
};
private static readonly ExifTag[] ExifTags = new ExifTag[56]
{
ExifTag.ExposureTime, ExifTag.FNumber, ExifTag.ExposureProgram,
ExifTag.SpectralSensitivity, ExifTag.ISOSpeedRatings, ExifTag.OECF,
ExifTag.ExifVersion, ExifTag.DateTimeOriginal, ExifTag.DateTimeDigitized,
ExifTag.ComponentsConfiguration, ExifTag.CompressedBitsPerPixel,
ExifTag.ShutterSpeedValue, ExifTag.ApertureValue, ExifTag.BrightnessValue,
ExifTag.ExposureBiasValue, ExifTag.MaxApertureValue, ExifTag.SubjectDistance,
ExifTag.MeteringMode, ExifTag.LightSource, ExifTag.Flash, ExifTag.FocalLength,
ExifTag.SubjectArea, ExifTag.MakerNote, ExifTag.UserComment, ExifTag.SubsecTime,
ExifTag.SubsecTimeOriginal, ExifTag.SubsecTimeDigitized, ExifTag.FlashpixVersion,
ExifTag.ColorSpace, ExifTag.PixelXDimension, ExifTag.PixelYDimension,
ExifTag.RelatedSoundFile, ExifTag.FlashEnergy, ExifTag.SpatialFrequencyResponse,
ExifTag.FocalPlaneXResolution, ExifTag.FocalPlaneYResolution,
ExifTag.FocalPlaneResolutionUnit, ExifTag.SubjectLocation, ExifTag.ExposureIndex,
ExifTag.SensingMethod, ExifTag.FileSource, ExifTag.SceneType, ExifTag.CFAPattern,
ExifTag.CustomRendered, ExifTag.ExposureMode, ExifTag.WhiteBalance,
ExifTag.DigitalZoomRatio, ExifTag.FocalLengthIn35mmFilm, ExifTag.SceneCaptureType,
ExifTag.GainControl, ExifTag.Contrast, ExifTag.Saturation, ExifTag.Sharpness,
ExifTag.DeviceSettingDescription, ExifTag.SubjectDistanceRange, ExifTag.ImageUniqueID
};
private static readonly ExifTag[] GPSTags = new ExifTag[31]
{
ExifTag.GPSVersionID, ExifTag.GPSLatitudeRef, ExifTag.GPSLatitude,
ExifTag.GPSLongitudeRef, ExifTag.GPSLongitude, ExifTag.GPSAltitudeRef,
ExifTag.GPSAltitude, ExifTag.GPSTimestamp, ExifTag.GPSSatellites, ExifTag.GPSStatus,
ExifTag.GPSMeasureMode, ExifTag.GPSDOP, ExifTag.GPSSpeedRef, ExifTag.GPSSpeed,
ExifTag.GPSTrackRef, ExifTag.GPSTrack, ExifTag.GPSImgDirectionRef,
ExifTag.GPSImgDirection, ExifTag.GPSMapDatum, ExifTag.GPSDestLatitudeRef,
ExifTag.GPSDestLatitude, ExifTag.GPSDestLongitudeRef, ExifTag.GPSDestLongitude,
ExifTag.GPSDestBearingRef, ExifTag.GPSDestBearing, ExifTag.GPSDestDistanceRef,
ExifTag.GPSDestDistance, ExifTag.GPSProcessingMethod, ExifTag.GPSAreaInformation,
ExifTag.GPSDateStamp, ExifTag.GPSDifferential
};
private const int StartIndex = 6;
private ExifParts allowedParts;
private bool bestPrecision;
private Collection<ExifValue> values;
private Collection<int> dataOffsets;
private Collection<int> ifdIndexes;
private Collection<int> exifIndexes;
private Collection<int> gpsIndexes;
public ExifWriter(Collection<ExifValue> values, ExifParts allowedParts, bool bestPrecision)
{
this.values = values;
this.allowedParts = allowedParts;
this.bestPrecision = bestPrecision;
this.ifdIndexes = GetIndexes(ExifParts.IfdTags, IfdTags);
this.exifIndexes = GetIndexes(ExifParts.ExifTags, ExifTags);
this.gpsIndexes = GetIndexes(ExifParts.GPSTags, GPSTags);
}
public byte[] GetData()
{
uint length = 0;
int exifIndex = -1;
int gpsIndex = -1;
if (this.exifIndexes.Count > 0)
exifIndex = (int)GetIndex(this.ifdIndexes, ExifTag.SubIFDOffset);
if (this.gpsIndexes.Count > 0)
gpsIndex = (int)GetIndex(this.ifdIndexes, ExifTag.GPSIFDOffset);
uint ifdLength = 2 + GetLength(this.ifdIndexes) + 4;
uint exifLength = GetLength(this.exifIndexes);
uint gpsLength = GetLength(this.gpsIndexes);
if (exifLength > 0)
exifLength += 2;
if (gpsLength > 0)
gpsLength += 2;
length = ifdLength + exifLength + gpsLength;
if (length == 6)
return null;
length += 10 + 4 + 2;
byte[] result = new byte[length];
result[0] = (byte)'E';
result[1] = (byte)'x';
result[2] = (byte)'i';
result[3] = (byte)'f';
result[4] = 0x00;
result[5] = 0x00;
result[6] = (byte)'I';
result[7] = (byte)'I';
result[8] = 0x2A;
result[9] = 0x00;
int i = 10;
uint ifdOffset = ((uint)i - StartIndex) + 4;
uint thumbnailOffset = ifdOffset + ifdLength + exifLength + gpsLength;
if (exifLength > 0)
this.values[exifIndex].Value = (ifdOffset + ifdLength);
if (gpsLength > 0)
this.values[gpsIndex].Value = (ifdOffset + ifdLength + exifLength);
i = Write(BitConverter.GetBytes(ifdOffset), result, i);
i = WriteHeaders(this.ifdIndexes, result, i);
i = Write(BitConverter.GetBytes(thumbnailOffset), result, i);
i = WriteData(this.ifdIndexes, result, i);
if (exifLength > 0)
{
i = WriteHeaders(this.exifIndexes, result, i);
i = WriteData(this.exifIndexes, result, i);
}
if (gpsLength > 0)
{
i = WriteHeaders(this.gpsIndexes, result, i);
i = WriteData(this.gpsIndexes, result, i);
}
Write(BitConverter.GetBytes((ushort)0), result, i);
return result;
}
private int GetIndex(Collection<int> indexes, ExifTag tag)
{
foreach (int index in indexes)
{
if (this.values[index].Tag == tag)
return index;
}
int newIndex = this.values.Count;
indexes.Add(newIndex);
this.values.Add(ExifValue.Create(tag, null));
return newIndex;
}
private Collection<int> GetIndexes(ExifParts part, ExifTag[] tags)
{
if (((int)this.allowedParts & (int)part) == 0)
return new Collection<int>();
Collection<int> result = new Collection<int>();
for (int i = 0; i < this.values.Count; i++)
{
ExifValue value = this.values[i];
if (!value.HasValue)
continue;
int index = Array.IndexOf(tags, value.Tag);
if (index > -1)
result.Add(i);
}
return result;
}
private uint GetLength(IEnumerable<int> indexes)
{
uint length = 0;
foreach (int index in indexes)
{
uint valueLength = (uint)this.values[index].Length;
if (valueLength > 4)
length += 12 + valueLength;
else
length += 12;
}
return length;
}
private static int Write(byte[] source, byte[] destination, int offset)
{
Buffer.BlockCopy(source, 0, destination, offset, source.Length);
return offset + source.Length;
}
private int WriteArray(ExifValue value, byte[] destination, int offset)
{
if (value.DataType == ExifDataType.Ascii)
return WriteValue(ExifDataType.Ascii, value.Value, destination, offset);
int newOffset = offset;
foreach (object obj in (Array)value.Value)
newOffset = WriteValue(value.DataType, obj, destination, newOffset);
return newOffset;
}
private int WriteData(Collection<int> indexes, byte[] destination, int offset)
{
if (this.dataOffsets.Count == 0)
return offset;
int newOffset = offset;
int i = 0;
foreach (int index in indexes)
{
ExifValue value = this.values[index];
if (value.Length > 4)
{
Write(BitConverter.GetBytes(newOffset - StartIndex), destination, this.dataOffsets[i++]);
newOffset = WriteValue(value, destination, newOffset);
}
}
return newOffset;
}
private int WriteHeaders(Collection<int> indexes, byte[] destination, int offset)
{
this.dataOffsets = new Collection<int>();
int newOffset = Write(BitConverter.GetBytes((ushort)indexes.Count), destination, offset);
if (indexes.Count == 0)
return newOffset;
foreach (int index in indexes)
{
ExifValue value = this.values[index];
newOffset = Write(BitConverter.GetBytes((ushort)value.Tag), destination, newOffset);
newOffset = Write(BitConverter.GetBytes((ushort)value.DataType), destination, newOffset);
newOffset = Write(BitConverter.GetBytes((uint)value.NumberOfComponents), destination, newOffset);
if (value.Length > 4)
this.dataOffsets.Add(newOffset);
else
WriteValue(value, destination, newOffset);
newOffset += 4;
}
return newOffset;
}
private int WriteRational(double value, byte[] destination, int offset)
{
uint numerator = 1;
uint denominator = 1;
if (double.IsPositiveInfinity(value))
denominator = 0;
else if (double.IsNegativeInfinity(value))
denominator = 0;
else
{
double val = Math.Abs(value);
double df = numerator / denominator;
double epsilon = this.bestPrecision ? double.Epsilon : .000001;
while (Math.Abs(df - val) > epsilon)
{
if (df < val)
numerator++;
else
{
denominator++;
numerator = (uint)(val * denominator);
}
df = numerator / (double)denominator;
}
}
Write(BitConverter.GetBytes(numerator), destination, offset);
Write(BitConverter.GetBytes(denominator), destination, offset + 4);
return offset + 8;
}
private int WriteSignedRational(double value, byte[] destination, int offset)
{
int numerator = 1;
int denominator = 1;
if (double.IsPositiveInfinity(value))
denominator = 0;
else if (double.IsNegativeInfinity(value))
denominator = 0;
else
{
double val = Math.Abs(value);
double df = numerator / denominator;
double epsilon = this.bestPrecision ? double.Epsilon : .000001;
while (Math.Abs(df - val) > epsilon)
{
if (df < val)
numerator++;
else
{
denominator++;
numerator = (int)(val * denominator);
}
df = numerator / (double)denominator;
}
}
Write(BitConverter.GetBytes(numerator * (value < 0.0 ? -1 : 1)), destination, offset);
Write(BitConverter.GetBytes(denominator), destination, offset + 4);
return offset + 8;
}
private int WriteValue(ExifDataType dataType, object value, byte[] destination, int offset)
{
switch (dataType)
{
case ExifDataType.Ascii:
return Write(Encoding.UTF8.GetBytes((string)value), destination, offset);
case ExifDataType.Byte:
case ExifDataType.Undefined:
destination[offset] = (byte)value;
return offset + 1;
case ExifDataType.DoubleFloat:
return Write(BitConverter.GetBytes((double)value), destination, offset);
case ExifDataType.Short:
return Write(BitConverter.GetBytes((ushort)value), destination, offset);
case ExifDataType.Long:
return Write(BitConverter.GetBytes((uint)value), destination, offset);
case ExifDataType.Rational:
return WriteRational((double)value, destination, offset);
case ExifDataType.SignedByte:
destination[offset] = unchecked((byte)((sbyte)value));
return offset + 1;
case ExifDataType.SignedLong:
return Write(BitConverter.GetBytes((int)value), destination, offset);
case ExifDataType.SignedShort:
return Write(BitConverter.GetBytes((short)value), destination, offset);
case ExifDataType.SignedRational:
return WriteSignedRational((double)value, destination, offset);
case ExifDataType.SingleFloat:
return Write(BitConverter.GetBytes((float)value), destination, offset);
default:
throw new NotImplementedException();
}
}
private int WriteValue(ExifValue value, byte[] destination, int offset)
{
if (value.IsArray && value.DataType != ExifDataType.Ascii)
return WriteArray(value, destination, offset);
else
return WriteValue(value.DataType, value.Value, destination, offset);
}
}
}

3
src/ImageProcessorCore/Profiles/Exif/README.md

@ -0,0 +1,3 @@
Adapted from Magick.NET:
https://github.com/dlemstra/Magick.NET/tree/784e23b1f5c824fc03d4b95d3387b3efe1ed510b/Magick.NET/Core/Profiles/Exif

30
tests/ImageProcessorCore.Tests/FileTestBase.cs

@ -19,21 +19,21 @@ namespace ImageProcessorCore.Tests
/// </summary>
protected static readonly List<string> Files = new List<string>
{
//"TestImages/Formats/Png/pl.png",
//"TestImages/Formats/Png/pd.png",
//"TestImages/Formats/Jpg/Floorplan.jpeg", // Perf: Enable for local testing only
"TestImages/Formats/Jpg/Calliphora.jpg",
//"TestImages/Formats/Jpg/turtle.jpg",
//"TestImages/Formats/Jpg/fb.jpg", // Perf: Enable for local testing only
//"TestImages/Formats/Jpg/progress.jpg", // Perf: Enable for local testing only
//"TestImages/Formats/Jpg/gamma_dalai_lama_gray.jpg", // Perf: Enable for local testing only
"TestImages/Formats/Bmp/Car.bmp",
// "TestImages/Formats/Bmp/neg_height.bmp", // Perf: Enable for local testing only
//"TestImages/Formats/Png/blur.png", // Perf: Enable for local testing only
//"TestImages/Formats/Png/indexed.png", // Perf: Enable for local testing only
"TestImages/Formats/Png/splash.png",
"TestImages/Formats/Gif/rings.gif",
//"TestImages/Formats/Gif/giphy.gif" // Perf: Enable for local testing only
//TestImages.Png.P1,
//TestImages.Png.Pd,
//TestImages.Jpg.Floorplan, // Perf: Enable for local testing only
TestImages.Jpg.Calliphora,
//TestImages.Jpg.Turtle,
//TestImages.Jpg.Fb, // Perf: Enable for local testing only
//TestImages.Jpg.Progress, // Perf: Enable for local testing only
//TestImages.Jpg.Gamma_dalai_lama_gray. // Perf: Enable for local testing only
TestImages.Bmp.Car,
//TestImages.Bmp.Neg_height, // Perf: Enable for local testing only
//TestImages.Png.Blur, // Perf: Enable for local testing only
//TestImages.Png.Indexed, // Perf: Enable for local testing only
TestImages.Png.Splash,
TestImages.Gif.Rings,
//TestImages.Gif.Giphy // Perf: Enable for local testing only
};
protected void ProgressUpdate(object sender, ProgressEventArgs e)

38
tests/ImageProcessorCore.Tests/Helpers/GuardTests.cs

@ -200,5 +200,43 @@ namespace ImageProcessorCore.Tests.Helpers
Exception ex = Record.Exception(() => Guard.MustBeBetweenOrEqualTo(0, -1, 1, "foo"));
Assert.Null(ex);
}
/// <summary>
/// Tests that the <see cref="M:Guard.IsTrue"/> method throws when the argument is false.
/// </summary>
[Fact]
public void IsTrueThrowsWhenArgIsFalse()
{
Assert.Throws<ArgumentException>(() => Guard.IsTrue(false, "foo"));
}
/// <summary>
/// Tests that the <see cref="M:Guard.IsTrue"/> method does not throw when the argument is true.
/// </summary>
[Fact]
public void IsTrueDoesThrowsWhenArgIsTrue()
{
Exception ex = Record.Exception(() => Guard.IsTrue(true, "foo"));
Assert.Null(ex);
}
/// <summary>
/// Tests that the <see cref="M:Guard.IsFalse"/> method throws when the argument is true.
/// </summary>
[Fact]
public void IsFalseThrowsWhenArgIsFalse()
{
Assert.Throws<ArgumentException>(() => Guard.IsFalse(true, "foo"));
}
/// <summary>
/// Tests that the <see cref="M:Guard.IsFalse"/> method does not throw when the argument is false.
/// </summary>
[Fact]
public void IsFalseDoesThrowsWhenArgIsTrue()
{
Exception ex = Record.Exception(() => Guard.IsFalse(false, "foo"));
Assert.Null(ex);
}
}
}

306
tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs

@ -0,0 +1,306 @@
// <copyright file="ExifProfileTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Tests
{
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Text;
using Xunit;
public class ExifProfileTests
{
[Fact]
public void Constructor()
{
using (FileStream stream = File.OpenRead(TestImages.Jpg.Calliphora))
{
Image image = new Image(stream);
Assert.Null(image.ExifProfile);
image.ExifProfile = new ExifProfile();
image.ExifProfile.SetValue(ExifTag.Copyright, "Dirk Lemstra");
image = WriteAndRead(image);
Assert.NotNull(image.ExifProfile);
Assert.Equal(1, image.ExifProfile.Values.Count());
ExifValue value = image.ExifProfile.Values.FirstOrDefault(val => val.Tag == ExifTag.Copyright);
TestValue(value, "Dirk Lemstra");
}
}
[Fact]
public void ConstructorEmpty()
{
new ExifProfile((byte[])null);
new ExifProfile(new byte[] { });
}
[Fact]
public void ConstructorCopy()
{
Assert.Throws<ArgumentNullException>(() => { new ExifProfile((ExifProfile)null); });
ExifProfile profile = GetExifProfile();
ExifProfile clone = new ExifProfile(profile);
TestProfile(clone);
profile.SetValue(ExifTag.ColorSpace, (ushort)2);
clone = new ExifProfile(profile);
TestProfile(clone);
}
[Fact]
public void WriteFraction()
{
using (MemoryStream memStream = new MemoryStream())
{
double exposureTime = 1.0 / 1600;
ExifProfile profile = GetExifProfile();
profile.SetValue(ExifTag.ExposureTime, exposureTime);
Image image = new Image(1, 1);
image.ExifProfile = profile;
image.SaveAsJpeg(memStream);
memStream.Position = 0;
image = new Image(memStream);
profile = image.ExifProfile;
Assert.NotNull(profile);
ExifValue value = profile.GetValue(ExifTag.ExposureTime);
Assert.NotNull(value);
Assert.NotEqual(exposureTime, value.Value);
memStream.Position = 0;
profile = GetExifProfile();
profile.SetValue(ExifTag.ExposureTime, exposureTime);
profile.BestPrecision = true;
image.ExifProfile = profile;
image.SaveAsJpeg(memStream);
memStream.Position = 0;
image = new Image(memStream);
profile = image.ExifProfile;
Assert.NotNull(profile);
value = profile.GetValue(ExifTag.ExposureTime);
TestValue(value, exposureTime);
}
}
[Fact]
public void ReadWriteInfinity()
{
using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan))
{
Image image = new Image(stream);
image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, double.PositiveInfinity);
image = WriteAndRead(image);
ExifValue value = image.ExifProfile.GetValue(ExifTag.ExposureBiasValue);
Assert.NotNull(value);
Assert.Equal(double.PositiveInfinity, value.Value);
image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, double.NegativeInfinity);
image = WriteAndRead(image);
value = image.ExifProfile.GetValue(ExifTag.ExposureBiasValue);
Assert.NotNull(value);
Assert.Equal(double.NegativeInfinity, value.Value);
image.ExifProfile.SetValue(ExifTag.FlashEnergy, double.NegativeInfinity);
image = WriteAndRead(image);
value = image.ExifProfile.GetValue(ExifTag.FlashEnergy);
Assert.NotNull(value);
Assert.Equal(double.PositiveInfinity, value.Value);
}
}
[Fact]
public void SetValue()
{
double[] latitude = new double[] { 12.3, 4.56, 789.0 };
using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan))
{
Image image = new Image(stream);
image.ExifProfile.SetValue(ExifTag.Software, "ImageProcessorCore");
ExifValue value = image.ExifProfile.GetValue(ExifTag.Software);
TestValue(value, "ImageProcessorCore");
Assert.Throws<ArgumentException>(() => { value.Value = 15; });
image.ExifProfile.SetValue(ExifTag.ShutterSpeedValue, 75.55);
value = image.ExifProfile.GetValue(ExifTag.ShutterSpeedValue);
TestValue(value, 75.55);
Assert.Throws<ArgumentException>(() => { value.Value = 75; });
image.ExifProfile.SetValue(ExifTag.XResolution, 150.0);
value = image.ExifProfile.GetValue(ExifTag.XResolution);
TestValue(value, 150.0);
Assert.Throws<ArgumentException>(() => { value.Value = "ImageProcessorCore"; });
image.ExifProfile.SetValue(ExifTag.ReferenceBlackWhite, null);
value = image.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite);
TestValue(value, (string)null);
image.ExifProfile.SetValue(ExifTag.GPSLatitude, latitude);
value = image.ExifProfile.GetValue(ExifTag.GPSLatitude);
TestValue(value, latitude);
image = WriteAndRead(image);
Assert.NotNull(image.ExifProfile);
Assert.Equal(17, image.ExifProfile.Values.Count());
value = image.ExifProfile.GetValue(ExifTag.Software);
TestValue(value, "ImageProcessorCore");
value = image.ExifProfile.GetValue(ExifTag.ShutterSpeedValue);
TestValue(value, 75.55);
value = image.ExifProfile.GetValue(ExifTag.XResolution);
TestValue(value, 150.0);
value = image.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite);
Assert.Null(value);
value = image.ExifProfile.GetValue(ExifTag.GPSLatitude);
TestValue(value, latitude);
image.ExifProfile.Parts = ExifParts.ExifTags;
image = WriteAndRead(image);
Assert.NotNull(image.ExifProfile);
Assert.Equal(8, image.ExifProfile.Values.Count());
Assert.NotNull(image.ExifProfile.GetValue(ExifTag.ColorSpace));
Assert.True(image.ExifProfile.RemoveValue(ExifTag.ColorSpace));
Assert.False(image.ExifProfile.RemoveValue(ExifTag.ColorSpace));
Assert.Null(image.ExifProfile.GetValue(ExifTag.ColorSpace));
Assert.Equal(7, image.ExifProfile.Values.Count());
}
}
[Fact]
public void Values()
{
ExifProfile profile = GetExifProfile();
TestProfile(profile);
var thumbnail = profile.CreateThumbnail<Color, uint>();
Assert.NotNull(thumbnail);
Assert.Equal(256, thumbnail.Width);
Assert.Equal(170, thumbnail.Height);
}
[Fact]
public void WriteTooLargeProfile()
{
StringBuilder junk = new StringBuilder();
for (int i = 0; i < 65500; i++)
junk.Append("I");
Image image = new Image(100, 100);
image.ExifProfile = new ExifProfile();
image.ExifProfile.SetValue(ExifTag.ImageDescription, junk.ToString());
using (MemoryStream memStream = new MemoryStream())
{
Assert.Throws<ImageFormatException>(() => image.SaveAsJpeg(memStream));
}
}
private static ExifProfile GetExifProfile()
{
using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan))
{
Image image = new Image(stream);
ExifProfile profile = image.ExifProfile;
Assert.NotNull(profile);
return profile;
}
}
private static Image WriteAndRead(Image image)
{
using (MemoryStream memStream = new MemoryStream())
{
image.SaveAsJpeg(memStream);
memStream.Position = 0;
return new Image(memStream);
}
}
private static void TestProfile(ExifProfile profile)
{
Assert.NotNull(profile);
Assert.Equal(16, profile.Values.Count());
foreach (ExifValue value in profile.Values)
{
Assert.NotNull(value.Value);
if (value.Tag == ExifTag.Software)
Assert.Equal("Windows Photo Editor 10.0.10011.16384", value.ToString());
if (value.Tag == ExifTag.XResolution)
Assert.Equal(300.0, value.Value);
if (value.Tag == ExifTag.PixelXDimension)
Assert.Equal(2338U, value.Value);
}
}
private static void TestValue(ExifValue value, string expected)
{
Assert.NotNull(value);
Assert.Equal(expected, value.Value);
}
private static void TestValue(ExifValue value, double expected)
{
Assert.NotNull(value);
Assert.Equal(expected, value.Value);
}
private static void TestValue(ExifValue value, double[] expected)
{
Assert.NotNull(value);
Assert.Equal(expected, (ICollection)value.Value);
}
}
}

50
tests/ImageProcessorCore.Tests/Profiles/Exif/ExifValueTests.cs

@ -0,0 +1,50 @@
// <copyright file="ExifValueTests.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Tests
{
using System.IO;
using System.Linq;
using Xunit;
public class ExifValueTests
{
private static ExifValue GetExifValue()
{
using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan))
{
Image image = new Image(stream);
ExifProfile profile = image.ExifProfile;
Assert.NotNull(profile);
return profile.Values.First();
}
}
[Fact]
public void IEquatable()
{
ExifValue first = GetExifValue();
ExifValue second = GetExifValue();
Assert.True(first == second);
Assert.True(first.Equals(second));
Assert.True(first.Equals((object)second));
}
[Fact]
public void Properties()
{
ExifValue value = GetExifValue();
Assert.Equal(ExifDataType.Ascii, value.DataType);
Assert.Equal(ExifTag.GPSDOP, value.Tag);
Assert.Equal(false, value.IsArray);
Assert.Equal("Windows Photo Editor 10.0.10011.16384", value.ToString());
Assert.Equal("Windows Photo Editor 10.0.10011.16384", value.Value);
}
}
}

52
tests/ImageProcessorCore.Tests/TestImages.cs

@ -0,0 +1,52 @@
// <copyright file="FileTestBase.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>
namespace ImageProcessorCore.Tests
{
/// <summary>
/// Class that contains all the test images.
/// </summary>
public static class TestImages
{
public static class Png
{
private static readonly string folder = "TestImages/Formats/Png/";
public static string P1 { get { return folder + "pl.png"; } }
public static string Pd { get { return folder + "pd.png"; } }
public static string Blur { get { return folder + "blur.png"; } }
public static string Indexed { get { return folder + "indexed.png"; } }
public static string Splash { get { return folder + "splash.png"; } }
}
public static class Jpg
{
private static readonly string folder = "TestImages/Formats/Jpg/";
public static string Floorplan { get { return folder + "Floorplan.jpeg"; } }
public static string Calliphora { get { return folder + "Calliphora.jpg"; } }
public static string Turtle { get { return folder + "turtle.jpg"; } }
public static string Fb { get { return folder + "fb.jpg"; } }
public static string Progress { get { return folder + "progress.jpg"; } }
public static string Gamma_dalai_lama_gray { get { return folder + "gamma_dalai_lama_gray.jpg"; } }
}
public static class Bmp
{
private static readonly string folder = "TestImages/Formats/Bmp/";
public static string Car { get { return folder + "Car.bmp"; } }
public static string Neg_height { get { return folder + "neg_height.bmp"; } }
}
public static class Gif
{
private static readonly string folder = "TestImages/Formats/Gif/";
public static string Rings { get { return folder + "rings.gif"; } }
public static string Giphy { get { return folder + "giphy.gif"; } }
}
}
}
Loading…
Cancel
Save