using Ink_Canvas.Controls; using Ink_Canvas.Helpers; using Newtonsoft.Json; using System; using System.Collections.Generic; 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; using System.Windows.Controls; using System.Windows.Forms; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; using System.Xml; using System.Xml.Linq; using Color = System.Drawing.Color; using File = System.IO.File; using Image = System.Windows.Controls.Image; using OpenFileDialog = Microsoft.Win32.OpenFileDialog; namespace Ink_Canvas { // 1. 定义元素信息结构 public class CanvasElementInfo { public string Type { get; set; } // "Image" | "Pdf" public string SourcePath { get; set; } public double Left { get; set; } public double Top { get; set; } public double Width { get; set; } public double Height { get; set; } public string Stretch { get; set; } = "Fill"; // 默认为Fill /// PDF 当前页(从 0 开始),仅 Type == Pdf 时有效。 public int? PdfCurrentPage { get; set; } /// 保存时的 PDF 总页数,用于校验;仅 Type == Pdf 时有效。 public int? PdfPageCount { get; set; } } public partial class MainWindow : Window { /// 收集画布上图片与 PDF 的元数据,写入 .elements.json(与墨迹文件同路径)。 private void CollectCanvasElementsMetadata(List elementInfos) { if (elementInfos == null || inkCanvas == null) return; 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() }); } else if (child is PdfEmbeddedView pdf && !string.IsNullOrEmpty(pdf.PdfPath)) { elementInfos.Add(new CanvasElementInfo { Type = "Pdf", SourcePath = pdf.PdfPath, Left = InkCanvas.GetLeft(pdf), Top = InkCanvas.GetTop(pdf), Width = pdf.Width, Height = pdf.Height, Stretch = "Uniform", PdfCurrentPage = (int)pdf.CurrentPageIndex, PdfPageCount = (int)pdf.PageCount }); } } } /// /// 保存墨迹的鼠标释放事件处理 /// /// 发送者 /// 鼠标按钮事件参数 /// /// 该方法会: /// 1. 检查是否是当前按下的对象,且墨迹画布是否可见 /// 2. 隐藏工具面板 /// 3. 隐藏通知面板 /// 4. 调用SaveInkCanvasStrokes方法保存墨迹 /// private void SymbolIconSaveStrokes_MouseUp(object sender, MouseButtonEventArgs e) { if (lastBorderMouseDownObject != sender || inkCanvas.Visibility != Visibility.Visible) return; AnimationsHelper.HideWithSlideAndFade(BorderTools); AnimationsHelper.HideWithSlideAndFade(BoardBorderTools); GridNotifications.Visibility = Visibility.Collapsed; SaveInkCanvasStrokes(true, true); } /// /// 保存墨迹画布的墨迹 /// /// 是否显示新的通知 /// 是否是用户手动保存 /// /// 该方法会: /// 1. 根据保存类型和模式确定保存路径 /// 2. 创建保存目录 /// 3. 根据当前模式生成保存文件名 /// 4. 根据设置选择保存模式: /// - 全页面保存模式:保存为图像或压缩包 /// - XML保存模式:保存为XML文件或压缩包 /// - 常规保存模式:保存为二进制格式或XML格式 /// 5. 异步上传保存的文件到Dlass /// 6. 保存元素信息 /// private void SaveInkCanvasStrokes(bool newNotice = true, bool saveByUser = false) { try { var savePath = Settings.Automation.AutoSavedStrokesLocation + (saveByUser ? @"\User Saved - " : @"\Auto Saved - ") + (currentMode == 0 ? "Annotation Strokes" : "BlackBoard Strokes"); if (!Directory.Exists(savePath)) Directory.CreateDirectory(savePath); string savePathWithName; if (currentMode != 0) // 黑板模式下 savePathWithName = savePath + @"\" + DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss-fff") + " Page-" + CurrentWhiteboardIndex + " StrokesCount-" + inkCanvas.Strokes.Count + ".icstk"; else //savePathWithName = savePath + @"\" + DateTime.Now.ToString("u").Replace(':', '-') + ".icstk"; savePathWithName = savePath + @"\" + DateTime.Now.ToString("yyyy-MM-dd HH-mm-ss-fff") + ".icstk"; 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) { // 检查是否是PPT模式 bool isPPTMode = BtnPPTSlideShowEnd.Visibility == Visibility.Visible && _pptManager?.IsConnected == true; if (isPPTMode) { // PPT模式:保存为多个XML文件 string basePath = Path.GetDirectoryName(savePathWithName); string baseFileName = Path.GetFileNameWithoutExtension(savePathWithName); int savedCount = 0; for (int i = 0; i < allPageStrokes.Count; i++) { var strokes = allPageStrokes[i]; if (strokes.Count > 0) { string pageFileName = Path.Combine(basePath, $"{baseFileName}_Page-{i + 1}.xml"); SaveStrokesAsXML(strokes, pageFileName, false); savedCount++; // 异步上传每个XML文件 _ = Task.Run(async () => { try { await Helpers.UploadHelper.UploadFileAsync(pageFileName); } catch (Exception) { } }); } } if (newNotice) { Task.Delay(100).ContinueWith(t => { Dispatcher.Invoke(() => { ShowNotification($"多页面XML墨迹成功保存为 {savedCount} 个XML文件"); }); }); } } else { // 非PPT模式:保存为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) { Task.Delay(100).ContinueWith(t => { Dispatcher.Invoke(() => { ShowNotification("墨迹成功保存为XML格式至 " + xmlPath); }); }); } } } else if (Settings.Automation.IsSaveFullPageStrokes) { // 全页面保存模式 - 检查是否存在多页面墨迹 bool hasMultiplePages = false; List allPageStrokes = new List(); // 检查PPT放映模式下的多页面墨迹 if (BtnPPTSlideShowEnd.Visibility == Visibility.Visible && _pptManager?.IsConnected == true) { hasMultiplePages = true; // 收集PPT放映模式下的所有页面墨迹 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) { // 多页面墨迹保存为压缩包 string zipFileName = Path.ChangeExtension(savePathWithName, "zip"); SaveMultiPageStrokesAsZip(allPageStrokes, zipFileName, newNotice); } else { // 单页面墨迹保存为图像 SaveSinglePageStrokesAsImage(savePathWithName, newNotice); } } else { // 常规保存模式 - 检查是否存在多页面墨迹 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) { // 多页面保存为多个icstk文件 string basePath = Path.GetDirectoryName(savePathWithName); string baseFileName = Path.GetFileNameWithoutExtension(savePathWithName); for (int i = 0; i < allPageStrokes.Count; i++) { var strokes = allPageStrokes[i]; if (strokes.Count > 0) { string pageFileName = Path.Combine(basePath, $"{baseFileName}_Page-{i + 1}.icstk"); using (var fs = new FileStream(pageFileName, FileMode.Create)) { strokes.Save(fs); } // 异步上传每个icstk文件 _ = Task.Run(async () => { try { await Helpers.UploadHelper.UploadFileAsync(pageFileName); } catch (Exception) { } }); } } if (newNotice) { Task.Delay(100).ContinueWith(t => { Dispatcher.Invoke(() => { ShowNotification($"多页面墨迹成功保存为 {allPageStrokes.Count} 个icstk文件"); }); }); } } else { // 单页面保存 if (Settings.Automation.IsSaveStrokesAsXML) { // 保存为XML格式 string xmlPath = Path.ChangeExtension(savePathWithName, ".xml"); SaveStrokesAsXML(inkCanvas.Strokes, xmlPath); if (newNotice) { Task.Delay(100).ContinueWith(t => { Dispatcher.Invoke(() => { ShowNotification("墨迹成功保存为XML格式至 " + xmlPath); }); }); } } else { // 保存为二进制格式 var fs = new FileStream(savePathWithName, FileMode.Create); inkCanvas.Strokes.Save(fs); fs.Close(); if (newNotice) { Task.Delay(100).ContinueWith(t => { Dispatcher.Invoke(() => { ShowNotification("墨迹成功保存至 " + savePathWithName); }); }); } } // 异步上传文件 _ = Task.Run(async () => { try { string uploadPath = Settings.Automation.IsSaveStrokesAsXML ? Path.ChangeExtension(savePathWithName, ".xml") : savePathWithName; await Helpers.UploadHelper.UploadFileAsync(uploadPath); } catch (Exception) { } }); // 保存元素信息 var elementInfos = new List(); CollectCanvasElementsMetadata(elementInfos); string elementsPath = Settings.Automation.IsSaveStrokesAsXML ? Path.ChangeExtension(savePathWithName, ".elements.json") : Path.ChangeExtension(savePathWithName, ".elements.json"); File.WriteAllText(elementsPath, JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented)); } } } catch (Exception ex) { ShowNotification("墨迹保存失败"); LogHelper.WriteLogToFile("墨迹保存失败 | " + ex, LogHelper.LogType.Error); } } /// /// 将StrokeCollection保存为XML格式 /// private void SaveStrokesAsXML(StrokeCollection strokes, string xmlPath, bool triggerUpload = true) { 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(); CollectCanvasElementsMetadata(elementInfos); File.WriteAllText(Path.ChangeExtension(xmlPath, ".elements.json"), JsonConvert.SerializeObject(elementInfos, Newtonsoft.Json.Formatting.Indented)); // 异步上传到Dlass if (triggerUpload) { _ = Task.Run(async () => { try { await Helpers.UploadHelper.UploadFileAsync(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, false); } } // 保存元数据信息 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 { await Helpers.UploadHelper.UploadFileAsync(zipFileName); } catch (Exception) { } }); if (newNotice) { Task.Delay(100).ContinueWith(t => { Dispatcher.Invoke(() => { 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; } } /// /// 将多页面墨迹保存为压缩包 /// private void SaveMultiPageStrokesAsZip(List allPageStrokes, string zipFileName, bool newNotice) { try { // 创建临时目录来存放文件 string tempDir = Path.Combine(Path.GetTempPath(), $"InkCanvas_MultiPage_{DateTime.Now:yyyyMMdd_HHmmss}"); Directory.CreateDirectory(tempDir); try { // 保存所有页面的文件到临时目录 for (int i = 0; i < allPageStrokes.Count; i++) { var strokes = allPageStrokes[i]; if (strokes.Count > 0) { // 保存墨迹文件 string strokeFileName = Path.Combine(tempDir, $"page_{i + 1:D4}.icstk"); using (var fs = new FileStream(strokeFileName, FileMode.Create)) { strokes.Save(fs); } // 保存页面图像 string imageFileName = Path.Combine(tempDir, $"page_{i + 1:D4}.png"); using (var fs = new FileStream(imageFileName, FileMode.Create)) { SavePageAsImage(strokes, fs); } } } // 保存元数据信息 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放映" : "白板")}"); 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} 条墨迹"); } } // 使用.NET Framework内置的压缩功能创建ZIP文件 if (File.Exists(zipFileName)) File.Delete(zipFileName); // 使用System.IO.Compression.FileSystem来创建ZIP ZipFile.CreateFromDirectory(tempDir, zipFileName); // 异步上传ZIP文件到Dlass _ = Task.Run(async () => { try { await Helpers.UploadHelper.UploadFileAsync(zipFileName); } catch (Exception) { } }); if (newNotice) { Task.Delay(100).ContinueWith(t => { Dispatcher.Invoke(() => { ShowNotification($"多页面墨迹成功保存至压缩包 {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($"保存多页面墨迹压缩包失败: {ex}", LogHelper.LogType.Error); throw; } } /// /// 将单页面墨迹保存为图像 /// private void SaveSinglePageStrokesAsImage(string savePathWithName, bool newNotice) { // 全页面保存模式 - 保存整个墨迹页面的图像 var bitmap = new Bitmap( Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height); using (var g = Graphics.FromImage(bitmap)) { // 创建黑色或透明背景 Color bgColor = Settings.Canvas.UsingWhiteboard ? Color.White : Color.FromArgb(22, 41, 36); // 黑板背景色 g.Clear(bgColor); // 将InkCanvas墨迹渲染到Visual var visual = new DrawingVisual(); using (var dc = visual.RenderOpen()) { // 创建一个VisualBrush,使用inkCanvas作为源 var visualBrush = new VisualBrush(inkCanvas); // 绘制矩形并填充为inkCanvas的内容 dc.DrawRectangle(visualBrush, null, new Rect(0, 0, inkCanvas.ActualWidth, inkCanvas.ActualHeight)); } // 创建适合墨迹画布尺寸的渲染位图 var rtb = new RenderTargetBitmap( (int)inkCanvas.ActualWidth, (int)inkCanvas.ActualHeight, 96, 96, PixelFormats.Pbgra32); rtb.Render(visual); // 转换为GDI+ Bitmap并保存 var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(rtb)); using (var ms = new MemoryStream()) { encoder.Save(ms); ms.Seek(0, SeekOrigin.Begin); var imgBitmap = new Bitmap(ms); // 将生成的墨迹图像绘制到屏幕截图上 // 居中绘制,确保墨迹位于屏幕中央 int x = (bitmap.Width - imgBitmap.Width) / 2; int y = (bitmap.Height - imgBitmap.Height) / 2; g.DrawImage(imgBitmap, x, y); // 保存为PNG string imagePathWithName = Path.ChangeExtension(savePathWithName, "png"); bitmap.Save(imagePathWithName, ImageFormat.Png); // 仍然保存墨迹文件以兼容旧版本 var fs = new FileStream(savePathWithName, FileMode.Create); inkCanvas.Strokes.Save(fs); fs.Close(); _ = Task.Run(async () => { try { await Helpers.UploadHelper.UploadFileAsync(imagePathWithName); } catch (Exception) { } }); } } // 显示提示 if (newNotice) { Task.Delay(100).ContinueWith(t => { Dispatcher.Invoke(() => { ShowNotification("墨迹成功全页面保存至 " + Path.ChangeExtension(savePathWithName, "png")); }); }); } } /// /// 将指定墨迹集合保存为图像到指定流 /// private void SavePageAsImage(StrokeCollection strokes, Stream outputStream) { try { // 创建临时InkCanvas来渲染墨迹 var tempCanvas = new InkCanvas(); tempCanvas.Strokes = strokes; tempCanvas.Width = inkCanvas.ActualWidth; tempCanvas.Height = inkCanvas.ActualHeight; // 创建渲染位图 var rtb = new RenderTargetBitmap( (int)tempCanvas.Width, (int)tempCanvas.Height, 96, 96, PixelFormats.Pbgra32); rtb.Render(tempCanvas); // 保存为PNG var encoder = new PngBitmapEncoder(); encoder.Frames.Add(BitmapFrame.Create(rtb)); encoder.Save(outputStream); } catch (Exception ex) { LogHelper.WriteLogToFile($"保存页面图像失败: {ex}", LogHelper.LogType.Error); throw; } } /// /// 打开墨迹文件的鼠标释放事件处理 /// /// 发送者 /// 鼠标按钮事件参数 /// /// 该方法会: /// 1. 检查是否是当前按下的对象 /// 2. 隐藏工具面板 /// 3. 打开文件选择对话框 /// 4. 根据文件扩展名选择不同的打开方式: /// - .zip:处理ICC压缩包 /// - .xml:处理XML格式墨迹文件 /// - 其他:处理单个墨迹文件(二进制格式) /// 5. 如果墨迹画布不可见,切换到鼠标模式 /// private void SymbolIconOpenStrokes_MouseUp(object sender, MouseButtonEventArgs e) { if (lastBorderMouseDownObject != sender) return; AnimationsHelper.HideWithSlideAndFade(BorderTools); AnimationsHelper.HideWithSlideAndFade(BoardBorderTools); var openFileDialog = new OpenFileDialog(); openFileDialog.InitialDirectory = Settings.Automation.AutoSavedStrokesLocation; openFileDialog.Title = "打开墨迹文件"; 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); try { string fileExtension = Path.GetExtension(openFileDialog.FileName).ToLower(); if (fileExtension == ".zip") { // 处理ICC压缩包(可能包含XML格式) OpenICCZipFile(openFileDialog.FileName); } else if (fileExtension == ".xml") { // 处理XML格式墨迹文件 OpenXMLStrokeFile(openFileDialog.FileName); } else { // 处理单个墨迹文件(二进制格式) OpenSingleStrokeFile(openFileDialog.FileName); } if (inkCanvas.Visibility != Visibility.Visible) SymbolIconCursor_Click(sender, null); } catch (Exception ex) { ShowNotification("墨迹打开失败"); LogHelper.WriteLogToFile($"墨迹打开失败: {ex}", LogHelper.LogType.Error); } } /// /// 打开ICC创建的.zip压缩包 /// private void OpenICCZipFile(string zipFilePath) { try { // 创建临时目录来解压文件 string tempDir = Path.Combine(Path.GetTempPath(), $"InkCanvas_Open_{DateTime.Now:yyyyMMdd_HHmmss}"); Directory.CreateDirectory(tempDir); try { // 解压ZIP文件 SafeZipExtractor.ExtractZipSafely(zipFilePath, tempDir, overwrite: true); // 读取元数据文件 string metadataFile = Path.Combine(tempDir, "metadata.txt"); if (!File.Exists(metadataFile)) { throw new Exception("压缩包中未找到元数据文件"); } var metadata = ReadMetadataFile(metadataFile); // 根据元数据信息决定恢复模式 bool isPPTMode = metadata.ContainsKey("模式") && metadata["模式"].Contains("PPT放映"); bool isWhiteboardMode = metadata.ContainsKey("模式") && metadata["模式"].Contains("白板"); // 检查当前是否处于PPT模式 bool isCurrentlyInPPTMode = BtnPPTSlideShowEnd.Visibility == Visibility.Visible && pptApplication != null; // 检查当前是否处于白板模式 bool isCurrentlyInWhiteboardMode = currentMode != 0; // 严格模式隔离:只在对应模式下恢复对应墨迹 if (isPPTMode && isCurrentlyInPPTMode) { // 只在PPT放映模式下恢复PPT墨迹 RestorePPTStrokesFromZip(tempDir, metadata); } else if (isWhiteboardMode && isCurrentlyInWhiteboardMode) { // 只在白板模式下恢复白板墨迹 RestoreWhiteboardStrokesFromZip(tempDir, metadata); } else { // 模式不匹配时,显示提示信息 string savedMode = isPPTMode ? "PPT放映" : (isWhiteboardMode ? "白板" : "未知"); string currentMode = isCurrentlyInPPTMode ? "PPT放映" : (isCurrentlyInWhiteboardMode ? "白板" : "桌面"); ShowNotification($"墨迹保存模式({savedMode})与当前模式({currentMode})不匹配,无法恢复墨迹"); LogHelper.WriteLogToFile($"模式不匹配:保存模式={savedMode},当前模式={currentMode}", LogHelper.LogType.Warning); } ShowNotification($"成功打开ICC压缩包,共{(metadata.ContainsKey("总页数") ? metadata["总页数"] : "0")}页"); } finally { // 清理临时目录 try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch (Exception ex) { LogHelper.WriteLogToFile($"清理临时目录失败: {ex}", LogHelper.LogType.Warning); } } } catch (Exception ex) { LogHelper.WriteLogToFile($"打开ICC压缩包失败: {ex}", LogHelper.LogType.Error); throw; } } /// /// 读取元数据文件 /// private Dictionary ReadMetadataFile(string metadataPath) { var metadata = new Dictionary(); using (var reader = new StreamReader(metadataPath, Encoding.UTF8)) { string line; while ((line = reader.ReadLine()) != null) { if (line.Contains(":")) { var parts = line.Split(new[] { ':' }, 2); if (parts.Length == 2) { metadata[parts[0].Trim()] = parts[1].Trim(); } } } } return metadata; } /// /// 从ZIP文件恢复PPT墨迹 /// private void RestorePPTStrokesFromZip(string tempDir, Dictionary metadata) { try { // 确保当前处于PPT放映模式 if (BtnPPTSlideShowEnd.Visibility != Visibility.Visible || pptApplication == null) { throw new InvalidOperationException("当前不在PPT放映模式,无法恢复PPT墨迹"); } // 检查PPT文件路径是否匹配 if (metadata.ContainsKey("PPT文件路径")) { string savedPptPath = metadata["PPT文件路径"]; string currentPptPath = pptApplication.SlideShowWindows[1].Presentation.FullName; if (!string.IsNullOrEmpty(savedPptPath) && !string.IsNullOrEmpty(currentPptPath)) { // 使用文件路径哈希值进行比较,避免路径格式差异 string savedHash = HashHelper.GetFileHash(savedPptPath); string currentHash = HashHelper.GetFileHash(currentPptPath); if (savedHash != currentHash) { throw new InvalidOperationException($"墨迹文件与当前PPT文件不匹配。保存的PPT: {savedPptPath},当前PPT: {currentPptPath}"); } } } // 清空当前墨迹 ClearStrokes(true); timeMachine.ClearStrokeHistory(); // 重置PPT墨迹存储 _singlePPTInkManager?.ClearAllStrokes(); // 读取所有页面的墨迹文件(支持.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)) { StrokeCollection strokes = null; string extension = Path.GetExtension(file).ToLower(); if (extension == ".xml") { // 从XML文件加载 strokes = LoadStrokesFromXML(file); } else { // 从二进制文件加载 using (var fs = new FileStream(file, FileMode.Open, FileAccess.Read)) { strokes = new StrokeCollection(fs); } } if (strokes != null && strokes.Count > 0) { _singlePPTInkManager?.ForceSaveSlideStrokes(pageNumber, strokes); } } } // 恢复当前页面的墨迹 if (_pptManager?.IsInSlideShow == true) { int currentSlide = _pptManager.GetCurrentSlideNumber(); var currentStrokes = _singlePPTInkManager?.LoadSlideStrokes(currentSlide); if (currentStrokes != null && currentStrokes.Count > 0) { inkCanvas.Strokes.Add(currentStrokes); } } LogHelper.WriteLogToFile($"成功恢复PPT墨迹,共{allFiles.Count}页"); } catch (Exception ex) { LogHelper.WriteLogToFile($"恢复PPT墨迹失败: {ex}", LogHelper.LogType.Error); throw; } } /// /// 从ZIP文件恢复白板墨迹 /// private void RestoreWhiteboardStrokesFromZip(string tempDir, Dictionary metadata) { try { // 确保当前处于白板模式 if (currentMode == 0) { throw new InvalidOperationException("当前不在白板模式,无法恢复白板墨迹"); } // 清空当前墨迹 ClearStrokes(true); timeMachine.ClearStrokeHistory(); // 读取总页数 int totalPages = 1; if (metadata.ContainsKey("总页数") && int.TryParse(metadata["总页数"], out int parsedPages)) { totalPages = parsedPages; } // 重置白板状态 WhiteboardTotalCount = totalPages; CurrentWhiteboardIndex = 1; // 清空历史记录 for (int i = 0; i < TimeMachineHistories.Length; i++) { TimeMachineHistories[i] = null; } // 读取所有页面的墨迹文件 var files = Directory.GetFiles(tempDir, "page_*.icstk"); foreach (var file in files) { 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)) { var strokes = new StrokeCollection(fs); if (strokes.Count > 0) { // 创建历史记录 var history = new TimeMachineHistory(strokes, TimeMachineHistoryType.UserInput, false); TimeMachineHistories[pageNumber] = new[] { history }; } } } } // 恢复第一页的墨迹 if (TimeMachineHistories[1] != null) { RestoreStrokes(); } // 更新UI显示 UpdateIndexInfoDisplay(); LogHelper.WriteLogToFile($"成功恢复白板墨迹,共{totalPages}页"); } catch (Exception ex) { LogHelper.WriteLogToFile($"恢复白板墨迹失败: {ex}", LogHelper.LogType.Error); throw; } } /// /// 打开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); } else if (string.Equals(info.Type, "Pdf", StringComparison.OrdinalIgnoreCase) && File.Exists(info.SourcePath)) { Dispatcher.BeginInvoke(new Action(() => { _ = RestorePdfFromElementInfoAsync(info); }), DispatcherPriority.Loaded); } } } } 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; } /// /// 打开单个墨迹文件 /// /// 墨迹文件的路径 /// /// 该方法会: /// 1. 打开墨迹文件并加载墨迹 /// 2. 检查文件是否包含墨迹 /// 3. 如果包含墨迹,清空当前墨迹并添加新墨迹 /// 4. 恢复元素信息 /// 5. 如果文件流中没有墨迹,尝试从内存流中加载 /// public void OpenSingleStrokeFile(string filePath) { var fileStreamHasNoStroke = false; using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { var strokes = new StrokeCollection(fs); fileStreamHasNoStroke = strokes.Count == 0; if (!fileStreamHasNoStroke) { ClearStrokes(true); timeMachine.ClearStrokeHistory(); inkCanvas.Strokes.Add(strokes); LogHelper.NewLog($"Strokes Insert: Strokes Count: {inkCanvas.Strokes.Count.ToString()}"); } } // 恢复元素信息 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); } else if (string.Equals(info.Type, "Pdf", StringComparison.OrdinalIgnoreCase) && File.Exists(info.SourcePath)) { Dispatcher.BeginInvoke(new Action(() => { _ = RestorePdfFromElementInfoAsync(info); }), DispatcherPriority.Loaded); } } } if (fileStreamHasNoStroke) using (var ms = new MemoryStream(File.ReadAllBytes(filePath))) { ms.Seek(0, SeekOrigin.Begin); var strokes = new StrokeCollection(ms); ClearStrokes(true); timeMachine.ClearStrokeHistory(); inkCanvas.Strokes.Add(strokes); LogHelper.NewLog($"Strokes Insert (2): Strokes Count: {strokes.Count.ToString()}"); } } } }