add:xml格式墨迹保存

This commit is contained in:
2025-12-28 00:08:08 +08:00
parent 88dd53302f
commit 3313a0a182
5 changed files with 500 additions and 16 deletions
+10
View File
@@ -3458,6 +3458,16 @@
</ui:SimpleStackPanel>
<TextBlock Text="# 开启后自动保存和手动保存墨迹时将以全屏模式保存。如果存在多个画布和墨迹,将把所有页面的墨迹按照每页为单位保存进一个压缩包中(注意,白板的墨迹只能在白板模式下打开,PPT的墨迹只能在PPT放映模式下打开)" TextWrapping="Wrap" Foreground="#a1a1aa" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Foreground="#fafafa" Text="保存为XML格式" VerticalAlignment="Center"
FontSize="14" Margin="0,0,16,0" />
<ui:ToggleSwitch OnContent="" OffContent=""
Name="ToggleSwitchSaveStrokesAsXML" IsOn="False"
FontFamily="Microsoft YaHei UI" FontWeight="Bold"
Toggled="ToggleSwitchSaveStrokesAsXML_Toggled" />
</ui:SimpleStackPanel>
<TextBlock Text="# 开启后保存墨迹时将使用XML格式(ISF格式),便于查看和编辑墨迹数据" TextWrapping="Wrap" Foreground="#a1a1aa" />
<Line HorizontalAlignment="Center" X1="0" Y1="0" X2="400" Y2="0" Stroke="#3f3f46"
StrokeThickness="1" Margin="0,4,0,4" />
<ui:SimpleStackPanel Orientation="Horizontal" HorizontalAlignment="Left">
+478 -16
View File
@@ -6,6 +6,7 @@ using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
@@ -15,6 +16,8 @@ using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Xml;
using System.Xml.Linq;
using Color = System.Drawing.Color;
using File = System.IO.File;
using Image = System.Windows.Controls.Image;
@@ -127,12 +130,86 @@ namespace Ink_Canvas
SaveSinglePageStrokesAsImage(savePathWithName, newNotice);
}
}
else if (Settings.Automation.IsSaveStrokesAsXML)
{
// XML保存模式 - 检查是否存在多页面墨迹
bool hasMultiplePages = false;
List<StrokeCollection> allPageStrokes = new List<StrokeCollection>();
// 检查PPT放映模式下的多页面墨迹
if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible && _pptManager?.IsConnected == true)
{
hasMultiplePages = true;
var totalSlides = _pptManager.SlidesCount;
var currentSlide = _pptManager.GetCurrentSlideNumber();
for (int i = 1; i <= totalSlides; i++)
{
var slideStrokes = _singlePPTInkManager?.LoadSlideStrokes(i);
if (slideStrokes != null && slideStrokes.Count > 0)
{
allPageStrokes.Add(slideStrokes);
}
else if (i == currentSlide && inkCanvas.Strokes.Count > 0)
{
allPageStrokes.Add(inkCanvas.Strokes.Clone());
}
else
{
allPageStrokes.Add(new StrokeCollection());
}
}
}
// 检查白板模式下的多页面墨迹
else if (currentMode != 0 && WhiteboardTotalCount > 1)
{
hasMultiplePages = true;
for (int i = 1; i <= WhiteboardTotalCount; i++)
{
if (TimeMachineHistories[i] != null)
{
var strokes = ApplyHistoriesToNewStrokeCollection(TimeMachineHistories[i]);
allPageStrokes.Add(strokes);
}
else
{
allPageStrokes.Add(new StrokeCollection());
}
}
}
if (hasMultiplePages && allPageStrokes.Count > 0)
{
// 多页面XML保存为压缩包
string zipFileName = Path.ChangeExtension(savePathWithName, "zip");
SaveMultiPageStrokesAsXMLZip(allPageStrokes, zipFileName, newNotice);
}
else
{
// 单页面XML保存
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
if (newNotice) ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
}
}
else
{
// 常规保存模式 - 仅保存墨迹对象
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
if (Settings.Automation.IsSaveStrokesAsXML)
{
// 保存为XML格式
string xmlPath = Path.ChangeExtension(savePathWithName, ".xml");
SaveStrokesAsXML(inkCanvas.Strokes, xmlPath);
if (newNotice) ShowNotification("墨迹成功保存为XML格式至 " + xmlPath);
}
else
{
// 保存为二进制格式
var fs = new FileStream(savePathWithName, FileMode.Create);
inkCanvas.Strokes.Save(fs);
fs.Close();
if (newNotice) ShowNotification("墨迹成功保存至 " + savePathWithName);
}
_ = Task.Run(async () =>
{
try
@@ -168,8 +245,7 @@ namespace Ink_Canvas
});
}
}
File.WriteAllText(Path.ChangeExtension(savePathWithName, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Formatting.Indented));
if (newNotice) ShowNotification("墨迹成功保存至 " + savePathWithName);
File.WriteAllText(Path.ChangeExtension(savePathWithName, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented));
}
}
catch (Exception ex)
@@ -179,6 +255,201 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 将StrokeCollection保存为XML格式
/// </summary>
private void SaveStrokesAsXML(StrokeCollection strokes, string xmlPath)
{
try
{
// 使用XDocument创建XML文档
XDocument doc = new XDocument(
new XDeclaration("1.0", "utf-8", "yes"),
new XElement("InkCanvasStrokes",
new XAttribute("Version", "1.0"),
new XAttribute("StrokeCount", strokes.Count),
new XAttribute("SaveTime", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")),
from stroke in strokes
select new XElement("Stroke",
new XAttribute("DrawingAttributes", SerializeDrawingAttributes(stroke.DrawingAttributes)),
new XElement("StylusPoints",
from point in stroke.StylusPoints
select new XElement("StylusPoint",
new XAttribute("X", point.X),
new XAttribute("Y", point.Y),
new XAttribute("PressureFactor", point.PressureFactor)
)
)
)
)
);
// 保存XML文件
using (var writer = new XmlTextWriter(xmlPath, Encoding.UTF8))
{
writer.Formatting = System.Xml.Formatting.Indented;
doc.Save(writer);
}
// 同时保存元素信息
var elementInfos = new List<CanvasElementInfo>();
foreach (var child in inkCanvas.Children)
{
if (child is Image img && img.Source is BitmapImage bmp)
{
elementInfos.Add(new CanvasElementInfo
{
Type = "Image",
SourcePath = bmp.UriSource?.LocalPath ?? "",
Left = InkCanvas.GetLeft(img),
Top = InkCanvas.GetTop(img),
Width = img.Width,
Height = img.Height,
Stretch = img.Stretch.ToString()
});
}
}
File.WriteAllText(Path.ChangeExtension(xmlPath, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented));
// 异步上传到Dlass
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(xmlPath);
}
catch (Exception)
{
}
});
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存XML格式墨迹失败: {ex}", LogHelper.LogType.Error);
throw;
}
}
/// <summary>
/// 序列化DrawingAttributes为字符串
/// </summary>
private string SerializeDrawingAttributes(DrawingAttributes da)
{
var sb = new StringBuilder();
sb.Append($"Color={da.Color};");
sb.Append($"Width={da.Width};");
sb.Append($"Height={da.Height};");
sb.Append($"FitToCurve={da.FitToCurve};");
sb.Append($"IsHighlighter={da.IsHighlighter};");
sb.Append($"IgnorePressure={da.IgnorePressure};");
sb.Append($"StylusTip={da.StylusTip};");
return sb.ToString();
}
/// <summary>
/// 将多页面墨迹保存为XML格式压缩包
/// </summary>
private void SaveMultiPageStrokesAsXMLZip(List<StrokeCollection> allPageStrokes, string zipFileName, bool newNotice)
{
try
{
// 创建临时目录来存放文件
string tempDir = Path.Combine(Path.GetTempPath(), $"InkCanvas_MultiPage_XML_{DateTime.Now:yyyyMMdd_HHmmss}");
Directory.CreateDirectory(tempDir);
try
{
// 保存所有页面的XML文件到临时目录
for (int i = 0; i < allPageStrokes.Count; i++)
{
var strokes = allPageStrokes[i];
if (strokes.Count > 0)
{
// 保存XML文件
string xmlFileName = Path.Combine(tempDir, $"page_{i + 1:D4}.xml");
SaveStrokesAsXML(strokes, xmlFileName);
}
}
// 保存元数据信息
string metadataFile = Path.Combine(tempDir, "metadata.txt");
using (var writer = new StreamWriter(metadataFile, false, Encoding.UTF8))
{
writer.WriteLine($"保存时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
writer.WriteLine($"总页数: {allPageStrokes.Count}");
writer.WriteLine($"模式: {(currentMode == 0 ? "PPT放映" : "")}");
writer.WriteLine($"格式: XML");
if (currentMode != 0)
{
writer.WriteLine($"当前页面: {CurrentWhiteboardIndex}");
writer.WriteLine($"总页面数: {WhiteboardTotalCount}");
}
else if (pptApplication != null)
{
writer.WriteLine($"PPT名称: {pptApplication.SlideShowWindows[1].Presentation.Name}");
writer.WriteLine($"PPT总页数: {pptApplication.SlideShowWindows[1].Presentation.Slides.Count}");
writer.WriteLine($"PPT文件路径: {pptApplication.SlideShowWindows[1].Presentation.FullName}");
}
for (int i = 0; i < allPageStrokes.Count; i++)
{
writer.WriteLine($"页面 {i + 1}: {allPageStrokes[i].Count} 条墨迹");
}
}
// 创建ZIP文件
if (File.Exists(zipFileName))
File.Delete(zipFileName);
ZipFile.CreateFromDirectory(tempDir, zipFileName);
// 异步上传ZIP文件到Dlass
_ = Task.Run(async () =>
{
try
{
var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0;
if (delayMinutes > 0)
{
await Task.Delay(TimeSpan.FromMinutes(delayMinutes));
}
await Helpers.DlassNoteUploader.UploadNoteFileAsync(zipFileName);
}
catch (Exception)
{
}
});
if (newNotice) ShowNotification($"多页面XML墨迹成功保存至压缩包 {zipFileName}");
}
finally
{
// 清理临时目录
try
{
if (Directory.Exists(tempDir))
Directory.Delete(tempDir, true);
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"清理临时目录失败: {ex}", LogHelper.LogType.Warning);
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"保存多页面XML墨迹压缩包失败: {ex}", LogHelper.LogType.Error);
throw;
}
}
/// <summary>
/// 将多页面墨迹保存为压缩包
/// </summary>
@@ -411,7 +682,7 @@ namespace Ink_Canvas
var openFileDialog = new OpenFileDialog();
openFileDialog.InitialDirectory = Settings.Automation.AutoSavedStrokesLocation;
openFileDialog.Title = "打开墨迹文件";
openFileDialog.Filter = "Ink Canvas Strokes File (*.icstk)|*.icstk|ICC压缩包 (*.zip)|*.zip";
openFileDialog.Filter = "Ink Canvas Strokes File (*.icstk)|*.icstk|XML墨迹文件 (*.xml)|*.xml|ICC压缩包 (*.zip)|*.zip|所有支持的文件 (*.icstk;*.xml;*.zip)|*.icstk;*.xml;*.zip";
if (openFileDialog.ShowDialog() != true) return;
LogHelper.WriteLogToFile($"Strokes Insert: Name: {openFileDialog.FileName}",
LogHelper.LogType.Event);
@@ -422,12 +693,17 @@ namespace Ink_Canvas
if (fileExtension == ".zip")
{
// 处理ICC压缩包
// 处理ICC压缩包(可能包含XML格式)
OpenICCZipFile(openFileDialog.FileName);
}
else if (fileExtension == ".xml")
{
// 处理XML格式墨迹文件
OpenXMLStrokeFile(openFileDialog.FileName);
}
else
{
// 处理单个墨迹文件
// 处理单个墨迹文件(二进制格式)
OpenSingleStrokeFile(openFileDialog.FileName);
}
@@ -583,21 +859,39 @@ namespace Ink_Canvas
// 重置PPT墨迹存储
_singlePPTInkManager?.ClearAllStrokes();
// 读取所有页面的墨迹文件
var files = Directory.GetFiles(tempDir, "page_*.icstk");
foreach (var file in files)
// 读取所有页面的墨迹文件(支持.icstk和.xml格式)
var icstkFiles = Directory.GetFiles(tempDir, "page_*.icstk");
var xmlFiles = Directory.GetFiles(tempDir, "page_*.xml");
var allFiles = new List<string>();
allFiles.AddRange(icstkFiles);
allFiles.AddRange(xmlFiles);
foreach (var file in allFiles)
{
var fileName = Path.GetFileNameWithoutExtension(file);
if (fileName.StartsWith("page_") && int.TryParse(fileName.Substring(5), out int pageNumber))
{
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read))
StrokeCollection strokes = null;
string extension = Path.GetExtension(file).ToLower();
if (extension == ".xml")
{
var strokes = new StrokeCollection(fs);
if (strokes.Count > 0)
// 从XML文件加载
strokes = LoadStrokesFromXML(file);
}
else
{
// 从二进制文件加载
using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read))
{
_singlePPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes);
strokes = new StrokeCollection(fs);
}
}
if (strokes != null && strokes.Count > 0)
{
_singlePPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes);
}
}
}
@@ -612,7 +906,7 @@ namespace Ink_Canvas
}
}
LogHelper.WriteLogToFile($"成功恢复PPT墨迹,共{files.Length}页");
LogHelper.WriteLogToFile($"成功恢复PPT墨迹,共{allFiles.Count}页");
}
catch (Exception ex)
{
@@ -693,6 +987,173 @@ namespace Ink_Canvas
}
}
/// <summary>
/// 打开XML格式的墨迹文件
/// </summary>
public void OpenXMLStrokeFile(string filePath)
{
try
{
XDocument doc = XDocument.Load(filePath);
var root = doc.Root;
if (root == null || root.Name != "InkCanvasStrokes")
{
throw new Exception("无效的XML墨迹文件格式");
}
var strokes = new StrokeCollection();
foreach (var strokeElement in root.Elements("Stroke"))
{
var drawingAttributesStr = strokeElement.Attribute("DrawingAttributes")?.Value ?? "";
var da = ParseDrawingAttributes(drawingAttributesStr);
var stylusPoints = new StylusPointCollection();
var stylusPointsElement = strokeElement.Element("StylusPoints");
if (stylusPointsElement != null)
{
foreach (var pointElement in stylusPointsElement.Elements("StylusPoint"))
{
double x = double.Parse(pointElement.Attribute("X")?.Value ?? "0");
double y = double.Parse(pointElement.Attribute("Y")?.Value ?? "0");
float pressure = float.Parse(pointElement.Attribute("PressureFactor")?.Value ?? "0.5");
stylusPoints.Add(new StylusPoint(x, y, pressure));
}
}
if (stylusPoints.Count > 0)
{
var stroke = new Stroke(stylusPoints) { DrawingAttributes = da };
strokes.Add(stroke);
}
}
ClearStrokes(true);
timeMachine.ClearStrokeHistory();
inkCanvas.Strokes.Add(strokes);
LogHelper.NewLog($"XML Strokes Insert: Strokes Count: {inkCanvas.Strokes.Count}");
// 恢复元素信息
var elementsFile = Path.ChangeExtension(filePath, ".elements.json");
if (File.Exists(elementsFile))
{
var elementInfos = JsonConvert.DeserializeObject<List<CanvasElementInfo>>(File.ReadAllText(elementsFile));
foreach (var info in elementInfos)
{
if (info.Type == "Image" && File.Exists(info.SourcePath))
{
var img = new Image
{
Source = new BitmapImage(new Uri(info.SourcePath)),
Width = info.Width,
Height = info.Height,
Stretch = Enum.TryParse<Stretch>(info.Stretch, out var stretch) ? stretch : Stretch.Fill
};
InkCanvas.SetLeft(img, info.Left);
InkCanvas.SetTop(img, info.Top);
inkCanvas.Children.Add(img);
}
}
}
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"打开XML墨迹文件失败: {ex}", LogHelper.LogType.Error);
throw;
}
}
/// <summary>
/// 从XML文件加载StrokeCollection(辅助方法,用于ZIP文件恢复)
/// </summary>
private StrokeCollection LoadStrokesFromXML(string xmlPath)
{
try
{
XDocument doc = XDocument.Load(xmlPath);
var root = doc.Root;
if (root == null || root.Name != "InkCanvasStrokes")
{
return new StrokeCollection();
}
var strokes = new StrokeCollection();
foreach (var strokeElement in root.Elements("Stroke"))
{
var drawingAttributesStr = strokeElement.Attribute("DrawingAttributes")?.Value ?? "";
var da = ParseDrawingAttributes(drawingAttributesStr);
var stylusPoints = new StylusPointCollection();
var stylusPointsElement = strokeElement.Element("StylusPoints");
if (stylusPointsElement != null)
{
foreach (var pointElement in stylusPointsElement.Elements("StylusPoint"))
{
double x = double.Parse(pointElement.Attribute("X")?.Value ?? "0");
double y = double.Parse(pointElement.Attribute("Y")?.Value ?? "0");
float pressure = float.Parse(pointElement.Attribute("PressureFactor")?.Value ?? "0.5");
stylusPoints.Add(new StylusPoint(x, y, pressure));
}
}
if (stylusPoints.Count > 0)
{
var stroke = new Stroke(stylusPoints) { DrawingAttributes = da };
strokes.Add(stroke);
}
}
return strokes;
}
catch (Exception ex)
{
LogHelper.WriteLogToFile($"从XML加载墨迹失败: {ex}", LogHelper.LogType.Error);
return new StrokeCollection();
}
}
/// <summary>
/// 从字符串解析DrawingAttributes
/// </summary>
private DrawingAttributes ParseDrawingAttributes(string attributesStr)
{
var da = new DrawingAttributes();
var parts = attributesStr.Split(';');
foreach (var part in parts)
{
var kv = part.Split('=');
if (kv.Length == 2)
{
var key = kv[0].Trim();
var value = kv[1].Trim();
switch (key)
{
case "Color":
da.Color = (System.Windows.Media.Color)System.Windows.Media.ColorConverter.ConvertFromString(value);
break;
case "Width":
da.Width = double.Parse(value);
break;
case "Height":
da.Height = double.Parse(value);
break;
case "FitToCurve":
da.FitToCurve = bool.Parse(value);
break;
case "IsHighlighter":
da.IsHighlighter = bool.Parse(value);
break;
case "IgnorePressure":
da.IgnorePressure = bool.Parse(value);
break;
case "StylusTip":
da.StylusTip = Enum.TryParse<StylusTip>(value, out var tip) ? tip : StylusTip.Ellipse;
break;
}
}
}
return da;
}
/// <summary>
/// 打开单个墨迹文件
/// </summary>
@@ -748,3 +1209,4 @@ namespace Ink_Canvas
}
}
}
+7
View File
@@ -2349,6 +2349,13 @@ namespace Ink_Canvas
SaveSettingsToFile();
}
private void ToggleSwitchSaveStrokesAsXML_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
Settings.Automation.IsSaveStrokesAsXML = ToggleSwitchSaveStrokesAsXML.IsOn;
SaveSettingsToFile();
}
private void ToggleSwitchEnableAutoSaveStrokes_Toggled(object sender, RoutedEventArgs e)
{
if (!isLoaded) return;
@@ -1114,6 +1114,8 @@ namespace Ink_Canvas
ToggleSwitchSaveFullPageStrokes.IsOn = Settings.Automation.IsSaveFullPageStrokes;
ToggleSwitchSaveStrokesAsXML.IsOn = Settings.Automation.IsSaveStrokesAsXML;
// 加载定时保存墨迹设置
ToggleSwitchEnableAutoSaveStrokes.IsOn = Settings.Automation.IsEnableAutoSaveStrokes;
// 初始化保存间隔下拉框
+3
View File
@@ -481,6 +481,9 @@ namespace Ink_Canvas
[JsonProperty("isSaveFullPageStrokes")]
public bool IsSaveFullPageStrokes;
[JsonProperty("isSaveStrokesAsXML")]
public bool IsSaveStrokesAsXML { get; set; } = false;
[JsonProperty("isAutoEnterAnnotationAfterKillHite")]
public bool IsAutoEnterAnnotationAfterKillHite { get; set; }