using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Forms; using System.Windows.Interop; using System.Windows.Media.Imaging; using System.Windows.Threading; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Views; namespace AxCopilot.Handlers; public class ScreenCaptureHandler : IActionHandler { private struct RECT { public int left; public int top; public int right; public int bottom; } private readonly SettingsService _settings; private const int SW_RESTORE = 9; private const byte VK_NEXT = 34; private const uint WM_VSCROLL = 277u; private const uint WM_KEYDOWN = 256u; private const uint WM_KEYUP = 257u; private const int SB_PAGEDOWN = 3; private static readonly (string Key, string Label, string Desc)[] _options = new(string, string, string)[4] { ("region", "영역 선택 캡처", "마우스로 드래그하여 원하는 영역만 캡처 · Shift+Enter: 타이머 캡처"), ("window", "활성 창 캡처", "런처 호출 전 활성 창만 캡처 · Shift+Enter: 타이머 캡처"), ("scroll", "스크롤 캡처", "활성 창을 끝까지 스크롤하며 페이지 전체 캡처 · Shift+Enter: 타이머 캡처"), ("screen", "전체 화면 캡처", "모든 모니터를 포함한 전체 화면 · Shift+Enter: 타이머 캡처") }; public string? Prefix => string.IsNullOrWhiteSpace(_settings.Settings.ScreenCapture.Prefix) ? "cap" : _settings.Settings.ScreenCapture.Prefix.Trim(); public PluginMetadata Metadata => new PluginMetadata("ScreenCapture", "화면 캡처 — cap screen/window/scroll/region", "1.0", "AX"); internal int ScrollDelayMs => Math.Max(50, _settings.Settings.ScreenCapture.ScrollDelayMs); public ScreenCaptureHandler(SettingsService settings) { _settings = settings; } [DllImport("user32.dll")] private static extern bool GetWindowRect(nint hWnd, out RECT lpRect); [DllImport("user32.dll")] private static extern bool IsWindow(nint hWnd); [DllImport("user32.dll")] private static extern bool SetForegroundWindow(nint hWnd); [DllImport("user32.dll")] private static extern bool ShowWindow(nint hWnd, int nCmdShow); [DllImport("user32.dll")] private static extern bool PrintWindow(nint hwnd, nint hdcBlt, uint nFlags); [DllImport("user32.dll")] private static extern bool IsWindowVisible(nint hWnd); [DllImport("user32.dll")] private static extern nint SendMessage(nint hWnd, uint Msg, nint wParam, nint lParam); [DllImport("user32.dll")] private static extern nint FindWindowEx(nint parent, nint child, string? className, string? windowText); public Task> GetItemsAsync(string query, CancellationToken ct) { string q = query.Trim().ToLowerInvariant(); string saveHint = "클립보드에 복사"; IEnumerable<(string, string, string)> enumerable; if (!string.IsNullOrWhiteSpace(q)) { enumerable = _options.Where(((string Key, string Label, string Desc) o) => o.Key.StartsWith(q) || o.Label.Contains(q)); } else { IEnumerable<(string, string, string)> options = _options; enumerable = options; } IEnumerable<(string, string, string)> source = enumerable; List list = source.Select<(string, string, string), LauncherItem>(((string Key, string Label, string Desc) o) => new LauncherItem(o.Label, o.Desc + " · " + saveHint, null, o.Key, null, "\ue722")).ToList(); if (!list.Any()) { list.Add(new LauncherItem("알 수 없는 캡처 모드: " + q, "screen / window / scroll / region", null, null, null, "\ue7ba")); } return Task.FromResult((IEnumerable)list); } public IEnumerable GetDelayItems(string mode) { string text = _options.FirstOrDefault(((string Key, string Label, string Desc) o) => o.Key == mode).Label ?? mode; return new LauncherItem[3] { new LauncherItem("3초 후 " + text, "Shift+Enter로 지연 캡처", null, "delay:" + mode + ":3", null, "\ue916"), new LauncherItem("5초 후 " + text, "Shift+Enter로 지연 캡처", null, "delay:" + mode + ":5", null, "\ue916"), new LauncherItem("10초 후 " + text, "Shift+Enter로 지연 캡처", null, "delay:" + mode + ":10", null, "\ue916") }; } public async Task ExecuteAsync(LauncherItem item, CancellationToken ct) { object data = item.Data; if (!(data is string data2)) { return; } if (data2.StartsWith("delay:")) { string[] parts = data2.Split(':'); if (parts.Length == 3 && int.TryParse(parts[2], out var delaySec)) { await ExecuteDelayedCaptureAsync(parts[1], delaySec, ct); return; } } await Task.Delay(150, ct); await CaptureDirectAsync(data2, ct); } private async Task ExecuteDelayedCaptureAsync(string mode, int delaySec, CancellationToken ct) { await Task.Delay(200, ct); for (int i = delaySec; i > 0; i--) { ct.ThrowIfCancellationRequested(); await Task.Delay(1000, ct); } await CaptureDirectAsync(mode, ct); } public async Task CaptureDirectAsync(string mode, CancellationToken ct = default(CancellationToken)) { try { string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); switch (mode) { case "screen": await CaptureScreenAsync(timestamp); break; case "window": await CaptureWindowAsync(timestamp); break; case "scroll": await CaptureScrollAsync(timestamp, ct); break; case "region": await CaptureRegionAsync(timestamp, ct); break; } } catch (Exception ex) { Exception ex2 = ex; LogService.Error("캡처 실패: " + ex2.Message); NotificationService.Notify("AX Copilot", "캡처 실패: " + ex2.Message); } } private async Task CaptureScreenAsync(string timestamp) { Rectangle bounds = GetAllScreenBounds(); using Bitmap bmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb); using Graphics g = Graphics.FromImage(bmp); g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy); CopyToClipboard(bmp); await Task.Delay(10); NotificationService.Notify("화면 캡처 완료", "클립보드에 복사되었습니다"); } private async Task CaptureWindowAsync(string timestamp) { nint hwnd = WindowTracker.PreviousWindow; if (hwnd == IntPtr.Zero || !IsWindow(hwnd)) { NotificationService.Notify("AX Copilot", "캡처할 창이 없습니다. 런처 호출 전 창을 확인하세요."); return; } nint launcherHwnd = GetLauncherHwnd(); if (launcherHwnd != IntPtr.Zero) { for (int i = 0; i < 10; i++) { if (!IsWindowVisible(launcherHwnd)) { break; } await Task.Delay(50); } } ShowWindow(hwnd, 9); SetForegroundWindow(hwnd); await Task.Delay(150); if (!GetWindowRect(hwnd, out var rect)) { return; } int w = rect.right - rect.left; int h = rect.bottom - rect.top; if (w <= 0 || h <= 0) { return; } using Bitmap bmp = CaptureWindow(hwnd, w, h, rect); CopyToClipboard(bmp); NotificationService.Notify("창 캡처 완료", "클립보드에 복사되었습니다"); } private async Task CaptureScrollAsync(string timestamp, CancellationToken ct) { nint hwnd = WindowTracker.PreviousWindow; if (hwnd == IntPtr.Zero || !IsWindow(hwnd)) { NotificationService.Notify("AX Copilot", "캡처할 창이 없습니다."); return; } nint launcherHwnd = GetLauncherHwnd(); if (launcherHwnd != IntPtr.Zero) { for (int i = 0; i < 10; i++) { if (!IsWindowVisible(launcherHwnd)) { break; } await Task.Delay(50, ct); } } ShowWindow(hwnd, 9); SetForegroundWindow(hwnd); await Task.Delay(200, ct); if (!GetWindowRect(hwnd, out var rect)) { return; } int w = rect.right - rect.left; int h = rect.bottom - rect.top; if (w <= 0 || h <= 0) { return; } nint scrollTarget = FindScrollableChild(hwnd); List frames = new List { CaptureWindow(hwnd, w, h, rect) }; for (int j = 0; j < 14; j++) { ct.ThrowIfCancellationRequested(); if (scrollTarget != IntPtr.Zero) { SendMessage(scrollTarget, 277u, new IntPtr(3), IntPtr.Zero); } else { SendPageDown(hwnd); } await Task.Delay(ScrollDelayMs, ct); if (!GetWindowRect(hwnd, out var newRect)) { break; } Bitmap frame = CaptureWindow(hwnd, w, h, newRect); int num; if (frames.Count > 0) { num = (AreSimilar(frames[frames.Count - 1], frame) ? 1 : 0); } else { num = 0; } if (num != 0) { frame.Dispose(); break; } frames.Add(frame); } using Bitmap stitched = StitchFrames(frames, h); foreach (Bitmap f in frames) { f.Dispose(); } CopyToClipboard(stitched); NotificationService.Notify("스크롤 캡처 완료", $"{stitched.Height}px · 클립보드에 복사되었습니다"); } private static Bitmap CaptureWindow(nint hwnd, int w, int h, RECT rect) { Bitmap bitmap = new Bitmap(w, h, PixelFormat.Format32bppArgb); using Graphics graphics = Graphics.FromImage(bitmap); nint hdc = graphics.GetHdc(); bool flag = PrintWindow(hwnd, hdc, 2u); graphics.ReleaseHdc(hdc); if (!flag) { graphics.CopyFromScreen(rect.left, rect.top, 0, 0, new Size(w, h), CopyPixelOperation.SourceCopy); } return bitmap; } private static Rectangle GetAllScreenBounds() { Screen[] allScreens = Screen.AllScreens; int num = allScreens.Min((Screen s) => s.Bounds.X); int num2 = allScreens.Min((Screen s) => s.Bounds.Y); int num3 = allScreens.Max((Screen s) => s.Bounds.Right); int num4 = allScreens.Max((Screen s) => s.Bounds.Bottom); return new Rectangle(num, num2, num3 - num, num4 - num2); } private static nint FindScrollableChild(nint hwnd) { string[] array = new string[7] { "Internet Explorer_Server", "Chrome_RenderWidgetHostHWND", "MozillaWindowClass", "RichEdit20W", "RICHEDIT50W", "TextBox", "EDIT" }; foreach (string className in array) { nint num = FindWindowEx(hwnd, IntPtr.Zero, className, null); if (num != IntPtr.Zero) { return num; } } return IntPtr.Zero; } private static void SendPageDown(nint hwnd) { SendMessage(hwnd, 256u, new IntPtr(34), IntPtr.Zero); SendMessage(hwnd, 257u, new IntPtr(34), IntPtr.Zero); } private unsafe static bool AreSimilar(Bitmap a, Bitmap b) { if (a.Width != b.Width || a.Height != b.Height) { return false; } int num = (int)((double)a.Height * 0.8); int width = a.Width; int height = a.Height; Rectangle rect = new Rectangle(0, num, width, height - num); Rectangle rect2 = new Rectangle(0, num, width, height - num); BitmapData bitmapData = a.LockBits(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); BitmapData bitmapData2 = b.LockBits(rect2, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); try { int num2 = 0; int num3 = 0; int stride = bitmapData.Stride; int num4 = width / 16 + 1; int num5 = (height - num) / 8 + 1; byte* ptr = (byte*)((IntPtr)bitmapData.Scan0).ToPointer(); byte* ptr2 = (byte*)((IntPtr)bitmapData2.Scan0).ToPointer(); for (int i = 0; i < num5; i++) { int num6 = i * 8; if (num6 >= height - num) { break; } for (int j = 0; j < num4; j++) { int num7 = j * 16; if (num7 >= width) { break; } int num8 = num6 * stride + num7 * 4; if (Math.Abs(ptr[num8] - ptr2[num8]) < 5 && Math.Abs(ptr[num8 + 1] - ptr2[num8 + 1]) < 5 && Math.Abs(ptr[num8 + 2] - ptr2[num8 + 2]) < 5) { num2++; } num3++; } } return num3 > 0 && (double)num2 / (double)num3 > 0.97; } finally { a.UnlockBits(bitmapData); b.UnlockBits(bitmapData2); } } private static Bitmap StitchFrames(List frames, int windowHeight) { if (frames.Count == 0) { return new Bitmap(1, 1); } if (frames.Count == 1) { return new Bitmap(frames[0]); } int width = frames[0].Width; List list = new List(); List list2 = new List(); int num = windowHeight; for (int i = 1; i < frames.Count; i++) { int num2 = FindOverlap(frames[i - 1], frames[i]); int num3 = ((num2 > 0) ? num2 : (windowHeight / 5)); int num4 = windowHeight - num3; if (num4 <= 0) { num4 = windowHeight / 4; num3 = windowHeight - num4; } list.Add(num3); list2.Add(num4); num += num4; } Bitmap bitmap = new Bitmap(width, num, PixelFormat.Format32bppArgb); using Graphics graphics = Graphics.FromImage(bitmap); graphics.DrawImage(frames[0], 0, 0, width, windowHeight); int num5 = windowHeight; for (int j = 1; j < frames.Count; j++) { int y = list[j - 1]; int num6 = list2[j - 1]; graphics.DrawImage(srcRect: new Rectangle(0, y, width, num6), destRect: new Rectangle(0, num5, width, num6), image: frames[j], srcUnit: GraphicsUnit.Pixel); num5 += num6; } return bitmap; } private unsafe static int FindOverlap(Bitmap prev, Bitmap next) { int num = Math.Min(prev.Width, next.Width); int height = prev.Height; if (height < 16 || num < 16) { return 0; } int num2 = (int)((double)height * 0.7); BitmapData bitmapData = prev.LockBits(new Rectangle(0, 0, prev.Width, prev.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); BitmapData bitmapData2 = next.LockBits(new Rectangle(0, 0, next.Width, next.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); try { int stride = bitmapData.Stride; int stride2 = bitmapData2.Stride; int result = 0; byte* ptr = (byte*)((IntPtr)bitmapData.Scan0).ToPointer(); byte* ptr2 = (byte*)((IntPtr)bitmapData2.Scan0).ToPointer(); for (int num3 = num2; num3 > 8; num3 -= 2) { int num4 = height - num3; if (num4 >= 0) { int num5 = 0; int num6 = 0; for (int i = 0; i < 8; i++) { int num7 = i * (num3 / 8); int num8 = num4 + num7; int num9 = num7; if (num8 >= height || num9 >= next.Height) { continue; } for (int j = 4; j < num - 4; j += 12) { int num10 = num8 * stride + j * 4; int num11 = num9 * stride2 + j * 4; if (num10 + 2 < bitmapData.Height * stride && num11 + 2 < bitmapData2.Height * stride2) { if (Math.Abs(ptr[num10] - ptr2[num11]) < 10 && Math.Abs(ptr[num10 + 1] - ptr2[num11 + 1]) < 10 && Math.Abs(ptr[num10 + 2] - ptr2[num11 + 2]) < 10) { num5++; } num6++; } } } if (num6 > 0 && (double)num5 / (double)num6 > 0.8) { result = num3; break; } } } return result; } finally { prev.UnlockBits(bitmapData); next.UnlockBits(bitmapData2); } } private async Task CaptureRegionAsync(string timestamp, CancellationToken ct) { Rectangle bounds = GetAllScreenBounds(); Bitmap fullBmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb); try { using (Graphics g = Graphics.FromImage(fullBmp)) { g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy); } Rectangle? selected = null; System.Windows.Application current = System.Windows.Application.Current; if (current != null) { ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate { RegionSelectWindow regionSelectWindow = new RegionSelectWindow(fullBmp, bounds); regionSelectWindow.ShowDialog(); selected = regionSelectWindow.SelectedRect; }); } if (!selected.HasValue || selected.Value.Width < 4 || selected.Value.Height < 4) { NotificationService.Notify("AX Copilot", "영역 선택이 취소되었습니다."); return; } Rectangle r = selected.Value; using Bitmap crop = new Bitmap(r.Width, r.Height, PixelFormat.Format32bppArgb); using (Graphics g2 = Graphics.FromImage(crop)) { g2.DrawImage(fullBmp, new Rectangle(0, 0, r.Width, r.Height), r, GraphicsUnit.Pixel); } CopyToClipboard(crop); NotificationService.Notify("영역 캡처 완료", $"{r.Width}×{r.Height} · 클립보드에 복사되었습니다"); await Task.CompletedTask; } finally { if (fullBmp != null) { ((IDisposable)fullBmp).Dispose(); } } } private static void CopyToClipboard(Bitmap bmp) { try { System.Windows.Application current = System.Windows.Application.Current; if (current == null) { return; } ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate { using MemoryStream memoryStream = new MemoryStream(); bmp.Save(memoryStream, ImageFormat.Bmp); memoryStream.Position = 0L; BitmapImage bitmapImage = new BitmapImage(); bitmapImage.BeginInit(); bitmapImage.StreamSource = memoryStream; bitmapImage.CacheOption = BitmapCacheOption.OnLoad; bitmapImage.EndInit(); ((Freezable)bitmapImage).Freeze(); System.Windows.Clipboard.SetImage(bitmapImage); }); } catch (Exception ex) { LogService.Warn("클립보드 이미지 복사 실패: " + ex.Message); } } private static nint GetLauncherHwnd() { try { nint hwnd = IntPtr.Zero; System.Windows.Application current = System.Windows.Application.Current; if (current != null) { ((DispatcherObject)current).Dispatcher.Invoke((Action)delegate { Window window = System.Windows.Application.Current.Windows.OfType().FirstOrDefault((Window w) => ((object)w).GetType().Name == "LauncherWindow"); if (window != null) { hwnd = new WindowInteropHelper(window).Handle; } }); } return hwnd; } catch { return IntPtr.Zero; } } }