From 940616cd3ed681d7cad2c52b86f3b1682e20803d Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Mon, 27 Dec 2021 18:24:37 +0100 Subject: [PATCH] Table: optimized rendering performance --- QuestPDF.Examples/TableExamples.cs | 14 ++- .../Elements/Table/EnumerableExtensions.cs | 25 ----- QuestPDF/Elements/Table/Table.cs | 92 +++++++++++-------- QuestPDF/QuestPDF.csproj | 2 +- 4 files changed, 59 insertions(+), 74 deletions(-) delete mode 100644 QuestPDF/Elements/Table/EnumerableExtensions.cs diff --git a/QuestPDF.Examples/TableExamples.cs b/QuestPDF.Examples/TableExamples.cs index 67d3949..f5498c1 100644 --- a/QuestPDF.Examples/TableExamples.cs +++ b/QuestPDF.Examples/TableExamples.cs @@ -103,7 +103,7 @@ namespace QuestPDF.Examples .ProducePdf() .PageSize(PageSizes.A4) .ShowResults() - .Render(container => GeneratePerformanceStructure(container, 100)); + .Render(container => GeneratePerformanceStructure(container, 2)); } [Test] @@ -117,7 +117,7 @@ namespace QuestPDF.Examples .EnableCaching() .EnableDebugging(false) .ShowResults() - .Render(container => GeneratePerformanceStructure(container, 1000)); + .Render(container => GeneratePerformanceStructure(container, 100)); } public static void GeneratePerformanceStructure(IContainer container, int repeats) @@ -136,16 +136,16 @@ namespace QuestPDF.Examples columns.RelativeColumn(); }); - foreach (var _ in Enumerable.Range(0, repeats)) + foreach (var i in Enumerable.Range(0, repeats)) { table.Cell().RowSpan(3).LabelCell("Project"); table.Cell().RowSpan(3).ValueCell(Placeholders.Sentence()); + table.Cell().LabelCell("Report number"); + table.Cell().ValueCell(i.ToString()); + table.Cell().LabelCell("Date"); table.Cell().ValueCell(Placeholders.ShortDate()); - - table.Cell().LabelCell("Report number"); - table.Cell().ValueCell(Placeholders.Integer()); table.Cell().LabelCell("Inspector"); table.Cell().ValueCell("Marcin ZiÄ…bek"); @@ -179,8 +179,6 @@ namespace QuestPDF.Examples table.Cell().LabelCell("Remarks"); table.Cell().ColumnSpan(3).ValueCell(Placeholders.Paragraph()); - - table.Cell().ColumnSpan(4).BorderBottom(2); } }); } diff --git a/QuestPDF/Elements/Table/EnumerableExtensions.cs b/QuestPDF/Elements/Table/EnumerableExtensions.cs deleted file mode 100644 index ef6de35..0000000 --- a/QuestPDF/Elements/Table/EnumerableExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace QuestPDF.Elements.Table -{ - internal static class EnumerableExtensions - { - public static IEnumerable Scan(this IEnumerable input, Func accumulate) - { - using var enumerator = input.GetEnumerator(); - - if (!enumerator.MoveNext()) - yield break; - - var state = enumerator.Current; - yield return state; - - while (enumerator.MoveNext()) - { - state = accumulate(state, enumerator.Current); - yield return state; - } - } - } -} \ No newline at end of file diff --git a/QuestPDF/Elements/Table/Table.cs b/QuestPDF/Elements/Table/Table.cs index 04ac5a3..0a86b35 100644 --- a/QuestPDF/Elements/Table/Table.cs +++ b/QuestPDF/Elements/Table/Table.cs @@ -2,11 +2,38 @@ using System.Collections.Generic; using System.Linq; using QuestPDF.Drawing; -using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Table { + /// + /// This dictionary allows to access key that does not exist. + /// Instead of throwing an exception, it returns a default value. + /// + internal class DynamicDictionary + { + private TValue Default { get; } + private IDictionary Dictionary { get; } = new Dictionary(); + + 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> Items => Dictionary.ToList(); + } + internal class Table : Element, IStateResettable { public List Columns { get; } = new List(); @@ -26,7 +53,7 @@ namespace QuestPDF.Elements.Table public void ResetState() { if (RowsCount == default) - RowsCount = Children.Max(x => x.Row + x.RowSpan); + RowsCount = Children.Max(x => x.Row + x.RowSpan - 1); if (OrderedChildren == default) { @@ -35,7 +62,7 @@ namespace QuestPDF.Elements.Table .ToDictionary(x => x.Key, x => x.OrderBy(y => y.Column).ToArray()); OrderedChildren = Enumerable - .Range(0, RowsCount) + .Range(0, RowsCount + 1) .Select(x => groups.TryGetValue(x, out var output) ? output : Array.Empty()) .ToArray(); } @@ -93,12 +120,9 @@ namespace QuestPDF.Elements.Table .ToList() .ForEach(x => cellOffsets[x] = Columns[x - 1].Width + cellOffsets[x - 1]); - - // update row heights - var rowsCount = RowsCount; - var rowBottomOffsets = new float[rowsCount]; - var childrenToTry = Enumerable.Range(CurrentRow - 1, RowsCount - CurrentRow).SelectMany(x => OrderedChildren[x]); + var rowBottomOffsets = new DynamicDictionary(); + var childrenToTry = Enumerable.Range(CurrentRow, RowsCount - CurrentRow + 1).SelectMany(x => OrderedChildren[x]); var currentRow = CurrentRow; @@ -106,59 +130,47 @@ namespace QuestPDF.Elements.Table { if (child.Row > currentRow) { + rowBottomOffsets[currentRow] = Math.Max(rowBottomOffsets[currentRow], rowBottomOffsets[currentRow - 1]); + if (rowBottomOffsets[currentRow - 1] > availableSpace.Height + Single.Epsilon) break; currentRow = child.Row; } - var rowIndex = child.Row - 1; - - if (rowIndex > 1) - rowBottomOffsets[rowIndex] = Math.Max(rowBottomOffsets[rowIndex], rowBottomOffsets[rowIndex-1]); - - var topOffset = 0f; - - if (rowIndex > 0) - topOffset = rowBottomOffsets[rowIndex - 1]; // look at previous row + var topOffset = rowBottomOffsets[child.Row - 1]; var height = GetCellSize(child).Height; var cellBottomOffset = height + topOffset; - var targetRowId = child.Row + child.RowSpan - 2; // -1 because indexing starts at 0, -1 because rowSpan starts at 1 + var targetRowId = child.Row + child.RowSpan - 1; // -1 because rowSpan starts at 1 rowBottomOffsets[targetRowId] = Math.Max(rowBottomOffsets[targetRowId], cellBottomOffset); } + + var rowHeights = new DynamicDictionary(); Enumerable - .Range(1, rowsCount - 1) - .ToList() - .ForEach(x => rowBottomOffsets[x] = Math.Max(rowBottomOffsets[x], rowBottomOffsets[x-1])); - - var rowHeights = new float[rowsCount]; - rowHeights[0] = rowBottomOffsets[0]; - - Enumerable - .Range(1, rowsCount - 1) + .Range(CurrentRow, rowBottomOffsets.Items.Count) .ToList() .ForEach(x => rowHeights[x] = rowBottomOffsets[x] - rowBottomOffsets[x-1]); // find rows count to render in this pass - var rowsToDisplay = rowHeights.Scan((x, y) => x + y).Count(x => x <= availableSpace.Height + Size.Epsilon); - rowHeights = rowHeights.Take(rowsToDisplay).ToArray(); - - var totalHeight = rowHeights.Sum(); - var totalWidth = Columns.Sum(x => x.Width); - - foreach (var cell in Children) - { - if (cell.Row >= CurrentRow && cell.Row > rowsToDisplay) - continue; + var maxRowToDisplay = rowBottomOffsets.Items.Where(x => x.Value <= availableSpace.Height + Size.Epsilon).Max(x => x.Key); + var totalHeight = rowHeights.Items.Where(x => x.Key <= maxRowToDisplay).Sum(x => x.Value); + var totalWidth = Columns.Sum(x => x.Width); + + var childrenToDraw = Enumerable + .Range(CurrentRow, maxRowToDisplay - CurrentRow + 1) + .SelectMany(x => OrderedChildren[x]); + + foreach (var cell in childrenToDraw) + { var leftOffset = cellOffsets[cell.Column - 1]; - var topOffset = cell.Row == 1 ? 0 : rowBottomOffsets[cell.Row - 2]; + var topOffset = cell.Row == 1 ? 0 : rowBottomOffsets[cell.Row - 1]; var width = GetCellWidth(cell); - var height = Enumerable.Range(cell.Row - 1, cell.RowSpan).TakeWhile(x => x < rowHeights.Length).Select(x => rowHeights[x]).Sum(); + var height = Enumerable.Range(cell.Row, cell.RowSpan).Select(x => rowHeights[x]).Sum(); cellRenderingCommands.Add(new TableCellRenderingCommand() { @@ -172,7 +184,7 @@ namespace QuestPDF.Elements.Table { Size = new Size(totalWidth, totalHeight), CellRenderingCommands = cellRenderingCommands, - MaxRowRendered = rowsToDisplay + MaxRowRendered = maxRowToDisplay }; float GetCellWidth(TableCell cell) diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index ec7954c..d0fc5f3 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -7,7 +7,7 @@ 2021.12.0 QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API. $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt")) - 10 + 9 true Logo.png https://www.questpdf.com/images/package-logo.png