22using Microsoft . Extensions . Logging ;
33using STranslate . Plugin . Ocr . WindowsNative . View ;
44using STranslate . Plugin . Ocr . WindowsNative . ViewModel ;
5+ using System . Collections . Generic ;
6+ using System . Text ;
57using System . Windows . Controls ;
68using Windows . Globalization ;
79using Windows . Graphics . Imaging ;
@@ -95,6 +97,8 @@ public async Task<OcrResult> RecognizeAsync(OcrRequest request, CancellationToke
9597 request . Language ) ;
9698
9799 // 1. byte[] -> SoftwareBitmap
100+ cancellationToken . ThrowIfCancellationRequested ( ) ;
101+
98102 using var stream = new InMemoryRandomAccessStream ( ) ;
99103 using ( var writer = new DataWriter ( stream ) )
100104 {
@@ -104,7 +108,12 @@ public async Task<OcrResult> RecognizeAsync(OcrRequest request, CancellationToke
104108 }
105109 stream . Seek ( 0 ) ;
106110
111+ cancellationToken . ThrowIfCancellationRequested ( ) ;
112+
107113 var decoder = await BitmapDecoder . CreateAsync ( stream ) ;
114+
115+ cancellationToken . ThrowIfCancellationRequested ( ) ;
116+
108117 var softwareBitmap = await decoder . GetSoftwareBitmapAsync ( ) ;
109118
110119 var originalWidth = ( uint ) softwareBitmap . PixelWidth ;
@@ -130,6 +139,7 @@ public async Task<OcrResult> RecognizeAsync(OcrRequest request, CancellationToke
130139 ScaledHeight = scaledHeight ,
131140 } ;
132141 softwareBitmap . Dispose ( ) ;
142+ cancellationToken . ThrowIfCancellationRequested ( ) ;
133143 softwareBitmap = await decoder . GetSoftwareBitmapAsync (
134144 BitmapPixelFormat . Bgra8 ,
135145 BitmapAlphaMode . Premultiplied ,
@@ -141,10 +151,17 @@ public async Task<OcrResult> RecognizeAsync(OcrRequest request, CancellationToke
141151 }
142152
143153 // 3. 创建 OCR 引擎
154+ // 优先采用宿主传入的 per-request 语言(request.Language → BCP-47 tag),
155+ // 未指定时回落到用户已在设置中保存的语言,均为空则走自动检测。
156+ var languageTag = GetLanguage ( request . Language ) ;
157+ if ( string . IsNullOrWhiteSpace ( languageTag ) )
158+ {
159+ languageTag = string . IsNullOrWhiteSpace ( Settings . LanguageTag )
160+ ? null
161+ : Settings . LanguageTag ;
162+ }
163+
144164 OcrEngine engine ;
145- var languageTag = string . IsNullOrWhiteSpace ( Settings . LanguageTag )
146- ? null
147- : Settings . LanguageTag ;
148165
149166 if ( languageTag != null )
150167 {
@@ -153,17 +170,17 @@ public async Task<OcrResult> RecognizeAsync(OcrRequest request, CancellationToke
153170
154171 if ( ! supported )
155172 {
156- var msg = string . Format ( Context . GetTranslation ( "LanguageNotSupported " ) , languageTag ) + "\n " +
157- Context . GetTranslation ( "LanguageNotSupported_Hint " ) ;
173+ var msg = string . Format ( Context . GetTranslation ( "STranslate_Plugin_Ocr_WindowsNative_LanguageNotSupported " ) , languageTag ) + "\n " +
174+ Context . GetTranslation ( "STranslate_Plugin_Ocr_WindowsNative_LanguageNotSupported_Hint " ) ;
158175 var err = new OcrResult ( ) ;
159176 return err . Fail ( msg ) ;
160177 }
161178
162179 engine = OcrEngine . TryCreateFromLanguage ( language ) ;
163180 if ( engine == null )
164181 {
165- var msg = Context . GetTranslation ( "EngineCreationFailed " ) + "\n " +
166- Context . GetTranslation ( "EngineCreationFailed_Hint " ) ;
182+ var msg = Context . GetTranslation ( "STranslate_Plugin_Ocr_WindowsNative_EngineCreationFailed " ) + "\n " +
183+ Context . GetTranslation ( "STranslate_Plugin_Ocr_WindowsNative_EngineCreationFailed_Hint " ) ;
167184 var err = new OcrResult ( ) ;
168185 return err . Fail ( msg ) ;
169186 }
@@ -174,21 +191,31 @@ public async Task<OcrResult> RecognizeAsync(OcrRequest request, CancellationToke
174191 engine = OcrEngine . TryCreateFromUserProfileLanguages ( ) ;
175192 if ( engine == null )
176193 {
177- var msg = Context . GetTranslation ( "AutoLanguageFailed " ) + "\n " +
178- Context . GetTranslation ( "AutoLanguageFailed_Hint " ) ;
194+ var msg = Context . GetTranslation ( "STranslate_Plugin_Ocr_WindowsNative_AutoLanguageFailed " ) + "\n " +
195+ Context . GetTranslation ( "STranslate_Plugin_Ocr_WindowsNative_AutoLanguageFailed_Hint " ) ;
179196 var err = new OcrResult ( ) ;
180197 return err . Fail ( msg ) ;
181198 }
182199 }
183200
184- // 4. 执行 OCR 识别
185- var ocrResult = await engine . RecognizeAsync ( softwareBitmap ) ;
201+ // 4. 执行 OCR 识别,并在完成后释放 SoftwareBitmap 句柄
202+ cancellationToken . ThrowIfCancellationRequested ( ) ;
203+
204+ Windows . Media . Ocr . OcrResult ? ocrResult ;
205+ try
206+ {
207+ ocrResult = await engine . RecognizeAsync ( softwareBitmap ) ;
208+ }
209+ finally
210+ {
211+ softwareBitmap . Dispose ( ) ;
212+ }
186213
187214 if ( string . IsNullOrWhiteSpace ( ocrResult ? . Text ) )
188215 {
189216 Context . Logger . LogWarning ( "OCR 识别结果为空" ) ;
190217 var err = new OcrResult ( ) ;
191- return err . Fail ( Context . GetTranslation ( "EmptyResult " ) ) ;
218+ return err . Fail ( Context . GetTranslation ( "STranslate_Plugin_Ocr_WindowsNative_EmptyResult " ) ) ;
192219 }
193220
194221 // 5. 组装返回结果 — 按行组织,每行包含 BoxPoints 位置信息
@@ -249,50 +276,63 @@ public async Task<OcrResult> RecognizeAsync(OcrRequest request, CancellationToke
249276 return result ;
250277 }
251278
279+ /// <summary>
280+ /// 归一化 OCR 识别文本中的空格:仅保留以下三类空格,其余一律移除。
281+ /// 1) 英文标点之后(如 "Hello, world" 中 ',' 后的空格);
282+ /// 2) 英文单词 / 数字之间(如 "hello world"、"foo 123");
283+ /// 3) 中文与英文 / 数字之间,双向(如 "你好 world"、"版本 2")。
284+ /// </summary>
252285 private static string NormalizeRecognizedText ( string text )
253286 {
254287 if ( string . IsNullOrWhiteSpace ( text ) )
255288 return text ;
256289
257290 var chars = text . ToCharArray ( ) ;
258- var writeIndex = 0 ;
291+ var result = new StringBuilder ( chars . Length ) ;
259292
260293 for ( var i = 0 ; i < chars . Length ; i ++ )
261294 {
262295 var current = chars [ i ] ;
263- if ( current == ' ' &&
264- i > 0 &&
265- i < chars . Length - 1 &&
266- IsCjkOrPunctuation ( chars [ i - 1 ] ) &&
267- IsCjkOrPunctuation ( chars [ i + 1 ] ) )
296+ if ( current == ' ' && i > 0 && i < chars . Length - 1
297+ && ! ShouldKeepSpace ( chars [ i - 1 ] , chars [ i + 1 ] ) )
268298 {
269299 continue ;
270300 }
271-
272- chars [ writeIndex ++ ] = current ;
301+ result . Append ( current ) ;
273302 }
274303
275- return writeIndex == chars . Length ? text : new string ( chars , 0 , writeIndex ) ;
304+ return result . ToString ( ) ;
276305 }
277306
307+ /// <summary>
308+ /// 判断 left 与 right 之间的空格是否应予保留。
309+ /// </summary>
310+ private static bool ShouldKeepSpace ( char left , char right ) =>
311+ // 英文标点之后保留空格
312+ IsEnglishPunctuation ( left ) ||
313+ // 英文单词 / 数字之间保留空格
314+ ( IsEnglishLetterOrDigit ( left ) && IsEnglishLetterOrDigit ( right ) ) ||
315+ // 中文与英文 / 数字之间保留空格(双向)
316+ ( IsCjk ( left ) && IsEnglishLetterOrDigit ( right ) ) ||
317+ ( IsEnglishLetterOrDigit ( left ) && IsCjk ( right ) ) ;
318+
278319 private static bool IsCjk ( char c ) =>
279- c is >= '\u3400 ' and <= '\u4DBF ' or
280- >= '\u4E00 ' and <= '\u9FFF ' or
281- >= '\uF900 ' and <= '\uFAFF ' or
282- >= '\u3040 ' and <= '\u30FF ' or
283- >= '\uAC00 ' and <= '\uD7AF ' ;
320+ c is >= '㐀 ' and <= '䶿 ' or
321+ >= '一 ' and <= '鿿 ' or
322+ >= '豈 ' and <= ' ' or
323+ >= ' ' and <= 'ヿ ' or
324+ >= '가 ' and <= ' ' ;
284325
285326 /// <summary>
286- /// \u4E2D\u65E5\u97E9\u6807\u70B9\u7B26\u53F7\uFF08CJK Symbols and Punctuation + \u5168\u89D2/\u534A\u89D2\u5F62\u5F0F\uFF09\u3002
287- /// \u8986\u76D6\u5E38\u89C1\u4E2D\u6587\u6807\u70B9\uFF1A\uFF0C\u3002\u3001\uFF1B\uFF1A\uFF01\uFF1F\uFF08\uFF09\u300A\u300B\u300C\u300D\u300E\u300F\u3010\u3011\u2026\u2014\u00B7\uFF5E\u201C\u201D\u2018\u2019\u7B49\u3002
327+ /// ASCII 字母或数字。
288328 /// </summary>
289- private static bool IsCjkPunctuation ( char c ) =>
290- c is >= '\u3000 ' and <= '\u303F ' or
291- >= '\uFF00 ' and <= '\uFFEF ' ;
329+ private static bool IsEnglishLetterOrDigit ( char c ) =>
330+ c is >= 'a' and <= 'z' or >= 'A' and <= 'Z' or >= '0' and <= '9' ;
292331
293332 /// <summary>
294- /// CJK \u6C49\u5B57/\u5047\u540D/\u8C1A\u6587 \u6216 CJK \u6807\u70B9\u2014\u2014\u7528\u4E8E\u5224\u65AD\u7A7A\u683C\u4E24\u4FA7\u662F\u5426\u540C\u5C5E CJK \u8BED\u5883\u3002
295- /// \u5F53\u7A7A\u683C\u4E24\u4FA7\u5B57\u7B26\u5747\u4E3A\u6B64\u7C7B\u65F6\uFF0C\u79FB\u9664\u8BE5\u7A7A\u683C\uFF08OCR \u8BC6\u522B\u7ED3\u679C\u4E2D CJK \u5B57\u7B26\u95F4\u4E0D\u5E94\u6709\u7A7A\u683C\uFF09\u3002
333+ /// ASCII 标点或符号(非字母、非数字、非空白)。
334+ /// 覆盖常见英文标点:, . ! ? ; : ' " ( ) [ ] { } / \ @ # $ % ^ & * _ + = ~ ` < > 等。
296335 /// </summary>
297- private static bool IsCjkOrPunctuation ( char c ) => IsCjk ( c ) || IsCjkPunctuation ( c ) ;
336+ private static bool IsEnglishPunctuation ( char c ) =>
337+ c < 128 && ! char . IsLetterOrDigit ( c ) && c != ' ' ;
298338}
0 commit comments