Improved readibility of the table layout algorithm

This commit is contained in:
MarcinZiabek
2021-12-30 20:29:32 +01:00
committed by Marcin Ziąbek
parent 06418238dc
commit b8ea30be23
5 changed files with 141 additions and 340 deletions
+1 -106
View File
@@ -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");
};
}
}
}
-113
View File
@@ -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");
};
}
}
}
@@ -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();
}
}
+106 -120
View File
@@ -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 maxRenderingRow = RowsCount;
var currentRow = CurrentRow;
var commands = new List<TableCellRenderingCommand>();
foreach (var cell in childrenToTry)
{
if (cell.Row > currentRow)
{
rowBottomOffsets[currentRow] = Math.Max(rowBottomOffsets[currentRow], rowBottomOffsets[currentRow - 1]);
if (rowBottomOffsets[currentRow - 1] > availableSpace.Height + Size.Epsilon)
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;
var cellSize = GetCellSize(cell, availableHeight);
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;
}
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()
{
Cell = cell,
Measurement = cellSize,
Size = new Size(width, cellSize.Height),
Offset = new Position(cellOffsets[cell.Column - 1], topOffset)
};
commands.Add(command);
}
var commands = GetRenderingCommands();
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);
AdjustLastCellSizes(tableHeight, commands);
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;
}
ICollection<TableCellRenderingCommand> GetRenderingCommands()
{
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)
{
// 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;
currentRow = cell.Row;
}
// cell visibility optimizations
if (cell.Row > maxRenderingRow)
break;
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 = cell.Measure(availableCellSize);
// 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)
});
}
// 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)
{
var height = tableHeight - command.Offset.Y;
command.Size = new Size(command.Size.Width, height);
}
}
// 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];
}
}
}
@@ -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; }