WIP Implemented virtualization of pages.

Render pages into seperate SKPictures
This commit is contained in:
Bebo-Maker
2022-03-19 17:26:58 +01:00
parent 6cca3ed057
commit ea2102fe4c
7 changed files with 181 additions and 165 deletions
+86 -19
View File
@@ -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
View File
@@ -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);
}
}
}
+18 -25
View File
@@ -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 EndPage()
{
var picture = _currentRecorder?.EndRecording();
if (picture != null && _currentSize.HasValue)
_pictures.Add(new RenderedPageInfo(picture, _currentSize.Value));
_currentRecorder?.Dispose();
_currentRecorder = null;
}
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);
}
}
}
-96
View File
@@ -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);
}
}
}
@@ -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
View File
@@ -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
View File
@@ -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));
});
}
}
}