Files
AX-Copilot-Codex/src/AxCopilot/Handlers/OcrHandler.cs
lacvet 0336904258 AX Commander 비교본 런처 기능 대량 이식
변경 목적: 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개를 확인했습니다.
2026-04-05 00:59:45 +09:00

265 lines
10 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.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;
}
}