변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
265 lines
10 KiB
C#
265 lines
10 KiB
C#
using System.Drawing;
|
||
using System.Drawing.Imaging;
|
||
using System.IO;
|
||
using System.Windows;
|
||
using AxCopilot.SDK;
|
||
using AxCopilot.Services;
|
||
using AxCopilot.Themes;
|
||
|
||
using WinBitmapDecoder = Windows.Graphics.Imaging.BitmapDecoder;
|
||
using WinBitmapPixelFmt = Windows.Graphics.Imaging.BitmapPixelFormat;
|
||
using WinSoftwareBitmap = Windows.Graphics.Imaging.SoftwareBitmap;
|
||
using WinOcrEngine = Windows.Media.Ocr.OcrEngine;
|
||
using WinStorageFile = Windows.Storage.StorageFile;
|
||
using WinFileAccessMode = Windows.Storage.FileAccessMode;
|
||
|
||
namespace AxCopilot.Handlers;
|
||
|
||
/// <summary>
|
||
/// L5-2: 화면 텍스트 OCR 추출 핸들러.
|
||
/// 예: ocr → 옵션 목록 (영역 선택 / 클립보드 이미지)
|
||
/// ocr region → 드래그 영역 선택 후 텍스트 추출
|
||
/// ocr clip → 클립보드 이미지에서 텍스트 추출
|
||
/// 결과 텍스트는 클립보드에 복사되고 런처 입력창에 채워집니다.
|
||
/// </summary>
|
||
public class OcrHandler : IActionHandler
|
||
{
|
||
public string? Prefix => "ocr";
|
||
|
||
public PluginMetadata Metadata => new(
|
||
"OcrExtractor",
|
||
"화면 텍스트 추출 (OCR)",
|
||
"1.0",
|
||
"AX");
|
||
|
||
private const string DataRegion = "__ocr_region__";
|
||
private const string DataClipboard = "__ocr_clipboard__";
|
||
|
||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||
{
|
||
var q = query.Trim().ToLowerInvariant();
|
||
|
||
var items = new List<LauncherItem>
|
||
{
|
||
new LauncherItem(
|
||
"화면 영역 텍스트 추출",
|
||
"드래그로 영역을 선택하면 텍스트를 자동으로 인식합니다 · F4 단축키 지원",
|
||
null, DataRegion,
|
||
Symbol: "\uE8D2"),
|
||
|
||
new LauncherItem(
|
||
"클립보드 이미지 텍스트 추출",
|
||
"클립보드에 복사된 이미지에서 텍스트를 인식합니다",
|
||
null, DataClipboard,
|
||
Symbol: "\uE77F")
|
||
};
|
||
|
||
// 쿼리 필터링
|
||
if (!string.IsNullOrEmpty(q))
|
||
{
|
||
items = items.Where(i =>
|
||
i.Title.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||
i.Subtitle.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
|
||
}
|
||
|
||
// OCR 미지원 안내
|
||
if (WinOcrEngine.TryCreateFromUserProfileLanguages() == null)
|
||
{
|
||
items.Clear();
|
||
items.Add(new LauncherItem(
|
||
"OCR 기능을 사용할 수 없습니다",
|
||
"Windows 설정 → 언어에서 OCR 지원 언어 팩을 설치하세요",
|
||
null, null,
|
||
Symbol: Symbols.Info));
|
||
}
|
||
|
||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||
}
|
||
|
||
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||
{
|
||
switch (item.Data as string)
|
||
{
|
||
case DataRegion:
|
||
await ExecuteRegionOcrAsync(ct);
|
||
break;
|
||
case DataClipboard:
|
||
await ExecuteClipboardOcrAsync(ct);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// ─── 영역 선택 OCR ───────────────────────────────────────────────────────
|
||
|
||
private static async Task ExecuteRegionOcrAsync(CancellationToken ct)
|
||
{
|
||
// 런처가 완전히 사라질 때까지 대기
|
||
await Task.Delay(180, ct);
|
||
|
||
System.Drawing.Rectangle? selected = null;
|
||
Bitmap? fullBmp = null;
|
||
|
||
// UI 스레드에서 오버레이 창 표시
|
||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||
{
|
||
var bounds = GetAllScreenBounds();
|
||
fullBmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
|
||
using var g = Graphics.FromImage(fullBmp);
|
||
g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy);
|
||
|
||
var overlay = new Views.RegionSelectWindow(fullBmp, bounds);
|
||
overlay.ShowDialog();
|
||
selected = overlay.SelectedRect;
|
||
});
|
||
|
||
if (selected == null || selected.Value.Width < 8 || selected.Value.Height < 8)
|
||
{
|
||
fullBmp?.Dispose();
|
||
NotificationService.Notify("AX Copilot", "영역 선택이 취소되었습니다.");
|
||
return;
|
||
}
|
||
|
||
// 선택 영역 크롭
|
||
var r = selected.Value;
|
||
using var crop = new Bitmap(r.Width, r.Height, PixelFormat.Format32bppArgb);
|
||
using (var cg = Graphics.FromImage(crop))
|
||
cg.DrawImage(fullBmp!, new System.Drawing.Rectangle(0, 0, r.Width, r.Height), r, System.Drawing.GraphicsUnit.Pixel);
|
||
fullBmp?.Dispose();
|
||
|
||
// OCR 실행
|
||
var text = await RunOcrOnBitmapAsync(crop);
|
||
|
||
// 결과 처리
|
||
HandleOcrResult(text, $"{r.Width}×{r.Height} 영역");
|
||
}
|
||
|
||
// ─── 클립보드 이미지 OCR ─────────────────────────────────────────────────
|
||
|
||
private static async Task ExecuteClipboardOcrAsync(CancellationToken ct)
|
||
{
|
||
Bitmap? clipBmp = null;
|
||
|
||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||
{
|
||
if (Clipboard.ContainsImage())
|
||
{
|
||
var src = Clipboard.GetImage();
|
||
if (src != null)
|
||
{
|
||
// BitmapSource → System.Drawing.Bitmap 변환
|
||
var encoder = new System.Windows.Media.Imaging.PngBitmapEncoder();
|
||
encoder.Frames.Add(System.Windows.Media.Imaging.BitmapFrame.Create(src));
|
||
using var ms = new MemoryStream();
|
||
encoder.Save(ms);
|
||
ms.Position = 0;
|
||
clipBmp = new Bitmap(ms);
|
||
}
|
||
}
|
||
});
|
||
|
||
if (clipBmp == null)
|
||
{
|
||
NotificationService.Notify("AX Copilot — OCR", "클립보드에 이미지가 없습니다.");
|
||
return;
|
||
}
|
||
|
||
using (clipBmp)
|
||
{
|
||
var text = await RunOcrOnBitmapAsync(clipBmp);
|
||
HandleOcrResult(text, "클립보드 이미지");
|
||
}
|
||
}
|
||
|
||
// ─── 공통: Bitmap → OCR ─────────────────────────────────────────────────
|
||
|
||
private static async Task<string?> RunOcrOnBitmapAsync(Bitmap bmp)
|
||
{
|
||
var engine = WinOcrEngine.TryCreateFromUserProfileLanguages();
|
||
if (engine == null) return null;
|
||
|
||
// Bitmap을 임시 PNG로 저장
|
||
var tmpPath = Path.Combine(Path.GetTempPath(), $"axocr_{Guid.NewGuid():N}.png");
|
||
try
|
||
{
|
||
bmp.Save(tmpPath, ImageFormat.Png);
|
||
|
||
var storageFile = await WinStorageFile.GetFileFromPathAsync(tmpPath);
|
||
using var stream = await storageFile.OpenAsync(WinFileAccessMode.Read);
|
||
var decoder = await WinBitmapDecoder.CreateAsync(stream);
|
||
|
||
WinSoftwareBitmap? origBitmap = null;
|
||
WinSoftwareBitmap? ocrBitmap = null;
|
||
try
|
||
{
|
||
origBitmap = await decoder.GetSoftwareBitmapAsync();
|
||
ocrBitmap = origBitmap.BitmapPixelFormat == WinBitmapPixelFmt.Bgra8
|
||
? origBitmap
|
||
: WinSoftwareBitmap.Convert(origBitmap, WinBitmapPixelFmt.Bgra8);
|
||
|
||
var result = await engine.RecognizeAsync(ocrBitmap);
|
||
var text = result.Text?.Trim();
|
||
if (text?.Length > 5_000) text = text[..5_000];
|
||
return string.IsNullOrWhiteSpace(text) ? null : text;
|
||
}
|
||
finally
|
||
{
|
||
if (!ReferenceEquals(origBitmap, ocrBitmap)) origBitmap?.Dispose();
|
||
ocrBitmap?.Dispose();
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogService.Warn($"OCR 실행 오류: {ex.Message}");
|
||
return null;
|
||
}
|
||
finally
|
||
{
|
||
try { if (File.Exists(tmpPath)) File.Delete(tmpPath); } catch { /* ignore */ }
|
||
}
|
||
}
|
||
|
||
// ─── 공통: 결과 처리 ────────────────────────────────────────────────────
|
||
|
||
private static void HandleOcrResult(string? text, string source)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(text))
|
||
{
|
||
NotificationService.Notify("OCR 완료", $"{source}에서 텍스트를 인식하지 못했습니다.");
|
||
return;
|
||
}
|
||
|
||
// 클립보드에 복사
|
||
Application.Current?.Dispatcher.Invoke(() =>
|
||
{
|
||
Clipboard.SetText(text);
|
||
});
|
||
|
||
// 런처를 다시 열고 결과 텍스트를 입력창에 채움
|
||
Application.Current?.Dispatcher.BeginInvoke(() =>
|
||
{
|
||
var launcher = Application.Current?.Windows
|
||
.OfType<Views.LauncherWindow>().FirstOrDefault();
|
||
if (launcher != null)
|
||
{
|
||
launcher.SetInputText(text.Length > 200 ? text[..200] : text);
|
||
launcher.Show();
|
||
}
|
||
}, System.Windows.Threading.DispatcherPriority.Background);
|
||
|
||
// 완료 알림
|
||
var preview = text.Length > 60 ? text[..57].Replace('\n', ' ') + "…" : text.Replace('\n', ' ');
|
||
NotificationService.Notify("OCR 완료", $"클립보드 복사됨: {preview}");
|
||
LogService.Info($"OCR 성공 ({source}, {text.Length}자)");
|
||
}
|
||
|
||
// ─── 헬퍼 ───────────────────────────────────────────────────────────────
|
||
|
||
private static System.Drawing.Rectangle GetAllScreenBounds()
|
||
{
|
||
var bounds = System.Drawing.Rectangle.Empty;
|
||
foreach (System.Windows.Forms.Screen screen in System.Windows.Forms.Screen.AllScreens)
|
||
bounds = System.Drawing.Rectangle.Union(bounds, screen.Bounds);
|
||
return bounds;
|
||
}
|
||
}
|