Skip to content

Commit 543335b

Browse files
committed
v1.0.2
1 parent c31cd48 commit 543335b

3 files changed

Lines changed: 79 additions & 39 deletions

File tree

STranslate.Plugin.Ocr.WindowsNative/Main.cs

Lines changed: 74 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
using Microsoft.Extensions.Logging;
33
using STranslate.Plugin.Ocr.WindowsNative.View;
44
using STranslate.Plugin.Ocr.WindowsNative.ViewModel;
5+
using System.Collections.Generic;
6+
using System.Text;
57
using System.Windows.Controls;
68
using Windows.Globalization;
79
using 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+
/// 覆盖常见英文标点:, . ! ? ; : ' " ( ) [ ] { } / \ @ # $ % ^ &amp; * _ + = ~ ` &lt; &gt; 等。
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
}

STranslate.Plugin.Ocr.WindowsNative/ViewModel/SettingsViewModel.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ private void RefreshLanguages()
101101

102102
AvailableLanguages =
103103
[
104-
new LanguageInfo(string.Empty, _context.GetTranslation("AutoDetect")),
104+
new LanguageInfo(string.Empty, _context.GetTranslation("STranslate_Plugin_Ocr_WindowsNative_AutoDetect")),
105105
.. languages.OrderBy(l => l.DisplayName),
106106
];
107107

@@ -117,7 +117,7 @@ .. languages.OrderBy(l => l.DisplayName),
117117
_context.Logger.LogError(ex, "刷新 OCR 语言列表失败");
118118
AvailableLanguages =
119119
[
120-
new LanguageInfo(string.Empty, _context.GetTranslation("AutoDetect")),
120+
new LanguageInfo(string.Empty, _context.GetTranslation("STranslate_Plugin_Ocr_WindowsNative_AutoDetect")),
121121
];
122122
IsOcrAvailable = false;
123123
}
@@ -148,13 +148,13 @@ private void UpdateEngineInfo()
148148
}
149149
else
150150
{
151-
EngineLanguage = _context.GetTranslation("EngineNotAvailable");
151+
EngineLanguage = _context.GetTranslation("STranslate_Plugin_Ocr_WindowsNative_EngineNotAvailable");
152152
}
153153
}
154154
catch (Exception ex)
155155
{
156156
_context.Logger.LogError(ex, "获取引擎信息失败");
157-
EngineLanguage = _context.GetTranslation("EngineNotAvailable");
157+
EngineLanguage = _context.GetTranslation("STranslate_Plugin_Ocr_WindowsNative_EngineNotAvailable");
158158
}
159159
}
160160

STranslate.Plugin.Ocr.WindowsNative/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"Name": "Windows OCR",
44
"Description": "适用于 STranslate 的 Windows 系统 OCR 插件",
55
"Author": "Evoke",
6-
"Version": "1.0.1",
6+
"Version": "1.0.2",
77
"Website": "https://learn.microsoft.com/en-us/uwp/api/windows.media.ocr.ocrengine",
88
"ExecuteFileName": "STranslate.Plugin.Ocr.WindowsNative.dll",
99
"IconPath": "icon.png"

0 commit comments

Comments
 (0)