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] [NonSerialized]
private Matrix _inverseZoomMatrix = new Matrix(1, 0, 0, 1, 0, 0); private Matrix _inverseZoomMatrix = new Matrix(1, 0, 0, 1, 0, 0);
[NonSerialized] [NonSerialized]
private float _zoomFactor = 1.0f; private Fraction _zoomFactor = Fraction.Identity;
public float ZoomFactor public Fraction ZoomFactor
{ {
get => _zoomFactor; get => _zoomFactor;
set set
{ {
_zoomFactor = value; _zoomFactor = value;
var inverse = _zoomFactor.Inverse();
_zoomMatrix = new Matrix(_zoomFactor, 0, 0, _zoomFactor, 0, 0); _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(); UpdateSize();
} }
} }
@ -1439,9 +1440,9 @@ namespace Greenshot.Drawing
LOG.Debug("Empty cliprectangle??"); LOG.Debug("Empty cliprectangle??");
return; 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 (_elements.HasIntersectingFilters(imageClipRectangle) || isZoomedIn)
{ {
if (_buffer != null) if (_buffer != null)
@ -1467,7 +1468,7 @@ namespace Greenshot.Drawing
//graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; //graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
DrawBackground(graphics, imageClipRectangle); DrawBackground(graphics, imageClipRectangle);
graphics.DrawImage(Image, imageClipRectangle, imageClipRectangle, GraphicsUnit.Pixel); 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); _elements.Draw(graphics, _buffer, RenderMode.EDIT, imageClipRectangle);
} }
targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor); targetGraphics.ScaleTransform(_zoomFactor, _zoomFactor);

View file

@ -1718,7 +1718,7 @@ namespace Greenshot {
// //
this.zoom25MenuItem.Name = "zoom25MenuItem"; this.zoom25MenuItem.Name = "zoom25MenuItem";
this.zoom25MenuItem.Size = new System.Drawing.Size(209, 22); this.zoom25MenuItem.Size = new System.Drawing.Size(209, 22);
this.zoom25MenuItem.Tag = "25"; this.zoom25MenuItem.Tag = "1/4";
this.zoom25MenuItem.Text = "25%"; this.zoom25MenuItem.Text = "25%";
this.zoom25MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); this.zoom25MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick);
// //
@ -1726,7 +1726,7 @@ namespace Greenshot {
// //
this.zoom50MenuItem.Name = "zoom50MenuItem"; this.zoom50MenuItem.Name = "zoom50MenuItem";
this.zoom50MenuItem.Size = new System.Drawing.Size(209, 22); this.zoom50MenuItem.Size = new System.Drawing.Size(209, 22);
this.zoom50MenuItem.Tag = "50"; this.zoom50MenuItem.Tag = "1/2";
this.zoom50MenuItem.Text = "50%"; this.zoom50MenuItem.Text = "50%";
this.zoom50MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); this.zoom50MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick);
// //
@ -1734,7 +1734,7 @@ namespace Greenshot {
// //
this.zoom66MenuItem.Name = "zoom66MenuItem"; this.zoom66MenuItem.Name = "zoom66MenuItem";
this.zoom66MenuItem.Size = new System.Drawing.Size(209, 22); this.zoom66MenuItem.Size = new System.Drawing.Size(209, 22);
this.zoom66MenuItem.Tag = "66"; this.zoom66MenuItem.Tag = "2/3";
this.zoom66MenuItem.Text = "66%"; this.zoom66MenuItem.Text = "66%";
this.zoom66MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); this.zoom66MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick);
// //
@ -1742,7 +1742,7 @@ namespace Greenshot {
// //
this.zoom75MenuItem.Name = "zoom75MenuItem"; this.zoom75MenuItem.Name = "zoom75MenuItem";
this.zoom75MenuItem.Size = new System.Drawing.Size(209, 22); this.zoom75MenuItem.Size = new System.Drawing.Size(209, 22);
this.zoom75MenuItem.Tag = "75"; this.zoom75MenuItem.Tag = "3/4";
this.zoom75MenuItem.Text = "75%"; this.zoom75MenuItem.Text = "75%";
this.zoom75MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); this.zoom75MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick);
// //
@ -1757,7 +1757,7 @@ namespace Greenshot {
this.zoomActualSizeMenuItem.Name = "zoomActualSizeMenuItem"; this.zoomActualSizeMenuItem.Name = "zoomActualSizeMenuItem";
this.zoomActualSizeMenuItem.ShortcutKeyDisplayString = "Ctrl+0"; this.zoomActualSizeMenuItem.ShortcutKeyDisplayString = "Ctrl+0";
this.zoomActualSizeMenuItem.Size = new System.Drawing.Size(209, 22); 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.Text = "100% - Actual Size";
this.zoomActualSizeMenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); this.zoomActualSizeMenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick);
// //
@ -1770,7 +1770,7 @@ namespace Greenshot {
// //
this.zoom200MenuItem.Name = "zoom200MenuItem"; this.zoom200MenuItem.Name = "zoom200MenuItem";
this.zoom200MenuItem.Size = new System.Drawing.Size(209, 22); this.zoom200MenuItem.Size = new System.Drawing.Size(209, 22);
this.zoom200MenuItem.Tag = "200"; this.zoom200MenuItem.Tag = "2/1";
this.zoom200MenuItem.Text = "200%"; this.zoom200MenuItem.Text = "200%";
this.zoom200MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); this.zoom200MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick);
// //
@ -1778,7 +1778,7 @@ namespace Greenshot {
// //
this.zoom300MenuItem.Name = "zoom300MenuItem"; this.zoom300MenuItem.Name = "zoom300MenuItem";
this.zoom300MenuItem.Size = new System.Drawing.Size(209, 22); this.zoom300MenuItem.Size = new System.Drawing.Size(209, 22);
this.zoom300MenuItem.Tag = "300"; this.zoom300MenuItem.Tag = "3/1";
this.zoom300MenuItem.Text = "300%"; this.zoom300MenuItem.Text = "300%";
this.zoom300MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); this.zoom300MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick);
// //
@ -1786,7 +1786,7 @@ namespace Greenshot {
// //
this.zoom400MenuItem.Name = "zoom400MenuItem"; this.zoom400MenuItem.Name = "zoom400MenuItem";
this.zoom400MenuItem.Size = new System.Drawing.Size(209, 22); this.zoom400MenuItem.Size = new System.Drawing.Size(209, 22);
this.zoom400MenuItem.Tag = "400"; this.zoom400MenuItem.Tag = "4/1";
this.zoom400MenuItem.Text = "400%"; this.zoom400MenuItem.Text = "400%";
this.zoom400MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); this.zoom400MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick);
// //
@ -1794,7 +1794,7 @@ namespace Greenshot {
// //
this.zoom600MenuItem.Name = "zoom600MenuItem"; this.zoom600MenuItem.Name = "zoom600MenuItem";
this.zoom600MenuItem.Size = new System.Drawing.Size(209, 22); this.zoom600MenuItem.Size = new System.Drawing.Size(209, 22);
this.zoom600MenuItem.Tag = "600"; this.zoom600MenuItem.Tag = "6/1";
this.zoom600MenuItem.Text = "600%"; this.zoom600MenuItem.Text = "600%";
this.zoom600MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick); this.zoom600MenuItem.Click += new System.EventHandler(this.ZoomSetValueMenuItemClick);
// //

View file

@ -69,8 +69,7 @@ namespace Greenshot {
/// <summary> /// <summary>
/// All provided zoom values (in percents) in ascending order. /// All provided zoom values (in percents) in ascending order.
/// </summary> /// </summary>
private readonly int[] ZOOM_VALUES = new[] { 25, 50, 66, 75, 100, 200, 300, 400, 600 }; 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) };
private int _zoomValue = 100;
/// <summary> /// <summary>
/// An Implementation for the IImageEditor, this way Plugins have access to the HWND handles wich can be used with Win32 API calls. /// 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) { 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]; var nextValue = nextIndex < 0 ? ZOOM_VALUES[ZOOM_VALUES.Length - 1] : ZOOM_VALUES[nextIndex];
ZoomSetValue(nextValue); ZoomSetValue(nextValue);
} }
private void ZoomOutMenuItemClick(object sender, EventArgs e) { 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]; var nextValue = nextIndex < 0 ? ZOOM_VALUES[0] : ZOOM_VALUES[nextIndex];
ZoomSetValue(nextValue); ZoomSetValue(nextValue);
@ -1548,7 +1549,7 @@ namespace Greenshot {
private void ZoomSetValueMenuItemClick(object sender, EventArgs e) { private void ZoomSetValueMenuItemClick(object sender, EventArgs e) {
var senderMenuItem = (ToolStripMenuItem)sender; var senderMenuItem = (ToolStripMenuItem)sender;
int zoomPercent = int.Parse((string)senderMenuItem.Tag); var zoomPercent = Fraction.Parse((string)senderMenuItem.Tag);
ZoomSetValue(zoomPercent); ZoomSetValue(zoomPercent);
} }
@ -1559,8 +1560,8 @@ namespace Greenshot {
var maxImageSize = maxWindowSize - chromeSize; var maxImageSize = maxWindowSize - chromeSize;
var imageSize = Surface.Image.Size; var imageSize = Surface.Image.Size;
static bool isFit(int zoom, int source, int boundary) static bool isFit(Fraction scale, int source, int boundary)
=> source * zoom / 100 <= boundary; => (int)(source * scale) <= boundary;
var nextIndex = Array.FindLastIndex( var nextIndex = Array.FindLastIndex(
ZOOM_VALUES, ZOOM_VALUES,
@ -1572,7 +1573,7 @@ namespace Greenshot {
ZoomSetValue(nextValue); ZoomSetValue(nextValue);
} }
private void ZoomSetValue(int value) { private void ZoomSetValue(Fraction value) {
var surface = Surface as Surface; var surface = Surface as Surface;
var panel = surface?.Parent as Panel; var panel = surface?.Parent as Panel;
if (panel == null) if (panel == null)
@ -1596,14 +1597,13 @@ namespace Greenshot {
} }
// Set the new zoom value // Set the new zoom value
_zoomValue = value; Surface.ZoomFactor = value;
Surface.ZoomFactor = 1f * value / 100;
Size = GetOptimalWindowSize(); Size = GetOptimalWindowSize();
AlignCanvasPositionAfterResize(); AlignCanvasPositionAfterResize();
// Update zoom controls // Update zoom controls
string valueString = value.ToString(); zoomStatusDropDownBtn.Text = ((int)(100 * (double)value)).ToString() + "%";
zoomStatusDropDownBtn.Text = valueString + "%"; var valueString = value.ToString();
foreach (var item in zoomMenuStrip.Items) { foreach (var item in zoomMenuStrip.Items) {
if (item is ToolStripMenuItem menuItem) { if (item is ToolStripMenuItem menuItem) {
menuItem.Checked = menuItem.Tag as string == valueString; 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.Drawing;
using System.IO; using System.IO;
using System.Windows.Forms; using System.Windows.Forms;
using GreenshotPlugin.Core;
using GreenshotPlugin.Effects; using GreenshotPlugin.Effects;
using GreenshotPlugin.Interfaces.Drawing; using GreenshotPlugin.Interfaces.Drawing;
@ -193,9 +194,9 @@ namespace GreenshotPlugin.Interfaces
} }
/// <summary> /// <summary>
/// Zoom value applied to the surface. 1.0f for actual size (100%). /// Zoom value applied to the surface.
/// </summary> /// </summary>
float ZoomFactor { get; set; } Fraction ZoomFactor { get; set; }
/// <summary> /// <summary>
/// Translate a point from image coorditate space to surface coordinate space. /// Translate a point from image coorditate space to surface coordinate space.
/// </summary> /// </summary>