Initial commit to new repository

This commit is contained in:
2026-04-03 18:22:19 +09:00
commit 4458bb0f52
7672 changed files with 452440 additions and 0 deletions

View File

@@ -0,0 +1,612 @@
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;
}
}
}