add:手写体识别
This commit is contained in:
@@ -100,19 +100,68 @@ namespace Ink_Canvas.Helpers
|
|||||||
return await WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes).ConfigureAwait(true);
|
return await WinRtInkShapeRecognizer.RecognizeShapeAsync(strokes).ConfigureAwait(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <param name="applyHandwritingBeautify">为 true 且走 WinRT 时,将识别成功的词替换为手写风格字体的轮廓墨迹(见设置中的字体列表)。</param>
|
||||||
|
/// <param name="handwritingFontFamilyList">逗号分隔的字体回退列表(WPF FontFamily);null 时使用内置默认。</param>
|
||||||
public Task<StrokeCollection> CorrectInkAsync(
|
public Task<StrokeCollection> CorrectInkAsync(
|
||||||
StrokeCollection strokes,
|
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);
|
return Task.FromResult(strokes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strokes == null || strokes.Count == 0)
|
||||||
|
{
|
||||||
|
LogHelper.WriteLogToFile("[手写体] CorrectInkAsync 跳过:无笔画。", LogHelper.LogType.Info);
|
||||||
|
return Task.FromResult(strokes);
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (ShapeRecognitionRouter.ResolveUseWinRt(mode) && _modernAnalyzer != null)
|
var useWinRt = ShapeRecognitionRouter.ResolveUseWinRt(mode);
|
||||||
return _modernAnalyzer.AnalyzeAndCorrectAsync(strokes);
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -121,6 +170,32 @@ namespace Ink_Canvas.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WinRT 手写体识别(需 64 位进程、Windows 10+ 及系统手写识别组件)。返回分词候选与包围框,供剪贴板或插件使用。
|
||||||
|
/// </summary>
|
||||||
|
public Task<HandwritingRecognitionResult> 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)
|
public bool IsValidShapeType(string shapeName)
|
||||||
{
|
{
|
||||||
return !string.IsNullOrEmpty(shapeName)
|
return !string.IsNullOrEmpty(shapeName)
|
||||||
@@ -166,9 +241,13 @@ namespace Ink_Canvas.Helpers
|
|||||||
|
|
||||||
internal sealed class ModernInkAnalyzer : IDisposable
|
internal sealed class ModernInkAnalyzer : IDisposable
|
||||||
{
|
{
|
||||||
public Task<StrokeCollection> AnalyzeAndCorrectAsync(StrokeCollection strokes)
|
public Task<StrokeCollection> AnalyzeAndCorrectAsync(
|
||||||
|
StrokeCollection strokes,
|
||||||
|
string handwritingFontFamilyList)
|
||||||
{
|
{
|
||||||
return Task.FromResult(strokes);
|
return WinRtHandwritingRecognizer.ConvertRecognizedTextToHandwritingInkAsync(
|
||||||
|
strokes,
|
||||||
|
handwritingFontFamilyList);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Ink_Canvas;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
@@ -101,7 +102,10 @@ namespace Ink_Canvas.Helpers
|
|||||||
{
|
{
|
||||||
_ = InkRecognitionManager.Instance;
|
_ = InkRecognitionManager.Instance;
|
||||||
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
|
if (ShapeRecognitionRouter.ResolveUseWinRt(mode))
|
||||||
|
{
|
||||||
WinRtInkShapeRecognizer.Warmup();
|
WinRtInkShapeRecognizer.Warmup();
|
||||||
|
WinRtHandwritingRecognizer.Warmup();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
RecognizeShapeIACore(new StrokeCollection());
|
RecognizeShapeIACore(new StrokeCollection());
|
||||||
}
|
}
|
||||||
@@ -111,6 +115,33 @@ namespace Ink_Canvas.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>WinRT 手写识别(64 位 + Windows 10+)。</summary>
|
||||||
|
public static Task<HandwritingRecognitionResult> RecognizeHandwritingUnifiedAsync(
|
||||||
|
StrokeCollection strokes,
|
||||||
|
ShapeRecognitionEngineMode mode) =>
|
||||||
|
InkRecognitionManager.Instance.RecognizeHandwritingAsync(strokes, mode);
|
||||||
|
|
||||||
|
/// <summary>WinRT 下将识别成功的词替换为手写体字形墨迹;是否应用由设置「WinRT 识别转手写体字形」控制。</summary>
|
||||||
|
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
|
||||||
|
StrokeCollection strokes,
|
||||||
|
ShapeRecognitionEngineMode mode) =>
|
||||||
|
InkRecognitionManager.Instance.CorrectInkAsync(
|
||||||
|
strokes,
|
||||||
|
mode,
|
||||||
|
MainWindow.Settings?.InkToShape?.EnableWinRtHandwritingStrokeBeautify ?? false,
|
||||||
|
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
|
||||||
|
|
||||||
|
/// <summary>显式指定是否应用手写体字形替换(忽略开关);字体仍从设置读取。</summary>
|
||||||
|
public static Task<StrokeCollection> CorrectHandwritingStrokesUnifiedAsync(
|
||||||
|
StrokeCollection strokes,
|
||||||
|
ShapeRecognitionEngineMode mode,
|
||||||
|
bool applyHandwritingBeautify) =>
|
||||||
|
InkRecognitionManager.Instance.CorrectInkAsync(
|
||||||
|
strokes,
|
||||||
|
mode,
|
||||||
|
applyHandwritingBeautify,
|
||||||
|
MainWindow.Settings?.InkToShape?.HandwritingCorrectionFontFamily);
|
||||||
|
|
||||||
internal static InkShapeRecognitionResult FromIACoreOrEmpty(ShapeRecognizeResult legacy)
|
internal static InkShapeRecognitionResult FromIACoreOrEmpty(ShapeRecognizeResult legacy)
|
||||||
{
|
{
|
||||||
if (legacy?.InkDrawingNode == null)
|
if (legacy?.InkDrawingNode == null)
|
||||||
|
|||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// WinRT 手写体识别,以及将识别结果用手写风格字体轮廓转为墨迹笔画(「识别转手写体字形」)。
|
||||||
|
/// </summary>
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将当前笔画集合识别为文字片段(含候选):先用墨迹分析得到分词与 <see cref="WinAnalysis.InkAnalysisInkWord.RecognizedText"/>,
|
||||||
|
/// 再对每一分词用 <see cref="WinRtInk.InkRecognizerContainer"/> 取 <c>GetTextCandidates</c>(与当前 SDK 中部分版本的
|
||||||
|
/// <see cref="WinRtInk.InkRecognitionResult"/> 未暴露笔画映射的局限兼容)。
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<HandwritingRecognitionResult> 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<uint, Stroke>();
|
||||||
|
|
||||||
|
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<HandwritingWordSegment>();
|
||||||
|
|
||||||
|
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<Stroke>();
|
||||||
|
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<string> candList = Array.Empty<string>();
|
||||||
|
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<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var primary = candList.Count > 0 ? candList[0] : analysisText;
|
||||||
|
var mergedCandidates = new List<string>();
|
||||||
|
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";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 识别手写词后,将「有识别文本」的分词替换为指定手写风格字体的字形轮廓墨迹;未识别或空文本的词保留原笔画。
|
||||||
|
/// </summary>
|
||||||
|
public static async Task<StrokeCollection> 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<Stroke, HandwritingWordSegment>();
|
||||||
|
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<Stroke>();
|
||||||
|
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<Stroke> CreateHandwritingGlyphStrokes(
|
||||||
|
string text,
|
||||||
|
Rect placeRect,
|
||||||
|
DrawingAttributes templateDa,
|
||||||
|
string fontFamilyList,
|
||||||
|
double pixelsPerDip)
|
||||||
|
{
|
||||||
|
var list = new List<Stroke>();
|
||||||
|
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<Stroke> StrokesFromOutlinedGeometry(Geometry geometry, DrawingAttributes da, double tolerance)
|
||||||
|
{
|
||||||
|
var list = new List<Stroke>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>单个手写词片段的识别结果。</summary>
|
||||||
|
public sealed class HandwritingWordSegment
|
||||||
|
{
|
||||||
|
public HandwritingWordSegment(
|
||||||
|
string text,
|
||||||
|
IReadOnlyList<string> textCandidates,
|
||||||
|
Rect boundingRectangle,
|
||||||
|
IReadOnlyList<Stroke> strokes)
|
||||||
|
{
|
||||||
|
Text = text ?? string.Empty;
|
||||||
|
TextCandidates = textCandidates ?? Array.Empty<string>();
|
||||||
|
BoundingRectangle = boundingRectangle;
|
||||||
|
Strokes = strokes ?? Array.Empty<Stroke>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Text { get; }
|
||||||
|
public IReadOnlyList<string> TextCandidates { get; }
|
||||||
|
public Rect BoundingRectangle { get; }
|
||||||
|
public IReadOnlyList<Stroke> Strokes { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>一次手写识别批次的汇总结果。</summary>
|
||||||
|
public sealed class HandwritingRecognitionResult
|
||||||
|
{
|
||||||
|
public static readonly HandwritingRecognitionResult Empty = new HandwritingRecognitionResult();
|
||||||
|
|
||||||
|
private HandwritingRecognitionResult()
|
||||||
|
{
|
||||||
|
Words = Array.Empty<HandwritingWordSegment>();
|
||||||
|
IsSuccess = false;
|
||||||
|
CombinedText = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HandwritingRecognitionResult(IReadOnlyList<HandwritingWordSegment> words)
|
||||||
|
{
|
||||||
|
Words = words ?? Array.Empty<HandwritingWordSegment>();
|
||||||
|
IsSuccess = Words.Count > 0;
|
||||||
|
CombinedText = string.Join("", Words.Select(w => w.Text ?? string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
public IReadOnlyList<HandwritingWordSegment> Words { get; }
|
||||||
|
public string CombinedText { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,7 +99,8 @@ namespace Ink_Canvas.Helpers
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke)
|
/// <summary>供 WinRT 手写等模块复用:将 WPF <see cref="Stroke"/> 转为 WinRT <see cref="global::Windows.UI.Input.Inking.InkStroke"/>。</summary>
|
||||||
|
internal static global::Windows.UI.Input.Inking.InkStroke CreateInkStrokeFromWpf(Stroke stroke)
|
||||||
{
|
{
|
||||||
if (stroke?.StylusPoints == null || stroke.StylusPoints.Count == 0)
|
if (stroke?.StylusPoints == null || stroke.StylusPoints.Count == 0)
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1137,6 +1137,16 @@
|
|||||||
</ikw:SimpleStackPanel>
|
</ikw:SimpleStackPanel>
|
||||||
<TextBlock Text="{i18n:I18n Key=InkRecog_ShapeEngineHint}" TextWrapping="Wrap"
|
<TextBlock Text="{i18n:I18n Key=InkRecog_ShapeEngineHint}" TextWrapping="Wrap"
|
||||||
Foreground="#a1a1aa" MaxWidth="520" HorizontalAlignment="Left" />
|
Foreground="#a1a1aa" MaxWidth="520" HorizontalAlignment="Left" />
|
||||||
|
<ikw:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||||
|
<TextBlock Foreground="#fafafa" Text="{i18n:I18n Key=InkRecog_HandwritingBeautify}"
|
||||||
|
VerticalAlignment="Center" FontSize="14" Margin="0,0,16,0" />
|
||||||
|
<ui:ToggleSwitch OnContent="" OffContent=""
|
||||||
|
Name="ToggleSwitchEnableWinRtHandwritingStrokeBeautify"
|
||||||
|
IsOn="False" FontFamily="Microsoft YaHei UI" FontWeight="Bold"
|
||||||
|
Toggled="ToggleSwitchEnableWinRtHandwritingStrokeBeautify_Toggled" />
|
||||||
|
</ikw:SimpleStackPanel>
|
||||||
|
<TextBlock Text="{i18n:I18n Key=InkRecog_HandwritingBeautifyHint}" TextWrapping="Wrap"
|
||||||
|
Foreground="#a1a1aa" MaxWidth="520" HorizontalAlignment="Left" />
|
||||||
<ikw:SimpleStackPanel Spacing="6"
|
<ikw:SimpleStackPanel Spacing="6"
|
||||||
Visibility="{Binding ElementName=ToggleSwitchEnableInkToShape, Path=IsOn, Converter={StaticResource BooleanToVisibilityConverter}}">
|
Visibility="{Binding ElementName=ToggleSwitchEnableInkToShape, Path=IsOn, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||||
<ikw:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
<ikw:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
|
||||||
|
|||||||
@@ -3859,7 +3859,8 @@ namespace Ink_Canvas
|
|||||||
Settings.InkToShape.IsInkToShapeTriangle = true;
|
Settings.InkToShape.IsInkToShapeTriangle = true;
|
||||||
Settings.InkToShape.IsInkToShapeRectangle = true;
|
Settings.InkToShape.IsInkToShapeRectangle = true;
|
||||||
Settings.InkToShape.IsInkToShapeRounded = true;
|
Settings.InkToShape.IsInkToShapeRounded = true;
|
||||||
|
Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify = false;
|
||||||
|
Settings.InkToShape.HandwritingCorrectionFontFamily = "Ink Free,KaiTi,Segoe Script";
|
||||||
|
|
||||||
Settings.Startup.IsEnableNibMode = false;
|
Settings.Startup.IsEnableNibMode = false;
|
||||||
Settings.Startup.IsAutoUpdate = true;
|
Settings.Startup.IsAutoUpdate = true;
|
||||||
@@ -3942,6 +3943,15 @@ namespace Ink_Canvas
|
|||||||
SaveSettingsToFile();
|
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)
|
private void ToggleSwitchEnableInkToShapeNoFakePressureTriangle_Toggled(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (!isLoaded) return;
|
if (!isLoaded) return;
|
||||||
|
|||||||
@@ -1060,6 +1060,10 @@ namespace Ink_Canvas
|
|||||||
ComboBoxShapeRecognitionEngine.SelectedIndex = eng;
|
ComboBoxShapeRecognitionEngine.SelectedIndex = eng;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ToggleSwitchEnableWinRtHandwritingStrokeBeautify != null)
|
||||||
|
ToggleSwitchEnableWinRtHandwritingStrokeBeautify.IsOn =
|
||||||
|
Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify;
|
||||||
|
|
||||||
ToggleSwitchEnableInkToShapeNoFakePressureRectangle.IsOn =
|
ToggleSwitchEnableInkToShapeNoFakePressureRectangle.IsOn =
|
||||||
Settings.InkToShape.IsInkToShapeNoFakePressureRectangle;
|
Settings.InkToShape.IsInkToShapeNoFakePressureRectangle;
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,20 @@ namespace Ink_Canvas
|
|||||||
/// <summary>串行执行墨迹转形状(尤其 WinRT 异步延后),避免多笔 BeginInvoke 交错修改 newStrokes。</summary>
|
/// <summary>串行执行墨迹转形状(尤其 WinRT 异步延后),避免多笔 BeginInvoke 交错修改 newStrokes。</summary>
|
||||||
private static readonly SemaphoreSlim InkToShapeSerial = new SemaphoreSlim(1, 1);
|
private static readonly SemaphoreSlim InkToShapeSerial = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 收笔后待参与「手写体字形替换」的最近笔迹(多笔一字/一词时由 WinRT InkAnalyzer 合并为 InkWord)。
|
||||||
|
/// </summary>
|
||||||
|
private readonly StrokeCollection _handwritingRecentStrokesForBeautify = new StrokeCollection();
|
||||||
|
|
||||||
|
private const int HandwritingBeautifyMaxRecentStrokes = 20;
|
||||||
|
|
||||||
|
/// <summary>手写体矫正:停笔后延迟执行(毫秒),多笔一字时等用户写完再识别。</summary>
|
||||||
|
private const int HandwritingBeautifyDebounceMs = 1000;
|
||||||
|
|
||||||
|
private DispatcherTimer _handwritingBeautifyDebounceTimer;
|
||||||
|
|
||||||
|
/// <summary>每次收笔并入批次时递增;防抖 Tick 携带快照,识别过程中若又有新笔则放弃本轮替换。</summary>
|
||||||
|
private ulong _handwritingBeautifyScheduleRevision;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 矩形参考线的列表
|
/// 矩形参考线的列表
|
||||||
@@ -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(
|
if (ShapeRecognitionRouter.ShouldRunShapeRecognition(
|
||||||
Settings.InkToShape.IsInkToShapeEnabled,
|
Settings.InkToShape.IsInkToShapeEnabled,
|
||||||
ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine)))
|
ShapeRecognitionRouter.FromSettingsInt(Settings.InkToShape.ShapeRecognitionEngine)))
|
||||||
@@ -734,6 +751,8 @@ namespace Ink_Canvas
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await InkToShapeProcessCoreAsync();
|
await InkToShapeProcessCoreAsync();
|
||||||
|
if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify)
|
||||||
|
ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -744,10 +763,16 @@ namespace Ink_Canvas
|
|||||||
}
|
}
|
||||||
|
|
||||||
InkToShapeProcessCoreAsync().GetAwaiter().GetResult();
|
InkToShapeProcessCoreAsync().GetAwaiter().GetResult();
|
||||||
|
if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify)
|
||||||
|
ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke);
|
||||||
}
|
}
|
||||||
|
|
||||||
InkToShapeProcess();
|
InkToShapeProcess();
|
||||||
}
|
}
|
||||||
|
else if (Settings.InkToShape.EnableWinRtHandwritingStrokeBeautify)
|
||||||
|
{
|
||||||
|
ScheduleHandwritingGlyphReplaceAfterStrokeCollected(strokeForHandwritingBeautify, isBoardBrushStroke);
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var stylusPoint in e.Stroke.StylusPoints)
|
foreach (var stylusPoint in e.Stroke.StylusPoints)
|
||||||
//LogHelper.WriteLogToFile(stylusPoint.PressureFactor.ToString(), LogHelper.LogType.Info);
|
//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();
|
return corners.OrderBy(p => Math.Atan2(p.Y - center.Y, p.X - center.X)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 收笔后:在墨迹转形状(若启用)完成之后,将笔画并入批次并启动/重置停笔防抖计时器,再于延迟后多笔合并矫正。
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>并入批次并重置 1s 计时器(多笔需停笔满延迟后才矫正)。</summary>
|
||||||
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,6 +229,8 @@
|
|||||||
<data name="InkRecog_ShapeEngineAuto" xml:space="preserve"><value>Auto</value></data>
|
<data name="InkRecog_ShapeEngineAuto" xml:space="preserve"><value>Auto</value></data>
|
||||||
<data name="InkRecog_ShapeEngineIACore" xml:space="preserve"><value>IACore</value></data>
|
<data name="InkRecog_ShapeEngineIACore" xml:space="preserve"><value>IACore</value></data>
|
||||||
<data name="InkRecog_ShapeEngineWinRT" xml:space="preserve"><value>WinRT</value></data>
|
<data name="InkRecog_ShapeEngineWinRT" xml:space="preserve"><value>WinRT</value></data>
|
||||||
|
<data name="InkRecog_HandwritingBeautify" xml:space="preserve"><value>WinRT: recognized text to handwriting glyphs</value></data>
|
||||||
|
<data name="InkRecog_HandwritingBeautifyHint" xml:space="preserve"><value># 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.</value></data>
|
||||||
<data name="InkRecog_BlockRectFakePressure" xml:space="preserve"><value>Block fake pressure on corrected rectangles</value></data>
|
<data name="InkRecog_BlockRectFakePressure" xml:space="preserve"><value>Block fake pressure on corrected rectangles</value></data>
|
||||||
<data name="InkRecog_BlockTriFakePressure" xml:space="preserve"><value>Block fake pressure on corrected triangles</value></data>
|
<data name="InkRecog_BlockTriFakePressure" xml:space="preserve"><value>Block fake pressure on corrected triangles</value></data>
|
||||||
<data name="InkRecog_FixTriangle" xml:space="preserve"><value>Correct freehand triangles</value></data>
|
<data name="InkRecog_FixTriangle" xml:space="preserve"><value>Correct freehand triangles</value></data>
|
||||||
|
|||||||
@@ -244,6 +244,8 @@
|
|||||||
<data name="InkRecog_ShapeEngineAuto" xml:space="preserve"><value>自动</value></data>
|
<data name="InkRecog_ShapeEngineAuto" xml:space="preserve"><value>自动</value></data>
|
||||||
<data name="InkRecog_ShapeEngineIACore" xml:space="preserve"><value>IACore</value></data>
|
<data name="InkRecog_ShapeEngineIACore" xml:space="preserve"><value>IACore</value></data>
|
||||||
<data name="InkRecog_ShapeEngineWinRT" xml:space="preserve"><value>WinRT</value></data>
|
<data name="InkRecog_ShapeEngineWinRT" xml:space="preserve"><value>WinRT</value></data>
|
||||||
|
<data name="InkRecog_HandwritingBeautify" xml:space="preserve"><value>WinRT 识别转手写体字形</value></data>
|
||||||
|
<data name="InkRecog_HandwritingBeautifyHint" xml:space="preserve"><value># 开启后,调用墨迹纠正 API 时:先 WinRT 识别手写词,再将识别成功的文字用手写风格字体(默认 Ink Free / 楷体 等,可在设置 JSON 的 handwritingCorrectionFontFamily 调整)转成字形轮廓墨迹替换原笔画。需 64 位与 WinRT。</value></data>
|
||||||
<data name="InkRecog_BlockRectFakePressure" xml:space="preserve"><value>阻止矫正后的矩形带有模拟压感值</value></data>
|
<data name="InkRecog_BlockRectFakePressure" xml:space="preserve"><value>阻止矫正后的矩形带有模拟压感值</value></data>
|
||||||
<data name="InkRecog_BlockTriFakePressure" xml:space="preserve"><value>阻止矫正后的三角形带有模拟压感值</value></data>
|
<data name="InkRecog_BlockTriFakePressure" xml:space="preserve"><value>阻止矫正后的三角形带有模拟压感值</value></data>
|
||||||
<data name="InkRecog_FixTriangle" xml:space="preserve"><value>矫正手绘三角形</value></data>
|
<data name="InkRecog_FixTriangle" xml:space="preserve"><value>矫正手绘三角形</value></data>
|
||||||
|
|||||||
@@ -747,12 +747,12 @@ namespace Ink_Canvas
|
|||||||
public double LineStraightenSensitivity { get; set; } = 0.20;
|
public double LineStraightenSensitivity { get; set; } = 0.20;
|
||||||
[JsonProperty("lineNormalizationThreshold")]
|
[JsonProperty("lineNormalizationThreshold")]
|
||||||
public double LineNormalizationThreshold { get; set; } = 0.5;
|
public double LineNormalizationThreshold { get; set; } = 0.5;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 形状识别后端:0=自动(x64 用 WinRT,x86 用 IACore),1=IACore,2=WinRT。
|
|
||||||
/// </summary>
|
|
||||||
[JsonProperty("shapeRecognitionEngine")]
|
[JsonProperty("shapeRecognitionEngine")]
|
||||||
public int ShapeRecognitionEngine { get; set; }
|
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
|
public class RandSettings
|
||||||
|
|||||||
Reference in New Issue
Block a user