Browse Source

Feature: capture element position

Marcin Ziąbek 8 months ago
parent
commit
ab125a660a

+ 36 - 36
Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternComponentProgressbarComponentExample.cs

@@ -61,50 +61,50 @@ public class CodePatternComponentProgressbarComponentExample
             })
             })
             .GeneratePdf();
             .GeneratePdf();
     }
     }
-}
-
-public class PageProgressbarComponent : IDynamicComponent
-{
-    public DynamicComponentComposeResult Compose(DynamicContext context)
+    
+    public class PageProgressbarComponent : IDynamicComponent
     {
     {
-        var content = context.CreateElement(element =>
+        public DynamicComponentComposeResult Compose(DynamicContext context)
         {
         {
-            var width = context.AvailableSize.Width * context.PageNumber / context.TotalPages;
+            var content = context.CreateElement(element =>
+            {
+                var width = context.AvailableSize.Width * context.PageNumber / context.TotalPages;
                 
                 
-            element
-                .Background(Colors.Blue.Lighten3)
-                .Height(5)
-                .Width(width)
-                .Background(Colors.Blue.Darken2);
-        });
+                element
+                    .Background(Colors.Blue.Lighten3)
+                    .Height(5)
+                    .Width(width)
+                    .Background(Colors.Blue.Darken2);
+            });
 
 
-        return new DynamicComponentComposeResult
-        {
-            Content = content,
-            HasMoreContent = false
-        };
+            return new DynamicComponentComposeResult
+            {
+                Content = content,
+                HasMoreContent = false
+            };
+        }
     }
     }
-}
 
 
-public class PageNumberSideComponent : IDynamicComponent
-{
-    public DynamicComponentComposeResult Compose(DynamicContext context)
+    public class PageNumberSideComponent : IDynamicComponent
     {
     {
-        var content = context.CreateElement(element =>
+        public DynamicComponentComposeResult Compose(DynamicContext context)
         {
         {
-            element
-                .Element(x => context.PageNumber % 2 == 0 ? x.AlignRight() : x.AlignLeft())
-                .Text(text =>
-                {
-                    text.Span("Page ");
-                    text.CurrentPageNumber();
-                });
-        });
+            var content = context.CreateElement(element =>
+            {
+                element
+                    .Element(x => context.PageNumber % 2 == 0 ? x.AlignRight() : x.AlignLeft())
+                    .Text(text =>
+                    {
+                        text.Span("Page ");
+                        text.CurrentPageNumber();
+                    });
+            });
 
 
-        return new DynamicComponentComposeResult
-        {
-            Content = content,
-            HasMoreContent = false
-        };
+            return new DynamicComponentComposeResult
+            {
+                Content = content,
+                HasMoreContent = false
+            };
+        }
     }
     }
 }
 }

+ 102 - 0
Source/QuestPDF.DocumentationExamples/CodePatterns/CodePatternElementPositionLocatorExample.cs

@@ -0,0 +1,102 @@
+using QuestPDF.Elements;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.DocumentationExamples.CodePatterns;
+
+public class CodePatternElementPositionLocatorExample
+{
+    [Test]
+    public void Example()
+    {
+        var content = GenerateReport();
+        File.WriteAllBytes("code-pattern-element-position-locator.pdf", content);
+    }
+    
+    public byte[] GenerateReport()
+    {
+        return Document
+            .Create(document =>
+            {
+                document.Page(page =>
+                {
+                    page.Size(PageSizes.A6);
+                    page.Margin(25);
+                    page.DefaultTextStyle(x => x.FontSize(20));
+                    
+                    page.Content()
+                        .Background(Colors.White)
+                        .Row(row =>
+                        {
+                            row.Spacing(10);
+
+                            row.ConstantItem(5).Dynamic(new DynamicTextSpanPositionCapture());
+ 
+                            row.RelativeItem().CapturePosition("container").Text(text =>
+                            {
+               
+                                text.Justify();
+                                text.DefaultTextStyle(x => x.FontSize(18));
+
+                                text.Span("Lorem Ipsum is simply dummy text of the printing and typesetting industry.");
+                                text.Element(TextInjectedElementAlignment.Top).CapturePosition("span_start");
+                                text.Span("Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.").BackgroundColor(Colors.Red.Lighten4);
+                                text.Element(TextInjectedElementAlignment.Bottom).CapturePosition("span_end");
+                                text.Span(" It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.");
+                            });
+                        });
+
+                    page.Footer().Text(text =>
+                    {
+                        text.Span("Page ");
+                        text.CurrentPageNumber();
+                        text.Span(" of ");
+                        text.TotalPages();
+                    });
+                });
+            })
+            .GeneratePdf();
+    }
+    
+
+    public class DynamicTextSpanPositionCapture : IDynamicComponent
+    {
+        public DynamicComponentComposeResult Compose(DynamicContext context)
+        {
+            var containerLocation = context.GetElementCapturedPositions("container").FirstOrDefault(x => x.PageNumber == context.PageNumber);
+
+            var spanStartLocation = context.GetElementCapturedPositions("span_start").FirstOrDefault();
+
+            var spanEndLocation = context.GetElementCapturedPositions("span_end").FirstOrDefault();
+
+            if (containerLocation == null || spanStartLocation == null || spanEndLocation == null || containerLocation.PageNumber > spanStartLocation.PageNumber || containerLocation.PageNumber < spanEndLocation.PageNumber)
+            {
+                return new DynamicComponentComposeResult
+                {
+                    Content = context.CreateElement(container => { }),
+                    HasMoreContent = false
+                };
+            }
+            
+            var positionStart = containerLocation.PageNumber > spanStartLocation.PageNumber ? containerLocation.Y : spanStartLocation.Y;
+            var positionEnd = containerLocation.PageNumber < spanEndLocation.PageNumber ? (containerLocation.Y + containerLocation.Height) : (spanEndLocation.Y + spanEndLocation.Height);
+
+            var content = context.CreateElement(container =>
+            {
+                container
+                    .TranslateX(0)
+                    .TranslateY(positionStart - containerLocation.Y)
+                    .Width(5)
+                    .Height(positionEnd - positionStart)
+                    .Background(Colors.Red.Medium);
+            });
+
+            return new DynamicComponentComposeResult
+            {
+                Content = content,
+                HasMoreContent = false
+            };
+        }
+    }
+}

+ 3 - 3
Source/QuestPDF/Drawing/DrawingCanvases/FreeDrawingCanvas.cs

@@ -53,17 +53,17 @@ namespace QuestPDF.Drawing.DrawingCanvases
 
 
         public void Translate(Position vector)
         public void Translate(Position vector)
         {
         {
-            CurrentMatrix = CurrentMatrix.Translate(vector.X, vector.Y);
+            CurrentMatrix *= SkCanvasMatrix.CreateTranslation(vector.X, vector.Y);
         }
         }
         
         
         public void Scale(float scaleX, float scaleY)
         public void Scale(float scaleX, float scaleY)
         {
         {
-            CurrentMatrix = CurrentMatrix.Scale(scaleX, scaleY);
+            CurrentMatrix *= SkCanvasMatrix.CreateScale(scaleX, scaleY);
         }
         }
         
         
         public void Rotate(float angle)
         public void Rotate(float angle)
         {
         {
-            CurrentMatrix = CurrentMatrix.Rotate(angle);
+            CurrentMatrix *= SkCanvasMatrix.CreateRotation(angle);
         }
         }
 
 
         public void DrawFilledRectangle(Position vector, Size size, Color color)
         public void DrawFilledRectangle(Position vector, Size size, Color color)

+ 8 - 0
Source/QuestPDF/Elements/Dynamic.cs

@@ -146,6 +146,14 @@ namespace QuestPDF.Elements
         /// Returns the vertical and horizontal space, in points, available to the dynamic component.
         /// Returns the vertical and horizontal space, in points, available to the dynamic component.
         /// </summary>
         /// </summary>
         public Size AvailableSize { get; internal set; }
         public Size AvailableSize { get; internal set; }
+        
+        /// <summary>
+        /// Returns all page locations of the captured element.
+        /// </summary>
+        public ICollection<PageElementLocation> GetElementCapturedPositions(string id)
+        {
+            return PageContext.GetContentPosition(id);
+        }
 
 
         /// <summary>
         /// <summary>
         /// Enables the creation of unattached layout structures and provides their size measurements.
         /// Enables the creation of unattached layout structures and provides their size measurements.

+ 32 - 0
Source/QuestPDF/Elements/ElementPositionLocator.cs

@@ -0,0 +1,32 @@
+using System;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements
+{
+    internal class ElementPositionLocator : ContainerElement
+    {
+        public string Id { get; set; }
+
+        internal override void Draw(Size availableSpace)
+        {
+            base.Draw(availableSpace);
+
+            var matrix = Canvas.GetCurrentMatrix();
+
+            var position = new PageElementLocation
+            {
+                Id = Id,
+
+                PageNumber = PageContext.CurrentPage,
+
+                Width = availableSpace.Width / matrix.ScaleX,
+                Height = availableSpace.Height / matrix.ScaleY,
+
+                X = matrix.TranslateX,
+                Y = matrix.TranslateY
+            };
+
+            PageContext.CaptureContentPosition(position);
+        }
+    }
+}

+ 12 - 0
Source/QuestPDF/Fluent/ElementExtensions.cs

@@ -500,6 +500,18 @@ namespace QuestPDF.Fluent
             });
             });
         }
         }
         
         
+        /// <summary>
+        /// Captures the size and location of its content.
+        /// The captured data can be then used in the Dynamic component to build and position other elements.
+        /// </summary>
+        public static IContainer CapturePosition(this IContainer element, string id)
+        {
+            return element.Element(new ElementPositionLocator
+            {
+                Id = id
+            });
+        }
+        
         #region Canvas [Obsolete]
         #region Canvas [Obsolete]
 
 
         private const string CanvasDeprecatedMessage = "The Canvas API has been deprecated since version 2024.3.0. Please use the .Svg(stringContent) API to provide custom content, and consult documentation webpage regarding integrating SkiaSharp with QuestPDF: https://www.questpdf.com/api-reference/skiasharp-integration.html";
         private const string CanvasDeprecatedMessage = "The Canvas API has been deprecated since version 2024.3.0. Please use the .Svg(stringContent) API to provide custom content, and consult documentation webpage regarding integrating SkiaSharp with QuestPDF: https://www.questpdf.com/api-reference/skiasharp-integration.html";

+ 19 - 1
Source/QuestPDF/Infrastructure/IPageContext.cs

@@ -1,4 +1,6 @@
-namespace QuestPDF.Infrastructure
+using System.Collections.Generic;
+
+namespace QuestPDF.Infrastructure
 {
 {
     internal sealed class DocumentLocation
     internal sealed class DocumentLocation
     {
     {
@@ -9,6 +11,19 @@
         public int Length => PageEnd - PageStart + 1;
         public int Length => PageEnd - PageStart + 1;
     }
     }
     
     
+    public class PageElementLocation
+    {
+        public string Id { get; set; }
+
+        public int PageNumber { get; set; }
+
+        public float Width { get; set; }
+        public float Height { get; set; }
+
+        public float X { get; set; }
+        public float Y { get; set; }
+    }
+    
     internal interface IPageContext
     internal interface IPageContext
     {
     {
         bool IsInitialRenderingPhase { get; }
         bool IsInitialRenderingPhase { get; }
@@ -17,5 +32,8 @@
         void SetSectionPage(string name);
         void SetSectionPage(string name);
         DocumentLocation? GetLocation(string name);
         DocumentLocation? GetLocation(string name);
         string GetDocumentLocationName(string locationName);
         string GetDocumentLocationName(string locationName);
+        
+        void CaptureContentPosition(PageElementLocation location);
+        ICollection<PageElementLocation> GetContentPosition(string id);
     }
     }
 }
 }

+ 13 - 0
Source/QuestPDF/Infrastructure/PageContext.cs

@@ -1,5 +1,6 @@
 using System;
 using System;
 using System.Collections.Generic;
 using System.Collections.Generic;
+using System.Linq;
 
 
 namespace QuestPDF.Infrastructure
 namespace QuestPDF.Infrastructure
 {
 {
@@ -64,5 +65,17 @@ namespace QuestPDF.Infrastructure
         {
         {
             return $"{CurrentDocumentId} | {locationName}";
             return $"{CurrentDocumentId} | {locationName}";
         }
         }
+        
+        private List<PageElementLocation> PageElementLocations { get; } = new();
+
+        public void CaptureContentPosition(PageElementLocation location)
+        {
+            PageElementLocations.Add(location);
+        }
+
+        public ICollection<PageElementLocation> GetContentPosition(string id)
+        {
+            return PageElementLocations.Where(x => x.Id == id).ToList();
+        }
     }
     }
 }
 }

+ 62 - 11
Source/QuestPDF/Skia/SkCanvasMatrix.cs

@@ -33,28 +33,79 @@ internal struct SkCanvasMatrix
         Perspective3 = 1
         Perspective3 = 1
     };
     };
     
     
-    public SkCanvasMatrix Translate(float x, float y)
+    public static SkCanvasMatrix CreateTranslation(float x, float y)
     {
     {
-        return this with { TranslateX = TranslateX + x, TranslateY = TranslateY + y };
-    }  
+        return new SkCanvasMatrix
+        {
+            ScaleX = 1,
+            SkewX = 0,
+            TranslateX = x,
+            
+            SkewY = 0,
+            ScaleY = 1,
+            TranslateY = y,
+            
+            Perspective1 = 0,
+            Perspective2 = 0,
+            Perspective3 = 1
+        };
+    }
     
     
-    public SkCanvasMatrix Scale(float x, float y)
+    public static SkCanvasMatrix CreateScale(float x, float y)
     {
     {
-        return this with { ScaleX = ScaleX * x, ScaleY = ScaleY * y };
+        return new SkCanvasMatrix
+        {
+            ScaleX = x,
+            SkewX = 0,
+            TranslateX = 0,
+            
+            SkewY = 0,
+            ScaleY = y,
+            TranslateY = 0,
+            
+            Perspective1 = 0,
+            Perspective2 = 0,
+            Perspective3 = 1
+        };
     }
     }
     
     
-    public SkCanvasMatrix Rotate(float angle)
+    public static SkCanvasMatrix CreateRotation(float angle)
     {
     {
         var radians = Math.PI * angle / 180;
         var radians = Math.PI * angle / 180;
         var cos = (float)Math.Cos(radians);
         var cos = (float)Math.Cos(radians);
         var sin = (float)Math.Sin(radians);
         var sin = (float)Math.Sin(radians);
         
         
-        return this with
+        return new SkCanvasMatrix
+        {
+            ScaleX = cos,
+            SkewX = sin,
+            TranslateX = 0,
+            
+            SkewY = -sin,
+            ScaleY = cos,
+            TranslateY = 0,
+            
+            Perspective1 = 0,
+            Perspective2 = 0,
+            Perspective3 = 1
+        };
+    }
+    
+    public static SkCanvasMatrix operator *(SkCanvasMatrix a, SkCanvasMatrix b)
+    {
+        return new SkCanvasMatrix
         {
         {
-            ScaleX = ScaleX * cos - SkewY * sin,
-            SkewX = ScaleX * sin + SkewY * cos,
-            ScaleY = ScaleY * cos - SkewX * sin,
-            SkewY = ScaleY * sin + SkewX * cos
+            ScaleX = a.ScaleX * b.ScaleX + a.SkewY * b.SkewX,
+            SkewX = a.ScaleX * b.SkewY + a.SkewY * b.ScaleY,
+            TranslateX = a.ScaleX * b.TranslateX + a.SkewY * b.TranslateY + a.TranslateX,
+            
+            SkewY = a.SkewX * b.ScaleX + a.ScaleY * b.SkewX,
+            ScaleY = a.SkewX * b.SkewY + a.ScaleY * b.ScaleY,
+            TranslateY = a.SkewX * b.TranslateX + a.ScaleY * b.TranslateY + a.TranslateY,
+            
+            Perspective1 = 0,
+            Perspective2 = 0,
+            Perspective3 = 1
         };
         };
     }
     }
 }
 }