Pārlūkot izejas kodu

Redesign and optimize the process of collecting semantic tree

Marcin Ziąbek 3 mēneši atpakaļ
vecāks
revīzija
945f53083d

+ 0 - 1
Source/QuestPDF.DocumentationExamples/SemanticExamples.cs

@@ -72,7 +72,6 @@ public class SemanticExamples
                     
                     page.Content()
                         .PaddingVertical(24)
-                        .SemanticDocument()
                         .Column(column =>
                         {
                             foreach (var category1 in categories)

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/CompanionDocumentCanvas.cs

@@ -86,6 +86,11 @@ namespace QuestPDF.Drawing.DocumentCanvases
         #endregion
         
         #region IDocumentCanvas
+
+        public void SetSemanticTree(SemanticTreeNode semanticTree)
+        {
+            
+        }
         
         public void BeginDocument()
         {

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/FreeDocumentCanvas.cs

@@ -7,6 +7,11 @@ internal sealed class FreeDocumentCanvas : IDocumentCanvas
 {
     private FreeDrawingCanvas DrawingCanvas { get; } = new();
         
+    public void SetSemanticTree(SemanticTreeNode semanticTree)
+    {
+            
+    }
+    
     public void BeginDocument()
     {
             

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/ImageDocumentCanvas.cs

@@ -44,6 +44,11 @@ namespace QuestPDF.Drawing.DocumentCanvases
         
         #region IDocumentCanvas
         
+        public void SetSemanticTree(SemanticTreeNode semanticTree)
+        {
+            
+        }
+        
         public void BeginDocument()
         {
             

+ 44 - 20
Source/QuestPDF/Drawing/DocumentCanvases/PdfDocumentCanvas.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Diagnostics;
+using System.Linq;
 using QuestPDF.Drawing.DrawingCanvases;
 using QuestPDF.Drawing.Exceptions;
 using QuestPDF.Infrastructure;
@@ -9,27 +10,33 @@ namespace QuestPDF.Drawing.DocumentCanvases
 {
     internal sealed class PdfDocumentCanvas : IDocumentCanvas, IDisposable
     {
-        private SkDocument Document { get; }
+        private SkWriteStream WriteStream { get; }
+        private DocumentMetadata DocumentMetadata { get; }
+        private DocumentSettings DocumentSettings { get; }
+        private SkPdfTag? SemanticTag { get; set; }
+        
+        private SkDocument? Document { get; set; }
         private SkCanvas? CurrentPageCanvas { get; set; }
         private ProxyDrawingCanvas DrawingCanvas { get; } = new();
         
-        // TODO: is there a better way to pass semantic-related data? Skia requires it BEFORE content generation
-        public PdfDocumentCanvas(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings, SkPdfTag? pdfTag)
+        public PdfDocumentCanvas(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings)
         {
-            Document = CreatePdf(stream, documentMetadata, documentSettings, pdfTag);
+            WriteStream = stream;
+            DocumentMetadata = documentMetadata;
+            DocumentSettings = documentSettings;
         }
 
-        private static SkDocument CreatePdf(SkWriteStream stream, DocumentMetadata documentMetadata, DocumentSettings documentSettings, SkPdfTag? pdfTag)
+        private SkDocument CreatePdf()
         {
             // do not extract to another method, as it will cause the SkText objects
             // to be disposed before the SkPdfDocument is created
-            using var title = new SkText(documentMetadata.Title);
-            using var author = new SkText(documentMetadata.Author);
-            using var subject = new SkText(documentMetadata.Subject);
-            using var keywords = new SkText(documentMetadata.Keywords);
-            using var creator = new SkText(documentMetadata.Creator);
-            using var producer = new SkText(documentMetadata.Producer);
-            using var language = new SkText(documentMetadata.Language);
+            using var title = new SkText(DocumentMetadata.Title);
+            using var author = new SkText(DocumentMetadata.Author);
+            using var subject = new SkText(DocumentMetadata.Subject);
+            using var keywords = new SkText(DocumentMetadata.Keywords);
+            using var creator = new SkText(DocumentMetadata.Creator);
+            using var producer = new SkText(DocumentMetadata.Producer);
+            using var language = new SkText(DocumentMetadata.Language);
             
             var internalMetadata = new SkPdfDocumentMetadata
             {
@@ -41,19 +48,19 @@ namespace QuestPDF.Drawing.DocumentCanvases
                 Producer = producer,
                 Language = language,
                 
-                CreationDate = new SkDateTime(documentMetadata.CreationDate),
-                ModificationDate = new SkDateTime(documentMetadata.ModifiedDate),
+                CreationDate = new SkDateTime(DocumentMetadata.CreationDate),
+                ModificationDate = new SkDateTime(DocumentMetadata.ModifiedDate),
                 
-                RasterDPI = documentSettings.ImageRasterDpi,
-                SupportPDFA = documentSettings.PdfA,
-                CompressDocument = documentSettings.CompressDocument,
+                RasterDPI = DocumentSettings.ImageRasterDpi,
+                SupportPDFA = DocumentSettings.PdfA,
+                CompressDocument = DocumentSettings.CompressDocument,
                 
-                SemanticNodeRoot = pdfTag?.Instance ?? IntPtr.Zero
+                SemanticNodeRoot = SemanticTag?.Instance ?? IntPtr.Zero
             };
             
             try
             {
-                return SkPdfDocument.Create(stream, internalMetadata);
+                return SkPdfDocument.Create(WriteStream, internalMetadata);
             }
             catch (TypeInitializationException exception)
             {
@@ -75,6 +82,9 @@ namespace QuestPDF.Drawing.DocumentCanvases
             CurrentPageCanvas?.Dispose();
             DrawingCanvas?.Dispose();
             
+            // don't dispose WriteStream - its lifetime is managed externally
+            SemanticTag?.Dispose();
+            
             GC.SuppressFinalize(this);
         }
         
@@ -82,9 +92,23 @@ namespace QuestPDF.Drawing.DocumentCanvases
         
         #region IDocumentCanvas
         
-        public void BeginDocument()
+        public void SetSemanticTree(SemanticTreeNode semanticTree)
         {
+            SemanticTag = Convert(semanticTree);
             
+            static SkPdfTag Convert(SemanticTreeNode node)
+            {
+                var result = SkPdfTag.Create(node.NodeId, node.Type, node.Alt, node.Lang);
+                var children = node.Children.Select(Convert).ToArray();
+                result.SetChildren(children);
+                
+                return result;
+            }
+        }
+        
+        public void BeginDocument()
+        {
+            Document ??= CreatePdf();
         }
 
         public void EndDocument()

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/SvgDocumentCanvas.cs

@@ -40,6 +40,11 @@ namespace QuestPDF.Drawing.DocumentCanvases
         
         #region IDocumentCanvas
         
+        public void SetSemanticTree(SemanticTreeNode semanticTree)
+        {
+            
+        }
+        
         public void BeginDocument()
         {
             

+ 5 - 0
Source/QuestPDF/Drawing/DocumentCanvases/XpsDocumentCanvas.cs

@@ -51,6 +51,11 @@ namespace QuestPDF.Drawing.DocumentCanvases
         
         #region IDocumentCanvas
         
+        public void SetSemanticTree(SemanticTreeNode semanticTree)
+        {
+            
+        }
+        
         public void BeginDocument()
         {
             

+ 35 - 148
Source/QuestPDF/Drawing/DocumentGenerator.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
-using System.Threading;
 using QuestPDF.Companion;
 using QuestPDF.Drawing.DocumentCanvases;
 using QuestPDF.Drawing.Exceptions;
@@ -28,33 +27,9 @@ namespace QuestPDF.Drawing
             
             var metadata = document.GetMetadata();
             var settings = document.GetSettings();
-            
-            using var semanticTree = GetSemanticTree(document);
-            
-            using var canvas = new PdfDocumentCanvas(stream, metadata, settings, semanticTree);
-            RenderDocument(canvas, document, settings);
 
-            static SkPdfTag GetSemanticTree(IDocument document)
-            {
-                // TODO: optimize semantic tree generation
-                // this implementation creates entire document structure only for semantic tree generation,
-                // and then disposes everything, instead of reusing the same structure also for document rendering
-                
-                // TODO: better handle the Lazy element
-                // with this implementation, the entire document is materialized for semantic tree generation, and the Lazy element does not optimize the memory usage
-                // most likely, the Lazy element should be aware of semantic tree generation, or there should be an addition structure, e.g. SemanticLazy or even SemanticLazyProxy
-                
-                // TODO: can this operation be handled during FreeCanvas drawing?
-                
-                // TODO: does caching operation via SkPicture support setting semantic node id?
-                
-                var container = new DocumentContainer();
-                document.Compose(container);
-            
-                var content = container.Compose();
-                content.PopulateSemanticTagIds();
-                return content.ExtractStructuralInformation();
-            }
+            using var canvas = new PdfDocumentCanvas(stream, metadata, settings);
+            RenderDocument(canvas, document, settings);
         }
         
         internal static void GenerateXps(SkWriteStream stream, IDocument document)
@@ -122,35 +97,34 @@ namespace QuestPDF.Drawing
 
         private static void RenderDocument(IDocumentCanvas canvas, IDocument document, DocumentSettings settings)
         {
-            canvas.BeginDocument();
-        
-            if (document is MergedDocument mergedDocument)
-                RenderMergedDocument(canvas, mergedDocument, settings);
-        
-            else
-                RenderSingleDocument(canvas, document, settings);
-        
-            canvas.EndDocument();
-        }
-
-        private static void RenderSingleDocument(IDocumentCanvas canvas, IDocument document, DocumentSettings settings)
-        {
+            // TODO: handle MergedDocument
+            
             var useOriginalImages = canvas is ImageDocumentCanvas;
 
             var content = ConfigureContent(document, settings, useOriginalImages);
             
-            // TODO: this step may not be required if document structure is used for both: semantic tree generation and document rendering
-            content.PopulateSemanticTagIds();
-            
             if (canvas is CompanionDocumentCanvas)
                 content.VisitChildren(x => x.CreateProxy(y => new LayoutProxy(y)));
             
             try
             {
+                var semanticTreeManager = new SemanticTreeManager();
+                content.InjectSemanticTreeManager(semanticTreeManager);
+                
                 var pageContext = new PageContext();
                 RenderPass(pageContext, new FreeDocumentCanvas(), content);
                 pageContext.ProceedToNextRenderingPhase();
+                
+                var semanticTree = semanticTreeManager.GetSemanticTree();
+                
+                if (semanticTree != null)
+                    canvas.SetSemanticTree(semanticTree);
+                
+                semanticTreeManager.Reset();
+                
+                canvas.BeginDocument();
                 RenderPass(pageContext, canvas, content);
+                canvas.EndDocument();
             
                 if (canvas is CompanionDocumentCanvas companionCanvas)
                     companionCanvas.Hierarchy = content.ExtractHierarchy();
@@ -160,62 +134,6 @@ namespace QuestPDF.Drawing
                 content.ReleaseDisposableChildren();
             }
         }
-        
-        private static void RenderMergedDocument(IDocumentCanvas canvas, MergedDocument document, DocumentSettings settings)
-        {
-            var useOriginalImages = canvas is ImageDocumentCanvas;
-            
-            var documentParts = Enumerable
-                .Range(0, document.Documents.Count)
-                .Select(index => new
-                {
-                    DocumentId = index,
-                    Content = ConfigureContent(document.Documents[index], settings, useOriginalImages)
-                })
-                .ToList();
-            
-            try
-            {
-                if (document.PageNumberStrategy == MergedDocumentPageNumberStrategy.Continuous)
-                {
-                    var documentPageContext = new PageContext();
-
-                    foreach (var documentPart in documentParts)
-                    {
-                        documentPageContext.SetDocumentId(documentPart.DocumentId);
-                        RenderPass(documentPageContext, new FreeDocumentCanvas(), documentPart.Content);
-                    }
-                
-                    documentPageContext.ProceedToNextRenderingPhase();
-
-                    foreach (var documentPart in documentParts)
-                    {
-                        documentPageContext.SetDocumentId(documentPart.DocumentId);
-                        RenderPass(documentPageContext, canvas, documentPart.Content);
-                        documentPart.Content.ReleaseDisposableChildren();
-                    }
-                }
-                else
-                {
-                    foreach (var documentPart in documentParts)
-                    {
-                        var pageContext = new PageContext();
-                        pageContext.SetDocumentId(documentPart.DocumentId);
-                    
-                        RenderPass(pageContext, new FreeDocumentCanvas(), documentPart.Content);
-                        pageContext.ProceedToNextRenderingPhase();
-                        RenderPass(pageContext, canvas, documentPart.Content);
-                    
-                        documentPart.Content.ReleaseDisposableChildren();
-                    }
-                }
-            }
-            catch
-            {
-                documentParts.ForEach(x => x.Content.ReleaseDisposableChildren());
-                throw;
-            }
-        }
 
         private static Container ConfigureContent(IDocument document, DocumentSettings settings, bool useOriginalImages)
         {
@@ -358,6 +276,24 @@ namespace QuestPDF.Drawing
             }
         }
 
+        internal static void InjectSemanticTreeManager(this Element content, SemanticTreeManager semanticTreeManager)
+        {
+            content.VisitChildren(x =>
+            {
+                if (x == null)
+                    return;
+                
+                if (x is SemanticTag semanticTag)
+                    semanticTag.SemanticTreeManager = semanticTreeManager;
+                
+                else if (x is Lazy lazy)
+                    lazy.SemanticTreeManager = semanticTreeManager;
+                
+                else if (x is DynamicHost dynamicHost)
+                    dynamicHost.SemanticTreeManager = semanticTreeManager;
+            });
+        }
+        
         internal static void InjectDependencies(this Element content, IPageContext pageContext, IDrawingCanvas canvas)
         {
             content.VisitChildren(x =>
@@ -535,54 +471,5 @@ namespace QuestPDF.Drawing
                     ApplyInheritedAndGlobalTexStyle(child, documentDefaultTextStyle);
             }
         }
-
-        internal static void PopulateSemanticTagIds(this Element element)
-        {
-            var currentId = 1;
-            Traverse(element);  
-            
-            void Traverse(Element element)
-            {
-                if (element is SemanticTag { Id: 0 } semanticTag)
-                {
-                    semanticTag.Id = currentId;
-                    currentId++;
-                }
-
-                if (element is ContainerElement container)
-                {
-                    Traverse(container.Child);
-                    return;
-                }
-                
-                foreach (var child in element.GetChildren())
-                    Traverse(child);
-            }
-        }
-
-        internal static SkPdfTag ExtractStructuralInformation(this Element root)
-        {
-            return GetSkiaTagFor(root).First();
-
-            static IEnumerable<SkPdfTag> GetSkiaTagFor(Element element)
-            {
-                if (element is SemanticTag { Id: > 0 } semanticTag)
-                {
-                    var result = SkPdfTag.Create(semanticTag.Id, semanticTag.TagType, semanticTag.Alt, semanticTag.Lang);
-                    result.SetChildren(GetSkiaTagFor(semanticTag.Child).ToArray());
-                    yield return result;
-                }
-                else if (element is ContainerElement container)
-                {
-                    foreach (var child in GetSkiaTagFor(container.Child))
-                        yield return child;
-                }
-                else
-                {
-                    foreach (var child in element.GetChildren().SelectMany(GetSkiaTagFor))
-                        yield return child;
-                }
-            }
-        }
     }
 }

+ 59 - 0
Source/QuestPDF/Drawing/SemanticTreeManager.cs

@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+
+namespace QuestPDF.Drawing;
+
+class SemanticTreeNode
+{
+    public int NodeId { get; set; }
+    public string Type { get; set; } = "";
+    public string? Alt { get; set; }
+    public string? Lang { get; set; }
+    public ICollection<SemanticTreeNode> Children { get; } = [];
+}
+
+class SemanticTreeManager
+{
+    private int CurrentNodeId { get; set; }
+    private SemanticTreeNode? Root { get; set; }
+    private Stack<SemanticTreeNode> Stack { get; set; } = [];
+
+    public int GetNextNodeId()
+    {
+        CurrentNodeId++;
+        return CurrentNodeId;
+    }
+    
+    public void AddNode(SemanticTreeNode node)
+    {
+        if (Root == null)
+        {
+            Root = node;
+            Stack.Push(node);
+            return;
+        }
+        
+        Stack.Peek()?.Children.Add(node);
+    }
+    
+    public void PushOnStack(SemanticTreeNode node)
+    {
+        Stack.Push(node);
+    }
+    
+    public void PopStack()
+    {
+        Stack.Pop();
+    }
+    
+    public void Reset()
+    {
+        CurrentNodeId = 0;
+        Root = null;
+        Stack.Clear();
+    }
+
+    public SemanticTreeNode? GetSemanticTree()
+    {
+        return Root;
+    }
+}

+ 6 - 1
Source/QuestPDF/Elements/Dynamic.cs

@@ -9,6 +9,8 @@ namespace QuestPDF.Elements
 {
     internal sealed class DynamicHost : Element, IStateful, IContentDirectionAware
     {
+        internal SemanticTreeManager SemanticTreeManager { get; set; }
+        
         private DynamicComponentProxy Child { get; }
         private object InitialComponentState { get; set; }
 
@@ -66,6 +68,7 @@ namespace QuestPDF.Elements
             {
                 PageContext = PageContext,
                 Canvas = Canvas,
+                SemanticTreeManager = SemanticTreeManager,
                 
                 TextStyle = TextStyle,
                 ContentDirection = ContentDirection,
@@ -115,7 +118,8 @@ namespace QuestPDF.Elements
     {
         internal IPageContext PageContext { get; set; }
         internal IDrawingCanvas Canvas { get; set; }
-
+        internal SemanticTreeManager SemanticTreeManager { get; set; }
+        
         internal TextStyle TextStyle { get; set; }
         internal ContentDirection ContentDirection { get; set; }
 
@@ -171,6 +175,7 @@ namespace QuestPDF.Elements
             container.ApplyDefaultImageConfiguration(ImageTargetDpi, ImageCompressionQuality, UseOriginalImage);
             
             container.InjectDependencies(PageContext, Canvas);
+            container.InjectSemanticTreeManager(SemanticTreeManager);
             container.VisitChildren(x => (x as IStateful)?.ResetState());
 
             container.Size = container.Measure(Size.Max);

+ 3 - 0
Source/QuestPDF/Elements/Lazy.cs

@@ -8,6 +8,8 @@ namespace QuestPDF.Elements;
 
 internal sealed class Lazy : ContainerElement, IContentDirectionAware, IStateful
 {
+    internal SemanticTreeManager SemanticTreeManager { get; set; }
+    
     public Action<IContainer> ContentSource { get; set; }
     public bool IsCacheable { get; set; }
 
@@ -61,6 +63,7 @@ internal sealed class Lazy : ContainerElement, IContentDirectionAware, IStateful
         container.ApplyDefaultImageConfiguration(ImageTargetDpi.Value, ImageCompressionQuality.Value, UseOriginalImage);
             
         container.InjectDependencies(PageContext, Canvas);
+        container.InjectSemanticTreeManager(SemanticTreeManager);
         container.VisitChildren(x => (x as IStateful)?.ResetState());
     }
     

+ 20 - 8
Source/QuestPDF/Elements/SemanticTag.cs

@@ -9,20 +9,32 @@ namespace QuestPDF.Elements;
 internal class SemanticTag : ContainerElement
 {
     public SemanticTreeManager SemanticTreeManager { get; set; }
+    public SemanticTreeNode? SemanticTreeNode { get; set; }
 
-    public int Id { get; set; } = 0;
     public string TagType { get; set; }
     public string? Alt { get; set; }
     public string? Lang { get; set; }
-    
+
     internal override void Draw(Size availableSpace)
     {
-        if (Id == 0)
-            Id = SemanticTreeManager.GetNextNodeId();
-
-        SemanticTreeManager.AddNode(null);
-        Canvas.SetSemanticNodeId(Id);
+        if (SemanticTreeNode == null)
+        {
+            var id = SemanticTreeManager.GetNextNodeId();
+            
+            SemanticTreeNode = new SemanticTreeNode
+            {
+                NodeId = id,
+                Type = TagType,
+                Alt = Alt,
+                Lang = Lang
+            };
+            
+            SemanticTreeManager.AddNode(SemanticTreeNode);
+        }
+        
+        SemanticTreeManager.PushOnStack(SemanticTreeNode);
+        Canvas.SetSemanticNodeId(SemanticTreeNode.NodeId);
         Child?.Draw(availableSpace);
-        SemanticTreeManager.UndoNesting();
+        SemanticTreeManager.PopStack();
     }
 }

+ 6 - 1
Source/QuestPDF/Infrastructure/IDocumentCanvas.cs

@@ -1,12 +1,17 @@
-namespace QuestPDF.Infrastructure
+using QuestPDF.Drawing;
+
+namespace QuestPDF.Infrastructure
 {
     internal interface IDocumentCanvas
     {
+        void SetSemanticTree(SemanticTreeNode semanticTree);
+        
         void BeginDocument();
         void EndDocument();
         
         void BeginPage(Size size);
         void EndPage();
+        
         IDrawingCanvas GetDrawingCanvas();
     }
 }