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.Imaging; using System.IO; using System.Linq; 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 Image = System.Windows.Controls.Image; namespace PRSDesktop; /// /// 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); } public DocumentViewList() { 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); itemsControl.ItemTemplate = TemplateGenerator.CreateDataTemplate(() => { var img = new Image(); img.Bind(Image.SourceProperty, x => x); img.Margin = new(0, 0, 0, 5); img.ContextMenu = itemsControl.ContextMenu; img.MouseLeftButtonDown += Img_MouseLeftButtonDown; return img; }); ZoomPanel.Content = itemsControl; border.Child = ZoomPanel; Content = border; BindingOperations.EnableCollectionSynchronization(ViewList, _viewListLock); } protected abstract Guid GetID(TDocument document); protected abstract Guid GetDocumentID(TDocument 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()); 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(); } } 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); } private void Img_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (sender is not Image image) return; if(e.ClickCount >= 2) { OpenImageWindow(image.Source); e.Handled = true; } } #endregion public event PropertyChangedEventHandler? PropertyChanged; protected void DoPropertyChanged([CallerMemberName] string propertyName = "") { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } public class DataEntryViewList : DocumentViewList { protected override IEnumerable LoadDocuments(IEnumerable ids) { return DataEntryCache.Cache.LoadDocuments(ids, checkTimestamp: true); } protected override Guid GetID(DataEntryDocument document) { return document.ID; } protected override Guid GetDocumentID(DataEntryDocument document) { return document.Document.ID; } }