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; /// /// L5-2: 화면 텍스트 OCR 추출 핸들러. /// 예: ocr → 옵션 목록 (영역 선택 / 클립보드 이미지) /// ocr region → 드래그 영역 선택 후 텍스트 추출 /// ocr clip → 클립보드 이미지에서 텍스트 추출 /// 결과 텍스트는 클립보드에 복사되고 런처 입력창에 채워집니다. /// 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> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim().ToLowerInvariant(); var items = new List { 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>(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 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().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; } }