diff --git a/Mk0.Tools.ImageCropper.sln b/Mk0.Tools.ImageCropper.sln
new file mode 100644
index 0000000..eedd6c9
--- /dev/null
+++ b/Mk0.Tools.ImageCropper.sln
@@ -0,0 +1,29 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.28307.438
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mk0.Tools.ImageCropper", "Mk0.Tools.ImageCropper\Mk0.Tools.ImageCropper.csproj", "{E39D1D47-4E7D-42AA-908B-9BB1EBC014FA}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {E39D1D47-4E7D-42AA-908B-9BB1EBC014FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E39D1D47-4E7D-42AA-908B-9BB1EBC014FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E39D1D47-4E7D-42AA-908B-9BB1EBC014FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E39D1D47-4E7D-42AA-908B-9BB1EBC014FA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C090F0F5-27B1-4C18-A884-8E61D23CC2F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C090F0F5-27B1-4C18-A884-8E61D23CC2F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C090F0F5-27B1-4C18-A884-8E61D23CC2F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C090F0F5-27B1-4C18-A884-8E61D23CC2F6}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {2074C3F7-F2C9-4CDA-B037-28990AF7E74F}
+ EndGlobalSection
+EndGlobal
diff --git a/Mk0.Tools.ImageCropper/GDIWrap.cs b/Mk0.Tools.ImageCropper/GDIWrap.cs
new file mode 100644
index 0000000..a30d393
--- /dev/null
+++ b/Mk0.Tools.ImageCropper/GDIWrap.cs
@@ -0,0 +1,233 @@
+using System;
+using System.Drawing;
+
+namespace Mk0.Tools.ImageCropper
+{
+ #region Enumerations
+ public enum RasterOps
+ {
+ R2_BLACK = 1,
+ R2_NOTMERGEPEN,
+ R2_MASKNOTPEN,
+ R2_NOTCOPYPEN,
+ R2_MASKPENNOT,
+ R2_NOT,
+ R2_XORPEN,
+ R2_NOTMASKPEN,
+ R2_MASKPEN,
+ R2_NOTXORPEN,
+ R2_NOP,
+ R2_MERGENOTPEN,
+ R2_COPYPEN,
+ R2_MERGEPENNOT,
+ R2_MERGEPEN,
+ R2_WHITE,
+ R2_LAST
+ }
+
+ public enum BrushStyles
+ {
+ BS_SOLID = 0,
+ BS_NULL = 1,
+ BS_HATCHED = 2,
+ BS_PATTERN = 3,
+ BS_INDEXED = 4,
+ BS_DIBPATTERN = 5,
+ BS_DIBPATTERNPT = 6,
+ BS_PATTERN8X8 = 7,
+ BS_MONOPATTERN = 9
+ }
+
+ public enum PenStyles
+ {
+ PS_SOLID = 0,
+ PS_DASH = 1,
+ PS_DOT = 2,
+ PS_DASHDOT = 3,
+ PS_DASHDOTDOT = 4
+ }
+ #endregion
+
+ public sealed class GDIWrap : IDisposable
+ {
+ #region Variables
+ private Color _borderColor;
+ private Color _fillColor;
+ private int _lineWidth;
+ private IntPtr _hdc, _oldBrush, _oldPen, _gdiPen, _gdiBrush;
+ private BrushStyles _brushStyle;
+ private PenStyles _penStyle;
+ #endregion
+
+ #region Constructors
+ ///
+ /// Initializes a new instance of the GDIWrap class.
+ ///
+ public GDIWrap()
+ { // Set up for XOR drawing to begin with
+ this._borderColor = Color.Transparent;
+ this._fillColor = Color.Black;
+ this._lineWidth = 2;
+ this._brushStyle = BrushStyles.BS_NULL;
+ this._penStyle = PenStyles.PS_SOLID;
+ }
+ #endregion
+
+ #region Properties
+ ///
+ /// Gets or sets the current BrushColor
+ ///
+ public Color BrushColor
+ {
+ get { return _fillColor; }
+
+ set { _fillColor = value; }
+ }
+
+ ///
+ /// Gets or sets the current BrushStyle. Set to BS_NULL for no brush.
+ ///
+ public BrushStyles BrushStyle
+ {
+ get { return _brushStyle; }
+
+ set { _brushStyle = value; }
+ }
+
+ ///
+ /// Gets or sets the current PenColor. Set to Color.Transparent for a XOR line.
+ ///
+ public Color PenColor
+ {
+ get { return _borderColor; }
+
+ set { _borderColor = value; }
+ }
+
+ ///
+ /// Gets or sets the current PenStyle.
+ ///
+ public PenStyles PenStyle
+ {
+ get { return _penStyle; }
+
+ set { _penStyle = value; }
+ }
+
+ ///
+ /// Gets or sets the current PenWidth.
+ ///
+ public int PenWidth
+ {
+ get { return _lineWidth; }
+
+ set { _lineWidth = value; }
+ }
+
+ #endregion
+
+ #region Methods
+ ///
+ /// Draws a line with the pen that has been set by the user. Uses gdi32->MoveToEx and gdi32->LineTo
+ ///
+ /// Graphics object. You can use CreateGraphics().
+ /// Initial point of line.
+ /// Termination point of line.
+ public void DrawLine(Graphics g, Point p1, Point p2)
+ {
+ InitPenAndBrush(g);
+ NativeMethods.MoveToEx(_hdc, p1.X, p1.Y, (IntPtr)null);
+ NativeMethods.LineTo(_hdc, p2.X, p2.Y);
+ Dispose(g);
+ }
+
+ ///
+ /// Draws a rectangle with the pen and brush that have been set by the user. Uses gdi32->Rectangle
+ ///
+ /// Graphics object. You can use CreateGraphics().
+ /// The shape to draw.
+ public void DrawRectangle(Graphics g, Rectangle myRect)
+ {
+ InitPenAndBrush(g);
+ NativeMethods.Rectangle(_hdc, myRect.Left, myRect.Top, myRect.Right, myRect.Bottom);
+ Dispose(g);
+ }
+
+ ///
+ /// Draws an ellipse with the pen and brush that have been set by the user. Uses gdi32->Ellipse
+ ///
+ /// Graphics object. You can use CreateGraphics().
+ /// First corner of ellipse (if you imagine its size as a rectangle).
+ /// Second corner of ellipse (if you imagine its size as a rectangle).
+ public void DrawEllipse(Graphics g, Point p1, Point p2)
+ {
+ InitPenAndBrush(g);
+ NativeMethods.Ellipse(_hdc, p1.X, p1.Y, p2.X, p2.Y);
+ Dispose(g);
+ }
+
+ public void Dispose()
+ {
+ }
+
+ private int GetRGBFromColor(Color fromColor)
+ {
+ return fromColor.ToArgb() & 0xFFFFFF;
+ }
+
+ ///
+ /// Initializes the pen and brush objects. Stores the old pen and brush so they can be recovered later.
+ ///
+ ///
+ private void InitPenAndBrush(Graphics g)
+ {
+ _hdc = g.GetHdc();
+ _gdiPen = NativeMethods.CreatePen(_penStyle, _lineWidth, GetRGBFromColor(PenColor));
+ _gdiBrush = NativeMethods.GetStockObject(5); // CreateSolidBrush(GetRGBFromColor(fillColor));
+ if (PenColor == Color.Transparent)
+ NativeMethods.SetROP2(_hdc, (int)RasterOps.R2_XORPEN);
+ _oldPen = NativeMethods.SelectObject(_hdc, _gdiPen);
+ _oldBrush = NativeMethods.SelectObject(_hdc, _gdiBrush);
+ }
+
+ ///
+ /// Reloads the old pen and brush.
+ /// Deletes the pen that was created by InitPenAndBrush(g).
+ /// Releases the handle to the device context and then disposes of the Graphics object.
+ ///
+ ///
+ private void Dispose(Graphics g)
+ {
+ NativeMethods.SelectObject(_hdc, _oldBrush);
+ NativeMethods.SelectObject(_hdc, _oldPen);
+ NativeMethods.DeleteObject(_gdiPen);
+ NativeMethods.DeleteObject(_gdiBrush);
+ g.ReleaseHdc(_hdc);
+ }
+
+ #endregion
+ internal static class NativeMethods
+ {
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern bool Ellipse(IntPtr hdc, int x1, int y1, int x2, int y2);
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern bool Rectangle(IntPtr hdc, int X1, int Y1, int X2, int Y2);
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern IntPtr MoveToEx(IntPtr hdc, int x, int y, IntPtr lpPoint);
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern bool LineTo(IntPtr hdc, int x, int y);
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern IntPtr CreatePen(PenStyles enPenStyle, int nWidth, int crColor);
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern IntPtr CreateSolidBrush(int crColor);
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern bool DeleteObject(IntPtr hObject);
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject);
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern IntPtr GetStockObject(int brStyle);
+ [System.Runtime.InteropServices.DllImportAttribute("gdi32.dll")]
+ internal static extern int SetROP2(IntPtr hdc, int enDrawMode);
+ }
+ }
+}
diff --git a/Mk0.Tools.ImageCropper/Mk0.Tools.ImageCropper.csproj b/Mk0.Tools.ImageCropper/Mk0.Tools.ImageCropper.csproj
new file mode 100644
index 0000000..0e63b83
--- /dev/null
+++ b/Mk0.Tools.ImageCropper/Mk0.Tools.ImageCropper.csproj
@@ -0,0 +1,65 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {E39D1D47-4E7D-42AA-908B-9BB1EBC014FA}
+ Library
+ Properties
+ Mk0.Tools.ImageCropper
+ Mk0.Tools.ImageCropper
+ v4.6.1
+ 512
+
+
+
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+ UserControl
+
+
+ RubberBand.cs
+
+
+
+
+
+
+
+
+ RubberBand.cs
+
+
+
+
+
\ No newline at end of file
diff --git a/Mk0.Tools.ImageCropper/Properties/AssemblyInfo.cs b/Mk0.Tools.ImageCropper/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..00af002
--- /dev/null
+++ b/Mk0.Tools.ImageCropper/Properties/AssemblyInfo.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle("Mk0.Tools.ImageCropper")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("Mk0.Tools.ImageCropper")]
+[assembly: AssemblyCopyright("Copyright © 2019 mk0.at")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// Setting ComVisible to false makes the types in this assembly not visible
+// to COM components. If you need to access a type in this assembly from
+// COM, set the ComVisible attribute to true on that type.
+[assembly: ComVisible(false)]
+
+// The following GUID is for the ID of the typelib if this project is exposed to COM
+[assembly: Guid("20b6e8e2-490c-48b0-9951-af1466162c67")]
+
+// Version information for an assembly consists of the following four values:
+//
+// Major Version
+// Minor Version
+// Build Number
+// Revision
+//
+// You can specify all the values or you can default the Build and Revision Numbers
+// by using the '*' as shown below:
+// [assembly: AssemblyVersion("1.0.*")]
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Mk0.Tools.ImageCropper/RubberBand.Designer.cs b/Mk0.Tools.ImageCropper/RubberBand.Designer.cs
new file mode 100644
index 0000000..25508ba
--- /dev/null
+++ b/Mk0.Tools.ImageCropper/RubberBand.Designer.cs
@@ -0,0 +1,80 @@
+namespace Mk0.Tools.ImageCropper
+{
+ partial class RubberBand
+ {
+ ///
+ /// Required designer variable.
+ ///
+ private System.ComponentModel.IContainer components = null;
+
+ ///
+ /// Clean up any resources being used.
+ ///
+ /// true if managed resources should be disposed; otherwise, false.
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing && (components != null))
+ {
+ components.Dispose();
+ }
+ base.Dispose(disposing);
+ }
+
+ #region Component Designer generated code
+
+ ///
+ /// Required method for Designer support - do not modify
+ /// the contents of this method with the code editor.
+ ///
+ private void InitializeComponent()
+ {
+ this.thePicture = new System.Windows.Forms.PictureBox();
+ this.panel1 = new System.Windows.Forms.Panel();
+ ((System.ComponentModel.ISupportInitialize)(this.thePicture)).BeginInit();
+ this.panel1.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // thePicture
+ //
+ this.thePicture.Location = new System.Drawing.Point(3, 3);
+ this.thePicture.Name = "thePicture";
+ this.thePicture.Size = new System.Drawing.Size(151, 134);
+ this.thePicture.SizeMode = System.Windows.Forms.PictureBoxSizeMode.AutoSize;
+ this.thePicture.TabIndex = 0;
+ this.thePicture.TabStop = false;
+ this.thePicture.Paint += new System.Windows.Forms.PaintEventHandler(this.OnPaint);
+ this.thePicture.MouseDoubleClick += new System.Windows.Forms.MouseEventHandler(this.OnMouseDoubleClick);
+ this.thePicture.MouseDown += new System.Windows.Forms.MouseEventHandler(this.OnMouseDown);
+ this.thePicture.MouseMove += new System.Windows.Forms.MouseEventHandler(this.OnMouseMove);
+ this.thePicture.MouseUp += new System.Windows.Forms.MouseEventHandler(this.OnMouseUp);
+ this.thePicture.Resize += new System.EventHandler(this.OnResize);
+ //
+ // panel1
+ //
+ this.panel1.AutoScroll = true;
+ this.panel1.Controls.Add(this.thePicture);
+ this.panel1.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.panel1.Location = new System.Drawing.Point(0, 0);
+ this.panel1.Name = "panel1";
+ this.panel1.Size = new System.Drawing.Size(150, 150);
+ this.panel1.TabIndex = 1;
+ //
+ // RubberBand
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.Controls.Add(this.panel1);
+ this.Name = "RubberBand";
+ ((System.ComponentModel.ISupportInitialize)(this.thePicture)).EndInit();
+ this.panel1.ResumeLayout(false);
+ this.panel1.PerformLayout();
+ this.ResumeLayout(false);
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.PictureBox thePicture;
+ private System.Windows.Forms.Panel panel1;
+ }
+}
diff --git a/Mk0.Tools.ImageCropper/RubberBand.cs b/Mk0.Tools.ImageCropper/RubberBand.cs
new file mode 100644
index 0000000..18cb937
--- /dev/null
+++ b/Mk0.Tools.ImageCropper/RubberBand.cs
@@ -0,0 +1,841 @@
+using System;
+using System.ComponentModel;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Mk0.Tools.ImageCropper
+{
+ public enum Operation
+ {
+ none,
+ draw,
+ move,
+ left,
+ top,
+ right,
+ bottom,
+ topLeft,
+ topRight,
+ bottomRight,
+ bottomLeft
+ }
+
+ ///
+ /// Manages the cropping rectangle - allows drawing, resizing, and moving. _theRectangle and _backupRectangle are stored
+ /// in units of pixels.
+ ///
+ public partial class RubberBand : UserControl
+ {
+ #region Member Variables;
+
+ private Operation _curOp; // Indicates the current operation
+ private bool _isDragging = false; // Indicates if the left mouse button is down
+ private Point _startPoint; // Set in the mouse down event. The anchor for our rectangle
+ private bool _disabled = false; // Indicates cropping is not allowed
+ private Rectangle _theRectDrawing = new Rectangle( // We draw this rectangle on the screen as our cropping rectangle
+ new Point(0, 0), new Size(0, 0)); // It is populated in the MouseUp event
+
+ private Rectangle _backupRectangle = new Rectangle( // Helps us handle double click event - we need to restore the rectangle
+ new Point(0, 0), new Size(0, 0)); // because double click also triggers a single click event.
+
+ private Rectangle _picRectangle = new Rectangle( // The area of our image - not the same as area of PictureBox control
+ new Point(0, 0), new Size(0, 0));
+
+ private GDIWrap _myGDI = new GDIWrap(); // DrawReversibleFrame caused a lot of flickering - so we use GDI methods
+ #endregion
+
+ #region Constructors
+ ///
+ /// Initializes a new instance of the CropperBox class
+ ///
+ public RubberBand()
+ {
+ InitializeComponent();
+ }
+ #endregion
+
+ public event EventHandler ImageCropped;
+
+ public event EventHandler CursorMove;
+
+ #region Public Properties and Methods
+ ///
+ /// Gets or sets how images will be sized in the control - true sized or sized to fit the screen
+ ///
+ [Category("Custom"), Description("Size mode of the picturebox control")]
+ public PictureBoxSizeMode SizeMode
+ {
+ get
+ {
+ return thePicture.SizeMode;
+ }
+
+ set
+ {
+ Rectangle origSize = ImageRect(); // This defines the current image rectangle
+ // Set docking style before we set size mode
+ // Otherwise we have repaint problems if pic was scrolled.
+ if (value == PictureBoxSizeMode.AutoSize)
+ thePicture.Dock = DockStyle.None;
+ else
+ thePicture.Dock = DockStyle.Fill;
+ thePicture.SizeMode = value;
+ _picRectangle = ImageRect();
+
+ // Now we can transform our cropping rectangle
+ _theRectDrawing.Height = (int)Math.Round(_theRectDrawing.Height * (float)_picRectangle.Height / origSize.Height);
+ _theRectDrawing.Width = (int)Math.Round(_theRectDrawing.Width * (float)_picRectangle.Width / origSize.Width);
+
+ // Get the offset for the cropping rectangle
+ Point offset = new Point(_theRectDrawing.X - origSize.X, _theRectDrawing.Y - origSize.Y);
+
+ // Scale the offset
+ offset.X = (int)Math.Round(offset.X * (float)_picRectangle.Width / origSize.Width);
+ offset.Y = (int)Math.Round(offset.Y * (float)_picRectangle.Height / origSize.Height);
+
+ // Set our new position
+ _theRectDrawing.X = offset.X + ImageRect().X;
+ _theRectDrawing.Y = offset.Y + ImageRect().Y;
+ }
+ }
+
+ ///
+ /// Gets or sets the image that will be displayed. When we set a new image, we check if the current
+ /// cropping rectangle fits within the image. If not, we reset it to zero.
+ ///
+ [Category("Custom"), Description("Gets or sets the image from the picturebox")]
+ public Image Image
+ {
+ get
+ {
+ return thePicture.Image;
+ }
+
+ set
+ {
+ // Change the image
+ thePicture.Image = value;
+
+ // Is the cropping rectangle completely within the new image?
+ if (!ImageRect().Contains(_theRectDrawing))
+ _theRectDrawing = new Rectangle(0, 0, 0, 0);
+ }
+ }
+
+ ///
+ /// Gets the image selected by the cropping rectangle. Returns null if no image is selected.
+ /// Return the entire image if no cropping rectangle is selected.
+ ///
+ [Category("Custom"), Description("Gets the image selected by the cropping rectangle")]
+ public Image SelectedImage
+ {
+ get
+ {
+ bool tempRect = false;
+ // Create a bitmap from our image
+ Bitmap bmpImage = new Bitmap(thePicture.Image);
+
+ // If we dont have a cropping rectangle, set the cropping rectangle to the
+ // dimensions of the entire image.
+ if (_theRectDrawing.Width <= 0 || _theRectDrawing.Height <= 0)
+ {
+ _theRectDrawing = ImageRect();
+ tempRect = true;
+ }
+
+ // Scale the cropping rectangle
+ Rectangle cropRect = CroppingArea;
+
+ // Clone the image defined by cropRect. Use the same pixel format
+ // as our original image.
+ Bitmap bmpCrop = bmpImage.Clone(cropRect, bmpImage.PixelFormat);
+ bmpImage.Dispose();
+ if (tempRect)
+ _theRectDrawing = new Rectangle(0, 0, 0, 0);
+
+ return (Image)bmpCrop;
+ }
+ }
+
+ ///
+ /// Gets or sets the Disabled property. We disable the control when we display the home page.
+ ///
+ [Category("Custom"), Description("Gets or sets the cropping capability of the control")]
+ public bool Disabled
+ {
+ get
+ {
+ return _disabled;
+ }
+
+ set
+ {
+ _disabled = value;
+ if (_disabled)
+ _theRectDrawing = new Rectangle(0, 0, 0, 0);
+ }
+ }
+ #endregion
+
+ #region Private Properties And Methods
+
+ ///
+ /// Gets the cropping area scaled to the specified size
+ ///
+ ///
+ ///
+ [Category("Custom"), Description("Obtain the current cropping rectangle scaled to the size of the specified image")]
+ private Rectangle CroppingArea
+ {
+ get
+ {
+ Rectangle cropRect = _theRectDrawing;
+ double ratio = GetScaleFactor(SizeMode == PictureBoxSizeMode.AutoSize);
+ if (ratio != 0.0)
+ {
+ cropRect.Offset(-_picRectangle.Left, -_picRectangle.Top);
+ cropRect.Height = (int)Math.Round(cropRect.Height / ratio);
+ cropRect.X = (int)Math.Round(cropRect.X / ratio);
+ cropRect.Width = (int)Math.Round(cropRect.Width / ratio);
+ cropRect.Y = (int)Math.Round(cropRect.Y / ratio);
+ }
+
+ return cropRect;
+ }
+ }
+
+ ///
+ /// Update the point based on the image size
+ ///
+ ///
+ ///
+ private Point CroppingPoint(Point pt)
+ {
+ Point adjustedPt = pt;
+ double ratio = GetScaleFactor(SizeMode == PictureBoxSizeMode.AutoSize);
+ if (ratio != 0.0)
+ {
+ adjustedPt.Offset(-_picRectangle.Left, -_picRectangle.Top);
+ adjustedPt.X = (int)Math.Round(adjustedPt.X / ratio);
+ adjustedPt.Y = (int)Math.Round(adjustedPt.Y / ratio);
+ }
+
+ return adjustedPt;
+ }
+
+ ///
+ /// If the cursor is outside a cropping area, start a new rubberband. If the cursor is within the
+ /// bounds of a rubber band, trigger a move operation. If the cursor is on the border of a rubber
+ /// band, trigger a resize operation. This event is triggered by the PictureBox control.
+ ///
+ ///
+ ///
+ private void OnMouseDown(object sender, MouseEventArgs e)
+ {
+ // Set the isDrag variable to true and get the starting point
+ // by using the PointToScreen method to convert form
+ // coordinates to screen coordinates.
+ Point myPoint = new Point(e.X, e.Y); // PictureBox coordinates are passed in
+ if (_disabled)
+ _curOp = Operation.none;
+ else if (e.Button == MouseButtons.Left)
+ {
+ this._isDragging = true;
+ this._picRectangle = ImageRect();
+ this._backupRectangle = _theRectDrawing; // Helps with undo if this is part of a double click event
+ this._curOp = DetermineMouseOperation(myPoint);
+ }
+ else
+ {
+ _curOp = Operation.none;
+ }
+
+ switch (_curOp)
+ {
+ case Operation.draw:
+ _startPoint = myPoint;
+
+ // Reset the rectangle.
+ _theRectDrawing = new Rectangle(0, 0, 0, 0);
+ thePicture.Invalidate();
+ break;
+ case Operation.none:
+ _isDragging = false;
+
+ // Reset the rectangle.
+ _theRectDrawing = new Rectangle(0, 0, 0, 0);
+ thePicture.Invalidate();
+ break;
+ default:
+ _startPoint = myPoint;
+
+ // Erase the inked rectangle
+ thePicture.Refresh();
+
+ // Draw the rectangle in reverse ink
+ Graphics g = thePicture.CreateGraphics();
+ _myGDI.DrawRectangle(g, _theRectDrawing);
+ g.Dispose();
+ break;
+ }
+ }
+
+ ///
+ /// If curOp is none, we will set the cursor depending on the current position. Otherwise,
+ /// we will modify or move the rubberand. This event is triggered by the PictureBox control.
+ ///
+ ///
+ ///
+ private void OnMouseMove(object sender, MouseEventArgs e)
+ {
+ Point myPoint = new Point(e.X, e.Y); // Picture box coordinates
+ Graphics g = thePicture.CreateGraphics();
+
+ // Determine what our current operation is. This depends on if we are dragging the mouse or not
+ if (_isDragging)
+ {
+ _curOp = CheckOperationChange(_curOp, myPoint); // The operation can change during a mouse drag
+ // Erase the old rectangle created in the mouse down event
+ _myGDI.DrawRectangle(g, _theRectDrawing);
+ }
+ else
+ {
+ _curOp = DetermineMouseOperation(myPoint); // If not dragging, see which cursor to use
+ }
+
+ bool isActive = IsActivePoint(ref myPoint);
+ switch (_curOp)
+ {
+ case Operation.draw:
+ // Calculate the endpoint and dimensions for the new
+ _theRectDrawing = NormalizeRectangle(_startPoint, myPoint);
+ break;
+ case Operation.move:
+ Cursor = Cursors.SizeAll;
+ if (_isDragging)
+ {
+ // Compute how much the cursor has moved since the previous call
+ Point myDiff = new Point(myPoint.X - _startPoint.X, myPoint.Y - _startPoint.Y);
+ myDiff = CalcMaxOffset(myDiff);
+ _theRectDrawing.Offset(myDiff);
+ _startPoint = myPoint;
+ }
+
+ break;
+ case Operation.left:
+ Cursor = Cursors.SizeWE;
+ if (_isDragging)
+ _theRectDrawing = NormalizeRectangle(myPoint.X, _theRectDrawing.Top, _theRectDrawing.Right, _theRectDrawing.Bottom);
+ break;
+ case Operation.top:
+ Cursor = Cursors.SizeNS;
+ if (_isDragging)
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, myPoint.Y, _theRectDrawing.Right, _theRectDrawing.Bottom);
+ break;
+ case Operation.right:
+ Cursor = Cursors.SizeWE;
+ if (_isDragging)
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, _theRectDrawing.Top, myPoint.X, _theRectDrawing.Bottom);
+ break;
+ case Operation.bottom:
+ Cursor = Cursors.SizeNS;
+ if (_isDragging)
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, _theRectDrawing.Top, _theRectDrawing.Right, myPoint.Y);
+ break;
+ case Operation.topLeft:
+ Cursor = Cursors.SizeNWSE;
+ if (_isDragging)
+ _theRectDrawing = NormalizeRectangle(myPoint.X, myPoint.Y, _theRectDrawing.Right, _theRectDrawing.Bottom);
+ break;
+ case Operation.topRight:
+ Cursor = Cursors.SizeNESW;
+ if (_isDragging)
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, myPoint.Y, myPoint.X, _theRectDrawing.Bottom);
+ break;
+ case Operation.bottomRight:
+ Cursor = Cursors.SizeNWSE;
+ if (_isDragging)
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, _theRectDrawing.Top, myPoint.X, myPoint.Y);
+ break;
+ case Operation.bottomLeft:
+ Cursor = Cursors.SizeNESW;
+ if (_isDragging)
+ _theRectDrawing = NormalizeRectangle(myPoint.X, _theRectDrawing.Top, _theRectDrawing.Right, myPoint.Y);
+ break;
+ default:
+ Cursor = Cursors.Cross;
+ break;
+ }
+
+ // Draw our new rectangle
+ if (_isDragging)
+ _myGDI.DrawRectangle(g, _theRectDrawing);
+ g.Dispose();
+
+ // Notify the host that the mouse is moved. If we have a cropping rectangle,
+ // size of the cropping rectangle. Otherwise, report the mouse position
+ if (CursorMove != null)
+ {
+ Rectangle crop = CroppingArea;
+ if (crop.Height <= 0 || crop.Width <= 0)
+ {
+ crop.Width = thePicture.Image.Width;
+ crop.Height = thePicture.Image.Height;
+ crop.X = 0;
+ crop.Y = 0;
+ // If we are within the picture, report the current mouse position
+ if (isActive)
+ CursorMove(this, new StatusEventArg(CroppingPoint(myPoint), crop));
+ else
+ CursorMove(this, new StatusEventArg(crop.Location, crop));
+ }
+ else
+ CursorMove(this, new StatusEventArg(crop.Location, crop));
+ }
+ }
+
+ ///
+ /// We are done with our mouse operation. This event is triggered by
+ /// the PictureBox.
+ ///
+ ///
+ ///
+ private void OnMouseUp(object sender, MouseEventArgs e)
+ {
+ Point myPoint = new Point(e.X, e.Y);
+ IsActivePoint(ref myPoint);
+
+ // Erase the current rectangle
+ Graphics g = thePicture.CreateGraphics();
+ if (_curOp != Operation.none)
+ _myGDI.DrawRectangle(g, _theRectDrawing);
+ g.Dispose();
+
+ // If the MouseUp event occurs, the user has stopped dragging
+ _isDragging = false;
+
+ // Note that some operations can change mid stride depending on where the user moves the mouse
+ _curOp = CheckOperationChange(_curOp, myPoint);
+
+ switch (_curOp)
+ {
+ case Operation.move:
+ myPoint = new Point(myPoint.X - _startPoint.X, myPoint.Y - _startPoint.Y);
+ myPoint = CalcMaxOffset(myPoint);
+ _theRectDrawing.Offset(myPoint);
+ break;
+ case Operation.left:
+ _theRectDrawing = NormalizeRectangle(myPoint.X, _theRectDrawing.Top, _theRectDrawing.Right, _theRectDrawing.Bottom);
+ break;
+ case Operation.right:
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, _theRectDrawing.Top, myPoint.X, _theRectDrawing.Bottom);
+ break;
+ case Operation.top:
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, myPoint.Y, _theRectDrawing.Right, _theRectDrawing.Bottom);
+ break;
+ case Operation.bottom:
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, _theRectDrawing.Top, _theRectDrawing.Right, myPoint.Y);
+ break;
+ case Operation.topLeft:
+ _theRectDrawing = NormalizeRectangle(myPoint.X, myPoint.Y, _theRectDrawing.Right, _theRectDrawing.Bottom);
+ break;
+ case Operation.topRight:
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, myPoint.Y, myPoint.X, _theRectDrawing.Bottom);
+ break;
+ case Operation.bottomLeft:
+ _theRectDrawing = NormalizeRectangle(myPoint.X, _theRectDrawing.Top, _theRectDrawing.Right, myPoint.Y);
+ break;
+ case Operation.bottomRight:
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Left, _theRectDrawing.Top, myPoint.X, myPoint.Y);
+ break;
+ }
+
+ if (_curOp != Operation.none)
+ {
+ // Create our new cropping rectangle - scale it in terms of the current image size
+ // The rectangle is currently in pictureBox coordinates. We need to scale it in terms
+ // of the current image.
+ myPoint.X = _theRectDrawing.Right;
+ myPoint.Y = _theRectDrawing.Bottom;
+ _theRectDrawing = NormalizeRectangle(_theRectDrawing.Location, myPoint);
+ thePicture.Invalidate();
+ _curOp = Operation.none;
+ }
+ }
+
+ ///
+ /// Notify our host that someone double clicked the pictureBox. Keep in mind that a
+ /// double click event also causes two mouse down events and one mouse up event prior to
+ /// triggering this event.
+ ///
+ ///
+ ///
+ private void OnMouseDoubleClick(object sender, MouseEventArgs e)
+ {
+ _theRectDrawing = _backupRectangle;
+ _curOp = Operation.none;
+ _isDragging = false;
+ ImageCropped?.Invoke(this, e);
+ }
+
+ ///
+ /// Paint event of the picturebox control. Draw the rectangle only if not being drawn by
+ /// mouse events. The rectangle is drawn using client coordinates of the PictureBox
+ ///
+ ///
+ ///
+ private void OnPaint(object sender, PaintEventArgs e)
+ {
+ if (!_isDragging)
+ _myGDI.DrawRectangle(e.Graphics, _theRectDrawing);
+ }
+
+ ///
+ /// Fired when the picturebox control is resized. We may also need to resize our cropping rectangle
+ /// and refresh the dimensions of picRectangle (thePicture is the PictureBox control).
+ ///
+ ///
+ ///
+ private void OnResize(object sender, EventArgs e)
+ {
+ _picRectangle = ImageRect();
+ }
+
+ ///
+ /// Create a normalized rectangle from two points. There does not seem to be
+ /// any built in method to do this. The two points represent the top left and bottom
+ /// right corners.
+ ///
+ ///
+ ///
+ ///
+ private Rectangle NormalizeRectangle(Point p1, Point p2)
+ {
+ Rectangle rc = new Rectangle();
+
+ // Normalize the rectangle.
+ if (p1.X < p2.X)
+ {
+ rc.X = p1.X;
+ rc.Width = p2.X - p1.X;
+ }
+ else
+ {
+ rc.X = p2.X;
+ rc.Width = p1.X - p2.X;
+ }
+
+ if (p1.Y < p2.Y)
+ {
+ rc.Y = p1.Y;
+ rc.Height = p2.Y - p1.Y;
+ }
+ else
+ {
+ rc.Y = p2.Y;
+ rc.Height = p1.Y - p2.Y;
+ }
+
+ return rc;
+ }
+
+ private Rectangle NormalizeRectangle(int x1, int y1, int x2, int y2)
+ {
+ Point p1 = new Point(x1, y1);
+ Point p2 = new Point(x2, y2);
+ return NormalizeRectangle(p1, p2);
+ }
+
+ ///
+ /// The amount to change picture size to fit within our panel. If the
+ /// image is true sized, return 1. Otherwise return the factor we will
+ /// increase/reduce the actual image size.
+ ///
+ ///
+ ///
+ private double GetScaleFactor(bool isTrueSize)
+ {
+ double ratio = 0.0;
+ double ratio1 = 0.0;
+
+ if (isTrueSize)
+ ratio = 1.0; // Image is true sized
+ else
+ {
+ // Get the dimensions of the picture so we can compute our sizing ratio
+ if (thePicture.Image != null)
+ {
+ ratio = ((float)panel1.Size.Width) / thePicture.Image.Width;
+ ratio1 = ((float)panel1.Size.Height) / thePicture.Image.Height;
+ }
+
+ if (ratio1 < ratio)
+ ratio = ratio1;
+ }
+
+ return ratio;
+ }
+
+ ///
+ /// When the picture is sized to fit, it is also centered inside the panel.
+ /// We need to determine how much it is offset from the 0,0 position.
+ ///
+ ///
+ ///
+ private Point GetScaleOffset(bool isTrueSize)
+ {
+ Point topLeft = new Point();
+ if (thePicture.Image == null)
+ return topLeft;
+
+ double ratio = GetScaleFactor(isTrueSize);
+ int width = (int)(thePicture.Image.Width * ratio);
+ int height = (int)(thePicture.Image.Height * ratio);
+
+ // Get the location of the picture. If not trueSize, it is centered in the panel
+ // smaller than the panel.
+ if (isTrueSize || width > panel1.Width)
+ topLeft.X = 0;
+ else
+ topLeft.X = (panel1.Width - width) / 2;
+ if (isTrueSize || height > panel1.Height)
+ topLeft.Y = 0;
+ else
+ topLeft.Y = (panel1.Height - height) / 2;
+
+ return topLeft;
+ }
+
+ ///
+ /// Return the rectangle that defines the current displayed image
+ /// in the picture box. Seems there should already be a method to
+ /// do this, but I could not find one. thePicture is our PictureBox
+ /// control. theImage is the size of the image contained therein.
+ /// The size of the image depends on the current SizeMode - the
+ /// image might be scaled to fit the picturebox.
+ ///
+ ///
+ private Rectangle ImageRect()
+ {
+ double ratio;
+ Point topLeft = new Point();
+ Rectangle imageRect = new Rectangle();
+
+ if (thePicture.Image == null)
+ return imageRect;
+
+ ratio = GetScaleFactor(thePicture.SizeMode == PictureBoxSizeMode.AutoSize);
+ topLeft = GetScaleOffset(thePicture.SizeMode == PictureBoxSizeMode.AutoSize);
+ imageRect.Width = (int)(thePicture.Image.Width * ratio);
+ imageRect.Height = (int)(thePicture.Image.Height * ratio);
+ imageRect.Location = topLeft;
+ return imageRect;
+ }
+
+ ///
+ /// Return true if the point is within the boundaries of the current
+ /// picturebox image. If not, modify pt so it is on the offending boundary.
+ ///
+ ///
+ ///
+ private bool IsActivePoint(ref Point pt)
+ {
+ bool isActive = false;
+ if (_picRectangle.Size.IsEmpty)
+ _picRectangle = ImageRect();
+
+ // Is the point within the picture area? The point must be in client coords
+ isActive = _picRectangle.Contains(pt);
+ if (!isActive)
+ {
+ // We will adjust the point to make it active
+ Rectangle myRect = _picRectangle;
+ if (pt.X < myRect.Left)
+ pt.X = myRect.Left;
+ if (pt.X > myRect.Right)
+ pt.X = myRect.Right;
+ if (pt.Y < myRect.Top)
+ pt.Y = myRect.Top;
+ if (pt.Y > myRect.Bottom)
+ pt.Y = myRect.Bottom;
+ }
+
+ return isActive;
+ }
+
+ ///
+ /// Return the appropriate operation type based on the point passed in
+ ///
+ /// Client coordinates are passed in
+ ///
+ private Operation DetermineMouseOperation(Point pt)
+ {
+ Operation retVal = Operation.none;
+ Rectangle outerFrame = _theRectDrawing;
+ Rectangle innerFrame = outerFrame;
+ Size sz = new Size(2, 2); // The amount of leeway to give our user moving the mouse
+ outerFrame.Inflate(sz); // Create an outer rectangle around our cropping area
+ innerFrame.Inflate(-sz.Width, -sz.Height); // Create an inner rectangle within our cropping area
+
+ // Check if we are near any of the rectangle corners. We have to offset these four rectangles because the
+ // hotspot on the stock cursor for SizeNESW and SizeNWSE is not in the center of the cursor where it should be.
+ Rectangle picCorner;
+
+ // Check for top left corner
+ picCorner = new Rectangle(outerFrame.Left, outerFrame.Top, sz.Width, sz.Height);
+ picCorner.Inflate(8, 8);
+ picCorner.Offset(-8, -8);
+ if (picCorner.Contains(pt))
+ retVal = Operation.topLeft;
+
+ // Check for top right corner
+ picCorner = new Rectangle(innerFrame.Right, outerFrame.Top, sz.Width, sz.Height);
+ picCorner.Inflate(8, 8);
+ picCorner.Offset(-8, -8);
+ if (picCorner.Contains(pt))
+ retVal = Operation.topRight;
+
+ // Check for bottom right corner
+ picCorner = new Rectangle(innerFrame.Right, innerFrame.Bottom, sz.Width, sz.Height);
+ picCorner.Inflate(8, 8);
+ picCorner.Offset(-8, -8);
+ if (picCorner.Contains(pt))
+ retVal = Operation.bottomRight;
+
+ // Check for bottom left corner
+ picCorner = new Rectangle(outerFrame.Left, innerFrame.Bottom, sz.Width, sz.Height);
+ picCorner.Inflate(8, 8);
+ picCorner.Offset(-8, -8);
+ if (picCorner.Contains(pt))
+ retVal = Operation.bottomLeft;
+
+ // If we are not within any corners, do a little more checking
+ if (retVal == Operation.none)
+ {
+ // Are we inside the outer rectangle?
+ if (outerFrame.Contains(pt))
+ {
+ // Are we inside the inner rectangle?
+ if (innerFrame.Contains(pt))
+ retVal = Operation.move;
+ else if (pt.Y >= outerFrame.Top && pt.Y <= innerFrame.Top)
+ retVal = Operation.top;
+ else if (pt.Y >= innerFrame.Bottom && pt.Y <= outerFrame.Bottom)
+ {
+ System.Diagnostics.Debug.Print("Outer: " + outerFrame.ToString());
+ System.Diagnostics.Debug.Print("Inner: " + innerFrame.ToString());
+ System.Diagnostics.Debug.Print("Point: " + pt.ToString());
+ retVal = Operation.bottom;
+ }
+ else if (pt.X >= innerFrame.Right && pt.X <= outerFrame.Right)
+ retVal = Operation.right;
+ else if (pt.X >= outerFrame.Left && pt.X <= innerFrame.Left)
+ retVal = Operation.left;
+ }
+ }
+
+ // Are we inside the picture area?
+ if (retVal == Operation.none)
+ {
+ if (_picRectangle.Contains(pt) && _isDragging)
+ retVal = Operation.draw;
+ }
+
+ System.Diagnostics.Debug.Print(retVal.ToString());
+ return retVal;
+ }
+
+ ///
+ /// The operation can change depending on where the user moves the mouse
+ ///
+ ///
+ ///
+ ///
+ private Operation CheckOperationChange(Operation curOp, Point myPoint)
+ {
+ switch (curOp)
+ {
+ case Operation.left:
+ if (myPoint.X > _theRectDrawing.Right)
+ curOp = Operation.right;
+ break;
+ case Operation.right:
+ if (myPoint.X < _theRectDrawing.Left)
+ curOp = Operation.left;
+ break;
+ case Operation.top:
+ if (myPoint.Y > _theRectDrawing.Bottom)
+ curOp = Operation.bottom;
+ break;
+ case Operation.bottom:
+ if (myPoint.Y < _theRectDrawing.Top)
+ curOp = Operation.top;
+ break;
+ case Operation.topLeft:
+ if ((myPoint.X > _theRectDrawing.Right) && (myPoint.Y > _theRectDrawing.Bottom))
+ curOp = Operation.bottomRight;
+ else if (myPoint.X > _theRectDrawing.Right)
+ curOp = Operation.topRight;
+ else if (myPoint.Y > _theRectDrawing.Bottom)
+ curOp = Operation.bottomLeft;
+ break;
+ case Operation.topRight:
+ if ((myPoint.X < _theRectDrawing.Left) && (myPoint.Y > _theRectDrawing.Bottom))
+ curOp = Operation.bottomLeft;
+ else if (myPoint.X < _theRectDrawing.Left)
+ curOp = Operation.topLeft;
+ else if (myPoint.Y > _theRectDrawing.Bottom)
+ curOp = Operation.bottomRight;
+ break;
+ case Operation.bottomLeft:
+ if ((myPoint.X > _theRectDrawing.Right) && (myPoint.Y < _theRectDrawing.Top))
+ curOp = Operation.topRight;
+ else if (myPoint.X > _theRectDrawing.Right)
+ curOp = Operation.bottomRight;
+ else if (myPoint.Y < _theRectDrawing.Top)
+ curOp = Operation.topLeft;
+ break;
+ case Operation.bottomRight:
+ if ((myPoint.X < _theRectDrawing.Left) && (myPoint.Y < _theRectDrawing.Top))
+ curOp = Operation.topLeft;
+ else if (myPoint.X < _theRectDrawing.Left)
+ curOp = Operation.bottomLeft;
+ else if (myPoint.Y < _theRectDrawing.Top)
+ curOp = Operation.topRight;
+ break;
+ }
+
+ return curOp;
+ }
+
+ ///
+ /// Move the cropping rectangle - but not out of the picture boundaries
+ ///
+ ///
+ ///
+ private Point CalcMaxOffset(Point offSet)
+ {
+ Rectangle testRect = _theRectDrawing;
+
+ // Move the rectangle the specified amount and see if Kosher
+ testRect.Offset(offSet);
+
+ // Do we need to adjust our offset?
+ if (!_picRectangle.Contains(testRect))
+ {
+ if (testRect.Top < _picRectangle.Top)
+ offSet.Y = offSet.Y + (_picRectangle.Top - testRect.Top);
+ if (testRect.Right > _picRectangle.Right)
+ offSet.X = offSet.X + (_picRectangle.Right - testRect.Right);
+ if (testRect.Bottom > _picRectangle.Bottom)
+ offSet.Y = offSet.Y + (_picRectangle.Bottom - testRect.Bottom);
+ if (testRect.Left < _picRectangle.Left)
+ offSet.X = offSet.X + (_picRectangle.Left - testRect.Left);
+ }
+
+ return offSet;
+ }
+
+ #endregion
+ }
+}
diff --git a/Mk0.Tools.ImageCropper/RubberBand.resx b/Mk0.Tools.ImageCropper/RubberBand.resx
new file mode 100644
index 0000000..1af7de1
--- /dev/null
+++ b/Mk0.Tools.ImageCropper/RubberBand.resx
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Mk0.Tools.ImageCropper/StatusEventArg.cs b/Mk0.Tools.ImageCropper/StatusEventArg.cs
new file mode 100644
index 0000000..7d9ba94
--- /dev/null
+++ b/Mk0.Tools.ImageCropper/StatusEventArg.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Drawing;
+
+namespace Mk0.Tools.ImageCropper
+{
+ ///
+ /// Passes the cropping rectangle and current cursor position to the host object
+ ///
+ public class StatusEventArg : EventArgs
+ {
+ public readonly Rectangle CroppingRect;
+ public readonly Point CursorPos;
+
+ public StatusEventArg(Point pos, Rectangle crop)
+ {
+ CursorPos = pos;
+ CroppingRect = crop;
+ }
+ }
+}
diff --git a/README.md b/README.md
index 552836c..e96c154 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,4 @@
-# Mk0.Tools.ImageCropper
\ No newline at end of file
+# Mk0.Tools.ImageCropper
+(C) 2019 mk0.at
+
+This Tool provides a new form control to crop images.
\ No newline at end of file