VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
  Persistable = 0  'NotPersistable
  DataBindingBehavior = 0  'vbNone
  DataSourceBehavior  = 0  'vbNone
  MTSTransactionMode  = 0  'NotAnMTSObject
END
Attribute VB_Name = "cCropper"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

' --- Events ---

' Raised when any of the Crop rectangle values change. Allows you to update host UI
Public Event CropRectChanged(ByVal p_Left As Long, ByVal p_Top As Long, ByVal p_Right As Long, ByVal p_Bottom As Long)

' --- Type Definitions ---

' Defines a structure to hold the coordinates of a rectangle.
Private Type Rect
   Left As Long     ' X-coordinate of the top-left corner.
   Top As Long      ' Y-coordinate of the top-left corner.
   Right As Long    ' X-coordinate of the bottom-right corner.
   Bottom As Long   ' Y-coordinate of the bottom-right corner.
End Type

' --- Enumerations ---

' Defines possible areas the mouse can interact with (hit testing).
Private Enum e_MouseHit
   mousehit_None                    ' Mouse is not over any interactive area.
   mousehit_Image                   ' Mouse is over the image area (but not the selection).
   mousehit_CropRect                ' Mouse is inside the selection rectangle (for moving).
   mousehit_CropRectHandleTopLeft   ' Mouse is over the top-left resize handle.
   mousehit_CropRectHandleTopRight  ' Mouse is over the top-right resize handle.
   mousehit_CropRectHandleBottomRight  ' Mouse is over the bottom-right resize handle.
   mousehit_CropRectHandleBottomLeft   ' Mouse is over the bottom-left resize handle.
End Enum

Public Enum e_ImageFormat
   [_imageformat_AutoDetect] = 0
   
   imageformat_Jpeg = 1
   imageformat_Png
   imageformat_Bmp
End Enum

' --- Constants ---

Private Const mc_HandlePx As Long = 12    ' Defines the size (width/height) of the selection rectangle resize handles in pixels.

' --- Module-Level Variables ---

Private mo_SrfOrig As RC6.cCairoSurface      ' Holds the original, unmodified image surface (using Cairo via vbRichClient).
Private mo_SrfDisplay As RC6.cCairoSurface      ' Holds the surface used for drawing onto the mo_HookedPictureBox (backbuffer).

Private m_IsInitialized As Boolean
Private mt_ScaleRect As Rect     ' Stores the rectangle defining the position and size of the *scaled* image on the display.
Private mt_CropRect As Rect    ' Stores the *current* selection rectangle, in *original image coordinates*.
Private mt_CropRectNew As Rect    ' Stores the selection rectangle *while it is being drawn* by the user, in *original image coordinates*.
Private m_IsCropRectSel As Boolean   ' Flag indicating if the selection rectangle is currently "selected" (shows resize handles).
Private m_MouseHitAt As e_MouseHit  ' Stores the result of the last mouse hit test (what the cursor is over).
Private m_MouseDownX As Long  ' Stores the X coordinate (in *image coordinates*) where the mouse button was pressed down.
Private m_MouseDownY As Long  ' Stores the Y coordinate (in *image coordinates*) where the mouse button was pressed down.
Private m_IsDrawingCropRect As Boolean  ' Flag indicating if the user is currently dragging to draw a new selection rectangle.

' Stores the scaling factor applied to the original image to fit it onto the display.
Private m_Scale As Double

' Host Picturebox (and related vars) that this class will render to
Private WithEvents mo_HookedPictureBox As VB.PictureBox
Attribute mo_HookedPictureBox.VB_VarHelpID = -1
Private m_HookedPictureBoxScaleMode As VBRUN.ScaleModeConstants      ' Remember original scale mode so we can restore it on termination

Public Sub Initialize(Optional po_PictureBox As VB.PictureBox = Nothing)
   ' IMPORTANT!! This method MUST be called by the host app before this class can be used
   
   ' If the Nothing is passed for the only parameter then the class will be working in programmatic (non-user interactive) mode
   ' Otherwise, pass a reference to a PictureBox to enable user interaction mode
   
   ' NOTE: The passed PictureBox's ScaleMode will be changed to vbPixels
   '       DO NOT change the scale mode in your host app or it will break rendering.
   '       When this class is terminated, the passed PictureBox's ScaleMode will be restored to the original mode
   
   Set mo_HookedPictureBox = po_PictureBox   ' We will render everything to this passed PictureBox
   If Not po_PictureBox Is Nothing Then
      m_HookedPictureBoxScaleMode = po_PictureBox.ScaleMode    ' MUCH easier to always work in Pixels
   
      po_PictureBox.ScaleMode = vbPixels  ' Remember this so we can restore it on termination
   End If
   
   m_IsInitialized = True
End Sub

Public Property Get IsInitialized() As Boolean
   ' Returns true if the Initialized method was called and completed successfully
   
   IsInitialized = m_IsInitialized
End Property

Public Property Get IsUserInteractiveMode() As Boolean
   ' Return True if hooked to a PictureBox for user interaction
   ' If False, then cropping will be programmatic only
   
   IsUserInteractiveMode = Not (mo_HookedPictureBox Is Nothing)
End Property

Public Property Get IsImageLoaded() As Boolean
   ' Returns True if the class has been successfully initialized and an image has been loaded via LoadImage.
   
   IsImageLoaded = Me.IsInitialized And Not (mo_SrfOrig Is Nothing)
End Property

Private Property Get IsDrawingCropRect() As Boolean
   IsDrawingCropRect = m_IsDrawingCropRect And Me.IsUserInteractiveMode ' Protection here in case IsDrawingCropRect = True in non-UI mode (shouldn't ever happen, but just in case)
End Property

Public Sub LoadImage(ByVal p_ImagePath As String, Optional ByVal p_ClearCropRectangleOnLoad As Boolean = True)
   ' Load the original/based image from a file into the mo_SrfOrig Cairo surface.
   ' You can optionally keep the crop rectangle from the previous image intact
   '     which makes it easier to apply the same crop rect to multiple images
   
   If Not IsInitialized Then Err.Raise 5, , "You must call Initialize first!"
   
   Set mo_SrfOrig = Cairo.CreateSurface(0, 0, , p_ImagePath)
   
   If p_ClearCropRectangleOnLoad Then ClearCropRect
   
   If Me.IsUserInteractiveMode Then
      mo_HookedPictureBox_Resize
   Else
      With mt_ScaleRect
         .Left = 0
         .Top = 0
         .Bottom = mo_SrfOrig.Width
         .Right = mo_SrfOrig.Height
      End With
   End If
End Sub

Public Sub SaveImage(ByVal p_ImagePath As String, _
                     Optional ByVal p_Format As e_ImageFormat = [_imageformat_AutoDetect], _
                     Optional ByVal p_SoftMaxBytes As Long = 0, _
                     Optional ByVal p_MaxWidthAndHeightPixels As Long = 0, _
                     Optional ByVal p_BackgroundAlpha As Double = 0#, _
                     Optional ByVal p_BackgroundColor As OLE_COLOR = vbBlack, _
                     Optional ByVal p_SharpenOnShrink As Boolean = False, _
                     Optional ByVal p_SaveUncroppedImage As Boolean = False)
   
   ' Save the image to a file. If there is a crop rectangle is defined and p_SaveUncroppedImage = False (default)
   ' then the cropped image will be saved. Otherwise the full image will be saved.
   
   ' Supported p_Format formats are JPG, PNG, and BMP
   
   ' If you pass a p_SoftMaxBytes value, then the class will try to save the image to be "around" the passed max bytes
   ' This is a "soft" max, so it may be a bit over or under the target, but the idea is to get an image that is
   ' near enough within reason for things like emailing large images.
   
   ' If you pass a p_MaxWidthAndHeightPixels value, the image will be constrained by height and width to the passed value
   ' and drawn centered in the image area preserving original the aspect ratio.
   ' When p_MaxWidthAndHeightPixels>0 the p_BackgroundAlpha and p_BackgroundColor parameter values will be used to fill
   ' any image space that not covered by the scaled image.
   
   ' If p_SharpenOnShrink = True and the image is being resize by a significant amount, then the shrunk image will be Sharpened
   '     This can make thumbnail details easier to see
   
   Dim lo_Srf As RC6.cCairoSurface
   Dim lo_SrfFull As RC6.cCairoSurface
   Dim lo_SrfResized As RC6.cCairoSurface
   Dim l_ImgLen As Long
   Dim la_ImgBytes() As Byte
   Dim l_SaveBmp As Boolean
   Dim l_Rescale As Boolean
   Dim l_RescaleFactor As Double
   Dim l_OrigWidth As Long
   Dim l_OrigHeight As Long
   
   ' Make sure passed parameter values are sensible
   
   If Not IsImageLoaded Then Err.Raise 5, , "Image is not loaded. Make sure to call Initialize and LoadImage first."
   If p_SoftMaxBytes > 0 And p_SoftMaxBytes < 50000 Then Err.Raise 5, , "Please use a reasonable maximum byte size - " & p_SoftMaxBytes & " is too low, try 50,000 or more."
   If p_MaxWidthAndHeightPixels < 0 Then Err.Raise 5, , "Max Pixels must be >= 0"
   
   ' If there's a crop rect and we're not forcing full image save, then get the cropped surface to save
   ' Otherwise get the full surface to save
   
   If (mt_CropRect.Left <> mt_CropRect.Right) And _
      (mt_CropRect.Top <> mt_CropRect.Bottom) And _
      Not p_SaveUncroppedImage Then
         
         Set lo_Srf = mo_SrfOrig.CropSurface(mt_CropRect.Left, _
                                             mt_CropRect.Top, _
                                             mt_CropRect.Right - mt_CropRect.Left, _
                                             mt_CropRect.Bottom - mt_CropRect.Top)
   Else
      Set lo_Srf = mo_SrfOrig
      
   End If
   
   l_OrigWidth = lo_Srf.Width
   l_OrigHeight = lo_Srf.Height
   
   If p_MaxWidthAndHeightPixels > 0 Then
      With Cairo.CreateSurface(p_MaxWidthAndHeightPixels, p_MaxWidthAndHeightPixels)
         With .CreateContext
            If p_BackgroundAlpha < 1# Then
               .SetSourceColor p_BackgroundColor, p_BackgroundAlpha
               .Paint
            End If
            
            .RenderSurfaceContent lo_Srf, 0, 0, p_MaxWidthAndHeightPixels, p_MaxWidthAndHeightPixels, CAIRO_FILTER_BEST, , True
                        
            Set lo_Srf = .Surface
         End With
      End With
      
   End If
   
   Do
      ' Save in the appropriate format (or auto-detect the most appropriate format if p_Format = 0
      Select Case p_Format
      Case imageformat_Jpeg
         lo_Srf.WriteContentToJpgByteArray la_ImgBytes
         
      Case imageformat_Png
         lo_Srf.WriteContentToPngByteArray la_ImgBytes
            
      Case imageformat_Bmp
         l_SaveBmp = True
         lo_Srf.BindToArray la_ImgBytes, True
            
      Case Else
         ' Autodetect based on file extension
         
         Select Case LCase$(Mid$(p_ImagePath, InStrRev(p_ImagePath, ".") + 1))
         Case "bmp"
            l_SaveBmp = True
            lo_Srf.BindToArray la_ImgBytes, True
         
         Case "png"
            lo_Srf.WriteContentToPngByteArray la_ImgBytes
         
         Case "jpg", "jpeg"
            lo_Srf.WriteContentToJpgByteArray la_ImgBytes
         
         Case Else
            ' Unknown extension, default to JPEG
            lo_Srf.WriteContentToJpgByteArray la_ImgBytes
            
         End Select
      End Select
      
      l_ImgLen = New_c.ArrInfo.ArrLength(la_ImgBytes)
      If l_SaveBmp Then lo_Srf.ReleaseArray la_ImgBytes
      
      l_Rescale = False
      If p_SoftMaxBytes > 0 Then
         If l_ImgLen > p_SoftMaxBytes * 1.25 Then
            l_Rescale = True
         End If
      End If
            
      If l_Rescale Then
         ' Image is too big for our soft-max byte size, let's resize and try again
         l_RescaleFactor = p_SoftMaxBytes / l_ImgLen
         
         If l_SaveBmp Then
            ' For BMPs we'll try to scale the image AREA by the square root of the rescale factor
            ' with some fudging to get us close to the desired max bytes to account for 32BPP->24BPP conversion
            
            l_RescaleFactor = Sqr(l_RescaleFactor * 4 / 3)
         
         Else
            ' For 32BPP target formats, we'll apply the square root of the scale factor without the 4/3 fudge factor
            
            l_RescaleFactor = Sqr(l_RescaleFactor)
         End If
         
         ' Create a new surface and draw original cropped/full image at the appropriate scale
         Set lo_SrfResized = Cairo.CreateSurface(lo_Srf.Width * l_RescaleFactor, _
                                                 lo_Srf.Height * l_RescaleFactor)
         If lo_SrfFull Is Nothing Then Set lo_SrfFull = lo_Srf
         
         With lo_SrfResized.CreateContext
            .RenderSurfaceContent lo_SrfFull, 0, 0, .Surface.Width, .Surface.Height, CAIRO_FILTER_BEST, , True
         End With
         
         Set lo_Srf = lo_SrfResized
      
      Else
         If p_SharpenOnShrink Then
            p_SharpenOnShrink = False
            
            ' Sharpen if shrinking image significantly
            If IIf(l_OrigWidth > l_OrigHeight, l_OrigWidth, l_OrigHeight) * 667 > IIf(lo_Srf.Width > lo_Srf.Height, lo_Srf.Width, lo_Srf.Height) Then
               lo_Srf.Sharpen
               If IIf(l_OrigWidth > l_OrigHeight, l_OrigWidth, l_OrigHeight) * 333 > IIf(lo_Srf.Width > lo_Srf.Height, lo_Srf.Width, lo_Srf.Height) Then
                  lo_Srf.Sharpen
               End If
            End If
         Else
            Exit Do
         End If
      End If
   Loop  ' Loop as necessary to get image to approach soft max bytes value
   
   ' Save final image to disk
   If l_SaveBmp Then
      SavePicture lo_Srf.Picture, p_ImagePath
   Else
      New_c.FSO.WriteByteContent p_ImagePath, la_ImgBytes
   End If
End Sub

Public Sub SetCropRect(ByVal p_Left As Long, ByVal p_Top As Long, ByVal p_Right As Long, ByVal p_Bottom As Long)
   mt_CropRect.Left = IIf(p_Left = 0, 0, ClampX(p_Left + 1))
   mt_CropRect.Right = IIf(p_Right = 0, 0, ClampX(mo_SrfOrig.Width - p_Right))
   mt_CropRect.Top = IIf(p_Top = 0, 0, ClampY(p_Top + 1))
   mt_CropRect.Bottom = IIf(p_Bottom = 0, 0, ClampY(mo_SrfOrig.Height - p_Bottom))
      
   Repaint
   RaiseCropChangedEvent
End Sub

Public Sub ClearCropRect()
   With mt_CropRect
      .Left = 0
      .Top = 0
      .Right = 0
      .Bottom = 0
   End With
   
   RaiseCropChangedEvent
End Sub

Private Sub RaiseCropChangedEvent()
   Dim lt_Rect As Rect
   
   If IsDrawingCropRect Then
      LSet lt_Rect = mt_CropRectNew
      
      If lt_Rect.Left = lt_Rect.Right And lt_Rect.Top = lt_Rect.Bottom Then Exit Sub
      
   Else
      LSet lt_Rect = mt_CropRect
   End If
   
   RaiseEvent CropRectChanged(IIf(lt_Rect.Left = 0, 0, lt_Rect.Left - 1), _
                              IIf(lt_Rect.Top = 0, 0, lt_Rect.Top - 1), _
                              IIf(lt_Rect.Right = 0, 0, mo_SrfOrig.Width - lt_Rect.Right), _
                              IIf(lt_Rect.Bottom = 0, 0, mo_SrfOrig.Height - lt_Rect.Bottom))
End Sub

' --- Event Handlers ---
Private Sub mo_HookedPictureBox_KeyUp(KeyCode As Integer, Shift As Integer)
   Select Case KeyCode
   Case vbKeyEscape ' If Escape key is pressed:
      ' Clear the Crop rectangle by setting its coordinates to zero.
      ClearCropRect
      
      ' Redraw the mo_HookedPictureBox to reflect the change.
      Repaint
   End Select
End Sub

' Handles mouse button presses on the mo_HookedPictureBox.
Private Sub mo_HookedPictureBox_MouseDown(Button As Integer, Shift As Integer, x As Single, y As Single)
   If Not IsImageLoaded Then Exit Sub
         
   ' Determine what the mouse is currently hovering over.
   CalcMouseHitAt x, y

   ' Process only left mouse button clicks.
   If Button = vbLeftButton Then
      ' Convert the mo_HookedPictureBox coordinates (X, Y) to original image coordinates.
      mo_HookedPictureBoxToImageCoordS x, y ' Note: This modifies X and Y directly.

      ' Permo_HookedPictureBox action based on where the mouse was clicked.
      Select Case m_MouseHitAt
      Case mousehit_Image ' Clicked on the image (not the selection or handles):
         ' Start drawing a new selection rectangle.
         m_IsDrawingCropRect = True

         ' Initialize the new selection rectangle at the click position.
         With mt_CropRectNew
            .Left = x
            .Top = y
            .Right = x
            .Bottom = y
         End With

         ' Reset mouse down coordinates (not needed for drawing new rect).
         m_MouseDownX = 0
         m_MouseDownY = 0

      Case mousehit_None ' Clicked outside the image area:
         ' Reset mouse down coordinates.
         m_IsDrawingCropRect = False
         m_MouseDownX = 0
         m_MouseDownY = 0

      Case Else ' Clicked on the selection rectangle or one of its handles:
         ' Store the starting mouse position (in image coordinates) for dragging/resizing.
         m_IsDrawingCropRect = False
         m_MouseDownX = x
         m_MouseDownY = y
      End Select
   End If
End Sub

' Handles mouse movement over the mo_HookedPictureBox.
Private Sub mo_HookedPictureBox_MouseMove(Button As Integer, Shift As Integer, x As Single, y As Single)
   If Not IsImageLoaded Then Exit Sub
         
   ' If no mouse button is pressed, just update the hit test inmo_HookedPictureBoxation (e.g., for cursor changes).
   If Button = 0 Then CalcMouseHitAt x, y

   ' If the left mouse button is being held down:
   If Button = vbLeftButton Then
      ' Convert current mo_HookedPictureBox coordinates to image coordinates.
      mo_HookedPictureBoxToImageCoordS x, y ' Note: This modifies X and Y directly.

      ' Permo_HookedPictureBox action based on what was under the cursor *when the button was pressed down*.
      Select Case m_MouseHitAt
      Case mousehit_Image ' Currently drawing a new selection rectangle:
         If IsDrawingCropRect Then
            ' Update the bottom-right corner of the new rectangle as the mouse moves.
            With mt_CropRectNew
               .Right = x
               .Bottom = y
            End With
            
            RaiseCropChangedEvent
         End If
         
      Case mousehit_CropRect ' Currently moving the existing selection rectangle:
         ' Calculate the distance moved since the last MouseMove event.
         Dim l_DeltaX As Long: l_DeltaX = x - m_MouseDownX
         Dim l_DeltaY As Long: l_DeltaY = y - m_MouseDownY

         ' Move the selection rectangle by the delta.
         With mt_CropRect
            .Left = .Left + l_DeltaX
            .Right = .Right + l_DeltaX
            .Top = .Top + l_DeltaY
            .Bottom = .Bottom + l_DeltaY

            ' --- Boundary checks to keep the rectangle within the image ---
            ' Check left boundary.
            If .Left < 1 Then
               .Right = .Right - .Left + 1 ' Adjust right edge to maintain width
               .Left = 1
            End If
            ' Check right boundary.
            If .Right > mo_SrfOrig.Width Then
               .Left = .Left - (.Right - mo_SrfOrig.Width) ' Adjust left edge to maintain width
               .Right = mo_SrfOrig.Width
            End If
            ' Check top boundary.
            If .Top < 1 Then
               .Bottom = .Bottom - .Top + 1 ' Adjust bottom edge to maintain height
               .Top = 1
            End If
            ' Check bottom boundary.
            If .Bottom > mo_SrfOrig.Height Then
               .Top = .Top - (.Bottom - mo_SrfOrig.Height) ' Adjust top edge to maintain height
               .Bottom = mo_SrfOrig.Height
            End If
         End With

         RaiseCropChangedEvent
         
         ' Update the last mouse position for the next move calculation.
         m_MouseDownX = x
         m_MouseDownY = y

      Case mousehit_CropRectHandleTopLeft ' Resizing using the top-left handle:
         With mt_CropRect
            ' Adjust the top and left edges based on mouse movement.
            .Left = ClampX(.Left + (x - m_MouseDownX))
            .Top = ClampY(.Top + (y - m_MouseDownY))
         End With
         
         RaiseCropChangedEvent
         
         ' Update the last mouse position so subsesquent delta calculations will be correct
         m_MouseDownX = x
         m_MouseDownY = y

      Case mousehit_CropRectHandleTopRight ' Resizing using the top-right handle:
         With mt_CropRect
            ' Adjust the top and right edges based on mouse movement.
            .Right = ClampX(.Right + (x - m_MouseDownX))
            .Top = ClampY(.Top + (y - m_MouseDownY))
         End With
         
         RaiseCropChangedEvent
         
         ' Update the last mouse position so subsesquent delta calculations will be correct
         m_MouseDownX = x
         m_MouseDownY = y

      Case mousehit_CropRectHandleBottomLeft ' Resizing using the bottom-left handle:
         With mt_CropRect
            ' Adjust the bottom and left edges based on mouse movement.
            .Left = ClampX(.Left + (x - m_MouseDownX))
            .Bottom = ClampY(.Bottom + (y - m_MouseDownY))
         End With
         
         RaiseCropChangedEvent
         
         ' Update the last mouse position so subsesquent delta calculations will be correct
         m_MouseDownX = x
         m_MouseDownY = y

      Case mousehit_CropRectHandleBottomRight ' Resizing using the bottom-right handle:
         With mt_CropRect
            ' Adjust the bottom and right edges based on mouse movement.
            .Right = ClampX(.Right + (x - m_MouseDownX))
            .Bottom = ClampY(.Bottom + (y - m_MouseDownY))
         
            If .Right <> .Left Or .Bottom <> .Top Then
               RaiseCropChangedEvent
            End If
            
         End With
         
         
         ' Update the last mouse position so subsesquent delta calculations will be correct
         m_MouseDownX = x
         m_MouseDownY = y

      End Select ' End Case m_MouseHitAt

      ' Redraw the mo_HookedPictureBox to show the updated rectangle (drawing, moving, or resizing).
      Repaint
   End If
End Sub

' Handles mouse button releases on the mo_HookedPictureBox.
Private Sub mo_HookedPictureBox_MouseUp(Button As Integer, Shift As Integer, x As Single, y As Single)
   If Not IsImageLoaded Then Exit Sub
         
   ' Process only left mouse button releases.
   If Button = vbLeftButton Then
      ' Convert mo_HookedPictureBox coordinates to image coordinates.
      mo_HookedPictureBoxToImageCoordS x, y ' Note: This modifies X and Y directly so the values after this point are in full image coordinates
      
      ' Finalize the action based on what was being done.
      Select Case m_MouseHitAt
      Case mousehit_Image ' Finished drawing a new rectangle:
         
         If m_IsDrawingCropRect Then
            m_IsDrawingCropRect = False
            
            ' Set the final bottom-right corner of the new rectangle.
            With mt_CropRectNew
               .Right = x
               .Bottom = y
            End With
   
            ' Ensure the rectangle has Left < Right and Top < Bottom.
            NormalizeRect mt_CropRectNew
   
            m_IsDrawingCropRect = False
   
            ' Check if the drawn rectangle is valid (has width and height).
            If mt_CropRectNew.Left <> mt_CropRectNew.Right And mt_CropRectNew.Top <> mt_CropRectNew.Bottom Then
               ' Rect valid, so Copy the newly drawn rectangle to the main selection rectangle.
               LSet mt_CropRect = mt_CropRectNew
               
               ' Clear the temporary new rectangle.
               With mt_CropRectNew
                  .Left = 0
                  .Top = 0
                  .Right = 0
                  .Bottom = 0
               End With
               ' Mark the main rectangle as selected (since it's now valid and active).
               m_IsCropRectSel = True ' It should be selected after drawing
            
               RaiseCropChangedEvent
            
            Else
               If m_IsCropRectSel Then
                  m_IsCropRectSel = False
               Else
                  ClearCropRect
               End If
               
            End If
         Else
            If Not m_IsCropRectSel Then ClearCropRect
         End If
         
      Case mousehit_None ' Clicked outside the image - deselect any existing selection.
         m_IsCropRectSel = False

      Case mousehit_CropRect ' Finished moving the rectangle:
         ' Keep the rectangle selected.
         m_IsCropRectSel = True

      Case Else ' Finished resizing the rectangle (using a handle):
         ' Ensure the resized rectangle has Left < Right and Top < Bottom.
         NormalizeRect mt_CropRect
         
         ' Keep the rectangle selected.
         m_IsCropRectSel = True

      End Select ' End Case m_MouseHitAt

      ' Redraw the mo_HookedPictureBox with the final state of the selection.
      Repaint

   End If ' End If Button = vbLeftButton

   m_IsDrawingCropRect = False

   ' Update hit test inmo_HookedPictureBoxation after the action is complete.
   CalcMouseHitAt x, y
End Sub

' Handles the mo_HookedPictureBox's Paint event (triggered by Refresh, loading, uncovering).
Private Sub mo_HookedPictureBox_Paint()
   ' Delegate the actual drawing work to the Repaint subroutine.
   Repaint
End Sub

' Handles the mo_HookedPictureBox being resized.
Private Sub mo_HookedPictureBox_Resize()
   If Not IsImageLoaded Then Exit Sub
         
   ' Create a new Cairo surface matching the mo_HookedPictureBox's current client area size.
   Set mo_SrfDisplay = Cairo.CreateSurface(mo_HookedPictureBox.ScaleWidth, mo_HookedPictureBox.ScaleHeight)
   
   ' Recalculate how the original image should be scaled and positioned to fit the new mo_HookedPictureBox size.
   CalcScaleRect mo_SrfOrig.Width, mo_SrfOrig.Height, mo_HookedPictureBox.ScaleWidth, mo_HookedPictureBox.ScaleHeight
   
   ' Redraw the mo_HookedPictureBox with the newly scaled image.
   Repaint
End Sub

' --- Drawing Subroutine ---

' Central routine for drawing the entire mo_HookedPictureBox content onto the backbuffer (mo_SrfDisplay) and then onto the screen.
Private Sub Repaint()
   Dim lt_CropRect As Rect  ' Temporary variable to hold the rectangle to be drawn (either the final or the new one).
   Dim l_HandleSize As Double ' Handle size in image coords
   Dim l_HandleHalfSize As Double   ' Handle half size for offsetting against selection rectangle corners

   ' Check if an image is loaded (in case we accidentally get here before an image is loaded)
   If Not Me.IsImageLoaded Then Exit Sub
   If Not Me.IsUserInteractiveMode Then Exit Sub
   
   ' Get a drawing context for the mo_HookedPictureBox's backbuffer surface.
   With mo_SrfDisplay.CreateContext
      ' Fill the background with a dark gray color.
      .SetSourceColor RGB(60, 60, 60)
      .Paint ' Fills the entire surface

      ' Save the current drawing state (transmo_HookedPictureBoxations, colors, etc.).
      .Save

      ' --- Draw the Scaled Image ---
      ' Move the origin to where the scaled image should start.
      .TranslateDrawings mt_ScaleRect.Left, mt_ScaleRect.Top
      ' Scale the coordinate system based on the calculated scale factor.
      .ScaleDrawings m_Scale, m_Scale

      ' Render the original image content onto the transmo_HookedPictureBoxed context.
      ' It will be drawn at (0,0) *in the translated and scaled coordinate system*,
      ' effectively positioning and scaling it within mt_ScaleRect on the mo_HookedPictureBox.
      .RenderSurfaceContent mo_SrfOrig, 0, 0, , , CAIRO_FILTER_FAST

      ' --- Draw Selection Overlay and Handles ---
      ' Determine which selection rectangle to draw (the temporary one being drawn or the final one).
      If m_IsDrawingCropRect Then
         lt_CropRect = mt_CropRectNew ' Use the rectangle being drawn
      Else
         lt_CropRect = mt_CropRect    ' Use the finalized rectangle
      End If

      ' Check if the selection rectangle is valid (has dimensions > 0).
      If (lt_CropRect.Left > 0 And lt_CropRect.Top > 0 And lt_CropRect.Right > 0 And lt_CropRect.Bottom > 0 And _
         lt_CropRect.Left <> lt_CropRect.Right And lt_CropRect.Top <> lt_CropRect.Bottom) Or _
         IsOverHandle Then

         ' --- Draw Semi-Transparent Overlay Outside Selection ---
         ' Method: Draw the selection rectangle, then draw the image boundary rectangle.
         ' Using the EVEN_ODD fill rule fills the area *between* these two paths.

         ' Define the inner path (selection rectangle).
         .Rectangle lt_CropRect.Left, _
                    lt_CropRect.Top, _
                    lt_CropRect.Right - lt_CropRect.Left, _
                    lt_CropRect.Bottom - lt_CropRect.Top

         ' Define the outer path (image boundary). Note: These are image coordinates.
         .Rectangle 0, _
                    0, _
                    mo_SrfOrig.Width + 0.5, _
                    mo_SrfOrig.Height + 0.5

         ' Set fill color to semi-transparent black.
         .SetSourceRGBA 0, 0, 0, 0.75
         ' Set the fill rule to Even-Odd
         ' to then fill the area outside the selection rectangle.
         .FillRule = CAIRO_FILL_RULE_EVEN_ODD
         .Fill

         ' --- Draw Resize Handles (if the rectangle is selected) ---
         If m_IsCropRectSel Then
            ' Calculate handle size in *image coordinates* by dividing pixel size by scale
            ' This ensures the handle size is drawn consistently on-screen regardless of scale

            l_HandleSize = (mc_HandlePx / m_Scale)
            l_HandleHalfSize = l_HandleSize / 2
            
            ' Define path for Top-Left handle.
            .Rectangle lt_CropRect.Left - l_HandleHalfSize, lt_CropRect.Top - l_HandleHalfSize, _
                       l_HandleSize, l_HandleSize

            ' Define path for Top-Right handle.
            .Rectangle lt_CropRect.Right - l_HandleHalfSize, lt_CropRect.Top - l_HandleHalfSize, _
                       l_HandleSize, l_HandleSize

            ' Define path for Bottom-Right handle.
            .Rectangle lt_CropRect.Right - l_HandleHalfSize, lt_CropRect.Bottom - l_HandleHalfSize, _
                       l_HandleSize, l_HandleSize

            ' Define path for Bottom-Left handle.
            .Rectangle lt_CropRect.Left - l_HandleHalfSize, lt_CropRect.Bottom - l_HandleHalfSize, _
                       l_HandleSize, l_HandleSize

            ' Fill the handles with white color.
            .SetSourceColor vbWhite
            .Fill True ' True = Preserve the path for stroking

            ' Outline the handles with black color.
            .SetSourceColor vbBlack
            ' Set line width (adjust for scaling so it appears constant).
            .SetLineWidth 1.5 / m_Scale
            ' Stroke the outlines.
            .Stroke
         End If ' End If m_IsCropRectSel
      End If ' End If valid lt_CropRect

      ' Restore the previous drawing state (removes translation and scaling).
      .Restore
   End With ' End With mo_SrfDisplay.CreateContext

   ' Draw the completed backbuffer (mo_SrfDisplay) onto the visible mo_HookedPictureBox window (mo_HookedPictureBox.hDC).
   mo_SrfDisplay.DrawToDC mo_HookedPictureBox.hDC, 0, 0, mo_HookedPictureBox.ScaleWidth, mo_HookedPictureBox.ScaleHeight
End Sub

' --- Helper Subroutines ---

' Calculates the rectangle (mt_ScaleRect) needed to display an image of
' original dimensions within a maximum area, preserving aspect ratio and centering.
Private Sub CalcScaleRect(ByVal p_OriginalWidth As Long, ByVal p_OriginalHeight As Long, ByVal p_MaxWidth As Long, ByVal p_MaxHeight As Long)
   Dim l_FitY As Double ' Scaling factor based on height
   Dim l_FitX As Double ' Scaling factor based on width
   Dim l_FitMin As Double ' The smaller of l_FitX and l_FitY

   Dim l_Left As Long   ' Calculated left position of the scaled image
   Dim l_Top As Long    ' Calculated top position of the scaled image
   Dim l_Width As Long  ' Calculated width of the scaled image
   Dim l_Height As Long ' Calculated height of the scaled image

   ' Ensure original dimensions are valid.
   If p_OriginalWidth = 0 Or p_OriginalHeight = 0 Then
      Err.Raise 5 ' Invalid procedure call or argument
   End If

   ' Calculate the scaling factors required to fit width and height independently.
   l_FitX = p_MaxWidth / p_OriginalWidth
   l_FitY = p_MaxHeight / p_OriginalHeight

   ' Use the *minimum* scaling factor to ensure the entire image fits within the bounds.
   l_FitMin = IIf(l_FitX < l_FitY, l_FitX, l_FitY)

   ' Store the calculated scale factor globally.
   m_Scale = l_FitMin

   ' Calculate the dimensions of the image after scaling.
   l_Width = p_OriginalWidth * l_FitMin
   l_Height = p_OriginalHeight * l_FitMin

   ' Calculate the top-left position to center the scaled image within the maximum area.
   l_Top = (p_MaxHeight / 2) - (l_Height / 2)
   l_Left = (p_MaxWidth / 2) - (l_Width / 2)

   ' Store the calculated position and size in the global mt_ScaleRect structure.
   With mt_ScaleRect
      .Left = l_Left
      .Top = l_Top
      .Right = l_Left + l_Width
      .Bottom = l_Top + l_Height
   End With
End Sub

' Converts coordinates from the mo_HookedPictureBox's coordinate system (device/pixels)
' to the original Image's coordinate system (user), taking into account
' the scaling and translation applied during drawing.
' IMPORTANT: Modifies p_X and p_Y parameters directly (passed ByRef implicitly for Single).
Private Sub mo_HookedPictureBoxToImageCoordS(p_X As Single, p_Y As Single)
   ' Check if surfaces are valid before proceeding.
   If mo_SrfDisplay Is Nothing Or mo_SrfOrig Is Nothing Then Exit Sub
   
   ' Get a drawing context (only needed for coordinate transmo_HookedPictureBoxation).
   With mo_SrfDisplay.CreateContext
      ' Apply the same transmo_HookedPictureBoxations used for drawing the image in reverse.
      .TranslateDrawings mt_ScaleRect.Left, mt_ScaleRect.Top
      .ScaleDrawings m_Scale, m_Scale

      ' Convert the device point (mo_HookedPictureBox coordinates) to user point (image coordinates).
      .DeviceToUserPointSingle p_X, p_Y
   End With

   ' --- Clamp Coordinates to Image Boundaries ---
   ' Ensure X is within the original image width [1, Width].
   If p_X < 1 Then
      p_X = 1
   ElseIf p_X > mo_SrfOrig.Width Then
      p_X = mo_SrfOrig.Width
   End If

   ' Ensure Y is within the original image height [1, Height].
   If p_Y < 1 Then
      p_Y = 1
   ElseIf p_Y > mo_SrfOrig.Height Then
      p_Y = mo_SrfOrig.Height
   End If
End Sub

' Ensures that a rectangle's Left <= Right and Top <= Bottom.
' If the rectangle has zero width or height, it resets it to all zeros.
' Modifies the passed Rect structure directly (ByRef).
Private Sub NormalizeRect(ByRef pt_Rect As Rect) ' Changed to ByRef for clarity
   Dim l_Val As Double ' Temporary variable for swapping

   ' Check for zero width or height (invalid rectangle).
   If pt_Rect.Left = pt_Rect.Right Or pt_Rect.Top = pt_Rect.Bottom Then
      ' Reset the rectangle to zero dimensions/position.
      With pt_Rect
         .Left = 0
         .Top = 0
         .Right = 0
         .Bottom = 0
      End With
   Else ' Valid rectangle, proceed with normalization.
      ' Ensure Left <= Right.
      If pt_Rect.Left > pt_Rect.Right Then
         l_Val = pt_Rect.Left
         pt_Rect.Left = pt_Rect.Right
         pt_Rect.Right = l_Val
      End If
      ' Ensure Top <= Bottom.
      If pt_Rect.Top > pt_Rect.Bottom Then
         l_Val = pt_Rect.Top
         pt_Rect.Top = pt_Rect.Bottom
         pt_Rect.Bottom = l_Val
      End If
   End If
End Sub

' Determines what interactive element (image, selection, handle) is under
' the given mo_HookedPictureBox coordinates (p_X, p_Y) and updates m_MouseHitAt.
Private Sub CalcMouseHitAt(ByVal p_X As Single, ByVal p_Y As Single)
   Dim l_MousePointer As VBRUN.MousePointerConstants
   
   l_MousePointer = vbDefault
   
    ' Check if surfaces are valid.
   If mo_SrfOrig Is Nothing Then
       m_MouseHitAt = mousehit_None
   
   ElseIf p_X < mt_ScaleRect.Left Or p_X > mt_ScaleRect.Right Then
      m_MouseHitAt = mousehit_None
   
   ElseIf p_Y < mt_ScaleRect.Top Or p_Y > mt_ScaleRect.Bottom Then
      m_MouseHitAt = mousehit_None
   
   ElseIf IsDrawingCropRect Then
      m_MouseHitAt = mousehit_Image
     
   Else
     ' Assume initially the hit is on the image background,
     ' we'll probe further to see if we're actually over the select rect or resize handles
      
      m_MouseHitAt = mousehit_Image
      
      mo_HookedPictureBoxToImageCoordS p_X, p_Y
      
      ' Check if we're in the selection rectangle or over a resize handle
      
      If Not m_IsDrawingCropRect Then
         If p_X >= mt_CropRect.Left And p_X <= mt_CropRect.Right Then
            If p_Y >= mt_CropRect.Top And p_Y <= mt_CropRect.Bottom Then
               ' We're within the select rectangle
               ' But we'll need to probe further to see if we're over a resize handle
               m_MouseHitAt = mousehit_CropRect
               
               l_MousePointer = vbSizeAll
            End If
         End If
      End If
      
      ' Check if point is over a resize handle
      If m_IsCropRectSel Then
         If p_X >= mt_CropRect.Left - mc_HandlePx / m_Scale / 2 And _
            p_X <= mt_CropRect.Left + mc_HandlePx / m_Scale / 2 Then
            
            If p_Y >= mt_CropRect.Top - mc_HandlePx / m_Scale / 2 And _
               p_Y <= mt_CropRect.Top + mc_HandlePx / m_Scale / 2 Then
               
                  m_MouseHitAt = mousehit_CropRectHandleTopLeft
               
                  l_MousePointer = vbSizeNWSE
            End If
         
            If p_Y >= mt_CropRect.Bottom - mc_HandlePx / m_Scale / 2 And _
               p_Y <= mt_CropRect.Bottom + mc_HandlePx / m_Scale / 2 Then
               
                  m_MouseHitAt = mousehit_CropRectHandleBottomLeft
               
                  l_MousePointer = vbSizeNESW
               
            End If
            
         ElseIf p_X >= mt_CropRect.Right - mc_HandlePx / m_Scale / 2 And _
            p_X <= mt_CropRect.Right + mc_HandlePx / m_Scale / 2 Then
            
            If p_Y >= mt_CropRect.Top - mc_HandlePx / m_Scale / 2 And _
               p_Y <= mt_CropRect.Top + mc_HandlePx / m_Scale / 2 Then
               
                  m_MouseHitAt = mousehit_CropRectHandleTopRight
               
                  l_MousePointer = vbSizeNESW
               
            End If
         
            If p_Y >= mt_CropRect.Bottom - mc_HandlePx / m_Scale / 2 And _
               p_Y <= mt_CropRect.Bottom + mc_HandlePx / m_Scale / 2 Then
               
                  m_MouseHitAt = mousehit_CropRectHandleBottomRight
               
                  l_MousePointer = vbSizeNWSE
               
            End If
         End If
      End If
   End If
   
   If mo_HookedPictureBox.MousePointer <> l_MousePointer Then mo_HookedPictureBox.MousePointer = l_MousePointer
End Sub

Private Function ClampX(ByVal p_X As Long) As Long
   ' Clamp passed X value to image width
   If p_X < 1 Then
      p_X = 1
   ElseIf p_X > mo_SrfOrig.Width Then
      p_X = mo_SrfOrig.Width
   End If

   ClampX = p_X
End Function

Private Function ClampY(ByVal p_Y As Long) As Long
   ' Clamp passed Y value to image height
   If p_Y < 1 Then
      p_Y = 1
   ElseIf p_Y > mo_SrfOrig.Height Then
      p_Y = mo_SrfOrig.Height
   End If

   ClampY = p_Y
End Function

Private Function IsOverHandle() As Boolean
   ' Return True if mouse is over a resize handle
   If Not m_IsCropRectSel Then Exit Function  ' Can't be over a handle if crop rect isn't selected.
   
   IsOverHandle = (m_MouseHitAt = mousehit_CropRectHandleTopLeft) Or _
                  (m_MouseHitAt = mousehit_CropRectHandleBottomLeft) Or _
                  (m_MouseHitAt = mousehit_CropRectHandleTopRight) Or _
                  (m_MouseHitAt = mousehit_CropRectHandleBottomRight)
End Function

Private Sub Class_Terminate()
   If Not mo_HookedPictureBox Is Nothing Then
      mo_HookedPictureBox.ScaleMode = m_HookedPictureBoxScaleMode
   End If
End Sub
