1using System; 2using System.Collections.Generic; 3using System.Drawing; 4using System.Drawing.Drawing2D; 5using System.Drawing.Imaging; 6using System.Drawing.Text; 7using System.IO; 8using System.Linq; 9using System.Runtime.InteropServices; 10using System.Windows.Forms; 11using WindowCapture.App; 12using WindowCapture.Detection; 13using WindowCapture.Effects; 14using WindowCapture.Helpers; 15using WindowCapture.Integration; 16using WindowCapture.Models; 17using WindowCapture.Native; 18 19namespace WindowCapture.UI 20{ 21 public partial class EditorForm 22 { 23 // ===== Final image rendering ===== 24 25 private Bitmap RenderFinalImage() 26 { 27 // Debug: log annotation counts 28 System.Diagnostics.Debug.WriteLine(string.Format( 29 "RenderFinalImage: Arrows={0}, Markers={1}, TextBlocks={2}, Bubbles={3}, Highlights={4}", 30 canvas.Arrows.Count, canvas.Markers.Count, canvas.TextBlocks.Count, 31 canvas.CommentBubbles.Count, canvas.Highlights.Count)); 32 33 // Use crop rect for final image 34 var finalBounds = cropRect; 35 var final = new Bitmap(finalBounds.Width, finalBounds.Height, PixelFormat.Format32bppArgb); 36 37 using (var g = Graphics.FromImage(final)) 38 { 39 g.SmoothingMode = SmoothingMode.AntiAlias; 40 g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; 41 g.InterpolationMode = InterpolationMode.HighQualityBicubic; 42 43 // Translate to crop origin 44 g.TranslateTransform(-finalBounds.X, -finalBounds.Y); 45 46 // Draw base - if highlights exist, show blurred/dimmed outside 47 if (canvas.Highlights.Count > 0) 48 { 49 // Check if any highlight has DimDisabled (no dimming at all) 50 bool anyDimDisabled = false; 51 foreach (var hl in canvas.Highlights) 52 { 53 if (hl.DimDisabled) 54 { 55 anyDimDisabled = true; 56 break; 57 } 58 } 59 60 // Check for outside effects first 61 bool hasOutsideEffect = false; 62 HighlightRect outsideEffectHl = null; 63 foreach (var hl in canvas.Highlights) 64 { 65 var layer = canvas.GetEffectLayer(hl.Id); 66 if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) 67 { 68 hasOutsideEffect = true; 69 outsideEffectHl = hl; 70 break; 71 } 72 } 73 74 if (anyDimDisabled) 75 { 76 // DimDisabled - draw original image as base (no dimming) 77 g.DrawImageUnscaled(captured, 0, 0); 78 } 79 else if (hasOutsideEffect && outsideEffectHl != null) 80 { 81 // Draw outside effect as base (already contains blur + dim) 82 var layer = canvas.GetEffectLayer(outsideEffectHl.Id); 83 var effectBitmap = layer.GetCachedResult(captured); 84 if (effectBitmap != null) 85 g.DrawImageUnscaled(effectBitmap, 0, 0); 86 else 87 g.DrawImageUnscaled(blurredBitmap, 0, 0); 88 } 89 else 90 { 91 // Standard dimmed background 92 g.DrawImageUnscaled(blurredBitmap, 0, 0); 93 } 94 95 // Draw highlight areas with their effects 96 foreach (var hl in canvas.Highlights) 97 { 98 var layer = canvas.GetEffectLayer(hl.Id); 99 100 // Skip outside effects - already drawn as base 101 if (layer != null && (layer.Type == EffectType.BlurOutside || layer.Type == EffectType.MotionBlurOutside)) 102 continue; 103 104 g.SetClip(hl.Rect); 105 106 if (layer != null && layer.Type != EffectType.None) 107 { 108 // Inside effect - draw effect bitmap 109 var effectBitmap = layer.GetCachedResult(captured); 110 if (effectBitmap != null) 111 g.DrawImageUnscaled(effectBitmap, 0, 0); 112 else 113 g.DrawImageUnscaled(captured, 0, 0); 114 } 115 else 116 { 117 // No effect - draw original (clear highlight area) 118 g.DrawImageUnscaled(captured, 0, 0); 119 } 120 } 121 g.ResetClip(); 122 123 // Draw highlight borders (only if global setting enabled) 124 if (Settings.ShowHighlightBorder) 125 { 126 foreach (var hl in canvas.Highlights) 127 { 128 // Shadow 129 using (var shadowPen = new Pen(Color.FromArgb(60, 0, 0, 0), Settings.HighlightBorderWidth + 2)) 130 { 131 var shadowRect = new Rectangle(hl.Rect.X + 2, hl.Rect.Y + 2, hl.Rect.Width, hl.Rect.Height); 132 g.DrawRectangle(shadowPen, shadowRect); 133 } 134 using (var pen = new Pen(Settings.HighlightBorderColor, Settings.HighlightBorderWidth)) 135 { 136 g.DrawRectangle(pen, hl.Rect); 137 } 138 } 139 } 140 } 141 else 142 { 143 g.DrawImageUnscaled(captured, 0, 0); 144 } 145 146 // Draw arrows 147 foreach (var arr in canvas.Arrows) 148 { 149 DrawArrowFinal(g, arr); 150 } 151 152 // Draw number markers 153 foreach (var mk in canvas.Markers) 154 { 155 DrawMarkerFinal(g, mk); 156 } 157 158 // Draw text blocks 159 foreach (var tb in canvas.TextBlocks) 160 { 161 DrawTextBlockFinal(g, tb); 162 } 163 164 // Draw comment bubbles 165 foreach (var bubble in canvas.CommentBubbles) 166 { 167 DrawBubbleFinal(g, bubble); 168 } 169 } 170 return final; 171 } 172 173 private void DrawArrowFinal(Graphics g, ArrowAnnotation arr) 174 { 175 // Arrow head parameters (same as in AnnotationCanvas) 176 float headLen = 20f; 177 float headWidth = 10f; 178 179 // Get tangent at end point for arrow head direction 180 var tangent = arr.GetBezierTangent(1.0f); 181 182 // Calculate arrow head points 183 var endPt = new PointF(arr.End.X, arr.End.Y); 184 var headBase = new PointF(endPt.X - tangent.X * headLen, endPt.Y - tangent.Y * headLen); 185 var perpX = -tangent.Y * headWidth; 186 var perpY = tangent.X * headWidth; 187 var headLeft = new PointF(headBase.X + perpX, headBase.Y + perpY); 188 var headRight = new PointF(headBase.X - perpX, headBase.Y - perpY); 189 190 // Shadow for curve 191 using (var shadowPen = new Pen(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0), arr.Width + 2)) 192 { 193 shadowPen.StartCap = LineCap.Round; 194 shadowPen.EndCap = LineCap.Round; 195 g.DrawBezier(shadowPen, 196 new PointF(arr.Start.X + Settings.ShadowOffset, arr.Start.Y + Settings.ShadowOffset), 197 new PointF(arr.Control1.X + Settings.ShadowOffset, arr.Control1.Y + Settings.ShadowOffset), 198 new PointF(arr.Control2.X + Settings.ShadowOffset, arr.Control2.Y + Settings.ShadowOffset), 199 new PointF(headBase.X + Settings.ShadowOffset, headBase.Y + Settings.ShadowOffset)); 200 } 201 202 // Shadow for arrow head 203 using (var shadowBrush = new SolidBrush(Color.FromArgb(Settings.ShadowAlpha, 0, 0, 0))) 204 { 205 var shadowHead = new PointF[] { 206 new PointF(endPt.X + Settings.ShadowOffset, endPt.Y + Settings.ShadowOffset), 207 new PointF(headLeft.X + Settings.ShadowOffset, headLeft.Y + Settings.ShadowOffset), 208 new PointF(headRight.X + Settings.ShadowOffset, headRight.Y + Settings.ShadowOffset) 209 }; 210 g.FillPolygon(shadowBrush, shadowHead); 211 } 212 213 // Main curve (without built-in arrow cap - we draw custom head) 214 using (var pen = new Pen(arr.Color, arr.Width)) 215 { 216 pen.StartCap = LineCap.Round; 217 pen.EndCap = LineCap.Round; 218 g.DrawBezier(pen, arr.Start, arr.Control1, arr.Control2, new Point((int)headBase.X, (int)headBase.Y)); 219 } 220 221 // Custom arrow head (filled triangle) 222 using (var brush = new SolidBrush(arr.Color)) 223 { 224 var headPoints = new PointF[] { endPt, headLeft, headRight }; 225 g.FillPolygon(brush, headPoints); 226 } 227 } 228 229 private void DrawMarkerFinal(Graphics g, NumberMarker marker) 230 { 231 // Use marker's individual font size 232 float fontSize = marker.FontSize; 233 string text = marker.Number.ToString(); 234 235 using (var font = new Font(Settings.MarkerFont, fontSize, FontStyle.Bold)) 236 { 237 var textSize = g.MeasureString(text, font); 238 float x = marker.Location.X - textSize.Width / 2; 239 float y = marker.Location.Y - textSize.Height / 2; 240 241 // Shadow (offset) 242 float shadowOffset = Math.Max(2, fontSize / 10); 243 using (var shadowBrush = new SolidBrush(Color.FromArgb(100, 0, 0, 0))) 244 { 245 g.DrawString(text, font, shadowBrush, x + shadowOffset, y + shadowOffset); 246 } 247 248 // Black outline (draw text multiple times with offset) 249 float outlineWidth = Math.Max(1.5f, fontSize / 15); 250 using (var outlineBrush = new SolidBrush(Color.Black)) 251 { 252 for (float ox = -outlineWidth; ox <= outlineWidth; ox += outlineWidth) 253 { 254 for (float oy = -outlineWidth; oy <= outlineWidth; oy += outlineWidth) 255 { 256 if (ox != 0 || oy != 0) 257 g.DrawString(text, font, outlineBrush, x + ox, y + oy); 258 } 259 } 260 } 261 262 // Main text 263 using (var brush = new SolidBrush(Settings.MarkerTextColor)) 264 { 265 g.DrawString(text, font, brush, x, y); 266 } 267 } 268 } 269 270 private void DrawTextBlockFinal(Graphics g, TextBlock tb) 271 { 272 // Check if 3D rotation is applied 273 bool has3DRotation = Math.Abs(tb.RotationX) > 0.1f || Math.Abs(tb.RotationY) > 0.1f; 274 275 if (has3DRotation) 276 { 277 DrawTextBlockFinal3D(g, tb); 278 return; 279 } 280 281 // Standard 2D drawing with shadow 282 DrawTextBlockFinalFlat(g, tb); 283 } 284 285 private void DrawTextBlockFinalFlat(Graphics g, TextBlock tb) 286 { 287 // Realistic shadow with soft edges and alpha falloff 288 if (tb.ShadowOffset > 0) 289 { 290 var shadowOffset = tb.GetShadowOffset(); 291 float shadowX = tb.Location.X + shadowOffset.X; 292 float shadowY = tb.Location.Y + shadowOffset.Y; 293 294 int baseAlpha = tb.ShadowColor.A; 295 float distanceFactor = 1f / (1f + tb.ShadowOffset * 0.05f); 296 int coreAlpha = (int)(baseAlpha * distanceFactor * 0.7f); 297 float blurRadius = tb.ShadowOffset * 0.4f; 298 299 int numRings = Math.Min(6, 2 + tb.ShadowOffset / 8); 300 int pointsPerRing = 8; 301 302 for (int ring = numRings; ring >= 1; ring--) 303 { 304 float ringRadius = blurRadius * ring / numRings; 305 float ringFactor = 1f - (float)ring / (numRings + 1); 306 ringFactor = ringFactor * ringFactor; 307 int ringAlpha = (int)(coreAlpha * 0.3f * ringFactor); 308 309 if (ringAlpha < 1) continue; 310 311 using (var brush = new SolidBrush(Color.FromArgb(ringAlpha, tb.ShadowColor))) 312 { 313 for (int p = 0; p < pointsPerRing; p++) 314 { 315 double angle = 2 * Math.PI * p / pointsPerRing; 316 float dx = (float)(ringRadius * Math.Cos(angle)); 317 float dy = (float)(ringRadius * Math.Sin(angle)); 318 g.DrawString(tb.Text, tb.TextFont, brush, shadowX + dx, shadowY + dy); 319 } 320 } 321 } 322 323 if (coreAlpha > 0) 324 { 325 using (var brush = new SolidBrush(Color.FromArgb(coreAlpha, tb.ShadowColor))) 326 { 327 g.DrawString(tb.Text, tb.TextFont, brush, shadowX, shadowY); 328 } 329 } 330 } 331 332 using (var brush = new SolidBrush(tb.TextColor)) 333 { 334 g.DrawString(tb.Text, tb.TextFont, brush, tb.Location); 335 } 336 } 337 338 private void DrawTextBlockFinal3D(Graphics g, TextBlock tb) 339 { 340 // Measure text to get center point 341 var textSize = g.MeasureString(tb.Text, tb.TextFont); 342 float centerX = tb.Location.X + textSize.Width / 2; 343 float centerY = tb.Location.Y + textSize.Height / 2; 344 345 // Convert rotation angles to radians 346 float rotXRad = tb.RotationX * (float)Math.PI / 180f; 347 float rotYRad = tb.RotationY * (float)Math.PI / 180f; 348 349 float cosX = (float)Math.Cos(rotXRad); 350 float cosY = (float)Math.Cos(rotYRad); 351 352 var oldTransform = g.Transform.Clone(); 353 var matrix = new Matrix(); 354 matrix.Translate(centerX, centerY); 355 356 float shearX = (float)Math.Sin(rotYRad) * 0.5f; 357 float scaleX = Math.Max(0.3f, cosY); 358 float shearY = (float)Math.Sin(rotXRad) * 0.5f; 359 float scaleY = Math.Max(0.3f, cosX); 360 361 matrix.Scale(scaleX, scaleY); 362 matrix.Shear(shearX, shearY); 363 matrix.Translate(-centerX, -centerY); 364 g.Transform = matrix; 365 366 // Draw shadow with transform 367 if (tb.ShadowOffset > 0) 368 { 369 var shadowOffset = tb.GetShadowOffset(); 370 float shadowX = tb.Location.X + shadowOffset.X; 371 float shadowY = tb.Location.Y + shadowOffset.Y; 372 373 int baseAlpha = tb.ShadowColor.A; 374 float distanceFactor = 1f / (1f + tb.ShadowOffset * 0.05f); 375 int coreAlpha = (int)(baseAlpha * distanceFactor * 0.7f); 376 float blurRadius = tb.ShadowOffset * 0.4f; 377 378 int numRings = Math.Min(6, 2 + tb.ShadowOffset / 8); 379 int pointsPerRing = 8; 380 381 for (int ring = numRings; ring >= 1; ring--) 382 { 383 float ringRadius = blurRadius * ring / numRings; 384 float ringFactor = 1f - (float)ring / (numRings + 1); 385 ringFactor = ringFactor * ringFactor; 386 int ringAlpha = (int)(coreAlpha * 0.3f * ringFactor); 387 388 if (ringAlpha < 1) continue; 389 390 using (var brush = new SolidBrush(Color.FromArgb(ringAlpha, tb.ShadowColor))) 391 { 392 for (int p = 0; p < pointsPerRing; p++) 393 { 394 double angle = 2 * Math.PI * p / pointsPerRing; 395 float dx = (float)(ringRadius * Math.Cos(angle)); 396 float dy = (float)(ringRadius * Math.Sin(angle)); 397 g.DrawString(tb.Text, tb.TextFont, brush, shadowX + dx, shadowY + dy); 398 } 399 } 400 } 401 402 if (coreAlpha > 0) 403 { 404 using (var brush = new SolidBrush(Color.FromArgb(coreAlpha, tb.ShadowColor))) 405 { 406 g.DrawString(tb.Text, tb.TextFont, brush, shadowX, shadowY); 407 } 408 } 409 } 410 411 using (var brush = new SolidBrush(tb.TextColor)) 412 { 413 g.DrawString(tb.Text, tb.TextFont, brush, tb.Location); 414 } 415 416 g.Transform = oldTransform; 417 oldTransform.Dispose(); 418 matrix.Dispose(); 419 } 420 421 private void DrawBubbleFinal(Graphics g, CommentBubble bubble) 422 { 423 var path = bubble.GetBubblePath(g); 424 if (path == null) return; 425 426 // Shadow 427 var shadowMatrix = new Matrix(); 428 shadowMatrix.Translate(3, 3); 429 var shadowPath = (GraphicsPath)path.Clone(); 430 shadowPath.Transform(shadowMatrix); 431 using (var shadowBrush = new SolidBrush(Color.FromArgb(60, 0, 0, 0))) 432 { 433 g.FillPath(shadowBrush, shadowPath); 434 } 435 shadowPath.Dispose(); 436 437 using (var brush = new SolidBrush(bubble.FillColor)) 438 { 439 g.FillPath(brush, path); 440 } 441 using (var pen = new Pen(bubble.BorderColor, bubble.BorderWidth)) 442 { 443 g.DrawPath(pen, path); 444 } 445 446 var textBounds = bubble.GetTextBounds(g); 447 using (var brush = new SolidBrush(bubble.TextColor)) 448 { 449 g.DrawString(bubble.Text, bubble.TextFont, brush, textBounds.Location); 450 } 451 452 path.Dispose(); 453 } 454 } 455}