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