diff --git a/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs index 94a17dcf..03026a13 100644 --- a/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs +++ b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs @@ -19,6 +19,9 @@ namespace Ink_Canvas.Helpers /// internal static class WinRtHandwritingRecognizer { + private static WinRtInk.InkRecognizer _preferredHandwritingRecognizer; + private static bool _preferredHandwritingRecognizerResolved; + private static void LogHandwriting(string message, LogHelper.LogType logType = LogHelper.LogType.Info) { LogHelper.WriteLogToFile("[手写体] " + message, logType); @@ -66,6 +69,9 @@ namespace Ink_Canvas.Helpers try { + var recognizer = new WinRtInk.InkRecognizerContainer(); + TryApplyPreferredHandwritingRecognizer(recognizer, traceRecognition); + var analyzer = new WinAnalysis.InkAnalyzer(); var idToWpf = new Dictionary(); @@ -92,19 +98,21 @@ namespace Ink_Canvas.Helpers LogHandwriting( "识别:AnalyzeAsync 未得到 Updated,Status=" + (analysisResult == null ? "null" : analysisResult.Status.ToString()) + - ",有效笔画数=" + idToWpf.Count); - return HandwritingRecognitionResult.Empty; + ",有效笔画数=" + idToWpf.Count + + ",尝试整批 RecognizeAsync 回退。"); + return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true); } var wordNodes = analyzer.AnalysisRoot?.FindNodes(WinAnalysis.InkAnalysisNodeKind.InkWord); if (wordNodes == null || wordNodes.Count == 0) { if (traceRecognition) - LogHandwriting("识别:未找到 InkWord 节点(可能被判为绘图或非书写),有效笔画数=" + idToWpf.Count); - return HandwritingRecognitionResult.Empty; + LogHandwriting( + "识别:未找到 InkWord 节点(墨迹分析常将非横平笔划判为绘图),有效笔画数=" + idToWpf.Count + + ",改用整批 RecognizeAsync 回退。"); + return await RecognizeHandwritingWholeInkAsync(strokes, traceRecognition).ConfigureAwait(true); } - var recognizer = new WinRtInk.InkRecognizerContainer(); var segments = new List(); foreach (var node in wordNodes) @@ -232,6 +240,243 @@ namespace Ink_Canvas.Helpers } } + private static void TryApplyPreferredHandwritingRecognizer( + WinRtInk.InkRecognizerContainer container, + bool logDetail) + { + if (container == null) + return; + try + { + if (!_preferredHandwritingRecognizerResolved) + { + _preferredHandwritingRecognizerResolved = true; + var all = container.GetRecognizers(); + _preferredHandwritingRecognizer = SelectBestInkRecognizer(all); + if (logDetail) + { + if (_preferredHandwritingRecognizer != null) + LogHandwriting("识别器:已选用 \"" + _preferredHandwritingRecognizer.Name + "\"。"); + else if (all != null && all.Count > 0) + LogHandwriting("识别器:未匹配到与 UI/区域语言对应的引擎,使用系统默认(共 " + all.Count + " 个)。"); + } + } + + if (_preferredHandwritingRecognizer != null) + container.SetDefaultRecognizer(_preferredHandwritingRecognizer); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("[手写体] 设置默认手写识别器失败: " + ex.Message, LogHelper.LogType.Warning); + } + } + + private static WinRtInk.InkRecognizer SelectBestInkRecognizer( + IReadOnlyList list) + { + if (list == null || list.Count == 0) + return null; + + var culture = PrimaryHandwritingCulture(); + var lang = (culture?.TwoLetterISOLanguageName ?? string.Empty).ToLowerInvariant(); + var name = culture?.Name ?? string.Empty; + + bool wantZhHans = lang == "zh" && + (name.IndexOf("hans", StringComparison.OrdinalIgnoreCase) >= 0 || + name.Equals("zh-cn", StringComparison.OrdinalIgnoreCase) || + name.Equals("zh-sg", StringComparison.OrdinalIgnoreCase) || + (name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) < 0 && + !name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) && + !name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) && + !name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase))); + + bool wantZhHant = lang == "zh" && + (name.IndexOf("hant", StringComparison.OrdinalIgnoreCase) >= 0 || + name.Equals("zh-tw", StringComparison.OrdinalIgnoreCase) || + name.Equals("zh-hk", StringComparison.OrdinalIgnoreCase) || + name.Equals("zh-mo", StringComparison.OrdinalIgnoreCase)); + + WinRtInk.InkRecognizer Pick(Func match) + { + foreach (var r in list) + { + var n = r?.Name; + if (string.IsNullOrEmpty(n)) + continue; + if (match(n)) + return r; + } + + return null; + } + + if (wantZhHans) + { + var r = Pick(n => + n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0 || + (n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("简体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("簡體", StringComparison.OrdinalIgnoreCase) >= 0)) || + (n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("Simplified", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Hans", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("PRC", StringComparison.OrdinalIgnoreCase) >= 0))); + if (r != null) + return r; + r = Pick(n => + n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + else if (wantZhHant) + { + var r = Pick(n => + n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0 || + (n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("繁体", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("繁體", StringComparison.OrdinalIgnoreCase) >= 0)) || + (n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0 && + (n.IndexOf("Traditional", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Hant", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Taiwan", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Hong Kong", StringComparison.OrdinalIgnoreCase) >= 0))); + if (r != null) + return r; + r = Pick(n => + n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + else if (lang == "ja") + { + var r = Pick(n => + n.IndexOf("Japanese", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("日本語", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("日语", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + else if (lang == "en") + { + var r = Pick(n => n.IndexOf("English", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + + if (lang == "zh") + { + var r = Pick(n => + n.IndexOf("中文", StringComparison.OrdinalIgnoreCase) >= 0 || + n.IndexOf("Chinese", StringComparison.OrdinalIgnoreCase) >= 0); + if (r != null) + return r; + } + + return null; + } + + private static CultureInfo PrimaryHandwritingCulture() + { + var ui = CultureInfo.CurrentUICulture; + var ct = CultureInfo.CurrentCulture; + if (string.Equals(ui.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase)) + return ui; + if (string.Equals(ct.TwoLetterISOLanguageName, "zh", StringComparison.OrdinalIgnoreCase)) + return ct; + return ui; + } + + private static async Task RecognizeHandwritingWholeInkAsync( + StrokeCollection strokes, + bool traceRecognition) + { + if (strokes == null || strokes.Count == 0) + return HandwritingRecognitionResult.Empty; + + var container = new WinRtInk.InkStrokeContainer(); + foreach (Stroke s in strokes) + { + var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s); + if (ink != null) + container.AddStroke(ink); + } + + var winStrokes = container.GetStrokes(); + if (winStrokes == null || winStrokes.Count == 0) + { + if (traceRecognition) + LogHandwriting("整批回退:无有效 WinRT 笔画。"); + return HandwritingRecognitionResult.Empty; + } + + var reco = new WinRtInk.InkRecognizerContainer(); + TryApplyPreferredHandwritingRecognizer(reco, false); + + IReadOnlyList rr; + try + { + rr = await reco + .RecognizeAsync(container, WinRtInk.InkRecognitionTarget.All) + .AsTask() + .ConfigureAwait(true); + } + catch (Exception ex) + { + if (traceRecognition) + LogHandwriting("整批回退:RecognizeAsync 异常:" + ex.Message); + return HandwritingRecognitionResult.Empty; + } + + if (rr == null || rr.Count == 0 || rr[0] == null) + { + if (traceRecognition) + LogHandwriting("整批回退:RecognizeAsync 无结果。"); + return HandwritingRecognitionResult.Empty; + } + + var cands = rr[0].GetTextCandidates(); + var primary = (cands != null && cands.Count > 0) ? cands[0] : string.Empty; + if (string.IsNullOrWhiteSpace(primary)) + { + if (traceRecognition) + LogHandwriting("整批回退:候选文本为空。"); + return HandwritingRecognitionResult.Empty; + } + + var merged = new List(); + if (cands != null) + { + foreach (var c in cands) + { + if (!string.IsNullOrEmpty(c) && !merged.Contains(c)) + merged.Add(c); + } + } + + var bounds = UnionStrokeBounds(strokes); + var group = new List(); + foreach (Stroke s in strokes) + group.Add(s); + + var seg = new HandwritingWordSegment(primary, merged, bounds, group); + return new HandwritingRecognitionResult(new List { seg }); + } + + private static Rect UnionStrokeBounds(StrokeCollection strokes) + { + if (strokes == null || strokes.Count == 0) + return Rect.Empty; + + var r = strokes[0].GetBounds(); + for (var i = 1; i < strokes.Count; i++) + r = Rect.Union(r, strokes[i].GetBounds()); + return r; + } + private const string DefaultHandwritingFontFamilyList = "Ink Free,KaiTi,Segoe Script"; /// diff --git a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs index 7c79eee7..fcb4fd1a 100644 --- a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs +++ b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs @@ -142,14 +142,24 @@ namespace Ink_Canvas } } - private void RunStrokeCollectedPostShapeRecognitionTail(InkCanvasStrokeCollectedEventArgs e, bool wasStraightened) + /// + /// 收笔后压感/墨迹平滑等尾部处理。返回「当前应登记到手写字形替换批次」的画布笔画引用: + /// 同步贝塞尔平滑若替换了笔画,则为新 ;否则为 .Stroke。 + /// 直线拉直后事件参数中的笔画可能已不在画布上,调用方需另行传入画布上的笔画(见收笔处)。 + /// + private Stroke RunStrokeCollectedPostShapeRecognitionTail(InkCanvasStrokeCollectedEventArgs e, bool wasStraightened) { + if (e?.Stroke == null) + return null; + + var handwritingScheduleStroke = e.Stroke; + try { foreach (var stylusPoint in e.Stroke.StylusPoints) if ((stylusPoint.PressureFactor > 0.501 || stylusPoint.PressureFactor < 0.5) && stylusPoint.PressureFactor != 0) - return; + return e.Stroke; try { @@ -202,7 +212,7 @@ namespace Ink_Canvas var n = e.Stroke.StylusPoints.Count - 1; var pressure = 0.1; var x = 10; - if (n == 1) return; + if (n == 1) return e.Stroke; if (n >= x) { for (var i = 0; i < n - x; i++) @@ -276,6 +286,7 @@ namespace Ink_Canvas inkCanvas.Strokes.Remove(e.Stroke); inkCanvas.Strokes.Add(smoothedStroke); _currentCommitType = CommitReason.UserInput; + handwritingScheduleStroke = smoothedStroke; } } } @@ -293,6 +304,8 @@ namespace Ink_Canvas { drawingAttributes.FitToCurve = true; } + + return handwritingScheduleStroke; } /// @@ -325,6 +338,10 @@ namespace Ink_Canvas && Math.Abs(strokeDrawingAttributes.Width - BoardBrushInkWidth) < 0.01 && Math.Abs(strokeDrawingAttributes.Height - BoardBrushInkHeight) < 0.01; + // 手写识别须与画布显示分离:在压感/触摸模拟/笔锋/直线拉直等修改 e.Stroke 之前快照原始落笔点集。 + var handwritingRawPointsForRecognizer = + CloneStylusPointCollectionForHandwritingInput(e.Stroke?.StylusPoints); + // 检查是否启用墨迹渐隐功能 if (Settings.Canvas.EnableInkFade && !isBoardBrushStroke) { @@ -372,6 +389,8 @@ namespace Ink_Canvas // 标记是否进行了直线拉直 bool wasStraightened = false; + StylusPointCollection preBrushHandwritingPoints = null; + Stroke strokeForHandwritingBeautify = null; if (Settings.Canvas.FitToCurve) drawingAttributes.FitToCurve = false; @@ -379,7 +398,7 @@ namespace Ink_Canvas { inkCanvas.Opacity = 1; var touchPressureSimulationApplied = false; - var preBrushHandwritingPoints = CloneStylusPointCollectionForHandwritingInput(e.Stroke?.StylusPoints); + preBrushHandwritingPoints = handwritingRawPointsForRecognizer; if (Settings.Canvas.DisablePressure) { @@ -500,6 +519,8 @@ namespace Ink_Canvas // Apply line straightening and endpoint snapping if ink-to-shape is enabled + Stroke straightStrokeForHandwritingKey = null; + if (Settings.InkToShape.IsInkToShapeEnabled) { // 检查是否启用了直线自动拉直功能 @@ -542,6 +563,8 @@ namespace Ink_Canvas inkCanvas.Strokes.Add(straightStroke); _currentCommitType = CommitReason.UserInput; + straightStrokeForHandwritingKey = straightStroke; + // We can't modify e.Stroke directly, but we need to update newStrokes // to ensure proper shape recognition for the straightened line if (newStrokes.Contains(e.Stroke)) @@ -555,8 +578,10 @@ namespace Ink_Canvas } } - Stroke strokeForHandwritingBeautify = e.Stroke; - if (wasStraightened && inkCanvas.Strokes.Count > 0) + strokeForHandwritingBeautify = e.Stroke; + if (wasStraightened && straightStrokeForHandwritingKey != null) + strokeForHandwritingBeautify = straightStrokeForHandwritingKey; + else if (wasStraightened && inkCanvas.Strokes.Count > 0) strokeForHandwritingBeautify = inkCanvas.Strokes[inkCanvas.Strokes.Count - 1]; @@ -910,9 +935,15 @@ namespace Ink_Canvas try { await InkToShapeProcessCoreAsync(); + var strokeAfterTail = RunStrokeCollectedPostShapeRecognitionTail(e, wsTail); if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) - ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeHw, isBoardBrushStroke, preBrushHwPts); - RunStrokeCollectedPostShapeRecognitionTail(e, wsTail); + { + var canvasStrokeForHw = wsTail ? strokeHw : strokeAfterTail; + ScheduleHandwritingGlyphReplaceAfterStrokeCollected( + canvasStrokeForHw, + isBoardBrushStroke, + preBrushHwPts); + } } catch (Exception ex) { @@ -925,14 +956,21 @@ namespace Ink_Canvas if (InkToShapeProcess()) return; } - else if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) - { - ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke, preBrushHandwritingPoints); - } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } - RunStrokeCollectedPostShapeRecognitionTail(e, wasStraightened); + var strokeAfterTailSync = RunStrokeCollectedPostShapeRecognitionTail(e, wasStraightened); + if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify + && !ShapeRecognitionRouter.ShouldRunShapeRecognition( + Settings.InkToShape.IsInkToShapeEnabled, + ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine))) + { + var canvasStrokeForHw = wasStraightened ? strokeForHandwritingBeautify : strokeAfterTailSync; + ScheduleHandwritingGlyphReplaceAfterStrokeCollected( + canvasStrokeForHw, + isBoardBrushStroke, + preBrushHandwritingPoints); + } } /// @@ -967,6 +1005,8 @@ namespace Ink_Canvas inkCanvas.Strokes.Remove(original); inkCanvas.Strokes.Add(smoothed); _currentCommitType = CommitReason.UserInput; + // 收笔尾部仍以 original 登记手写批次;异步平滑后画布对象变为 smoothed,须迁移引用,否则防抖识别时字典 miss 会退回画布几何(非实时笔锋常见)。 + MigrateHandwritingBeautifyCanvasStrokeReference(original, smoothed); } else { @@ -2730,6 +2770,29 @@ namespace Ink_Canvas return corners.OrderBy(p => Math.Atan2(p.Y - center.Y, p.X - center.X)).ToList(); } + /// + /// 异步墨迹平滑将画布上的 替换为 后,把手写字形替换批次里的画布引用一并迁移,使识别仍命中「原始点快照」字典项。 + /// + private void MigrateHandwritingBeautifyCanvasStrokeReference(Stroke fromStroke, Stroke toStroke) + { + if (fromStroke == null || toStroke == null || ReferenceEquals(fromStroke, toStroke)) + return; + if (!Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + return; + + if (_handwritingBeautifyInkInputByCanvasStroke.TryGetValue(fromStroke, out var inkInput)) + { + _handwritingBeautifyInkInputByCanvasStroke.Remove(fromStroke); + _handwritingBeautifyInkInputByCanvasStroke[toStroke] = inkInput; + } + + for (var i = 0; i < _handwritingRecentStrokesForBeautify.Count; i++) + { + if (ReferenceEquals(_handwritingRecentStrokesForBeautify[i], fromStroke)) + _handwritingRecentStrokesForBeautify[i] = toStroke; + } + } + /// /// 收笔后:在墨迹转形状(若启用)完成之后,将笔画并入批次并启动/重置停笔防抖计时器,再于延迟后多笔合并矫正。 /// @@ -2788,10 +2851,19 @@ namespace Ink_Canvas if (preBrushHandwritingPoints != null && preBrushHandwritingPoints.Count > 0) { - _handwritingBeautifyInkInputByCanvasStroke[strokeForBeautify] = new Stroke(preBrushHandwritingPoints) + // 再拷贝一份给识别专用 Stroke,避免与外部 StylusPointCollection 或 WPF Stroke 内部共享后被改写。 + var ptsForRecognizer = CloneStylusPointCollectionForHandwritingInput(preBrushHandwritingPoints); + if (ptsForRecognizer != null && ptsForRecognizer.Count > 0) { - DrawingAttributes = strokeForBeautify.DrawingAttributes.Clone() - }; + _handwritingBeautifyInkInputByCanvasStroke[strokeForBeautify] = new Stroke(ptsForRecognizer) + { + DrawingAttributes = strokeForBeautify.DrawingAttributes.Clone() + }; + } + else + { + _handwritingBeautifyInkInputByCanvasStroke.Remove(strokeForBeautify); + } } else { @@ -2856,9 +2928,17 @@ namespace Ink_Canvas continue; canvasStrokes.Add(s); if (_handwritingBeautifyInkInputByCanvasStroke.TryGetValue(s, out var inkInput) && inkInput != null) + { recognitionInput.Add(inkInput); + } else + { + LogHelper.WriteLogToFile( + "[手写体] 批次识别输入回退为画布笔画(未命中原始点快照)。画布点数=" + + (s.StylusPoints?.Count ?? 0), + LogHelper.LogType.Info); recognitionInput.Add(s); + } } if (canvasStrokes.Count == 0)