using Everything2Everything.Core.Providers; using Windows.Globalization; using Windows.Graphics.Imaging; using Windows.Media.Ocr; using Windows.Storage.Streams; namespace Everything2Everything.Core.Converters; public sealed class OcrProvider : IConverterProvider { private static readonly string[] OcrInputs = { ".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff", ".webp", ".gif", ".heic", ".heif", ".pdf" }; private static readonly string[] OcrOutputs = { ".txt" }; private readonly PdfProvider _pdfProvider; public OcrProvider() : this(new PdfProvider()) { } public OcrProvider(PdfProvider pdfProvider) { _pdfProvider = pdfProvider; } public ProviderCapability Capability { get; } = new( Id: "ocr", DisplayName: "OCR (이미지/PDF → 텍스트)", SupportedConversions: ProviderCapability.PairsFromMatrix(OcrInputs, OcrOutputs, LossClass.Rasterize), Status: ProviderStatus.Available, Summary: "Windows OCR 엔진으로 이미지 또는 PDF 페이지에서 텍스트를 추출해 .txt로 저장합니다.", ExternalDependencies: Array.Empty(), RoadmapNote: "Windows에 설치된 OCR 언어 팩을 사용 — 한국어/영어는 Windows 11 기본 포함."); public Task CheckAvailabilityAsync(CancellationToken cancellationToken = default) { try { var langs = OcrEngine.AvailableRecognizerLanguages; if (langs is null || langs.Count == 0) return Task.FromResult(ProviderAvailability.NotReady( "Windows OCR 언어 팩이 설치되어 있지 않습니다. 설정 → 시간 및 언어 → 언어에서 OCR 기능을 추가하세요.")); return Task.FromResult(ProviderAvailability.Ready); } catch (Exception ex) { return Task.FromResult(ProviderAvailability.NotReady("Windows OCR 엔진 초기화 실패: " + ex.Message)); } } public async Task ConvertAsync( string sourcePath, string outputDirectory, string outputExtension, ConvertOptions options, IProgress? progress, CancellationToken cancellationToken) { var outExt = ConversionPair.Normalize(outputExtension); var inputExt = Path.GetExtension(sourcePath).ToLowerInvariant(); var baseName = Path.GetFileNameWithoutExtension(sourcePath); var pages = inputExt == ".pdf" ? await ExtractPdfPagesAsync(sourcePath, options, progress, cancellationToken).ConfigureAwait(false) : new List { sourcePath }; if (pages.Count == 0) return ConvertResult.Fail(sourcePath, "OCR 입력 페이지를 추출하지 못했습니다."); try { var engine = ResolveEngine(options.Ocr.Language); if (engine is null) return ConvertResult.Fail(sourcePath, $"요청한 언어({options.Ocr.Language})에 맞는 OCR 엔진을 찾을 수 없습니다. 사용 가능: {string.Join(", ", OcrEngine.AvailableRecognizerLanguages.Select(l => l.LanguageTag))}"); var pageTexts = new List(pages.Count); for (var i = 0; i < pages.Count; i++) { cancellationToken.ThrowIfCancellationRequested(); var text = await RecognizeAsync(engine, pages[i], cancellationToken).ConfigureAwait(false); pageTexts.Add(text); progress?.Report((i + 1.0) / pages.Count * 0.9); } var path = OutputPathHelper.ResolveOutputPath(outputDirectory, baseName, null, outExt, options.OnCollision); if (OutputPathHelper.ShouldSkip(path, options.OnCollision)) return ConvertResult.Skip(sourcePath, "기존 파일이 있어 건너뜁니다."); if (outExt == ".txt") { var combined = pages.Count == 1 ? pageTexts[0] : string.Join(Environment.NewLine + Environment.NewLine + "---" + Environment.NewLine + Environment.NewLine, pageTexts); await File.WriteAllTextAsync(path, combined, System.Text.Encoding.UTF8, cancellationToken).ConfigureAwait(false); } else { return ConvertResult.Fail(sourcePath, $"지원하지 않는 출력 형식: {outExt}"); } progress?.Report(1.0); return ConvertResult.Ok(sourcePath, new[] { path }); } finally { if (inputExt == ".pdf") { foreach (var p in pages) { try { if (File.Exists(p)) File.Delete(p); } catch { } } } } } private static OcrEngine? ResolveEngine(string requestedLanguage) { if (string.IsNullOrWhiteSpace(requestedLanguage) || string.Equals(requestedLanguage, "auto", StringComparison.OrdinalIgnoreCase)) { return OcrEngine.TryCreateFromUserProfileLanguages(); } var preferences = requestedLanguage.Split(new[] { '+', ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); var available = OcrEngine.AvailableRecognizerLanguages; foreach (var pref in preferences) { var matched = available.FirstOrDefault(l => l.LanguageTag.StartsWith(pref, StringComparison.OrdinalIgnoreCase)); if (matched is not null) return OcrEngine.TryCreateFromLanguage(matched); } return OcrEngine.TryCreateFromUserProfileLanguages(); } private static async Task RecognizeAsync(OcrEngine engine, string imagePath, CancellationToken cancellationToken) { using var fileStream = File.OpenRead(imagePath); using var memory = new MemoryStream(); await fileStream.CopyToAsync(memory, cancellationToken).ConfigureAwait(false); memory.Position = 0; using var randomAccess = new InMemoryRandomAccessStream(); using (var writer = new DataWriter(randomAccess.GetOutputStreamAt(0))) { writer.WriteBytes(memory.ToArray()); await writer.StoreAsync(); } randomAccess.Seek(0); var decoder = await BitmapDecoder.CreateAsync(randomAccess); using var bitmap = await decoder.GetSoftwareBitmapAsync(); var ocrResult = await engine.RecognizeAsync(bitmap); return ocrResult?.Text ?? string.Empty; } private async Task> ExtractPdfPagesAsync( string pdfPath, ConvertOptions options, IProgress? progress, CancellationToken cancellationToken) { var tempDir = Path.Combine(Path.GetTempPath(), $"e2e_ocr_{Guid.NewGuid():N}"); Directory.CreateDirectory(tempDir); var renderOptions = new ConvertOptions { OutputLocation = OutputLocation.Custom, CustomOutputDirectory = tempDir, OnCollision = NameCollision.Overwrite, PdfRender = new PdfRenderOptions { Dpi = Math.Max(150, options.PdfRender.Dpi) }, }; var inner = new Progress(p => progress?.Report(p * 0.4)); var result = _pdfProvider.ConvertCore(pdfPath, tempDir, ".png", renderOptions, inner, cancellationToken); if (result.Status != ConvertStatus.Success) return new List(); return result.OutputPaths.ToList(); } }