Files
AX-Copilot-Codex/.decompiledproj/AxCopilot/Handlers/ScreenCaptureHandler.cs

613 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<IEnumerable<LauncherItem>> 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<LauncherItem> 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<LauncherItem>)list);
}
public IEnumerable<LauncherItem> 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<Bitmap> frames = new List<Bitmap> { 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<Bitmap> 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<int> list = new List<int>();
List<int> list2 = new List<int>();
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<Window>().FirstOrDefault((Window w) => ((object)w).GetType().Name == "LauncherWindow");
if (window != null)
{
hwnd = new WindowInteropHelper(window).Handle;
}
});
}
return hwnd;
}
catch
{
return IntPtr.Zero;
}
}
}