From 18b737b22b740756348327c10a33713e8552e985 Mon Sep 17 00:00:00 2001 From: CJKmkp <2564608840@qq.com> Date: Sun, 29 Mar 2026 12:24:13 +0800 Subject: [PATCH] =?UTF-8?q?add:=E6=89=8B=E5=86=99=E4=BD=93=E8=AF=86?= =?UTF-8?q?=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Helpers/InkRecognitionManager.cs | 93 ++- Ink Canvas/Helpers/InkRecognizeHelper.cs | 31 + .../Helpers/WinRtHandwritingRecognizer.cs | 599 ++++++++++++++++++ Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs | 3 +- Ink Canvas/MainWindow.xaml | 10 + Ink Canvas/MainWindow_cs/MW_Settings.cs | 12 +- Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs | 4 + .../MW_SimulatePressure&InkToShape.cs | 174 +++++ Ink Canvas/Properties/Strings.enUS.xml | 2 + Ink Canvas/Properties/Strings.resx | 2 + Ink Canvas/Resources/Settings.cs | 8 +- 11 files changed, 925 insertions(+), 13 deletions(-) create mode 100644 Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs diff --git a/Ink Canvas/Helpers/InkRecognitionManager.cs b/Ink Canvas/Helpers/InkRecognitionManager.cs index 19be366f..69c86b32 100644 --- a/Ink Canvas/Helpers/InkRecognitionManager.cs +++ b/Ink Canvas/Helpers/InkRecognitionManager.cs @@ -100,19 +100,68 @@ namespace Ink_Canvas.Helpers return await WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes).ConfigureAwait(true); } + /// 为 true 且走 WinRT 时,将识别成功的词替换为手写风格字体的轮廓墨迹(见设置中的字体列表)。 + /// 逗号分隔的字体回退列表(WPF FontFamily);null 时使用内置默认。 public Task CorrectInkAsync( StrokeCollection strokes, - ShapeRecognitionEngineMode mode) + ShapeRecognitionEngineMode mode, + bool applyHandwritingBeautify = false, + string handwritingFontFamilyList = null) { - if (!_isInitialized || strokes == null || strokes.Count == 0) + if (!_isInitialized) + { + LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:InkRecognitionManager 未初始化。", LogHelper.LogType.Info); return Task.FromResult(strokes); + } + + if (strokes == null || strokes.Count == 0) + { + LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:无笔画。", LogHelper.LogType.Info); + return Task.FromResult(strokes); + } try { - if (ShapeRecognitionRouter.ResolveUseWinRt(mode) && _modernAnalyzer != null) - return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes); + var useWinRt = ShapeRecognitionRouter.ResolveUseWinRt(mode); + if (!applyHandwritingBeautify) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:未开启「识别转手写体字形」(applyHandwritingBeautify=false)。笔画数=" + + strokes.Count, + LogHelper.LogType.Info); + return Task.FromResult(strokes); + } - return Task.FromResult(strokes); + if (!useWinRt) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:当前引擎非 WinRT(模式=" + mode + ")。笔画数=" + strokes.Count, + LogHelper.LogType.Info); + return Task.FromResult(strokes); + } + + if (!Environment.Is64BitProcess) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:非 64 位进程,WinRT 手写体替换不可用。笔画数=" + strokes.Count, + LogHelper.LogType.Info); + return Task.FromResult(strokes); + } + + if (_modernAnalyzer == null) + { + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 跳过:ModernInkAnalyzer 未就绪(WinRT 初始化失败?)。笔画数=" + + strokes.Count, + LogHelper.LogType.Warning); + return Task.FromResult(strokes); + } + + LogHelper.WriteLogToFile( + "[手写体] CorrectInkAsync 开始:笔画数=" + strokes.Count + + ",字体=" + (string.IsNullOrWhiteSpace(handwritingFontFamilyList) ? "(默认)" : handwritingFontFamilyList.Trim()), + LogHelper.LogType.Info); + return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes, handwritingFontFamilyList); } catch (Exception ex) { @@ -121,6 +170,32 @@ namespace Ink_Canvas.Helpers } } + /// + /// WinRT 手写体识别(需 64 位进程、Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。 + /// + public Task RecognizeHandwritingAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode) + { + if (!_isInitialized || strokes == null || strokes.Count == 0) + return Task.FromResult(HandwritingRecognitionResult.Empty); + + try + { + if (!Environment.Is64BitProcess + || !ShapeRecognitionRouter.ResolveUseWinRt(mode) + || !WinRtHandwritingRecognizer.IsApiAvailable) + return Task.FromResult(HandwritingRecognitionResult.Empty); + + return WinRtHandwritingRecognizer.RecognizeHandwritingAsync(strokes); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("手写识别失败: " + ex.Message, LogHelper.LogType.Error); + return Task.FromResult(HandwritingRecognitionResult.Empty); + } + } + public bool IsValidShapeType(string shapeName) { return !string.IsNullOrEmpty(shapeName) @@ -166,9 +241,13 @@ namespace Ink_Canvas.Helpers internal sealed class ModernInkAnalyzer : IDisposable { - public Task AnalyzeAndCorrectAsync(StrokeCollection strokes) + public Task AnalyzeAndCorrectAsync( + StrokeCollection strokes, + string handwritingFontFamilyList) { - return Task.FromResult(strokes); + return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync( + strokes, + handwritingFontFamilyList); } public void Dispose() diff --git a/Ink Canvas/Helpers/InkRecognizeHelper.cs b/Ink Canvas/Helpers/InkRecognizeHelper.cs index 34f5cfe6..7ec48cf3 100644 --- a/Ink Canvas/Helpers/InkRecognizeHelper.cs +++ b/Ink Canvas/Helpers/InkRecognizeHelper.cs @@ -1,3 +1,4 @@ +using Ink_Canvas; using System.Linq; using System.Threading.Tasks; using System.Windows; @@ -101,7 +102,10 @@ namespace Ink_Canvas.Helpers { _ = InkRecognitionManager.Instance; if (ShapeRecognitionRouter.ResolveUseWinRt(mode)) + { WinRtInkShapeRecognizer.Warmup(); + WinRtHandwritingRecognizer.Warmup(); + } else RecognizeShapeIACore(new StrokeCollection()); } @@ -111,6 +115,33 @@ namespace Ink_Canvas.Helpers } } + /// WinRT 手写识别(64 位 + Windows 10+)。 + public static Task RecognizeHandwritingUnifiedAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode) => + InkRecognitionManager.Instance.RecognizeHandwritingAsync(strokes, mode); + + /// WinRT 下将识别成功的词替换为手写体字形墨迹;是否应用由设置「WinRT 识别转手写体字形」控制。 + public static Task CorrectHandwritingStrokesUnifiedAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode) => + InkRecognitionManager.Instance.CorrectInkAsync( + strokes, + mode, + MainWindow.Settings?.InkToShape?.EnableWinRtHandwritingStrokeBeautify ?? false, + MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily); + + /// 显式指定是否应用手写体字形替换(忽略开关);字体仍从设置读取。 + public static Task CorrectHandwritingStrokesUnifiedAsync( + StrokeCollection strokes, + ShapeRecognitionEngineMode mode, + bool applyHandwritingBeautify) => + InkRecognitionManager.Instance.CorrectInkAsync( + strokes, + mode, + applyHandwritingBeautify, + MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily); + internal static InkShapeRecognitionResult FromIACoreOrEmpty(ShapeRecognizeResult legacy) { if (legacy?.InkDrawingNode == null) diff --git a/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs new file mode 100644 index 00000000..94a17dcf --- /dev/null +++ b/Ink Canvas/Helpers/WinRtHandwritingRecognizer.cs @@ -0,0 +1,599 @@ +using OSVersionExtension; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Ink; +using System.Windows.Input; +using System.Windows.Media; +using WinAnalysis = global::Windows.UI.Input.Inking.Analysis; +using WinRtInk = global::Windows.UI.Input.Inking; + +namespace Ink_Canvas.Helpers +{ + /// + /// WinRT 手写体识别,以及将识别结果用手写风格字体轮廓转为墨迹笔画(「识别转手写体字形」)。 + /// + internal static class WinRtHandwritingRecognizer + { + private static void LogHandwriting(string message, LogHelper.LogType logType = LogHelper.LogType.Info) + { + LogHelper.WriteLogToFile("[手写体] " + message, logType); + } + + public static bool IsApiAvailable => + OSVersion.GetOperatingSystem() >= OSVersionExtension.OperatingSystem.Windows10; + + public static void Warmup() + { + if (!IsApiAvailable || !Environment.Is64BitProcess) return; + try + { + var d = Application.Current?.Dispatcher; + if (d == null) return; + d.BeginInvoke(new Action(async () => + { + try + { + await RecognizeHandwritingAsync(new StrokeCollection()).ConfigureAwait(true); + } + catch + { + // ignore + } + })); + } + catch + { + // ignore + } + } + + /// + /// 将当前笔画集合识别为文字片段(含候选):先用墨迹分析得到分词与 , + /// 再对每一分词用 GetTextCandidates(与当前 SDK 中部分版本的 + /// 未暴露笔画映射的局限兼容)。 + /// + public static async Task RecognizeHandwritingAsync(StrokeCollection strokes) + { + if (!IsApiAvailable || strokes == null || strokes.Count == 0) + return HandwritingRecognitionResult.Empty; + + var traceRecognition = strokes.Count > 0; + + try + { + var analyzer = new WinAnalysis.InkAnalyzer(); + var idToWpf = new Dictionary(); + + foreach (Stroke s in strokes) + { + var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(s); + if (ink == null) continue; + analyzer.AddDataForStroke(ink); + analyzer.SetStrokeDataKind(ink.Id, WinAnalysis.InkAnalysisStrokeKind.Writing); + idToWpf[ink.Id] = s; + } + + if (idToWpf.Count == 0) + { + if (traceRecognition) + LogHandwriting("识别:无有效 WinRT 笔画(全部转换失败),输入笔画数=" + strokes.Count); + return HandwritingRecognitionResult.Empty; + } + + var analysisResult = await analyzer.AnalyzeAsync().AsTask().ConfigureAwait(true); + if (analysisResult == null || analysisResult.Status != WinAnalysis.InkAnalysisStatus.Updated) + { + if (traceRecognition) + LogHandwriting( + "识别:AnalyzeAsync 未得到 Updated,Status=" + + (analysisResult == null ? "null" : analysisResult.Status.ToString()) + + ",有效笔画数=" + idToWpf.Count); + return HandwritingRecognitionResult.Empty; + } + + var wordNodes = analyzer.AnalysisRoot?.FindNodes(WinAnalysis.InkAnalysisNodeKind.InkWord); + if (wordNodes == null || wordNodes.Count == 0) + { + if (traceRecognition) + LogHandwriting("识别:未找到 InkWord 节点(可能被判为绘图或非书写),有效笔画数=" + idToWpf.Count); + return HandwritingRecognitionResult.Empty; + } + + var recognizer = new WinRtInk.InkRecognizerContainer(); + var segments = new List(); + + foreach (var node in wordNodes) + { + if (!(node is WinAnalysis.InkAnalysisInkWord word)) + continue; + + var ids = word.GetStrokeIds(); + if (ids == null || ids.Count == 0) + continue; + + var group = new List(); + foreach (var sid in ids) + { + if (idToWpf.TryGetValue(sid, out var st)) + group.Add(st); + } + + if (group.Count == 0) + continue; + + var wbr = word.BoundingRect; + var wpfRect = new Rect(wbr.X, wbr.Y, wbr.Width, wbr.Height); + var analysisText = word.RecognizedText ?? string.Empty; + + IReadOnlyList candList = Array.Empty(); + try + { + if (recognizer != null) + { + var mini = new WinRtInk.InkStrokeContainer(); + foreach (var st in group) + { + var ink = WinRtInkShapeRecognizer.CreateInkStrokeFromWpf(st); + if (ink != null) + mini.AddStroke(ink); + } + + var miniStrokes = mini.GetStrokes(); + if (miniStrokes != null && miniStrokes.Count > 0) + { + var rr = await recognizer + .RecognizeAsync(mini, WinRtInk.InkRecognitionTarget.All) + .AsTask() + .ConfigureAwait(true); + if (rr != null && rr.Count > 0 && rr[0] != null) + { + var cands = rr[0].GetTextCandidates(); + if (cands != null && cands.Count > 0) + candList = cands.ToList(); + } + } + } + } + catch + { + candList = Array.Empty(); + } + + var primary = candList.Count > 0 ? candList[0] : analysisText; + var mergedCandidates = new List(); + if (candList.Count > 0) + { + foreach (var c in candList) + { + if (!string.IsNullOrEmpty(c) && !mergedCandidates.Contains(c)) + mergedCandidates.Add(c); + } + } + + if (!string.IsNullOrEmpty(analysisText) && !mergedCandidates.Contains(analysisText)) + mergedCandidates.Insert(0, analysisText); + + if (mergedCandidates.Count == 0 && !string.IsNullOrEmpty(primary)) + mergedCandidates.Add(primary); + + segments.Add(new HandwritingWordSegment( + primary, + mergedCandidates, + wpfRect, + group)); + } + + if (segments.Count == 0) + { + if (traceRecognition) + LogHandwriting("识别:分词列表为空(InkWord 无有效笔画映射)。"); + return HandwritingRecognitionResult.Empty; + } + + var hr = new HandwritingRecognitionResult(segments); + if (traceRecognition) + { + var preview = hr.CombinedText; + if (preview.Length > 120) + preview = preview.Substring(0, 117) + "..."; + LogHandwriting( + "识别成功:词数=" + segments.Count + + ",合并文本=\"" + preview + "\"" + + ",进程位数=" + (Environment.Is64BitProcess ? "x64" : "x86")); + for (var i = 0; i < segments.Count; i++) + { + var seg = segments[i]; + var t = seg.Text ?? ""; + if (t.Length > 40) + t = t.Substring(0, 37) + "..."; + LogHandwriting( + " 词[" + i + "] 文本=\"" + t + "\",笔画数=" + seg.Strokes.Count + + ",候选数=" + (seg.TextCandidates?.Count ?? 0) + + ",框=(" + Math.Round(seg.BoundingRectangle.X, 1) + "," + + Math.Round(seg.BoundingRectangle.Y, 1) + "," + + Math.Round(seg.BoundingRectangle.Width, 1) + "×" + + Math.Round(seg.BoundingRectangle.Height, 1) + ")"); + } + } + + return hr; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("WinRT 手写识别失败: " + ex.Message, LogHelper.LogType.Warning); + if (strokes != null && strokes.Count > 0) + LogHandwriting("识别异常:" + ex.Message, LogHelper.LogType.Warning); + return HandwritingRecognitionResult.Empty; + } + } + + private const string DefaultHandwritingFontFamilyList = "Ink Free,KaiTi,Segoe Script"; + + /// + /// 识别手写词后,将「有识别文本」的分词替换为指定手写风格字体的字形轮廓墨迹;未识别或空文本的词保留原笔画。 + /// + public static async Task ConvertRecognizedTextToHandwritingInkAsync( + StrokeCollection strokes, + string handwritingFontFamilyList) + { + if (!IsApiAvailable || strokes == null || strokes.Count == 0) + { + if (strokes != null && strokes.Count > 0 && !IsApiAvailable) + LogHandwriting("字形替换:跳过,IsApiAvailable=false。"); + return strokes; + } + + var fontList = string.IsNullOrWhiteSpace(handwritingFontFamilyList) + ? DefaultHandwritingFontFamilyList + : handwritingFontFamilyList.Trim(); + LogHandwriting( + "字形替换开始:输入笔画数=" + strokes.Count + + ",字体链=\"" + fontList + "\"" + + ",PixelsPerDip=" + Math.Round(GetPixelsPerDipSafe(), 3)); + + try + { + var reco = await RecognizeHandwritingAsync(strokes).ConfigureAwait(true); + if (!reco.IsSuccess || reco.Words == null || reco.Words.Count == 0) + { + LogHandwriting( + "字形替换中止:识别未成功(IsSuccess=" + reco.IsSuccess + + ",词数=" + (reco.Words?.Count ?? 0) + "),原样返回笔画。"); + return strokes; + } + + var firstStrokeToSegment = new Dictionary(); + foreach (var w in reco.Words) + { + if (w?.Strokes == null || w.Strokes.Count == 0) + continue; + var ordered = w.Strokes.OrderBy(st => IndexOfStrokeInCollection(strokes, st)).ToList(); + var first = ordered[0]; + if (!firstStrokeToSegment.ContainsKey(first)) + firstStrokeToSegment[first] = w; + } + + if (firstStrokeToSegment.Count == 0) + { + LogHandwriting("字形替换中止:无法建立「首笔画→分词」映射,原样返回。"); + return strokes; + } + + var consumed = new HashSet(); + var result = new StrokeCollection(); + var pixelsPerDip = GetPixelsPerDipSafe(); + var replacedWordCount = 0; + var keptOriginalWordCount = 0; + var glyphStrokeTotal = 0; + + foreach (Stroke s in strokes) + { + if (consumed.Contains(s)) + continue; + + if (!firstStrokeToSegment.TryGetValue(s, out var seg)) + { + result.Add(s); + continue; + } + + if (string.IsNullOrWhiteSpace(seg.Text)) + { + LogHandwriting( + " 分词:文本为空,保留原笔画,笔画数=" + seg.Strokes.Count); + keptOriginalWordCount++; + foreach (var z in seg.Strokes) + { + if (!consumed.Contains(z)) + { + result.Add(z); + consumed.Add(z); + } + } + + continue; + } + + var templateDa = seg.Strokes[0]?.DrawingAttributes?.Clone() ?? new DrawingAttributes(); + OutlineAttributesForGlyphInk(templateDa); + + var glyphStrokes = CreateHandwritingGlyphStrokes( + seg.Text.Trim(), + seg.BoundingRectangle, + templateDa, + fontList, + pixelsPerDip); + + if (glyphStrokes == null || glyphStrokes.Count == 0) + { + LogHandwriting( + " 分词:字形轮廓生成失败,保留原笔画。文本=\"" + + (seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) + "\""); + keptOriginalWordCount++; + foreach (var z in seg.Strokes) + { + if (!consumed.Contains(z)) + { + result.Add(z); + consumed.Add(z); + } + } + + continue; + } + + foreach (var nk in glyphStrokes) + result.Add(nk); + glyphStrokeTotal += glyphStrokes.Count; + replacedWordCount++; + LogHandwriting( + " 分词:已替换为手写体字形墨迹,文本=\"" + + (seg.Text.Length > 30 ? seg.Text.Substring(0, 27) + "..." : seg.Text) + + "\",生成笔画数=" + glyphStrokes.Count + ",移除原笔画数=" + seg.Strokes.Count); + + foreach (var z in seg.Strokes) + consumed.Add(z); + } + + LogHandwriting( + "字形替换结束:输出笔画数=" + result.Count + + "(输入=" + strokes.Count + "),替换词数=" + replacedWordCount + + ",保留原迹词数=" + keptOriginalWordCount + + ",字形子笔画合计=" + glyphStrokeTotal); + return result; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("WinRT 手写体字形替换失败: " + ex.Message, LogHelper.LogType.Warning); + LogHandwriting("字形替换异常:" + ex, LogHelper.LogType.Warning); + return strokes; + } + } + + private static int IndexOfStrokeInCollection(StrokeCollection collection, Stroke stroke) + { + if (collection == null || stroke == null) + return int.MaxValue; + for (var i = 0; i < collection.Count; i++) + { + if (ReferenceEquals(collection[i], stroke)) + return i; + } + + return int.MaxValue; + } + + private static void OutlineAttributesForGlyphInk(DrawingAttributes da) + { + if (da == null) return; + var w = Math.Max(0.8, Math.Min(da.Width, da.Height) * 0.2); + da.Width = w; + da.Height = w; + da.StylusTip = StylusTip.Ellipse; + da.IsHighlighter = false; + } + + private static double GetPixelsPerDipSafe() + { + try + { + if (Application.Current?.MainWindow is Visual v) + return VisualTreeHelper.GetDpi(v).PixelsPerDip; + } + catch + { + // ignore + } + + return 1.0; + } + + private static Typeface ResolveHandwritingTypeface(string fontFamilyList) + { + try + { + var ff = new FontFamily(fontFamilyList ?? DefaultHandwritingFontFamilyList); + return new Typeface(ff, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); + } + catch + { + return new Typeface( + SystemFonts.MessageFontFamily, + SystemFonts.MessageFontStyle, + SystemFonts.MessageFontWeight, + FontStretches.Normal); + } + } + + private static List CreateHandwritingGlyphStrokes( + string text, + Rect placeRect, + DrawingAttributes templateDa, + string fontFamilyList, + double pixelsPerDip) + { + var list = new List(); + if (string.IsNullOrEmpty(text) || placeRect.Width < 1 || placeRect.Height < 1) + return list; + + var typeface = ResolveHandwritingTypeface(fontFamilyList); + var culture = CultureInfo.CurrentCulture; + var em = Math.Max(6.0, placeRect.Height * 0.72); + FormattedText ft = null; + + for (var i = 0; i < 14; i++) + { + ft = new FormattedText( + text, + culture, + FlowDirection.LeftToRight, + typeface, + em, + Brushes.Black, + new NumberSubstitution(NumberCultureSource.Text, culture, NumberSubstitutionMethod.Context), + TextFormattingMode.Display, + pixelsPerDip); + + if (ft.Width <= placeRect.Width * 0.96 && ft.Height <= placeRect.Height * 0.96) + break; + + em *= 0.9; + if (em < 4.5) + break; + } + + if (ft == null || ft.Width < 0.5 || ft.Height < 0.5) + return list; + + var scale = Math.Min( + placeRect.Width * 0.94 / Math.Max(1e-6, ft.Width), + placeRect.Height * 0.94 / Math.Max(1e-6, ft.Height)); + var tx = placeRect.Left + (placeRect.Width - ft.Width * scale) / 2.0; + var ty = placeRect.Top + (placeRect.Height - ft.Height * scale) / 2.0; + + Geometry geom; + try + { + geom = ft.BuildGeometry(new Point(0, 0)); + } + catch + { + return list; + } + + if (geom == null || geom.IsEmpty()) + return list; + + var m = new Matrix(scale, 0, 0, scale, tx, ty); + geom.Transform = new MatrixTransform(m); + return StrokesFromOutlinedGeometry(geom, templateDa, 0.35); + } + + private static List StrokesFromOutlinedGeometry(Geometry geometry, DrawingAttributes da, double tolerance) + { + var list = new List(); + if (geometry == null || geometry.IsEmpty() || da == null) + return list; + + Geometry outlined; + try + { + outlined = geometry.GetOutlinedPathGeometry(tolerance, ToleranceType.Absolute); + } + catch + { + return list; + } + + if (outlined == null || outlined.IsEmpty()) + return list; + + Geometry flat; + try + { + flat = outlined.GetFlattenedPathGeometry(tolerance, ToleranceType.Absolute); + } + catch + { + return list; + } + + if (!(flat is PathGeometry pg)) + return list; + + foreach (var fig in pg.Figures) + { + var pts = new StylusPointCollection(); + pts.Add(new StylusPoint(fig.StartPoint.X, fig.StartPoint.Y, 0.5f)); + foreach (var seg in fig.Segments) + { + switch (seg) + { + case LineSegment ls: + pts.Add(new StylusPoint(ls.Point.X, ls.Point.Y, 0.5f)); + break; + case PolyLineSegment pls: + foreach (var p in pls.Points) + pts.Add(new StylusPoint(p.X, p.Y, 0.5f)); + break; + } + } + + if (pts.Count >= 2) + list.Add(new Stroke(pts) { DrawingAttributes = da.Clone() }); + } + + return list; + } + } + + /// 单个手写词片段的识别结果。 + public sealed class HandwritingWordSegment + { + public HandwritingWordSegment( + string text, + IReadOnlyList textCandidates, + Rect boundingRectangle, + IReadOnlyList strokes) + { + Text = text ?? string.Empty; + TextCandidates = textCandidates ?? Array.Empty(); + BoundingRectangle = boundingRectangle; + Strokes = strokes ?? Array.Empty(); + } + + public string Text { get; } + public IReadOnlyList TextCandidates { get; } + public Rect BoundingRectangle { get; } + public IReadOnlyList Strokes { get; } + } + + /// 一次手写识别批次的汇总结果。 + public sealed class HandwritingRecognitionResult + { + public static readonly HandwritingRecognitionResult Empty = new HandwritingRecognitionResult(); + + private HandwritingRecognitionResult() + { + Words = Array.Empty(); + IsSuccess = false; + CombinedText = string.Empty; + } + + public HandwritingRecognitionResult(IReadOnlyList words) + { + Words = words ?? Array.Empty(); + IsSuccess = Words.Count > 0; + CombinedText = string.Join("", Words.Select(w => w.Text ?? string.Empty)); + } + + public bool IsSuccess { get; } + public IReadOnlyList Words { get; } + public string CombinedText { get; } + } +} diff --git a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs index 2257ad0d..bfd0618f 100644 --- a/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs +++ b/Ink Canvas/Helpers/WinRtInkShapeRecognizer.cs @@ -99,7 +99,8 @@ namespace Ink_Canvas.Helpers } } - private static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke) + /// 供 WinRT 手写等模块复用:将 WPF 转为 WinRT + internal static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke) { if (stroke?.StylusPoints == null || stroke.StylusPoints.Count == 0) return null; diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 9af46a13..035591b9 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -1137,6 +1137,16 @@ + + + + + diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index 9fb01303..0749adcd 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -3859,7 +3859,8 @@ namespace Ink_Canvas Settings.InkToShape.IsInkToShapeTriangle = true; Settings.InkToShape.IsInkToShapeRectangle = true; Settings.InkToShape.IsInkToShapeRounded = true; - + Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify = false; + Settings.InkToShape.HandwritingCorrectionFontFamily = "Ink Free,KaiTi,Segoe Script"; Settings.Startup.IsEnableNibMode = false; Settings.Startup.IsAutoUpdate = true; @@ -3942,6 +3943,15 @@ namespace Ink_Canvas SaveSettingsToFile(); } + private void ToggleSwitchEnableWinRtHandwritingStrokeBeautify_Toggled(object sender, RoutedEventArgs e) + { + if (!isLoaded) return; + Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify = + ToggleSwitchEnableWinRtHandwritingStrokeBeautify != null && + ToggleSwitchEnableWinRtHandwritingStrokeBeautify.IsOn; + SaveSettingsToFile(); + } + private void ToggleSwitchEnableInkToShapeNoFakePressureTriangle_Toggled(object sender, RoutedEventArgs e) { if (!isLoaded) return; diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index a525e3ae..c9616ec1 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -1060,6 +1060,10 @@ namespace Ink_Canvas ComboBoxShapeRecognitionEngine.SelectedIndex = eng; } + if (ToggleSwitchEnableWinRtHandwritingStrokeBeautify != null) + ToggleSwitchEnableWinRtHandwritingStrokeBeautify.IsOn = + Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify; + ToggleSwitchEnableInkToShapeNoFakePressureRectangle.IsOn = Settings.InkToShape.IsInkToShapeNoFakePressureRectangle; diff --git a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs index 9dceb38a..0b1935dc 100644 --- a/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs +++ b/Ink Canvas/MainWindow_cs/MW_SimulatePressure&InkToShape.cs @@ -43,7 +43,20 @@ namespace Ink_Canvas /// 串行执行墨迹转形状(尤其 WinRT 异步延后),避免多笔 BeginInvoke 交错修改 newStrokes。 private static readonly SemaphoreSlim InkToShapeSerial = new SemaphoreSlim(1, 1); + /// + /// 收笔后待参与「手写体字形替换」的最近笔迹(多笔一字/一词时由 WinRT InkAnalyzer 合并为 InkWord)。 + /// + private readonly StrokeCollection _handwritingRecentStrokesForBeautify = new StrokeCollection(); + private const int HandwritingBeautifyMaxRecentStrokes = 20; + + /// 手写体矫正:停笔后延迟执行(毫秒),多笔一字时等用户写完再识别。 + private const int HandwritingBeautifyDebounceMs = 1000; + + private DispatcherTimer _handwritingBeautifyDebounceTimer; + + /// 每次收笔并入批次时递增;防抖 Tick 携带快照,识别过程中若又有新笔则放弃本轮替换。 + private ulong _handwritingBeautifyScheduleRevision; /// /// 矩形参考线的列表 @@ -388,6 +401,10 @@ namespace Ink_Canvas } } + Stroke strokeForHandwritingBeautify = e.Stroke; + if (wasStraightened && inkCanvas.Strokes.Count > 0) + strokeForHandwritingBeautify = inkCanvas.Strokes[inkCanvas.Strokes.Count - 1]; + if (ShapeRecognitionRouter.ShouldRunShapeRecognition( Settings.InkToShape.IsInkToShapeEnabled, ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine))) @@ -734,6 +751,8 @@ namespace Ink_Canvas try { await InkToShapeProcessCoreAsync(); + if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke); } catch (Exception ex) { @@ -744,10 +763,16 @@ namespace Ink_Canvas } InkToShapeProcessCoreAsync().GetAwaiter().GetResult(); + if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke); } InkToShapeProcess(); } + else if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + { + ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke); + } foreach (var stylusPoint in e.Stroke.StylusPoints) //LogHelper.WriteLogToFile(stylusPoint.PressureFactor.ToString(), LogHelper.LogType.Info); @@ -2668,6 +2693,155 @@ namespace Ink_Canvas return corners.OrderBy(p => Math.Atan2(p.Y - center.Y, p.X - center.X)).ToList(); } + /// + /// 收笔后:在墨迹转形状(若启用)完成之后,将笔画并入批次并启动/重置停笔防抖计时器,再于延迟后多笔合并矫正。 + /// + private void PruneHandwritingBeautifyBatch() + { + for (var i = _handwritingRecentStrokesForBeautify.Count - 1; i >= 0; i--) + { + if (!inkCanvas.Strokes.Contains(_handwritingRecentStrokesForBeautify[i])) + _handwritingRecentStrokesForBeautify.RemoveAt(i); + } + } + + private void EnsureHandwritingBeautifyDebounceTimer() + { + if (_handwritingBeautifyDebounceTimer != null) + return; + _handwritingBeautifyDebounceTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher) + { + Interval = TimeSpan.FromMilliseconds(HandwritingBeautifyDebounceMs) + }; + _handwritingBeautifyDebounceTimer.Tick += HandwritingBeautifyDebounceTimer_Tick; + } + + /// 并入批次并重置 1s 计时器(多笔需停笔满延迟后才矫正)。 + private void ScheduleHandwritingGlyphReplaceAfterStrokeCollected(Stroke strokeForBeautify, bool isBoardBrushStroke) + { + if (!Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify) + return; + if (isBoardBrushStroke) + return; + if (strokeForBeautify == null || strokeForBeautify.DrawingAttributes?.IsHighlighter == true) + return; + if (!inkCanvas.Strokes.Contains(strokeForBeautify)) + return; + + _handwritingBeautifyScheduleRevision++; + + var alreadyInBatch = false; + foreach (Stroke x in _handwritingRecentStrokesForBeautify) + { + if (ReferenceEquals(x, strokeForBeautify)) + { + alreadyInBatch = true; + break; + } + } + + if (!alreadyInBatch) + _handwritingRecentStrokesForBeautify.Add(strokeForBeautify); + + PruneHandwritingBeautifyBatch(); + while (_handwritingRecentStrokesForBeautify.Count > HandwritingBeautifyMaxRecentStrokes) + _handwritingRecentStrokesForBeautify.RemoveAt(0); + + EnsureHandwritingBeautifyDebounceTimer(); + _handwritingBeautifyDebounceTimer.Stop(); + _handwritingBeautifyDebounceTimer.Interval = TimeSpan.FromMilliseconds(HandwritingBeautifyDebounceMs); + _handwritingBeautifyDebounceTimer.Start(); + } + + private async void HandwritingBeautifyDebounceTimer_Tick(object sender, EventArgs e) + { + if (_handwritingBeautifyDebounceTimer != null) + _handwritingBeautifyDebounceTimer.Stop(); + + var revisionWhenIdle = _handwritingBeautifyScheduleRevision; + try + { + await HandwritingGlyphReplaceFromBatchCoreAsync(revisionWhenIdle).ConfigureAwait(true); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("[手写体] 停笔防抖矫正异常: " + ex.Message, LogHelper.LogType.Warning); + Debug.WriteLine(ex); + } + } + + private async Task HandwritingGlyphReplaceFromBatchCoreAsync(ulong revisionWhenIdle) + { + await InkToShapeSerial.WaitAsync().ConfigureAwait(true); + try + { + if (_handwritingBeautifyScheduleRevision != revisionWhenIdle) + return; + + PruneHandwritingBeautifyBatch(); + + var input = new StrokeCollection(); + foreach (Stroke s in _handwritingRecentStrokesForBeautify) + { + if (inkCanvas.Strokes.Contains(s)) + input.Add(s); + } + + if (input.Count == 0) + return; + if (_handwritingBeautifyScheduleRevision != revisionWhenIdle) + return; + + var shapeMode = ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine); + var result = await InkRecognizeHelper.CorrectHandwritingStrokesUnifiedAsync(input, shapeMode).ConfigureAwait(true); + + if (_handwritingBeautifyScheduleRevision != revisionWhenIdle) + return; + if (result == null || result.Count == 0) + return; + if (ReferenceEquals(result, input)) + return; + + var anyInputStillPresent = false; + foreach (Stroke s in input) + { + if (inkCanvas.Strokes.Contains(s)) + { + anyInputStillPresent = true; + break; + } + } + + if (!anyInputStillPresent) + return; + + SetNewBackupOfStroke(); + _currentCommitType = CommitReason.ShapeRecognition; + foreach (Stroke s in input) + { + if (inkCanvas.Strokes.Contains(s)) + inkCanvas.Strokes.Remove(s); + } + + foreach (Stroke s in result) + inkCanvas.Strokes.Add(s); + _currentCommitType = CommitReason.UserInput; + + foreach (Stroke s in input) + _handwritingRecentStrokesForBeautify.Remove(s); + + PruneHandwritingBeautifyBatch(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile("[手写体] 收笔替换异常: " + ex.Message, LogHelper.LogType.Warning); + } + finally + { + InkToShapeSerial.Release(); + } + } + #endregion } } \ No newline at end of file diff --git a/Ink Canvas/Properties/Strings.enUS.xml b/Ink Canvas/Properties/Strings.enUS.xml index 7b469ec7..8b85772e 100644 --- a/Ink Canvas/Properties/Strings.enUS.xml +++ b/Ink Canvas/Properties/Strings.enUS.xml @@ -229,6 +229,8 @@ Auto IACore WinRT + WinRT: recognized text to handwriting glyphs + # When the ink correction API runs: WinRT recognizes ink words, then replaces recognized text with outline strokes from a handwriting-style font (default Ink Free / KaiTi / Segoe Script; set handwritingCorrectionFontFamily in Settings JSON). 64-bit + WinRT. Block fake pressure on corrected rectangles Block fake pressure on corrected triangles Correct freehand triangles diff --git a/Ink Canvas/Properties/Strings.resx b/Ink Canvas/Properties/Strings.resx index 44ea35ee..3f00ba51 100644 --- a/Ink Canvas/Properties/Strings.resx +++ b/Ink Canvas/Properties/Strings.resx @@ -244,6 +244,8 @@ 自动 IACore WinRT + WinRT 识别转手写体字形 + # 开启后,调用墨迹纠正 API 时:先 WinRT 识别手写词,再将识别成功的文字用手写风格字体(默认 Ink Free / 楷体 等,可在设置 JSON 的 handwritingCorrectionFontFamily 调整)转成字形轮廓墨迹替换原笔画。需 64 位与 WinRT。 阻止矫正后的矩形带有模拟压感值 阻止矫正后的三角形带有模拟压感值 矫正手绘三角形 diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index e4023769..88eef9c6 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -747,12 +747,12 @@ namespace Ink_Canvas public double LineStraightenSensitivity { get; set; } = 0.20; [JsonProperty("lineNormalizationThreshold")] public double LineNormalizationThreshold { get; set; } = 0.5; - - /// - /// 形状识别后端:0=自动(x64 用 WinRT,x86 用 IACore),1=IACore,2=WinRT。 - /// [JsonProperty("shapeRecognitionEngine")] public int ShapeRecognitionEngine { get; set; } + [JsonProperty("enableWinRtHandwritingStrokeBeautify")] + public bool EnableWinRtHandwritingStrokeBeautify { get; set; } + [JsonProperty("handwritingCorrectionFontFamily")] + public string HandwritingCorrectionFontFamily { get; set; } = "Ink Free,KaiTi,Segoe Script"; } public class RandSettings