|
@@ -0,0 +1,466 @@
|
|
|
+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;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// Control that allows to view a list of documents, within a zoom control, and providing methods to rotate/explode data.
|
|
|
+/// </summary>
|
|
|
+/// <remarks>
|
|
|
+/// This is originally from the Data entry panel, and this implementation is a <i>little bit</i> scuffed. Basically, because the <see cref="DataEntryDocument"/>
|
|
|
+/// is not an <see cref="EntityDocument{T}"/>, 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.
|
|
|
+/// <b/>
|
|
|
+/// Note one needs also to provide <see cref="UpdateDocument"/>. 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.
|
|
|
+/// </remarks>
|
|
|
+/// <typeparam name="TDocument"></typeparam>
|
|
|
+public abstract class DocumentViewList<TDocument> : UserControl, INotifyPropertyChanged
|
|
|
+{
|
|
|
+ public static readonly DependencyProperty CanRotateImageProperty = DependencyProperty.Register(nameof(CanRotateImage), typeof(bool), typeof(DocumentViewList<TDocument>), new PropertyMetadata(true, CanRotateImage_Changed));
|
|
|
+
|
|
|
+ private static void CanRotateImage_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
+ {
|
|
|
+ if (d is not DocumentViewList<TDocument> list) return;
|
|
|
+ list.DoPropertyChanged(e.Property.Name);
|
|
|
+ }
|
|
|
+
|
|
|
+ private IList<TDocument> _documents = [];
|
|
|
+ public IList<TDocument> 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<ViewDocument> ViewDocuments { get; } = new();
|
|
|
+ public ObservableCollection<ImageSource> 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<TDocument, Document>? 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<ImageSource, ImageSource>(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<ImageSource, ImageSource>(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<ImageSource, ImageSource>(Image.SourceProperty, x => x);
|
|
|
+ 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<Document> LoadDocuments(IEnumerable<Guid> ids);
|
|
|
+
|
|
|
+ public void UpdateViewList(IList<TDocument> 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<Document> documents)
|
|
|
+ {
|
|
|
+ var bitmaps = new Dictionary<Guid, List<ImageSource>>();
|
|
|
+ foreach (var document in documents.Where(x=>x.Data?.Any() == true))
|
|
|
+ {
|
|
|
+ List<byte[]> images;
|
|
|
+ var bitmapImages = new List<ImageSource>();
|
|
|
+ var extension = Path.GetExtension(document.FileName).ToLower();
|
|
|
+ if (extension == ".pdf")
|
|
|
+ {
|
|
|
+ images = new List<byte[]>();
|
|
|
+ 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<byte[]> { 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<ImageSource>());
|
|
|
+
|
|
|
+ foreach (var image in bitmapImages)
|
|
|
+ {
|
|
|
+ if (!bitmaps.TryGetValue(document.ID, out var list))
|
|
|
+ {
|
|
|
+ list = new List<ImageSource>();
|
|
|
+ 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<DataEntryDocumentWindow> 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
|
|
|
+ {
|
|
|
+ window = new DataEntryDocumentWindow();
|
|
|
+ window.Topmost = true;
|
|
|
+ window.Images.Add(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<DataEntryDocument>
|
|
|
+{
|
|
|
+ protected override IEnumerable<Document> LoadDocuments(IEnumerable<Guid> 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;
|
|
|
+ }
|
|
|
+}
|