using Comal.Classes; using InABox.Clients; using InABox.Core; using InABox.Wpf; using InABox.WPF; using PRSDesktop.Panels.DataEntry; using Syncfusion.Pdf; using Syncfusion.Pdf.Parsing; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using Syncfusion.OCRProcessor; using Syncfusion.Pdf.Graphics; using Color = System.Drawing.Color; using Image = System.Windows.Controls.Image; using Rectangle = System.Windows.Shapes.Rectangle; namespace PRSDesktop; public class DocumentOCRContextMenuArgs(ContextMenu menu, string text) : EventArgs { public ContextMenu Menu { get; private set; } = menu; public string Text { get; private set; } = text; } public delegate void DocumentOCRContextMenuOpeningEvent(object sender, DocumentOCRContextMenuArgs args); /// /// Control that allows to view a list of documents, within a zoom control, and providing methods to rotate/explode data. /// /// /// This is originally from the Data entry panel, and this implementation is a little bit scuffed. Basically, because the /// is not an , there is no good shared interface, so I made this abstract, with a type argument. Then to get the "EntityDocument" /// ID or the Document ID, there are abstract functions. /// /// Note one needs also to provide . This is a function used by the "Rotate Image" button, and its implementation needs /// to update the THumbnail of the Entity Document, and save it, along with refreshing the view list. /// /// public abstract class DocumentViewList : UserControl, INotifyPropertyChanged { public static readonly DependencyProperty CanRotateImageProperty = DependencyProperty.Register(nameof(CanRotateImage), typeof(bool), typeof(DocumentViewList), new PropertyMetadata(true, CanRotateImage_Changed)); private static void CanRotateImage_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is not DocumentViewList list) return; list.DoPropertyChanged(e.Property.Name); } private IList _documents = []; public IList Documents { get => _documents; set { UpdateViewList(value); } } private readonly object _viewListLock = new object(); private class ViewDocument { public ImageSource Image { get; set; } public TDocument Document { get; set; } public int PageNumber { get; set; } public ViewDocument(ImageSource image, TDocument document, int page) { Image = image; Document = document; PageNumber = page; } } private List ViewDocuments { get; } = new(); public ObservableCollection ViewList { get; init; } = new(); private ZoomPanel ZoomPanel; private bool _canExplode; public bool CanExplode { get => _canExplode; set { _canExplode = value; DoPropertyChanged(); } } public event Action? Explode; public event Action? ExplodeAll; public event Action? UpdateDocument; public bool CanRotateImage { get => (bool)GetValue(CanRotateImageProperty); set => SetValue(CanRotateImageProperty, value); } private static OCRProcessor? processor = null; public DocumentViewList() { var tesseractpath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "Tesseract"); if (Directory.Exists(tesseractpath)) { processor ??= new OCRProcessor(tesseractpath); processor.Settings.Language = Languages.English; processor.Settings.PageSegment = PageSegMode.SparseText; var fontfile = Path.Combine(tesseractpath, "ARIALUNI.ttf"); if (File.Exists(fontfile)) { try { using (var fontStream = new FileStream(fontfile, FileMode.Open)) processor.UnicodeFont = new PdfTrueTypeFont(fontStream, 8); } catch (Exception e) { Logger.Send(LogType.Error,"",$"Unable to Load ARIALUNI.TTF: {e.Message}\n{e.StackTrace}"); } } } var border = new Border(); border.BorderBrush = Colors.Gray.ToBrush(); border.Background = Colors.DimGray.ToBrush(); ZoomPanel = new ZoomPanel(); var itemsControl = new ItemsControl(); itemsControl.Margin = new Thickness(10); itemsControl.ItemsSource = ViewList; var factory = new FrameworkElementFactory(typeof(StackPanel)); factory.SetValue(StackPanel.OrientationProperty, Orientation.Vertical); itemsControl.ItemsPanel = new ItemsPanelTemplate(factory); itemsControl.ContextMenu = new ContextMenu(); var explode = itemsControl.ContextMenu.AddItem("Regroup Pages", null, Explode_Click); explode.Bind(VisibilityProperty, this, x => x.CanExplode, new InABox.WPF.BooleanToVisibilityConverter(Visibility.Visible, Visibility.Collapsed)); var explodeAll = itemsControl.ContextMenu.AddItem("Explode All Pages", null, ExplodeAll_Click); explodeAll.Bind(VisibilityProperty, this, x => x.CanExplode, new InABox.WPF.BooleanToVisibilityConverter(Visibility.Visible, Visibility.Collapsed)); var viewImage = new MenuItem() { Header = "View Image" }; viewImage.ToolTip = "Show this image in a separate window."; viewImage.Bind(MenuItem.TagProperty, x => x); viewImage.Click += ViewImage_Click; itemsControl.ContextMenu.Items.Add(viewImage); var rotateImage = new MenuItem() { Header = "Rotate Document" }; rotateImage.ToolTip = "Rotate this document 90° clockwise"; rotateImage.Bind(MenuItem.TagProperty, x => x); rotateImage.SetBinding(MenuItem.IsEnabledProperty, new Binding("CanRotateImage") { Source = this }); rotateImage.Click += RotateImage_Click; itemsControl.ContextMenu.Items.Add(rotateImage); //var ocrImage = new MenuItem() //{ // Header = "OCR Document (Preview)" //}; //ocrImage.ToolTip = "Extract Text from this Image (Preview Only)"; //ocrImage.Bind(MenuItem.TagProperty, x => x); //ocrImage.Click += OCRImage_Click; //itemsControl.ContextMenu.Items.Add(ocrImage); itemsControl.ItemTemplate = TemplateGenerator.CreateDataTemplate(() => { var grid = new Grid(); System.Windows.Point? dragStartPoint = null; var dragObject = new Border() { BorderThickness = new Thickness(0.75), BorderBrush = System.Windows.Media.Brushes.Firebrick, Background = new SolidColorBrush(Colors.Red) { Opacity = 0.2 }, Visibility = Visibility.Collapsed }; ContextMenu dragMenu = new ContextMenu(); dragMenu.Closed += (s, e) => { Logger.Send(LogType.Information,"","DragMenu Closed"); dragStartPoint = null; dragObject.Visibility = Visibility.Collapsed; }; var img = new Image(); img.Bind(Image.SourceProperty, x => x); img.Margin = new(0, 0, 0, 5); img.ContextMenu = itemsControl.ContextMenu; img.PreviewMouseLeftButtonDown += (sender, args) => { if (sender is not Image image) return; if(args.ClickCount >= 2) { OpenImageWindow(image.Source); args.Handled = true; } if (processor == null || dragStartPoint != null || dragMenu.IsOpen) return; Logger.Send(LogType.Information,"",$"Starting Drag - {sender.GetType()}"); dragStartPoint = args.GetPosition(img); Logger.Send(LogType.Information,"",$"Setting DragObject to be visible"); dragObject.Visibility = Visibility.Visible; Logger.Send(LogType.Information,"",$"Set DragObject to be visible"); dragObject.Margin = new Thickness(dragStartPoint.Value.X, dragStartPoint.Value.Y, img.ActualWidth-dragStartPoint.Value.X, img.ActualHeight-dragStartPoint.Value.Y); }; img.PreviewMouseMove += (sender, args) => { if (processor == null || dragStartPoint == null || dragMenu.IsOpen) return; Logger.Send(LogType.Information,"",$"MouseMove with DragObject set"); var point = args.GetPosition(img); var top = Math.Min(point.Y, dragStartPoint.Value.Y); var left = Math.Min(point.X, dragStartPoint.Value.X); var bottom = Math.Max(point.Y, dragStartPoint.Value.Y); var right = Math.Max(point.X, dragStartPoint.Value.X); Logger.Send(LogType.Information,"",$"Drag Rectangle is {left},{top} -> {right},{bottom}"); dragObject.Margin = new Thickness(left+1, top+1, (img.ActualWidth-right)+1, (img.ActualHeight-bottom)+1); }; img.PreviewMouseLeftButtonUp += (sender, args) => { if (processor == null) return; Logger.Send(LogType.Information,"",$"Hiding DragObject"); if (dragStartPoint == null) return; Logger.Send(LogType.Information,"",$"Ending Drag"); if (sender is not Image image || image.Source is not BitmapSource bitmapSource) return; var croprect = new RectangleF( (float)dragObject.Margin.Left-0.75F, (float)dragObject.Margin.Top-0.75F, (float)(img.ActualWidth+0.75F-(dragObject.Margin.Left+dragObject.Margin.Right)), (float)(img.ActualHeight+0.75F-(dragObject.Margin.Top+dragObject.Margin.Bottom)) ); if (croprect.Width < 5 || croprect.Height < 5) { dragStartPoint = null; dragObject.Visibility = Visibility.Collapsed; return; } var bitmap = ImageUtils.BitmapSourceToBitmap(bitmapSource); Bitmap bmp = bitmap.Clone(croprect, bitmap.PixelFormat); bmp = Resize(bmp, bmp.Width * 10, bmp.Height * 10); bmp = SetGrayscale(bmp); bmp = RemoveNoise(bmp); bmp.Save(Path.Combine(CoreUtils.GetPath(),"ocr.bmp")); string text = processor.PerformOCR(bmp, CoreUtils.GetPath()).Trim(); if (string.IsNullOrWhiteSpace(text)) { dragStartPoint = null; dragObject.Visibility = Visibility.Collapsed; return; } dragMenu.Items.Clear(); OCRContextMenuOpening?.Invoke(this, new DocumentOCRContextMenuArgs(dragMenu, text)); if (dragMenu.Items.Count == 0) { dragStartPoint = null; dragObject.Visibility = Visibility.Collapsed; return; } dragMenu.Items.Insert(0,new MenuItem() { Header = text, IsEnabled = false}); dragMenu.Items.Insert(1,new Separator()); dragMenu.IsOpen = true; }; img.MouseUp += (sender, args) => { Logger.Send(LogType.Information, "", $"MouseUp"); }; grid.Children.Add(img); grid.Children.Add(dragObject); return grid; }); ZoomPanel.Content = itemsControl; border.Child = ZoomPanel; Content = border; BindingOperations.EnableCollectionSynchronization(ViewList, _viewListLock); } public event DocumentOCRContextMenuOpeningEvent OCRContextMenuOpening; protected abstract Guid GetID(TDocument document); protected abstract Guid GetDocumentID(TDocument document); protected abstract string GetDocumentFileName(IEnumerable documents, Document document); protected abstract IEnumerable LoadDocuments(IEnumerable ids); public void UpdateViewList(IList documents, bool force = false) { if (!force && documents.Count == _documents.Count && !documents.Any(x => _documents.All(y => GetID(x) != GetID(y)))) return; _documents = documents; ViewList.Clear(); ViewDocuments.Clear(); if(_documents.Count == 0) { return; } Task.Run(() => { var docs = LoadDocuments(Documents.Select(GetDocumentID).Distinct()); foreach (var doc in docs) doc.FileName = GetDocumentFileName(Documents, doc); LoadDocuments(docs); }).ContinueWith((task) => { if(task.Exception is not null) { MessageWindow.ShowError("An error occurred while loading the documents", task.Exception); } }, TaskScheduler.FromCurrentSynchronizationContext()); } private void LoadDocuments(IEnumerable documents) { var bitmaps = new Dictionary>(); foreach (var document in documents.Where(x=>x.Data?.Any() == true)) { List images; var bitmapImages = new List(); var extension = Path.GetExtension(document.FileName).ToLower(); if (extension == ".pdf") { images = new List(); try { bitmapImages = ImageUtils.RenderPDFToImageSources(document.Data); } catch (Exception e) { MessageBox.Show($"Cannot load document '{document.FileName}': {e.Message}"); } } else if (extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".bmp") { images = new List { document.Data }; } else { images = ImageUtils.RenderTextFileToImages(Encoding.UTF8.GetString(document.Data)); } bitmapImages.AddRange(images.Select(x => { try { return ImageUtils.LoadImage(x); } catch (Exception e) { Dispatcher.BeginInvoke(() => { MessageWindow.ShowError($"Cannot load document '{document.FileName}", e); }); } return null; }).Where(x => x != null).Cast()); foreach (var image in bitmapImages) { if (!bitmaps.TryGetValue(document.ID, out var list)) { list = new List(); bitmaps[document.ID] = list; } list.Add(image); } } ViewDocuments.Clear(); var maxWidth = 0.0; foreach (var scan in Documents) { if (bitmaps.TryGetValue(GetDocumentID(scan), out var list)) { int page = 1; foreach (var bitmap in list) { maxWidth = Math.Max(maxWidth, bitmap.Width); ViewDocuments.Add(new(bitmap, scan, page)); page++; } } } lock (_viewListLock) { ViewList.Clear(); foreach(var doc in ViewDocuments) { ViewList.Add(doc.Image); } if(maxWidth != 0.0) { ZoomPanel.Scale = ZoomPanel.ActualWidth / (maxWidth * 1.1); ZoomPanel.MinScale = ZoomPanel.Scale / 2; } } } private void RotateDocument(Document doc, int pageNumber) { var extension = Path.GetExtension(doc.FileName).ToLower(); if (extension == ".pdf") { var loadeddoc = new PdfLoadedDocument(doc.Data); bool allPages = loadeddoc.PageCount() > 1; if (allPages) { allPages = MessageWindow.New() .Message("Do you want to rotate all pages in this PDF?") .Title("Rotate all?") .AddYesButton("All pages") .AddNoButton("Just this page") .Display().Result == MessageWindowResult.Yes; } if(allPages) { foreach (var page in loadeddoc.GetPages()) { var rotation = (int)page.Rotation; rotation = (rotation + 1) % 4; page.Rotation = (PdfPageRotateAngle)rotation; } } else if(pageNumber <= loadeddoc.PageCount()) { var page = loadeddoc.GetPage(pageNumber - 1); var rotation = (int)page.Rotation; rotation = (rotation + 1) % 4; page.Rotation = (PdfPageRotateAngle)rotation; } doc.Data = loadeddoc.SaveToBytes(); } else if (extension == ".jpg" || extension == ".jpeg" || extension == ".png" || extension == ".bmp") { using var stream = new MemoryStream(doc.Data); var bitmap = Bitmap.FromStream(stream); bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone); using var outStream = new MemoryStream(); bitmap.Save(outStream, extension switch { ".jpg" or ".jpeg" => ImageFormat.Jpeg, ".png" => ImageFormat.Png, _ => ImageFormat.Bmp }); doc.Data = outStream.ToArray(); } else { using var stream = new MemoryStream(doc.Data); var loadeddoc = DataEntryReGroupWindow.RenderToPDF(doc.FileName, stream); foreach (var page in loadeddoc.GetPages()) { var rotation = (int)page.Rotation; rotation = (rotation + 1) % 4; page.Rotation = (PdfPageRotateAngle)rotation; } doc.Data = loadeddoc.SaveToBytes(); } } public Bitmap Resize(Bitmap bmp, int newWidth, int newHeight) { Bitmap result = new Bitmap(newWidth, newHeight); using (Graphics g = Graphics.FromImage(result)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.DrawImage(bmp, 0, 0, newWidth, newHeight); } return result; // Bitmap temp = (Bitmap)bmp; // // Bitmap bmap = new Bitmap(newWidth, newHeight, temp.PixelFormat); // // double nWidthFactor = (double)temp.Width / (double)newWidth; // double nHeightFactor = (double)temp.Height / (double)newHeight; // // double fx, fy, nx, ny; // int cx, cy, fr_x, fr_y; // Color color1 = new Color(); // Color color2 = new Color(); // Color color3 = new Color(); // Color color4 = new Color(); // byte nRed, nGreen, nBlue; // // byte bp1, bp2; // // for (int x = 0; x < bmap.Width; ++x) // { // for (int y = 0; y < bmap.Height; ++y) // { // // fr_x = (int)Math.Floor(x * nWidthFactor); // fr_y = (int)Math.Floor(y * nHeightFactor); // cx = fr_x + 1; // if (cx >= temp.Width) cx = fr_x; // cy = fr_y + 1; // if (cy >= temp.Height) cy = fr_y; // fx = x * nWidthFactor - fr_x; // fy = y * nHeightFactor - fr_y; // nx = 1.0 - fx; // ny = 1.0 - fy; // // color1 = temp.GetPixel(fr_x, fr_y); // color2 = temp.GetPixel(cx, fr_y); // color3 = temp.GetPixel(fr_x, cy); // color4 = temp.GetPixel(cx, cy); // // // Blue // bp1 = (byte)(nx * color1.B + fx * color2.B); // // bp2 = (byte)(nx * color3.B + fx * color4.B); // // nBlue = (byte)(ny * (double)(bp1) + fy * (double)(bp2)); // // // Green // bp1 = (byte)(nx * color1.G + fx * color2.G); // // bp2 = (byte)(nx * color3.G + fx * color4.G); // // nGreen = (byte)(ny * (double)(bp1) + fy * (double)(bp2)); // // // Red // bp1 = (byte)(nx * color1.R + fx * color2.R); // // bp2 = (byte)(nx * color3.R + fx * color4.R); // // nRed = (byte)(ny * (double)(bp1) + fy * (double)(bp2)); // // bmap.SetPixel(x, y, System.Drawing.Color.FromArgb // (255, nRed, nGreen, nBlue)); // } // } // // // // return bmap; } public Bitmap SetGrayscale(Bitmap img) { Bitmap temp = (Bitmap)img; Bitmap bmap = (Bitmap)temp.Clone(); Color c; for (int i = 0; i < bmap.Width; i++) { for (int j = 0; j < bmap.Height; j++) { c = bmap.GetPixel(i, j); byte gray = (byte)(.299 * c.R + .587 * c.G + .114 * c.B); bmap.SetPixel(i, j, Color.FromArgb(gray, gray, gray)); } } return (Bitmap)bmap.Clone(); } public Bitmap RemoveNoise(Bitmap bmap) { for (var x = 0; x < bmap.Width; x++) { for (var y = 0; y < bmap.Height; y++) { var pixel = bmap.GetPixel(x, y); if (pixel.R < 162 && pixel.G < 162 && pixel.B < 162) bmap.SetPixel(x, y, Color.Black); else if (pixel.R > 162 && pixel.G > 162 && pixel.B > 162) bmap.SetPixel(x, y, Color.White); } } return bmap; } // private async void OCRImage_Click(object sender, RoutedEventArgs e) // { // if (sender is not MenuItem item || item.Tag is not ImageSource image) return; // // var document = ViewDocuments.FirstOrDefault(x => x.Image == image); // if (document is null) // { // MessageWindow.ShowError("An error occurred", "Document does not exist in ViewDocuments list"); // return; // } // // var doc = LoadDocuments(CoreUtils.One(GetDocumentID(document.Document))).First(); // // if (doc.FileName.ToLower().EndsWith(".txt")) // { // var text = System.Text.Encoding.UTF8.GetString(doc.Data); // File.WriteAllText(Path.Combine(CoreUtils.GetPath(),"txt-ocr.txt"),text); // return; // } // else // { // List pagetexts = new List(); // var images = doc.FileName.ToLower().EndsWith(".pdf") // ? ImageUtils.RenderPDFToImageBytes(doc.Data, ImageUtils.ImageEncoding.PNG).ToList() // : new List() { doc.Data }; // // using (var fontStream = new FileStream(Path.Combine(CoreUtils.GetPath(), "ARIALUNI.ttf"), FileMode.Open)) // { // using (var font = new PdfTrueTypeFont(fontStream, 8)) // { // using (OCRProcessor processor = new OCRProcessor(CoreUtils.GetPath())) // { // processor.Settings.Language = Languages.English; // processor.Settings.PageSegment = PageSegMode.SparseText; // processor.Settings.OCREngineMode = OCREngineMode.LSTMOnly; // //processor.UnicodeFont = font; // foreach (var img in images) // { // using (var ms = new MemoryStream(img)) // { // using (var bitmap = new Bitmap(ms)) // { // var bmp = Resize(bitmap, bitmap.Width * 3, bitmap.Height * 3); // bmp = SetGrayscale(bmp); // bmp = RemoveNoise(bmp); // bmp.Save(Path.Combine(CoreUtils.GetPath(),"ocr.bmp")); // string text = processor.PerformOCR(bitmap, CoreUtils.GetPath()); // pagetexts.Add(text); // } // } // // } // } // } // // fontStream.Close(); // } // File.WriteAllText(Path.Combine(CoreUtils.GetPath(), "image-ocr.txt"), String.Join("\n\n",pagetexts)); // // } // // try // { // // TextRecognizer? _textRecognizer; // // var readyState = TextRecognizer.GetReadyState(); // // if (readyState is AIFeatureReadyState.NotSupportedOnCurrentSystem or AIFeatureReadyState.DisabledByUser) // // return; // // // // if (readyState == AIFeatureReadyState.NotReady) // // await TextRecognizer.EnsureReadyAsync(); // // // // _textRecognizer = await TextRecognizer.CreateAsync(); // // var bitmap = await ByteArrayToSoftwareBitmapAsync(doc.Data); // // // // SoftwareBitmap displayableImage = SoftwareBitmap.Convert(bitmap, // // BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied); // // // // var imageBuffer = ImageBuffer.CreateForSoftwareBitmap(displayableImage); // // var result = _textRecognizer.RecognizeTextFromImage(imageBuffer); // // File.WriteAllText(Path.Combine(CoreUtils.GetPath(),"ai-ocr.txt"),String.Join("\n",result.Lines.Select(x=>x.Text))); // // // // else // // { // // var images = doc.FileName.ToLower().EndsWith(".pdf") // // ? ImageUtils.RenderPDFToImageBytes(doc.Data).ToList() // // : new List() { doc.Data }; // // // // using (OCRProcessor processor = new OCRProcessor(CoreUtils.GetPath())) // // { // // PdfLoadedDocument pdfLoadedDocument = new PdfLoadedDocument(doc.Data); // // processor.Settings.Language = Languages.English; // // var text = processor.PerformOCR(pdfLoadedDocument); // // File.WriteAllText(Path.Combine(CoreUtils.GetPath(),"pdf-ocr.txt"),text); // // } // // } // // else // // { // // using (var stream = new MemoryStream(doc.Data)) // // { // // using (var bmp = new Bitmap(stream)) // // { // // using (var fontStream = new FileStream(Path.Combine(CoreUtils.GetPath(), "ARIALUNI.ttf"), // // FileMode.Open)) // // { // // using (var font = new PdfTrueTypeFont(fontStream, 8)) // // { // // using (OCRProcessor processor = new OCRProcessor(CoreUtils.GetPath())) // // { // // processor.Settings.Language = Languages.English; // // processor.UnicodeFont = font; // // string text = processor.PerformOCR(bmp, CoreUtils.GetPath()); // // File.WriteAllText(Path.Combine(CoreUtils.GetPath(), "image-ocr.txt"), text); // // } // // } // // // // fontStream.Close(); // // } // // } // // } // // }} // } // catch(Exception err) // { // MessageWindow.ShowError("Something went wrong while trying to scan this document.", err); // return; // } // // } private void RotateImage_Click(object sender, RoutedEventArgs e) { if (sender is not MenuItem item || item.Tag is not ImageSource image) return; var document = ViewDocuments.FirstOrDefault(x => x.Image == image); if (document is null) { MessageWindow.ShowError("An error occurred", "Document does not exist in ViewDocuments list"); return; } var doc = LoadDocuments(CoreUtils.One(GetDocumentID(document.Document))).First(); try { RotateDocument(doc, document.PageNumber); } catch(Exception err) { MessageWindow.ShowError("Something went wrong while trying to rotate this document.", err); return; } Client.Save(doc, "Rotated by user."); UpdateDocument?.Invoke(document.Document, doc); } private void ViewImage_Click(object sender, RoutedEventArgs e) { if (sender is not MenuItem item || item.Tag is not ImageSource image) return; OpenImageWindow(image); } private void ExplodeAll_Click() { ExplodeAll?.Invoke(); } private void Explode_Click() { Explode?.Invoke(); } #region Image Window private List OpenWindows = new(); public void CloseImageWindows() { while (OpenWindows.Count > 0) { var win = OpenWindows.Last(); OpenWindows.RemoveAt(OpenWindows.Count - 1); win.Close(); } } private void OpenImageWindow(ImageSource image) { var window = OpenWindows.FirstOrDefault(x => x.Images.Contains(image)); if (window is not null) { window.Activate(); } else { var docID = GetDocumentID(ViewDocuments.First(x => x.Image == image).Document); var docs = ViewDocuments.Where(x => GetDocumentID(x.Document) == docID); window = new DataEntryDocumentWindow(); window.Topmost = true; foreach(var doc in docs) { window.Images.Add(doc.Image); } OpenWindows.Add(window); window.Closed += OpenWindow_Closed; window.Show(); } } private void OpenWindow_Closed(object? sender, EventArgs e) { if (sender is not DataEntryDocumentWindow window) return; OpenWindows.Remove(window); } #endregion public event PropertyChangedEventHandler? PropertyChanged; protected void DoPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }