diff --git a/QuestPDF.Previewer/DocumentRenderOperation.cs b/QuestPDF.Previewer/DocumentRenderOperation.cs deleted file mode 100644 index 6feeb13..0000000 --- a/QuestPDF.Previewer/DocumentRenderOperation.cs +++ /dev/null @@ -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); - } - } - } -} diff --git a/QuestPDF.Previewer/DocumentRenderer.cs b/QuestPDF.Previewer/DocumentRenderer.cs new file mode 100644 index 0000000..e3863e6 --- /dev/null +++ b/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(); + } + } +} diff --git a/QuestPDF.Previewer/PreviewerControl.cs b/QuestPDF.Previewer/PreviewerControl.cs index cd9e4ac..fa7b37d 100644 --- a/QuestPDF.Previewer/PreviewerControl.cs +++ b/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 DocumentProperty = - AvaloniaProperty.Register(nameof(Document)); + AvaloniaProperty.Register(nameof(Document)); public IDocument? Document { @@ -18,7 +18,7 @@ namespace QuestPDF.Previewer } public static readonly StyledProperty IsGeneratingDocumentProperty = - AvaloniaProperty.Register(nameof(IsGeneratingDocument)); + AvaloniaProperty.Register(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 PageSpacingProperty = + AvaloniaProperty.Register(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; } + + Width = _renderer.Bounds.Width; + Height = _renderer.Bounds.Height; + + if (_renderer.Picture == null) + return; + + context.Custom(new SkPictureRenderOperation(_renderer.Picture, Bounds)); } - private void Render(DrawingContext context, IDocument document) + internal void InvalidateDocument() { - //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 + Dispatcher.UIThread.Post(() => { - 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)); + _renderer.UpdateDocument(Document); + InvalidateVisual(); + }); } private void DrawException(DrawingContext context, Exception ex) diff --git a/QuestPDF.Previewer/PreviewerView.axaml.cs b/QuestPDF.Previewer/PreviewerView.axaml.cs index d20378d..e323d9c 100644 --- a/QuestPDF.Previewer/PreviewerView.axaml.cs +++ b/QuestPDF.Previewer/PreviewerView.axaml.cs @@ -36,7 +36,7 @@ namespace QuestPDF.Previewer public void InvalidatePreview() { - _previewHost.InvalidateVisual(); + _previewHost.InvalidateDocument(); } } } diff --git a/QuestPDF.Previewer/SkPictureRenderOperation.cs b/QuestPDF.Previewer/SkPictureRenderOperation.cs new file mode 100644 index 0000000..39c285e --- /dev/null +++ b/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); + } + } +}