From 01980e563583f6d5156400f5ae312ef7797cbef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zi=C4=85bek?= Date: Thu, 6 Jan 2022 00:06:24 +0100 Subject: [PATCH] Table: row height fixes for overlapping cells. Added code examples --- QuestPDF.Examples/CanvasExamples.cs | 31 ++- .../DifferentHeaderOnFirstPageExample.cs | 55 +++++ QuestPDF.Examples/TableExamples.cs | 229 ++++++++---------- QuestPDF/Elements/Table/Table.cs | 11 +- QuestPDF/Elements/Table/TableCell.cs | 4 +- QuestPDF/Elements/Table/TableLayoutPlanner.cs | 16 +- .../Elements/Table/TableLayoutValidator.cs | 12 +- QuestPDF/Fluent/TableExtensions.cs | 21 +- 8 files changed, 214 insertions(+), 165 deletions(-) create mode 100644 QuestPDF.Examples/DifferentHeaderOnFirstPageExample.cs diff --git a/QuestPDF.Examples/CanvasExamples.cs b/QuestPDF.Examples/CanvasExamples.cs index 748e590..0de9357 100644 --- a/QuestPDF.Examples/CanvasExamples.cs +++ b/QuestPDF.Examples/CanvasExamples.cs @@ -15,30 +15,41 @@ namespace QuestPDF.Examples { RenderingTest .Create() + .PageSize(175, 100) .ProduceImages() .ShowResults() .Render(container => { container - .Background(Colors.White) + .Background(Colors.Grey.Lighten2) .Padding(25) .Box() .Layers(layers => { - layers.PrimaryLayer().Padding(10).Text("Sample text"); - layers.Layer().Canvas((canvas, size) => { - using var paint = new SKPaint + DrawRoundedRectangle(Colors.White, false); + DrawRoundedRectangle(Colors.Blue.Darken2, true); + + void DrawRoundedRectangle(string color, bool isStroke) { - Color = SKColor.Parse(Colors.Black), - IsStroke = true, - StrokeWidth = 1, - IsAntialias = true - }; + using var paint = new SKPaint + { + Color = SKColor.Parse(color), + IsStroke = isStroke, + StrokeWidth = 2, + IsAntialias = true + }; - canvas.DrawRoundRect(0, 0, size.Width, size.Height, 20, 20, paint); + canvas.DrawRoundRect(0, 0, size.Width, size.Height, 20, 20, paint); + } }); + + layers + .PrimaryLayer() + .PaddingVertical(10) + .PaddingHorizontal(20) + .Text("Sample text", TextStyle.Default.Size(16).Color(Colors.Blue.Darken2).SemiBold()); }); }); } diff --git a/QuestPDF.Examples/DifferentHeaderOnFirstPageExample.cs b/QuestPDF.Examples/DifferentHeaderOnFirstPageExample.cs new file mode 100644 index 0000000..9cae448 --- /dev/null +++ b/QuestPDF.Examples/DifferentHeaderOnFirstPageExample.cs @@ -0,0 +1,55 @@ +using System.Linq; +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class DifferentHeaderOnFirstPageExample + { + [Test] + public void Placeholder() + { + RenderingTest + .Create() + .PageSize(PageSizes.A6) + .ProduceImages() + .ShowResults() + .EnableDebugging() + .RenderDocument(container => + { + container.Page(page => + { + page.Size(PageSizes.A6); + page.Margin(30); + page.Background(Colors.White); + + page.Header().Stack(stack => + { + stack.Item().ShowOnce().Background(Colors.Blue.Lighten2).Height(60); + stack.Item().SkipOnce().Background(Colors.Green.Lighten2).Height(40); + }); + + page.Content().PaddingVertical(10).Stack(stack => + { + stack.Spacing(10); + + foreach (var _ in Enumerable.Range(0, 13)) + stack.Item().Background(Colors.Grey.Lighten2).Height(40); + }); + + page.Footer().AlignCenter().Text(text => + { + text.DefaultTextStyle(TextStyle.Default.Size(16)); + + text.CurrentPageNumber(); + text.Span(" / "); + text.TotalPages(); + }); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/TableExamples.cs b/QuestPDF.Examples/TableExamples.cs index 8ff8e1b..5c4d45f 100644 --- a/QuestPDF.Examples/TableExamples.cs +++ b/QuestPDF.Examples/TableExamples.cs @@ -40,23 +40,11 @@ namespace QuestPDF.Examples columns.RelativeColumn(); }); - // by using the custom element, we can reuse the same style - table.Cell().Row(1).Column(4).Element(x => TextContent(x, "A")); - table.Cell().Row(2).Column(2).Element(x => TextContent(x, "A")); - table.Cell().Row(3).Column(3).Element(x => TextContent(x, "A")); - table.Cell().Row(4).Column(1).Element(x => TextContent(x, "A")); - - static void TextContent(IContainer container, string text) - { - container - .Border(1) - .Background(Colors.Grey.Lighten3) - .MinWidth(50) - .MinHeight(50) - .AlignCenter() - .AlignMiddle() - .Text(text); - } + // by using custom 'Element' method, we can reuse visual configuration + table.Cell().Row(1).Column(4).Element(Block).Text("A"); + table.Cell().Row(2).Column(2).Element(Block).Text("B"); + table.Cell().Row(3).Column(3).Element(Block).Text("C"); + table.Cell().Row(4).Column(1).Element(Block).Text("D"); }); }); } @@ -86,21 +74,10 @@ namespace QuestPDF.Examples columns.RelativeColumn(); }); - table.DefaultCellStyle(cell => - { - return cell - .Border(1) - .Background(Colors.Grey.Lighten3) - .MinWidth(50) - .MinHeight(50) - .AlignCenter() - .AlignMiddle(); - }); - - table.Cell().Row(1).Column(1).Text("A"); - table.Cell().Row(2).Column(2).Text("B"); - table.Cell().Row(1).Column(3).Text("C"); - table.Cell().Row(2).Column(4).Text("D"); + table.Cell().Row(1).Column(1).Element(Block).Text("A"); + table.Cell().Row(2).Column(2).Element(Block).Text("B"); + table.Cell().Row(1).Column(3).Element(Block).Text("C"); + table.Cell().Row(2).Column(4).Element(Block).Text("D"); }); }); } @@ -160,17 +137,52 @@ namespace QuestPDF.Examples columns.RelativeColumn(); }); - table.Cell().TextBox("A"); - table.Cell().Row(2).Column(2).TextBox("B"); - table.Cell().TextBox("C"); - table.Cell().Row(3).Column(3).TextBox("D"); - table.Cell().ColumnSpan(2).TextBox("E"); + table.Cell().Element(Block).Text("A"); + table.Cell().Row(2).Column(2).Element(Block).Text("B"); + table.Cell().Element(Block).Text("C"); + table.Cell().Row(3).Column(3).Element(Block).Text("D"); + table.Cell().ColumnSpan(2).Element(Block).Text("E"); }); }); } [Test] public void ExtendLastCellsToTableBottom() + { + RenderingTest + .Create() + .ProduceImages() + .PageSize(220, 170) + .ShowResults() + .Render(container => + { + container + .Padding(10) + .Box() + .Border(1) + .Table(table => + { + table.ColumnsDefinition(columns => + { + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + columns.RelativeColumn(); + }); + + table.ExtendLastCellsToTableBottom(); + + table.Cell().Row(1).Column(1).Element(Block).Text("A"); + table.Cell().Row(3).Column(1).Element(Block).Text("B"); + table.Cell().Row(2).Column(2).Element(Block).Text("C"); + table.Cell().Row(3).Column(3).Element(Block).Text("D"); + table.Cell().Row(2).RowSpan(2).Column(4).Element(Block).Text("E"); + }); + }); + } + + [Test] + public void Overlapping() { RenderingTest .Create() @@ -191,43 +203,10 @@ namespace QuestPDF.Examples columns.RelativeColumn(); columns.RelativeColumn(); }); - - table.ExtendLastCellsToTableBottom(); - table.Cell().Row(1).Column(1).TextBox("A"); - table.Cell().Row(3).Column(1).TextBox("B"); - table.Cell().Row(2).Column(2).TextBox("C"); - table.Cell().Row(3).Column(3).TextBox("D"); - }); - }); - } - - [Test] - public void Overlapping() - { - RenderingTest - .Create() - .ProduceImages() - .PageSize(170, 120) - .ShowResults() - .Render(container => - { - container - .Padding(10) - .Box() - .Border(1) - .Table(table => - { - table.ColumnsDefinition(columns => - { - columns.RelativeColumn(); - columns.RelativeColumn(); - columns.RelativeColumn(); - }); - - table.Cell().RowSpan(2).ColumnSpan(2).Background(Colors.Green.Lighten3); - table.Cell().Background(Colors.Blue.Lighten3).MinHeight(50); - table.Cell().Row(2).Column(2).ColumnSpan(2).Background(Colors.Red.Lighten3).MinHeight(50); + table.Cell().Row(1).RowSpan(3).Column(1).ColumnSpan(3).Background(Colors.Grey.Lighten3).MinHeight(150); + table.Cell().Row(1).RowSpan(2).Column(1).ColumnSpan(2).Background(Colors.Grey.Lighten1).MinHeight(100); + table.Cell().Row(3).Column(3).Background(Colors.Grey.Darken1).MinHeight(50); }); }); } @@ -254,15 +233,15 @@ namespace QuestPDF.Examples columns.RelativeColumn(); }); - table.Cell().RowSpan(2).ColumnSpan(2).TextBox("1"); - table.Cell().ColumnSpan(2).TextBox("2"); - table.Cell().TextBox("3"); - table.Cell().TextBox("4"); - table.Cell().RowSpan(2).TextBox("5"); - table.Cell().ColumnSpan(2).TextBox("6"); - table.Cell().RowSpan(2).TextBox("7"); - table.Cell().TextBox("8"); - table.Cell().TextBox("9"); + table.Cell().RowSpan(2).ColumnSpan(2).Element(Block).Text("1"); + table.Cell().ColumnSpan(2).Element(Block).Text("2"); + table.Cell().Element(Block).Text("3"); + table.Cell().Element(Block).Text("4"); + table.Cell().RowSpan(2).Element(Block).Text("5"); + table.Cell().ColumnSpan(2).Element(Block).Text("6"); + table.Cell().RowSpan(2).Element(Block).Text("7"); + table.Cell().Element(Block).Text("8"); + table.Cell().Element(Block).Text("9"); }); }); } @@ -289,15 +268,15 @@ namespace QuestPDF.Examples columns.RelativeColumn(); }); - table.Cell().RowSpan(4).TextBox("1"); + table.Cell().RowSpan(4).Element(Block).Text("1"); - table.Cell().RowSpan(2).TextBox("2"); - table.Cell().RowSpan(1).TextBox("3"); - table.Cell().RowSpan(1).TextBox("4"); + table.Cell().RowSpan(2).Element(Block).Text("2"); + table.Cell().RowSpan(1).Element(Block).Text("3"); + table.Cell().RowSpan(1).Element(Block).Text("4"); - table.Cell().RowSpan(2).TextBox("5"); - table.Cell().RowSpan(1).TextBox("6"); - table.Cell().RowSpan(1).TextBox("7"); + table.Cell().RowSpan(2).Element(Block).Text("5"); + table.Cell().RowSpan(1).Element(Block).Text("6"); + table.Cell().RowSpan(1).Element(Block).Text("7"); }); }); } @@ -332,24 +311,38 @@ namespace QuestPDF.Examples .Border(1) .Decoration(decoration => { + IContainer DefaultCellStyle(IContainer container, string backgroundColor) + { + return container + .Border(1) + .BorderColor(Colors.Grey.Lighten1) + .Background(backgroundColor) + .PaddingVertical(5) + .PaddingHorizontal(10) + .AlignCenter() + .AlignMiddle(); + } + decoration .Header() .DefaultTextStyle(TextStyle.Default.SemiBold()) .Table(table => { table.ColumnsDefinition(DefineTableColumns); - table.DefaultCellStyle(cell => DefineDefaultCellStyle(cell, Colors.Grey.Lighten3)); - table.Cell().RowSpan(2).ExtendHorizontal().AlignLeft().Text("Document type"); + table.Cell().RowSpan(2).Element(CellStyle).ExtendHorizontal().AlignLeft().Text("Document type"); - table.Cell().ColumnSpan(2).Text("Inches"); - table.Cell().ColumnSpan(2).Text("Points"); + table.Cell().ColumnSpan(2).Element(CellStyle).Text("Inches"); + table.Cell().ColumnSpan(2).Element(CellStyle).Text("Points"); - table.Cell().Text("Width"); - table.Cell().Text("Height"); + table.Cell().Element(CellStyle).Text("Width"); + table.Cell().Element(CellStyle).Text("Height"); - table.Cell().Text("Width"); - table.Cell().Text("Height"); + table.Cell().Element(CellStyle).Text("Width"); + table.Cell().Element(CellStyle).Text("Height"); + + // you can extend already existing styles by creating additional methods + IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.Grey.Lighten3); }); decoration @@ -357,19 +350,20 @@ namespace QuestPDF.Examples .Table(table => { table.ColumnsDefinition(DefineTableColumns); - table.DefaultCellStyle(cell => DefineDefaultCellStyle(cell, Colors.White)); foreach (var page in pageSizes) { - table.Cell().ExtendHorizontal().AlignLeft().Text(page.name); + table.Cell().Element(CellStyle).ExtendHorizontal().AlignLeft().Text(page.name); // inches - table.Cell().Text(page.width); - table.Cell().Text(page.height); + table.Cell().Element(CellStyle).Text(page.width); + table.Cell().Element(CellStyle).Text(page.height); // points - table.Cell().Text(page.width * inchesToPoints); - table.Cell().Text(page.height * inchesToPoints); + table.Cell().Element(CellStyle).Text(page.width * inchesToPoints); + table.Cell().Element(CellStyle).Text(page.height * inchesToPoints); + + IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.White); } }); @@ -383,18 +377,6 @@ namespace QuestPDF.Examples columns.ConstantColumn(75); columns.ConstantColumn(75); } - - IContainer DefineDefaultCellStyle(IContainer container, string backgroundColor) - { - return container - .Border(1) - .BorderColor(Colors.Grey.Lighten1) - .Background(backgroundColor) - .PaddingVertical(5) - .PaddingHorizontal(10) - .AlignCenter() - .AlignMiddle(); - } }); }); } @@ -404,13 +386,13 @@ namespace QuestPDF.Examples { RenderingTest .Create() - .ProduceImages() + .ProducePdf() .PageSize(PageSizes.A4) .MaxPages(10_000) .EnableCaching() .EnableDebugging(false) .ShowResults() - .Render(container => GeneratePerformanceStructure(container, 1)); + .Render(container => GeneratePerformanceStructure(container, 1000)); } public static void GeneratePerformanceStructure(IContainer container, int repeats) @@ -479,20 +461,17 @@ namespace QuestPDF.Examples } }); } - } - - public static class TableTestsExtensions - { - public static void TextBox(this IContainer container, string text) + + // this method uses a higher order function to define a custom and dynamic style + static IContainer Block(IContainer container) { - container + return container .Border(1) .Background(Colors.Grey.Lighten3) .MinWidth(50) .MinHeight(50) .AlignCenter() - .AlignMiddle() - .Text(text, TextStyle.Default.Size(16)); + .AlignMiddle(); } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Table/Table.cs b/QuestPDF/Elements/Table/Table.cs index 5eb9402..89df77d 100644 --- a/QuestPDF/Elements/Table/Table.cs +++ b/QuestPDF/Elements/Table/Table.cs @@ -173,7 +173,7 @@ namespace QuestPDF.Elements.Table break; foreach (var row in Enumerable.Range(cell.Row, cell.Row - currentRow)) - rowBottomOffsets[row] = rowBottomOffsets[row - 1]; + rowBottomOffsets[row] = Math.Max(rowBottomOffsets[row - 1], rowBottomOffsets[row]); currentRow = cell.Row; } @@ -226,11 +226,8 @@ namespace QuestPDF.Elements.Table var maxRow = commands.Select(x => x.Cell).Max(x => x.Row + x.RowSpan); - if (maxRow > currentRow) - { - foreach (var row in Enumerable.Range(currentRow + 1, maxRow - currentRow)) - rowBottomOffsets[row] = rowBottomOffsets[row - 1]; - } + foreach (var row in Enumerable.Range(CurrentRow, maxRow - CurrentRow)) + rowBottomOffsets[row] = Math.Max(rowBottomOffsets[row - 1], rowBottomOffsets[row]); AdjustCellSizes(commands, rowBottomOffsets); @@ -246,7 +243,9 @@ namespace QuestPDF.Elements.Table { var lastRow = command.Cell.Row + command.Cell.RowSpan - 1; var height = rowBottomOffsets[lastRow] - command.Offset.Y; + command.Size = new Size(command.Size.Width, height); + command.Offset = new Position(command.Offset.X, rowBottomOffsets[command.Cell.Row - 1]); } } diff --git a/QuestPDF/Elements/Table/TableCell.cs b/QuestPDF/Elements/Table/TableCell.cs index 7512116..52d88a1 100644 --- a/QuestPDF/Elements/Table/TableCell.cs +++ b/QuestPDF/Elements/Table/TableCell.cs @@ -2,10 +2,10 @@ namespace QuestPDF.Elements.Table { internal class TableCell : Container, ITableCellContainer { - public int Row { get; set; } = 1; + public int Row { get; set; } = 0; public int RowSpan { get; set; } = 1; - public int Column { get; set; } = 1; + public int Column { get; set; } = 0; public int ColumnSpan { get; set; } = 1; public bool IsRendered { get; set; } diff --git a/QuestPDF/Elements/Table/TableLayoutPlanner.cs b/QuestPDF/Elements/Table/TableLayoutPlanner.cs index b5fba5b..8cd3891 100644 --- a/QuestPDF/Elements/Table/TableLayoutPlanner.cs +++ b/QuestPDF/Elements/Table/TableLayoutPlanner.cs @@ -25,6 +25,8 @@ namespace QuestPDF.Elements.Table .Where(x => x.Row + x.RowSpan > currentLocation.y) .ToList(); } + + SetPartialLocation(cell); if (cell.HasLocation()) { @@ -77,9 +79,21 @@ namespace QuestPDF.Elements.Table return neighbours.Any(cell.CollidesWith); } + private static void SetPartialLocation(this TableCell cell) + { + if (cell.Row == default && cell.Column == default) + return; + + if (cell.Row == default) + cell.Row = 1; + + if (cell.Column == default) + cell.Column = 1; + } + private static bool HasLocation(this TableCell cell) { - return cell.Row != 1 || cell.Column != 1; + return cell.Row != 0 && cell.Column != 0; } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Table/TableLayoutValidator.cs b/QuestPDF/Elements/Table/TableLayoutValidator.cs index b486ab5..7a44e3a 100644 --- a/QuestPDF/Elements/Table/TableLayoutValidator.cs +++ b/QuestPDF/Elements/Table/TableLayoutValidator.cs @@ -15,13 +15,21 @@ namespace QuestPDF.Elements.Table private static void ValidateCellPositions(int columnsCount, ICollection cells) { + const string prefix = "Detected issue in table cells configuration."; + foreach (var cell in cells) { + if (cell.Column < 1) + throw new DocumentComposeException($"{prefix} A cell column position should be greater or equal to 1. Got {cell.Column}."); + + if (cell.Row < 1) + throw new DocumentComposeException($"{prefix} A cell row position should be greater or equal to 1. Got {cell.Row}."); + if (cell.Column > columnsCount) - throw new DocumentLayoutException($"Cell location is incorrect. Cell starts at column that does not exist. Cell details: {GetCellDetails(cell)}."); + throw new DocumentComposeException($"{prefix} Cell starts at column that does not exist. Cell details: {GetCellDetails(cell)}."); if (cell.Column + cell.ColumnSpan - 1 > columnsCount) - throw new DocumentLayoutException($"Cell location is incorrect. Cell spans over columns that do not exist. Cell details: {GetCellDetails(cell)}."); + throw new DocumentComposeException($"{prefix} Table cell location is incorrect. Cell spans over columns that do not exist. Cell details: {GetCellDetails(cell)}."); } string GetCellDetails(TableCell cell) diff --git a/QuestPDF/Fluent/TableExtensions.cs b/QuestPDF/Fluent/TableExtensions.cs index 8cad9ad..09d50b3 100644 --- a/QuestPDF/Fluent/TableExtensions.cs +++ b/QuestPDF/Fluent/TableExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics; +using QuestPDF.Drawing.Exceptions; using QuestPDF.Elements; using QuestPDF.Elements.Table; using QuestPDF.Infrastructure; @@ -35,7 +36,6 @@ namespace QuestPDF.Fluent public class TableDescriptor { private Table Table { get; } - private Func DefaultCellStyleFunc { get; set; } = x => x; internal TableDescriptor(Table table) { @@ -52,29 +52,13 @@ namespace QuestPDF.Fluent { Table.ExtendLastCellsToTableBottom = true; } - - public void DefaultCellStyle(Func mapper) - { - DefaultCellStyleFunc = mapper; - } - + public ITableCellContainer Cell() { var cell = new TableCell(); Table.Cells.Add(cell); return cell; } - - internal void ApplyDefaultCellStyle() - { - foreach (var cell in Table.Cells) - { - var container = new Container(); - DefaultCellStyleFunc(container).Element(cell.Child); - - cell.Child = container; - } - } } public static class TableExtensions @@ -85,7 +69,6 @@ namespace QuestPDF.Fluent var descriptor = new TableDescriptor(table); handler(descriptor); - descriptor.ApplyDefaultCellStyle(); table.PlanCellPositions(); table.ValidateCellPositions();