1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Drawing2D; 5using System.Drawing.Imaging; 6using System.Windows.Forms; 7using WindowCapture.Models; 8using WindowCapture.Effects; 9 10namespace WindowCapture.UI 11{ 12 public class AnnotationCanvas : Panel 13 { 14 // Reference to editor form 15 private EditorForm editor; 16 17 // Annotations 18 public List<HighlightRect> Highlights = new List<HighlightRect>(); 19 public List<ArrowAnnotation> Arrows = new List<ArrowAnnotation>(); 20 public List<NumberMarker> Markers = new List<NumberMarker>(); 21 public List<TextBlock> TextBlocks = new List<TextBlock>(); 22 public List<CommentBubble> CommentBubbles = new List<CommentBubble>(); 23 public List<UndoAction> UndoStack = new List<UndoAction>(); 24 25 // Drawing state 26 private Point drawStart; 27 private Point currentEnd; 28 private bool tracking; 29 private bool isDrawingRect; 30 private bool isDrawingArrow; 31 private int nextMarkerNumber = 1; 32 33 // Drag state 34 private ArrowAnnotation draggingArrow; 35 private int draggingControlPoint; // 1=cp1, 2=cp2, 3=start, 4=end 36 private NumberMarker draggingMarker; 37 private HighlightRect draggingHighlight; 38 private TextBlock draggingTextBlock; 39 private CommentBubble draggingBubble; 40 private Point dragOffset; 41 42 // Crop state 43 private int cropHandle; // 0=none, 1-4=edges, 5-8=corners 44 private bool isCropping; 45 46 // Highlight resize state 47 private HighlightRect resizingHighlight; 48 private int resizeHandle; // 0=none, 1-4=edges, 5-8=corners 49 50 // Highlight clicked for blur effect (set on MouseDown, processed on MouseUp) 51 private HighlightRect clickedHighlightForBlur; 52 private MouseButtons clickedButton; 53 54 // Deferred single-click blur toggle. The single-click effect must wait out the 55 // double-click window so a double-click (motion blur) can cancel it; otherwise every 56 // double-click first fires the single-click toggle and the two effects fight each other. 57 private System.Windows.Forms.Timer blurClickTimer; 58 private HighlightRect pendingBlurHl; 59 private MouseButtons pendingBlurButton; 60 private const int DoubleClickMs = 350; 61 62 // Text editing 63 private TextBlock editingTextBlock; 64 private CommentBubble editingBubble; 65 private TextBox inputTextBox; 66 private Action<string> textInputCallback; 67 68 // Public property to check if text input is active 69 public bool IsTextInputActive { get { return inputTextBox != null && inputTextBox.Visible; } } 70 71 // Double-click detection 72 private DateTime lastClickTime; 73 private Point lastClickPoint; 74 private MouseButtons lastClickButton; 75 76 // Original bitmap for blur restore 77 private Bitmap originalBitmap; 78 79 // Animated GIF support 80 private Image animatedGifImage; 81 private bool isAnimatingGif; 82 83 // GDI fallback double-buffer 84 private Bitmap gdiBackBuffer; 85 private int gdiBackBufferW, gdiBackBufferH; 86 87 // Noise texture for Acrylic-style grain (regenerated when intensity changes) 88 private static Bitmap noiseTexture; 89 private static int noiseCachedIntensity = -1; 90 91 private static Bitmap GetNoiseTexture(int intensity) 92 { 93 if (intensity <= 0) return null; 94 if (noiseTexture != null && noiseCachedIntensity == intensity) return noiseTexture; 95 if (noiseTexture != null) { noiseTexture.Dispose(); noiseTexture = null; } 96 97 const int sz = 128; 98 var bmp = new Bitmap(sz, sz, PixelFormat.Format32bppArgb); 99 var rng = new Random(42); 100 int aMin = Math.Max(1, intensity / 2); 101 int aMax = Math.Max(aMin + 1, intensity); 102 var bits = bmp.LockBits(new Rectangle(0, 0, sz, sz), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); 103 unsafe 104 { 105 byte* ptr = (byte*)bits.Scan0; 106 for (int i = 0; i < sz * sz; i++) 107 { 108 byte v = (byte)(rng.Next(160, 255)); 109 byte a = (byte)rng.Next(aMin, aMax + 1); 110 ptr[0] = v; ptr[1] = v; ptr[2] = v; ptr[3] = a; 111 ptr += 4; 112 } 113 } 114 bmp.UnlockBits(bits); 115 noiseTexture = bmp; 116 noiseCachedIntensity = intensity; 117 return bmp; 118 } 119 120 /// <summary>Draw noise grain only on the background area (excluding the image rect).</summary> 121 private static void DrawNoiseBackground(Graphics g, int width, int height, Rectangle imageRect) 122 { 123 int intensity = Settings.NoiseIntensity; 124 if (intensity <= 0) return; 125 var noise = GetNoiseTexture(intensity); 126 if (noise == null) return; 127 128 // Clip to exclude image area, draw noise only on background 129 var clip = g.Clip; 130 using (var region = new Region(new Rectangle(0, 0, width, height))) 131 { 132 if (imageRect.Width > 0 && imageRect.Height > 0) 133 region.Exclude(imageRect); 134 g.Clip = region; 135 using (var tb = new TextureBrush(noise, WrapMode.Tile)) 136 g.FillRectangle(tb, 0, 0, width, height); 137 } 138 g.Clip = clip; 139 } 140 141 // Effect layers (smart filters) 142 private List<EffectLayer> effectLayers = new List<EffectLayer>(); 143 144 // Potential drag state (for distinguishing click from drag) 145 private Point mouseDownPoint; 146 private bool potentialDrag; 147 private HighlightRect potentialDragHighlight; 148 149 // MMB hold for dim toggle 150 private HighlightRect mmbHighlight; 151 private System.Windows.Forms.Timer mmbTimer; 152 153 // Panning (drag canvas when zoomed in) 154 private bool isPanning; 155 private Point panStart; 156 private Point canvasStartLocation; 157 158 // Cached thumbnail for smooth panning/zooming 159 private Bitmap cachedThumbnail; 160 private const int ThumbnailMaxSize = 800; // Max dimension for thumbnail 161 private bool isZooming; 162 163 // D2D rendering mode 164 private bool d2dActive; 165 private DateTime lastD2DRetry; // throttle device-loss recovery attempts 166 167 // Static Viewport Architecture 168 private float drawScale = 1.0f; 169 private float drawX = 0f; 170 private float drawY = 0f; 171 172 public float DrawScale { get { return drawScale; } } 173 public float DrawX { get { return drawX; } } 174 public float DrawY { get { return drawY; } } 175 176 public void SetViewportTransform(float scale, float x, float y) 177 { 178 drawScale = scale; 179 drawX = x; 180 drawY = y; 181 Invalidate(); 182 } 183 184 public AnnotationCanvas(EditorForm parent) 185 { 186 editor = parent; 187 188 // Hardware-accelerated rendering settings 189 DoubleBuffered = false; // DirectX handles buffering 190 SetStyle( 191 ControlStyles.AllPaintingInWmPaint | 192 ControlStyles.UserPaint | 193 ControlStyles.ResizeRedraw | 194 ControlStyles.Opaque | 195 ControlStyles.SupportsTransparentBackColor, 196 true); 197 BackColor = Color.Transparent; 198 UpdateStyles(); 199 200 // Create input textbox for text editing 201 inputTextBox = new TextBox(); 202 inputTextBox.Visible = false; 203 inputTextBox.BorderStyle = BorderStyle.FixedSingle; 204 inputTextBox.Font = new Font(Settings.TextBlockFont, Settings.TextBlockFontSize); 205 inputTextBox.Multiline = true; 206 inputTextBox.BackColor = Color.FromArgb(30, 32, 38); 207 inputTextBox.ForeColor = Color.FromArgb(220, 228, 242); 208 inputTextBox.KeyDown += InputTextBox_KeyDown; 209 inputTextBox.LostFocus += InputTextBox_LostFocus; 210 Controls.Add(inputTextBox); 211 212 // MMB timer for dim toggle 213 mmbTimer = new System.Windows.Forms.Timer(); 214 mmbTimer.Interval = 200; 215 mmbTimer.Tick += MmbTimer_Tick; 216 217 // Deferred single-click blur toggle timer 218 blurClickTimer = new System.Windows.Forms.Timer(); 219 blurClickTimer.Interval = DoubleClickMs; 220 blurClickTimer.Tick += BlurClickTimer_Tick; 221 } 222 223 private void BlurClickTimer_Tick(object sender, EventArgs e) 224 { 225 blurClickTimer.Stop(); 226 var hl = pendingBlurHl; 227 pendingBlurHl = null; 228 if (hl == null) return; 229 // No double-click arrived within the window — apply the single-click blur toggle now. 230 if (pendingBlurButton == MouseButtons.Left) 231 { 232 if (hl.CurrentBlurState == BlurState.BlurInside) RemoveBlurEffect(hl); 233 else ApplyBlurInside(hl); 234 } 235 else if (pendingBlurButton == MouseButtons.Right) 236 { 237 if (hl.CurrentBlurState == BlurState.BlurOutside) RemoveBlurEffect(hl); 238 else ApplyBlurOutside(hl); 239 } 240 } 241 242 public void SetD2DActive(bool active) 243 { 244 d2dActive = active; 245 } 246 247 private const int WM_ERASEBKGND = 0x0014; 248 249 protected override void WndProc(ref Message m) 250 { 251 if (m.Msg == WM_ERASEBKGND) { m.Result = (IntPtr)1; return; } 252 base.WndProc(ref m); 253 } 254 255 protected override void OnPaintBackground(PaintEventArgs e) 256 { 257 // Suppress background erase to fix flickering 258 } 259 260 private void MmbTimer_Tick(object sender, EventArgs e) 261 { 262 mmbTimer.Stop(); 263 if (mmbHighlight != null) 264 { 265 // 200ms elapsed - toggle dim 266 mmbHighlight.DimDisabled = !mmbHighlight.DimDisabled; 267 Invalidate(); 268 mmbHighlight = null; 269 } 270 } 271 272 public void SetOriginalBitmap(Bitmap bmp) 273 { 274 StopGifAnimation(); 275 if (originalBitmap != null) originalBitmap.Dispose(); 276 originalBitmap = (Bitmap)bmp.Clone(); 277 } 278 279 /// <summary>Load an animated GIF. The Image is NOT cloned — animation data preserved.</summary> 280 public void SetAnimatedGif(Image gif) 281 { 282 StopGifAnimation(); 283 if (originalBitmap != null) originalBitmap.Dispose(); 284 285 animatedGifImage = gif; 286 originalBitmap = new Bitmap(gif.Width, gif.Height, PixelFormat.Format32bppArgb); 287 using (var g = Graphics.FromImage(originalBitmap)) 288 g.DrawImage(gif, 0, 0, gif.Width, gif.Height); 289 290 if (ImageAnimator.CanAnimate(gif)) 291 { 292 isAnimatingGif = true; 293 ImageAnimator.Animate(gif, OnGifFrameChanged); 294 } 295 } 296 297 private void OnGifFrameChanged(object sender, EventArgs e) 298 { 299 // ImageAnimator fires from a background thread — must marshal to UI 300 if (!IsHandleCreated || IsDisposed) return; 301 try 302 { 303 BeginInvoke((Action)UpdateGifFrame); 304 } 305 catch { } 306 } 307 308 private void UpdateGifFrame() 309 { 310 if (animatedGifImage == null || originalBitmap == null) return; 311 try 312 { 313 ImageAnimator.UpdateFrames(animatedGifImage); 314 using (var g = Graphics.FromImage(originalBitmap)) 315 { 316 g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy; 317 g.DrawImage(animatedGifImage, 0, 0, originalBitmap.Width, originalBitmap.Height); 318 } 319 // Skip D2D upload per frame — too slow. Force GDI path for GIF rendering. 320 Invalidate(); 321 } 322 catch { } 323 } 324 325 public void StopGifAnimation() 326 { 327 if (isAnimatingGif && animatedGifImage != null) 328 { 329 ImageAnimator.StopAnimate(animatedGifImage, OnGifFrameChanged); 330 animatedGifImage.Dispose(); 331 animatedGifImage = null; 332 isAnimatingGif = false; 333 } 334 } 335 336 // Thumbnail management for smooth panning/zooming 337 private void EnsureThumbnailCreated() 338 { 339 if (cachedThumbnail != null) return; 340 if (originalBitmap == null) return; 341 342 int w = originalBitmap.Width; 343 int h = originalBitmap.Height; 344 float scale = 1f; 345 if (w > ThumbnailMaxSize || h > ThumbnailMaxSize) 346 scale = Math.Min((float)ThumbnailMaxSize / w, (float)ThumbnailMaxSize / h); 347 348 int tw = Math.Max(1, (int)(w * scale)); 349 int th = Math.Max(1, (int)(h * scale)); 350 351 // Check if any real effects are active 352 bool hasEffects = false; 353 foreach (var l in effectLayers) 354 if (l.Type != EffectType.None) { hasEffects = true; break; } 355 356 if (hasEffects && Highlights.Count > 0) 357 { 358 // Render effects composite at full res, then scale to thumbnail 359 using (var composite = RenderEffectsComposite()) 360 { 361 cachedThumbnail = new Bitmap(tw, th, System.Drawing.Imaging.PixelFormat.Format32bppArgb); 362 using (var g = Graphics.FromImage(cachedThumbnail)) 363 { 364 g.InterpolationMode = InterpolationMode.Bilinear; 365 g.DrawImage(composite, 0, 0, tw, th); 366 } 367 } 368 } 369 else 370 { 371 cachedThumbnail = new Bitmap(tw, th, System.Drawing.Imaging.PixelFormat.Format32bppArgb); 372 using (var g = Graphics.FromImage(cachedThumbnail)) 373 { 374 g.InterpolationMode = InterpolationMode.Bilinear; 375 g.DrawImage(originalBitmap, 0, 0, tw, th); 376 } 377 } 378 } 379 380 /// <summary>Render image + effects (no annotations) into a full-res bitmap for thumbnail caching.</summary> 381 private Bitmap RenderEffectsComposite() 382 { 383 int w = originalBitmap.Width, h = originalBitmap.Height; 384 var result = new Bitmap(w, h, System.Drawing.Imaging.PixelFormat.Format32bppArgb); 385 using (var g = Graphics.FromImage(result)) 386 { 387 g.InterpolationMode = InterpolationMode.HighQualityBicubic; 388 389 bool anyDimDisabled = false; 390 bool hasOutsideEffect = false; 391 HighlightRect outsideHL = null; 392 393 foreach (var hl in Highlights) 394 { 395 if (hl.DimDisabled) anyDimDisabled = true; 396 var layer = FindEffectLayer(hl.Id); 397 if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) 398 { hasOutsideEffect = true; outsideHL = hl; } 399 } 400 401 // Draw base 402 if (anyDimDisabled) 403 { 404 g.DrawImageUnscaled(originalBitmap, 0, 0); 405 } 406 else if (hasOutsideEffect && outsideHL != null) 407 { 408 // BlurOutside: base already has blur+dim everywhere except its own rect. 409 var layer = FindEffectLayer(outsideHL.Id); 410 var effectBmp = layer.GetCachedResult(originalBitmap); 411 if (effectBmp != null) 412 g.DrawImageUnscaled(effectBmp, 0, 0); 413 else if (editor.BlurredBitmap != null) 414 g.DrawImageUnscaled(editor.BlurredBitmap, 0, 0); 415 else 416 g.DrawImageUnscaled(originalBitmap, 0, 0); 417 } 418 else 419 { 420 // Inside-effect or no-effect base. With "dim around selection" on, use the dimmed 421 // image (restore loop un-dims the selected regions); otherwise a clear base. 422 if (Settings.DimAroundSelection && editor.BlurredBitmap != null) 423 g.DrawImageUnscaled(editor.BlurredBitmap, 0, 0); 424 else 425 g.DrawImageUnscaled(originalBitmap, 0, 0); 426 } 427 428 // Draw highlight regions 429 foreach (var hl in Highlights) 430 { 431 var layer = FindEffectLayer(hl.Id); 432 if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) 433 continue; 434 435 g.SetClip(hl.Rect); 436 if (layer != null && layer.Type != EffectType.None) 437 { 438 var effectBmp = layer.GetCachedResult(originalBitmap); 439 if (effectBmp != null) 440 g.DrawImageUnscaled(effectBmp, 0, 0); 441 else 442 g.DrawImageUnscaled(originalBitmap, 0, 0); 443 } 444 else 445 { 446 g.DrawImageUnscaled(originalBitmap, 0, 0); 447 } 448 } 449 g.ResetClip(); 450 } 451 return result; 452 } 453 454 private void DisposeThumbnail() 455 { 456 if (cachedThumbnail != null) { cachedThumbnail.Dispose(); cachedThumbnail = null; } 457 } 458 459 public void BeginZoomAnimation() 460 { 461 isZooming = true; 462 // Skip thumbnail when effects are active — D2D renders blur/dim via GPU shaders 463 // which can't be pre-baked into a static bitmap. Let D2D render at full quality. 464 bool hasEffects = false; 465 foreach (var l in effectLayers) 466 if (l.Type != EffectType.None) { hasEffects = true; break; } 467 // Also skip the (raw) thumbnail when the spotlight dim is active, so pan/zoom renders the 468 // same dimmed composite as the static view instead of flashing the brighter raw image. 469 if (!hasEffects && !(Settings.DimAroundSelection && Highlights.Count > 0)) 470 EnsureThumbnailCreated(); 471 } 472 473 public void EndZoomAnimation() 474 { 475 isZooming = false; 476 if (!isPanning) DisposeThumbnail(); 477 Invalidate(); 478 } 479 private bool UseFastRendering { get { return isPanning || isZooming; } } 480 481 // Convert viewport coords to image coords (accounting for drawScale/X/Y) 482 private Point ToImageCoords(Point localPt) 483 { 484 return new Point( 485 (int)Math.Round((localPt.X - drawX) / drawScale), 486 (int)Math.Round((localPt.Y - drawY) / drawScale)); 487 } 488 489 // Convert image coords to viewport coords 490 private Point ToScreenCoords(Point imgPt) 491 { 492 return new Point( 493 (int)Math.Round(imgPt.X * drawScale + drawX), 494 (int)Math.Round(imgPt.Y * drawScale + drawY)); 495 } 496 497 // Check which crop handle is at position 498 private int GetCropHandle(Point imgPt) 499 { 500 int m = 6; var cr = editor.CropRect; 501 if (Math.Abs(imgPt.X - cr.Left) < m && Math.Abs(imgPt.Y - cr.Top) < m) return 5; 502 if (Math.Abs(imgPt.X - cr.Right) < m && Math.Abs(imgPt.Y - cr.Top) < m) return 6; 503 if (Math.Abs(imgPt.X - cr.Left) < m && Math.Abs(imgPt.Y - cr.Bottom) < m) return 7; 504 if (Math.Abs(imgPt.X - cr.Right) < m && Math.Abs(imgPt.Y - cr.Bottom) < m) return 8; 505 if (Math.Abs(imgPt.X - cr.Left) < m && imgPt.Y > cr.Top && imgPt.Y < cr.Bottom) return 1; 506 if (Math.Abs(imgPt.X - cr.Right) < m && imgPt.Y > cr.Top && imgPt.Y < cr.Bottom) return 2; 507 if (Math.Abs(imgPt.Y - cr.Top) < m && imgPt.X > cr.Left && imgPt.X < cr.Right) return 3; 508 if (Math.Abs(imgPt.Y - cr.Bottom) < m && imgPt.X > cr.Left && imgPt.X < cr.Right) return 4; 509 return 0; 510 } 511 512 private Cursor GetCursorForHandle(int handle) 513 { 514 switch (handle) 515 { 516 case 1: case 2: return Cursors.SizeWE; 517 case 3: case 4: return Cursors.SizeNS; 518 case 5: case 8: return Cursors.SizeNWSE; 519 case 6: case 7: return Cursors.SizeNESW; 520 default: return Cursors.Default; 521 } 522 } 523 524 protected override void OnMouseWheel(MouseEventArgs e) 525 { 526 base.OnMouseWheel(e); 527 528 // RMB held: resize text/bubble font or arrow width under cursor 529 if ((Control.MouseButtons & MouseButtons.Right) != 0) 530 { 531 var imgPt = ToImageCoords(e.Location); 532 using (var g = CreateGraphics()) 533 { 534 foreach (var tb in TextBlocks) 535 { 536 if (tb.Contains(imgPt, g)) 537 { 538 float step = e.Delta > 0 ? 1f : -1f; 539 float newSize = tb.TextFont.Size + step; 540 if (newSize >= 6f && newSize <= 300f) 541 { 542 tb.TextFont = new Font(tb.TextFont.FontFamily, newSize, tb.TextFont.Style); 543 Invalidate(); 544 } 545 return; 546 } 547 } 548 foreach (var bubble in CommentBubbles) 549 { 550 if (bubble.Contains(imgPt, g)) 551 { 552 float step = e.Delta > 0 ? 1f : -1f; 553 float newSize = bubble.TextFont.Size + step; 554 if (newSize >= 6f && newSize <= 300f) 555 { 556 bubble.TextFont = new Font(bubble.TextFont.FontFamily, newSize, bubble.TextFont.Style); 557 Invalidate(); 558 } 559 return; 560 } 561 } 562 } 563 foreach (var arr in Arrows) 564 { 565 int cpSize = (int)(14 / drawScale); 566 if (arr.GetStartRect(cpSize).Contains(imgPt) || arr.GetEndRect(cpSize).Contains(imgPt) || 567 arr.GetControl1Rect(cpSize).Contains(imgPt) || arr.GetControl2Rect(cpSize).Contains(imgPt)) 568 { 569 float step = e.Delta > 0 ? 0.5f : -0.5f; 570 float newWidth = arr.Width + step; 571 if (newWidth >= 1f && newWidth <= 20f) { arr.Width = newWidth; Invalidate(); } 572 return; 573 } 574 } 575 // If RMB+wheel not over any element, fall through to zoom 576 } 577 578 // Viewer mode: wheel browses files, Ctrl+wheel zooms 579 if (editor.IsViewerMode && !editor.IsVideoFile && !editor.IsAudioFile && (Control.ModifierKeys & Keys.Control) == 0) 580 { 581 editor.BrowseFile(e.Delta > 0 ? -1 : 1); 582 return; 583 } 584 585 float delta = e.Delta > 0 ? 0.1f : -0.1f; 586 editor.ApplyZoom(delta, e.Location); 587 } 588 589 // Window drag from background area 590 private bool canvasDraggingForm; 591 private Point canvasDragStart; 592 593 protected override void OnMouseDown(MouseEventArgs e) 594 { 595 base.OnMouseDown(e); 596 if (e.Button == MouseButtons.Left && editor.TryClickGlassButton(e.Location)) return; 597 Focus(); 598 599 // Check if click is outside the image area → start window drag 600 if (e.Button == MouseButtons.Left && originalBitmap != null) 601 { 602 float iw = originalBitmap.Width * drawScale; 603 float ih = originalBitmap.Height * drawScale; 604 var imageRect = new RectangleF(drawX, drawY, iw, ih); 605 if (!imageRect.Contains(e.Location)) 606 { 607 canvasDraggingForm = true; 608 canvasDragStart = e.Location; 609 Cursor = Cursors.SizeAll; 610 return; 611 } 612 } 613 614 var imgPt = ToImageCoords(e.Location); 615 bool isDoubleClick = (DateTime.Now - lastClickTime).TotalMilliseconds < DoubleClickMs && Math.Abs(e.Location.X - lastClickPoint.X) < 15 && Math.Abs(e.Location.Y - lastClickPoint.Y) < 15 && e.Button == lastClickButton; 616 lastClickTime = DateTime.Now; lastClickPoint = e.Location; lastClickButton = e.Button; 617 if (isDoubleClick) { HandleDoubleClick(e.Button, imgPt); return; } 618 if (e.Button == MouseButtons.Left) 619 { 620 if (editor.AltPressed) return; 621 int handle = GetCropHandle(imgPt); 622 if (handle > 0) { cropHandle = handle; isCropping = true; tracking = true; return; } 623 int cpSize = (int)(14 / drawScale); 624 foreach (var arr in Arrows) 625 { 626 if (arr.GetStartRect(cpSize).Contains(imgPt)) { draggingArrow = arr; draggingControlPoint = 3; tracking = true; return; } 627 if (arr.GetEndRect(cpSize).Contains(imgPt)) { draggingArrow = arr; draggingControlPoint = 4; tracking = true; return; } 628 if (arr.GetControl1Rect(cpSize).Contains(imgPt)) { draggingArrow = arr; draggingControlPoint = 1; tracking = true; return; } 629 if (arr.GetControl2Rect(cpSize).Contains(imgPt)) { draggingArrow = arr; draggingControlPoint = 2; tracking = true; return; } 630 } 631 int markerSize = Settings.MarkerSize; 632 foreach (var mk in Markers) if (new Rectangle(mk.Location.X - markerSize / 2, mk.Location.Y - markerSize / 2, markerSize, markerSize).Contains(imgPt)) { draggingMarker = mk; dragOffset = new Point(imgPt.X - mk.Location.X, imgPt.Y - mk.Location.Y); tracking = true; return; } 633 foreach (var hl in Highlights) 634 { 635 int hlHandle = GetHighlightHandle(hl, imgPt); 636 if (hlHandle > 0) { resizingHighlight = hl; resizeHandle = hlHandle; tracking = true; return; } 637 if (hl.Rect.Contains(imgPt)) { mouseDownPoint = imgPt; potentialDrag = true; potentialDragHighlight = hl; clickedHighlightForBlur = hl; clickedButton = e.Button; return; } 638 var outer = new Rectangle(hl.Rect.X - 12, hl.Rect.Y - 12, hl.Rect.Width + 24, hl.Rect.Height + 24); 639 if (outer.Contains(imgPt) && !hl.Rect.Contains(imgPt)) { draggingHighlight = hl; dragOffset = new Point(imgPt.X - hl.Rect.X, imgPt.Y - hl.Rect.Y); tracking = true; return; } 640 } 641 foreach (var tb in TextBlocks) { using (var g = CreateGraphics()) { var size = g.MeasureString(tb.Text, tb.TextFont); if (new Rectangle(tb.Location.X, tb.Location.Y, (int)size.Width + 10, (int)size.Height + 5).Contains(imgPt)) { draggingTextBlock = tb; dragOffset = new Point(imgPt.X - tb.Location.X, imgPt.Y - tb.Location.Y); tracking = true; return; } } } 642 using (var g = CreateGraphics()) foreach (var bubble in CommentBubbles) if (bubble.Contains(imgPt, g)) { draggingBubble = bubble; dragOffset = new Point(imgPt.X - bubble.Location.X, imgPt.Y - bubble.Location.Y); tracking = true; return; } 643 isDrawingRect = true; drawStart = imgPt; currentEnd = imgPt; tracking = true; 644 } 645 else if (e.Button == MouseButtons.Right) 646 { 647 foreach (var hl in Highlights) if (hl.Rect.Contains(imgPt)) { clickedHighlightForBlur = hl; clickedButton = e.Button; return; } 648 isDrawingArrow = true; drawStart = imgPt; currentEnd = imgPt; tracking = true; 649 } 650 else if (e.Button == MouseButtons.Middle) 651 { 652 foreach (var hl in Highlights) if (hl.Rect.Contains(imgPt)) { mmbHighlight = hl; mmbTimer.Start(); return; } 653 isPanning = true; panStart = e.Location; canvasStartLocation = new Point((int)drawX, (int)drawY); Cursor = Cursors.SizeAll; 654 bool panHasEffects = false; foreach (var l in effectLayers) if (l.Type != EffectType.None) { panHasEffects = true; break; } 655 if (!panHasEffects && !(Settings.DimAroundSelection && Highlights.Count > 0)) EnsureThumbnailCreated(); 656 } 657 } 658 659 protected override void OnMouseMove(MouseEventArgs e) 660 { 661 base.OnMouseMove(e); 662 if (canvasDraggingForm) 663 { 664 var form = editor; 665 form.Location = new Point( 666 form.Location.X + e.Location.X - canvasDragStart.X, 667 form.Location.Y + e.Location.Y - canvasDragStart.Y); 668 return; 669 } 670 if (isPanning) 671 { 672 int dx = e.Location.X - panStart.X; int dy = e.Location.Y - panStart.Y; 673 float newX = canvasStartLocation.X + dx; float newY = canvasStartLocation.Y + dy; 674 int vw = Width, vh = Height; float imgW = originalBitmap.Width * drawScale, imgH = originalBitmap.Height * drawScale; 675 if (imgW <= vw) newX = (vw - imgW) / 2f; else newX = Math.Max(vw - imgW, Math.Min(0, newX)); 676 if (imgH <= vh) newY = (vh - imgH) / 2f; else newY = Math.Max(vh - imgH, Math.Min(0, newY)); 677 SetViewportTransform(drawScale, newX, newY); return; 678 } 679 var imgPt = ToImageCoords(e.Location); 680 if (potentialDrag && potentialDragHighlight != null) { if (Math.Abs(imgPt.X - mouseDownPoint.X) > 5 || Math.Abs(imgPt.Y - mouseDownPoint.Y) > 5) { draggingHighlight = potentialDragHighlight; dragOffset = new Point(mouseDownPoint.X - potentialDragHighlight.Rect.X, mouseDownPoint.Y - potentialDragHighlight.Rect.Y); clickedHighlightForBlur = null; potentialDrag = false; potentialDragHighlight = null; tracking = true; } } 681 if (isCropping && cropHandle > 0) 682 { 683 var cr = editor.CropRect; int minSize = 20; 684 switch (cropHandle) 685 { 686 case 1: cr = new Rectangle(Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), cr.Y, cr.Right - Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), cr.Height); break; 687 case 2: cr = new Rectangle(cr.X, cr.Y, Math.Max(cr.Left + minSize, Math.Min(imgPt.X, editor.Bounds.Width)) - cr.X, cr.Height); break; 688 case 3: cr = new Rectangle(cr.X, Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize)), cr.Width, cr.Bottom - Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize))); break; 689 case 4: cr = new Rectangle(cr.X, cr.Y, cr.Width, Math.Max(cr.Top + minSize, Math.Min(imgPt.Y, editor.Bounds.Height)) - cr.Y); break; 690 case 5: cr = new Rectangle(Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize)), cr.Right - Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), cr.Bottom - Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize))); break; 691 case 6: cr = new Rectangle(cr.X, Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize)), Math.Max(cr.Left + minSize, Math.Min(imgPt.X, editor.Bounds.Width)) - cr.X, cr.Bottom - Math.Max(0, Math.Min(imgPt.Y, cr.Bottom - minSize))); break; 692 case 7: cr = new Rectangle(Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), cr.Y, cr.Right - Math.Max(0, Math.Min(imgPt.X, cr.Right - minSize)), Math.Max(cr.Top + minSize, Math.Min(imgPt.Y, editor.Bounds.Height)) - cr.Y); break; 693 case 8: cr = new Rectangle(cr.X, cr.Y, Math.Max(cr.Left + minSize, Math.Min(imgPt.X, editor.Bounds.Width)) - cr.X, Math.Max(cr.Top + minSize, Math.Min(imgPt.Y, editor.Bounds.Height)) - cr.Y); break; 694 } 695 editor.SetCropRect(cr); Invalidate(); return; 696 } 697 if (resizingHighlight != null && resizeHandle > 0) 698 { 699 var r = resizingHighlight.Rect; int minSize = 20; 700 switch (resizeHandle) 701 { 702 case 1: resizingHighlight.Rect = new Rectangle(Math.Min(imgPt.X, r.Right - minSize), r.Y, r.Right - Math.Min(imgPt.X, r.Right - minSize), r.Height); break; 703 case 2: resizingHighlight.Rect = new Rectangle(r.X, r.Y, Math.Max(r.Left + minSize, imgPt.X) - r.X, r.Height); break; 704 case 3: resizingHighlight.Rect = new Rectangle(r.X, Math.Min(imgPt.Y, r.Bottom - minSize), r.Width, r.Bottom - Math.Min(imgPt.Y, r.Bottom - minSize)); break; 705 case 4: resizingHighlight.Rect = new Rectangle(r.X, r.Y, r.Width, Math.Max(r.Top + minSize, imgPt.Y) - r.Y); break; 706 case 5: resizingHighlight.Rect = new Rectangle(Math.Min(imgPt.X, r.Right - minSize), Math.Min(imgPt.Y, r.Bottom - minSize), r.Right - Math.Min(imgPt.X, r.Right - minSize), r.Bottom - Math.Min(imgPt.Y, r.Bottom - minSize)); break; 707 case 6: resizingHighlight.Rect = new Rectangle(r.X, Math.Min(imgPt.Y, r.Bottom - minSize), Math.Max(r.Left + minSize, imgPt.X) - r.X, r.Bottom - Math.Min(imgPt.Y, r.Bottom - minSize)); break; 708 case 7: resizingHighlight.Rect = new Rectangle(Math.Min(imgPt.X, r.Right - minSize), r.Y, r.Right - Math.Min(imgPt.X, r.Right - minSize), Math.Max(r.Top + minSize, imgPt.Y) - r.Y); break; 709 case 8: resizingHighlight.Rect = new Rectangle(r.X, r.Y, Math.Max(r.Left + minSize, imgPt.X) - r.X, Math.Max(r.Top + minSize, imgPt.Y) - r.Y); break; 710 } 711 SyncEffectLayerPosition(resizingHighlight); Invalidate(); return; 712 } 713 if (draggingArrow != null) { if (draggingControlPoint == 1) draggingArrow.Control1 = imgPt; else if (draggingControlPoint == 2) draggingArrow.Control2 = imgPt; else if (draggingControlPoint == 3) { int dx = imgPt.X - draggingArrow.Start.X; int dy = imgPt.Y - draggingArrow.Start.Y; draggingArrow.Start = imgPt; draggingArrow.Control1 = new PointF(draggingArrow.Control1.X + dx, draggingArrow.Control1.Y + dy); } else if (draggingControlPoint == 4) { int dx = imgPt.X - draggingArrow.End.X; int dy = imgPt.Y - draggingArrow.End.Y; draggingArrow.End = imgPt; draggingArrow.Control2 = new PointF(draggingArrow.Control2.X + dx, draggingArrow.Control2.Y + dy); } draggingArrow.HasCurve = true; Invalidate(); return; } 714 if (draggingMarker != null) { draggingMarker.Location = new Point(imgPt.X - dragOffset.X, imgPt.Y - dragOffset.Y); Invalidate(); return; } 715 if (draggingHighlight != null) { draggingHighlight.Rect = new Rectangle(imgPt.X - dragOffset.X, imgPt.Y - dragOffset.Y, draggingHighlight.Rect.Width, draggingHighlight.Rect.Height); SyncEffectLayerPosition(draggingHighlight); Invalidate(); return; } 716 if (draggingTextBlock != null) { draggingTextBlock.Location = new Point(imgPt.X - dragOffset.X, imgPt.Y - dragOffset.Y); Invalidate(); return; } 717 if (draggingBubble != null) { int dx = imgPt.X - dragOffset.X - draggingBubble.Location.X; int dy = imgPt.Y - dragOffset.Y - draggingBubble.Location.Y; draggingBubble.Location = new Point(imgPt.X - dragOffset.X, imgPt.Y - dragOffset.Y); draggingBubble.TailPoint = new Point(draggingBubble.TailPoint.X + dx, draggingBubble.TailPoint.Y + dy); Invalidate(); return; } 718 if (tracking) { currentEnd = imgPt; Invalidate(); } 719 if (editor.AltPressed) editor.UpdateAutoDetect(imgPt); 720 UpdateCursor(imgPt); 721 editor.UpdateGlassButtonsFromCanvas(e.Location); 722 } 723 724 private void UpdateCursor(Point loc) 725 { 726 if (editor.AltPressed) { Cursor = Cursors.Cross; return; } 727 int handle = GetCropHandle(loc); 728 if (handle > 0) { Cursor = GetCursorForHandle(handle); return; } 729 int cpSize = 14; 730 foreach (var arr in Arrows) if (arr.GetStartRect(cpSize).Contains(loc) || arr.GetEndRect(cpSize).Contains(loc) || arr.GetControl1Rect(cpSize).Contains(loc) || arr.GetControl2Rect(cpSize).Contains(loc)) { Cursor = Cursors.SizeAll; return; } 731 int markerSize = Settings.MarkerSize; 732 foreach (var mk in Markers) if (new Rectangle(mk.Location.X - markerSize / 2, mk.Location.Y - markerSize / 2, markerSize, markerSize).Contains(loc)) { Cursor = Cursors.SizeAll; return; } 733 foreach (var hl in Highlights) { int hlHandle = GetHighlightHandle(hl, loc); if (hlHandle > 0) { Cursor = GetCursorForHandle(hlHandle); return; } if (hl.Rect.Contains(loc)) { Cursor = Cursors.Hand; return; } var outer = new Rectangle(hl.Rect.X - 12, hl.Rect.Y - 12, hl.Rect.Width + 24, hl.Rect.Height + 24); if (outer.Contains(loc)) { Cursor = Cursors.SizeAll; return; } } 734 Cursor = Cursors.Default; 735 } 736 737 protected override void OnMouseUp(MouseEventArgs e) 738 { 739 base.OnMouseUp(e); 740 if (canvasDraggingForm) { canvasDraggingForm = false; Cursor = Cursors.Default; return; } 741 var imgPt = ToImageCoords(e.Location); potentialDrag = false; potentialDragHighlight = null; 742 if (e.Button == MouseButtons.Middle) { if (mmbHighlight != null) { mmbTimer.Stop(); var mk = new NumberMarker(imgPt, nextMarkerNumber++); Markers.Add(mk); UndoStack.Add(new UndoAction("marker", mk)); mmbHighlight = null; Invalidate(); return; } if (isPanning) { int dx = Math.Abs(e.Location.X - panStart.X); int dy = Math.Abs(e.Location.Y - panStart.Y); isPanning = false; Cursor = Cursors.Default; if (!isZooming) DisposeThumbnail(); if (dx < 5 && dy < 5) { var mk = new NumberMarker(imgPt, nextMarkerNumber++); Markers.Add(mk); UndoStack.Add(new UndoAction("marker", mk)); } Invalidate(); return; } } 743 if (clickedHighlightForBlur != null) { var hl = clickedHighlightForBlur; clickedHighlightForBlur = null; if (hl.Rect.Contains(imgPt)) { pendingBlurHl = hl; pendingBlurButton = clickedButton; blurClickTimer.Stop(); blurClickTimer.Start(); } return; } 744 if (e.Button == MouseButtons.Left) 745 { 746 if (isCropping) { isCropping = false; cropHandle = 0; tracking = false; Invalidate(); return; } 747 if (resizingHighlight != null) { resizingHighlight = null; resizeHandle = 0; tracking = false; Invalidate(); return; } 748 if (draggingArrow != null) { draggingArrow = null; draggingControlPoint = 0; tracking = false; Invalidate(); return; } 749 if (draggingMarker != null) { draggingMarker = null; tracking = false; Invalidate(); return; } 750 if (draggingHighlight != null) { draggingHighlight = null; tracking = false; Invalidate(); return; } 751 if (draggingTextBlock != null) { draggingTextBlock = null; tracking = false; Invalidate(); return; } 752 if (draggingBubble != null) { draggingBubble = null; tracking = false; Invalidate(); return; } 753 if (isDrawingRect) { var rect = MakeRect(drawStart, imgPt); if (rect.Width > 5 && rect.Height > 5) { var hl = new HighlightRect(rect); Highlights.Add(hl); UndoStack.Add(new UndoAction("highlight", hl)); } isDrawingRect = false; tracking = false; Invalidate(); } 754 } 755 else if (e.Button == MouseButtons.Right && isDrawingArrow) { var dx = imgPt.X - drawStart.X; var dy = imgPt.Y - drawStart.Y; if (Math.Sqrt(dx * dx + dy * dy) > 10) { var arr = new ArrowAnnotation(drawStart, imgPt); Arrows.Add(arr); UndoStack.Add(new UndoAction("arrow", arr)); } isDrawingArrow = false; tracking = false; Invalidate(); } 756 } 757 758 private void HandleDoubleClick(MouseButtons button, Point imgPt) 759 { 760 isDrawingArrow = false; isDrawingRect = false; tracking = false; 761 // Cancel any pending single-click blur toggle — this gesture is a double-click. 762 blurClickTimer.Stop(); pendingBlurHl = null; 763 if (button == MouseButtons.Left) { foreach (var hl in Highlights) if (hl.Rect.Contains(imgPt)) { if (hl.CurrentBlurState == BlurState.MotionInside) RemoveBlurEffect(hl); else ApplyMotionBlurInside(hl); return; } CreateTextBlock(imgPt); } 764 else if (button == MouseButtons.Right) { foreach (var hl in Highlights) if (hl.Rect.Contains(imgPt)) { if (hl.CurrentBlurState == BlurState.MotionOutside) RemoveBlurEffect(hl); else ApplyMotionBlurOutside(hl); return; } CreateCommentBubble(imgPt); } 765 } 766 767 private int GetHighlightHandle(HighlightRect hl, Point imgPt) { int m = 8; var r = hl.Rect; if (Math.Abs(imgPt.X - r.Left) < m && Math.Abs(imgPt.Y - r.Top) < m) return 5; if (Math.Abs(imgPt.X - r.Right) < m && Math.Abs(imgPt.Y - r.Top) < m) return 6; if (Math.Abs(imgPt.X - r.Left) < m && Math.Abs(imgPt.Y - r.Bottom) < m) return 7; if (Math.Abs(imgPt.X - r.Right) < m && Math.Abs(imgPt.Y - r.Bottom) < m) return 8; if (Math.Abs(imgPt.X - r.Left) < m && imgPt.Y > r.Top && imgPt.Y < r.Bottom) return 1; if (Math.Abs(imgPt.X - r.Right) < m && imgPt.Y > r.Top && imgPt.Y < r.Bottom) return 2; if (Math.Abs(imgPt.Y - r.Top) < m && imgPt.X > r.Left && imgPt.X < r.Right) return 3; if (Math.Abs(imgPt.Y - r.Bottom) < m && imgPt.X > r.Left && imgPt.X < r.Right) return 4; return 0; } 768 private EffectLayer FindEffectLayer(int highlightId) { foreach (var layer in effectLayers) if (layer.HighlightId == highlightId) return layer; return null; } 769 public EffectLayer GetEffectLayer(int highlightId) { return FindEffectLayer(highlightId); } 770 private void RemoveEffectLayer(int highlightId) { var layer = FindEffectLayer(highlightId); if (layer != null) { layer.Dispose(); effectLayers.Remove(layer); } } 771 private void ApplyBlurInside(HighlightRect hl) { if (originalBitmap == null) return; RemoveEffectLayer(hl.Id); var layer = new EffectLayer(hl.Id) { Type = EffectType.BlurInside, TargetRect = hl.Rect, Intensity = (int)Math.Round(hl.BlurRadius), FeatherWidth = Settings.FeatherWidth }; effectLayers.Add(layer); hl.CurrentBlurState = BlurState.BlurInside; Invalidate(); } 772 private void ApplyBlurOutside(HighlightRect hl) { if (originalBitmap == null) return; RemoveEffectLayer(hl.Id); var layer = new EffectLayer(hl.Id) { Type = EffectType.BlurOutside, TargetRect = hl.Rect, Intensity = (int)Math.Round(hl.BlurRadius), FeatherWidth = Settings.FeatherWidth }; effectLayers.Add(layer); hl.CurrentBlurState = BlurState.BlurOutside; Invalidate(); } 773 private void ApplyMotionBlurInside(HighlightRect hl) { if (originalBitmap == null) return; RemoveEffectLayer(hl.Id); var layer = new EffectLayer(hl.Id) { Type = EffectType.MotionBlurInside, TargetRect = hl.Rect, Intensity = hl.MotionBlurDistance, FeatherWidth = Settings.FeatherWidth }; effectLayers.Add(layer); hl.CurrentBlurState = BlurState.MotionInside; Invalidate(); } 774 private void ApplyMotionBlurOutside(HighlightRect hl) { if (originalBitmap == null) return; RemoveEffectLayer(hl.Id); var layer = new EffectLayer(hl.Id) { Type = EffectType.MotionBlurOutside, TargetRect = hl.Rect, Intensity = hl.MotionBlurDistance, FeatherWidth = Settings.FeatherWidth }; effectLayers.Add(layer); hl.CurrentBlurState = BlurState.MotionOutside; Invalidate(); } 775 private void RemoveBlurEffect(HighlightRect hl) { RemoveEffectLayer(hl.Id); hl.CurrentBlurState = BlurState.None; Invalidate(); } 776 private void SyncEffectLayerPosition(HighlightRect hl) { var layer = FindEffectLayer(hl.Id); if (layer != null) { layer.TargetRect = hl.Rect; layer.Invalidate(); } } 777 public Bitmap GetCompositeImage() { if (originalBitmap == null) return null; if (effectLayers.Count == 0) return originalBitmap; Bitmap result = null; foreach (var layer in effectLayers) { var effectBitmap = layer.GetCachedResult(originalBitmap); if (effectBitmap != null) result = effectBitmap; } return result ?? originalBitmap; } 778 public Bitmap GetFullCompositeImage() { if (originalBitmap == null) return null; Bitmap baseImage = GetCompositeImage() ?? originalBitmap; Bitmap result = new Bitmap(baseImage.Width, baseImage.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb); using (Graphics g = Graphics.FromImage(result)) { g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; g.InterpolationMode = InterpolationMode.HighQualityBicubic; if (Highlights.Count > 0) { bool hasOutsideEffect = false; HighlightRect outsideEffectHighlight = null; bool anyDimDisabled = false; foreach (var hl in Highlights) { if (hl.DimDisabled) anyDimDisabled = true; var layer = FindEffectLayer(hl.Id); if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) { hasOutsideEffect = true; outsideEffectHighlight = hl; } } if (anyDimDisabled) g.DrawImage(originalBitmap, 0, 0, baseImage.Width, baseImage.Height); else if (hasOutsideEffect && outsideEffectHighlight != null) { var layer = FindEffectLayer(outsideEffectHighlight.Id); var effectBitmap = layer.GetCachedResult(originalBitmap); if (effectBitmap != null) g.DrawImage(effectBitmap, 0, 0, baseImage.Width, baseImage.Height); else g.DrawImage(originalBitmap, 0, 0, baseImage.Width, baseImage.Height); } else if (Settings.DimAroundSelection && editor.BlurredBitmap != null) g.DrawImage(editor.BlurredBitmap, 0, 0, baseImage.Width, baseImage.Height); else g.DrawImage(originalBitmap, 0, 0, baseImage.Width, baseImage.Height); foreach (var hl in Highlights) { var layer = FindEffectLayer(hl.Id); if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) continue; if (layer != null && layer.Type != EffectType.None) { var effectBitmap = layer.GetCachedResult(originalBitmap); if (effectBitmap != null) { g.SetClip(hl.Rect); g.DrawImage(effectBitmap, 0, 0, baseImage.Width, baseImage.Height); } } else { g.SetClip(hl.Rect); g.DrawImage(originalBitmap, 0, 0, baseImage.Width, baseImage.Height); } } g.ResetClip(); } else g.DrawImage(baseImage, 0, 0, baseImage.Width, baseImage.Height); if (Settings.ShowHighlightBorder) foreach (var hl in Highlights) { using (var shadowPen = new Pen(Color.FromArgb(60, 0, 0, 0), Settings.HighlightBorderWidth + 2)) g.DrawRectangle(shadowPen, new Rectangle(hl.Rect.X + 2, hl.Rect.Y + 2, hl.Rect.Width, hl.Rect.Height)); using (var pen = new Pen(Settings.HighlightBorderColor, Settings.HighlightBorderWidth)) g.DrawRectangle(pen, hl.Rect); } foreach (var arr in Arrows) DrawArrowForExport(g, arr); foreach (var mk in Markers) DrawMarker(g, mk); foreach (var tb in TextBlocks) DrawTextBlock(g, tb); foreach (var bubble in CommentBubbles) DrawBubble(g, bubble); } return result; } 779 private void DrawArrowForExport(Graphics g, ArrowAnnotation arr) { float headLen = 20f, headWidth = 10f; var tangent = arr.GetBezierTangent(1.0f); var endPt = new PointF(arr.End.X, arr.End.Y); var headBase = new PointF(endPt.X - tangent.X * headLen, endPt.Y - tangent.Y * headLen); var headLeft = new PointF(headBase.X + (-tangent.Y * headWidth), headBase.Y + (tangent.X * headWidth)); var headRight = new PointF(headBase.X - (-tangent.Y * headWidth), headBase.Y - (tangent.X * headWidth)); using (var shadowPen = new Pen(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0), arr.Width + 2)) { shadowPen.StartCap = LineCap.Round; shadowPen.EndCap = LineCap.Round; g.DrawBezier(shadowPen, new PointF(arr.Start.X + Settings.ShadowOffset, arr.Start.Y + Settings.ShadowOffset), new PointF(arr.Control1.X + Settings.ShadowOffset, arr.Control1.Y + Settings.ShadowOffset), new PointF(arr.Control2.X + Settings.ShadowOffset, arr.Control2.Y + Settings.ShadowOffset), new PointF(headBase.X + Settings.ShadowOffset, headBase.Y + Settings.ShadowOffset)); } using (var shadowBrush = new SolidBrush(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0))) g.FillPolygon(shadowBrush, new PointF[] { new PointF(endPt.X + Settings.ShadowOffset, endPt.Y + Settings.ShadowOffset), new PointF(headLeft.X + Settings.ShadowOffset, headLeft.Y + Settings.ShadowOffset), new PointF(headRight.X + Settings.ShadowOffset, headRight.Y + Settings.ShadowOffset) }); using (var pen = new Pen(arr.Color, arr.Width)) { pen.StartCap = LineCap.Round; pen.EndCap = LineCap.Round; g.DrawBezier(pen, arr.Start, arr.Control1, arr.Control2, headBase); } using (var brush = new SolidBrush(arr.Color)) g.FillPolygon(brush, new PointF[] { endPt, headLeft, headRight }); } 780 private void CreateTextBlock(Point location) { var textBlock = new TextBlock(location); TextBlocks.Add(textBlock); editingTextBlock = textBlock; ShowTextInput(location, textBlock.TextFont, delegate(string text) { textBlock.Text = text; textBlock.IsEditing = false; if (string.IsNullOrEmpty(text)) TextBlocks.Remove(textBlock); else UndoStack.Add(new UndoAction("text", textBlock)); editingTextBlock = null; Invalidate(); }); } 781 private void CreateCommentBubble(Point location) { var bubble = new CommentBubble(location); CommentBubbles.Add(bubble); editingBubble = bubble; ShowTextInput(location, bubble.TextFont, delegate(string text) { bubble.Text = text; bubble.IsEditing = false; if (string.IsNullOrEmpty(text)) CommentBubbles.Remove(bubble); else UndoStack.Add(new UndoAction("bubble", bubble)); editingBubble = null; Invalidate(); }); } 782 private void ShowTextBlockPopup(TextBlock tb, Point controlLocation) { var screenPt = this.PointToScreen(controlLocation); screenPt.X += 10; screenPt.Y += 10; var popup = new TextBlockPopup(tb, screenPt, () => { Invalidate(); }); popup.Show(); } 783 private void ShowTextInput(Point imgLocation, Font font, Action<string> callback) { textInputCallback = callback; var pt = ToScreenCoords(imgLocation); inputTextBox.Location = pt; inputTextBox.Font = font; inputTextBox.Text = ""; inputTextBox.Size = new Size(200, 60); inputTextBox.Visible = true; inputTextBox.Focus(); } 784 private void InputTextBox_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter && !e.Shift) { e.SuppressKeyPress = true; FinishTextInput(); } else if (e.KeyCode == Keys.Escape) CancelTextInput(); } 785 private void InputTextBox_LostFocus(object sender, EventArgs e) { if (inputTextBox.Visible) FinishTextInput(); } 786 private void FinishTextInput() { inputTextBox.Visible = false; if (textInputCallback != null) textInputCallback(inputTextBox.Text.Trim()); textInputCallback = null; } 787 private void CancelTextInput() { inputTextBox.Visible = false; if (textInputCallback != null) textInputCallback(""); textInputCallback = null; } 788 private Rectangle MakeRect(Point a, Point b) { return new Rectangle(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Abs(a.X - b.X), Math.Abs(a.Y - b.Y)); } 789 public void ResetMarkerCount() { nextMarkerNumber = 1; } 790 public void Undo() { if (UndoStack.Count == 0) return; var action = UndoStack[UndoStack.Count - 1]; UndoStack.RemoveAt(UndoStack.Count - 1); switch (action.Type) { case "highlight": var highlight = (HighlightRect)action.Item; RemoveEffectLayer(highlight.Id); Highlights.Remove(highlight); break; case "arrow": Arrows.Remove((ArrowAnnotation)action.Item); break; case "marker": Markers.Remove((NumberMarker)action.Item); nextMarkerNumber = Markers.Count > 0 ? Markers[Markers.Count - 1].Number + 1 : 1; break; case "text": TextBlocks.Remove((TextBlock)action.Item); break; case "bubble": CommentBubbles.Remove((CommentBubble)action.Item); break; } Invalidate(); } 791 792 protected override void OnPaint(PaintEventArgs e) 793 { 794 if (originalBitmap == null) return; 795 796 // GPU was active but the device was lost (GPU reset / RDP / driver update) → try to rebuild 797 // it (throttled). Only fires on machines where D2D worked before, so no-GPU boxes never retry. 798 if (d2dActive && !D2DRenderer.IsAvailable && (DateTime.Now - lastD2DRetry).TotalSeconds > 2) 799 { 800 lastD2DRetry = DateTime.Now; 801 try { editor.TryRecoverD2D(); } catch { } 802 } 803 804 var gfx = e.Graphics; 805 float sw = originalBitmap.Width * drawScale; float sh = originalBitmap.Height * drawScale; 806 RectangleF destRectF = new RectangleF(drawX, drawY, sw, sh); 807 Rectangle destRect = new Rectangle((int)Math.Round(drawX), (int)Math.Round(drawY), (int)Math.Round(sw), (int)Math.Round(sh)); 808 bool useThumbnail = UseFastRendering && cachedThumbnail != null; 809 // Skip D2D for animated GIFs — bitmap changes every frame, GDI draws directly 810 if (!isAnimatingGif && D2DRenderer.IsAvailable && D2DRenderer.BeginDraw()) 811 { 812 try 813 { 814 // Background tint from settings (floats 0-1) 815 D2DRenderer.Clear( 816 Settings.BlurTintColor.R / 255f, 817 Settings.BlurTintColor.G / 255f, 818 Settings.BlurTintColor.B / 255f, 819 Settings.BlurTintAlpha / 255f); 820 // Noise grain drawn via GDI interop after image (see below) 821 if (useThumbnail) D2DRenderer.DrawImage(destRectF, 1f); 822 else if (Highlights.Count > 0) 823 { 824 bool anyDimDisabled = false, anyOutside = false; 825 foreach (var hl in Highlights) 826 { 827 if (hl.DimDisabled) anyDimDisabled = true; 828 var layer = FindEffectLayer(hl.Id); 829 if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) anyOutside = true; 830 } 831 float bgScale = (float)originalBitmap.Width / Settings.ReferenceResolution; 832 int scaledBlur = Math.Max(1, (int)Math.Round(Settings.BlurRadius * bgScale)); 833 834 if (anyDimDisabled) 835 { 836 D2DRenderer.DrawImage(destRectF, 1f); // "peek original" toggle 837 } 838 else if (anyOutside) 839 { 840 // Blur + dim everything OUTSIDE, then keep EVERY selected region clear 841 // (so other highlights aren't blurred/darkened — that was the bug). 842 D2DRenderer.DrawImageBlurred(destRectF, scaledBlur); 843 D2DRenderer.DrawDimOverlay(destRectF, Settings.DimAlpha / 255f); 844 foreach (var hl in Highlights) 845 { 846 var hlRF = new RectangleF(destRectF.X + hl.Rect.X * drawScale, destRectF.Y + hl.Rect.Y * drawScale, hl.Rect.Width * drawScale, hl.Rect.Height * drawScale); 847 var hlLayer = FindEffectLayer(hl.Id); 848 bool inside = hlLayer != null && (hlLayer.Type == EffectType.BlurInside || hlLayer.Type == EffectType.MotionBlurInside); 849 D2DRenderer.PushClip(hlRF); 850 if (inside) D2DRenderer.DrawImageBlurred(destRectF, hlLayer.Intensity); 851 else D2DRenderer.DrawImage(destRectF, 1f); 852 D2DRenderer.PopClip(); 853 } 854 } 855 else 856 { 857 // No "outside" effect. Clear image as base; optionally dim AROUND the selection 858 // (configurable spotlight), and blur INSIDE any inside-effect rects. The 859 // selected regions themselves always stay clear/sharp. 860 D2DRenderer.DrawImage(destRectF, 1f); 861 bool spotlight = Settings.DimAroundSelection; 862 if (spotlight) D2DRenderer.DrawDimOverlay(destRectF, Settings.DimAlpha / 255f); 863 foreach (var hl in Highlights) 864 { 865 var hlLayer = FindEffectLayer(hl.Id); 866 bool inside = hlLayer != null && (hlLayer.Type == EffectType.BlurInside || hlLayer.Type == EffectType.MotionBlurInside); 867 if (!spotlight && !inside) continue; // nothing to restore for this region 868 var hlRF = new RectangleF(destRectF.X + hl.Rect.X * drawScale, destRectF.Y + hl.Rect.Y * drawScale, hl.Rect.Width * drawScale, hl.Rect.Height * drawScale); 869 D2DRenderer.PushClip(hlRF); 870 if (inside) D2DRenderer.DrawImageBlurred(destRectF, hlLayer.Intensity); 871 else D2DRenderer.DrawImage(destRectF, 1f); // un-dim the selected region 872 D2DRenderer.PopClip(); 873 } 874 } 875 } 876 else D2DRenderer.DrawImage(destRectF, 1f); 877 var g = D2DRenderer.GetGdiGraphics(); 878 if (g != null) { try { 879 DrawNoiseBackground(g, Width, Height, destRect); 880 g.SmoothingMode = SmoothingMode.AntiAlias; g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; g.TranslateTransform(drawX, drawY); g.ScaleTransform(drawScale, drawScale); DrawAnnotations(g); g.ResetTransform(); DrawUIOverlay(g); 881 } finally { D2DRenderer.ReleaseGdiGraphics(g); } } 882 } 883 catch (Exception ex) { System.Diagnostics.Debug.WriteLine("D2D OnPaint error: " + ex.Message); } 884 D2DRenderer.EndDraw(); return; 885 } 886 // GDI Fallback with manual double-buffering 887 if (gdiBackBuffer == null || gdiBackBufferW != Width || gdiBackBufferH != Height) 888 { 889 if (gdiBackBuffer != null) gdiBackBuffer.Dispose(); 890 gdiBackBufferW = Math.Max(1, Width); 891 gdiBackBufferH = Math.Max(1, Height); 892 gdiBackBuffer = new Bitmap(gdiBackBufferW, gdiBackBufferH, System.Drawing.Imaging.PixelFormat.Format32bppPArgb); 893 } 894 using (var bg = Graphics.FromImage(gdiBackBuffer)) 895 { 896 // Background tint from settings (area around the image) 897 bg.Clear(Color.FromArgb(Settings.BlurTintAlpha, Settings.BlurTintColor)); 898 DrawNoiseBackground(bg, Width, Height, destRect); 899 bg.SmoothingMode = SmoothingMode.AntiAlias; bg.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; bg.InterpolationMode = useThumbnail ? InterpolationMode.Low : InterpolationMode.HighQualityBicubic; 900 if (useThumbnail) bg.DrawImage(cachedThumbnail, destRect); else bg.DrawImage(originalBitmap, destRect); 901 bg.TranslateTransform(drawX, drawY); bg.ScaleTransform(drawScale, drawScale); DrawAnnotations(bg); bg.ResetTransform(); DrawUIOverlay(bg); 902 } 903 gfx.DrawImageUnscaled(gdiBackBuffer, 0, 0); 904 } 905 906 private void DrawUIOverlay(Graphics g) { editor.PaintUIOnCanvas(g); } 907 private void DrawAnnotations(Graphics g) { var cr = editor.CropRect; var imgBounds = new Rectangle(0, 0, editor.Bounds.Width, editor.Bounds.Height); if (cr != imgBounds && cr.Width > 0 && cr.Height > 0) { using (var dimBrush = new SolidBrush(Color.FromArgb(120, 0, 0, 0))) { if (cr.Top > 0) g.FillRectangle(dimBrush, 0, 0, imgBounds.Width, cr.Top); if (cr.Bottom < imgBounds.Height) g.FillRectangle(dimBrush, 0, cr.Bottom, imgBounds.Width, imgBounds.Height - cr.Bottom); if (cr.Left > 0) g.FillRectangle(dimBrush, 0, cr.Top, cr.Left, cr.Height); if (cr.Right < imgBounds.Width) g.FillRectangle(dimBrush, cr.Right, cr.Top, imgBounds.Width - cr.Right, cr.Height); } using (var borderPen = new Pen(Color.FromArgb(180, 255, 255, 255), 1f)) { borderPen.DashStyle = DashStyle.Dash; g.DrawRectangle(borderPen, cr); } } if (Highlights.Count > 0 && Settings.ShowHighlightBorder) { foreach (var hl in Highlights) { using (var shadowPen = new Pen(Color.FromArgb(60, 0, 0, 0), Settings.HighlightBorderWidth + 2)) g.DrawRectangle(shadowPen, new Rectangle(hl.Rect.X + 2, hl.Rect.Y + 2, hl.Rect.Width, hl.Rect.Height)); using (var pen = new Pen(Settings.HighlightBorderColor, Settings.HighlightBorderWidth)) g.DrawRectangle(pen, hl.Rect); DrawHighlightResizeHandles(g, hl.Rect); } } foreach (var arr in Arrows) DrawArrowWithControlPoints(g, arr); foreach (var mk in Markers) DrawMarker(g, mk); foreach (var tb in TextBlocks) DrawTextBlock(g, tb); foreach (var bubble in CommentBubbles) DrawBubble(g, bubble); DrawInProgress(g); DrawCropHandles(g); } 908 909 /// <summary>Live preview of the shape currently being dragged, drawn every mouse-move 910 /// so the user sees the rectangle/arrow grow instead of it appearing only on mouse-up.</summary> 911 private void DrawInProgress(Graphics g) 912 { 913 if (isDrawingRect) 914 { 915 var rect = MakeRect(drawStart, currentEnd); 916 if (rect.Width > 0 || rect.Height > 0) 917 { 918 using (var fill = new SolidBrush(Color.FromArgb(36, Settings.HighlightBorderColor))) 919 g.FillRectangle(fill, rect); 920 using (var shadowPen = new Pen(Color.FromArgb(60, 0, 0, 0), Settings.HighlightBorderWidth + 2)) 921 g.DrawRectangle(shadowPen, new Rectangle(rect.X + 2, rect.Y + 2, rect.Width, rect.Height)); 922 using (var pen = new Pen(Settings.HighlightBorderColor, Math.Max(1f, Settings.HighlightBorderWidth))) 923 g.DrawRectangle(pen, rect); 924 } 925 } 926 else if (isDrawingArrow) 927 { 928 int dx = currentEnd.X - drawStart.X, dy = currentEnd.Y - drawStart.Y; 929 if (dx * dx + dy * dy > 9) // ignore the first couple of px (matches the commit threshold) 930 { 931 var preview = new ArrowAnnotation(drawStart, currentEnd); 932 DrawArrowForExport(g, preview); 933 } 934 } 935 } 936 private void DrawArrowWithControlPoints(Graphics g, ArrowAnnotation arr) 937 { 938 float headLen = 20f, headWidth = 10f; 939 var tangent = arr.GetBezierTangent(1.0f); 940 var endPt = new PointF(arr.End.X, arr.End.Y); 941 var headBase = new PointF(endPt.X - tangent.X * headLen, endPt.Y - tangent.Y * headLen); 942 var headLeft = new PointF(headBase.X + (-tangent.Y * headWidth), headBase.Y + (tangent.X * headWidth)); 943 var headRight = new PointF(headBase.X - (-tangent.Y * headWidth), headBase.Y - (tangent.X * headWidth)); 944 945 // Shadow 946 using (var shadowPen = new Pen(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0), arr.Width + 2)) 947 { 948 shadowPen.StartCap = LineCap.Round; shadowPen.EndCap = LineCap.Round; 949 g.DrawBezier(shadowPen, 950 new PointF(arr.Start.X + Settings.ShadowOffset, arr.Start.Y + Settings.ShadowOffset), 951 new PointF(arr.Control1.X + Settings.ShadowOffset, arr.Control1.Y + Settings.ShadowOffset), 952 new PointF(arr.Control2.X + Settings.ShadowOffset, arr.Control2.Y + Settings.ShadowOffset), 953 new PointF(headBase.X + Settings.ShadowOffset, headBase.Y + Settings.ShadowOffset)); 954 } 955 using (var shadowBrush = new SolidBrush(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0))) 956 g.FillPolygon(shadowBrush, new PointF[] { 957 new PointF(endPt.X + Settings.ShadowOffset, endPt.Y + Settings.ShadowOffset), 958 new PointF(headLeft.X + Settings.ShadowOffset, headLeft.Y + Settings.ShadowOffset), 959 new PointF(headRight.X + Settings.ShadowOffset, headRight.Y + Settings.ShadowOffset) }); 960 961 // Arrow curve 962 using (var pen = new Pen(arr.Color, arr.Width)) 963 { 964 pen.StartCap = LineCap.Round; pen.EndCap = LineCap.Round; 965 g.DrawBezier(pen, arr.Start, arr.Control1, arr.Control2, headBase); 966 } 967 // Arrow head 968 using (var brush = new SolidBrush(arr.Color)) 969 g.FillPolygon(brush, new PointF[] { endPt, headLeft, headRight }); 970 971 // Bezier control point handles 972 int cpSize = 8; 973 // Tangent lines from endpoints to control points 974 using (var tangentPen = new Pen(Color.FromArgb(120, 180, 180, 255), 1f)) 975 { 976 tangentPen.DashStyle = DashStyle.Dash; 977 g.DrawLine(tangentPen, arr.Start, arr.Control1); 978 g.DrawLine(tangentPen, arr.End, arr.Control2); 979 } 980 // Start point handle (square) 981 var startRect = arr.GetStartRect(cpSize); 982 using (var brush = new SolidBrush(Color.FromArgb(200, 100, 200, 255))) 983 g.FillRectangle(brush, startRect); 984 using (var pen = new Pen(Color.FromArgb(220, 60, 60, 120), 1f)) 985 g.DrawRectangle(pen, startRect); 986 // End point handle (square) 987 var endRect = arr.GetEndRect(cpSize); 988 using (var brush = new SolidBrush(Color.FromArgb(200, 100, 200, 255))) 989 g.FillRectangle(brush, endRect); 990 using (var pen = new Pen(Color.FromArgb(220, 60, 60, 120), 1f)) 991 g.DrawRectangle(pen, endRect); 992 // Control point 1 handle (circle) 993 var cp1Rect = arr.GetControl1Rect(cpSize); 994 using (var brush = new SolidBrush(Color.FromArgb(200, 255, 160, 80))) 995 g.FillEllipse(brush, cp1Rect); 996 using (var pen = new Pen(Color.FromArgb(220, 120, 80, 40), 1f)) 997 g.DrawEllipse(pen, cp1Rect); 998 // Control point 2 handle (circle) 999 var cp2Rect = arr.GetControl2Rect(cpSize); 1000 using (var brush = new SolidBrush(Color.FromArgb(200, 255, 160, 80))) 1001 g.FillEllipse(brush, cp2Rect); 1002 using (var pen = new Pen(Color.FromArgb(220, 120, 80, 40), 1f)) 1003 g.DrawEllipse(pen, cp2Rect); 1004 } 1005 private void DrawMarker(Graphics g, NumberMarker marker) { int size = Settings.MarkerSize; var rect = new Rectangle(marker.Location.X - size / 2, marker.Location.Y - size / 2, size, size); using (var shadowBrush = new SolidBrush(Color.FromArgb(80, 0, 0, 0))) g.FillEllipse(shadowBrush, rect.X + 3, rect.Y + 3, size, size); using (var brush = new SolidBrush(Settings.MarkerColor)) g.FillEllipse(brush, rect); using (var pen = new Pen(Settings.MarkerBorderColor, 2)) g.DrawEllipse(pen, rect); using (var font = new Font(Settings.MarkerFont, size / 2.5f, FontStyle.Bold)) using (var brush = new SolidBrush(Settings.MarkerTextColor)) { StringFormat sf = new StringFormat(); sf.Alignment = StringAlignment.Center; sf.LineAlignment = StringAlignment.Center; g.DrawString(marker.Number.ToString(), font, brush, rect, sf); } } 1006 private void DrawTextBlock(Graphics g, TextBlock tb) { if (tb.ShadowOffset > 0) using (var brush = new SolidBrush(tb.ShadowColor)) g.DrawString(tb.Text, tb.TextFont, brush, tb.Location.X + tb.ShadowOffset, tb.Location.Y + tb.ShadowOffset); using (var brush = new SolidBrush(tb.TextColor)) g.DrawString(tb.Text, tb.TextFont, brush, tb.Location); } 1007 private void DrawBubble(Graphics g, CommentBubble bubble) { var path = bubble.GetBubblePath(g); if (path == null) return; var shadowMatrix = new Matrix(); shadowMatrix.Translate(3, 3); var shadowPath = (GraphicsPath)path.Clone(); shadowPath.Transform(shadowMatrix); using (var shadowBrush = new SolidBrush(Color.FromArgb(60, 0, 0, 0))) g.FillPath(shadowBrush, shadowPath); shadowPath.Dispose(); using (var brush = new SolidBrush(bubble.FillColor)) g.FillPath(brush, path); using (var pen = new Pen(bubble.BorderColor, bubble.BorderWidth)) g.DrawPath(pen, path); var textBounds = bubble.GetTextBounds(g); using (var brush = new SolidBrush(bubble.TextColor)) g.DrawString(bubble.Text, bubble.TextFont, brush, textBounds.Location); path.Dispose(); } 1008 private void DrawCropHandles(Graphics g) { var cr = editor.CropRect; int handleSize = 8; using (var brush = new SolidBrush(Color.White)) using (var pen = new Pen(Color.FromArgb(100, 100, 100), 1)) { DrawHandle(g, brush, pen, cr.Left, cr.Top, handleSize); DrawHandle(g, brush, pen, cr.Right, cr.Top, handleSize); DrawHandle(g, brush, pen, cr.Left, cr.Bottom, handleSize); DrawHandle(g, brush, pen, cr.Right, cr.Bottom, handleSize); DrawHandle(g, brush, pen, cr.Left, cr.Top + cr.Height / 2, handleSize); DrawHandle(g, brush, pen, cr.Right, cr.Top + cr.Height / 2, handleSize); DrawHandle(g, brush, pen, cr.Left + cr.Width / 2, cr.Top, handleSize); DrawHandle(g, brush, pen, cr.Left + cr.Width / 2, cr.Bottom, handleSize); } using (var pen = new Pen(Color.FromArgb(100, 255, 255, 255), 1)) { pen.DashStyle = DashStyle.Dash; g.DrawRectangle(pen, cr); } } 1009 private void DrawHandle(Graphics g, Brush brush, Pen pen, int x, int y, int size) { var rect = new Rectangle(x - size / 2, y - size / 2, size, size); g.FillRectangle(brush, rect); g.DrawRectangle(pen, rect); } 1010 private void DrawHighlightResizeHandles(Graphics g, Rectangle rect) { int handleSize = 6; using (var brush = new SolidBrush(Color.White)) using (var pen = new Pen(Settings.HighlightBorderColor, 1)) { DrawHandle(g, brush, pen, rect.Left, rect.Top, handleSize); DrawHandle(g, brush, pen, rect.Right, rect.Top, handleSize); DrawHandle(g, brush, pen, rect.Left, rect.Bottom, handleSize); DrawHandle(g, brush, pen, rect.Right, rect.Bottom, handleSize); DrawHandle(g, brush, pen, rect.Left + rect.Width / 2, rect.Top, handleSize); DrawHandle(g, brush, pen, rect.Left + rect.Width / 2, rect.Bottom, handleSize); DrawHandle(g, brush, pen, rect.Left, rect.Top + rect.Height / 2, handleSize); DrawHandle(g, brush, pen, rect.Right, rect.Top + rect.Height / 2, handleSize); } } 1011 public void SetThumbnail(Bitmap bmp) { DisposeThumbnail(); if (bmp != null) cachedThumbnail = (Bitmap)bmp.Clone(); } 1012 protected override void Dispose(bool disposing) { if (disposing) { StopGifAnimation(); if (mmbTimer != null) { mmbTimer.Stop(); mmbTimer.Dispose(); } if (blurClickTimer != null) { blurClickTimer.Stop(); blurClickTimer.Dispose(); } DisposeThumbnail(); if (originalBitmap != null) originalBitmap.Dispose(); if (gdiBackBuffer != null) gdiBackBuffer.Dispose(); if (inputTextBox != null) inputTextBox.Dispose(); foreach (var layer in effectLayers) layer.Dispose(); effectLayers.Clear(); } base.Dispose(disposing); } 1013 } 1014}