Browse Source

Implement document preview.

Bebo-Maker 3 years ago
parent
commit
b5645bfbd5

+ 55 - 0
QuestPDF.Previewer/DocumentRenderOperation.cs

@@ -0,0 +1,55 @@
+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);
+            }
+        }
+    }
+}

+ 50 - 0
QuestPDF.Previewer/PreviewerCanvas.cs

@@ -0,0 +1,50 @@
+using QuestPDF.Drawing;
+using QuestPDF.Infrastructure;
+using SkiaSharp;
+
+namespace QuestPDF.Previewer
+{
+    internal sealed class PreviewerCanvas : SkiaCanvasBase, IRenderingCanvas
+    {
+        private bool _isFirstPage;
+        private Size? _lastPageSize;
+        private float? _distanceToCenter;
+
+        public float PageSpacing { get; set; }
+        public float MaxPageWidth { get; set; }
+
+        public override void BeginDocument()
+        {
+            _isFirstPage = true;
+        }
+
+        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);
+            }
+
+            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);
+
+            if (_distanceToCenter.HasValue)
+                Canvas.Translate(-_distanceToCenter.Value, 0);
+        }
+    }
+}

+ 84 - 0
QuestPDF.Previewer/PreviewerControl.cs

@@ -0,0 +1,84 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using QuestPDF.Drawing;
+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 float PageSpacing { get; set; } = 20;
+
+        public PreviewerControl()
+        {
+            DocumentProperty
+              .Changed
+              .Subscribe(_ => InvalidateVisual());
+        }
+
+        public override void Render(DrawingContext context)
+        {
+            if (Document == null)
+                return;
+
+            try
+            {
+                IsGeneratingDocument = true;
+                Render(context, Document);
+            }
+            finally
+            {
+                IsGeneratingDocument = false;
+            }
+        }
+
+        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);
+                return;
+            }
+
+            Width = docSizeTracker.PageSizes.Max(p => p.Width);
+            Height = docSizeTracker.PageSizes.Sum(p => p.Height) + ((docSizeTracker.PageSizes.Count - 1) * PageSpacing);
+
+            context.Custom(new DocumentRenderOperation(document, new Rect(0, 0, Width, Height), PageSpacing));
+        }
+
+        private void DrawException(DrawingContext context, Exception ex)
+        {
+            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(Parent.Bounds.Width / 2, Parent.Bounds.Height / 2));
+            var center = new Point((Parent.Bounds.Width - fmtText.Bounds.Width) / 2, (Parent.Bounds.Height - fmtText.Bounds.Height) / 2);
+            context.DrawText(Brushes.Black, center, fmtText);
+        }
+    }
+}

+ 17 - 0
QuestPDF.Previewer/PreviewerView.axaml

@@ -0,0 +1,17 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+						 xmlns:previewer="clr-namespace:QuestPDF.Previewer"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="QuestPDF.Previewer.PreviewerView">
+	<Grid>
+		<Grid.RowDefinitions>
+			<RowDefinition Height="*"/>
+		</Grid.RowDefinitions>
+
+		<ScrollViewer Background="LightGray">
+			<previewer:PreviewerControl x:Name="PreviewerSurface" Margin="25" />
+			</ScrollViewer>
+	</Grid>
+</UserControl>

+ 42 - 0
QuestPDF.Previewer/PreviewerView.axaml.cs

@@ -0,0 +1,42 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Previewer
+{
+    internal class PreviewerView : UserControl
+    {
+        private readonly PreviewerControl _previewHost;
+
+        public static readonly StyledProperty<IDocument?> DocumentProperty =
+            AvaloniaProperty.Register<PreviewerControl, IDocument?>(nameof(Document));
+
+        public IDocument? Document
+        {
+            get => GetValue(DocumentProperty);
+            set => SetValue(DocumentProperty, value);
+        }
+
+        public PreviewerView()
+        {
+            InitializeComponent();
+
+            _previewHost = this.FindControl<PreviewerControl>("PreviewerSurface");
+
+            DocumentProperty
+              .Changed
+              .Subscribe(v => _previewHost.Document = v.NewValue.Value);
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+
+        public void InvalidatePreview()
+        {
+            _previewHost.InvalidateVisual();
+        }
+    }
+}