Przeglądaj źródła

Improved readibility of the table layout algorithm

MarcinZiabek 4 lat temu
rodzic
commit
b8ea30be23

+ 1 - 106
QuestPDF.Examples/TableExamples.cs

@@ -16,85 +16,6 @@ namespace QuestPDF.Examples
     {
         public static Random Random { get; } = new Random();
 
-        [Test]
-        public void Example()
-        {
-            RenderingTest
-                .Create()
-                .ProduceImages()
-                .PageSize(PageSizes.A4)
-                .ShowResults()
-                .Render(container =>
-                {
-                    container
-                        .Padding(25)
-                        .Box()
-                        .Border(2) 
-                        .Table(table =>
-                        {
-                            table.ColumnsDefinition(columns =>
-                            {
-                                columns.ConstantColumn(100);
-                                columns.RelativeColumn();
-                                columns.ConstantColumn(100);
-                                columns.ConstantColumn(200);
-                            });
-
-                            table.Cell().ColumnSpan(2).Element(CreateBox("A"));
-                            table.Cell().Element(CreateBox("B"));
-                            table.Cell().Element(CreateBox("C"));
-                            
-                            table.Cell().Element(CreateBox("D"));
-                            table.Cell().RowSpan(2).Element(CreateBox("E"));
-                            table.Cell().RowSpan(3).ColumnSpan(2).Element(CreateBox("F"));
-                            
-                            table.Cell().RowSpan(2).Element(CreateBox("G"));
-                            table.Cell().RowSpan(2).Element(CreateBox("H"));
-                            table.Cell().Element(CreateBox("I"));
-                            table.Cell().Element(CreateBox("J"));
-                            table.Cell().RowSpan(2).Element(CreateBox("K"));
-                            table.Cell().ColumnSpan(2).Element(CreateBox("L"));
-                            table.Cell().Element(CreateBox("M"));
-                        });
-                });
-        }
-        
-        [Test]
-        public void TreeTable()
-        {
-            RenderingTest
-                .Create()
-                .ProducePdf()
-                .PageSize(PageSizes.A4)
-                .ShowResults()
-                .Render(container =>
-                {
-                    container
-                        .Padding(25)
-                        .Box()
-                        .Border(2) 
-                        .Table(table =>
-                        {
-                            table.ColumnsDefinition(columns =>
-                            {
-                                columns.RelativeColumn(100);
-                                columns.RelativeColumn(100);
-                                columns.RelativeColumn(100);
-                            });
-
-                            table.Cell().RowSpan(4).Element(CreateBox("A"));
-                            
-                            table.Cell().RowSpan(2).Element(CreateBox("B"));
-                            table.Cell().Element(CreateBox("C"));
-                            table.Cell().Element(CreateBox("D"));
-                            
-                            table.Cell().RowSpan(2).Element(CreateBox("E"));
-                            table.Cell().Element(CreateBox("F"));
-                            table.Cell().Element(CreateBox("G"));
-                        });
-                });
-        }
-        
         [Test]
         public void TemperatureReport()
         {
@@ -102,18 +23,7 @@ namespace QuestPDF.Examples
                 .Create()
                 .ProducePdf()
                 .PageSize(PageSizes.A4)
-                .ShowResults()
-                .Render(container => GeneratePerformanceStructure(container, 10));
-        }
-        
-        [Test]
-        public void TemperatureReport_PerformanceTest()
-        {
-            RenderingTest
-                .Create()
-                .ProducePdf()
-                .PageSize(PageSizes.A4)
-                .MaxPages(10000)
+                .MaxPages(10_000)
                 .EnableCaching()
                 .EnableDebugging(false)
                 .ShowResults()
@@ -184,20 +94,5 @@ namespace QuestPDF.Examples
                     }
                 });
         }
-        
-        private Action<IContainer> CreateBox(string label)
-        {
-            return container =>
-            {
-                var height = Random.Next(2, 6) * 10;
-                    
-                container
-                    .Background(Placeholders.BackgroundColor())
-                    // .AlignCenter()
-                    // .AlignMiddle()
-                    .Height(height);
-                    // .Text($"{label}: {height}px");
-            };
-        }
     }
 }

+ 0 - 113
QuestPDF.Examples/TablePerformanceTest.cs

@@ -1,113 +0,0 @@
-using System;
-using System.Diagnostics;
-using System.Linq;
-using BenchmarkDotNet.Attributes;
-using BenchmarkDotNet.Configs;
-using BenchmarkDotNet.Engines;
-using BenchmarkDotNet.Running;
-using NUnit.Framework;
-using QuestPDF.Drawing;
-using QuestPDF.Drawing.Proxy;
-using QuestPDF.Elements;
-using QuestPDF.Fluent;
-using QuestPDF.Helpers;
-using QuestPDF.Infrastructure;
-
-namespace QuestPDF.Examples
-{
-    [SimpleJob(RunStrategy.Monitoring, launchCount: 0, warmupCount: 1, targetCount: 16)]
-    [MinColumn, MaxColumn, MeanColumn, MedianColumn]
-    public class TablePerformanceTest
-    {
-        private static Random Random { get; } = new Random();
-        
-        private PageContext PageContext { get; set; }
-        private DocumentMetadata Metadata { get; set; }
-        private Container Content { get; set; }
-
-        [Test]
-        public void Run()
-        {
-            var configuration = ManualConfig
-                .Create(DefaultConfig.Instance)
-                .WithOptions(ConfigOptions.DisableOptimizationsValidator);
-            
-            BenchmarkRunner.Run<TablePerformanceTest>(configuration);
-        }
-        
-        [IterationSetup]
-        [SetUp]
-        public void GenerateReportData()
-        {
-            Metadata = new DocumentMetadata()
-            {
-                DocumentLayoutExceptionThreshold = 1000
-            };
-            
-            var documentContainer = new DocumentContainer();
-
-            documentContainer.Page(page =>
-            {
-                page.Size(PageSizes.A3);
-                GeneratePerformanceStructure(page.Content(), 10_000);
-            });
-            
-            Content = documentContainer.Compose();
-
-            PageContext = new PageContext();
-            DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
-
-            Content.HandleVisitor(x =>
-            {
-                if (x is ICacheable)
-                    x.CreateProxy(y => new CacheProxy(y));
-            });
-        }
-
-        [Benchmark]
-        [Test]
-        public void GenerationTest()
-        {
-            DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
-        }
-        
-        void GeneratePerformanceStructure(IContainer container, int itemsCount)
-        {
-            container
-                .Padding(25)
-                .Table(table =>
-                {
-                    table.ColumnsDefinition(columns =>
-                    {
-                        foreach (var size in Enumerable.Range(0, 10))
-                            columns.ConstantColumn(100);
-                    });
-
-                    foreach (var i in Enumerable.Range(1, itemsCount))
-                    {
-                        table
-                            .Cell()
-                            .RowSpan((uint)Random.Next(1, 5))
-                            .ColumnSpan((uint)Random.Next(1, 5))
-                            .Element(CreateBox(i.ToString()));
-                    }
-                });
-        }
-        
-        private Action<IContainer> CreateBox(string label)
-        {
-            return container =>
-            {
-                var height = Random.Next(2, 6) * 20;
-                    
-                container
-                    .Border(2)
-                    .Background(Placeholders.BackgroundColor())
-                    .AlignCenter()
-                    .AlignMiddle()
-                    .Height(height)
-                    .Text($"{label}: {height}px");
-            };
-        }
-    }
-}

+ 33 - 0
QuestPDF/Elements/Table/DynamicDictionary.cs

@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace QuestPDF.Elements.Table
+{
+    /// <summary>
+    /// This dictionary allows to access key that does not exist.
+    /// Instead of throwing an exception, it returns a default value.
+    /// </summary>
+    internal class DynamicDictionary<TKey, TValue>
+    {
+        private TValue Default { get; }
+        private IDictionary<TKey, TValue> Dictionary { get; } = new Dictionary<TKey, TValue>();
+
+        public DynamicDictionary()
+        {
+            
+        }
+        
+        public DynamicDictionary(TValue defaultValue)
+        {
+            Default = defaultValue;
+        }
+        
+        public TValue this[TKey key]
+        {
+            get => Dictionary.TryGetValue(key, out var value) ? value : Default;
+            set => Dictionary[key] = value;
+        }
+
+        public List<KeyValuePair<TKey, TValue>> Items => Dictionary.ToList();
+    }
+}

+ 97 - 111
QuestPDF/Elements/Table/Table.cs

@@ -7,41 +7,17 @@ using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Elements.Table
 {
-    /// <summary>
-    /// This dictionary allows to access key that does not exist.
-    /// Instead of throwing an exception, it returns a default value.
-    /// </summary>
-    internal class DynamicDictionary<TKey, TValue>
-    {
-        private TValue Default { get; }
-        private IDictionary<TKey, TValue> Dictionary { get; } = new Dictionary<TKey, TValue>();
-
-        public DynamicDictionary()
-        {
-            
-        }
-        
-        public DynamicDictionary(TValue defaultValue)
-        {
-            Default = defaultValue;
-        }
-        
-        public TValue this[TKey key]
-        {
-            get => Dictionary.TryGetValue(key, out var value) ? value : Default;
-            set => Dictionary[key] = value;
-        }
-
-        public List<KeyValuePair<TKey, TValue>> Items => Dictionary.ToList();
-    }
-    
     internal class Table : Element, IStateResettable
     {
         public List<TableColumnDefinition> Columns { get; } = new List<TableColumnDefinition>();
         public List<TableCell> Children { get; } = new List<TableCell>();
         public float Spacing { get; set; }
         
+        // cache for efficient cell finding
+        // index of first array - number of row
+        // nested array - collection of all cells starting at given row
         private TableCell[][] OrderedChildren { get; set; }
+        
         private int RowsCount { get; set; }
         private int CurrentRow { get; set; }
         
@@ -135,85 +111,112 @@ namespace QuestPDF.Elements.Table
         
         private ICollection<TableCellRenderingCommand> PlanLayout(Size availableSpace)
         {
-            var cellOffsets = new float[Columns.Count + 1];
-            cellOffsets[0] = 0;
+            var columnOffsets = GetColumnLeftOffsets(Columns);
             
-            Enumerable
-                .Range(1, cellOffsets.Length - 1)
-                .ToList()
-                .ForEach(x => cellOffsets[x] = Columns[x - 1].Width + cellOffsets[x - 1]);
-            
-            // update row heights
-            var rowBottomOffsets = new DynamicDictionary<int, float>();
-            var childrenToTry = Enumerable.Range(CurrentRow, RowsCount - CurrentRow + 1).SelectMany(x => OrderedChildren[x]);
+            var commands = GetRenderingCommands();
+            var tableHeight = commands.Max(cell => cell.Offset.Y + cell.Size.Height);
             
-            var maxRenderingRow = RowsCount;
-            var currentRow = CurrentRow;
+            AdjustCellSizes(tableHeight, commands);
+            AdjustLastCellSizes(tableHeight, commands);
 
-            var commands = new List<TableCellRenderingCommand>();
+            return commands;
+
+            static float[] GetColumnLeftOffsets(IList<TableColumnDefinition> columns)
+            {
+                var cellOffsets = new float[columns.Count + 1];
+                cellOffsets[0] = 0;
+
+                foreach (var column in Enumerable.Range(1, cellOffsets.Length - 1))
+                    cellOffsets[column] = columns[column - 1].Width + cellOffsets[column - 1];
+
+                return cellOffsets;
+            }
             
-            foreach (var cell in childrenToTry)
+            ICollection<TableCellRenderingCommand> GetRenderingCommands()
             {
-                if (cell.Row > currentRow)
+                var rowBottomOffsets = new DynamicDictionary<int, float>();
+                
+                var childrenToTry = Enumerable
+                    .Range(CurrentRow, RowsCount - CurrentRow + 1)
+                    .SelectMany(x => OrderedChildren[x]);
+            
+                var currentRow = CurrentRow;
+                var maxRenderingRow = RowsCount;
+                
+                var commands = new List<TableCellRenderingCommand>();
+                
+                foreach (var cell in childrenToTry)
                 {
-                    rowBottomOffsets[currentRow] = Math.Max(rowBottomOffsets[currentRow], rowBottomOffsets[currentRow - 1]);
+                    // update position of previous row
+                    if (cell.Row > currentRow)
+                    {
+                        rowBottomOffsets[currentRow] = Math.Max(rowBottomOffsets[currentRow], rowBottomOffsets[currentRow - 1]);
+                            
+                        if (rowBottomOffsets[currentRow - 1] > availableSpace.Height + Size.Epsilon)
+                            break;
                         
-                    if (rowBottomOffsets[currentRow - 1] > availableSpace.Height + Size.Epsilon)
+                        currentRow = cell.Row;
+                    }
+
+                    // cell visibility optimizations
+                    if (cell.Row > maxRenderingRow)
                         break;
                     
-                    currentRow = cell.Row;
-                }
-
-                if (cell.Row > maxRenderingRow)
-                    break;
-                
-                if (cell.IsRendered)
-                    continue;
-                
-                var topOffset = rowBottomOffsets[cell.Row - 1];
-                var availableHeight = availableSpace.Height - topOffset + Size.Epsilon;
+                    if (cell.IsRendered)
+                        continue;
+                    
+                    // calculate cell position / size
+                    var topOffset = rowBottomOffsets[cell.Row - 1];
+                    
+                    var availableWidth = GetCellWidth(cell);
+                    var availableHeight = availableSpace.Height - topOffset + Size.Epsilon;
+                    var availableCellSize = new Size(availableWidth, availableHeight);
 
-                var cellSize = GetCellSize(cell, availableHeight);
+                    var cellSize = cell.Measure(availableCellSize);
 
-                if (cellSize.Type == SpacePlanType.PartialRender)
-                {
-                    maxRenderingRow = Math.Min(maxRenderingRow, cell.Row + cell.RowSpan - 1);
-                }
-                
-                if (cellSize.Type == SpacePlanType.Wrap)
-                {
-                    maxRenderingRow = Math.Min(maxRenderingRow, cell.Row - 1);
-                    continue;
+                    // corner case: cell within the row is not fully rendered, do not attempt to render next row
+                    if (cellSize.Type == SpacePlanType.PartialRender)
+                    {
+                        maxRenderingRow = Math.Min(maxRenderingRow, cell.Row + cell.RowSpan - 1);
+                    }
+                    
+                    // corner case: cell within the row want to wrap to the next page, do not attempt to render this row
+                    if (cellSize.Type == SpacePlanType.Wrap)
+                    {
+                        maxRenderingRow = Math.Min(maxRenderingRow, cell.Row - 1);
+                        continue;
+                    }
+                    
+                    // update position of the last row that cell occupies
+                    var bottomRow = cell.Row + cell.RowSpan - 1;
+                    rowBottomOffsets[bottomRow] = Math.Max(rowBottomOffsets[bottomRow], topOffset + cellSize.Height);
+
+                    // accept cell to be rendered
+                    commands.Add(new TableCellRenderingCommand()
+                    {
+                        Cell = cell,
+                        Measurement = cellSize,
+                        Size = new Size(availableWidth, cellSize.Height),
+                        Offset = new Position(columnOffsets[cell.Column - 1], topOffset)
+                    });
                 }
-                
-                var cellBottomOffset = cellSize.Height + topOffset;
-                
-                var targetRowId = cell.Row + cell.RowSpan - 1; // -1 because rowSpan starts at 1
-                rowBottomOffsets[targetRowId] = Math.Max(rowBottomOffsets[targetRowId], cellBottomOffset);
 
-                var width = GetCellWidth(cell);
-                
-                var command = new TableCellRenderingCommand()
+                // corner case: reject cell if other cells within the same rows are rejected
+                return commands.Where(x => x.Cell.Row <= maxRenderingRow).ToList();
+            }
+            
+            // if two cells end up on the same row (a.Row + a.RowSpan = b.Row + b.RowSpan),
+            // bottom edges of their bounding boxes should be at the same level
+            static void AdjustCellSizes(float tableHeight, ICollection<TableCellRenderingCommand> commands)
+            {
+                foreach (var command in commands)
                 {
-                    Cell = cell,
-                    Measurement = cellSize,
-                    Size = new Size(width, cellSize.Height),
-                    Offset = new Position(cellOffsets[cell.Column - 1], topOffset)
-                };
-                
-                commands.Add(command);
+                    var height = tableHeight - command.Offset.Y;
+                    command.Size = new Size(command.Size.Width, height);
+                }
             }
-
-            var tableHeight = commands.Max(cell => cell.Offset.Y + cell.Size.Height);
-
-            commands = commands.Where(x => x.Cell.Row <= maxRenderingRow).ToList();
-
-
-            AdjustLastCellSizes(tableHeight, commands);
-            AdjustCellSizes(tableHeight, commands);
-
-            return commands;
-
+            
+            // all cells, that are last ones in their respective columns, should take all remaining space
             static void AdjustLastCellSizes(float tableHeight, ICollection<TableCellRenderingCommand> commands)
             {
                 var columnsCount = commands.Select(x => x.Cell).Max(x => x.Column + x.ColumnSpan - 1);
@@ -231,27 +234,10 @@ namespace QuestPDF.Elements.Table
                     lastCellInColumn.Size = new Size(lastCellInColumn.Size.Width, tableHeight - lastCellInColumn.Offset.Y);
                 }
             }
-            
-            static void AdjustCellSizes(float tableHeight, ICollection<TableCellRenderingCommand> commands)
-            {
-                foreach (var command in commands)
-                {
-                    var height = tableHeight - command.Offset.Y;
-                    command.Size = new Size(command.Size.Width, height);
-                }
-            }
-            
+
             float GetCellWidth(TableCell cell)
             {
-                return cellOffsets[cell.Column + cell.ColumnSpan - 1] - cellOffsets[cell.Column - 1];
-            }
-            
-            SpacePlan GetCellSize(TableCell cell, float availableHeight)
-            {
-                var width = GetCellWidth(cell);
-                var cellSize = new Size(width, availableHeight);
-
-                return cell.Measure(cellSize);
+                return columnOffsets[cell.Column + cell.ColumnSpan - 1] - columnOffsets[cell.Column - 1];
             }
         }
     }

+ 1 - 1
QuestPDF/Elements/Table/TableColumnDefinition.cs

@@ -2,7 +2,7 @@ namespace QuestPDF.Elements.Table
 {
     internal class TableColumnDefinition
     {
-        public float ConstantSize { get;  }
+        public float ConstantSize { get; }
         public float RelativeSize { get; }
 
         internal float Width { get; set; }