From 938a6ea5d6a6659b9eb24d39b0ee685f1e775a59 Mon Sep 17 00:00:00 2001 From: dirk Date: Fri, 26 Aug 2016 10:52:49 +0200 Subject: [PATCH] Refactored the Rational type. Former-commit-id: 1272c4c34838ae467fbe63050adb9b6a476fbcc2 Former-commit-id: a0510c5bea4432f5758af7c4058fa359794dc70b Former-commit-id: 2f5006c4a70827e9f796698303cda409a2266ef2 --- .../Numerics/BigRational.cs | 219 +++++++ src/ImageProcessorCore/Numerics/Rational.cs | 545 ++++-------------- .../Numerics/SignedRational.cs | 199 +++++++ .../Profiles/Exif/ExifReader.cs | 10 +- .../Profiles/Exif/ExifValue.cs | 10 +- .../Profiles/Exif/ExifWriter.cs | 13 +- .../Numerics/RationalTests.cs | 64 +- .../Numerics/SignedRationalTests.cs | 122 ++++ .../Profiles/Exif/ExifProfileTests.cs | 110 +--- 9 files changed, 735 insertions(+), 557 deletions(-) create mode 100644 src/ImageProcessorCore/Numerics/BigRational.cs create mode 100644 src/ImageProcessorCore/Numerics/SignedRational.cs create mode 100644 tests/ImageProcessorCore.Tests/Numerics/SignedRationalTests.cs diff --git a/src/ImageProcessorCore/Numerics/BigRational.cs b/src/ImageProcessorCore/Numerics/BigRational.cs new file mode 100644 index 0000000000..9659796740 --- /dev/null +++ b/src/ImageProcessorCore/Numerics/BigRational.cs @@ -0,0 +1,219 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + using System.Text; + + internal struct BigRational : IEquatable + { + private bool IsIndeterminate + { + get + { + if (Denominator != 0) + return false; + + return Numerator == 0; + } + } + + private bool IsInteger => Denominator == 1; + + private bool IsNegativeInfinity + { + get + { + if (Denominator != 0) + return false; + + return Numerator == -1; + } + } + + private bool IsPositiveInfinity + { + get + { + if (Denominator != 0) + return false; + + return Numerator == 1; + } + } + + private bool IsZero + { + get + { + if (Denominator != 1) + return false; + + return Numerator == 0; + } + } + + private static long GreatestCommonDivisor(long a, long b) + { + return b == 0 ? a : GreatestCommonDivisor(b, a % b); + } + + private void Simplify() + { + if (IsIndeterminate) + return; + + if (IsNegativeInfinity) + return; + + if (IsPositiveInfinity) + return; + + if (IsInteger) + return; + + if (IsZero) + return; + + if (Numerator == 0) + { + Denominator = 0; + return; + } + + if (Numerator == Denominator) + { + Numerator = 1; + Denominator = 1; + } + + long gcd = GreatestCommonDivisor(Math.Abs(Numerator), Math.Abs(Denominator)); + if (gcd > 1) + { + Numerator = Numerator / gcd; + Denominator = Denominator / gcd; + } + } + + public BigRational(long numerator, long denominator) + : this(numerator, denominator, false) + { + } + + public BigRational(long numerator, long denominator, bool simplify) + { + Numerator = numerator; + Denominator = denominator; + + if (simplify) + Simplify(); + } + + public BigRational(double value, bool bestPrecision) + { + if (double.IsNaN(value)) + { + Numerator = Denominator = 0; + return; + } + + if (double.IsPositiveInfinity(value)) + { + Numerator = 1; + Denominator = 0; + return; + } + + if (double.IsNegativeInfinity(value)) + { + Numerator = -1; + Denominator = 0; + return; + } + + Numerator = 1; + Denominator = 1; + + double val = Math.Abs(value); + double df = Numerator / Denominator; + double epsilon = bestPrecision ? double.Epsilon : .000001; + + while (Math.Abs(df - val) > epsilon) + { + if (df < val) + Numerator++; + else + { + Denominator++; + Numerator = (int)(val * Denominator); + } + + df = Numerator / (double)Denominator; + } + + if (value < 0.0) + Numerator *= -1; + + Simplify(); + } + + public long Denominator + { + get; + private set; + } + + public long Numerator + { + get; + private set; + } + + public bool Equals(BigRational other) + { + if (Denominator == other.Denominator) + return Numerator == other.Numerator; + + if (Numerator == 0 && Denominator == 0) + return other.Numerator == 0 && other.Denominator == 0; + + if (other.Numerator == 0 && other.Denominator == 0) + return Numerator == 0 && Denominator == 0; + + return (Numerator * other.Denominator) == (Denominator * other.Numerator); + } + + public override int GetHashCode() + { + return ((Numerator * 397) ^ Denominator).GetHashCode(); + } + + public string ToString(IFormatProvider provider) + { + if (IsIndeterminate) + return "[ Indeterminate ]"; + + if (IsPositiveInfinity) + return "[ PositiveInfinity ]"; + + if (IsNegativeInfinity) + return "[ NegativeInfinity ]"; + + if (IsZero) + return "0"; + + if (IsInteger) + return Numerator.ToString(provider); + + StringBuilder sb = new StringBuilder(); + sb.Append(Numerator.ToString(provider)); + sb.Append("/"); + sb.Append(Denominator.ToString(provider)); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Numerics/Rational.cs b/src/ImageProcessorCore/Numerics/Rational.cs index 090af6e3ca..1de71fa860 100644 --- a/src/ImageProcessorCore/Numerics/Rational.cs +++ b/src/ImageProcessorCore/Numerics/Rational.cs @@ -7,11 +7,9 @@ namespace ImageProcessorCore { using System; using System.Globalization; - using System.Numerics; - using System.Text; /// - /// Represents a number that can be expressed as a fraction + /// 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. @@ -19,532 +17,183 @@ namespace ImageProcessorCore public struct Rational : IEquatable { /// - /// Represents a rational object that is not a number; NaN (0, 0) + /// Initializes a new instance of the struct. /// - public static Rational Indeterminate = new Rational(BigInteger.Zero); - - /// - /// Represents a rational object that is equal to 0 (0, 1) - /// - public static Rational Zero = new Rational(BigInteger.Zero); - - /// - /// Represents a rational object that is equal to 1 (1, 1) - /// - public static Rational One = new Rational(BigInteger.One); - - /// - /// Represents a Rational object that is equal to negative infinity (-1, 0) - /// - public static readonly Rational NegativeInfinity = new Rational(BigInteger.MinusOne, BigInteger.Zero); - - /// - /// Represents a Rational object that is equal to positive infinity (1, 0) - /// - public static readonly Rational PositiveInfinity = new Rational(BigInteger.One, BigInteger.Zero); - - /// - /// The maximum number of decimal places - /// - private const int DoubleMaxScale = 308; - - /// - /// The maximum precision (numbers after the decimal point) - /// - private static readonly BigInteger DoublePrecision = BigInteger.Pow(10, DoubleMaxScale); - - /// - /// Represents double.MaxValue - /// - private static readonly BigInteger DoubleMaxValue = (BigInteger)double.MaxValue; - - /// - /// Represents double.MinValue - /// - private static readonly BigInteger DoubleMinValue = (BigInteger)double.MinValue; - - /// - /// 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() + ///The to convert to an instance of this type. + public Rational(double value) + : this(value, false) { - this.Numerator = numerator; - this.Denominator = denominator; - - this.Simplify(); } /// - /// Initializes a new instance of the struct. + /// 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() + ///The to convert to an instance of this type. + ///Specifies if the instance should be created with the best precision possible. + public Rational(double value, bool bestPrecision) { - this.Numerator = numerator; - this.Denominator = denominator; + BigRational rational = new BigRational(Math.Abs(value), bestPrecision); - this.Simplify(); + Numerator = (uint)rational.Numerator; + Denominator = (uint)rational.Denominator; } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// - /// The big integer to create the rational from. - public Rational(BigInteger value) - : this() + /// The integer to create the rational from. + public Rational(uint value) + : this(value, 1) { - this.Numerator = value; - this.Denominator = BigInteger.One; - - this.Simplify(); } /// - /// Initializes a new instance of the struct. + /// 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; - } - - if (double.IsPositiveInfinity(value)) - { - this = PositiveInfinity; - return; - } - - if (double.IsNegativeInfinity(value)) - { - this = NegativeInfinity; - return; - } - - // TODO: Not happy with parsing a string like this. I should be able to use maths but maths is HARD! - 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 + /// 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(numerator, denominator, true) { - get - { - if (this.Denominator != BigInteger.Zero) - { - return false; - } - - return this.Numerator == BigInteger.Zero; - } } /// - /// Gets a value indicating whether this instance is an integer (n, 1) + /// Initializes a new instance of the struct. /// - public bool IsInteger => this.Denominator == BigInteger.One; - - /// - /// Gets a value indicating whether this instance is equal to 0 (0, 1) - /// - public bool IsZero + /// 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. + /// Specified if the rational should be simplified. + public Rational(uint numerator, uint denominator, bool simplify) { - get - { - if (this.Denominator != BigInteger.One) - { - return false; - } + BigRational rational = new BigRational(numerator, denominator, simplify); - return this.Numerator == BigInteger.Zero; - } + Numerator = (uint)rational.Numerator; + Denominator = (uint)rational.Denominator; } /// - /// Gets a value indicating whether this instance is equal to 1 (1, 1) + /// Determines whether the specified instances are considered equal. /// - public bool IsOne + /// The first to compare. + /// The second to compare. + /// + public static bool operator ==(Rational left, Rational right) { - get - { - if (this.Denominator != BigInteger.One) - { - return false; - } - - return this.Numerator == BigInteger.One; - } + return Equals(left, right); } /// - /// Gets a value indicating whether this instance is equal to negative infinity (-1, 0) + /// Determines whether the specified instances are not considered equal. /// - public bool IsNegativeInfinity + /// The first to compare. + /// The second to compare. + /// + public static bool operator !=(Rational left, Rational right) { - get - { - if (this.Denominator != BigInteger.Zero) - { - return false; - } - - return this.Numerator == BigInteger.MinusOne; - } + return !Equals(left, right); } /// - /// Gets a value indicating whether this instance is equal to positive infinity (1, 0) + /// Gets the numerator of a number. /// - public bool IsPositiveInfinity + public uint Numerator { - get - { - if (this.Denominator != BigInteger.Zero) - { - return false; - } - - return this.Numerator == BigInteger.One; - } + get; + private set; } /// - /// Converts a rational number to the nearest double. + /// Gets the denominator of a number. /// - /// - /// The . - /// - public double ToDouble() + public uint Denominator { - // Shortcut return values - 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; - } - - // The Double value type represents a double-precision 64-bit number with - // values ranging from -1.79769313486232e308 to +1.79769313486232e308 - // values that do not fit into this range are returned as +/-Infinity - if (SafeCastToDouble(this.Numerator) && SafeCastToDouble(this.Denominator)) - { - return (double)this.Numerator / (double)this.Denominator; - } - - // Scale the numerator to preserve the fraction part through the integer division - // We could probably adjust this to make it less precise if need be. - BigInteger denormalized = (this.Numerator * DoublePrecision) / this.Denominator; - if (denormalized.IsZero) - { - // underflow to -+0 - return (this.Numerator.Sign < 0) ? BitConverter.Int64BitsToDouble(unchecked((long)0x8000000000000000)) : 0d; - } - - double result = 0; - bool isDouble = false; - int scale = DoubleMaxScale; - - while (scale > 0) - { - if (!isDouble) - { - if (SafeCastToDouble(denormalized)) - { - result = (double)denormalized; - isDouble = true; - } - else - { - denormalized = denormalized / 10; - } - } - - result = result / 10; - scale--; - } - - if (!isDouble) - { - return (this.Numerator.Sign < 0) ? double.NegativeInfinity : double.PositiveInfinity; - } - else - { - return result; - } + get; + private set; } - /// + /// + /// Determines whether the specified is equal to this . + /// + ///The to compare this with. public override bool Equals(object obj) { if (obj is Rational) - { - return this.Equals((Rational)obj); - } + return Equals((Rational)obj); return false; } - /// + /// + /// Determines whether the specified is equal to this . + /// + ///The to compare this with. public bool Equals(Rational other) { - // Standard: a/b = c/d - if (this.Denominator == other.Denominator) - { - return this.Numerator == other.Numerator; - } - - // Indeterminate - if (this.Numerator == BigInteger.Zero && this.Denominator == BigInteger.Zero) - { - return other.Numerator == BigInteger.Zero && other.Denominator == BigInteger.Zero; - } - - if (other.Numerator == BigInteger.Zero && other.Denominator == BigInteger.Zero) - { - return this.Numerator == BigInteger.Zero && this.Denominator == BigInteger.Zero; - } + BigRational left = new BigRational(Numerator, Denominator); + BigRational right = new BigRational(other.Numerator, other.Denominator); - // ad = bc - return (this.Numerator * other.Denominator) == (this.Denominator * other.Numerator); - } - - /// - public override int GetHashCode() - { - return this.GetHashCode(this); + return left.Equals(right); } - /// - public override string ToString() + /// + /// Converts the specified to an instance of this type. + /// + ///The to convert to an instance of this type. + public static Rational FromDouble(double value) { - return this.ToString(CultureInfo.InvariantCulture); + return new Rational(value, false); } - /// - /// 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) + /// + /// Converts the specified to an instance of this type. + /// + ///The to convert to an instance of this type. + ///Specifies if the instance should be created with the best precision possible. + public static Rational FromDouble(double value, bool bestPrecision) { - if (this.IsIndeterminate) - { - return "[ Indeterminate ]"; - } - - if (this.IsPositiveInfinity) - { - return "[ PositiveInfinity ]"; - } - - if (this.IsNegativeInfinity) - { - return "[ NegativeInfinity ]"; - } - - if (this.IsZero) - { - return "0"; - } - - 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(); + return new Rational(value, bestPrecision); } - /// - /// Simplifies the rational. - /// - private void Simplify() + /// + /// Serves as a hash of this type. + /// + public override int GetHashCode() { - if (this.IsIndeterminate) - { - return; - } - - if (this.IsNegativeInfinity) - { - return; - } - - if (this.IsPositiveInfinity) - { - return; - } - - if (this.IsInteger) - { - return; - } - - if (this.Numerator == BigInteger.Zero) - { - this.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; - } + BigRational self = new BigRational(Numerator, Denominator); + return self.GetHashCode(); } /// - /// Converts the string representation of a number into its rational value + /// Converts a rational number to the nearest . /// - /// A string that contains a number to convert. - /// The - internal static Rational Parse(string value) + /// + /// The . + /// + public double ToDouble() { - int periodIndex = value.IndexOf(".", StringComparison.Ordinal); - int eIndex = value.IndexOf("E", StringComparison.Ordinal); - int slashIndex = value.IndexOf("/", StringComparison.Ordinal); - - // An integer such as 3 - if (periodIndex == -1 && eIndex == -1 && slashIndex == -1) - { - return new Rational(BigInteger.Parse(value)); - } - - // A fraction such as 22/7 - if (periodIndex == -1 && eIndex == -1 && slashIndex != -1) - { - return new Rational(BigInteger.Parse(value.Substring(0, slashIndex)), - BigInteger.Parse(value.Substring(slashIndex + 1))); - } - - // No scientific Notation such as 3.14159 - if (eIndex == -1) - { - BigInteger n = BigInteger.Parse(value.Replace(".", string.Empty)); - BigInteger d = (BigInteger)Math.Pow(10, value.Length - periodIndex - 1); - return new Rational(n, d); - } - - // Scientific notation such as 3.14159E-2 - int characteristic = int.Parse(value.Substring(eIndex + 1)); - BigInteger ten = 10; - BigInteger numerator = BigInteger.Parse(value.Substring(0, eIndex).Replace(".", string.Empty)); - BigInteger denominator = new BigInteger(Math.Pow(10, eIndex - 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); + return Numerator / (double)Denominator; } /// - /// Returns a value indicating whether the given big integer can be - /// safely cast to a double. + /// Converts the numeric value of this instance to its equivalent string representation. /// - /// The value to test. - /// true if the value can be safely cast - private static bool SafeCastToDouble(BigInteger value) + public override string ToString() { - return DoubleMinValue <= value && value <= DoubleMaxValue; + return ToString(CultureInfo.InvariantCulture); } /// - /// Returns the hash code for this instance. + /// Converts the numeric value of this instance to its equivalent string representation using + /// the specified culture-specific format information. /// - /// - /// The instance of to return the hash code for. + /// + /// An object that supplies culture-specific formatting information. /// - /// - /// A 32-bit signed integer that is the hash code for this instance. - /// - private int GetHashCode(Rational rational) + /// + public string ToString(IFormatProvider provider) { - return ((rational.Numerator * 397) ^ rational.Denominator).GetHashCode(); + BigRational rational = new BigRational(Numerator, Denominator); + return rational.ToString(provider); } } } \ No newline at end of file diff --git a/src/ImageProcessorCore/Numerics/SignedRational.cs b/src/ImageProcessorCore/Numerics/SignedRational.cs new file mode 100644 index 0000000000..d216b30d4e --- /dev/null +++ b/src/ImageProcessorCore/Numerics/SignedRational.cs @@ -0,0 +1,199 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore +{ + using System; + using System.Globalization; + + /// + /// 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 SignedRational : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + ///The to convert to an instance of this type. + public SignedRational(double value) + : this(value, false) + { + } + + /// + /// Initializes a new instance of the struct. + /// + ///The to convert to an instance of this type. + ///Specifies if the instance should be created with the best precision possible. + public SignedRational(double value, bool bestPrecision) + { + BigRational rational = new BigRational(value, bestPrecision); + + Numerator = (int)rational.Numerator; + Denominator = (int)rational.Denominator; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The integer to create the rational from. + public SignedRational(int value) + : this(value, 1) + { + } + + /// + /// 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 SignedRational(int numerator, int denominator) + : this(numerator, denominator, true) + { + } + + /// + /// 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. + /// Specified if the rational should be simplified. + public SignedRational(int numerator, int denominator, bool simplify) + { + BigRational rational = new BigRational(numerator, denominator, simplify); + + Numerator = (int)rational.Numerator; + Denominator = (int)rational.Denominator; + } + + /// + /// Determines whether the specified instances are considered equal. + /// + /// The first to compare. + /// The second to compare. + /// + public static bool operator ==(SignedRational left, SignedRational right) + { + return Equals(left, right); + } + + /// + /// Determines whether the specified instances are not considered equal. + /// + /// The first to compare. + /// The second to compare. + /// + public static bool operator !=(SignedRational left, SignedRational right) + { + return !Equals(left, right); + } + + /// + /// Gets the numerator of a number. + /// + public int Numerator + { + get; + private set; + } + + /// + /// Gets the denominator of a number. + /// + public int Denominator + { + get; + private set; + } + + /// + /// Determines whether the specified is equal to this . + /// + ///The to compare this with. + public override bool Equals(object obj) + { + if (obj is SignedRational) + return Equals((SignedRational)obj); + + return false; + } + + /// + /// Determines whether the specified is equal to this . + /// + ///The to compare this with. + public bool Equals(SignedRational other) + { + BigRational left = new BigRational(Numerator, Denominator); + BigRational right = new BigRational(other.Numerator, other.Denominator); + + return left.Equals(right); + } + + /// + /// Converts the specified to an instance of this type. + /// + ///The to convert to an instance of this type. + public static SignedRational FromDouble(double value) + { + return new SignedRational(value, false); + } + + /// + /// Converts the specified to an instance of this type. + /// + ///The to convert to an instance of this type. + ///Specifies if the instance should be created with the best precision possible. + public static SignedRational FromDouble(double value, bool bestPrecision) + { + return new SignedRational(value, bestPrecision); + } + + /// + /// Serves as a hash of this type. + /// + public override int GetHashCode() + { + BigRational self = new BigRational(Numerator, Denominator); + return self.GetHashCode(); + } + + /// + /// Converts a rational number to the nearest . + /// + /// + /// The . + /// + public double ToDouble() + { + return Numerator / (double)Denominator; + } + + /// + /// Converts the numeric value of this instance to its equivalent string representation. + /// + public override string ToString() + { + return 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) + { + BigRational rational = new BigRational(Numerator, Denominator); + return rational.ToString(provider); + } + } +} \ No newline at end of file diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs b/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs index 333a00d0a7..546d5b846b 100644 --- a/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs +++ b/src/ImageProcessorCore/Profiles/Exif/ExifReader.cs @@ -442,13 +442,13 @@ namespace ImageProcessorCore { if (!this.ValidateArray(data, 8, 4)) { - return Rational.Zero; + return new Rational(); } uint numerator = BitConverter.ToUInt32(data, 0); uint denominator = BitConverter.ToUInt32(data, 4); - return new Rational(numerator, denominator); + return new Rational(numerator, denominator, false); } private sbyte ToSignedByte(byte[] data) @@ -466,17 +466,17 @@ namespace ImageProcessorCore return BitConverter.ToInt32(data, 0); } - private Rational ToSignedRational(byte[] data) + private SignedRational ToSignedRational(byte[] data) { if (!this.ValidateArray(data, 8, 4)) { - return Rational.Zero; + return new SignedRational(); } int numerator = BitConverter.ToInt32(data, 0); int denominator = BitConverter.ToInt32(data, 4); - return new Rational(numerator, denominator); + return new SignedRational(numerator, denominator, false); } private short ToSignedShort(byte[] data) diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs b/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs index 3fd195b4ea..a79bc85efc 100644 --- a/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs +++ b/src/ImageProcessorCore/Profiles/Exif/ExifValue.cs @@ -539,13 +539,12 @@ namespace ImageProcessorCore case ExifDataType.DoubleFloat: Guard.IsTrue(type == typeof(double), nameof(value), $"Value should be a double{(this.IsArray ? " array." : ".")}"); break; - case ExifDataType.Rational: - case ExifDataType.SignedRational: - Guard.IsTrue(type == typeof(Rational), nameof(value), $"Value should be a Rational{(this.IsArray ? " array." : ".")}"); - break; case ExifDataType.Long: Guard.IsTrue(type == typeof(uint), nameof(value), $"Value should be an unsigned int{(this.IsArray ? " array." : ".")}"); break; + case ExifDataType.Rational: + Guard.IsTrue(type == typeof(Rational), nameof(value), $"Value should be a Rational{(this.IsArray ? " array." : ".")}"); + break; case ExifDataType.Short: Guard.IsTrue(type == typeof(ushort), nameof(value), $"Value should be an unsigned short{(this.IsArray ? " array." : ".")}"); break; @@ -555,6 +554,9 @@ namespace ImageProcessorCore case ExifDataType.SignedLong: Guard.IsTrue(type == typeof(int), nameof(value), $"Value should be an int{(this.IsArray ? " array." : ".")}"); break; + case ExifDataType.SignedRational: + Guard.IsTrue(type == typeof(SignedRational), nameof(value), $"Value should be a SignedRational{(this.IsArray ? " array." : ".")}"); + break; case ExifDataType.SignedShort: Guard.IsTrue(type == typeof(short), nameof(value), $"Value should be a short{(this.IsArray ? " array." : ".")}"); break; diff --git a/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs b/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs index 269ce52c4b..e89b0d1a91 100644 --- a/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs +++ b/src/ImageProcessorCore/Profiles/Exif/ExifWriter.cs @@ -329,17 +329,16 @@ namespace ImageProcessorCore private int WriteRational(Rational value, byte[] destination, int offset) { - // Ensure no overflow - Write(BitConverter.GetBytes((uint)(value.Numerator * (value.ToDouble() < 0.0 ? -1 : 1))), destination, offset); - Write(BitConverter.GetBytes((uint)value.Denominator), destination, offset + 4); + Write(BitConverter.GetBytes(value.Numerator), destination, offset); + Write(BitConverter.GetBytes(value.Denominator), destination, offset + 4); return offset + 8; } - private int WriteSignedRational(Rational value, byte[] destination, int offset) + private int WriteSignedRational(SignedRational value, byte[] destination, int offset) { - Write(BitConverter.GetBytes((int)value.Numerator), destination, offset); - Write(BitConverter.GetBytes((int)value.Denominator), destination, offset + 4); + Write(BitConverter.GetBytes(value.Numerator), destination, offset); + Write(BitConverter.GetBytes(value.Denominator), destination, offset + 4); return offset + 8; } @@ -370,7 +369,7 @@ namespace ImageProcessorCore case ExifDataType.SignedShort: return Write(BitConverter.GetBytes((short)value), destination, offset); case ExifDataType.SignedRational: - return this.WriteSignedRational((Rational)value, destination, offset); + return this.WriteSignedRational((SignedRational)value, destination, offset); case ExifDataType.SingleFloat: return Write(BitConverter.GetBytes((float)value), destination, offset); default: diff --git a/tests/ImageProcessorCore.Tests/Numerics/RationalTests.cs b/tests/ImageProcessorCore.Tests/Numerics/RationalTests.cs index 246349f82b..e83ec5118e 100644 --- a/tests/ImageProcessorCore.Tests/Numerics/RationalTests.cs +++ b/tests/ImageProcessorCore.Tests/Numerics/RationalTests.cs @@ -22,6 +22,7 @@ namespace ImageProcessorCore.Tests Rational r2 = new Rational(3, 2); Assert.Equal(r1, r2); + Assert.True(r1 == r2); Rational r3 = new Rational(7.55); Rational r4 = new Rational(755, 100); @@ -41,6 +42,7 @@ namespace ImageProcessorCore.Tests Rational second = new Rational(100, 100); Assert.NotEqual(first, second); + Assert.True(first != second); } /// @@ -49,9 +51,65 @@ namespace ImageProcessorCore.Tests [Fact] public void ConstructorAssignsProperties() { - Rational first = new Rational(4, 5); - Assert.Equal(4, first.Numerator); - Assert.Equal(5, first.Denominator); + Rational rational = new Rational(7, 55); + Assert.Equal(7U, rational.Numerator); + Assert.Equal(55U, rational.Denominator); + + rational = new Rational(755, 100); + Assert.Equal(151U, rational.Numerator); + Assert.Equal(20U, rational.Denominator); + + rational = new Rational(755, 100, false); + Assert.Equal(755U, rational.Numerator); + Assert.Equal(100U, rational.Denominator); + + rational = new Rational(-7.55); + Assert.Equal(151U, rational.Numerator); + Assert.Equal(20U, rational.Denominator); + + rational = new Rational(7); + Assert.Equal(7U, rational.Numerator); + Assert.Equal(1U, rational.Denominator); + } + + [Fact] + public void Fraction() + { + Rational first = new Rational(1.0 / 1600); + Rational second = new Rational(1.0 / 1600, true); + Assert.False(first.Equals(second)); + } + + [Fact] + public void ToDouble() + { + Rational rational = new Rational(0, 0); + Assert.Equal(double.NaN, rational.ToDouble()); + + rational = new Rational(2, 0); + Assert.Equal(double.PositiveInfinity, rational.ToDouble()); + } + + [Fact] + public void ToStringRepresention() + { + Rational rational = new Rational(0, 0); + Assert.Equal("[ Indeterminate ]", rational.ToString()); + + rational = new Rational(double.PositiveInfinity); + Assert.Equal("[ PositiveInfinity ]", rational.ToString()); + + rational = new Rational(double.NegativeInfinity); + Assert.Equal("[ PositiveInfinity ]", rational.ToString()); + + rational = new Rational(0, 1); + Assert.Equal("0", rational.ToString()); + + rational = new Rational(2, 1); + Assert.Equal("2", rational.ToString()); + + rational = new Rational(1, 2); + Assert.Equal("1/2", rational.ToString()); } } } \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/Numerics/SignedRationalTests.cs b/tests/ImageProcessorCore.Tests/Numerics/SignedRationalTests.cs new file mode 100644 index 0000000000..fff7f0fd34 --- /dev/null +++ b/tests/ImageProcessorCore.Tests/Numerics/SignedRationalTests.cs @@ -0,0 +1,122 @@ +// +// Copyright (c) James Jackson-South and contributors. +// Licensed under the Apache License, Version 2.0. +// + +namespace ImageProcessorCore.Tests +{ + using Xunit; + + /// + /// Tests the struct. + /// + public class SignedRationalTests + { + /// + /// Tests the equality operators for equality. + /// + [Fact] + public void AreEqual() + { + SignedRational r1 = new SignedRational(3, 2); + SignedRational r2 = new SignedRational(3, 2); + + Assert.Equal(r1, r2); + Assert.True(r1 == r2); + + SignedRational r3 = new SignedRational(7.55); + SignedRational r4 = new SignedRational(755, 100); + SignedRational r5 = new SignedRational(151, 20); + + Assert.Equal(r3, r4); + Assert.Equal(r4, r5); + } + + /// + /// Tests the equality operators for inequality. + /// + [Fact] + public void AreNotEqual() + { + SignedRational first = new SignedRational(0, 100); + SignedRational second = new SignedRational(100, 100); + + Assert.NotEqual(first, second); + Assert.True(first != second); + } + + /// + /// Tests whether the Rational constructor correctly assign properties. + /// + [Fact] + public void ConstructorAssignsProperties() + { + SignedRational rational = new SignedRational(7, -55); + Assert.Equal(7, rational.Numerator); + Assert.Equal(-55, rational.Denominator); + + rational = new SignedRational(-755, 100); + Assert.Equal(-151, rational.Numerator); + Assert.Equal(20, rational.Denominator); + + rational = new SignedRational(-755, -100, false); + Assert.Equal(-755, rational.Numerator); + Assert.Equal(-100, rational.Denominator); + + rational = new SignedRational(-151, -20); + Assert.Equal(-151, rational.Numerator); + Assert.Equal(-20, rational.Denominator); + + rational = new SignedRational(-7.55); + Assert.Equal(-151, rational.Numerator); + Assert.Equal(20, rational.Denominator); + + rational = new SignedRational(7); + Assert.Equal(7, rational.Numerator); + Assert.Equal(1, rational.Denominator); + } + + [Fact] + public void Fraction() + { + SignedRational first = new SignedRational(1.0 / 1600); + SignedRational second = new SignedRational(1.0 / 1600, true); + Assert.False(first.Equals(second)); + } + + [Fact] + public void ToDouble() + { + SignedRational rational = new SignedRational(0, 0); + Assert.Equal(double.NaN, rational.ToDouble()); + + rational = new SignedRational(2, 0); + Assert.Equal(double.PositiveInfinity, rational.ToDouble()); + + rational = new SignedRational(-2, 0); + Assert.Equal(double.NegativeInfinity, rational.ToDouble()); + } + + [Fact] + public void ToStringRepresention() + { + SignedRational rational = new SignedRational(0, 0); + Assert.Equal("[ Indeterminate ]", rational.ToString()); + + rational = new SignedRational(double.PositiveInfinity); + Assert.Equal("[ PositiveInfinity ]", rational.ToString()); + + rational = new SignedRational(double.NegativeInfinity); + Assert.Equal("[ NegativeInfinity ]", rational.ToString()); + + rational = new SignedRational(0, 1); + Assert.Equal("0", rational.ToString()); + + rational = new SignedRational(2, 1); + Assert.Equal("2", rational.ToString()); + + rational = new SignedRational(1, 2); + Assert.Equal("1/2", rational.ToString()); + } + } +} \ No newline at end of file diff --git a/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs b/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs index 438e903363..988c7747df 100644 --- a/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageProcessorCore.Tests/Profiles/Exif/ExifProfileTests.cs @@ -64,12 +64,11 @@ namespace ImageProcessorCore.Tests { using (MemoryStream memStream = new MemoryStream()) { - // double exposureTime = 1.0 / 1600; - Rational exposureTime = new Rational(1, 1600); + double exposureTime = 1.0 / 1600; ExifProfile profile = GetExifProfile(); - profile.SetValue(ExifTag.ExposureTime, exposureTime); + profile.SetValue(ExifTag.ExposureTime, new Rational(exposureTime)); Image image = new Image(1, 1); image.ExifProfile = profile; @@ -84,12 +83,12 @@ namespace ImageProcessorCore.Tests ExifValue value = profile.GetValue(ExifTag.ExposureTime); Assert.NotNull(value); - Assert.Equal(exposureTime, value.Value); + Assert.NotEqual(exposureTime, ((Rational)value.Value).ToDouble()); memStream.Position = 0; profile = GetExifProfile(); - profile.SetValue(ExifTag.ExposureTime, exposureTime); + profile.SetValue(ExifTag.ExposureTime, new Rational(exposureTime, true)); image.ExifProfile = profile; image.SaveAsJpeg(memStream); @@ -101,7 +100,7 @@ namespace ImageProcessorCore.Tests Assert.NotNull(profile); value = profile.GetValue(ExifTag.ExposureTime); - TestValue(value, exposureTime); + Assert.Equal(exposureTime, ((Rational)value.Value).ToDouble()); } } @@ -111,104 +110,29 @@ namespace ImageProcessorCore.Tests using (FileStream stream = File.OpenRead(TestImages.Jpg.Floorplan)) { Image image = new Image(stream); - image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, Rational.PositiveInfinity); + image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, new SignedRational(double.PositiveInfinity)); image = WriteAndRead(image); ExifValue value = image.ExifProfile.GetValue(ExifTag.ExposureBiasValue); Assert.NotNull(value); - Assert.Equal(Rational.PositiveInfinity, value.Value); + Assert.Equal(new SignedRational(double.PositiveInfinity), value.Value); - image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, Rational.NegativeInfinity); + image.ExifProfile.SetValue(ExifTag.ExposureBiasValue, new SignedRational(double.NegativeInfinity)); image = WriteAndRead(image); value = image.ExifProfile.GetValue(ExifTag.ExposureBiasValue); Assert.NotNull(value); - Assert.Equal(Rational.NegativeInfinity, value.Value); + Assert.Equal(new SignedRational(double.NegativeInfinity), value.Value); - image.ExifProfile.SetValue(ExifTag.FlashEnergy, Rational.NegativeInfinity); + image.ExifProfile.SetValue(ExifTag.FlashEnergy, new Rational(double.NegativeInfinity)); image = WriteAndRead(image); value = image.ExifProfile.GetValue(ExifTag.FlashEnergy); Assert.NotNull(value); - Assert.Equal(Rational.PositiveInfinity, value.Value); + Assert.Equal(new Rational(double.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() { @@ -224,11 +148,11 @@ namespace ImageProcessorCore.Tests Assert.Throws(() => { value.Value = 15; }); - image.ExifProfile.SetValue(ExifTag.ShutterSpeedValue, new Rational(75.55)); + image.ExifProfile.SetValue(ExifTag.ShutterSpeedValue, new SignedRational(75.55)); value = image.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); - TestValue(value, new Rational(7555, 100)); + TestValue(value, new SignedRational(7555, 100)); Assert.Throws(() => { value.Value = 75; }); @@ -258,7 +182,7 @@ namespace ImageProcessorCore.Tests TestValue(value, "ImageProcessorCore"); value = image.ExifProfile.GetValue(ExifTag.ShutterSpeedValue); - TestValue(value, new Rational(75.55)); + TestValue(value, new SignedRational(75.55)); value = image.ExifProfile.GetValue(ExifTag.XResolution); TestValue(value, new Rational(150.0)); @@ -372,6 +296,12 @@ namespace ImageProcessorCore.Tests Assert.Equal(expected, value.Value); } + private static void TestValue(ExifValue value, SignedRational expected) + { + Assert.NotNull(value); + Assert.Equal(expected, value.Value); + } + private static void TestValue(ExifValue value, Rational[] expected) { Assert.NotNull(value);