Browse Source

WIP Implemented virtualization of pages.

Render pages into seperate SKPictures
Bebo-Maker 3 years ago
parent
commit
ea2102fe4c

+ 86 - 19
QuestPDF.Previewer/DocumentRenderer.cs

@@ -1,22 +1,55 @@
-using QuestPDF.Drawing;
+using System.Collections.ObjectModel;
+using System.ComponentModel;
+using Avalonia.Threading;
+using QuestPDF.Drawing;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using SkiaSharp;
 
 namespace QuestPDF.Previewer
 {
-    internal class DocumentRenderer
+    public record RenderedPageInfo(SKPicture Picture, Size Size);
+
+    internal class DocumentRenderer : INotifyPropertyChanged
     {
         public float PageSpacing { get; set; }
         public Size Bounds { get; private set; }
         public IDocument? Document { get; private set; }
-        public SKPicture? Picture { get; private set; }
-        public Exception? RenderException { get; private set; }
-        public bool IsRendering { get; private set; }
+
+        public event PropertyChangedEventHandler? PropertyChanged;
+
+        private ObservableCollection<RenderedPageInfo> _pages = new();
+        public ObservableCollection<RenderedPageInfo> Pages
+        {
+            get => _pages;
+            set
+            {
+                if (_pages != value)
+                {
+                    _pages = value;
+                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pages)));
+                }
+            }
+        }
+
+        private bool _isRendering;
+        public bool IsRendering
+        {
+            get => _isRendering;
+            private set
+            {
+                if (_isRendering != value)
+                {
+                    _isRendering = value;
+                    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRendering)));
+                }
+            }
+        }
 
         public void UpdateDocument(IDocument? document)
         {
             Document = document;
-            RenderException = null;
             if (document != null)
             {
                 try
@@ -26,9 +59,7 @@ namespace QuestPDF.Previewer
                 }
                 catch (Exception ex)
                 {
-                    RenderException = ex;
-                    Picture?.Dispose();
-                    Picture = null;
+                    RenderDocument(CreateExceptionDocument(ex));
                 }
                 finally
                 {
@@ -39,23 +70,59 @@ namespace QuestPDF.Previewer
 
         private void RenderDocument(IDocument document)
         {
-            Picture?.Dispose();
+            var canvas = new PreviewerCanvas();
 
-            var canvas = new PreviewerCanvas()
-            {
-                PageSpacing = PageSpacing,
-            };
-
-            using var recorder = new SKPictureRecorder();
             DocumentGenerator.RenderDocument(canvas, new SizeTrackingCanvas(), document, s =>
             {
                 var width = s.PageSizes.Max(p => p.Width);
                 var height = s.PageSizes.Sum(p => p.Height) + ((s.PageSizes.Count - 1) * PageSpacing);
                 Bounds = new Size(width, height);
-                canvas.Canvas = recorder.BeginRecording(new SKRect(0, 0, width, height));
-                canvas.MaxPageWidth = width;
             });
-            Picture = recorder.EndRecording();
+
+            foreach (var pages in Pages)
+                pages?.Picture?.Dispose();
+            Dispatcher.UIThread.Post(() =>
+            {
+                Pages.Clear();
+                Pages = new ObservableCollection<RenderedPageInfo>(canvas.Pictures);
+            });
+        }
+
+        private static IDocument CreateExceptionDocument(Exception exception)
+        {
+            return Fluent.Document.Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Size(PageSizes.A4);
+                    page.Margin(1, Unit.Inch);
+                    page.PageColor(Colors.Red.Lighten4);
+                    page.DefaultTextStyle(x => x.FontSize(16));
+
+                    page.Header()
+                        .BorderBottom(2)
+                        .BorderColor(Colors.Red.Medium)
+                        .PaddingBottom(5)
+                        .Text("Ooops! Something went wrong...").FontSize(28).FontColor(Colors.Red.Medium).Bold();
+
+                    page.Content().PaddingVertical(20).Column(column =>
+                    {
+                        var currentException = exception;
+
+                        while (currentException != null)
+                        {
+                            column.Item().Text(exception.GetType().Name).FontSize(20).SemiBold();
+                            column.Item().Text(exception.Message).FontSize(14);
+                            column.Item().PaddingTop(10).Text(exception.StackTrace).FontSize(10).Light();
+
+                            currentException = currentException.InnerException;
+
+                            if (currentException != null)
+                                column.Item().PaddingVertical(15).LineHorizontal(2).LineColor(Colors.Red.Medium);
+                        }
+                    });
+                });
+            });
         }
     }
 }

+ 2 - 15
QuestPDF.Previewer/HotReloadManager.cs

@@ -7,24 +7,11 @@ namespace QuestPDF.Previewer
     /// </summary>
     internal static class HotReloadManager
     {
-        private static readonly List<Action> _actions = new();
-
-        public static void Register(Action action)
-        {
-            _actions.Add(action);
-        }
-
-        public static void Unregister(Action action)
-        {
-            _actions.Remove(action);
-        }
-
-        public static void ClearCache(Type[]? _) { }
+        public static event EventHandler? UpdateApplicationRequested;
 
         public static void UpdateApplication(Type[]? _)
         {
-            foreach (var action in _actions)
-                action();
+            UpdateApplicationRequested?.Invoke(null, EventArgs.Empty);
         }
     }
 }

+ 16 - 23
QuestPDF.Previewer/PreviewerCanvas.cs

@@ -6,45 +6,38 @@ namespace QuestPDF.Previewer
 {
     internal sealed class PreviewerCanvas : SkiaCanvasBase, IRenderingCanvas
     {
-        private bool _isFirstPage;
-        private Size? _lastPageSize;
-        private float? _distanceToCenter;
+        private SKPictureRecorder? _currentRecorder;
 
-        public float PageSpacing { get; set; }
-        public float MaxPageWidth { get; set; }
+        private readonly List<RenderedPageInfo> _pictures = new();
+        public IReadOnlyList<RenderedPageInfo> Pictures => _pictures;
+
+        private Size? _currentSize;
 
         public override void BeginDocument()
         {
-            _isFirstPage = true;
+            _pictures.Clear();
         }
 
         public override void BeginPage(Size size)
         {
-            _lastPageSize = size;
-
-            if (!_isFirstPage)
-                Canvas.Translate(0, PageSpacing);
-
-            if (MaxPageWidth > 0)
-            {
-                _distanceToCenter = (MaxPageWidth - size.Width) / 2;
-                Canvas.Translate(_distanceToCenter.Value, 0);
-            }
+            _currentSize = size;
+            _currentRecorder = new SKPictureRecorder();
+            Canvas = _currentRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height));
 
             using var paint = new SKPaint() { Color = SKColors.White };
             Canvas.DrawRect(0, 0, size.Width, size.Height, paint);
-
-            _isFirstPage = false;
         }
 
-        public override void EndDocument() { }
         public override void EndPage()
         {
-            if (_lastPageSize.HasValue)
-                Canvas.Translate(0, _lastPageSize.Value.Height);
+            var picture = _currentRecorder?.EndRecording();
+            if (picture != null && _currentSize.HasValue)
+                _pictures.Add(new RenderedPageInfo(picture, _currentSize.Value));
 
-            if (_distanceToCenter.HasValue)
-                Canvas.Translate(-_distanceToCenter.Value, 0);
+            _currentRecorder?.Dispose();
+            _currentRecorder = null;
         }
+
+        public override void EndDocument() { }
     }
 }

+ 0 - 96
QuestPDF.Previewer/PreviewerControl.cs

@@ -1,96 +0,0 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Media;
-using Avalonia.Threading;
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Previewer
-{
-    internal class PreviewerControl : Control
-    {
-        public static readonly StyledProperty<IDocument?> DocumentProperty =
-            AvaloniaProperty.Register<PreviewerControl, IDocument?>(nameof(Document));
-
-        public IDocument? Document
-        {
-            get => GetValue(DocumentProperty);
-            set => SetValue(DocumentProperty, value);
-        }
-
-        public static readonly StyledProperty<bool> IsGeneratingDocumentProperty =
-            AvaloniaProperty.Register<PreviewerControl, bool>(nameof(IsGeneratingDocument));
-
-        public bool IsGeneratingDocument
-        {
-            get => GetValue(IsGeneratingDocumentProperty);
-            set => SetValue(IsGeneratingDocumentProperty, value);
-        }
-
-        public static readonly StyledProperty<double> PageSpacingProperty =
-            AvaloniaProperty.Register<PreviewerControl, double>(nameof(PageSpacing), 20);
-
-        public double PageSpacing
-        {
-            get => GetValue(PageSpacingProperty);
-            set => SetValue(PageSpacingProperty, value);
-        }
-
-        private readonly DocumentRenderer _renderer = new();
-
-        public PreviewerControl()
-        {
-            ClipToBounds = true;
-
-            _renderer.PageSpacing = (float)PageSpacing;
-            DocumentProperty.Changed.Subscribe(_ => _renderer.UpdateDocument(Document));
-            PageSpacingProperty.Changed.Subscribe(f => _renderer.PageSpacing = (float)f.NewValue.Value);
-        }
-
-        public override void Render(DrawingContext context)
-        {
-            IsGeneratingDocument = _renderer.IsRendering;
-            if (_renderer.IsRendering)
-                return;
-
-            if (_renderer.RenderException != null)
-            {
-                context.DrawRectangle(Brushes.Transparent, null, new Rect(0, 0, Bounds.Width, Bounds.Height));
-                DrawException(context, _renderer.RenderException);
-                return;
-            }
-
-            Width = _renderer.Bounds.Width;
-            Height = _renderer.Bounds.Height;
-
-            if (_renderer.Picture == null)
-                return;
-
-            context.Custom(new SkCustomDrawOperation(
-                new Rect(0, 0, Bounds.Width, Bounds.Height),
-                c => c.DrawPicture(_renderer.Picture)));
-        }
-
-        internal void InvalidateDocument()
-        {
-            Dispatcher.UIThread.Post(() =>
-            {
-                _renderer.UpdateDocument(Document);
-                InvalidateVisual();
-            });
-        }
-
-        private void DrawException(DrawingContext context, Exception ex)
-        {
-            var parentBounds = Parent?.Bounds ?? Bounds;
-
-            var exceptionMsg = string.Join("\n", ex.GetType(), ex.Message);
-
-            var fmtText = new FormattedText($"Exception occured:\n{exceptionMsg}", 
-                Typeface.Default, 25, TextAlignment.Left, TextWrapping.Wrap, 
-                new Avalonia.Size(parentBounds.Width / 2, parentBounds.Height / 2));
-
-            var center = new Point((parentBounds.Width - fmtText.Bounds.Width) / 2, (parentBounds.Height - fmtText.Bounds.Height) / 2);
-            context.DrawText(Brushes.Black, center, fmtText);
-        }
-    }
-}

+ 44 - 0
QuestPDF.Previewer/PreviewerPageControl.cs

@@ -0,0 +1,44 @@
+using System.Reactive.Linq;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+
+namespace QuestPDF.Previewer
+{
+    internal class PreviewerPageControl : Control
+    {
+        public static readonly StyledProperty<RenderedPageInfo?> PageProperty =
+            AvaloniaProperty.Register<PreviewerPageControl, RenderedPageInfo?>(nameof(Page));
+
+        public RenderedPageInfo? Page
+        {
+            get => GetValue(PageProperty);
+            set => SetValue(PageProperty, value);
+        }
+
+        public PreviewerPageControl()
+        {
+            PageProperty.Changed.Take(1).Subscribe(p =>
+            {
+                var size = p.NewValue.Value?.Size ?? Infrastructure.Size.Zero;
+                Width = size.Width;
+                Height = size.Height;
+            });
+
+            ClipToBounds = true;
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            base.Render(context);
+
+            //context.DrawRectangle(Brushes.Red, null, new Rect(0, 0, Bounds.Width, Bounds.Height));
+
+            var picture = Page?.Picture;
+            if (picture != null)
+                context.Custom(new SkCustomDrawOperation(
+                    new Rect(0, 0, Bounds.Width, Bounds.Height),
+                    c => c.DrawPicture(picture)));
+        }
+    }
+}

+ 22 - 4
QuestPDF.Previewer/PreviewerWindow.axaml

@@ -9,6 +9,7 @@
 							Icon="/Images/Logo.png"
 							UseLayoutRounding="True"
 							Background="{x:Null}"
+							x:Name="Window"
 							Title="QuestPDF Document Preview">
 	<FluentWindow.Styles>
 		<Style Selector="TitleBar:fullscreen">
@@ -26,16 +27,33 @@
 		<Grid Margin="10,40,10,0">
 			<Grid.RowDefinitions>
 				<RowDefinition Height="40"/>
+				<RowDefinition Height="Auto"/>
 				<RowDefinition Height="*"/>
 			</Grid.RowDefinitions>
 
-			<DockPanel LastChildFill="False" VerticalAlignment="Center" Margin="20,0" Width="{Binding #PreviewerSurface.Width, Mode=OneWay}">
+			<DockPanel LastChildFill="False" VerticalAlignment="Center" Margin="20,0">
 				<TextBlock VerticalAlignment="Center" Text="QuestPDF Document Preview" DockPanel.Dock="Left" FontWeight="SemiBold" />
 				<Button x:Name="GeneratePdf" Content="Generate PDF" DockPanel.Dock="Right" />
 			</DockPanel>
-			
-			<ScrollViewer Grid.Row="1" VerticalAlignment="Top">
-				<previewer:PreviewerControl x:Name="PreviewerSurface" Margin="25" />
+
+			<ProgressBar IsVisible="{Binding #Window.DocumentRenderer.IsRendering}"
+									 Margin="20,0"
+									 Grid.Row="1"
+									 HorizontalAlignment="Stretch"									 
+									 Height="5"
+								   IsIndeterminate="True"/>
+
+			<ScrollViewer Grid.Row="2" VerticalAlignment="Top">
+				<ItemsRepeater HorizontalAlignment="Center" Margin="0,10" Grid.Row="1" VerticalAlignment="Top" Items="{Binding #Window.DocumentRenderer.Pages, Mode=OneWay}">
+					<ItemsRepeater.Layout>
+						<UniformGridLayout MaximumRowsOrColumns="1" Orientation="Horizontal" MinRowSpacing="25" MinColumnSpacing="25" />
+					</ItemsRepeater.Layout>
+					<ItemsRepeater.ItemTemplate>
+						<DataTemplate>
+							<previewer:PreviewerPageControl Page="{Binding}" />
+						</DataTemplate>
+					</ItemsRepeater.ItemTemplate>
+				</ItemsRepeater>
 			</ScrollViewer>
 		</Grid>
 	</Panel>

+ 9 - 6
QuestPDF.Previewer/PreviewerWindow.axaml.cs

@@ -1,13 +1,14 @@
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Previewer
 {
     internal class PreviewerWindow : FluentWindow
     {
-        private readonly PreviewerControl _previewHost;
+        public DocumentRenderer DocumentRenderer { get; } = new();
 
         public static readonly StyledProperty<IDocument?> DocumentProperty =
             AvaloniaProperty.Register<PreviewerWindow, IDocument?>(nameof(Document));
@@ -22,13 +23,11 @@ namespace QuestPDF.Previewer
         {
             InitializeComponent();
 
-            _previewHost = this.FindControl<PreviewerControl>("PreviewerSurface");
-
             this.FindControl<Button>("GeneratePdf")
                 .Click += (_, _) => _ = PreviewerUtils.SavePdfWithDialog(Document, this);
 
-            DocumentProperty.Changed.Subscribe(v => _previewHost.Document = v.NewValue.Value);
-            HotReloadManager.Register(InvalidatePreview);
+            DocumentProperty.Changed.Subscribe(v => Task.Run(() => DocumentRenderer.UpdateDocument(v.NewValue.Value)));
+            HotReloadManager.UpdateApplicationRequested += (_, _) => InvalidatePreview();
         }
 
         private void InitializeComponent()
@@ -38,7 +37,11 @@ namespace QuestPDF.Previewer
 
         private void InvalidatePreview()
         {
-            _previewHost.InvalidateDocument();
+            Dispatcher.UIThread.Post(() =>
+            {
+                var document = Document;
+                _ = Task.Run(() => DocumentRenderer.UpdateDocument(document));
+            });
         }
     }
 }