diff --git a/QuestPDF.Previewer/DocumentRenderOperation.cs b/QuestPDF.Previewer/DocumentRenderOperation.cs new file mode 100644 index 0000000..6feeb13 --- /dev/null +++ b/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); + } + } + } +} diff --git a/QuestPDF.Previewer/PreviewerCanvas.cs b/QuestPDF.Previewer/PreviewerCanvas.cs new file mode 100644 index 0000000..f168aef --- /dev/null +++ b/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); + } + } +} diff --git a/QuestPDF.Previewer/PreviewerControl.cs b/QuestPDF.Previewer/PreviewerControl.cs new file mode 100644 index 0000000..cd9e4ac --- /dev/null +++ b/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 DocumentProperty = + AvaloniaProperty.Register(nameof(Document)); + + public IDocument? Document + { + get => GetValue(DocumentProperty); + set => SetValue(DocumentProperty, value); + } + + public static readonly StyledProperty IsGeneratingDocumentProperty = + AvaloniaProperty.Register(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); + } + } +} diff --git a/QuestPDF.Previewer/PreviewerView.axaml b/QuestPDF.Previewer/PreviewerView.axaml new file mode 100644 index 0000000..aa749fa --- /dev/null +++ b/QuestPDF.Previewer/PreviewerView.axaml @@ -0,0 +1,17 @@ + + + + + + + + + + + diff --git a/QuestPDF.Previewer/PreviewerView.axaml.cs b/QuestPDF.Previewer/PreviewerView.axaml.cs new file mode 100644 index 0000000..d20378d --- /dev/null +++ b/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 DocumentProperty = + AvaloniaProperty.Register(nameof(Document)); + + public IDocument? Document + { + get => GetValue(DocumentProperty); + set => SetValue(DocumentProperty, value); + } + + public PreviewerView() + { + InitializeComponent(); + + _previewHost = this.FindControl("PreviewerSurface"); + + DocumentProperty + .Changed + .Subscribe(v => _previewHost.Document = v.NewValue.Value); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public void InvalidatePreview() + { + _previewHost.InvalidateVisual(); + } + } +}