From 93e4c4d562f112f3d20b961139195e4be40c3501 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 16 Aug 2016 01:22:31 +1000 Subject: [PATCH] Add Rational struct [skip ci] Former-commit-id: 42f38934bd4650dfdb7e733bebcdbe11eb57cd1f Former-commit-id: ff53006f57bd6b40ee863692b41eda42ce33b7b3 Former-commit-id: d1358cebb22a24144a19eef570c23e645a42714a --- src/ImageProcessorCore/Numerics/Rational.cs | 397 ++++++++++++++++++ .../Profiles/Exif/ExifReader.cs | 39 +- .../Profiles/Exif/ExifValue.cs | 14 +- .../Profiles/Exif/ExifWriter.cs | 141 ++++--- src/ImageProcessorCore/project.json | 1 + .../Profiles/Exif/ExifProfileTests.cs | 122 +++++- tests/ImageProcessorCore.Tests/TestImages.cs | 1 + .../TestImages/Formats/Jpg/exif.jpg | Bin 0 -> 32764 bytes 8 files changed, 627 insertions(+), 88 deletions(-) create mode 100644 src/ImageProcessorCore/Numerics/Rational.cs create mode 100644 tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/exif.jpg diff --git a/src/ImageProcessorCore/Numerics/Rational.cs b/src/ImageProcessorCore/Numerics/Rational.cs new file mode 100644 index 0000000000..fd578dc7ca --- /dev/null +++ b/src/ImageProcessorCore/Numerics/Rational.cs @@ -0,0 +1,397 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + using System.Globalization; + using System.Numerics; + using System.Runtime.InteropServices; + using System.Text; + + /// + /// Represents a number that can be expressed as a fraction + /// + /// + /// This is a very simplified implimentation of a rational number designed for use with + /// metadata only. + /// + public struct Rational : IEquatable + { + /// + /// Represents a rational object that is not a number. + /// + public static Rational Indeterminate = new Rational(0, 0); + + /// + /// Represents a rational object that is equal to 0. + /// + public static Rational Zero = new Rational(0, 1); + + /// + /// Represents a rational object that is equal to 1. + /// + public static Rational One = new Rational(1, 1); + + /// + /// Represents a Rational object that is equal to negative infinity (-1, 0). + /// + public static readonly Rational NegativeInfinity = new Rational(-1, 0); + + /// + /// Represents a Rational object that is equal to positive infinity (+1, 0). + /// + public static readonly Rational PositiveInfinity = new Rational(1, 0); + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The number above the line in a vulgar fraction showing how many of the parts + /// indicated by the denominator are taken. + /// + /// + /// The number below the line in a vulgar fraction; a divisor. + /// + public Rational(uint numerator, uint denominator) + : this() + { + this.Numerator = numerator; + this.Denominator = denominator; + + this.Simplify(); + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The number above the line in a vulgar fraction showing how many of the parts + /// indicated by the denominator are taken. + /// + /// + /// The number below the line in a vulgar fraction; a divisor. + /// + public Rational(int numerator, int denominator) + : this() + { + this.Numerator = numerator; + this.Denominator = denominator; + + this.Simplify(); + } + + /// + /// Initializes a new instance of the struct. + /// + /// + /// The number above the line in a vulgar fraction showing how many of the parts + /// indicated by the denominator are taken. + /// + /// + /// The number below the line in a vulgar fraction; a divisor. + /// + public Rational(BigInteger numerator, BigInteger denominator) + : this() + { + this.Numerator = numerator; + this.Denominator = denominator; + + this.Simplify(); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The big integer to create the rational from. + public Rational(BigInteger value) + : this() + { + this.Numerator = value; + this.Denominator = BigInteger.One; + + this.Simplify(); + } + + /// + /// Initializes a new instance of the struct. + /// + /// The double to create the rational from. + public Rational(double value) + : this() + { + if (double.IsNaN(value)) + { + this = Indeterminate; + return; + } + else if (double.IsPositiveInfinity(value)) + { + this = PositiveInfinity; + return; + } + else if (double.IsNegativeInfinity(value)) + { + this = NegativeInfinity; + return; + } + + this = Parse(value.ToString("R", CultureInfo.InvariantCulture)); + } + + /// + /// Gets the numerator of a number. + /// + public BigInteger Numerator { get; private set; } + + /// + /// Gets the denominator of a number. + /// + public BigInteger Denominator { get; private set; } + + /// + /// Gets a value indicating whether this instance is indeterminate. + /// + public bool IsIndeterminate => (this.Equals(Indeterminate)); + + /// + /// Gets a value indicating whether this instance is an integer. + /// + public bool IsInteger => (this.Denominator == 1); + + /// + /// Gets a value indicating whether this instance is equal to 0 + /// + public bool IsZero => (this.Equals(Zero)); + + /// + /// Gets a value indicating whether this instance is equal to 1. + /// + public bool IsOne => (this.Equals(One)); + + /// + /// Gets a value indicating whether this instance is equal to negative infinity (-1, 0). + /// + public bool IsNegativeInfinity => (this.Equals(NegativeInfinity)); + + /// + /// Gets a value indicating whether this instance is equal to positive infinity (1, 0). + /// + public bool IsPositiveInfinity => (this.Equals(PositiveInfinity)); + + /// + /// Converts a rational number to the nearest double. + /// + /// + /// The . + /// + public double ToDouble() + { + if (this.IsIndeterminate) + { + return double.NaN; + } + + if (this.IsPositiveInfinity) + { + return double.PositiveInfinity; + } + + if (this.IsNegativeInfinity) + { + return double.NegativeInfinity; + } + + if (this.IsInteger) + { + return (double)this.Numerator; + } + + return (double)(this.Numerator / this.Denominator); + } + + /// + public override bool Equals(object obj) + { + if (obj is Rational) + { + return this.Equals((Rational)obj); + } + + return false; + } + + /// + public bool Equals(Rational other) + { + if (this.Denominator == other.Denominator) + { + return this.Numerator == other.Numerator; + } + else if (this.Numerator == BigInteger.Zero && this.Denominator == BigInteger.Zero) + { + return other.Numerator == BigInteger.Zero && other.Denominator == BigInteger.Zero; + } + else if (other.Numerator == BigInteger.Zero && other.Denominator == BigInteger.Zero) + { + return this.Numerator == BigInteger.Zero && this.Denominator == BigInteger.Zero; + } + else + { + return (this.Numerator * other.Denominator) == (this.Denominator * other.Numerator); + } + } + + /// + public override int GetHashCode() + { + unchecked + { + return ((int)this.Numerator * 397) ^ (int)this.Denominator; + } + } + + /// + public override string ToString() + { + return this.ToString(CultureInfo.InvariantCulture); + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation using + /// the specified culture-specific format information. + /// + /// + /// An object that supplies culture-specific formatting information. + /// + /// + public string ToString(IFormatProvider provider) + + { + if (this.IsIndeterminate) + { + return "[ Indeterminate ]"; + } + + if (this.IsPositiveInfinity) + { + return "[ PositiveInfinity ]"; + } + + if (this.IsNegativeInfinity) + { + return "[ NegativeInfinity ]"; + } + + if (this.IsInteger) + { + return this.Numerator.ToString(provider); + } + + StringBuilder sb = new StringBuilder(); + sb.Append(this.Numerator.ToString("R", provider)); + sb.Append("/"); + sb.Append(this.Denominator.ToString("R", provider)); + return sb.ToString(); + } + + /// + /// Simplifies the rational. + /// + private void Simplify() + { + if (this.IsIndeterminate) + { + return; + } + + if (this.IsNegativeInfinity) + { + return; + } + + if (this.IsPositiveInfinity) + { + return; + } + + if (this.IsInteger) + { + return; + } + + if (this.Numerator == BigInteger.Zero) + { + Denominator = BigInteger.One; + return; + } + + if (this.Numerator == this.Denominator) + { + this.Numerator = BigInteger.One; + this.Denominator = BigInteger.One; + return; + } + + BigInteger gcd = BigInteger.GreatestCommonDivisor(this.Numerator, this.Denominator); + if (gcd > BigInteger.One) + { + this.Numerator = this.Numerator / gcd; + this.Denominator = this.Denominator / gcd; + } + } + + /// + /// Converts the string representation of a number into its rational value + /// + /// A string that contains a number to convert. + /// The + internal static Rational Parse(string value) + { + int periodIndex = value.IndexOf("."); + int eIndeix = value.IndexOf("E"); + int slashIndex = value.IndexOf("/"); + + // An integer such as 7 + if (periodIndex == -1 && eIndeix == -1 && slashIndex == -1) + { + return new Rational(BigInteger.Parse(value)); + } + + // A fraction such as 3/7 + if (periodIndex == -1 && eIndeix == -1 && slashIndex != -1) + { + return new Rational(BigInteger.Parse(value.Substring(0, slashIndex)), + BigInteger.Parse(value.Substring(slashIndex + 1))); + } + + // No scientific Notation such as 5.997 + if (eIndeix == -1) + { + BigInteger n = BigInteger.Parse(value.Replace(".", "")); + BigInteger d = (BigInteger)Math.Pow(10, value.Length - periodIndex - 1); + return new Rational(n, d); + } + + // Scientific notation such as 2.4556E-2 + int characteristic = int.Parse(value.Substring(eIndeix + 1)); + BigInteger ten = 10; + BigInteger numerator = BigInteger.Parse(value.Substring(0, eIndeix).Replace(".", "")); + BigInteger denominator = new BigInteger(Math.Pow(10, eIndeix - periodIndex - 1)); + BigInteger charPower = BigInteger.Pow(ten, Math.Abs(characteristic)); + + if (characteristic > 0) + { + numerator = numerator * charPower; + } + else + { + denominator = denominator * charPower; + } + + return new Rational(numerator, denominator); + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs b/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs index ef601596ec..1ebb9c9e9e 100644 --- a/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs +++ b/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs @@ -439,20 +439,34 @@ namespace ImageProcessorCore return result; } - private double ToRational(byte[] data) + private Rational ToRational(byte[] data) { if (!ValidateArray(data, 8, 4)) { - return default(double); + return Rational.Zero; } uint numerator = BitConverter.ToUInt32(data, 0); uint denominator = BitConverter.ToUInt32(data, 4); // TODO: investigate the possibility of a Rational struct - return numerator / (double)denominator; + return new Rational(numerator, denominator); } + //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; + //} + private sbyte ToSignedByte(byte[] data) { return unchecked((sbyte)data[0]); @@ -468,19 +482,32 @@ namespace ImageProcessorCore return BitConverter.ToInt32(data, 0); } - private double ToSignedRational(byte[] data) + private Rational ToSignedRational(byte[] data) { if (!ValidateArray(data, 8, 4)) { - return default(double); + return Rational.Zero; } int numerator = BitConverter.ToInt32(data, 0); int denominator = BitConverter.ToInt32(data, 4); - return numerator / (double)denominator; + return new Rational(numerator, denominator); } + //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)) diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs b/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs index 86b1d3b368..9609ff3667 100644 --- a/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs +++ b/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs @@ -517,9 +517,13 @@ namespace ImageProcessorCore 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.Rational: case ExifDataType.SignedRational: - Guard.IsTrue(type == typeof(double), nameof(value), $"Value should be a double{(IsArray ? " array." : ".")}"); + Guard.IsTrue(type == typeof(Rational), nameof(value), $"Value should be a Rational{(IsArray ? " array." : ".")}"); break; case ExifDataType.Long: Guard.IsTrue(type == typeof(uint), nameof(value), $"Value should be an unsigned int{(IsArray ? " array." : ".")}"); @@ -543,7 +547,7 @@ namespace ImageProcessorCore Guard.IsTrue(type == typeof(byte), nameof(value), "Value should be a byte array."); break; default: - throw new NotImplementedException(); + throw new NotSupportedException(); } } @@ -576,7 +580,8 @@ namespace ImageProcessorCore case ExifDataType.Long: return ((uint)value).ToString(CultureInfo.InvariantCulture); case ExifDataType.Rational: - return ((double)value).ToString(CultureInfo.InvariantCulture); + //return ((double)value).ToString(CultureInfo.InvariantCulture); + return ((Rational)value).ToString(CultureInfo.InvariantCulture); case ExifDataType.Short: return ((ushort)value).ToString(CultureInfo.InvariantCulture); case ExifDataType.SignedByte: @@ -584,7 +589,8 @@ namespace ImageProcessorCore case ExifDataType.SignedLong: return ((int)value).ToString(CultureInfo.InvariantCulture); case ExifDataType.SignedRational: - return ((double)value).ToString(CultureInfo.InvariantCulture); + //return ((double)value).ToString(CultureInfo.InvariantCulture); + return ((Rational)value).ToString(CultureInfo.InvariantCulture); case ExifDataType.SignedShort: return ((short)value).ToString(CultureInfo.InvariantCulture); case ExifDataType.SingleFloat: diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs b/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs index cee38cda5b..f41668a585 100644 --- a/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs +++ b/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs @@ -292,76 +292,93 @@ namespace ImageProcessorCore return newOffset; } - private int WriteRational(double value, byte[] destination, int offset) + private int WriteRational(Rational 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); + Write(BitConverter.GetBytes((uint)(value.Numerator * (value.ToDouble() < 0.0 ? -1 : 1))), destination, offset); + Write(BitConverter.GetBytes((uint)value.Denominator), destination, offset + 4); return offset + 8; } - private int WriteSignedRational(double value, byte[] destination, int offset) + //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(Rational 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); + // TODO: Check this. + Write(BitConverter.GetBytes((int)value.Numerator), destination, offset); + Write(BitConverter.GetBytes((int)value.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) @@ -379,7 +396,7 @@ namespace ImageProcessorCore case ExifDataType.Long: return Write(BitConverter.GetBytes((uint)value), destination, offset); case ExifDataType.Rational: - return WriteRational((double)value, destination, offset); + return WriteRational((Rational)value, destination, offset); case ExifDataType.SignedByte: destination[offset] = unchecked((byte)((sbyte)value)); return offset + 1; @@ -388,7 +405,7 @@ namespace ImageProcessorCore case ExifDataType.SignedShort: return Write(BitConverter.GetBytes((short)value), destination, offset); case ExifDataType.SignedRational: - return WriteSignedRational((double)value, destination, offset); + return WriteSignedRational((Rational)value, destination, offset); case ExifDataType.SingleFloat: return Write(BitConverter.GetBytes((float)value), destination, offset); default: diff --git a/src/ImageProcessorCore/project.json b/src/ImageProcessorCore/project.json index 85d2f9723a..633b9ac81f 100644 --- a/src/ImageProcessorCore/project.json +++ b/src/ImageProcessorCore/project.json @@ -28,6 +28,7 @@ "System.Resources.ResourceManager": "4.0.1", "System.Runtime.Extensions": "4.1.0", "System.Runtime.InteropServices": "4.1.0", + "System.Runtime.Numerics": "4.0.1", "System.Text.Encoding.Extensions": "4.0.11", "System.Threading": "4.0.11", "System.Threading.Tasks": "4.0.11", diff --git a/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs b/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs index 908d49b59e..31307f2cf6 100644 --- a/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs @@ -64,7 +64,8 @@ namespace ImageProcessorCore.Tests { using (MemoryStream memStream = new MemoryStream()) { - double exposureTime = 1.0 / 1600; + // double exposureTime = 1.0 / 1600; + Rational exposureTime = new Rational(1, 1600); ExifProfile profile = GetExifProfile(); @@ -83,7 +84,7 @@ namespace ImageProcessorCore.Tests ExifValue value = profile.GetValue(ExifTag.ExposureTime); Assert.NotNull(value); - Assert.NotEqual(exposureTime, value.Value); + Assert.Equal(exposureTime, value.Value); memStream.Position = 0; profile = GetExifProfile(); @@ -111,33 +112,108 @@ namespace ImageProcessorCore.Tests using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan)) { Image image = new Image(stream); - image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, double.PositiveInfinity); + image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, Rational.PositiveInfinity); image = WriteAndRead(image); ExifValue value = image.ExifProfile.GetValue(ExifTag.ExposureBiasValue); Assert.NotNull(value); - Assert.Equal(double.PositiveInfinity, value.Value); + Assert.Equal(Rational.PositiveInfinity, value.Value); - image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, double.NegativeInfinity); + image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, Rational.NegativeInfinity); image = WriteAndRead(image); value = image.ExifProfile.GetValue(ExifTag.ExposureBiasValue); Assert.NotNull(value); - Assert.Equal(double.NegativeInfinity, value.Value); + Assert.Equal(Rational.NegativeInfinity, value.Value); - image.ExifProfile.SetValue(ExifTag.FlashEnergy, double.NegativeInfinity); + image.ExifProfile.SetValue(ExifTag.FlashEnergy, Rational.NegativeInfinity); image = WriteAndRead(image); value = image.ExifProfile.GetValue(ExifTag.FlashEnergy); Assert.NotNull(value); - Assert.Equal(double.PositiveInfinity, value.Value); + Assert.Equal(Rational.PositiveInfinity, value.Value); } } + //[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 }; + Rational[] latitude = new Rational[] { new Rational(12.3), new Rational(4.56), new Rational(789.0) }; using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan)) { @@ -149,17 +225,18 @@ namespace ImageProcessorCore.Tests Assert.Throws(() => { value.Value = 15; }); - image.ExifProfile.SetValue(ExifTag.ShutterSpeedValue, 75.55); + image.ExifProfile.SetValue(ExifTag.ShutterSpeedValue, new Rational(75.55)); value = image.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); - TestValue(value, 75.55); + + TestValue(value, new Rational(7555, 100)); Assert.Throws(() => { value.Value = 75; }); - image.ExifProfile.SetValue(ExifTag.XResolution, 150.0); + image.ExifProfile.SetValue(ExifTag.XResolution, new Rational(150.0)); value = image.ExifProfile.GetValue(ExifTag.XResolution); - TestValue(value, 150.0); + TestValue(value, new Rational(150, 1)); Assert.Throws(() => { value.Value = "ImageProcessorCore"; }); @@ -182,10 +259,10 @@ namespace ImageProcessorCore.Tests TestValue(value, "ImageProcessorCore"); value = image.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); - TestValue(value, 75.55); + TestValue(value, new Rational(75.55)); value = image.ExifProfile.GetValue(ExifTag.XResolution); - TestValue(value, 150.0); + TestValue(value, new Rational(150.0)); value = image.ExifProfile.GetValue(ExifTag.ReferenceBlackWhite); Assert.Null(value); @@ -277,7 +354,7 @@ namespace ImageProcessorCore.Tests Assert.Equal("Windows Photo Editor 10.0.10011.16384", value.ToString()); if (value.Tag == ExifTag.XResolution) - Assert.Equal(300.0, value.Value); + Assert.Equal(new Rational(300.0), value.Value); if (value.Tag == ExifTag.PixelXDimension) Assert.Equal(2338U, value.Value); @@ -290,6 +367,19 @@ namespace ImageProcessorCore.Tests Assert.Equal(expected, value.Value); } + private static void TestValue(ExifValue value, Rational expected) + { + Assert.NotNull(value); + Assert.Equal(expected, value.Value); + } + + private static void TestValue(ExifValue value, Rational[] expected) + { + Assert.NotNull(value); + + Assert.Equal(expected, (ICollection)value.Value); + } + private static void TestValue(ExifValue value, double expected) { Assert.NotNull(value); diff --git a/tests/ImageProcessorCore.Tests/TestImages.cs b/tests/ImageProcessorCore.Tests/TestImages.cs index f8379d9f0f..61c5f25cdd 100644 --- a/tests/ImageProcessorCore.Tests/TestImages.cs +++ b/tests/ImageProcessorCore.Tests/TestImages.cs @@ -25,6 +25,7 @@ namespace ImageProcessorCore.Tests { private static readonly string folder = "TestImages/Formats/Jpg/"; + public static string Exif { get { return folder + "exif.jpeg"; } } 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"; } } diff --git a/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/exif.jpg b/tests/ImageProcessorCore.Tests/TestImages/Formats/Jpg/exif.jpg new file mode 100644 index 0000000000000000000000000000000000000000..933719d1cf71cfe3e1794e258eb56a2c15c6fa40 GIT binary patch literal 32764 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