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