diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 2eb6247b..0a8e1176 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -3458,6 +3458,16 @@ + + + + + + diff --git a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs index 8d5d5df6..2ef7f379 100644 --- a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs +++ b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs @@ -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 allPageStrokes = new List(); + + // 检查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 } } + /// + /// 将StrokeCollection保存为XML格式 + /// + 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(); + 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; + } + } + + /// + /// 序列化DrawingAttributes为字符串 + /// + 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(); + } + + /// + /// 将多页面墨迹保存为XML格式压缩包 + /// + private void SaveMultiPageStrokesAsXMLZip(List 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; + } + } + /// /// 将多页面墨迹保存为压缩包 /// @@ -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(); + 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 } } + /// + /// 打开XML格式的墨迹文件 + /// + 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>(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(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; + } + } + + /// + /// 从XML文件加载StrokeCollection(辅助方法,用于ZIP文件恢复) + /// + 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(); + } + } + + /// + /// 从字符串解析DrawingAttributes + /// + 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(value, out var tip) ? tip : StylusTip.Ellipse; + break; + } + } + } + return da; + } + /// /// 打开单个墨迹文件 /// @@ -748,3 +1209,4 @@ namespace Ink_Canvas } } } + diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index c807c0b4..a7d9d6b0 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -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; diff --git a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs index 876b7672..b78b7e20 100644 --- a/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs +++ b/Ink Canvas/MainWindow_cs/MW_SettingsToLoad.cs @@ -1114,6 +1114,8 @@ namespace Ink_Canvas ToggleSwitchSaveFullPageStrokes.IsOn = Settings.Automation.IsSaveFullPageStrokes; + ToggleSwitchSaveStrokesAsXML.IsOn = Settings.Automation.IsSaveStrokesAsXML; + // 加载定时保存墨迹设置 ToggleSwitchEnableAutoSaveStrokes.IsOn = Settings.Automation.IsEnableAutoSaveStrokes; // 初始化保存间隔下拉框 diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index 02a2cee4..dc44bae6 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -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; }