Browse Source

Previewer: replaced the rendering system based on the SkPicture class and its serialization mechanism, with on-demand image rendering

Marcin Ziąbek 1 year ago
parent
commit
dbcf0a60e2

+ 24 - 10
Source/QuestPDF.Previewer/CommunicationService.cs

@@ -12,7 +12,9 @@ class CommunicationService
 {
 {
     public static CommunicationService Instance { get; } = new ();
     public static CommunicationService Instance { get; } = new ();
     
     
-    public event Action<DocumentSnapshot>? OnDocumentRefreshed;
+    public event Action<DocumentStructure>? OnDocumentUpdated;
+    public Func<ICollection<PageSnapshotIndex>>? OnPageSnapshotsRequested { get; set; }
+    public Action<ICollection<RenderedPageSnapshot>> OnPageSnapshotsProvided  { get; set; }
 
 
     private WebApplication? Application { get; set; }
     private WebApplication? Application { get; set; }
 
 
@@ -35,7 +37,9 @@ class CommunicationService
 
 
         Application.MapGet("ping", HandlePing);
         Application.MapGet("ping", HandlePing);
         Application.MapGet("version", HandleVersion);
         Application.MapGet("version", HandleVersion);
-        Application.MapPost("update/preview", HandleUpdatePreview);
+        Application.MapPost("preview/update", HandlePreviewRefresh);
+        Application.MapGet("preview/getRenderingRequests", HandleGetRequests);
+        Application.MapPost("preview/provideRenderedImages", HandleProvidedSnapshotImages);
             
             
         return Application.RunAsync($"http://localhost:{port}/");
         return Application.RunAsync($"http://localhost:{port}/");
     }
     }
@@ -48,7 +52,7 @@ class CommunicationService
 
 
     private async Task<IResult> HandlePing()
     private async Task<IResult> HandlePing()
     {
     {
-        return OnDocumentRefreshed == null 
+        return OnDocumentUpdated == null 
             ? Results.StatusCode(StatusCodes.Status503ServiceUnavailable) 
             ? Results.StatusCode(StatusCodes.Status503ServiceUnavailable) 
             : Results.Ok();
             : Results.Ok();
     }
     }
@@ -58,17 +62,27 @@ class CommunicationService
         return Results.Json(GetType().Assembly.GetName().Version);
         return Results.Json(GetType().Assembly.GetName().Version);
     }
     }
     
     
-    private async Task<IResult> HandleUpdatePreview(HttpRequest request)
+    private async Task HandlePreviewRefresh(DocumentStructure documentStructure)
     {
     {
-        var documentSnapshot = JsonSerializer.Deserialize<DocumentSnapshot>(request.Form["command"], JsonSerializerOptions);
+        Task.Run(() => OnDocumentUpdated(documentStructure));
+    }
+
+    private async Task<ICollection<PageSnapshotIndex>> HandleGetRequests()
+    {
+        return OnPageSnapshotsRequested();
+    }
+    
+    private async Task HandleProvidedSnapshotImages(HttpRequest request)
+    {
+        var renderedPages = JsonSerializer.Deserialize<ICollection<RenderedPageSnapshot>>(request.Form["metadata"], JsonSerializerOptions);
 
 
-        foreach (var pageSnapshot in documentSnapshot.Pages)
+        foreach (var renderedPage in renderedPages)
         {
         {
-            using var stream = request.Form.Files[pageSnapshot.Id].OpenReadStream();
-            pageSnapshot.Picture = SKPicture.Deserialize(stream);
+            using var memoryStream = new MemoryStream();
+            await request.Form.Files.GetFile(renderedPage.ToString()).CopyToAsync(memoryStream);
+            renderedPage.Image = SKImage.FromEncodedData(memoryStream.ToArray());
         }
         }
 
 
-        Task.Run(() => OnDocumentRefreshed(documentSnapshot));
-        return Results.Ok();
+        Task.Run(() => OnPageSnapshotsProvided(renderedPages));
     }
     }
 }
 }

+ 0 - 19
Source/QuestPDF.Previewer/DocumentSnapshot.cs

@@ -1,19 +0,0 @@
-using SkiaSharp;
-
-namespace QuestPDF.Previewer;
-
-internal sealed class DocumentSnapshot
-{
-    public bool DocumentContentHasLayoutOverflowIssues { get; set; }
-    public ICollection<PageSnapshot> Pages { get; set; }
-
-    public class PageSnapshot
-    {
-        public string Id { get; set; }
-        
-        public float Width { get; set; }
-        public float Height { get; set; }
-        
-        public SKPicture Picture { get; set; }
-    }
-}

+ 0 - 20
Source/QuestPDF.Previewer/Helpers.cs

@@ -1,20 +0,0 @@
-using SkiaSharp;
-
-namespace QuestPDF.Previewer;
-
-class Helpers
-{
-    public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<DocumentSnapshot.PageSnapshot> pages)
-    {
-        using var stream = File.Create(filePath);
-            
-        using var document = SKDocument.CreatePdf(stream);
-            
-        foreach (var page in pages)
-        {
-            using var canvas = document.BeginPage(page.Width, page.Height);
-            canvas.DrawPicture(page.Picture);
-            document.EndPage();
-        }
-    }
-}

+ 100 - 19
Source/QuestPDF.Previewer/InteractiveCanvas.cs

@@ -10,7 +10,10 @@ namespace QuestPDF.Previewer;
 class InteractiveCanvas : ICustomDrawOperation
 class InteractiveCanvas : ICustomDrawOperation
 {
 {
     public Rect Bounds { get; set; }
     public Rect Bounds { get; set; }
-    public ICollection<DocumentSnapshot.PageSnapshot> Pages { get; set; }
+    public float RenderingScale { get; set; }
+
+    private List<DocumentStructure.PageSize> PageSizes { get; set; } = new();
+    private List<RenderedPageSnapshot> PageSnapshotCache { get; set; } = new();
 
 
     private float Width => (float)Bounds.Width;
     private float Width => (float)Bounds.Width;
     private float Height => (float)Bounds.Height;
     private float Height => (float)Bounds.Height;
@@ -19,15 +22,15 @@ class InteractiveCanvas : ICustomDrawOperation
     public float TranslateX { get; set; }
     public float TranslateX { get; set; }
     public float TranslateY { get; set; }
     public float TranslateY { get; set; }
 
 
-    private const float MinScale = 0.1f;
-    private const float MaxScale = 10f;
+    private const float MinScale = 1 / 8f;
+    private const float MaxScale = 8f;
 
 
     private const float PageSpacing = 25f;
     private const float PageSpacing = 25f;
     private const float SafeZone = 25f;
     private const float SafeZone = 25f;
 
 
-    public float TotalPagesHeight => Pages.Sum(x => x.Height) + (Pages.Count - 1) * PageSpacing;
+    public float TotalPagesHeight => PageSizes.Sum(x => x.Height) + (PageSizes.Count - 1) * PageSpacing;
     public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
     public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
-    public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
+    public float MaxWidth => PageSizes.Any() ? PageSizes.Max(x => x.Width) : 0;
     
     
     public float MaxTranslateY => TotalHeight - Height / Scale;
     public float MaxTranslateY => TotalHeight - Height / Scale;
 
 
@@ -52,6 +55,40 @@ class InteractiveCanvas : ICustomDrawOperation
         }
         }
     }
     }
 
 
+    #region interaction
+
+    public void SetNewDocumentStructure(DocumentStructure document)
+    {
+        foreach (var renderedSnapshot in PageSnapshotCache)
+            renderedSnapshot.Image.Dispose();
+        
+        PageSnapshotCache.Clear();
+        PageSizes = document.Pages.ToList();
+    }
+    
+    public ICollection<PageSnapshotIndex> GetMissingSnapshots()
+    {
+        var requiredKeys = GetVisiblePages(padding: 500).Select(x => (x.pageIndex, PreferredZoomLevel)).ToList();
+        var availableKeys = PageSnapshotCache.Select(x => (x.PageIndex, x.ZoomLevel)).ToList();
+
+        var missingKeys = requiredKeys.Except(availableKeys).ToArray();
+
+        return missingKeys
+            .Select(x => new PageSnapshotIndex
+            {
+                PageIndex = x.Item1,
+                ZoomLevel = x.Item2
+            })
+            .ToList();
+    }
+
+    public void AddSnapshots(ICollection<RenderedPageSnapshot> snapshots)
+    {
+        PageSnapshotCache.AddRange(snapshots);
+    }
+    
+    #endregion
+    
     #region transformations
     #region transformations
     
     
     private void LimitScale()
     private void LimitScale()
@@ -109,6 +146,31 @@ class InteractiveCanvas : ICustomDrawOperation
     #endregion
     #endregion
     
     
     #region rendering
     #region rendering
+
+    private int PreferredZoomLevel => (int)Math.Clamp(Math.Ceiling(Math.Log2(Scale * RenderingScale)), -2, 2);
+    
+    private IEnumerable<(int pageIndex, float verticalOffset)> GetVisiblePages(float padding = 100)
+    {
+        padding /= Scale;
+        
+        var visibleOffsetFrom = TranslateY - padding;
+        var visibleOffsetTo = TranslateY + Height / Scale + padding;
+        
+        var topOffset = 0f;
+
+        foreach (var pageIndex in Enumerable.Range(0, PageSizes.Count))
+        {
+            var page = PageSizes.ElementAt(pageIndex);
+            
+            if (topOffset + page.Height > visibleOffsetFrom)
+                yield return (pageIndex, topOffset);
+            
+            topOffset += page.Height + PageSpacing;
+            
+            if (topOffset > visibleOffsetTo)
+                yield break;
+        }
+    }
     
     
     public void Render(ImmediateDrawingContext context)
     public void Render(ImmediateDrawingContext context)
     {
     {
@@ -122,14 +184,11 @@ class InteractiveCanvas : ICustomDrawOperation
             return;
             return;
         
         
         // draw document
         // draw document
-        if (Pages.Count <= 0)
+        if (PageSizes.Count <= 0)
             return;
             return;
-
+  
         LimitScale();
         LimitScale();
         LimitTranslate();
         LimitTranslate();
-    
-        if (canvas == null)
-            throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
 
 
         var originalMatrix = canvas.TotalMatrix;
         var originalMatrix = canvas.TotalMatrix;
 
 
@@ -137,24 +196,46 @@ class InteractiveCanvas : ICustomDrawOperation
         
         
         canvas.Scale(Scale);
         canvas.Scale(Scale);
         canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
         canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
-        
-        foreach (var page in Pages)
+
+        foreach (var (pageIndex, offset) in GetVisiblePages())
         {
         {
-            canvas.Translate(-page.Width / 2f, 0);
-            DrawBlankPage(canvas, page.Width, page.Height);
-            DrawPageSnapshot(canvas, page);
-            canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
+            var pageSize = PageSizes.ElementAt(pageIndex);
+            
+            canvas.Save();
+            canvas.Translate(-pageSize.Width / 2f, offset);
+            DrawBlankPage(canvas, pageSize.Width, pageSize.Height);
+            DrawPageSnapshot(canvas, pageIndex);
+            canvas.Restore();
         }
         }
 
 
         canvas.SetMatrix(originalMatrix);
         canvas.SetMatrix(originalMatrix);
         DrawInnerGradient(canvas);
         DrawInnerGradient(canvas);
     }
     }
     
     
-    private static void DrawPageSnapshot(SKCanvas canvas, DocumentSnapshot.PageSnapshot pageSnapshot)
+    private void DrawPageSnapshot(SKCanvas canvas, int pageIndex)
     {
     {
+        var page = PageSizes.ElementAt(pageIndex);
+
+        var renderedSnapshot = PageSnapshotCache
+            .Where(x => x.PageIndex == pageIndex)
+            .OrderBy(x => Math.Abs(PreferredZoomLevel - x.ZoomLevel))
+            .ThenByDescending(x => x.ZoomLevel)
+            .FirstOrDefault();
+        
+        if (renderedSnapshot == null)
+            return;
+        
+        using var drawImagePaint = new SKPaint
+        {
+            FilterQuality = SKFilterQuality.High
+        };
+
+        var renderingScale = (float)Math.Pow(2, renderedSnapshot.ZoomLevel);
+        
         canvas.Save();
         canvas.Save();
-        canvas.ClipRect(new SKRect(0, 0, pageSnapshot.Width, pageSnapshot.Height));
-        canvas.DrawPicture(pageSnapshot.Picture);
+        canvas.ClipRect(new SKRect(0, 0, page.Width, page.Height));
+        canvas.Scale(1 / renderingScale);
+        canvas.DrawImage(renderedSnapshot.Image, SKPoint.Empty, drawImagePaint);
         canvas.Restore();
         canvas.Restore();
     }
     }
     
     

+ 28 - 0
Source/QuestPDF.Previewer/Models.cs

@@ -0,0 +1,28 @@
+using SkiaSharp;
+
+namespace QuestPDF.Previewer;
+
+class DocumentStructure
+{
+    public bool DocumentContentHasLayoutOverflowIssues { get; set; }
+    public ICollection<PageSize> Pages { get; set; }
+    
+    public class PageSize
+    {
+        public float Width { get; set; }
+        public float Height { get; set; }
+    }
+}
+
+class PageSnapshotIndex
+{
+    public int PageIndex { get; set; }
+    public int ZoomLevel { get; set; }
+
+    public override string ToString() => $"{ZoomLevel}/{PageIndex}";
+}
+
+class RenderedPageSnapshot : PageSnapshotIndex
+{
+    public SKImage Image { get; set; }
+}

+ 15 - 15
Source/QuestPDF.Previewer/PreviewerControl.cs

@@ -9,16 +9,7 @@ namespace QuestPDF.Previewer
 {
 {
     class PreviewerControl : Control
     class PreviewerControl : Control
     {
     {
-        private InteractiveCanvas InteractiveCanvas { get; set; } = new ();
-        
-        public static readonly StyledProperty<ObservableCollection<DocumentSnapshot.PageSnapshot>> PagesProperty =
-            AvaloniaProperty.Register<PreviewerControl, ObservableCollection<DocumentSnapshot.PageSnapshot>>(nameof(Pages));
-        
-        public ObservableCollection<DocumentSnapshot.PageSnapshot>? Pages
-        {
-            get => GetValue(PagesProperty);
-            set => SetValue(PagesProperty, value);
-        }
+        private InteractiveCanvas InteractiveCanvas { get; set; } = new();
 
 
         public static readonly StyledProperty<float> CurrentScrollProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(CurrentScroll));
         public static readonly StyledProperty<float> CurrentScrollProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(CurrentScroll));
         
         
@@ -38,11 +29,19 @@ namespace QuestPDF.Previewer
         
         
         public PreviewerControl()
         public PreviewerControl()
         {
         {
-            PagesProperty.Changed.Subscribe(x =>
+            CommunicationService.Instance.OnDocumentUpdated += document =>
             {
             {
-                InteractiveCanvas.Pages = x.NewValue.Value;
-                InvalidateVisual();
-            });
+                InteractiveCanvas.SetNewDocumentStructure(document);
+                Dispatcher.UIThread.InvokeAsync(InvalidateVisual).GetTask();
+            };
+            
+            CommunicationService.Instance.OnPageSnapshotsRequested += InteractiveCanvas.GetMissingSnapshots;
+            
+            CommunicationService.Instance.OnPageSnapshotsProvided += snapshots =>
+            {
+                InteractiveCanvas.AddSnapshots(snapshots);
+                Dispatcher.UIThread.InvokeAsync(InvalidateVisual).GetTask();
+            };
 
 
             CurrentScrollProperty.Changed.Subscribe(x =>
             CurrentScrollProperty.Changed.Subscribe(x =>
             {
             {
@@ -109,7 +108,8 @@ namespace QuestPDF.Previewer
         {
         {
             CurrentScroll = InteractiveCanvas.ScrollPercentY;
             CurrentScroll = InteractiveCanvas.ScrollPercentY;
             ScrollViewportSize = InteractiveCanvas.ScrollViewportSizeY;
             ScrollViewportSize = InteractiveCanvas.ScrollViewportSizeY;
-    
+
+            InteractiveCanvas.RenderingScale = (float)VisualRoot.RenderScaling;
             InteractiveCanvas.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
             InteractiveCanvas.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
 
 
             context.Custom(InteractiveCanvas);
             context.Custom(InteractiveCanvas);

+ 1 - 2
Source/QuestPDF.Previewer/PreviewerWindow.axaml

@@ -49,8 +49,7 @@
 			                            HorizontalAlignment="Stretch" 
 			                            HorizontalAlignment="Stretch" 
 			                            VerticalAlignment="Stretch"
 			                            VerticalAlignment="Stretch"
 			                            CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
 			                            CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
-			                            ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
-			                            Pages="{Binding Pages, Mode=OneWay}" />
+			                            ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}" />
 			
 			
 			<Border IsVisible="{Binding DocumentContentHasLayoutOverflowIssues}" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" HorizontalAlignment="Left" Padding="16,8" Background="#F44336" CornerRadius="8" BoxShadow="0 0 8 0 #44000000" Margin="32">
 			<Border IsVisible="{Binding DocumentContentHasLayoutOverflowIssues}" Grid.Row="1" Grid.Column="0" VerticalAlignment="Top" HorizontalAlignment="Left" Padding="16,8" Background="#F44336" CornerRadius="8" BoxShadow="0 0 8 0 #44000000" Margin="32">
 				<StackPanel Orientation="Horizontal" Spacing="8">
 				<StackPanel Orientation="Horizontal" Spacing="8">

+ 1 - 37
Source/QuestPDF.Previewer/PreviewerWindowViewModel.cs

@@ -2,19 +2,11 @@
 using System.Diagnostics;
 using System.Diagnostics;
 using Avalonia.Threading;
 using Avalonia.Threading;
 using ReactiveUI;
 using ReactiveUI;
-using Unit = System.Reactive.Unit;
 
 
 namespace QuestPDF.Previewer
 namespace QuestPDF.Previewer
 {
 {
     internal sealed class PreviewerWindowViewModel : ReactiveObject
     internal sealed class PreviewerWindowViewModel : ReactiveObject
     {
     {
-        private ObservableCollection<DocumentSnapshot.PageSnapshot> _pages = new();
-        public ObservableCollection<DocumentSnapshot.PageSnapshot> Pages
-        {
-            get => _pages;
-            set => this.RaiseAndSetIfChanged(ref _pages, value);
-        }
-        
         private bool _documentContentHasLayoutOverflowIssues;
         private bool _documentContentHasLayoutOverflowIssues;
         public bool DocumentContentHasLayoutOverflowIssues
         public bool DocumentContentHasLayoutOverflowIssues
         {
         {
@@ -47,25 +39,9 @@ namespace QuestPDF.Previewer
             private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _verticalScrollbarVisible, value));
             private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _verticalScrollbarVisible, value));
         }
         }
 
 
-        public ReactiveCommand<Unit, Unit> ShowPdfCommand { get; }
-        public ReactiveCommand<Unit, Unit> ShowDocumentationCommand { get; }
-        public ReactiveCommand<Unit, Unit> SponsorProjectCommand { get; }
-
         public PreviewerWindowViewModel()
         public PreviewerWindowViewModel()
         {
         {
-            CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview;
-            
-            ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
-            ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/api-reference/index.html"));
-            SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
-        }
-
-        private void ShowPdf()
-        {
-            var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.pdf");
-            Helpers.GeneratePdfFromDocumentSnapshots(filePath, Pages);
-
-            OpenLink(filePath);
+            CommunicationService.Instance.OnDocumentUpdated += x => DocumentContentHasLayoutOverflowIssues = x.DocumentContentHasLayoutOverflowIssues;
         }
         }
         
         
         private static void OpenLink(string path)
         private static void OpenLink(string path)
@@ -81,17 +57,5 @@ namespace QuestPDF.Previewer
 
 
             openBrowserProcess.Start();
             openBrowserProcess.Start();
         }
         }
-        
-        private void HandleUpdatePreview(DocumentSnapshot documentSnapshot)
-        {
-            var oldPages = Pages;
-            
-            Pages.Clear();
-            Pages = new ObservableCollection<DocumentSnapshot.PageSnapshot>(documentSnapshot.Pages);
-            DocumentContentHasLayoutOverflowIssues = documentSnapshot.DocumentContentHasLayoutOverflowIssues;
-            
-            foreach (var page in oldPages)
-                page.Picture.Dispose();
-        }
     }
     }
 }
 }

+ 1 - 1
Source/QuestPDF.ReportSample/Helpers.cs

@@ -10,7 +10,7 @@ namespace QuestPDF.ReportSample
 {
 {
     public static class Helpers
     public static class Helpers
     {
     {
-        public static Random Random { get; } = new Random(1);
+        public static Random Random { get; } = new Random();
         
         
         public static string GetTestItem(string path) => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", path);
         public static string GetTestItem(string path) => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Resources", path);
 
 

+ 13 - 1
Source/QuestPDF/Drawing/PreviewerCanvas.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
 using QuestPDF.Skia;
 
 
@@ -14,6 +15,17 @@ namespace QuestPDF.Drawing
             Picture = picture;
             Picture = picture;
             Size = size;
             Size = size;
         }
         }
+        
+        public byte[] RenderImage(int zoomLevel)
+        {
+            var scale = (float)Math.Pow(2, zoomLevel);
+            
+            using var bitmap = new SkBitmap((int)(Size.Width * scale), (int)(Size.Height * scale));
+            using var canvas = SkCanvas.CreateFromBitmap(bitmap);
+            canvas.Scale(scale, scale);
+            canvas.DrawPicture(Picture);
+            return bitmap.EncodeAsJpeg(90).ToSpan().ToArray();
+        }
     }
     }
     
     
     internal class PreviewerDocumentSnapshot
     internal class PreviewerDocumentSnapshot

+ 2 - 0
Source/QuestPDF/Previewer/PreviewerExtensions.cs

@@ -1,6 +1,7 @@
 using System;
 using System;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using QuestPDF.Drawing;
 using QuestPDF.Infrastructure;
 using QuestPDF.Infrastructure;
 
 
 namespace QuestPDF.Previewer
 namespace QuestPDF.Previewer
@@ -24,6 +25,7 @@ namespace QuestPDF.Previewer
             previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
             previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
 
 
             await previewerService.Connect();
             await previewerService.Connect();
+            previewerService.StartRenderRequestedPageSnapshotsTask(cancellationToken);
             await RefreshPreview();
             await RefreshPreview();
             
             
             HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();
             HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();

+ 28 - 0
Source/QuestPDF/Previewer/PreviewerModels.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+
+namespace QuestPDF.Previewer;
+
+#if NET6_0_OR_GREATER
+
+class DocumentStructure
+{
+    public bool DocumentContentHasLayoutOverflowIssues { get; set; }
+    public ICollection<PageSize> Pages { get; set; }
+    
+    public class PageSize
+    {
+        public float Width { get; set; }
+        public float Height { get; set; }
+    }
+}
+
+class PageSnapshotIndex
+{
+    public int PageIndex { get; set; }
+    public int ZoomLevel { get; set; }
+
+    public override string ToString() => $"{ZoomLevel}/{PageIndex}";
+}
+
+#endif

+ 0 - 23
Source/QuestPDF/Previewer/PreviewerRefreshCommand.cs

@@ -1,23 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-#if NET6_0_OR_GREATER
-
-namespace QuestPDF.Previewer
-{
-    internal class PreviewerRefreshCommand
-    {
-        public bool DocumentContentHasLayoutOverflowIssues { get; set; }
-        public ICollection<Page> Pages { get; set; }
-
-        public class Page
-        {
-            public string Id { get; } = Guid.NewGuid().ToString("N");
-            
-            public float Width { get; init; }
-            public float Height { get; init; }
-        }
-    }
-}
-
-#endif

+ 71 - 23
Source/QuestPDF/Previewer/PreviewerService.cs

@@ -3,6 +3,7 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics;
+using System.Linq;
 using System.Net.Http;
 using System.Net.Http;
 using System.Net.Http.Json;
 using System.Net.Http.Json;
 using System.Threading;
 using System.Threading;
@@ -16,11 +17,13 @@ namespace QuestPDF.Previewer
         private int Port { get; }
         private int Port { get; }
         private HttpClient HttpClient { get; }
         private HttpClient HttpClient { get; }
         
         
-        public  event Action? OnPreviewerStopped;
+        public event Action? OnPreviewerStopped;
 
 
         private const int RequiredPreviewerVersionMajor = 2023;
         private const int RequiredPreviewerVersionMajor = 2023;
         private const int RequiredPreviewerVersionMinor = 12;
         private const int RequiredPreviewerVersionMinor = 12;
         
         
+        private static PreviewerDocumentSnapshot? CurrentDocumentSnapshot { get; set; }
+        
         public PreviewerService(int port)
         public PreviewerService(int port)
         {
         {
             Port = port;
             Port = port;
@@ -133,36 +136,81 @@ namespace QuestPDF.Previewer
         
         
         public async Task RefreshPreview(PreviewerDocumentSnapshot previewerDocumentSnapshot)
         public async Task RefreshPreview(PreviewerDocumentSnapshot previewerDocumentSnapshot)
         {
         {
-            using var multipartContent = new MultipartFormDataContent();
-
-            var pages = new List<PreviewerRefreshCommand.Page>();
-            
-            foreach (var picture in previewerDocumentSnapshot.Pictures)
+            // clean old state
+            if (CurrentDocumentSnapshot != null)
             {
             {
-                var page = new PreviewerRefreshCommand.Page
-                {
-                    Width = picture.Size.Width,
-                    Height = picture.Size.Height
-                };
-                
-                pages.Add(page);
-
-                var pictureStream = picture.Picture.Serialize().AsStream();
-                multipartContent.Add(new StreamContent(pictureStream), page.Id, page.Id);
+                foreach (var previewerPageSnapshot in CurrentDocumentSnapshot.Pictures)
+                    previewerPageSnapshot.Picture.Dispose();
             }
             }
-
-            var command = new PreviewerRefreshCommand
+            
+            // set new state
+            CurrentDocumentSnapshot = previewerDocumentSnapshot;
+            
+            var documentStructure = new DocumentStructure
             {
             {
                 DocumentContentHasLayoutOverflowIssues = previewerDocumentSnapshot.DocumentContentHasLayoutOverflowIssues,
                 DocumentContentHasLayoutOverflowIssues = previewerDocumentSnapshot.DocumentContentHasLayoutOverflowIssues,
-                Pages = pages
+                
+                Pages = previewerDocumentSnapshot
+                    .Pictures
+                    .Select(x => new DocumentStructure.PageSize()
+                    {
+                        Width = x.Size.Width,
+                        Height = x.Size.Height
+                    })
+                    .ToArray()
             };
             };
             
             
-            multipartContent.Add(JsonContent.Create(command), "command");
+            await HttpClient.PostAsync("/preview/update", JsonContent.Create(documentStructure));
+        }
+        
+        public void StartRenderRequestedPageSnapshotsTask(CancellationToken cancellationToken)
+        {
+            Task.Run(async () =>
+            {
+                while (true)
+                {
+                    var sw = new Stopwatch();
+                    sw.Start();
+                    await RenderRequestedPageSnapshots();
+                    sw.Stop();
+                    await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
+                }
+            });
+        }
+        
+        private async Task RenderRequestedPageSnapshots()
+        {
+            // get requests
+            var getRequestedSnapshots = await HttpClient.GetAsync("/preview/getRenderingRequests");
+            var requestedSnapshots = await getRequestedSnapshots.Content.ReadFromJsonAsync<ICollection<PageSnapshotIndex>>();
+            
+            if (!requestedSnapshots.Any())
+                return;
+      
+            // render snapshots
+            using var multipartContent = new MultipartFormDataContent();
+
+            var renderingTasks = requestedSnapshots
+                .Select(async index =>
+                {
+                    var image = CurrentDocumentSnapshot
+                        .Pictures
+                        .ElementAt(index.PageIndex)
+                        .RenderImage(index.ZoomLevel);
+
+                    return (index, image);
+                })
+                .ToList();
+
+            var renderedSnapshots = await Task.WhenAll(renderingTasks);
+            
+            // prepare response and send
+            foreach (var (index, image) in renderedSnapshots)
+                multipartContent.Add(new ByteArrayContent(image), index.ToString(), index.ToString());
 
 
-            using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
+            multipartContent.Add(JsonContent.Create(requestedSnapshots), "metadata");
 
 
-            foreach (var picture in previewerDocumentSnapshot.Pictures)
-                picture.Picture.Dispose();
+            await HttpClient.PostAsync("/preview/provideRenderedImages", multipartContent);
         }
         }
     }
     }
 }
 }