Browse Source

Optimize document rendering.

We dont need to render the document two times anymore.
Bebo-Maker 3 years ago
parent
commit
346513ee85

+ 0 - 55
QuestPDF.Previewer/DocumentRenderOperation.cs

@@ -1,55 +0,0 @@
-using Avalonia;
-using Avalonia.Platform;
-using Avalonia.Rendering.SceneGraph;
-using Avalonia.Skia;
-using QuestPDF.Drawing;
-using QuestPDF.Infrastructure;
-using SkiaSharp;
-
-namespace QuestPDF.Previewer
-{
-    internal class DocumentRenderOperation : ICustomDrawOperation
-    {
-        public IDocument Document { get; }
-        public float PageSpacing { get; }
-        public Rect Bounds { get; }
-
-        public DocumentRenderOperation(IDocument document, Rect bounds, float pageSpacing)
-        {
-            Document = document;
-            Bounds = bounds;
-            PageSpacing = pageSpacing;
-        }
-
-        public void Dispose() { }
-
-        public bool Equals(ICustomDrawOperation? other)
-        {
-            return other is DocumentRenderOperation renderer && renderer.Document == Document;
-        }
-
-        public bool HitTest(Point p)
-        {
-            return false;
-        }
-
-        public void Render(IDrawingContextImpl context)
-        {
-            var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
-            if (canvas == null)
-                throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
-
-            using (new SKAutoCanvasRestore(canvas))
-            {
-                var previewerCanvas = new PreviewerCanvas()
-                {
-                    Canvas = canvas,
-                    PageSpacing = PageSpacing,
-                    MaxPageWidth = (float)Bounds.Width,
-                };
-
-                DocumentGenerator.RenderDocument(previewerCanvas, Document);
-            }
-        }
-    }
-}

+ 61 - 0
QuestPDF.Previewer/DocumentRenderer.cs

@@ -0,0 +1,61 @@
+using QuestPDF.Drawing;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
+
+namespace QuestPDF.Previewer
+{
+    internal class DocumentRenderer
+    {
+        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 void UpdateDocument(IDocument? document)
+        {
+            Document = document;
+            RenderException = null;
+            if (document != null)
+            {
+                try
+                {
+                    IsRendering = true;
+                    RenderDocument(document);
+                }
+                catch (Exception ex)
+                {
+                    RenderException = ex;
+                    Picture?.Dispose();
+                    Picture = null;
+                }
+                finally
+                {
+                    IsRendering = false;
+                }
+            }
+        }
+
+        private void RenderDocument(IDocument document)
+        {
+            Picture?.Dispose();
+
+            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();
+        }
+    }
+}

+ 35 - 33
QuestPDF.Previewer/PreviewerControl.cs

@@ -1,7 +1,7 @@
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Media;
-using QuestPDF.Drawing;
+using Avalonia.Threading;
 using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Previewer
@@ -9,7 +9,7 @@ namespace QuestPDF.Previewer
     internal class PreviewerControl : Control
     {
         public static readonly StyledProperty<IDocument?> DocumentProperty =
-          AvaloniaProperty.Register<PreviewerControl, IDocument?>(nameof(Document));
+            AvaloniaProperty.Register<PreviewerControl, IDocument?>(nameof(Document));
 
         public IDocument? Document
         {
@@ -18,7 +18,7 @@ namespace QuestPDF.Previewer
         }
 
         public static readonly StyledProperty<bool> IsGeneratingDocumentProperty =
-          AvaloniaProperty.Register<PreviewerControl, bool>(nameof(IsGeneratingDocument));
+            AvaloniaProperty.Register<PreviewerControl, bool>(nameof(IsGeneratingDocument));
 
         public bool IsGeneratingDocument
         {
@@ -26,51 +26,53 @@ namespace QuestPDF.Previewer
             set => SetValue(IsGeneratingDocumentProperty, value);
         }
 
-        public float PageSpacing { get; set; } = 20;
+        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()
         {
-            DocumentProperty
-              .Changed
-              .Subscribe(_ => InvalidateVisual());
+            _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)
         {
-            if (Document == null)
+            IsGeneratingDocument = _renderer.IsRendering;
+            if (_renderer.IsRendering)
                 return;
 
-            try
+            if (_renderer.RenderException != null)
             {
-                IsGeneratingDocument = true;
-                Render(context, Document);
-            }
-            finally
-            {
-                IsGeneratingDocument = false;
+                context.DrawRectangle(Brushes.Transparent, null, new Rect(0, 0, Bounds.Width, Bounds.Height));
+                DrawException(context, _renderer.RenderException);
+                return;
             }
-        }
 
-        private void Render(DrawingContext context, IDocument document)
-        {
-            //TODO optimize, currently we generate the document twice.
-            // First generation for calculating the sizes
-            // Second generation for the actual rendering.
-            var docSizeTracker = new SizeTrackingCanvas();
-            try
-            {
-                DocumentGenerator.RenderDocument(docSizeTracker, document);
-            }
-            catch (Exception ex)
-            {
-                DrawException(context, ex);
+            Width = _renderer.Bounds.Width;
+            Height = _renderer.Bounds.Height;
+
+            if (_renderer.Picture == null)
                 return;
-            }
 
-            Width = docSizeTracker.PageSizes.Max(p => p.Width);
-            Height = docSizeTracker.PageSizes.Sum(p => p.Height) + ((docSizeTracker.PageSizes.Count - 1) * PageSpacing);
+            context.Custom(new SkPictureRenderOperation(_renderer.Picture, Bounds));
+        }
 
-            context.Custom(new DocumentRenderOperation(document, new Rect(0, 0, Width, Height), PageSpacing));
+        internal void InvalidateDocument()
+        {
+            Dispatcher.UIThread.Post(() =>
+            {
+                _renderer.UpdateDocument(Document);
+                InvalidateVisual();
+            });
         }
 
         private void DrawException(DrawingContext context, Exception ex)

+ 1 - 1
QuestPDF.Previewer/PreviewerView.axaml.cs

@@ -36,7 +36,7 @@ namespace QuestPDF.Previewer
 
         public void InvalidatePreview()
         {
-            _previewHost.InvalidateVisual();
+            _previewHost.InvalidateDocument();
         }
     }
 }

+ 44 - 0
QuestPDF.Previewer/SkPictureRenderOperation.cs

@@ -0,0 +1,44 @@
+using Avalonia;
+using Avalonia.Platform;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Skia;
+using SkiaSharp;
+
+namespace QuestPDF.Previewer
+{
+    internal class SkPictureRenderOperation : ICustomDrawOperation
+    {
+        public SKPicture? Picture { get; }
+        public Rect Bounds { get; }
+
+        public SkPictureRenderOperation(SKPicture picture, Rect bounds)
+        {
+            Picture = picture;
+            Bounds = bounds;
+        }
+
+        public void Dispose() { }
+
+        public bool Equals(ICustomDrawOperation? other)
+        {
+            return false;
+        }
+
+        public bool HitTest(Point p)
+        {
+            return false;
+        }
+
+        public void Render(IDrawingContextImpl context)
+        {
+            if (Picture == null)
+                return;
+
+            var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
+            if (canvas == null)
+                throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
+
+            canvas.DrawPicture(Picture);
+        }
+    }
+}