From dcf75fd081b6b0792069fa7a88c4cccf0b8ee626 Mon Sep 17 00:00:00 2001 From: Killy Date: Thu, 30 Apr 2020 17:57:36 +0300 Subject: [PATCH] Use Fractions to represent zoom factor The goal is to be able to get as close as possible to perfect 66.(6)% (2/3) zoom factor, and also remove types mismatch between the editor form and the surface. --- Greenshot/Drawing/Surface.cs | 13 +- Greenshot/Forms/ImageEditorForm.Designer.cs | 18 +-- Greenshot/Forms/ImageEditorForm.cs | 24 ++-- GreenshotPlugin/Core/Fraction.cs | 152 ++++++++++++++++++++ GreenshotPlugin/Interfaces/ISurface.cs | 5 +- 5 files changed, 183 insertions(+), 29 deletions(-) create mode 100644 GreenshotPlugin/Core/Fraction.cs diff --git a/Greenshot/Drawing/Surface.cs b/Greenshot/Drawing/Surface.cs index 339e4b02c..811466cf7 100644 --- a/Greenshot/Drawing/Surface.cs +++ b/Greenshot/Drawing/Surface.cs @@ -307,15 +307,16 @@ namespace Greenshot.Drawing [NonSerialized] private Matrix _inverseZoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); [NonSerialized] - private float _zoomFactor = 1.0f; - public float ZoomFactor + private Fraction _zoomFactor = Fraction.Identity; + public Fraction ZoomFactor { get => _zoomFactor; set { _zoomFactor = value; + var inverse = _zoomFactor.Inverse(); _zoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); - _inverseZoomMatrix = new Matrix(1f / _zoomFactor, 0, 0, 1f / _zoomFactor, 0, 0); + _inverseZoomMatrix = new Matrix(inverse, 0, 0, inverse, 0, 0); UpdateSize(); } } @@ -1439,9 +1440,9 @@ namespace Greenshot.Drawing LOG.Debug("Empty cliprectangle??"); return; } - Rectangle imageClipRectangle = ZoomClipRectangle(targetClipRectangle, 1.0 / _zoomFactor, 2); + Rectangle imageClipRectangle = ZoomClipRectangle(targetClipRectangle, _zoomFactor.Inverse(), 2); - bool isZoomedIn = ZoomFactor > 1f; + bool isZoomedIn = _zoomFactor > Fraction.Identity; if (_elements.HasIntersectingFilters(imageClipRectangle) || isZoomedIn) { if (_buffer != null) @@ -1467,7 +1468,7 @@ namespace Greenshot.Drawing //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; DrawBackground(graphics, imageClipRectangle); graphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); - graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), 1.0 / _zoomFactor, 2)); + graphics.SetClip(ZoomClipRectangle(Rectangle.Round(targetGraphics.ClipBounds), _zoomFactor.Inverse(), 2)); _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle); } targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); diff --git a/Greenshot/Forms/ImageEditorForm.Designer.cs b/Greenshot/Forms/ImageEditorForm.Designer.cs index 606b72773..04da3d0a0 100644 --- a/Greenshot/Forms/ImageEditorForm.Designer.cs +++ b/Greenshot/Forms/ImageEditorForm.Designer.cs @@ -1718,7 +1718,7 @@ namespace Greenshot { // this.zoom25MenuItem.Name = "zoom25MenuItem"; this.zoom25MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom25MenuItem.Tag = "25"; + this.zoom25MenuItem.Tag = "1/4"; this.zoom25MenuItem.Text = "25%"; this.zoom25MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1726,7 +1726,7 @@ namespace Greenshot { // this.zoom50MenuItem.Name = "zoom50MenuItem"; this.zoom50MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom50MenuItem.Tag = "50"; + this.zoom50MenuItem.Tag = "1/2"; this.zoom50MenuItem.Text = "50%"; this.zoom50MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1734,7 +1734,7 @@ namespace Greenshot { // this.zoom66MenuItem.Name = "zoom66MenuItem"; this.zoom66MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom66MenuItem.Tag = "66"; + this.zoom66MenuItem.Tag = "2/3"; this.zoom66MenuItem.Text = "66%"; this.zoom66MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1742,7 +1742,7 @@ namespace Greenshot { // this.zoom75MenuItem.Name = "zoom75MenuItem"; this.zoom75MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom75MenuItem.Tag = "75"; + this.zoom75MenuItem.Tag = "3/4"; this.zoom75MenuItem.Text = "75%"; this.zoom75MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1757,7 +1757,7 @@ namespace Greenshot { this.zoomActualSizeMenuItem.Name = "zoomActualSizeMenuItem"; this.zoomActualSizeMenuItem.ShortcutKeyDisplayString = "Ctrl+0"; this.zoomActualSizeMenuItem.Size = new System.Drawing.Size(209, 22); - this.zoomActualSizeMenuItem.Tag = "100"; + this.zoomActualSizeMenuItem.Tag = "1/1"; this.zoomActualSizeMenuItem.Text = "100% - Actual Size"; this.zoomActualSizeMenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1770,7 +1770,7 @@ namespace Greenshot { // this.zoom200MenuItem.Name = "zoom200MenuItem"; this.zoom200MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom200MenuItem.Tag = "200"; + this.zoom200MenuItem.Tag = "2/1"; this.zoom200MenuItem.Text = "200%"; this.zoom200MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1778,7 +1778,7 @@ namespace Greenshot { // this.zoom300MenuItem.Name = "zoom300MenuItem"; this.zoom300MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom300MenuItem.Tag = "300"; + this.zoom300MenuItem.Tag = "3/1"; this.zoom300MenuItem.Text = "300%"; this.zoom300MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1786,7 +1786,7 @@ namespace Greenshot { // this.zoom400MenuItem.Name = "zoom400MenuItem"; this.zoom400MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom400MenuItem.Tag = "400"; + this.zoom400MenuItem.Tag = "4/1"; this.zoom400MenuItem.Text = "400%"; this.zoom400MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // @@ -1794,7 +1794,7 @@ namespace Greenshot { // this.zoom600MenuItem.Name = "zoom600MenuItem"; this.zoom600MenuItem.Size = new System.Drawing.Size(209, 22); - this.zoom600MenuItem.Tag = "600"; + this.zoom600MenuItem.Tag = "6/1"; this.zoom600MenuItem.Text = "600%"; this.zoom600MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); // diff --git a/Greenshot/Forms/ImageEditorForm.cs b/Greenshot/Forms/ImageEditorForm.cs index ca0cb1a7b..0b5c40d42 100644 --- a/Greenshot/Forms/ImageEditorForm.cs +++ b/Greenshot/Forms/ImageEditorForm.cs @@ -69,8 +69,7 @@ namespace Greenshot { /// /// All provided zoom values (in percents) in ascending order. /// - private readonly int[] ZOOM_VALUES = new[] { 25, 50, 66, 75, 100, 200, 300, 400, 600 }; - private int _zoomValue = 100; + private readonly Fraction[] ZOOM_VALUES = new Fraction[] { (1, 4), (1, 2), (2, 3), (3, 4), (1 ,1), (2, 1), (3, 1), (4, 1), (6, 1) }; /// /// An Implementation for the IImageEditor, this way Plugins have access to the HWND handles wich can be used with Win32 API calls. @@ -1533,14 +1532,16 @@ namespace Greenshot { } private void ZoomInMenuItemClick(object sender, EventArgs e) { - var nextIndex = Array.FindIndex(ZOOM_VALUES, v => v > _zoomValue); + var zoomValue = Surface.ZoomFactor; + var nextIndex = Array.FindIndex(ZOOM_VALUES, v => v > zoomValue); var nextValue = nextIndex < 0 ? ZOOM_VALUES[ZOOM_VALUES.Length - 1] : ZOOM_VALUES[nextIndex]; ZoomSetValue(nextValue); } private void ZoomOutMenuItemClick(object sender, EventArgs e) { - var nextIndex = Array.FindLastIndex(ZOOM_VALUES, v => v < _zoomValue); + var zoomValue = Surface.ZoomFactor; + var nextIndex = Array.FindLastIndex(ZOOM_VALUES, v => v < zoomValue); var nextValue = nextIndex < 0 ? ZOOM_VALUES[0] : ZOOM_VALUES[nextIndex]; ZoomSetValue(nextValue); @@ -1548,7 +1549,7 @@ namespace Greenshot { private void ZoomSetValueMenuItemClick(object sender, EventArgs e) { var senderMenuItem = (ToolStripMenuItem)sender; - int zoomPercent = int.Parse((string)senderMenuItem.Tag); + var zoomPercent = Fraction.Parse((string)senderMenuItem.Tag); ZoomSetValue(zoomPercent); } @@ -1559,8 +1560,8 @@ namespace Greenshot { var maxImageSize = maxWindowSize - chromeSize; var imageSize = Surface.Image.Size; - static bool isFit(int zoom, int source, int boundary) - => source * zoom / 100 <= boundary; + static bool isFit(Fraction scale, int source, int boundary) + => (int)(source * scale) <= boundary; var nextIndex = Array.FindLastIndex( ZOOM_VALUES, @@ -1572,7 +1573,7 @@ namespace Greenshot { ZoomSetValue(nextValue); } - private void ZoomSetValue(int value) { + private void ZoomSetValue(Fraction value) { var surface = Surface as Surface; var panel = surface?.Parent as Panel; if (panel == null) @@ -1596,14 +1597,13 @@ namespace Greenshot { } // Set the new zoom value - _zoomValue = value; - Surface.ZoomFactor = 1f * value / 100; + Surface.ZoomFactor = value; Size = GetOptimalWindowSize(); AlignCanvasPositionAfterResize(); // Update zoom controls - string valueString = value.ToString(); - zoomStatusDropDownBtn.Text = valueString + "%"; + zoomStatusDropDownBtn.Text = ((int)(100 * (double)value)).ToString() + "%"; + var valueString = value.ToString(); foreach (var item in zoomMenuStrip.Items) { if (item is ToolStripMenuItem menuItem) { menuItem.Checked = menuItem.Tag as string == valueString; diff --git a/GreenshotPlugin/Core/Fraction.cs b/GreenshotPlugin/Core/Fraction.cs new file mode 100644 index 000000000..312b91bb8 --- /dev/null +++ b/GreenshotPlugin/Core/Fraction.cs @@ -0,0 +1,152 @@ +using System; +using System.Text.RegularExpressions; + +namespace GreenshotPlugin.Core +{ + /// + /// Basic Fraction (Rational) numbers with features only needed to represent scale factors. + /// + public readonly struct Fraction : IEquatable, IComparable + { + public static Fraction Identity { get; } = new Fraction(1, 1); + + public uint Numerator { get; } + public uint Denominator { get; } + + public Fraction(uint numerator, uint denominator) + { + if (denominator == 0) + { + throw new ArgumentException("Can't divide by zero.", nameof(denominator)); + } + if (numerator == 0) + { + throw new ArgumentException("Zero is not supported by this implementation.", nameof(numerator)); + } + var gcd = GreatestCommonDivisor(numerator, denominator); + Numerator = numerator / gcd; + Denominator = denominator / gcd; + } + + public Fraction Inverse() + => new Fraction(Denominator, Numerator); + + #region Parse + + private static readonly Regex PARSE_REGEX = new Regex(@"^([1-9][0-9]*)\/([1-9][0-9]*)$", RegexOptions.Compiled); + public static bool TryParse(string str, out Fraction result) + { + var match = PARSE_REGEX.Match(str); + if (!match.Success) + { + result = Identity; + return false; + } + var numerator = uint.Parse(match.Groups[1].Value); + var denominator = uint.Parse(match.Groups[2].Value); + result = new Fraction(numerator, denominator); + return true; + } + + public static Fraction Parse(string str) + => TryParse(str, out var result) + ? result + : throw new ArgumentException($"Could not parse the input \"{str}\".", nameof(str)); + + #endregion + + #region Overrides, interface implementations + + public override string ToString() + => $"{Numerator}/{Denominator}"; + + public override bool Equals(object obj) + => obj is Fraction fraction && Equals(fraction); + + public bool Equals(Fraction other) + => Numerator == other.Numerator && Denominator == other.Denominator; + + public override int GetHashCode() + { + unchecked + { + int hashCode = -1534900553; + hashCode = hashCode * -1521134295 + Numerator.GetHashCode(); + hashCode = hashCode * -1521134295 + Denominator.GetHashCode(); + return hashCode; + } + } + + public int CompareTo(Fraction other) + => (int)(Numerator * other.Denominator) - (int)(other.Numerator * Denominator); + + #endregion + + #region Equality operators + + public static bool operator ==(Fraction left, Fraction right) + => left.Equals(right); + + public static bool operator !=(Fraction left, Fraction right) + => !(left == right); + + #endregion + + #region Comparison operators + + public static bool operator <(Fraction left, Fraction right) + => left.CompareTo(right) < 0; + + public static bool operator <=(Fraction left, Fraction right) + => left.CompareTo(right) <= 0; + + public static bool operator >(Fraction left, Fraction right) + => left.CompareTo(right) > 0; + + public static bool operator >=(Fraction left, Fraction right) + => left.CompareTo(right) >= 0; + + #endregion + + #region Scale operators + + public static Fraction operator *(Fraction left, Fraction right) + => new Fraction(left.Numerator * right.Numerator, left.Denominator * right.Denominator); + + public static Fraction operator *(Fraction left, uint right) + => new Fraction(left.Numerator * right, left.Denominator); + + public static Fraction operator *(uint left, Fraction right) + => new Fraction(left * right.Numerator, right.Denominator); + + public static Fraction operator /(Fraction left, Fraction right) + => new Fraction(left.Numerator * right.Denominator, left.Denominator * right.Numerator); + + public static Fraction operator /(Fraction left, uint right) + => new Fraction(left.Numerator, left.Denominator * right); + + public static Fraction operator /(uint left, Fraction right) + => new Fraction(left * right.Denominator, right.Numerator); + + #endregion + + #region Type conversion operators + + public static implicit operator double(Fraction fraction) + => 1.0 * fraction.Numerator / fraction.Denominator; + + public static implicit operator float(Fraction fraction) + => 1.0f * fraction.Numerator / fraction.Denominator; + + public static implicit operator Fraction(uint number) + => new Fraction(number, 1u); + + public static implicit operator Fraction((uint numerator, uint demoninator) tuple) + => new Fraction(tuple.numerator, tuple.demoninator); + + #endregion + + private static uint GreatestCommonDivisor(uint a, uint b) + => (b != 0) ? GreatestCommonDivisor(b, a % b) : a; + } +} diff --git a/GreenshotPlugin/Interfaces/ISurface.cs b/GreenshotPlugin/Interfaces/ISurface.cs index 73b31b05d..068ab0f29 100644 --- a/GreenshotPlugin/Interfaces/ISurface.cs +++ b/GreenshotPlugin/Interfaces/ISurface.cs @@ -23,6 +23,7 @@ using System; using System.Drawing; using System.IO; using System.Windows.Forms; +using GreenshotPlugin.Core; using GreenshotPlugin.Effects; using GreenshotPlugin.Interfaces.Drawing; @@ -193,9 +194,9 @@ namespace GreenshotPlugin.Interfaces } /// - /// Zoom value applied to the surface. 1.0f for actual size (100%). + /// Zoom value applied to the surface. /// - float ZoomFactor { get; set; } + Fraction ZoomFactor { get; set; } /// /// Translate a point from image coorditate space to surface coordinate space. ///