Compare commits

...

29 Commits

Author SHA1 Message Date
Marcin Ziąbek 51508bbe0b Table: added support for headers and footers 2022-01-06 21:10:54 +01:00
Marcin Ziąbek 62b5054a2f Updated release notes 2022-01-06 00:40:32 +01:00
Marcin Ziąbek 51dac22422 2022.1.0-beta3 Renaming Box() to MinimalBox() 2022-01-06 00:22:07 +01:00
Marcin Ziąbek 01980e5635 Table: row height fixes for overlapping cells. Added code examples 2022-01-06 00:06:24 +01:00
Marcin Ziąbek 6e59fa6e3d Table: improved repeating+paging support, added example 2022-01-05 20:30:23 +01:00
Marcin Ziąbek 4003337d01 Fixed: traversing elements graph 2022-01-05 01:35:29 +01:00
Marcin Ziąbek 5931b12b90 VisitChildren -> GetChildren traversal refactorization 2022-01-05 01:10:05 +01:00
Marcin Ziąbek 935c575221 Table: added default style, fixed bugs, added examples 2022-01-05 00:52:28 +01:00
Marcin Ziąbek 18a710e2a3 Table: fixed cross-cell size adjusting 2022-01-03 17:13:57 +01:00
Marcin Ziąbek d39e93b924 Update Table.cs 2022-01-02 22:27:48 +01:00
MarcinZiabek c8cc968705 Typo fixes 2021-12-30 21:53:42 +01:00
MarcinZiabek ffccbe37a4 2022.1.0-beta0 2021-12-30 21:43:07 +01:00
MarcinZiabek a6439c1883 2022.1.0-alpha0 2021-12-30 21:28:09 +01:00
MarcinZiabek c0e4ec6df1 Border radius: added example 2021-12-30 21:07:34 +01:00
MarcinZiabek a80258aa58 Improved readibility of the table layout algorithm 2021-12-30 20:29:32 +01:00
MarcinZiabek 48c5771abb Table layout: refactorization 2021-12-30 19:56:51 +01:00
MarcinZiabek b79122aca5 Table: layouting improvements for paging 2021-12-30 19:42:40 +01:00
MarcinZiabek 2fcf543817 Fixed: corner cases of paging support 2021-12-30 18:40:54 +01:00
MarcinZiabek 7f72e50eb8 Improved wrapping support 2021-12-29 22:51:01 +01:00
MarcinZiabek f718cb3324 Table: optimized rendering performance 2021-12-27 18:24:37 +01:00
MarcinZiabek efdc8903a5 Table: minor optimizations + performance test 2021-12-26 17:04:59 +01:00
MarcinZiabek 647b1fd0f4 Rendering algorithm partially working 2021-12-25 11:53:39 +01:00
MarcinZiabek 8a62591940 Table: refactorization, cell placement algorithm 2021-12-24 14:40:54 +01:00
Marcin Ziąbek 6da3075e7a Improved table paging support 2021-12-19 05:11:23 +01:00
Marcin Ziąbek 0a27b80b73 Row: added support for mixed columns (constant + relational), good when performing visual column merging 2021-12-18 16:54:35 +01:00
Marcin Ziąbek 656d1a6ecf Improved Row element stability 2021-12-16 01:39:26 +01:00
Marcin Ziąbek 5f636beb9d Added missing assertions to test that are supposed to throw an exception 2021-12-09 17:10:45 +01:00
Marcin Ziąbek 10683776bb 2021.12.0 2021-12-06 09:28:02 +01:00
Marcin Ziąbek f2c47c4fd3 2021.12.0-alpha1 2021-11-30 00:34:04 +01:00
61 changed files with 1765 additions and 267 deletions
-32
View File
@@ -1,32 +0,0 @@
using System.IO;
using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class BarCode
{
[Test]
public void Example()
{
FontManager.RegisterFontType("LibreBarcode39", File.OpenRead("LibreBarcode39-Regular.ttf"));
RenderingTest
.Create()
.PageSize(400, 100)
.ShowResults()
.Render(container =>
{
container
.Background(Colors.White)
.AlignCenter()
.AlignMiddle()
.Text("*QuestPDF*", TextStyle.Default.FontType("LibreBarcode39").Size(64));
});
}
}
}
+12 -11
View File
@@ -1,4 +1,6 @@
using NUnit.Framework; using System.IO;
using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Examples.Engine; using QuestPDF.Examples.Engine;
using QuestPDF.Fluent; using QuestPDF.Fluent;
using QuestPDF.Helpers; using QuestPDF.Helpers;
@@ -6,25 +8,24 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Examples namespace QuestPDF.Examples
{ {
[TestFixture]
public class BarcodeExamples public class BarcodeExamples
{ {
[Test] [Test]
public void Barcode() public void Example()
{ {
FontManager.RegisterFontType("LibreBarcode39", File.OpenRead("LibreBarcode39-Regular.ttf"));
RenderingTest RenderingTest
.Create() .Create()
.PageSize(300, 300) .PageSize(400, 100)
.ShowResults()
.Render(container => .Render(container =>
{ {
container container
.Background("#FFF") .Background(Colors.White)
.Padding(25) .AlignCenter()
.Stack(stack => .AlignMiddle()
{ .Text("*QuestPDF*", TextStyle.Default.FontType("LibreBarcode39").Size(64));
stack.Item().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Barcode Example");
stack.Item().Border(1).Padding(5).AlignCenter().Text("*123456789*", TextStyle.Default.FontType("CarolinaBar-Demo-25E2").Size(20));
});
}); });
} }
} }
+57
View File
@@ -0,0 +1,57 @@
using Microcharts;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Examples
{
public class CanvasExamples
{
[Test]
public void BorderRadius()
{
RenderingTest
.Create()
.PageSize(175, 100)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Background(Colors.Grey.Lighten2)
.Padding(25)
.MinimalBox()
.Layers(layers =>
{
layers.Layer().Canvas((canvas, size) =>
{
DrawRoundedRectangle(Colors.White, false);
DrawRoundedRectangle(Colors.Blue.Darken2, true);
void DrawRoundedRectangle(string color, bool isStroke)
{
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);
}
});
layers
.PrimaryLayer()
.PaddingVertical(10)
.PaddingHorizontal(20)
.Text("Sample text", TextStyle.Default.Size(16).Color(Colors.Blue.Darken2).SemiBold());
});
});
}
}
}
+32 -9
View File
@@ -1,3 +1,4 @@
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using QuestPDF.Examples.Engine; using QuestPDF.Examples.Engine;
using QuestPDF.Fluent; using QuestPDF.Fluent;
@@ -43,19 +44,41 @@ namespace QuestPDF.Examples
RenderingTest RenderingTest
.Create() .Create()
.PageSize(300, 300) .PageSize(400, 600)
.ProduceImages()
.ShowResults() .ShowResults()
.Render(container => .Render(container =>
{ {
container.Extend().Canvas((canvas, size) => container
{ .Background(Colors.White)
var bar = new BarChart .Padding(25)
.Stack(stack =>
{ {
Entries = entries, stack
IsAnimated = false, .Item()
}; .PaddingBottom(10)
bar.Draw(canvas, (int)size.Width, (int)size.Height); .Text("Chart example", TextStyle.Default.Size(20).SemiBold().Color(Colors.Blue.Medium));
});
stack
.Item()
.Border(1)
.ExtendHorizontal()
.Height(300)
.Canvas((canvas, size) =>
{
var chart = new BarChart
{
Entries = entries,
LabelOrientation = Orientation.Horizontal,
ValueLabelOrientation = Orientation.Horizontal,
IsAnimated = false,
};
chart.DrawContent(canvas, (int)size.Width, (int)size.Height);
});
});
}); });
} }
} }
+3 -4
View File
@@ -16,8 +16,7 @@ namespace QuestPDF.Examples
.PageSize(PageSizes.A4) .PageSize(PageSizes.A4)
.ProducePdf() .ProducePdf()
.ShowResults() .ShowResults()
.Render(x => x.Image(new byte[] { 1, 2, 3 })); .Render(x => GenerateStructure(x, 16));
//.Render(x => GenerateStructure(x, 16));
} }
private void GenerateStructure(IContainer container, int level) private void GenerateStructure(IContainer container, int level)
@@ -33,7 +32,7 @@ namespace QuestPDF.Examples
if (level % 3 == 0) if (level % 3 == 0)
{ {
container container
.Border(level / 10f) .Border(level / 4f)
.BorderColor(Colors.Black) .BorderColor(Colors.Black)
.Row(row => .Row(row =>
{ {
@@ -44,7 +43,7 @@ namespace QuestPDF.Examples
else else
{ {
container container
.Border(level / 10f) .Border(level / 4f)
.BorderColor(Colors.Black) .BorderColor(Colors.Black)
.Stack(stack => .Stack(stack =>
{ {
+60 -16
View File
@@ -1,4 +1,5 @@
using NUnit.Framework; using NUnit.Framework;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Examples.Engine; using QuestPDF.Examples.Engine;
using QuestPDF.Fluent; using QuestPDF.Fluent;
using QuestPDF.Helpers; using QuestPDF.Helpers;
@@ -10,23 +11,66 @@ namespace QuestPDF.Examples
[Test] [Test]
public void Stack() public void Stack()
{ {
RenderingTest Assert.Throws<DocumentLayoutException>(() =>
.Create() {
.PageSize(500, 360) RenderingTest
.Render(container => .Create()
{ .PageSize(500, 360)
container .Render(container =>
.Background(Colors.White) {
.Padding(15) container
.Grid(grid => .Padding(10)
{ .Width(100)
grid.Spacing(15); .Background(Colors.Grey.Lighten3)
.DebugPointer("Example debug pointer")
.Stack(x =>
{
x.Item().Text("Test");
x.Item().Width(150);
});
});
});
}
grid.Item().Background(Colors.Grey.Medium).Text(Placeholders.LoremIpsum()); [Test]
grid.Item().DebugPointer("Test").Background(Colors.Grey.Lighten1).Height(1000); // <-- problem public void Simple()
grid.Item().Background(Colors.Grey.Lighten2).Height(150); {
}); Assert.Throws<DocumentLayoutException>(() =>
}); {
RenderingTest
.Create()
.PageSize(500, 360)
.Render(container =>
{
container
.Padding(10)
.Width(100)
.Background(Colors.Grey.Lighten3)
.Width(150)
.Text("Test");
});
});
}
[Test]
public void DebugPointer()
{
Assert.Throws<DocumentLayoutException>(() =>
{
RenderingTest
.Create()
.PageSize(500, 360)
.Render(container =>
{
container
.Background(Colors.Grey.Lighten3)
.Padding(10)
.Width(100)
.DebugPointer("Example debug pointer")
.Width(150)
.Text("Example");
});
});
} }
} }
} }
@@ -0,0 +1,53 @@
using System.Drawing;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class DefaultTextStyleExamples
{
[Test]
public void Placeholder()
{
RenderingTest
.Create()
.PageSize(220, 270)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(container =>
{
container
.Padding(10)
.DefaultTextStyle(TextStyle.Default.Bold().Underline())
.Stack(stack =>
{
stack.Item().Text("Default style applies to all children", TextStyle.Default);
stack.Item().Text("You can override certain styles", TextStyle.Default.Underline(false).Color(Colors.Green.Darken2));
stack.Item().PaddingTop(10).Border(1).Grid(grid =>
{
grid.Columns(4);
foreach (var i in Enumerable.Range(1, 16))
{
grid.Item()
.Border(1)
.BorderColor(Colors.Grey.Lighten1)
.Background(Colors.Grey.Lighten3)
.Width(50)
.Height(50)
.AlignCenter()
.AlignMiddle()
.Text(i, TextStyle.Default.Size(16 + i / 4));
}
});
});
});
}
}
}
@@ -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();
});
});
});
}
}
}
+4 -4
View File
@@ -474,7 +474,7 @@ namespace QuestPDF.Examples
.Padding(15) .Padding(15)
.Border(4) .Border(4)
.BorderColor(Colors.Blue.Medium) .BorderColor(Colors.Blue.Medium)
//.Box() //.MinimalBox()
.Background(Colors.Grey.Lighten2) .Background(Colors.Grey.Lighten2)
.Padding(15) .Padding(15)
.Text("Test of the \n box element", TextStyle.Default.Size(20)); .Text("Test of the \n box element", TextStyle.Default.Size(20));
@@ -542,7 +542,7 @@ namespace QuestPDF.Examples
{ {
container container
.Background("#FFF") .Background("#FFF")
.Box() .MinimalBox()
.Padding(25) .Padding(25)
@@ -588,7 +588,7 @@ namespace QuestPDF.Examples
return element; return element;
}) })
.Box() .MinimalBox()
.Background(Colors.White) .Background(Colors.White)
.Padding(10) .Padding(10)
.Text($"Rotated {turns * 90}°", TextStyle.Default.Size(16)); .Text($"Rotated {turns * 90}°", TextStyle.Default.Size(16));
@@ -686,7 +686,7 @@ namespace QuestPDF.Examples
return element; return element;
}) })
.Box() .MinimalBox()
.Background(Colors.White) .Background(Colors.White)
.Padding(10) .Padding(10)
.Text($"Flipped {turns}", TextStyle.Default.Size(16)); .Text($"Flipped {turns}", TextStyle.Default.Size(16));
+21 -6
View File
@@ -20,6 +20,8 @@ namespace QuestPDF.Examples.Engine
private Size Size { get; set; } private Size Size { get; set; }
private int? MaxPagesThreshold { get; set; } private int? MaxPagesThreshold { get; set; }
private bool ShowResult { get; set; } private bool ShowResult { get; set; }
private bool ApplyCaching { get; set; }
private bool ApplyDebugging { get; set; }
private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images; private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images;
private RenderingTest() private RenderingTest()
@@ -67,6 +69,24 @@ namespace QuestPDF.Examples.Engine
return this; return this;
} }
public RenderingTest MaxPages(int value)
{
MaxPagesThreshold = value;
return this;
}
public RenderingTest EnableCaching(bool value = true)
{
ApplyCaching = value;
return this;
}
public RenderingTest EnableDebugging(bool value = true)
{
ApplyDebugging = value;
return this;
}
public void Render(Action<IContainer> content) public void Render(Action<IContainer> content)
{ {
RenderDocument(container => RenderDocument(container =>
@@ -79,15 +99,10 @@ namespace QuestPDF.Examples.Engine
}); });
} }
public void MaxPages(int value)
{
MaxPagesThreshold = value;
}
public void RenderDocument(Action<IDocumentContainer> content) public void RenderDocument(Action<IDocumentContainer> content)
{ {
MaxPagesThreshold ??= ResultType == RenderingTestResult.Pdf ? 1000 : 10; MaxPagesThreshold ??= ResultType == RenderingTestResult.Pdf ? 1000 : 10;
var document = new SimpleDocument(content, MaxPagesThreshold.Value); var document = new SimpleDocument(content, MaxPagesThreshold.Value, ApplyCaching, ApplyDebugging);
Render(document); Render(document);
} }
+8 -2
View File
@@ -13,11 +13,15 @@ namespace QuestPDF.Examples.Engine
private Action<IDocumentContainer> Content { get; } private Action<IDocumentContainer> Content { get; }
private int MaxPages { get; } private int MaxPages { get; }
private bool ApplyCaching { get; }
private bool ApplyDebugging { get; }
public SimpleDocument(Action<IDocumentContainer> content, int maxPages) public SimpleDocument(Action<IDocumentContainer> content, int maxPages, bool applyCaching, bool applyDebugging)
{ {
Content = content; Content = content;
MaxPages = maxPages; MaxPages = maxPages;
ApplyCaching = applyCaching;
ApplyDebugging = applyDebugging;
} }
public DocumentMetadata GetMetadata() public DocumentMetadata GetMetadata()
@@ -25,7 +29,9 @@ namespace QuestPDF.Examples.Engine
return new DocumentMetadata() return new DocumentMetadata()
{ {
RasterDpi = PageSizes.PointsPerInch * ImageScalingFactor, RasterDpi = PageSizes.PointsPerInch * ImageScalingFactor,
DocumentLayoutExceptionThreshold = MaxPages DocumentLayoutExceptionThreshold = MaxPages,
ApplyCaching = ApplyCaching,
ApplyDebugging = ApplyDebugging
}; };
} }
+6 -4
View File
@@ -12,12 +12,14 @@ namespace QuestPDF.Examples
{ {
return container return container
.Border(1) .Border(1)
.Background(dark ? Colors.Grey.Lighten2 : Colors.White) .BorderColor(Colors.Grey.Medium)
.Padding(10); .Background(dark ? Colors.Grey.Lighten3 : Colors.White)
.Padding(5);
} }
public static void LabelCell(this IContainer container, string text) => container.Cell(true).Text(text, TextStyle.Default.Medium()); public static void LabelCell(this IContainer container, string text) => container.Cell(true).Text(text, TextStyle.Default.SemiBold());
public static IContainer ValueCell(this IContainer container) => container.Cell(false); public static IContainer ValueCell(this IContainer container) => container.Cell(false);
public static void ValueCell(this IContainer container, string text) => container.ValueCell().Text(text, TextStyle.Default);
} }
public class FrameExample public class FrameExample
@@ -36,7 +38,7 @@ namespace QuestPDF.Examples
.Padding(25) .Padding(25)
.Stack(stack => .Stack(stack =>
{ {
for(var i=1; i<=4; i++) for(var i = 1; i <= 4; i++)
{ {
stack.Item().Row(row => stack.Item().Row(row =>
{ {
+10 -6
View File
@@ -1,5 +1,6 @@
using System.IO; using System.IO;
using NUnit.Framework; using NUnit.Framework;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Examples.Engine; using QuestPDF.Examples.Engine;
using QuestPDF.Fluent; using QuestPDF.Fluent;
using QuestPDF.Helpers; using QuestPDF.Helpers;
@@ -36,12 +37,15 @@ namespace QuestPDF.Examples
[Test] [Test]
public void Exception() public void Exception()
{ {
RenderingTest Assert.Throws<DocumentComposeException>(() =>
.Create() {
.PageSize(PageSizes.A5) RenderingTest
.ProducePdf() .Create()
.ShowResults() .PageSize(PageSizes.A2)
.Render(page => page.Image("non_existent.png")); .ProducePdf()
.ShowResults()
.Render(page => page.Image("non_existent.png"));
});
} }
} }
} }
+2 -2
View File
@@ -37,7 +37,7 @@ namespace QuestPDF.Examples
decoration decoration
.Content() .Content()
.PaddingTop(25) .PaddingTop(25)
//.Box() //.MinimalBox()
.Border(1) .Border(1)
.Background(Colors.Grey.Lighten2) .Background(Colors.Grey.Lighten2)
.Inlined(inlined => .Inlined(inlined =>
@@ -94,7 +94,7 @@ namespace QuestPDF.Examples
{ {
container container
.Padding(20) .Padding(20)
.Box() .MinimalBox()
.Border(1) .Border(1)
.Background(Colors.Grey.Lighten4) .Background(Colors.Grey.Lighten4)
.Inlined(inlined => .Inlined(inlined =>
+2
View File
@@ -36,6 +36,8 @@ namespace QuestPDF.Examples
RenderingTest RenderingTest
.Create() .Create()
.PageSize(350, 280) .PageSize(350, 280)
.ProducePdf()
.ShowResults()
.Render(container => .Render(container =>
{ {
container container
@@ -6,6 +6,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="microcharts" Version="0.9.5.9" /> <PackageReference Include="microcharts" Version="0.9.5.9" />
<PackageReference Include="nunit" Version="3.13.2" /> <PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
+94
View File
@@ -0,0 +1,94 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class RowExamples
{
[Test]
public void ColumnTypes()
{
RenderingTest
.Create()
.ProducePdf()
.PageSize(650, 300)
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.MinimalBox()
.Border(1)
.Stack(stack =>
{
stack.Item().LabelCell("Total width: 600px");
stack.Item().Row(row =>
{
row.ConstantColumn(150).ValueCell("150px");
row.ConstantColumn(100).ValueCell("100px");
row.RelativeColumn(4).ValueCell("200px");
row.RelativeColumn(3).ValueCell("150px");
});
stack.Item().Row(row =>
{
row.ConstantColumn(100).ValueCell("100px");
row.ConstantColumn(50).ValueCell("50px");
row.Column(constantWidth: 100, relativeWidth: 4).ValueCell("350px");
row.RelativeColumn(2).ValueCell("100px");
row.RelativeColumn(1).ValueCell("50px");
});
});
});
}
[Test]
public void Stability()
{
// up to version 2021.12, this code would always result with the infinite layout exception
RenderingTest
.Create()
.ProducePdf()
.MaxPages(100)
.PageSize(250, 150)
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.Row(row =>
{
row.RelativeColumn().Stack(stack =>
{
stack.Item().ShowOnce().Element(CreateBox).Text("X");
stack.Item().Element(CreateBox).Text("1");
stack.Item().Element(CreateBox).Text("2");
});
row.RelativeColumn().Stack(stack =>
{
stack.Item().Element(CreateBox).Text("1");
stack.Item().Element(CreateBox).Text("2");
});
});
});
static IContainer CreateBox(IContainer container)
{
return container
.ExtendHorizontal()
.ExtendVertical()
.Background(Colors.Grey.Lighten4)
.Border(1)
.AlignCenter()
.AlignMiddle()
.ShowOnce();
}
}
}
}
+465
View File
@@ -0,0 +1,465 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using IContainer = QuestPDF.Infrastructure.IContainer;
namespace QuestPDF.Examples
{
public class TableExamples
{
[Test]
public void BasicPlacement()
{
RenderingTest
.Create()
.ProduceImages()
.PageSize(220, 220)
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
// 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");
});
});
}
[Test]
public void DefaultCellStyle()
{
RenderingTest
.Create()
.ProduceImages()
.PageSize(220, 120)
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.DefaultTextStyle(TextStyle.Default.Size(16))
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
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");
});
});
}
[Test]
public void ColumnsDefinition()
{
RenderingTest
.Create()
.ProduceImages()
.PageSize(320, 80)
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(50);
columns.ConstantColumn(100);
columns.RelativeColumn(2);
columns.RelativeColumn(3);
});
table.Cell().ColumnSpan(4).LabelCell("Total width: 300px");
table.Cell().ValueCell("50px");
table.Cell().ValueCell("100px");
table.Cell().ValueCell("100px");
table.Cell().ValueCell("150px");
});
});
}
[Test]
public void PartialAutoPlacement()
{
RenderingTest
.Create()
.ProduceImages()
.PageSize(220, 220)
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
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)
.MinimalBox()
.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()
.ProduceImages()
.PageSize(170, 170)
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
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);
});
});
}
[Test]
public void Spans()
{
RenderingTest
.Create()
.ProduceImages()
.PageSize(220, 220)
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
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");
});
});
}
[Test]
public void Stability()
{
RenderingTest
.Create()
.ProduceImages()
.PageSize(300, 300)
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
table.Cell().RowSpan(4).Element(Block).Text("1");
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).Element(Block).Text("5");
table.Cell().RowSpan(1).Element(Block).Text("6");
table.Cell().RowSpan(1).Element(Block).Text("7");
});
});
}
[Test]
public void TableHeader()
{
RenderingTest
.Create()
.ProduceImages()
.PageSize(500, 200)
.ShowResults()
.EnableDebugging()
.Render(container =>
{
var pageSizes = new List<(string name, double width, double height)>()
{
("Letter (ANSI A)", 8.5f, 11),
("Legal", 8.5f, 14),
("Ledger (ANSI B)", 11, 17),
("Tabloid (ANSI B)", 17, 11),
("ANSI C", 22, 17),
("ANSI D", 34, 22),
("ANSI E", 44, 34)
};
const int inchesToPoints = 72;
container
.Padding(10)
.MinimalBox()
.Border(1)
.Table(table =>
{
IContainer DefaultCellStyle(IContainer container, string backgroundColor)
{
return container
.Border(1)
.BorderColor(Colors.Grey.Lighten1)
.Background(backgroundColor)
.PaddingVertical(5)
.PaddingHorizontal(10)
.AlignCenter()
.AlignMiddle();
}
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.ConstantColumn(75);
columns.ConstantColumn(75);
columns.ConstantColumn(75);
columns.ConstantColumn(75);
});
table.Header(header =>
{
header.Cell().RowSpan(2).Element(CellStyle).ExtendHorizontal().AlignLeft().Text("Document type");
header.Cell().ColumnSpan(2).Element(CellStyle).Text("Inches");
header.Cell().ColumnSpan(2).Element(CellStyle).Text("Points");
header.Cell().Element(CellStyle).Text("Width");
header.Cell().Element(CellStyle).Text("Height");
header.Cell().Element(CellStyle).Text("Width");
header.Cell().Element(CellStyle).Text("Height");
// you can extend already existing styles by creating additional methods
IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.Grey.Lighten3);
});
foreach (var page in pageSizes)
{
table.Cell().Element(CellStyle).ExtendHorizontal().AlignLeft().Text(page.name);
// inches
table.Cell().Element(CellStyle).Text(page.width);
table.Cell().Element(CellStyle).Text(page.height);
// points
table.Cell().Element(CellStyle).Text(page.width * inchesToPoints);
table.Cell().Element(CellStyle).Text(page.height * inchesToPoints);
IContainer CellStyle(IContainer container) => DefaultCellStyle(container, Colors.White);
}
});
});
}
[Test]
public void PerformanceText_TemperatureReport()
{
RenderingTest
.Create()
.ProducePdf()
.PageSize(PageSizes.A4)
.MaxPages(10_000)
.EnableCaching()
.EnableDebugging(false)
.ShowResults()
.Render(container => GeneratePerformanceStructure(container, 1000));
}
public static void GeneratePerformanceStructure(IContainer container, int repeats)
{
container
.Padding(25)
//.Background(Colors.Blue.Lighten2)
.MinimalBox()
.Border(1)
//.Background(Colors.Red.Lighten2)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(100);
columns.RelativeColumn();
columns.ConstantColumn(100);
columns.RelativeColumn();
});
table.ExtendLastCellsToTableBottom();
foreach (var i in Enumerable.Range(0, repeats))
{
table.Cell().RowSpan(3).LabelCell("Project");
table.Cell().RowSpan(3).ShowEntire().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("Inspector");
table.Cell().ValueCell("Marcin Ziąbek");
table.Cell().ColumnSpan(2).LabelCell("Morning weather");
table.Cell().ColumnSpan(2).LabelCell("Evening weather");
table.Cell().ValueCell("Time");
table.Cell().ValueCell("7:13");
table.Cell().ValueCell("Time");
table.Cell().ValueCell("18:25");
table.Cell().ValueCell("Description");
table.Cell().ValueCell("Sunny");
table.Cell().ValueCell("Description");
table.Cell().ValueCell("Windy");
table.Cell().ValueCell("Wind");
table.Cell().ValueCell("Mild");
table.Cell().ValueCell("Wind");
table.Cell().ValueCell("Strong");
table.Cell().ValueCell("Temperature");
table.Cell().ValueCell("17°C");
table.Cell().ValueCell("Temperature");
table.Cell().ValueCell("32°C");
table.Cell().LabelCell("Remarks");
table.Cell().ColumnSpan(3).ValueCell(Placeholders.Paragraph());
}
});
}
// this method uses a higher order function to define a custom and dynamic style
static IContainer Block(IContainer container)
{
return container
.Border(1)
.Background(Colors.Grey.Lighten3)
.MinWidth(50)
.MinHeight(50)
.AlignCenter()
.AlignMiddle();
}
}
}
+7 -7
View File
@@ -24,7 +24,7 @@ namespace QuestPDF.Examples
{ {
container container
.Padding(5) .Padding(5)
.Box() .MinimalBox()
.Border(1) .Border(1)
.Padding(10) .Padding(10)
.Text(text => .Text(text =>
@@ -48,7 +48,7 @@ namespace QuestPDF.Examples
{ {
container container
.Padding(5) .Padding(5)
.Box() .MinimalBox()
.Border(1) .Border(1)
.Padding(10) .Padding(10)
.Text(text => .Text(text =>
@@ -76,7 +76,7 @@ namespace QuestPDF.Examples
{ {
container container
.Padding(5) .Padding(5)
.Box() .MinimalBox()
.Border(1) .Border(1)
.Padding(10) .Padding(10)
.Text(text => .Text(text =>
@@ -108,7 +108,7 @@ namespace QuestPDF.Examples
container container
.Padding(20) .Padding(20)
.Padding(10) .Padding(10)
.Box() .MinimalBox()
.Border(1) .Border(1)
.Padding(5) .Padding(5)
.Padding(10) .Padding(10)
@@ -138,7 +138,7 @@ namespace QuestPDF.Examples
container container
.Padding(20) .Padding(20)
.Padding(10) .Padding(10)
.Box() .MinimalBox()
.Border(1) .Border(1)
.Padding(5) .Padding(5)
.Padding(10) .Padding(10)
@@ -167,7 +167,7 @@ namespace QuestPDF.Examples
container container
.Padding(20) .Padding(20)
.Padding(10) .Padding(10)
.Box() .MinimalBox()
.Border(1) .Border(1)
.Padding(5) .Padding(5)
.Padding(10) .Padding(10)
@@ -230,7 +230,7 @@ namespace QuestPDF.Examples
container container
.Padding(20) .Padding(20)
.Padding(10) .Padding(10)
.Box() .MinimalBox()
.Border(1) .Border(1)
.Padding(5) .Padding(5)
.Padding(10) .Padding(10)
@@ -31,12 +31,12 @@ namespace QuestPDF.ReportSample.Layouts
stack.Item().ShowOnce().Padding(5).AlignMiddle().Row(row => stack.Item().ShowOnce().Padding(5).AlignMiddle().Row(row =>
{ {
row.RelativeColumn(2).AlignMiddle().Text("PRIMARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold()); row.RelativeColumn(2).AlignMiddle().Text("PRIMARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
row.RelativeColumn(1).AlignRight().Box().AlignMiddle().Background(Colors.Blue.Darken2).Padding(30); row.RelativeColumn(1).AlignRight().MinimalBox().AlignMiddle().Background(Colors.Blue.Darken2).Padding(30);
}); });
stack.Item().SkipOnce().Padding(5).Row(row => stack.Item().SkipOnce().Padding(5).Row(row =>
{ {
row.RelativeColumn(2).Text("SECONDARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold()); row.RelativeColumn(2).Text("SECONDARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold());
row.RelativeColumn(1).AlignRight().Box().Background(Colors.Blue.Lighten4).Padding(15); row.RelativeColumn(1).AlignRight().MinimalBox().Background(Colors.Blue.Lighten4).Padding(15);
}); });
}); });
} }
+2 -1
View File
@@ -11,6 +11,7 @@ using NUnit.Framework;
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Drawing.Proxy; using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
using QuestPDF.ReportSample.Layouts; using QuestPDF.ReportSample.Layouts;
@@ -55,7 +56,7 @@ namespace QuestPDF.ReportSample
var sw = new Stopwatch(); var sw = new Stopwatch();
sw.Start(); sw.Start();
Content.HandleVisitor(x => Content.VisitChildren(x =>
{ {
if (x is ICacheable) if (x is ICacheable)
x.CreateProxy(y => new CacheProxy(y)); x.CreateProxy(y => new CacheProxy(y));
+2
View File
@@ -24,6 +24,8 @@ namespace QuestPDF.ReportSample
[Test] [Test]
public void GenerateAndShowPdf() public void GenerateAndShowPdf()
{ {
ImagePlaceholder.Solid = true;
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf"); var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf");
Report.GeneratePdf(path); Report.GeneratePdf(path);
Process.Start("explorer.exe", path); Process.Start("explorer.exe", path);
+4 -4
View File
@@ -10,13 +10,13 @@ namespace QuestPDF.UnitTests
public class BoxTests public class BoxTests
{ {
[Test] [Test]
public void Measure() => SimpleContainerTests.Measure<Box>(); public void Measure() => SimpleContainerTests.Measure<MinimalBox>();
[Test] [Test]
public void Draw_Wrap() public void Draw_Wrap()
{ {
TestPlan TestPlan
.For(x => new Box .For(x => new MinimalBox
{ {
Child = x.CreateChild() Child = x.CreateChild()
}) })
@@ -29,7 +29,7 @@ namespace QuestPDF.UnitTests
public void Measure_PartialRender() public void Measure_PartialRender()
{ {
TestPlan TestPlan
.For(x => new Box .For(x => new MinimalBox
{ {
Child = x.CreateChild() Child = x.CreateChild()
}) })
@@ -43,7 +43,7 @@ namespace QuestPDF.UnitTests
public void Measure_FullRender() public void Measure_FullRender()
{ {
TestPlan TestPlan
.For(x => new Box .For(x => new MinimalBox
{ {
Child = x.CreateChild() Child = x.CreateChild()
}) })
+6 -6
View File
@@ -213,7 +213,7 @@ namespace QuestPDF.UnitTests.TestEngine
public TestPlan CheckMeasureResult(SpacePlan expected) public TestPlan CheckMeasureResult(SpacePlan expected)
{ {
Element.HandleVisitor(x => x?.Initialize(null, Canvas)); Element.VisitChildren(x => x?.Initialize(null, Canvas));
var actual = Element.Measure(OperationInput); var actual = Element.Measure(OperationInput);
@@ -228,7 +228,7 @@ namespace QuestPDF.UnitTests.TestEngine
public TestPlan CheckDrawResult() public TestPlan CheckDrawResult()
{ {
Element.HandleVisitor(x => x?.Initialize(null, Canvas)); Element.VisitChildren(x => x?.Initialize(null, Canvas));
Element.Draw(OperationInput); Element.Draw(OperationInput);
return this; return this;
} }
@@ -271,10 +271,10 @@ namespace QuestPDF.UnitTests.TestEngine
availableSpace ??= new Size(400, 300); availableSpace ??= new Size(400, 300);
var canvas = new FreeCanvas(); var canvas = new FreeCanvas();
value.HandleVisitor(x => x.Initialize(null, canvas)); value.VisitChildren(x => x.Initialize(null, canvas));
var valueMeasure = value.Measure(availableSpace.Value); var valueMeasure = value.Measure(availableSpace.Value);
expected.HandleVisitor(x => x.Initialize(null, canvas)); expected.VisitChildren(x => x.Initialize(null, canvas));
var expectedMeasure = expected.Measure(availableSpace.Value); var expectedMeasure = expected.Measure(availableSpace.Value);
valueMeasure.Should().BeEquivalentTo(expectedMeasure); valueMeasure.Should().BeEquivalentTo(expectedMeasure);
@@ -285,11 +285,11 @@ namespace QuestPDF.UnitTests.TestEngine
availableSpace ??= new Size(400, 300); availableSpace ??= new Size(400, 300);
var valueCanvas = new OperationRecordingCanvas(); var valueCanvas = new OperationRecordingCanvas();
value.HandleVisitor(x => x.Initialize(null, valueCanvas)); value.VisitChildren(x => x.Initialize(null, valueCanvas));
value.Draw(availableSpace.Value); value.Draw(availableSpace.Value);
var expectedCanvas = new OperationRecordingCanvas(); var expectedCanvas = new OperationRecordingCanvas();
expected.HandleVisitor(x => x.Initialize(null, expectedCanvas)); expected.VisitChildren(x => x.Initialize(null, expectedCanvas));
expected.Draw(availableSpace.Value); expected.Draw(availableSpace.Value);
valueCanvas.Operations.Should().BeEquivalentTo(expectedCanvas.Operations); valueCanvas.Operations.Should().BeEquivalentTo(expectedCanvas.Operations);
+34 -25
View File
@@ -7,6 +7,8 @@ using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Elements.Text; using QuestPDF.Elements.Text;
using QuestPDF.Elements.Text.Items; using QuestPDF.Elements.Text.Items;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing namespace QuestPDF.Drawing
@@ -42,20 +44,15 @@ namespace QuestPDF.Drawing
var container = new DocumentContainer(); var container = new DocumentContainer();
document.Compose(container); document.Compose(container);
var content = container.Compose(); var content = container.Compose();
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
var metadata = document.GetMetadata(); var metadata = document.GetMetadata();
var pageContext = new PageContext(); var pageContext = new PageContext();
DebuggingState debuggingState = null; var debuggingState = metadata.ApplyDebugging ? ApplyDebugging(content) : null;
if (System.Diagnostics.Debugger.IsAttached) if (metadata.ApplyCaching)
{
debuggingState = ApplyDebugging(content);
}
else
{
ApplyCaching(content); ApplyCaching(content);
}
RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState); RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
RenderPass(pageContext, canvas, content, metadata, debuggingState); RenderPass(pageContext, canvas, content, metadata, debuggingState);
@@ -64,8 +61,8 @@ namespace QuestPDF.Drawing
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DocumentMetadata documentMetadata, DebuggingState? debuggingState) internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DocumentMetadata documentMetadata, DebuggingState? debuggingState)
where TCanvas : ICanvas, IRenderingCanvas where TCanvas : ICanvas, IRenderingCanvas
{ {
content.HandleVisitor(x => x?.Initialize(pageContext, canvas)); content.VisitChildren(x => x?.Initialize(pageContext, canvas));
content.HandleVisitor(x => (x as IStateResettable)?.ResetState()); content.VisitChildren(x => (x as IStateResettable)?.ResetState());
canvas.BeginDocument(); canvas.BeginDocument();
@@ -128,7 +125,7 @@ namespace QuestPDF.Drawing
private static void ApplyCaching(Container content) private static void ApplyCaching(Container content)
{ {
content.HandleVisitor(x => content.VisitChildren(x =>
{ {
if (x is ICacheable) if (x is ICacheable)
x.CreateProxy(y => new CacheProxy(y)); x.CreateProxy(y => new CacheProxy(y));
@@ -139,7 +136,7 @@ namespace QuestPDF.Drawing
{ {
var debuggingState = new DebuggingState(); var debuggingState = new DebuggingState();
content.HandleVisitor(x => content.VisitChildren(x =>
{ {
x.CreateProxy(y => new DebuggingProxy(debuggingState, y)); x.CreateProxy(y => new DebuggingProxy(debuggingState, y));
}); });
@@ -147,26 +144,38 @@ namespace QuestPDF.Drawing
return debuggingState; return debuggingState;
} }
internal static void ApplyDefaultTextStyle(this Element content, TextStyle documentDefaultTextStyle) private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
{ {
documentDefaultTextStyle.ApplyGlobalStyle(TextStyle.LibraryDefault); if (content == null)
return;
content.HandleVisitor(element => if (content is TextBlock textBlock)
{ {
var text = element as TextBlock; foreach (var textBlockItem in textBlock.Items)
if (text == null)
return;
foreach (var child in text.Children)
{ {
if (child is TextBlockSpan textSpan) if (textBlockItem is TextBlockSpan textSpan)
{
textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle); textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle);
}
if (child is TextBlockElement textElement) else if (textBlockItem is TextBlockElement textElement)
{
ApplyDefaultTextStyle(textElement.Element, documentDefaultTextStyle); ApplyDefaultTextStyle(textElement.Element, documentDefaultTextStyle);
}
} }
});
return;
}
var targetTextStyle = documentDefaultTextStyle;
if (content is DefaultTextStyle defaultTextStyleElement)
{
defaultTextStyleElement.TextStyle.ApplyParentStyle(documentDefaultTextStyle);
targetTextStyle = defaultTextStyleElement.TextStyle;
}
foreach (var child in content.GetChildren())
ApplyDefaultTextStyle(child, targetTextStyle);
} }
} }
} }
+3
View File
@@ -24,6 +24,9 @@ namespace QuestPDF.Drawing
/// </summary> /// </summary>
public int DocumentLayoutExceptionThreshold { get; set; } = 250; public int DocumentLayoutExceptionThreshold { get; set; } = 250;
public bool ApplyCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
public bool ApplyDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
public static DocumentMetadata Default => new DocumentMetadata(); public static DocumentMetadata Default => new DocumentMetadata();
} }
} }
+2 -2
View File
@@ -76,7 +76,7 @@ namespace QuestPDF.Drawing.Proxy
title += debugPointer.Highlight ? " 🌟" : string.Empty; title += debugPointer.Highlight ? " 🌟" : string.Empty;
} }
if (item.SpacePlan.Type == SpacePlanType.Wrap) if (item.SpacePlan.Type != SpacePlanType.FullRender)
title = "🔥 " + title; title = "🔥 " + title;
builder.AppendLine(indent + title); builder.AppendLine(indent + title);
@@ -109,7 +109,7 @@ namespace QuestPDF.Drawing.Proxy
Value = x.GetValue(element) Value = x.GetValue(element)
}) })
.Where(x => !(x.Value is IElement)) .Where(x => !(x.Value is IElement))
.Where(x => !(x.Value is IEnumerable<IElement>)) .Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle)) .Where(x => !(x.Value is TextStyle))
.Select(x => $"{x.Property}: {FormatValue(x.Value)}"); .Select(x => $"{x.Property}: {FormatValue(x.Value)}");
+1 -1
View File
@@ -31,7 +31,7 @@ namespace QuestPDF.Drawing
if (Type == SpacePlanType.Wrap) if (Type == SpacePlanType.Wrap)
return Type.ToString(); return Type.ToString();
return $"{Type} | width {Width:N2} | height {Height:N2}"; return $"{Type} (Width: {Width:N3}, Height: {Height:N3})";
} }
public static implicit operator Size(SpacePlan spacePlan) public static implicit operator Size(SpacePlan spacePlan)
+2 -2
View File
@@ -22,9 +22,9 @@ namespace QuestPDF.Elements
if (Handler == null || skiaCanvas == null) if (Handler == null || skiaCanvas == null)
return; return;
var currentMatrix = skiaCanvas.TotalMatrix; var originalMatrix = skiaCanvas.TotalMatrix;
Handler.Invoke(skiaCanvas, availableSpace); Handler.Invoke(skiaCanvas, availableSpace);
skiaCanvas.SetMatrix(currentMatrix); skiaCanvas.SetMatrix(originalMatrix);
} }
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@ namespace QuestPDF.Elements
.Layer() .Layer()
.ShowIf(!string.IsNullOrWhiteSpace(Text)) .ShowIf(!string.IsNullOrWhiteSpace(Text))
.AlignCenter() .AlignCenter()
.Box() .MinimalBox()
.Background(Colors.White) .Background(Colors.White)
.Padding(2) .Padding(2)
.Text(Text, TextStyle.Default.Color(Color).FontType(Fonts.Consolas).Size(8)); .Text(Text, TextStyle.Default.Color(Color).FontType(Fonts.Consolas).Size(8));
+4 -5
View File
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Fluent; using QuestPDF.Fluent;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -17,12 +18,10 @@ namespace QuestPDF.Elements
public Element ContentElement { get; set; } = Empty.Instance; public Element ContentElement { get; set; } = Empty.Instance;
public DecorationType Type { get; set; } public DecorationType Type { get; set; }
internal override void HandleVisitor(Action<Element?> visit) internal override IEnumerable<Element?> GetChildren()
{ {
DecorationElement?.HandleVisitor(visit); yield return DecorationElement;
ContentElement?.HandleVisitor(visit); yield return ContentElement;
base.HandleVisitor(visit);
} }
internal override void CreateProxy(Func<Element, Element> create) internal override void CreateProxy(Func<Element, Element> create)
+10
View File
@@ -0,0 +1,10 @@
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class DefaultTextStyle : ContainerElement
{
public TextStyle TextStyle { get; set; } = TextStyle.Default;
}
}
+2 -3
View File
@@ -44,10 +44,9 @@ namespace QuestPDF.Elements
ChildrenQueue = new Queue<InlinedElement>(Elements); ChildrenQueue = new Queue<InlinedElement>(Elements);
} }
internal override void HandleVisitor(Action<Element?> visit) internal override IEnumerable<Element?> GetChildren()
{ {
Elements.ForEach(x => x.HandleVisitor(visit)); return Elements;
base.HandleVisitor(visit);
} }
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
+2 -3
View File
@@ -15,10 +15,9 @@ namespace QuestPDF.Elements
{ {
public List<Layer> Children { get; set; } = new List<Layer>(); public List<Layer> Children { get; set; } = new List<Layer>();
internal override void HandleVisitor(Action<Element?> visit) internal override IEnumerable<Element?> GetChildren()
{ {
Children.ForEach(x => x.HandleVisitor(visit)); return Children;
base.HandleVisitor(visit);
} }
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
@@ -3,7 +3,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements namespace QuestPDF.Elements
{ {
internal class Box : ContainerElement internal class MinimalBox : ContainerElement
{ {
internal override void Draw(Size availableSpace) internal override void Draw(Size availableSpace)
{ {
+2 -2
View File
@@ -40,6 +40,8 @@ namespace QuestPDF.Elements
.PaddingTop(MarginTop) .PaddingTop(MarginTop)
.PaddingBottom(MarginBottom) .PaddingBottom(MarginBottom)
.DefaultTextStyle(DefaultTextStyle)
.Decoration(decoration => .Decoration(decoration =>
{ {
decoration decoration
@@ -60,8 +62,6 @@ namespace QuestPDF.Elements
.Element(Footer); .Element(Footer);
}); });
(container as Element).ApplyDefaultTextStyle(DefaultTextStyle);
bool IsClose(float x, float y) bool IsClose(float x, float y)
{ {
return Math.Abs(x - y) < Size.Epsilon; return Math.Abs(x - y) < Size.Epsilon;
+43 -48
View File
@@ -6,9 +6,16 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements namespace QuestPDF.Elements
{ {
internal abstract class RowElement : Constrained internal class RowElement : Constrained
{ {
public float Size { get; protected set; } public float ConstantSize { get; }
public float RelativeSize { get; }
public RowElement(float constantSize, float relativeSize)
{
ConstantSize = constantSize;
RelativeSize = relativeSize;
}
public void SetWidth(float width) public void SetWidth(float width)
{ {
@@ -17,34 +24,24 @@ namespace QuestPDF.Elements
} }
} }
internal class ConstantRowElement : RowElement internal class BinaryRow : Element, ICacheable, IStateResettable
{
public ConstantRowElement(float size)
{
Size = size;
SetWidth(size);
}
}
internal class RelativeRowElement : RowElement
{
public RelativeRowElement(float size)
{
Size = size;
}
}
internal class BinaryRow : Element, ICacheable
{ {
internal Element Left { get; set; } internal Element Left { get; set; }
internal Element Right { get; set; } internal Element Right { get; set; }
internal override void HandleVisitor(Action<Element?> visit) private bool IsLeftRendered { get; set; }
{ private bool IsRightRendered { get; set; }
Left.HandleVisitor(visit);
Right.HandleVisitor(visit);
base.HandleVisitor(visit); public void ResetState()
{
IsLeftRendered = false;
IsRightRendered = false;
}
internal override IEnumerable<Element?> GetChildren()
{
yield return Left;
yield return Right;
} }
internal override void CreateProxy(Func<Element?, Element?> create) internal override void CreateProxy(Func<Element?, Element?> create)
@@ -60,17 +57,24 @@ namespace QuestPDF.Elements
if (leftMeasurement.Type == SpacePlanType.Wrap) if (leftMeasurement.Type == SpacePlanType.Wrap)
return SpacePlan.Wrap(); return SpacePlan.Wrap();
if (leftMeasurement.Type == SpacePlanType.FullRender)
IsLeftRendered = true;
var rightMeasurement = Right.Measure(new Size(availableSpace.Width - leftMeasurement.Width, availableSpace.Height)); var rightMeasurement = Right.Measure(new Size(availableSpace.Width - leftMeasurement.Width, availableSpace.Height));
if (rightMeasurement.Type == SpacePlanType.Wrap) if (rightMeasurement.Type == SpacePlanType.Wrap)
return SpacePlan.Wrap(); return SpacePlan.Wrap();
if (leftMeasurement.Type == SpacePlanType.FullRender)
IsRightRendered = true;
var totalWidth = leftMeasurement.Width + rightMeasurement.Width; var totalWidth = leftMeasurement.Width + rightMeasurement.Width;
var totalHeight = Math.Max(leftMeasurement.Height, rightMeasurement.Height); var totalHeight = Math.Max(leftMeasurement.Height, rightMeasurement.Height);
var targetSize = new Size(totalWidth, totalHeight); var targetSize = new Size(totalWidth, totalHeight);
if (leftMeasurement.Type == SpacePlanType.PartialRender || rightMeasurement.Type == SpacePlanType.PartialRender) if ((!IsLeftRendered && leftMeasurement.Type == SpacePlanType.PartialRender) ||
(!IsRightRendered && rightMeasurement.Type == SpacePlanType.PartialRender))
return SpacePlan.PartialRender(targetSize); return SpacePlan.PartialRender(targetSize);
return SpacePlan.FullRender(targetSize); return SpacePlan.FullRender(targetSize);
@@ -93,16 +97,15 @@ namespace QuestPDF.Elements
{ {
public float Spacing { get; set; } = 0; public float Spacing { get; set; } = 0;
public ICollection<RowElement> Children { get; internal set; } = new List<RowElement>(); public ICollection<RowElement> Items { get; internal set; } = new List<RowElement>();
private Element? RootElement { get; set; } private Element? RootElement { get; set; }
internal override void HandleVisitor(Action<Element?> visit) internal override IEnumerable<Element?> GetChildren()
{ {
if (RootElement == null) if (RootElement == null)
ComposeTree(); ComposeTree();
RootElement.HandleVisitor(visit); yield return RootElement;
base.HandleVisitor(visit);
} }
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
@@ -121,31 +124,23 @@ namespace QuestPDF.Elements
private void ComposeTree() private void ComposeTree()
{ {
Children = AddSpacing(Children, Spacing); Items = AddSpacing(Items, Spacing);
var elements = Children.Cast<Element>().ToArray(); var elements = Items.Cast<Element>().ToArray();
RootElement = BuildTree(elements); RootElement = BuildTree(elements);
} }
private void UpdateElementsWidth(float availableWidth) private void UpdateElementsWidth(float availableWidth)
{ {
var constantWidth = Children var constantWidth = Items.Sum(x => x.ConstantSize);
.Where(x => x is ConstantRowElement) var relativeWidth = Items.Sum(x => x.RelativeSize);
.Cast<ConstantRowElement>()
.Sum(x => x.Size);
var relativeWidth = Children var widthPerRelativeUnit = (relativeWidth > 0) ? (availableWidth - constantWidth) / relativeWidth : 0;
.Where(x => x is RelativeRowElement)
.Cast<RelativeRowElement>()
.Sum(x => x.Size);
var widthPerRelativeUnit = (availableWidth - constantWidth) / relativeWidth; foreach (var row in Items)
{
Children row.SetWidth(row.ConstantSize + row.RelativeSize * widthPerRelativeUnit);
.Where(x => x is RelativeRowElement) }
.Cast<RelativeRowElement>()
.ToList()
.ForEach(x => x.SetWidth(x.Size * widthPerRelativeUnit));
} }
private static ICollection<RowElement> AddSpacing(ICollection<RowElement> elements, float spacing) private static ICollection<RowElement> AddSpacing(ICollection<RowElement> elements, float spacing)
@@ -154,7 +149,7 @@ namespace QuestPDF.Elements
return elements; return elements;
return elements return elements
.SelectMany(x => new[] { new ConstantRowElement(spacing), x }) .SelectMany(x => new[] { new RowElement(spacing, 0), x })
.Skip(1) .Skip(1)
.ToList(); .ToList();
} }
+5 -7
View File
@@ -16,12 +16,10 @@ namespace QuestPDF.Elements
internal bool IsFirstRendered { get; set; } = false; internal bool IsFirstRendered { get; set; } = false;
internal override void HandleVisitor(Action<Element?> visit) internal override IEnumerable<Element?> GetChildren()
{ {
First?.HandleVisitor(visit); yield return First;
Second?.HandleVisitor(visit); yield return Second;
base.HandleVisitor(visit);
} }
public void ResetState() public void ResetState()
@@ -97,12 +95,12 @@ namespace QuestPDF.Elements
internal class Stack : IComponent internal class Stack : IComponent
{ {
public ICollection<Element> Children { get; } = new List<Element>(); public ICollection<Element> Items { get; } = new List<Element>();
public float Spacing { get; set; } = 0; public float Spacing { get; set; } = 0;
public void Compose(IContainer container) public void Compose(IContainer container)
{ {
var elements = AddSpacing(Spacing, Children); var elements = AddSpacing(Spacing, Items);
container container
.PaddingBottom(-Spacing) .PaddingBottom(-Spacing)
@@ -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();
}
}
@@ -0,0 +1,9 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
public interface ITableCellContainer : IContainer
{
}
}
+277
View File
@@ -0,0 +1,277 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
internal class Table : Element, IStateResettable
{
public List<TableColumnDefinition> Columns { get; set; } = new List<TableColumnDefinition>();
public List<TableCell> Cells { get; set; } = new List<TableCell>();
public bool ExtendLastCellsToTableBottom { 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[][] OrderedCells { get; set; }
private int StartingRowsCount { get; set; }
private int RowsCount { get; set; }
private int CurrentRow { get; set; }
internal override IEnumerable<Element?> GetChildren()
{
return Cells;
}
public void ResetState()
{
if (StartingRowsCount == default)
StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
if (RowsCount == default)
RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
if (OrderedCells == default)
{
var groups = Cells
.GroupBy(x => x.Row)
.ToDictionary(x => x.Key, x => x.OrderBy(y => y.Column).ToArray());
OrderedCells = Enumerable
.Range(0, RowsCount + 1)
.Select(x => groups.TryGetValue(x, out var output) ? output : Array.Empty<TableCell>())
.ToArray();
}
Cells.ForEach(x => x.IsRendered = false);
CurrentRow = 1;
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!Cells.Any())
return SpacePlan.FullRender(Size.Zero);
UpdateColumnsWidth(availableSpace.Width);
var renderingCommands = PlanLayout(availableSpace);
if (!renderingCommands.Any())
return SpacePlan.Wrap();
var width = Columns.Sum(x => x.Width);
var height = renderingCommands.Max(x => x.Offset.Y + x.Size.Height);
var tableSize = new Size(width, height);
if (tableSize.Width > availableSpace.Width + Size.Epsilon)
return SpacePlan.Wrap();
return FindLastRenderedRow(renderingCommands) == StartingRowsCount
? SpacePlan.FullRender(tableSize)
: SpacePlan.PartialRender(tableSize);
}
internal override void Draw(Size availableSpace)
{
UpdateColumnsWidth(availableSpace.Width);
var renderingCommands = PlanLayout(availableSpace);
foreach (var command in renderingCommands)
{
if (command.Measurement.Type == SpacePlanType.FullRender)
command.Cell.IsRendered = true;
Canvas.Translate(command.Offset);
command.Cell.Draw(command.Size);
Canvas.Translate(command.Offset.Reverse());
}
CurrentRow = FindLastRenderedRow(renderingCommands) + 1;
var isFullRender = FindLastRenderedRow(renderingCommands) == StartingRowsCount;
if (isFullRender)
ResetState();
}
private int FindLastRenderedRow(ICollection<TableCellRenderingCommand> commands)
{
return commands
.GroupBy(x => x.Cell.Row)
.Where(x => x.All(y => y.Measurement.Type == SpacePlanType.FullRender))
.Select(x => x.Key)
.DefaultIfEmpty(0)
.Max();
}
private void UpdateColumnsWidth(float availableWidth)
{
var constantWidth = Columns.Sum(x => x.ConstantSize);
var relativeWidth = Columns.Sum(x => x.RelativeSize);
var widthPerRelativeUnit = (relativeWidth > 0) ? (availableWidth - constantWidth) / relativeWidth : 0;
foreach (var column in Columns)
{
column.Width = column.ConstantSize + column.RelativeSize * widthPerRelativeUnit;
}
}
private ICollection<TableCellRenderingCommand> PlanLayout(Size availableSpace)
{
var columnOffsets = GetColumnLeftOffsets(Columns);
var commands = GetRenderingCommands();
if (!commands.Any())
return commands;
var tableHeight = commands.Max(cell => cell.Offset.Y + cell.Size.Height);
if (ExtendLastCellsToTableBottom)
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 cellsToTry = Enumerable
.Range(CurrentRow, RowsCount - CurrentRow + 1)
.SelectMany(x => OrderedCells[x]);
var currentRow = CurrentRow;
var maxRenderingRow = RowsCount;
var commands = new List<TableCellRenderingCommand>();
foreach (var cell in cellsToTry)
{
// 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;
foreach (var row in Enumerable.Range(cell.Row, cell.Row - currentRow))
rowBottomOffsets[row] = Math.Max(rowBottomOffsets[row - 1], rowBottomOffsets[row]);
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;
var availableCellSize = new Size(availableWidth, availableHeight);
var cellSize = cell.Measure(availableCellSize);
// corner case: if 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: if 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)
});
}
if (!commands.Any())
return commands;
var maxRow = commands.Select(x => x.Cell).Max(x => x.Row + x.RowSpan);
foreach (var row in Enumerable.Range(CurrentRow, maxRow - CurrentRow))
rowBottomOffsets[row] = Math.Max(rowBottomOffsets[row - 1], rowBottomOffsets[row]);
AdjustCellSizes(commands, rowBottomOffsets);
// corner case: reject cell if other cells within the same row are rejected
return commands.Where(x => x.Cell.Row <= maxRenderingRow).ToList();
}
// corner sase: 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(ICollection<TableCellRenderingCommand> commands, DynamicDictionary<int, float> rowBottomOffsets)
{
foreach (var command in commands)
{
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]);
}
}
// corner sase: 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);
foreach (var column in Enumerable.Range(1, columnsCount))
{
var lastCellInColumn = commands
.Where(x => x.Cell.Column <= column && column < x.Cell.Column + x.Cell.ColumnSpan)
.OrderByDescending(x => x.Cell.Row + x.Cell.RowSpan)
.FirstOrDefault();
if (lastCellInColumn == null)
continue;
lastCellInColumn.Size = new Size(lastCellInColumn.Size.Width, tableHeight - lastCellInColumn.Offset.Y);
}
}
float GetCellWidth(TableCell cell)
{
return columnOffsets[cell.Column + cell.ColumnSpan - 1] - columnOffsets[cell.Column - 1];
}
}
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace QuestPDF.Elements.Table
{
internal class TableCell : Container, ITableCellContainer
{
public int Row { get; set; } = 0;
public int RowSpan { get; set; } = 1;
public int Column { get; set; } = 0;
public int ColumnSpan { get; set; } = 1;
public bool IsRendered { get; set; }
}
}
@@ -0,0 +1,13 @@
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
internal class TableCellRenderingCommand
{
public TableCell Cell { get; set; }
public SpacePlan Measurement { get; set; }
public Size Size { get; set; }
public Position Offset { get; set; }
}
}
@@ -0,0 +1,16 @@
namespace QuestPDF.Elements.Table
{
internal class TableColumnDefinition
{
public float ConstantSize { get; }
public float RelativeSize { get; }
internal float Width { get; set; }
public TableColumnDefinition(float constantSize, float relativeSize)
{
ConstantSize = constantSize;
RelativeSize = relativeSize;
}
}
}
@@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Fluent;
namespace QuestPDF.Elements.Table
{
static class TableLayoutValidator
{
public static void PlanCellPositions(this Table table)
{
PlanCellPositions(table.Columns.Count, table.Cells);
}
private static void PlanCellPositions(int columnsCount, ICollection<TableCell> cells)
{
var cellsWindow = new List<TableCell>();
(int x, int y) currentLocation = (1, 1);
foreach (var cell in cells)
{
if (cellsWindow.Count > Math.Max(columnsCount, 16))
{
cellsWindow = cellsWindow
.Where(x => x.Row + x.RowSpan > currentLocation.y)
.ToList();
}
SetPartialLocation(cell);
if (cell.HasLocation())
{
cellsWindow.Add(cell);
currentLocation = (cell.Column, cell.Row);
continue;
}
foreach (var location in GenerateCoordinates(columnsCount, currentLocation))
{
if (location.x + cell.ColumnSpan - 1 > columnsCount)
continue;
cell.Column = location.x;
cell.Row = location.y;
if (cell.CollidesWithAnyOf(cellsWindow))
continue;
cellsWindow.Add(cell);
currentLocation = (cell.Column, cell.Row);
break;
}
}
}
private static IEnumerable<(int x, int y)> GenerateCoordinates(int columnsCount, (int x, int y) startPosition)
{
if (startPosition.x > columnsCount)
throw new ArgumentException();
foreach (var x in Enumerable.Range(startPosition.x, columnsCount - startPosition.x + 1))
yield return (x, startPosition.y);
foreach (var y in Enumerable.Range(startPosition.y + 1, 1_000_000))
foreach (var x in Enumerable.Range(1, columnsCount))
yield return (x, y);
}
private static bool CollidesWith(this TableCell cell, TableCell neighbour)
{
return cell.Column < neighbour.Column + neighbour.ColumnSpan &&
cell.Column + cell.ColumnSpan > neighbour.Column &&
cell.Row < neighbour.Row + neighbour.RowSpan &&
cell.RowSpan + cell.Row > neighbour.Row;
}
private static bool CollidesWithAnyOf(this TableCell cell, ICollection<TableCell> neighbours)
{
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 != 0 && cell.Column != 0;
}
}
}
@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Fluent;
namespace QuestPDF.Elements.Table
{
static class TableLayoutPlanner
{
public static void ValidateCellPositions(this Table table)
{
ValidateCellPositions(table.Columns.Count, table.Cells);
}
private static void ValidateCellPositions(int columnsCount, ICollection<TableCell> 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 DocumentComposeException($"{prefix} Cell starts at column that does not exist. Cell details: {GetCellDetails(cell)}.");
if (cell.Column + cell.ColumnSpan - 1 > columnsCount)
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)
{
return $"Row {cell.Row}, Column {cell.Column}, RowSpan {cell.RowSpan}, ColumnSpan {cell.ColumnSpan}";
}
}
}
}
@@ -1,5 +1,6 @@
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation; using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items namespace QuestPDF.Elements.Text.Items
@@ -10,8 +11,8 @@ namespace QuestPDF.Elements.Text.Items
public TextMeasurementResult? Measure(TextMeasurementRequest request) public TextMeasurementResult? Measure(TextMeasurementRequest request)
{ {
Element.HandleVisitor(x => (x as IStateResettable)?.ResetState()); Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.HandleVisitor(x => x.Initialize(request.PageContext, request.Canvas)); Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height)); var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
@@ -35,8 +36,8 @@ namespace QuestPDF.Elements.Text.Items
public void Draw(TextDrawingRequest request) public void Draw(TextDrawingRequest request)
{ {
Element.HandleVisitor(x => (x as IStateResettable)?.ResetState()); Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.HandleVisitor(x => x.Initialize(request.PageContext, request.Canvas)); Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
request.Canvas.Translate(new Position(0, request.TotalAscent)); request.Canvas.Translate(new Position(0, request.TotalAscent));
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent)); Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
+7 -5
View File
@@ -11,14 +11,16 @@ namespace QuestPDF.Elements.Text
internal class TextBlock : Element, IStateResettable internal class TextBlock : Element, IStateResettable
{ {
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<ITextBlockItem> Children { get; set; } = new List<ITextBlockItem>(); public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
public Queue<ITextBlockItem> RenderingQueue { get; set; } public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
public int CurrentElementIndex { get; set; }
private Queue<ITextBlockItem> RenderingQueue { get; set; }
private int CurrentElementIndex { get; set; }
public void ResetState() public void ResetState()
{ {
RenderingQueue = new Queue<ITextBlockItem>(Children); RenderingQueue = new Queue<ITextBlockItem>(Items);
CurrentElementIndex = 0; CurrentElementIndex = 0;
} }
@@ -30,7 +32,7 @@ namespace QuestPDF.Elements.Text
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList(); var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
if (!lines.Any()) if (!lines.Any())
return SpacePlan.PartialRender(Size.Zero); return SpacePlan.Wrap();
var width = lines.Max(x => x.Width); var width = lines.Max(x => x.Width);
var height = lines.Sum(x => x.LineHeight); var height = lines.Sum(x => x.LineHeight);
+16 -1
View File
@@ -136,14 +136,29 @@ namespace QuestPDF.Fluent
}); });
} }
// TODO: deprecated Box method in QuestPDF 2022.1
[Obsolete("This element has been renamed. Please use the MinimalBox method.")]
public static IContainer Box(this IContainer element) public static IContainer Box(this IContainer element)
{ {
return element.Element(new Box()); return element.Element(new MinimalBox());
}
public static IContainer MinimalBox(this IContainer element)
{
return element.Element(new MinimalBox());
} }
public static IContainer Unconstrained(this IContainer element) public static IContainer Unconstrained(this IContainer element)
{ {
return element.Element(new Unconstrained()); return element.Element(new Unconstrained());
} }
public static IContainer DefaultTextStyle(this IContainer element, TextStyle textStyle)
{
return element.Element(new DefaultTextStyle
{
TextStyle = textStyle
});
}
} }
} }
+8 -6
View File
@@ -15,17 +15,19 @@ namespace QuestPDF.Fluent
public IContainer ConstantColumn(float width) public IContainer ConstantColumn(float width)
{ {
var element = new ConstantRowElement(width); return Column(constantWidth: width);
Row.Children.Add(element);
return element;
} }
public IContainer RelativeColumn(float width = 1) public IContainer RelativeColumn(float width = 1)
{ {
var element = new RelativeRowElement(width); return Column(relativeWidth: width);
}
Row.Children.Add(element); public IContainer Column(float constantWidth = 0, float relativeWidth = 0)
{
var element = new RowElement(constantWidth, relativeWidth);
Row.Items.Add(element);
return element; return element;
} }
} }
+1 -1
View File
@@ -16,7 +16,7 @@ namespace QuestPDF.Fluent
public IContainer Item() public IContainer Item()
{ {
var container = new Container(); var container = new Container();
Stack.Children.Add(container); Stack.Items.Add(container);
return container; return container;
} }
} }
+162
View File
@@ -0,0 +1,162 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Elements.Table;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public class TableColumnsDefinitionDescriptor
{
internal List<TableColumnDefinition> Columns { get; } = new();
public void ConstantColumn(float width)
{
ComplexColumn(constantWidth: width);
}
public void RelativeColumn(float width = 1)
{
ComplexColumn(relativeWidth: width);
}
public void ComplexColumn(float constantWidth = 0, float relativeWidth = 0)
{
var columnDefinition = new TableColumnDefinition(constantWidth, relativeWidth);
Columns.Add(columnDefinition);
}
}
public class TableCellDescriptor
{
private ICollection<TableCell> Cells { get; }
internal TableCellDescriptor(ICollection<TableCell> cells)
{
Cells = cells;
}
public ITableCellContainer Cell()
{
var cell = new TableCell();
Cells.Add(cell);
return cell;
}
}
public class TableDescriptor
{
internal List<TableColumnDefinition> Columns { get; private set; }
private Table HeaderTable { get; } = new();
private Table ContentTable { get; } = new();
private Table FooterTable { get; } = new();
public void ColumnsDefinition(Action<TableColumnsDefinitionDescriptor> handler)
{
var descriptor = new TableColumnsDefinitionDescriptor();
handler(descriptor);
HeaderTable.Columns = descriptor.Columns;
ContentTable.Columns = descriptor.Columns;
FooterTable.Columns = descriptor.Columns;
}
public void ExtendLastCellsToTableBottom()
{
ContentTable.ExtendLastCellsToTableBottom = true;
}
public void Header(Action<TableCellDescriptor> handler)
{
var descriptor = new TableCellDescriptor(HeaderTable.Cells);
handler(descriptor);
}
public void Footer(Action<TableCellDescriptor> handler)
{
var descriptor = new TableCellDescriptor(FooterTable.Cells);
handler(descriptor);
}
public ITableCellContainer Cell()
{
var cell = new TableCell();
ContentTable.Cells.Add(cell);
return cell;
}
internal IElement CreateElement()
{
var container = new Container();
ConfigureTable(HeaderTable);
ConfigureTable(ContentTable);
ConfigureTable(FooterTable);
container
.Decoration(decoration =>
{
decoration.Header().Element(HeaderTable);
decoration.Content().Element(ContentTable);
decoration.Footer().Element(FooterTable);
});
return container;
}
private static void ConfigureTable(Table table)
{
if (!table.Columns.Any())
throw new DocumentComposeException($"Table should have at least one column. Please call the '{nameof(ColumnsDefinition)}' method to define columns.");
table.PlanCellPositions();
table.ValidateCellPositions();
}
}
public static class TableExtensions
{
public static void Table(this IContainer element, Action<TableDescriptor> handler)
{
var descriptor = new TableDescriptor();
handler(descriptor);
element.Element(descriptor.CreateElement());
}
}
public static class TableCellExtensions
{
private static ITableCellContainer TableCell(this ITableCellContainer element, Action<TableCell> handler)
{
if (element is TableCell tableCell)
handler(tableCell);
return element;
}
public static ITableCellContainer Column(this ITableCellContainer tableCellContainer, uint value)
{
return tableCellContainer.TableCell(x => x.Column = (int)value);
}
public static ITableCellContainer ColumnSpan(this ITableCellContainer tableCellContainer, uint value)
{
return tableCellContainer.TableCell(x => x.ColumnSpan = (int)value);
}
public static ITableCellContainer Row(this ITableCellContainer tableCellContainer, uint value)
{
return tableCellContainer.TableCell(x => x.Row = (int)value);
}
public static ITableCellContainer RowSpan(this ITableCellContainer tableCellContainer, uint value)
{
return tableCellContainer.TableCell(x => x.RowSpan = (int)value);
}
}
}
+4 -7
View File
@@ -47,7 +47,7 @@ namespace QuestPDF.Fluent
if (!TextBlocks.Any()) if (!TextBlocks.Any())
TextBlocks.Add(new TextBlock()); TextBlocks.Add(new TextBlock());
TextBlocks.Last().Children.Add(item); TextBlocks.Last().Items.Add(item);
} }
public void Span(string? text, TextStyle? style = null) public void Span(string? text, TextStyle? style = null)
@@ -73,7 +73,7 @@ namespace QuestPDF.Fluent
.Skip(1) .Skip(1)
.Select(x => new TextBlock .Select(x => new TextBlock
{ {
Children = new List<ITextBlockItem> { x } Items = new List<ITextBlockItem> { x }
}) })
.ToList() .ToList()
.ForEach(TextBlocks.Add); .ForEach(TextBlocks.Add);
@@ -167,17 +167,14 @@ namespace QuestPDF.Fluent
Element = container Element = container
}); });
return container.AlignBottom().Box(); return container.AlignBottom().MinimalBox();
} }
internal void Compose(IContainer container) internal void Compose(IContainer container)
{ {
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment); TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
foreach (var textBlockSpan in TextBlocks.SelectMany(x => x.Children).Where(x => x is TextBlockSpan).Cast<TextBlockSpan>()) container.DefaultTextStyle(DefaultStyle).Stack(stack =>
textBlockSpan.Style.ApplyParentStyle(DefaultStyle);
container.Stack(stack =>
{ {
stack.Spacing(Spacing); stack.Spacing(Spacing);
+10
View File
@@ -1,8 +1,10 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using QuestPDF.Infrastructure;
namespace QuestPDF.Helpers namespace QuestPDF.Helpers
{ {
@@ -41,5 +43,13 @@ namespace QuestPDF.Helpers
{ {
return Regex.Replace(text, @"([a-z])([A-Z])", "$1 $2"); return Regex.Replace(text, @"([a-z])([A-Z])", "$1 $2");
} }
internal static void VisitChildren(this Element? element, Action<Element?> handler)
{
foreach (var child in element.GetChildren().Where(x => x != null))
VisitChildren(child, handler);
handler(element);
}
} }
} }
+1 -1
View File
@@ -7,7 +7,7 @@ namespace QuestPDF.Helpers
{ {
public static class Placeholders public static class Placeholders
{ {
public static readonly Random Random = new Random(); public static readonly Random Random = new Random(3); // 3
#region Word Cache #region Word Cache
+3 -3
View File
@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using QuestPDF.Drawing; using QuestPDF.Drawing;
using QuestPDF.Elements; using QuestPDF.Elements;
@@ -14,10 +15,9 @@ namespace QuestPDF.Infrastructure
set => Child = value as Element; set => Child = value as Element;
} }
internal override void HandleVisitor(Action<Element?> visit) internal override IEnumerable<Element?> GetChildren()
{ {
Child?.HandleVisitor(visit); yield return Child;
base.HandleVisitor(visit);
} }
internal override void CreateProxy(Func<Element?, Element?> create) internal override void CreateProxy(Func<Element?, Element?> create)
+2 -7
View File
@@ -10,9 +10,9 @@ namespace QuestPDF.Infrastructure
internal IPageContext PageContext { get; set; } internal IPageContext PageContext { get; set; }
internal ICanvas Canvas { get; set; } internal ICanvas Canvas { get; set; }
internal virtual void HandleVisitor(Action<Element?> visit) internal virtual IEnumerable<Element?> GetChildren()
{ {
visit(this); yield break;
} }
internal void Initialize(IPageContext pageContext, ICanvas canvas) internal void Initialize(IPageContext pageContext, ICanvas canvas)
@@ -28,10 +28,5 @@ namespace QuestPDF.Infrastructure
internal abstract SpacePlan Measure(Size availableSpace); internal abstract SpacePlan Measure(Size availableSpace);
internal abstract void Draw(Size availableSpace); internal abstract void Draw(Size availableSpace);
protected virtual IEnumerable<string> GetDebugInformation()
{
yield break;
}
} }
} }
+2
View File
@@ -17,5 +17,7 @@
{ {
return new Position(-X, -Y); return new Position(-X, -Y);
} }
public override string ToString() => $"(Left: {X:N3}, Top: {Y:N3})";
} }
} }
+2 -1
View File
@@ -3,6 +3,7 @@
public readonly struct Size public readonly struct Size
{ {
public const float Epsilon = 0.001f; public const float Epsilon = 0.001f;
public const float Infinity = float.PositiveInfinity;
public readonly float Width; public readonly float Width;
public readonly float Height; public readonly float Height;
@@ -16,6 +17,6 @@
Height = height; Height = height;
} }
public override string ToString() => $"(W: {Width}, H: {Height})"; public override string ToString() => $"(Width: {Width:N3}, Height: {Height:N3})";
} }
} }
+2 -2
View File
@@ -4,10 +4,10 @@
<Authors>MarcinZiabek</Authors> <Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company> <Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId> <PackageId>QuestPDF</PackageId>
<Version>2021.12.0-alpha0</Version> <Version>2022.1.0-beta4</Version>
<PackageDescription>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.</PackageDescription> <PackageDescription>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.</PackageDescription>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes> <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>8</LangVersion> <LangVersion>9</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageIcon>Logo.png</PackageIcon> <PackageIcon>Logo.png</PackageIcon>
<PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl> <PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl>
+7 -1
View File
@@ -1 +1,7 @@
- Added support for generating documents in the XPS file format. Introduced new element: `Table` - a great way to construct complex document structures, e.g. reports. This element covers all cases offered by combination of the `Stack` and the `Row` elements. Additionally, it provides support for more complex layouts and corner cases. Updating to the `Table` element can greatly simplify your code 😁
Other changes:
- Added new element `DefaultTextStyle` - it allows set new text style to all its children,
- Improved the default paging behavior for the `Row` element. In some minor corner cases it might cause infinite layout exceptions and confuse developers.
- Improved the `Row` element: added new type of column that combines constant and relative widths.
- Fixed default page sizes for: Letter and Legal.