[Phase L2-4/L2-5] 클립보드 OCR 이미지 검색 + Ctrl+Click 다중 선택
Phase L2-4: 클립보드 이미지 OCR 텍스트 추출 및 검색 - AxCopilot.csproj: TFM net8.0-windows → net8.0-windows10.0.17763.0 (Windows OCR API 활성화) - ClipboardEntry: OcrText 프로퍼티 추가 (set), Preview → OCR 텍스트 우선 표시 (72자 상한) - SavedClipEntry: OcrText 직렬화 필드 추가, BuildSnapshot/LoadHistory 연동 - ClipboardHistoryService.OnClipboardUpdate: 이미지 저장 후 백그라운드 OCR 트리거 (EnableOcrSearch 설정 체크, capturedEntry.OcrText 비동기 갱신) - ClipboardHistoryService.ImageCache.cs: ExtractOcrTextAsync() 추가 (WinRT BitmapDecoder → SoftwareBitmap → OcrEngine.RecognizeAsync, 5,000자 상한) WinRT 별칭(WinBitmapDecoder, WinSoftwareBitmap 등) 으로 WPF 네임스페이스 충돌 방지 - AppSettings.Models.cs: ClipboardHistorySettings.EnableOcrSearch (default=true) - ClipboardHistoryHandler.GetItemsAsync: OcrText 포함 검색, 'OCR ·' 표시 배지 Phase L2-5: Ctrl+Click 클립보드 항목 다중 선택 - LauncherWindow.Shell.cs: ResultList_PreviewMouseLeftButtonUp에 Ctrl+Click 분기 추가 (IsClipboardMode + Ctrl 조합 시 ToggleMergeItem 호출, 기존 단일 선택 흐름 유지) - LauncherWindow.ShortcutHelp.cs: Ctrl+Click / Shift+↑↓ / 병합 단축키 도움말 추가 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -73,8 +73,8 @@
|
||||
| L2-1 | **클립보드 이미지 원본 해상도 보존** | 원본 PNG를 `clipboard_images/` 캐시 폴더에 저장, 썸네일(80px)은 표시용으로만 사용. 캐시 정리 정책 (30일/500MB). Enter 복원 시 원본 해상도로 클립보드 복사 | ✅ 완료 |
|
||||
| L2-2 | **Shift+Enter 실행 시 자동 클립보드 복사** | Shift+Enter로 외부 뷰어 열기 전에 해당 항목을 시스템 클립보드에 자동 복사. 텍스트/이미지(원본 해상도) 모두 지원 | ✅ 완료 |
|
||||
| ✅ L2-3 | **클립보드 이미지 미리보기 창** | `#` 이미지 항목에서 Shift+Enter → `ClipboardImagePreviewWindow`. 원본 해상도 표시, Ctrl+휠/+−/0/F 줌, PNG·JPEG·BMP 저장, Ctrl+C 복사 | 중간 |
|
||||
| L2-4 | **클립보드 검색 강화** | 이미지 OCR 텍스트 추출 → 텍스트 기반 이미지 검색. Windows OCR API (로컬) 활용 | 중간 |
|
||||
| L2-5 | **클립보드 항목 병합** | 여러 텍스트 항목을 선택하여 하나로 병합 (줄바꿈 구분). Ctrl+Click 다중 선택 | 낮음 |
|
||||
| ✅ L2-4 | **클립보드 검색 강화** | 이미지 OCR 텍스트 추출 → 텍스트 기반 이미지 검색. Windows OCR API (로컬) 활용. `OcrText` 필드, `ExtractOcrTextAsync()`, TFM `net8.0-windows10.0.17763.0` | 중간 |
|
||||
| ✅ L2-5 | **클립보드 항목 병합** | Ctrl+Click 마우스 다중 선택 추가 (Shift+↑/↓ 키보드 선택은 기존 구현). `ResultList_PreviewMouseLeftButtonUp` 분기 | 낮음 |
|
||||
|
||||
### Phase L2 추가 완료 (v1.5.3)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
|
||||
@@ -72,9 +72,11 @@ public class ClipboardHistoryHandler : IActionHandler
|
||||
else if (catFilter != null)
|
||||
filtered = filtered.Where(e => e.Category == catFilter);
|
||||
|
||||
// 텍스트 검색
|
||||
// 텍스트 검색 — 텍스트 내용, Preview, OCR 추출 텍스트 모두 포함 (Phase L2-4)
|
||||
if (!string.IsNullOrEmpty(q))
|
||||
filtered = filtered.Where(e => e.Preview.ToLowerInvariant().Contains(q));
|
||||
filtered = filtered.Where(e =>
|
||||
e.Preview.ToLowerInvariant().Contains(q) ||
|
||||
(e.OcrText != null && e.OcrText.ToLowerInvariant().Contains(q)));
|
||||
|
||||
// 핀 항목을 상단에 배치
|
||||
var sorted = filtered
|
||||
@@ -86,9 +88,11 @@ public class ClipboardHistoryHandler : IActionHandler
|
||||
{
|
||||
var pinMark = e.IsPinned ? "\uD83D\uDCCC " : "";
|
||||
var catMark = e.Category != "일반" ? $"[{e.Category}] " : "";
|
||||
// 이미지 항목에 OCR 인덱스 완료 표시
|
||||
var ocrMark = !e.IsText && e.OcrText != null ? "OCR · " : "";
|
||||
return new LauncherItem(
|
||||
$"{pinMark}{e.Preview}",
|
||||
$"{catMark}{e.RelativeTime} · {e.CopiedAt:MM/dd HH:mm}",
|
||||
$"{catMark}{ocrMark}{e.RelativeTime} · {e.CopiedAt:MM/dd HH:mm}",
|
||||
null,
|
||||
e,
|
||||
Symbol: e.IsPinned ? Symbols.Favorite : (e.IsText ? Symbols.History : Symbols.Picture));
|
||||
|
||||
@@ -189,6 +189,10 @@ public class ClipboardHistorySettings
|
||||
@"^\d{4}[\s\-]?\d{4}[\s\-]?\d{4}[\s\-]?\d{4}$", // 카드번호
|
||||
@"^(?:\d{1,3}\.){3}\d{1,3}$" // IP 주소
|
||||
};
|
||||
|
||||
/// <summary>이미지 클립보드 항목에서 OCR 텍스트를 자동 추출하여 검색에 활용 (Windows OCR, 로컬 처리)</summary>
|
||||
[JsonPropertyName("enableOcrSearch")]
|
||||
public bool EnableOcrSearch { get; set; } = true;
|
||||
}
|
||||
|
||||
// ─── 시스템 명령 ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -9,6 +9,13 @@ using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media.Imaging;
|
||||
using AxCopilot.Models;
|
||||
// WinRT OCR — 별칭으로 WPF Media.Imaging 네임스페이스 충돌 방지
|
||||
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.Services;
|
||||
|
||||
@@ -122,4 +129,55 @@ public partial class ClipboardHistoryService
|
||||
}
|
||||
catch (Exception) { return null; }
|
||||
}
|
||||
|
||||
// ─── Phase L2-4: OCR 텍스트 추출 ─────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Windows OCR API를 사용하여 이미지 파일에서 텍스트를 추출합니다.
|
||||
/// 로컬 처리 전용 (인터넷 연결 불필요). 사용자 언어 설정에 맞는 OCR 엔진 사용.
|
||||
/// </summary>
|
||||
internal static async Task<string?> ExtractOcrTextAsync(string imagePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(imagePath)) return null;
|
||||
|
||||
// 사용자 프로파일 언어 기반 OCR 엔진 생성 (한국어/영어 등 시스템 설정 따름)
|
||||
var engine = WinOcrEngine.TryCreateFromUserProfileLanguages();
|
||||
if (engine == null) return null;
|
||||
|
||||
// Windows Storage API로 이미지 파일 로드
|
||||
var storageFile = await WinStorageFile.GetFileFromPathAsync(imagePath);
|
||||
using var stream = await storageFile.OpenAsync(WinFileAccessMode.Read);
|
||||
var decoder = await WinBitmapDecoder.CreateAsync(stream);
|
||||
|
||||
// OCR은 Bgra8 포맷을 요구하므로 변환
|
||||
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();
|
||||
|
||||
// OCR 결과 5,000자 상한 (전체 문서 스캔 대응)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ public partial class ClipboardHistoryService : IDisposable
|
||||
OriginalImagePath = entry.OriginalImagePath,
|
||||
IsPinned = entry.IsPinned,
|
||||
Category = entry.Category,
|
||||
OcrText = entry.OcrText,
|
||||
};
|
||||
_history.Insert(0, updated);
|
||||
}
|
||||
@@ -218,6 +219,7 @@ public partial class ClipboardHistoryService : IDisposable
|
||||
OriginalImagePath = s.OriginalImagePath,
|
||||
IsPinned = s.IsPinned,
|
||||
Category = s.Category,
|
||||
OcrText = s.OcrText,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -300,6 +302,7 @@ public partial class ClipboardHistoryService : IDisposable
|
||||
OriginalImagePath = e.OriginalImagePath,
|
||||
IsPinned = e.IsPinned,
|
||||
Category = e.Category,
|
||||
OcrText = e.OcrText,
|
||||
};
|
||||
}).ToList();
|
||||
}
|
||||
@@ -316,6 +319,8 @@ public partial class ClipboardHistoryService : IDisposable
|
||||
public string? OriginalImagePath { get; set; }
|
||||
public bool IsPinned { get; set; }
|
||||
public string Category { get; set; } = "일반";
|
||||
/// <summary>이미지에서 추출된 OCR 텍스트 (텍스트 항목은 null).</summary>
|
||||
public string? OcrText { get; set; }
|
||||
}
|
||||
|
||||
// ─── 내부 ──────────────────────────────────────────────────────────────
|
||||
@@ -380,6 +385,21 @@ public partial class ClipboardHistoryService : IDisposable
|
||||
Image = thumb,
|
||||
OriginalImagePath = originalPath,
|
||||
};
|
||||
|
||||
// Phase L2-4: OCR 텍스트 추출 (백그라운드, 로컬 처리)
|
||||
if (_settings.Settings.ClipboardHistory.EnableOcrSearch && originalPath != null)
|
||||
{
|
||||
var capturedEntry = entry;
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
var ocrText = await ExtractOcrTextAsync(originalPath).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(ocrText))
|
||||
{
|
||||
capturedEntry.OcrText = ocrText;
|
||||
_ = SaveHistoryAsync();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (entry == null) return;
|
||||
@@ -439,15 +459,30 @@ public record ClipboardEntry(string Text, DateTime CopiedAt)
|
||||
/// <summary>카테고리 (URL, 코드, 경로, 일반)</summary>
|
||||
public string Category { get; set; } = "일반";
|
||||
|
||||
/// <summary>
|
||||
/// Windows OCR API로 추출한 이미지 텍스트 (백그라운드 처리, null이면 미처리 또는 추출 실패).
|
||||
/// 이미지 항목의 텍스트 기반 검색에 활용됩니다.
|
||||
/// </summary>
|
||||
public string? OcrText { get; set; }
|
||||
|
||||
/// <summary>텍스트 항목 여부</summary>
|
||||
public bool IsText => Image == null;
|
||||
|
||||
/// <summary>UI 표시용 첫 줄 미리보기 (최대 80자)</summary>
|
||||
/// <summary>UI 표시용 첫 줄 미리보기 (최대 80자). 이미지의 경우 OCR 텍스트 우선 표시.</summary>
|
||||
public string Preview
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsText) return "[이미지]";
|
||||
if (!IsText)
|
||||
{
|
||||
// OCR 텍스트가 있으면 첫 줄 미리보기로 표시
|
||||
if (!string.IsNullOrWhiteSpace(OcrText))
|
||||
{
|
||||
var ocrLine = OcrText.Replace("\r\n", " ").Replace("\n", " ").Replace("\r", " ").Trim();
|
||||
return ocrLine.Length > 72 ? ocrLine[..69] + "…" : ocrLine;
|
||||
}
|
||||
return "[이미지]";
|
||||
}
|
||||
var line = Text.Replace("\r\n", "↵ ").Replace("\n", "↵ ").Replace("\r", "↵ ");
|
||||
return line.Length > 80 ? line[..77] + "…" : line;
|
||||
}
|
||||
|
||||
@@ -125,6 +125,14 @@ public partial class LauncherWindow
|
||||
var clickedItem = lvi.Content as SDK.LauncherItem;
|
||||
if (clickedItem == null) return;
|
||||
|
||||
// Phase L2-5: Ctrl+Click → 클립보드 히스토리 모드에서 다중 선택 토글
|
||||
if ((Keyboard.Modifiers & ModifierKeys.Control) != 0 && _vm.IsClipboardMode)
|
||||
{
|
||||
_vm.ToggleMergeItem(clickedItem);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var timeSinceLastClick = (now - _lastClickTime).TotalMilliseconds;
|
||||
|
||||
|
||||
@@ -42,6 +42,9 @@ public partial class LauncherWindow
|
||||
"Ctrl+Enter 관리자 실행",
|
||||
"Alt+Enter 속성 보기",
|
||||
"Shift+Enter 대형 텍스트 / 이미지 미리보기 (#)",
|
||||
"Ctrl+Click 클립보드 항목 다중 선택 (#)",
|
||||
"Shift+↑/↓ 클립보드 항목 선택/해제 (#)",
|
||||
"Shift+Enter 선택 항목 병합 (MergeCount > 0)",
|
||||
};
|
||||
|
||||
CustomMessageBox.Show(
|
||||
|
||||
Reference in New Issue
Block a user