Forge
csharp4405de34
1using System.Text.Json;
2using Avalonia;
3using Avalonia.Controls;
4using Avalonia.Media.Imaging;
5
6namespace CascadeIDE.Services;
7
8/// <summary>
9/// Рендер окна в PNG (для MCP <c>capture_window</c>, в т.ч. все окна при <c>scope=all</c>).
10/// Вызывать только с UI-потока.
11/// </summary>
12public static class IdeWindowScreenshot
13{
14 /// <summary>PNG в памяти и размеры в пикселях.</summary>
15 public static (byte[] PngBytes, int WidthPx, int HeightPx) CaptureWindowPng(Window window)
16 {
17 ArgumentNullException.ThrowIfNull(window);
18 var scale = window.RenderScaling;
19 var cw = window.ClientSize.Width;
20 var ch = window.ClientSize.Height;
21 if (cw <= 0 || ch <= 0)
22 cw = window.Bounds.Width;
23 if (ch <= 0)
24 ch = window.Bounds.Height;
25 var pw = Math.Max(1, (int)Math.Ceiling(cw * scale));
26 var ph = Math.Max(1, (int)Math.Ceiling(ch * scale));
27 var pixelSize = new PixelSize(pw, ph);
28 using var rtb = new RenderTargetBitmap(pixelSize);
29 rtb.Render(window);
30 using var ms = new MemoryStream();
31 rtb.Save(ms);
32 return (ms.ToArray(), pw, ph);
33 }
34
35 /// <summary>
36 /// JSON для MCP: base64 PNG и опционально сохранение под workspace.
37 /// </summary>
38 public static string BuildCaptureJson(byte[] pngBytes, int widthPx, int heightPx, string? savedFullPath)
39 {
40 var b64 = Convert.ToBase64String(pngBytes);
41 return JsonSerializer.Serialize(new
42 {
43 format = "png_base64",
44 width = widthPx,
45 height = heightPx,
46 data = b64,
47 saved_path = savedFullPath
48 });
49 }
50
51 /// <summary>
52 /// Сохранить PNG под корнем workspace, если пути заданы и путь остаётся внутри корня.
53 /// </summary>
54 public static string? TrySaveUnderWorkspace(string workspaceRoot, string relativePath, byte[] pngBytes)
55 {
56 if (string.IsNullOrWhiteSpace(workspaceRoot) || string.IsNullOrWhiteSpace(relativePath))
57 return null;
58 var root = CanonicalFilePath.Normalize(workspaceRoot.Trim());
59 var combined = CanonicalFilePath.Normalize(Path.Combine(root, relativePath.Trim().Replace('/', Path.DirectorySeparatorChar)));
60 if (!IsStrictSubPath(root, combined))
61 return null;
62 var dir = Path.GetDirectoryName(combined);
63 if (!string.IsNullOrEmpty(dir))
64 Directory.CreateDirectory(dir);
65 File.WriteAllBytes(combined, pngBytes);
66 return combined;
67 }
68
69 private static bool IsStrictSubPath(string rootDir, string candidateFile)
70 {
71 rootDir = CanonicalFilePath.Normalize(rootDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar))
72 + Path.DirectorySeparatorChar;
73 candidateFile = CanonicalFilePath.Normalize(candidateFile);
74 return candidateFile.StartsWith(rootDir, StringComparison.OrdinalIgnoreCase);
75 }
76}
77
View only · write via MCP/CIDE