Browse Source

Feature: implemented caching mechanism based on recording canvas commands via SkPictureRecorder and storing them as SkPicture (#932)

Marcin Ziąbek 1 year ago
parent
commit
6399c5179a

+ 76 - 0
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -176,6 +176,9 @@ namespace QuestPDF.Drawing
             content.ApplyInheritedAndGlobalTexStyle(TextStyle.Default);
             content.ApplyInheritedAndGlobalTexStyle(TextStyle.Default);
             content.ApplyContentDirection(settings.ContentDirection);
             content.ApplyContentDirection(settings.ContentDirection);
             content.ApplyDefaultImageConfiguration(settings.ImageRasterDpi, settings.ImageCompressionQuality, useOriginalImages);
             content.ApplyDefaultImageConfiguration(settings.ImageRasterDpi, settings.ImageCompressionQuality, useOriginalImages);
+
+            if (Settings.EnableCaching)
+                content.ApplyCaching();
             
             
             return content;
             return content;
         }
         }
@@ -306,6 +309,79 @@ namespace QuestPDF.Drawing
                 x.Canvas = canvas;
                 x.Canvas = canvas;
             });
             });
         }
         }
+        
+        internal static void ApplyCaching(this Element? content)
+        {
+            Traverse(content);
+
+            // returns true if can apply caching
+            bool Traverse(Element? content)
+            {
+                if (content is TextBlock textBlock)
+                {
+                    foreach (var textBlockItem in textBlock.Items)
+                    {
+                        if (textBlockItem is TextBlockPageNumber)
+                            return false;
+                        
+                        if (textBlockItem is TextBlockElement textBlockElement)
+                            return Traverse(textBlockElement.Element);
+                    }
+
+                    return true;
+                }
+
+                if (content is DynamicHost)
+                    return false;
+                
+                if (content is ContainerElement containerElement)
+                    return Traverse(containerElement.Child);
+
+                var canApplyCachingPerChild = content.GetChildren().Select(Traverse).ToArray();
+                
+                if (canApplyCachingPerChild.All(x => x))
+                    return true;
+
+                if (content is Row row && row.Items.Any(x => x.Type == RowItemType.Auto))
+                    return false;
+
+                var childIndex = 0;
+                
+                content.CreateProxy(x =>
+                {
+                    var canApplyCaching = canApplyCachingPerChild[childIndex];
+                    childIndex++;
+
+                    return canApplyCaching ? new SnapshotRecorder(x) : x;
+                });
+                
+                return false;
+            }
+        }
+        
+        internal static bool ApplyCaching2(this Element? content)
+        {
+            const int threshold = 10_000;
+
+            var counter = 0;
+            Traverse(content);
+            return counter > threshold;
+            
+            void Traverse(Element? content)
+            {
+                if (counter > threshold)
+                    return;
+                
+                if (content is TextBlock)
+                    counter++;
+                
+                if (content is ContainerElement containerElement)
+                    Traverse(containerElement.Child);
+
+                foreach (var element in content.GetChildren())
+                    Traverse(element);
+            }
+        }
 
 
         internal static void ApplyContentDirection(this Element? content, ContentDirection? direction = null)
         internal static void ApplyContentDirection(this Element? content, ContentDirection? direction = null)
         {
         {

+ 77 - 0
Source/QuestPDF/Drawing/Proxy/SnapshotRecorder.cs

@@ -0,0 +1,77 @@
+using System;
+using System.Collections.Generic;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+using QuestPDF.Skia;
+
+namespace QuestPDF.Drawing.Proxy;
+
+internal class SnapshotRecorder : ElementProxy
+{
+    SnapshotRecorderCanvas RecorderCanvas { get; } = new();
+    SkPictureRecorder PictureRecorder { get; } = new();
+    Dictionary<(int pageNumber, float availableWidth, float availableHeight), SpacePlan> MeasureCache { get; } = new();
+    Dictionary<int, SkPicture> DrawCache { get; } = new();
+
+    ~SnapshotRecorder()
+    {
+        PictureRecorder.Dispose();
+        
+        foreach (var cacheValue in DrawCache.Values)
+            cacheValue.Dispose();
+    }
+    
+    public SnapshotRecorder(Element child)
+    {
+        Child = child;
+    }
+    
+    private void Initialize()
+    {
+        if (Child.Canvas is SnapshotRecorderCanvas)
+            return;
+        
+        Child.VisitChildren(x => x.Canvas = RecorderCanvas);
+    }
+    
+    internal override SpacePlan Measure(Size availableSpace)
+    {
+        Initialize();
+
+        var cacheItem = (PageContext.CurrentPage, availableSpace.Width, availableSpace.Height);
+        
+        if (MeasureCache.TryGetValue(cacheItem, out var measurement))
+            return measurement;
+        
+        var result = base.Measure(availableSpace);
+        MeasureCache[cacheItem] = result;
+        return result;
+    }
+        
+    internal override void Draw(Size availableSpace)
+    {
+        // element may overflow the available space
+        // capture as much as possible around the origin point
+        var cachePictureSize = Size.Max;
+        var cachePictureOffset = new Position(cachePictureSize.Width / 2, cachePictureSize.Height / 2);
+        
+        if (DrawCache.TryGetValue(PageContext.CurrentPage, out var snapshot))
+        {
+            Canvas.Translate(cachePictureOffset.Reverse());
+            Canvas.DrawPicture(snapshot);
+            Canvas.Translate(cachePictureOffset);
+            
+            snapshot.Dispose();
+            return;
+        }
+        
+        using var canvas = PictureRecorder.BeginRecording(Size.Max.Width, Size.Max.Height);
+        RecorderCanvas.Canvas = canvas;
+
+        RecorderCanvas.Translate(cachePictureOffset);
+        base.Draw(availableSpace);
+        
+        DrawCache[PageContext.CurrentPage] = PictureRecorder.EndRecording();
+        RecorderCanvas.Canvas = null;
+    }
+}

+ 14 - 0
Source/QuestPDF/Drawing/SnapshotRecorderCanvas.cs

@@ -0,0 +1,14 @@
+namespace QuestPDF.Drawing;
+
+internal class SnapshotRecorderCanvas : SkiaCanvasBase
+{
+    public override void BeginDocument()
+    {
+        
+    }
+
+    public override void EndDocument()
+    {
+        
+    }
+}