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.
This commit is contained in:
Killy 2020-04-30 17:57:36 +03:00
commit dcf75fd081
5 changed files with 183 additions and 29 deletions

View file

@ -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);

View file

@ -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);
//

View file

@ -69,8 +69,7 @@ namespace Greenshot {
/// <summary>
/// All provided zoom values (in percents) in ascending order.
/// </summary>
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) };
/// <summary>
/// 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;

View file

@ -0,0 +1,152 @@
using System;
using System.Text.RegularExpressions;
namespace GreenshotPlugin.Core
{
/// <summary>
/// Basic Fraction (Rational) numbers with features only needed to represent scale factors.
/// </summary>
public readonly struct Fraction : IEquatable<Fraction>, IComparable<Fraction>
{
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;
}
}

View file

@ -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
}
/// <summary>
/// Zoom value applied to the surface. 1.0f for actual size (100%).
/// Zoom value applied to the surface.
/// </summary>
float ZoomFactor { get; set; }
Fraction ZoomFactor { get; set; }
/// <summary>
/// Translate a point from image coorditate space to surface coordinate space.
/// </summary>