diff --git a/QuestPDF.Examples/BarCode.cs b/QuestPDF.Examples/BarCode.cs index f90aca1..fd4853b 100644 --- a/QuestPDF.Examples/BarCode.cs +++ b/QuestPDF.Examples/BarCode.cs @@ -18,7 +18,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(400, 100) - .FileName() .ShowResults() .Render(container => { diff --git a/QuestPDF.Examples/BarcodeExamples.cs b/QuestPDF.Examples/BarcodeExamples.cs index 5711c37..b9b3f9f 100644 --- a/QuestPDF.Examples/BarcodeExamples.cs +++ b/QuestPDF.Examples/BarcodeExamples.cs @@ -15,7 +15,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 300) - .FileName() .Render(container => { container diff --git a/QuestPDF.Examples/ComplexLayoutBenchmark.cs b/QuestPDF.Examples/ComplexLayoutBenchmark.cs new file mode 100644 index 0000000..8f7df56 --- /dev/null +++ b/QuestPDF.Examples/ComplexLayoutBenchmark.cs @@ -0,0 +1,57 @@ +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class ComplexLayoutBenchmark + { + [Test] + public void ComplexLayout() + { + RenderingTest + .Create() + .PageSize(PageSizes.A4) + .ProducePdf() + .ShowResults() + .Render(x => x.Image(new byte[] { 1, 2, 3 })); + //.Render(x => GenerateStructure(x, 16)); + } + + private void GenerateStructure(IContainer container, int level) + { + if (level <= 0) + { + container.Background(Placeholders.BackgroundColor()).Height(10); + return; + } + + level--; + + if (level % 3 == 0) + { + container + .Border(level / 10f) + .BorderColor(Colors.Black) + .Row(row => + { + row.RelativeColumn().Element(x => GenerateStructure(x, level)); + row.RelativeColumn().Element(x => GenerateStructure(x, level)); + }); + } + else + { + container + .Border(level / 10f) + .BorderColor(Colors.Black) + .Stack(stack => + { + stack.Item().Element(x => GenerateStructure(x, level)); + stack.Item().Element(x => GenerateStructure(x, level)); + }); + } + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/DefaultTextStyleExample.cs b/QuestPDF.Examples/DefaultTextStyleExample.cs new file mode 100644 index 0000000..c88f59c --- /dev/null +++ b/QuestPDF.Examples/DefaultTextStyleExample.cs @@ -0,0 +1,48 @@ +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class DefaultTextStyleExample + { + [Test] + public void DefaultTextStyle() + { + RenderingTest + .Create() + .ProduceImages() + .ShowResults() + .RenderDocument(container => + { + container.Page(page => + { + // all text in this set of pages has size 20 + page.DefaultTextStyle(TextStyle.Default.Size(20)); + + page.Margin(20); + page.Size(PageSizes.A4); + page.Background(Colors.White); + + page.Content().Stack(stack => + { + stack.Item().Text(Placeholders.Sentence()); + + stack.Item().Text(text => + { + // text in this block is additionally semibold + text.DefaultTextStyle(TextStyle.Default.SemiBold()); + + text.Line(Placeholders.Sentence()); + + // this text has size 20 but also semibold and red + text.Span(Placeholders.Sentence(), TextStyle.Default.Color(Colors.Red.Medium)); + }); + }); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/ElementExamples.cs b/QuestPDF.Examples/ElementExamples.cs index 309f03d..993c6a2 100644 --- a/QuestPDF.Examples/ElementExamples.cs +++ b/QuestPDF.Examples/ElementExamples.cs @@ -17,7 +17,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(200, 150) - .FileName() .Render(container => { container @@ -33,7 +32,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 300) - .FileName() .Render(container => { container @@ -63,7 +61,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(740, 200) - .FileName() .Render(container => { container @@ -104,7 +101,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(740, 200) - .FileName() .Render(container => { container @@ -126,7 +122,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(500, 360) - .FileName() .Render(container => { container @@ -149,7 +144,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(210, 210) - .FileName() .Render(container => { container @@ -172,7 +166,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 200) - .FileName() .Render(container => { var text = ""; @@ -195,7 +188,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(400, 230) - .FileName() .Render(container => { var textStyle = TextStyle.Default.Size(14); @@ -230,7 +222,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 200) - .FileName() .Render(container => { container @@ -260,7 +251,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(400, 250) - .FileName() .Render(container => { container @@ -336,7 +326,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 300) - .FileName() .Render(container => { container @@ -381,7 +370,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(450, 150) - .FileName() .Render(container => { container @@ -414,7 +402,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(500, 175) - .FileName() .Render(container => { container @@ -441,7 +428,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 300) - .FileName() .Render(container => { container @@ -481,7 +467,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 150) - .FileName() .Render(container => { container @@ -502,7 +487,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 175) - .FileName() .Render(container => { container @@ -554,7 +538,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 200) - .FileName() .Render(container => { container @@ -582,7 +565,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(650, 450) - .FileName() .Render(container => { container @@ -621,7 +603,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 300) - .FileName() .Render(container => { container @@ -648,7 +629,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 300) - .FileName() .Render(container => { container @@ -680,7 +660,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(350, 350) - .FileName() .Render(container => { container @@ -722,7 +701,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(200, 200) - .FileName() .Render(container => { container @@ -748,7 +726,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(400, 350) - .FileName() .Render(container => { container @@ -785,7 +762,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(500, 225) - .FileName() .Render(container => { container @@ -839,7 +815,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(600, 310) - .FileName() .Render(container => { container diff --git a/QuestPDF.Examples/Engine/RenderingTest.cs b/QuestPDF.Examples/Engine/RenderingTest.cs index e8aacf2..3309c4a 100644 --- a/QuestPDF.Examples/Engine/RenderingTest.cs +++ b/QuestPDF.Examples/Engine/RenderingTest.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using QuestPDF.Elements; using QuestPDF.Fluent; +using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.Examples.Engine @@ -17,6 +18,7 @@ namespace QuestPDF.Examples.Engine { private string FileNamePrefix = "test"; private Size Size { get; set; } + private int? MaxPagesThreshold { get; set; } private bool ShowResult { get; set; } private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images; @@ -25,12 +27,12 @@ namespace QuestPDF.Examples.Engine } - public static RenderingTest Create() + public static RenderingTest Create([CallerMemberName] string fileName = "test") { - return new RenderingTest(); + return new RenderingTest().FileName(fileName); } - public RenderingTest FileName([CallerMemberName] string fileName = "test") + public RenderingTest FileName(string fileName) { FileNamePrefix = fileName; return this; @@ -67,12 +69,31 @@ namespace QuestPDF.Examples.Engine public void Render(Action content) { - var container = new Container(); - content(container); + RenderDocument(container => + { + container.Page(page => + { + page.Size(new PageSize(Size.Width, Size.Height)); + page.Content().Container().Background(Colors.White).Element(content); + }); + }); + } + + public void MaxPages(int value) + { + MaxPagesThreshold = value; + } - var maxPages = ResultType == RenderingTestResult.Pdf ? 1000 : 10; - var document = new SimpleDocument(container, Size, maxPages); + public void RenderDocument(Action content) + { + MaxPagesThreshold ??= ResultType == RenderingTestResult.Pdf ? 1000 : 10; + var document = new SimpleDocument(content, MaxPagesThreshold.Value); + Render(document); + } + + private void Render(IDocument document) + { if (ResultType == RenderingTestResult.Images) { Func fileNameSchema = i => $"{FileNamePrefix}-${i}.png"; diff --git a/QuestPDF.Examples/Engine/SimpleDocument.cs b/QuestPDF.Examples/Engine/SimpleDocument.cs index 9e2feb7..25d3887 100644 --- a/QuestPDF.Examples/Engine/SimpleDocument.cs +++ b/QuestPDF.Examples/Engine/SimpleDocument.cs @@ -1,4 +1,5 @@ -using QuestPDF.Drawing; +using System; +using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Fluent; using QuestPDF.Helpers; @@ -10,14 +11,12 @@ namespace QuestPDF.Examples.Engine { public const int ImageScalingFactor = 2; - private IContainer Container { get; } - private Size Size { get; } + private Action Content { get; } private int MaxPages { get; } - public SimpleDocument(IContainer container, Size size, int maxPages) + public SimpleDocument(Action content, int maxPages) { - Container = container; - Size = size; + Content = content; MaxPages = maxPages; } @@ -32,11 +31,7 @@ namespace QuestPDF.Examples.Engine public void Compose(IDocumentContainer container) { - container.Page(page => - { - page.Size(new PageSize(Size.Width, Size.Height)); - page.Content().Container().Background(Colors.White).Element(Container as Container); - }); + Content(container); } } } \ No newline at end of file diff --git a/QuestPDF.Examples/EnsureSpaceExample.cs b/QuestPDF.Examples/EnsureSpaceExample.cs new file mode 100644 index 0000000..0808774 --- /dev/null +++ b/QuestPDF.Examples/EnsureSpaceExample.cs @@ -0,0 +1,53 @@ +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class EnsureSpaceExample + { + [Test] + public void EnsureSpaceWith() + { + RenderingTest + .Create() + .ProduceImages() + .ShowResults() + .RenderDocument(container => + { + container.Page(page => + { + page.Margin(20); + page.Size(PageSizes.A7.Landscape()); + page.Background(Colors.White); + + page.Header().Text("With ensure space", TextStyle.Default.SemiBold()); + + page.Content().Stack(stack => + { + stack + .Item() + .ExtendHorizontal() + .Height(75) + .Background(Colors.Grey.Lighten2); + + stack + .Item() + .EnsureSpace(100) + .Text(Placeholders.LoremIpsum()); + }); + + page.Footer().Text(text => + { + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" out of "); + text.TotalPages(); + }); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/FrameExample.cs b/QuestPDF.Examples/FrameExample.cs index f8d8ae0..614aa54 100644 --- a/QuestPDF.Examples/FrameExample.cs +++ b/QuestPDF.Examples/FrameExample.cs @@ -28,7 +28,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(550, 400) - .FileName() .ShowResults() .Render(container => { diff --git a/QuestPDF.Examples/ImageExamples.cs b/QuestPDF.Examples/ImageExamples.cs new file mode 100644 index 0000000..ad24b57 --- /dev/null +++ b/QuestPDF.Examples/ImageExamples.cs @@ -0,0 +1,47 @@ +using System.IO; +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; + +namespace QuestPDF.Examples +{ + public class ImageExamples + { + [Test] + public void LoadingImage() + { + RenderingTest + .Create() + .PageSize(PageSizes.A5) + .ProducePdf() + .ShowResults() + .Render(page => + { + page.Padding(25).Stack(stack => + { + stack.Spacing(25); + + stack.Item().Image("logo.png"); + + var binaryData = File.ReadAllBytes("logo.png"); + stack.Item().Image(binaryData); + + using var stream = new FileStream("logo.png", FileMode.Open); + stack.Item().Image(stream); + }); + }); + } + + [Test] + public void Exception() + { + RenderingTest + .Create() + .PageSize(PageSizes.A5) + .ProducePdf() + .ShowResults() + .Render(page => page.Image("non_existent.png")); + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/InlinedExamples.cs b/QuestPDF.Examples/InlinedExamples.cs new file mode 100644 index 0000000..adf4e68 --- /dev/null +++ b/QuestPDF.Examples/InlinedExamples.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using System.Reflection.Metadata.Ecma335; +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class InlinedExamples + { + [Test] + public void Inlined() + { + RenderingTest + .Create() + .PageSize(800, 650) + .ProduceImages() + .ShowResults() + .Render(container => + { + container + .Padding(25) + .Decoration(decoration => + { + decoration.Header().Text(text => + { + text.DefaultTextStyle(TextStyle.Default.Size(20)); + + text.CurrentPageNumber(); + text.Span(" / "); + text.TotalPages(); + }); + + decoration + .Content() + .PaddingTop(25) + //.Box() + .Border(1) + .Background(Colors.Grey.Lighten2) + .Inlined(inlined => + { + inlined.Spacing(25); + + inlined.AlignSpaceAround(); + inlined.BaselineMiddle(); + + var random = new Random(123); + + foreach (var _ in Enumerable.Range(0, 50)) + { + var width = random.Next(2, 7); + var height = random.Next(2, 7); + + var sizeText = $"{width}×{height}"; + + inlined + .Item() + .Border(1) + .Width(width * 25) + .Height(height * 25) + .Background(Placeholders.BackgroundColor()) + .Layers(layers => + { + layers.Layer().Grid(grid => + { + grid.Columns(width); + Enumerable.Range(0, width * height).ToList().ForEach(x => grid.Item().Border(1).BorderColor(Colors.White).Width(25).Height(25)); + }); + + layers + .PrimaryLayer() + .AlignCenter() + .AlignMiddle() + .Text(sizeText, TextStyle.Default.Size(15)); + }); + } + }); + }); + }); + } + + [Test] + public void Inline_AlignLeft_BaselineBottom() + { + RenderingTest + .Create() + .PageSize(400, 250) + .ProduceImages() + .ShowResults() + .Render(container => + { + container + .Padding(20) + .Border(1) + .Background(Colors.Grey.Lighten3) + .Inlined(inlined => + { + inlined.VerticalSpacing(50); + inlined.HorizontalSpacing(20); + inlined.AlignSpaceAround(); + inlined.BaselineTop(); + + foreach (var _ in Enumerable.Range(0, 20)) + inlined.Item().Element(RandomBlock); + }); + }); + + void RandomBlock(IContainer container) + { + container + .Width(Placeholders.Random.Next(1, 5) * 20) + .Height(Placeholders.Random.Next(1, 5) * 20) + .Border(1) + .BorderColor(Colors.Grey.Darken2) + .Background(Placeholders.BackgroundColor()); + } + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/LoremPicsumExample.cs b/QuestPDF.Examples/LoremPicsumExample.cs index 3919bec..c20094a 100644 --- a/QuestPDF.Examples/LoremPicsumExample.cs +++ b/QuestPDF.Examples/LoremPicsumExample.cs @@ -36,7 +36,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(350, 280) - .FileName() .Render(container => { container diff --git a/QuestPDF.Examples/Padding.cs b/QuestPDF.Examples/Padding.cs index 6d7bfca..e20b71b 100644 --- a/QuestPDF.Examples/Padding.cs +++ b/QuestPDF.Examples/Padding.cs @@ -13,7 +13,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(300, 300) - .FileName() .ShowResults() .Render(container => { @@ -37,7 +36,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(200, 150) - .FileName() .Render(container => { container @@ -62,7 +60,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(200, 150) - .FileName() .Render(container => { container @@ -113,7 +110,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(200, 150) - .FileName() .Render(container => { container diff --git a/QuestPDF.Examples/QuestPDF.Examples.csproj b/QuestPDF.Examples/QuestPDF.Examples.csproj index 5dc14b7..a7a0014 100644 --- a/QuestPDF.Examples/QuestPDF.Examples.csproj +++ b/QuestPDF.Examples/QuestPDF.Examples.csproj @@ -23,6 +23,9 @@ PreserveNewest + + PreserveNewest + diff --git a/QuestPDF.Examples/ShowOnceExample.cs b/QuestPDF.Examples/ShowOnceExample.cs new file mode 100644 index 0000000..72e4727 --- /dev/null +++ b/QuestPDF.Examples/ShowOnceExample.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class ShowOnceExample + { + [Test] + public void ShowOnce() + { + RenderingTest + .Create() + .ProduceImages() + .ShowResults() + .RenderDocument(container => + { + container.Page(page => + { + page.Margin(20); + page.Size(PageSizes.A7.Landscape()); + page.Background(Colors.White); + + page.Header().Text("With show once", TextStyle.Default.SemiBold()); + + page.Content().PaddingVertical(5).Row(row => + { + row.RelativeColumn() + .Background(Colors.Grey.Lighten2) + .Border(1) + .Padding(5) + .ShowOnce() + .Text(Placeholders.Label()); + + row.RelativeColumn(2) + .Border(1) + .Padding(5) + .Text(Placeholders.Paragraph()); + }); + + page.Footer().Text(text => + { + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" out of "); + text.TotalPages(); + }); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/SkipOnceExample.cs b/QuestPDF.Examples/SkipOnceExample.cs new file mode 100644 index 0000000..8e73e9d --- /dev/null +++ b/QuestPDF.Examples/SkipOnceExample.cs @@ -0,0 +1,47 @@ +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class SkipOnceExample + { + [Test] + public void SkipOnce() + { + RenderingTest + .Create() + .ProduceImages() + .ShowResults() + .RenderDocument(container => + { + container.Page(page => + { + page.Margin(20); + page.Size(PageSizes.A7.Landscape()); + page.Background(Colors.White); + + page.Header().Stack(stack => + { + stack.Item().ShowOnce().Text("This header is visible on the first page."); + stack.Item().SkipOnce().Text("This header is visible on the second page and all following."); + }); + + page.Content() + .PaddingVertical(10) + .Text(Placeholders.Paragraphs(), TextStyle.Default.Color(Colors.Grey.Medium)); + + page.Footer().Text(text => + { + text.Span("Page "); + text.CurrentPageNumber(); + text.Span(" out of "); + text.TotalPages(); + }); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.Examples/TextBenchmark.cs b/QuestPDF.Examples/TextBenchmark.cs index 990a370..301df11 100644 --- a/QuestPDF.Examples/TextBenchmark.cs +++ b/QuestPDF.Examples/TextBenchmark.cs @@ -21,7 +21,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(PageSizes.A4) - .FileName() .ProducePdf() .ShowResults() .Render(x => ComposeBook(x, chapters)); @@ -43,8 +42,7 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(PageSizes.A4) - .FileName() - .ProducePdf() + .ProducePdf() .Render(x => ComposeBook(x, chapters)); } diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index b01400f..9d1b82e 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -17,7 +17,7 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(500, 300) - .FileName() + .ProduceImages() .ShowResults() .Render(container => @@ -42,7 +42,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(500, 300) - .FileName() .ProduceImages() .ShowResults() .Render(container => @@ -71,7 +70,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(500, 200) - .FileName() .ProduceImages() .ShowResults() .Render(container => @@ -103,7 +101,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(PageSizes.A4) - .FileName() .ProducePdf() .ShowResults() .Render(container => @@ -134,7 +131,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(PageSizes.A4) - .FileName() .ProducePdf() .ShowResults() .Render(container => @@ -164,7 +160,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(PageSizes.A4) - .FileName() .ProducePdf() .ShowResults() .Render(container => @@ -228,7 +223,6 @@ namespace QuestPDF.Examples RenderingTest .Create() .PageSize(PageSizes.A4) - .FileName() .ProducePdf() .ShowResults() .Render(container => diff --git a/QuestPDF.Examples/logo.png b/QuestPDF.Examples/logo.png new file mode 100644 index 0000000..a0d780f Binary files /dev/null and b/QuestPDF.Examples/logo.png differ diff --git a/QuestPDF.ReportSample/DataSource.cs b/QuestPDF.ReportSample/DataSource.cs index 1b278c2..6f92802 100644 --- a/QuestPDF.ReportSample/DataSource.cs +++ b/QuestPDF.ReportSample/DataSource.cs @@ -7,9 +7,6 @@ namespace QuestPDF.ReportSample { public static class DataSource { - public static int SectionCounter { get; set; } - public static int FieldCounter { get; set; } - public static ReportModel GetReport() { return new ReportModel @@ -116,8 +113,8 @@ namespace QuestPDF.ReportSample Date = DateTime.Now - TimeSpan.FromDays(Helpers.Random.NextDouble() * 100), Location = Helpers.RandomLocation(), - MapContextSource = x => Placeholders.Image(400, 300), - MapDetailsSource = x => Placeholders.Image(400, 300) + MapContextSource = Placeholders.Image, + MapDetailsSource = Placeholders.Image }; } } diff --git a/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs b/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs new file mode 100644 index 0000000..2b3898f --- /dev/null +++ b/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs @@ -0,0 +1,85 @@ +using QuestPDF.Drawing; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.ReportSample.Layouts +{ + public class DifferentHeadersTemplate : IDocument + { + public DocumentMetadata GetMetadata() => DocumentMetadata.Default; + + public void Compose(IDocumentContainer container) + { + container + .Page(page => + { + page.Margin(40); + + page.Size(PageSizes.A4); + + page.Header().Element(ComposeHeader); + page.Content().Element(ComposeContent); + page.Footer().Element(ComposeFooter); + }); + } + + private void ComposeHeader(IContainer container) + { + container.Background(Colors.Grey.Lighten3).Border(1).Stack(stack => + { + 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(1).AlignRight().Box().AlignMiddle().Background(Colors.Blue.Darken2).Padding(30); + }); + stack.Item().SkipOnce().Padding(5).Row(row => + { + 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); + }); + }); + } + + private void ComposeContent(IContainer container) + { + container.Stack(stack => + { + stack.Item().PaddingVertical(80).Text("First"); + stack.Item().PageBreak(); + stack.Item().PaddingVertical(80).Text("Second"); + stack.Item().PageBreak(); + stack.Item().PaddingVertical(80).Text("Third"); + stack.Item().PageBreak(); + }); + } + + private void ComposeFooter(IContainer container) + { + container.Background(Colors.Grey.Lighten3).Stack(stack => + { + stack.Item().ShowOnce().Background(Colors.Grey.Lighten3).Row(row => + { + row.RelativeColumn().Text(x => + { + x.CurrentPageNumber(); + x.Span(" / "); + x.TotalPages(); + }); + row.RelativeColumn().AlignRight().Text("Footer for header"); + }); + + stack.Item().SkipOnce().Background(Colors.Grey.Lighten3).Row(row => + { + row.RelativeColumn().Text(x => + { + x.CurrentPageNumber(); + x.Span(" / "); + x.TotalPages(); + }); + row.RelativeColumn().AlignRight().Text("Footer for every page except header"); + }); + }); + } + } +} \ No newline at end of file diff --git a/QuestPDF.ReportSample/Layouts/ImagePlaceholder.cs b/QuestPDF.ReportSample/Layouts/ImagePlaceholder.cs new file mode 100644 index 0000000..0a2a63a --- /dev/null +++ b/QuestPDF.ReportSample/Layouts/ImagePlaceholder.cs @@ -0,0 +1,20 @@ +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.ReportSample.Layouts +{ + public class ImagePlaceholder : IComponent + { + public static bool Solid { get; set; } = false; + + public void Compose(IContainer container) + { + if (Solid) + container.Background(Placeholders.Color()); + + else + container.Image(Placeholders.Image); + } + } +} \ No newline at end of file diff --git a/QuestPDF.ReportSample/Layouts/ImageTemplate.cs b/QuestPDF.ReportSample/Layouts/ImageTemplate.cs deleted file mode 100644 index 673d470..0000000 --- a/QuestPDF.ReportSample/Layouts/ImageTemplate.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System; -using QuestPDF.Fluent; -using QuestPDF.Infrastructure; - -namespace QuestPDF.ReportSample.Layouts -{ - public class ImageTemplate : IComponent - { - private Func Source { get; } - private float AspectRatio { get; } - - public ImageTemplate(byte[] source, float aspectRatio = 1.333333f) : this(_ => source, aspectRatio) - { - - } - - public ImageTemplate(Func source, float aspectRatio = 1.333333f) - { - Source = source; - AspectRatio = aspectRatio; - } - - public void Compose(IContainer container) - { - container - .AspectRatio(AspectRatio) - .Background("#EEEEEE") - .Image(Source(Size.Zero)); - } - } -} \ No newline at end of file diff --git a/QuestPDF.ReportSample/Layouts/PhotoTemplate.cs b/QuestPDF.ReportSample/Layouts/PhotoTemplate.cs index b265d03..d8fa483 100644 --- a/QuestPDF.ReportSample/Layouts/PhotoTemplate.cs +++ b/QuestPDF.ReportSample/Layouts/PhotoTemplate.cs @@ -30,14 +30,14 @@ namespace QuestPDF.ReportSample.Layouts container .Row(row => { - row.RelativeColumn(2).AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3); + row.RelativeColumn(2).AspectRatio(4 / 3f).Component(); row.RelativeColumn().PaddingLeft(5).Stack(stack => { stack.Spacing(7f); - stack.Item().AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3); - stack.Item().AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3); + stack.Item().AspectRatio(4 / 3f).Component(); + stack.Item().AspectRatio(4 / 3f).Component(); }); }); } @@ -48,13 +48,13 @@ namespace QuestPDF.ReportSample.Layouts { grid.Columns(6); - grid.Item().LabelCell().Text("Date", Typography.Normal); - grid.Item(2).ValueCell().Text(Model.Date?.ToString("g") ?? string.Empty, Typography.Normal); - grid.Item().LabelCell().Text("Location", Typography.Normal); - grid.Item(2).ValueCell().Text(Model.Location.Format(), Typography.Normal); + grid.Item().LabelCell().Text("Date"); + grid.Item(2).ValueCell().Text(Model.Date?.ToString("g") ?? string.Empty); + grid.Item().LabelCell().Text("Location"); + grid.Item(2).ValueCell().Text(Model.Location.Format()); - grid.Item().LabelCell().Text("Comments", Typography.Normal); - grid.Item(5).ValueCell().Text(Model.Comments, Typography.Normal); + grid.Item().LabelCell().Text("Comments"); + grid.Item(5).ValueCell().Text(Model.Comments); }); } } diff --git a/QuestPDF.ReportSample/Layouts/SectionTemplate.cs b/QuestPDF.ReportSample/Layouts/SectionTemplate.cs index 178bc0c..df712e2 100644 --- a/QuestPDF.ReportSample/Layouts/SectionTemplate.cs +++ b/QuestPDF.ReportSample/Layouts/SectionTemplate.cs @@ -30,11 +30,11 @@ namespace QuestPDF.ReportSample.Layouts { stack.Item().EnsureSpace(25).Row(row => { - row.ConstantColumn(150).LabelCell().Text(part.Label, Typography.Normal); + row.ConstantColumn(150).LabelCell().Text(part.Label); var frame = row.RelativeColumn().ValueCell(); if (part is ReportSectionText text) - frame.ShowEntire().Text(text.Text, Typography.Normal); + frame.ShowEntire().Text(text.Text); if (part is ReportSectionMap map) frame.Element(x => MapElement(x, map)); @@ -51,7 +51,7 @@ namespace QuestPDF.ReportSample.Layouts { if (model.ImageSource == null || model.Location == null) { - container.Text("No location provided", Typography.Normal); + container.Text("No location provided"); return; } @@ -59,25 +59,25 @@ namespace QuestPDF.ReportSample.Layouts { stack.Spacing(5); - stack.Item().MaxWidth(250).AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3); - stack.Item().Text(model.Location.Format(), Typography.Normal); + stack.Item().MaxWidth(250).AspectRatio(4 / 3f).Component(); + stack.Item().Text(model.Location.Format()); }); } void PhotosElement(IContainer container, ReportSectionPhotos model) { - if (model.Photos.Count == 0) + if (model.Photos.Count == 0) { container.Text("No photos", Typography.Normal); return; } - container.DebugArea("Photos").Grid(grid => + container.Debug("Photos").Grid(grid => { grid.Spacing(5); grid.Columns(3); - model.Photos.ForEach(x => grid.Item().AspectRatio(4 / 3f).Background(Colors.Grey.Lighten3)); + model.Photos.ForEach(x => grid.Item().AspectRatio(4 / 3f).Component()); }); } } diff --git a/QuestPDF.ReportSample/Layouts/StandardReport.cs b/QuestPDF.ReportSample/Layouts/StandardReport.cs index 8f4b2f8..119ba2c 100644 --- a/QuestPDF.ReportSample/Layouts/StandardReport.cs +++ b/QuestPDF.ReportSample/Layouts/StandardReport.cs @@ -27,6 +27,8 @@ namespace QuestPDF.ReportSample.Layouts container .Page(page => { + page.DefaultTextStyle(Typography.Normal); + page.MarginVertical(40); page.MarginHorizontal(50); @@ -37,8 +39,6 @@ namespace QuestPDF.ReportSample.Layouts page.Footer().AlignCenter().Text(text => { - text.DefaultTextStyle(Typography.Normal); - text.CurrentPageNumber(); text.Span(" / "); text.TotalPages(); @@ -69,8 +69,8 @@ namespace QuestPDF.ReportSample.Layouts { grid.Item().Text(text => { - text.Span($"{field.Label}: ", Typography.Normal.SemiBold()); - text.Span(field.Value, Typography.Normal); + text.Span($"{field.Label}: ", TextStyle.Default.SemiBold()); + text.Span(field.Value); }); } }); diff --git a/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs b/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs index 087cfff..64ff8f2 100644 --- a/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs +++ b/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs @@ -41,9 +41,9 @@ namespace QuestPDF.ReportSample.Layouts .InternalLink(locationName) .Row(row => { - row.ConstantColumn(25).Text($"{number}.", Typography.Normal); - row.RelativeColumn().Text(locationName, Typography.Normal); - row.ConstantColumn(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName, Typography.Normal)); + row.ConstantColumn(25).Text($"{number}."); + row.RelativeColumn().Text(locationName); + row.ConstantColumn(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName)); }); } } diff --git a/QuestPDF.ReportSample/Tests.cs b/QuestPDF.ReportSample/Tests.cs index 78f0da4..6983880 100644 --- a/QuestPDF.ReportSample/Tests.cs +++ b/QuestPDF.ReportSample/Tests.cs @@ -33,6 +33,8 @@ namespace QuestPDF.ReportSample [Test] public void Profile() { + ImagePlaceholder.Solid = true; + var container = new DocumentContainer(); Report.Compose(container); var content = container.Compose(); diff --git a/QuestPDF.ReportSample/Typography.cs b/QuestPDF.ReportSample/Typography.cs index 1d8992c..434bf37 100644 --- a/QuestPDF.ReportSample/Typography.cs +++ b/QuestPDF.ReportSample/Typography.cs @@ -8,6 +8,6 @@ namespace QuestPDF.ReportSample { public static TextStyle Title => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Darken3).Size(26).Black(); public static TextStyle Headline => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Medium).Size(16).SemiBold(); - public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Black).Size(11).LineHeight(1.1f); + public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Verdana).Color(Colors.Black).Size(10).LineHeight(1.2f); } } \ No newline at end of file diff --git a/QuestPDF.UnitTests/ConstrainedTests.cs b/QuestPDF.UnitTests/ConstrainedTests.cs index 05c68c5..99490f1 100644 --- a/QuestPDF.UnitTests/ConstrainedTests.cs +++ b/QuestPDF.UnitTests/ConstrainedTests.cs @@ -9,6 +9,8 @@ namespace QuestPDF.UnitTests [TestFixture] public class ConstrainedTests { + #region Height + [Test] public void Measure_MinHeight_ExpectWrap() { @@ -88,5 +90,91 @@ namespace QuestPDF.UnitTests .ExpectChildMeasure(new Size(400, 100), SpacePlan.Wrap()) .CheckMeasureResult(SpacePlan.Wrap()); } + + #endregion + + #region Width + + [Test] + public void Measure_MinWidth_ExpectWrap() + { + TestPlan + .For(x => new Constrained + { + MinWidth = 100 + }) + .MeasureElement(new Size(50, 400)) + .CheckMeasureResult(new Wrap()); + } + + [Test] + public void Measure_MinWidth_ExtendHeight() + { + TestPlan + .For(x => new Constrained + { + MinWidth = 100, + Child = x.CreateChild() + }) + .MeasureElement(new Size(200, 400)) + .ExpectChildMeasure(new Size(200, 400), new FullRender(50, 400)) + .CheckMeasureResult(new FullRender(100, 400)); + } + + [Test] + public void Measure_MinWidth_PassHeight() + { + TestPlan + .For(x => new Constrained + { + MinWidth = 100, + Child = x.CreateChild() + }) + .MeasureElement(new Size(200, 400)) + .ExpectChildMeasure(new Size(200, 400), new FullRender(150, 400)) + .CheckMeasureResult(new FullRender(150, 400)); + } + + [Test] + public void Measure_MaxWidth_Empty() + { + TestPlan + .For(x => new Constrained + { + MaxWidth = 100 + }) + .MeasureElement(new Size(150, 400)) + .CheckMeasureResult(new FullRender(0, 0)); + } + + [Test] + public void Measure_MaxWidth_PartialRender() + { + TestPlan + .For(x => new Constrained + { + MaxWidth = 100, + Child = x.CreateChild() + }) + .MeasureElement(new Size(200, 400)) + .ExpectChildMeasure(new Size(100, 400), new PartialRender(75, 400)) + .CheckMeasureResult(new PartialRender(75, 400)); + } + + [Test] + public void Measure_MaxWidth_ExpectWrap() + { + TestPlan + .For(x => new Constrained + { + MaxWidth = 100, + Child = x.CreateChild() + }) + .MeasureElement(new Size(200, 400)) + .ExpectChildMeasure(new Size(100, 400), new Wrap()) + .CheckMeasureResult(new Wrap()); + } + + #endregion } } \ No newline at end of file diff --git a/QuestPDF.UnitTests/DynamicImageTests.cs b/QuestPDF.UnitTests/DynamicImageTests.cs index 2755cb2..91f379d 100644 --- a/QuestPDF.UnitTests/DynamicImageTests.cs +++ b/QuestPDF.UnitTests/DynamicImageTests.cs @@ -66,7 +66,7 @@ namespace QuestPDF.UnitTests .ExpectCanvasDrawImage(Position.Zero, new Size(400, 300)) .CheckDrawResult(); - passedSize.Should().Be(new Size(400, 300)); + passedSize.Should().BeEquivalentTo(new Size(400, 300)); } byte[] GenerateImage(Size size) diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index 51d8489..3ac70a5 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -1,9 +1,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using QuestPDF.Drawing.Exceptions; using QuestPDF.Drawing.Proxy; using QuestPDF.Elements; +using QuestPDF.Elements.Text; +using QuestPDF.Elements.Text.Items; using QuestPDF.Infrastructure; namespace QuestPDF.Drawing @@ -107,6 +110,12 @@ namespace QuestPDF.Drawing { ElementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode." }; + throw new DocumentLayoutException( + $"Composed layout generates infinite document. This may happen in two cases. " + + $"1) Your document and its layout configuration is correct but the content takes more than {documentMetadata.DocumentLayoutExceptionThreshold} pages. " + + $"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " + + $"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." + + $"Please analyze your documents structure to detect this element and fix its size constraints."); } } @@ -130,5 +139,27 @@ namespace QuestPDF.Drawing return debuggingState; } + + internal static void ApplyDefaultTextStyle(this Element content, TextStyle documentDefaultTextStyle) + { + documentDefaultTextStyle.ApplyGlobalStyle(TextStyle.LibraryDefault); + + content.HandleVisitor(element => + { + var text = element as TextBlock; + + if (text == null) + return; + + foreach (var child in text.Children) + { + if (child is TextBlockSpan textSpan) + textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle); + + if (child is TextBlockElement textElement) + ApplyDefaultTextStyle(textElement.Element, documentDefaultTextStyle); + } + }); + } } -} +} \ No newline at end of file diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index 5e0e9e3..cd600e4 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -33,7 +33,7 @@ namespace QuestPDF.Drawing internal static SKPaint ToPaint(this TextStyle style) { - return Paints.GetOrAdd(style.ToString(), key => Convert(style)); + return Paints.GetOrAdd(style.Key, key => Convert(style)); static SKPaint Convert(TextStyle style) { @@ -41,7 +41,7 @@ namespace QuestPDF.Drawing { Color = SKColor.Parse(style.Color), Typeface = GetTypeface(style), - TextSize = style.Size, + TextSize = (style.Size ?? 12), TextEncoding = SKTextEncoding.Utf32 }; } @@ -51,16 +51,16 @@ namespace QuestPDF.Drawing if (Typefaces.TryGetValue(style.FontType, out var result)) return result; - var slant = style.IsItalic ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright; + var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright; - return SKTypeface.FromFamilyName(style.FontType, (int)style.FontWeight, (int)SKFontStyleWidth.Normal, slant) + return SKTypeface.FromFamilyName(style.FontType, (int)(style.FontWeight ?? FontWeight.Normal), (int)SKFontStyleWidth.Normal, slant) ?? throw new ArgumentException($"The typeface {style.FontType} could not be found."); } } internal static SKFontMetrics ToFontMetrics(this TextStyle style) { - return FontMetrics.GetOrAdd(style.ToString(), key => style.ToPaint().FontMetrics); + return FontMetrics.GetOrAdd(style.Key, key => style.ToPaint().FontMetrics); } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Constrained.cs b/QuestPDF/Elements/Constrained.cs index 7c27eff..910bb91 100644 --- a/QuestPDF/Elements/Constrained.cs +++ b/QuestPDF/Elements/Constrained.cs @@ -22,8 +22,8 @@ namespace QuestPDF.Elements return SpacePlan.Wrap(); var available = new Size( - LimitMin(availableSpace.Width, MaxWidth), - LimitMin(availableSpace.Height, MaxHeight)); + Min(MaxWidth, availableSpace.Width), + Min(MaxHeight, availableSpace.Height)); var measurement = base.Measure(available); @@ -31,8 +31,8 @@ namespace QuestPDF.Elements return SpacePlan.Wrap(); var actualSize = new Size( - LimitMax(measurement.Width, MinWidth), - LimitMax(measurement.Height, MinHeight)); + Max(MinWidth, measurement.Width), + Max(MinHeight, measurement.Height)); if (measurement.Type == SpacePlanType.FullRender) return SpacePlan.FullRender(actualSize); @@ -46,20 +46,20 @@ namespace QuestPDF.Elements internal override void Draw(Size availableSpace) { var available = new Size( - LimitMin(availableSpace.Width, MaxWidth), - LimitMin(availableSpace.Height, MaxHeight)); + Min(MaxWidth, availableSpace.Width), + Min(MaxHeight, availableSpace.Height)); - base.Draw(available); - } - - private float LimitMin(float value, float? limit) - { - return limit.HasValue ? Math.Min(value, limit.Value) : value; + Child?.Draw(available); } - private float LimitMax(float value, float? limit) + private static float Min(float? x, float y) { - return limit.HasValue ? Math.Max(value, limit.Value) : value; + return x.HasValue ? Math.Min(x.Value, y) : y; + } + + private static float Max(float? x, float y) + { + return x.HasValue ? Math.Max(x.Value, y) : y; } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Grid.cs b/QuestPDF/Elements/Grid.cs index 4649ac4..7014bf6 100644 --- a/QuestPDF/Elements/Grid.cs +++ b/QuestPDF/Elements/Grid.cs @@ -2,7 +2,6 @@ using System.Linq; using QuestPDF.Fluent; using QuestPDF.Infrastructure; -using static QuestPDF.Infrastructure.HorizontalAlignment; namespace QuestPDF.Elements { @@ -20,7 +19,7 @@ namespace QuestPDF.Elements public Queue ChildrenQueue { get; set; } = new Queue(); public int ColumnsCount { get; set; } = DefaultColumnsCount; - public HorizontalAlignment Alignment { get; set; } = Left; + public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; public float VerticalSpacing { get; set; } = 0; public float HorizontalSpacing { get; set; } = 0; @@ -62,15 +61,15 @@ namespace QuestPDF.Elements var emptySpace = ColumnsCount - columnsWidth; var hasEmptySpace = emptySpace >= Size.Epsilon; - if (Alignment == Center) + if (Alignment == HorizontalAlignment.Center) emptySpace /= 2; - if (hasEmptySpace && Alignment != Left) + if (hasEmptySpace && Alignment != HorizontalAlignment.Left) row.RelativeColumn(emptySpace); elements.ForEach(x => row.RelativeColumn(x.Columns).Element(x.Child)); - if (hasEmptySpace && Alignment != Right) + if (hasEmptySpace && Alignment != HorizontalAlignment.Right) row.RelativeColumn(emptySpace); } } diff --git a/QuestPDF/Elements/Inlined.cs b/QuestPDF/Elements/Inlined.cs new file mode 100644 index 0000000..8f0cb0b --- /dev/null +++ b/QuestPDF/Elements/Inlined.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class InlinedElement : Container + { + public ISpacePlan? MeasureCache { get; set; } + + internal override ISpacePlan Measure(Size availableSpace) + { + // TODO: once element caching proxy is introduces, this can be removed + + MeasureCache ??= Child.Measure(Size.Max); + return MeasureCache; + } + } + + internal enum InlinedAlignment + { + Left, + Center, + Right, + Justify, + SpaceAround + } + + internal class Inlined : Element, IStateResettable + { + public List Elements { get; internal set; } = new List(); + private Queue ChildrenQueue { get; set; } + + internal float VerticalSpacing { get; set; } + internal float HorizontalSpacing { get; set; } + + internal InlinedAlignment ElementsAlignment { get; set; } + internal VerticalAlignment BaselineAlignment { get; set; } + + public void ResetState() + { + ChildrenQueue = new Queue(Elements); + } + + internal override void HandleVisitor(Action visit) + { + Elements.ForEach(x => x.HandleVisitor(visit)); + base.HandleVisitor(visit); + } + + internal override ISpacePlan Measure(Size availableSpace) + { + if (!ChildrenQueue.Any()) + return new FullRender(Size.Zero); + + var lines = Compose(availableSpace); + + if (!lines.Any()) + return new Wrap(); + + var lineSizes = lines + .Select(line => + { + var size = GetLineSize(line); + var heightWithSpacing = size.Height + (line.Count - 1) * HorizontalSpacing; + return new Size(size.Width, heightWithSpacing); + }) + .ToList(); + + var width = lineSizes.Max(x => x.Width); + var height = lineSizes.Sum(x => x.Height) + (lines.Count - 1) * VerticalSpacing; + var targetSize = new Size(width, height); + + var isPartiallyRendered = lines.Sum(x => x.Count) != ChildrenQueue.Count; + + if (isPartiallyRendered) + return new PartialRender(targetSize); + + return new FullRender(targetSize); + } + + internal override void Draw(Size availableSpace) + { + var lines = Compose(availableSpace); + var topOffset = 0f; + + foreach (var line in lines) + { + var height = line.Select(x => x.Measure(Size.Max) as Size).Where(x => x != null).Max(x => x.Height); + DrawLine(line); + + topOffset += height + VerticalSpacing; + Canvas.Translate(new Position(0, height + VerticalSpacing)); + } + + Canvas.Translate(new Position(0, -topOffset)); + lines.SelectMany(x => x).ToList().ForEach(x => ChildrenQueue.Dequeue()); + + void DrawLine(ICollection elements) + { + var lineSize = GetLineSize(elements); + + var elementOffset = ElementOffset(); + var leftOffset = AlignOffset(); + Canvas.Translate(new Position(leftOffset, 0)); + + foreach (var element in elements) + { + var size = element.Measure(Size.Max) as Size; + var baselineOffset = BaselineOffset(size, lineSize.Height); + + Canvas.Translate(new Position(0, baselineOffset)); + element.Draw(size); + Canvas.Translate(new Position(0, -baselineOffset)); + + leftOffset += size.Width + elementOffset; + Canvas.Translate(new Position(size.Width + elementOffset, 0)); + } + + Canvas.Translate(new Position(-leftOffset, 0)); + + float ElementOffset() + { + var difference = availableSpace.Width - lineSize.Width; + + if (elements.Count == 1) + return 0; + + if (ElementsAlignment == InlinedAlignment.Justify) + return difference / (elements.Count - 1); + + if (ElementsAlignment == InlinedAlignment.SpaceAround) + return difference / (elements.Count + 1); + + return HorizontalSpacing; + } + + float AlignOffset() + { + if (ElementsAlignment == InlinedAlignment.Left) + return 0; + + if (ElementsAlignment == InlinedAlignment.Justify) + return 0; + + if (ElementsAlignment == InlinedAlignment.SpaceAround) + return elementOffset; + + var difference = availableSpace.Width - lineSize.Width - (elements.Count - 1) * HorizontalSpacing; + + if (ElementsAlignment == InlinedAlignment.Center) + return difference / 2; + + if (ElementsAlignment == InlinedAlignment.Right) + return difference; + + return 0; + } + + float BaselineOffset(Size elementSize, float lineHeight) + { + if (BaselineAlignment == VerticalAlignment.Top) + return 0; + + var difference = lineHeight - elementSize.Height; + + if (BaselineAlignment == VerticalAlignment.Middle) + return difference / 2; + + return difference; + } + } + } + + Size GetLineSize(ICollection elements) + { + var sizes = elements + .Select(x => x.Measure(Size.Max) as Size) + .Where(x => x != null) + .ToList(); + + var width = sizes.Sum(x => x.Width); + var height = sizes.Max(x => x.Height); + + return new Size(width, height); + } + + // list of lines, each line is a list of elements + private ICollection> Compose(Size availableSize) + { + var queue = new Queue(ChildrenQueue); + var result = new List>(); + + var topOffset = 0f; + + while (true) + { + var line = GetNextLine(); + + if (!line.Any()) + break; + + var height = line + .Select(x => x.Measure(availableSize) as Size) + .Where(x => x != null) + .Max(x => x.Height); + + if (topOffset + height > availableSize.Height + Size.Epsilon) + break; + + topOffset += height + VerticalSpacing; + result.Add(line); + } + + return result; + + ICollection GetNextLine() + { + var result = new List(); + var leftOffset = GetInitialAlignmentOffset(); + + while (true) + { + if (!queue.Any()) + break; + + var element = queue.Peek(); + var size = element.Measure(Size.Max) as Size; + + if (size == null) + break; + + if (leftOffset + size.Width > availableSize.Width + Size.Epsilon) + break; + + queue.Dequeue(); + leftOffset += size.Width + HorizontalSpacing; + result.Add(element); + } + + return result; + } + + float GetInitialAlignmentOffset() + { + // this method makes sure that the spacing between elements is no lesser than configured + + if (ElementsAlignment == InlinedAlignment.SpaceAround) + return HorizontalSpacing * 2; + + return 0; + } + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Page.cs b/QuestPDF/Elements/Page.cs index 35efe7e..27e6946 100644 --- a/QuestPDF/Elements/Page.cs +++ b/QuestPDF/Elements/Page.cs @@ -1,4 +1,5 @@ using System; +using QuestPDF.Drawing; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; @@ -7,6 +8,8 @@ namespace QuestPDF.Elements { internal class Page : IComponent { + public TextStyle DefaultTextStyle { get; set; } = new TextStyle(); + public Size MinSize { get; set; } = PageSizes.A4; public Size MaxSize { get; set; } = PageSizes.A4; @@ -15,6 +18,8 @@ namespace QuestPDF.Elements public float MarginTop { get; set; } public float MarginBottom { get; set; } + public string BackgroundColor { get; set; } = Colors.Transparent; + public Element Header { get; set; } = Empty.Instance; public Element Content { get; set; } = Empty.Instance; public Element Footer { get; set; } = Empty.Instance; @@ -22,12 +27,13 @@ namespace QuestPDF.Elements public void Compose(IContainer container) { container - .MinWidth(MinSize.Width) .MinHeight(MinSize.Height) .MaxWidth(MaxSize.Width) .MaxHeight(MaxSize.Height) + + .Background(BackgroundColor) .PaddingLeft(MarginLeft) .PaddingRight(MarginRight) @@ -54,6 +60,8 @@ namespace QuestPDF.Elements .Element(Footer); }); + (container as Element).ApplyDefaultTextStyle(DefaultTextStyle); + bool IsClose(float x, float y) { return Math.Abs(x - y) < Size.Epsilon; diff --git a/QuestPDF/Elements/SkipOnce.cs b/QuestPDF/Elements/SkipOnce.cs new file mode 100644 index 0000000..b1bb9c2 --- /dev/null +++ b/QuestPDF/Elements/SkipOnce.cs @@ -0,0 +1,34 @@ +using QuestPDF.Drawing.SpacePlan; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Elements +{ + internal class SkipOnce : ContainerElement, IStateResettable + { + private bool FirstPageWasSkipped { get; set; } + + public void ResetState() + { + FirstPageWasSkipped = false; + } + + internal override ISpacePlan Measure(Size availableSpace) + { + if (Child == null || !FirstPageWasSkipped) + return new FullRender(Size.Zero); + + return Child.Measure(availableSpace); + } + + internal override void Draw(Size availableSpace) + { + if (Child == null) + return; + + if (FirstPageWasSkipped) + Child.Draw(availableSpace); + + FirstPageWasSkipped = true; + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Items/ITextBlockItem.cs b/QuestPDF/Elements/Text/Items/ITextBlockItem.cs index 581dcc9..570087b 100644 --- a/QuestPDF/Elements/Text/Items/ITextBlockItem.cs +++ b/QuestPDF/Elements/Text/Items/ITextBlockItem.cs @@ -1,4 +1,5 @@ using QuestPDF.Elements.Text.Calculation; +using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Text.Items { diff --git a/QuestPDF/Elements/Text/Items/TextBlockExternalLink.cs b/QuestPDF/Elements/Text/Items/TextBlockExternalLink.cs index 9eac125..5337fca 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockExternalLink.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockExternalLink.cs @@ -3,33 +3,22 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Text.Items { - internal class TextBlockExternalLink : ITextBlockItem + internal class TextBlockExternalLink : TextBlockSpan { - public TextStyle Style { get; set; } = new TextStyle(); - public string Text { get; set; } public string Url { get; set; } - public TextMeasurementResult? Measure(TextMeasurementRequest request) + public override TextMeasurementResult? Measure(TextMeasurementRequest request) { - return GetItem().MeasureWithoutCache(request); + return MeasureWithoutCache(request); } - public void Draw(TextDrawingRequest request) + public override void Draw(TextDrawingRequest request) { request.Canvas.Translate(new Position(0, request.TotalAscent)); request.Canvas.DrawExternalLink(Url, new Size(request.TextSize.Width, request.TextSize.Height)); request.Canvas.Translate(new Position(0, -request.TotalAscent)); - GetItem().Draw(request); - } - - private TextBlockSpan GetItem() - { - return new TextBlockSpan - { - Style = Style, - Text = Text - }; + base.Draw(request); } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Items/TextBlockInternalLink.cs b/QuestPDF/Elements/Text/Items/TextBlockInternalLink.cs index 914cd61..6b28113 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockInternalLink.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockInternalLink.cs @@ -3,33 +3,22 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Text.Items { - internal class TextBlockInternalLink : ITextBlockItem + internal class TextBlockInternalLink : TextBlockSpan { - public TextStyle Style { get; set; } = new TextStyle(); - public string Text { get; set; } public string LocationName { get; set; } - public TextMeasurementResult? Measure(TextMeasurementRequest request) + public override TextMeasurementResult? Measure(TextMeasurementRequest request) { - return GetItem().MeasureWithoutCache(request); + return MeasureWithoutCache(request); } - public void Draw(TextDrawingRequest request) + public override void Draw(TextDrawingRequest request) { request.Canvas.Translate(new Position(0, request.TotalAscent)); request.Canvas.DrawLocationLink(LocationName, new Size(request.TextSize.Width, request.TextSize.Height)); request.Canvas.Translate(new Position(0, -request.TotalAscent)); - GetItem().Draw(request); - } - - private TextBlockSpan GetItem() - { - return new TextBlockSpan - { - Style = Style, - Text = Text - }; + base.Draw(request); } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs b/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs index fda65ac..85a829d 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs @@ -3,34 +3,31 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Text.Items { - internal class TextBlockPageNumber : ITextBlockItem + internal class TextBlockPageNumber : TextBlockSpan { - public TextStyle Style { get; set; } = new TextStyle(); public string SlotName { get; set; } - public TextMeasurementResult? Measure(TextMeasurementRequest request) + public override TextMeasurementResult? Measure(TextMeasurementRequest request) { - return GetItem(request.PageContext).MeasureWithoutCache(request); + SetPageNumber(request.PageContext); + return MeasureWithoutCache(request); } - public void Draw(TextDrawingRequest request) + public override void Draw(TextDrawingRequest request) { - GetItem(request.PageContext).Draw(request); + SetPageNumber(request.PageContext); + base.Draw(request); } - private TextBlockSpan GetItem(IPageContext context) + private void SetPageNumber(IPageContext context) { var pageNumberPlaceholder = 123; var pageNumber = context.GetRegisteredLocations().Contains(SlotName) ? context.GetLocationPage(SlotName) : pageNumberPlaceholder; - - return new TextBlockSpan - { - Style = Style, - Text = pageNumber.ToString() - }; + + Text = pageNumber.ToString(); } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 2c0e4b2..a8d0436 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -14,7 +14,7 @@ namespace QuestPDF.Elements.Text.Items private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>(); - public TextMeasurementResult? Measure(TextMeasurementRequest request) + public virtual TextMeasurementResult? Measure(TextMeasurementRequest request) { var cacheKey = (request.StartIndex, request.AvailableWidth); @@ -45,7 +45,7 @@ namespace QuestPDF.Elements.Text.Items { Width = 0, - LineHeight = Style.LineHeight, + LineHeight = Style.LineHeight ?? 1, Ascent = fontMetrics.Ascent, Descent = fontMetrics.Descent }; @@ -96,7 +96,7 @@ namespace QuestPDF.Elements.Text.Items Ascent = fontMetrics.Ascent, Descent = fontMetrics.Descent, - LineHeight = Style.LineHeight, + LineHeight = Style.LineHeight ?? 1, StartIndex = startIndex, EndIndex = endIndex, @@ -105,7 +105,7 @@ namespace QuestPDF.Elements.Text.Items }; } - public void Draw(TextDrawingRequest request) + public virtual void Draw(TextDrawingRequest request) { var fontMetrics = Style.ToFontMetrics(); @@ -115,12 +115,12 @@ namespace QuestPDF.Elements.Text.Items request.Canvas.DrawText(text, Position.Zero, Style); // draw underline - if (Style.HasUnderline && fontMetrics.UnderlinePosition.HasValue) - DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness.Value); + if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue) + DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1); // draw stroke - if (Style.HasStrikethrough && fontMetrics.StrikeoutPosition.HasValue) - DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness.Value); + if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue) + DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness ?? 1); void DrawLine(float offset, float thickness) { diff --git a/QuestPDF/Fluent/ElementExtensions.cs b/QuestPDF/Fluent/ElementExtensions.cs index f41d050..895cf6e 100644 --- a/QuestPDF/Fluent/ElementExtensions.cs +++ b/QuestPDF/Fluent/ElementExtensions.cs @@ -70,7 +70,12 @@ namespace QuestPDF.Fluent { return element.Element(new ShowOnce()); } - + + public static IContainer SkipOnce(this IContainer element) + { + return element.Element(new SkipOnce()); + } + public static IContainer ShowEntire(this IContainer element) { return element.Element(new ShowEntire()); diff --git a/QuestPDF/Fluent/ImageExtensions.cs b/QuestPDF/Fluent/ImageExtensions.cs index 7a3fe4e..bc29883 100644 --- a/QuestPDF/Fluent/ImageExtensions.cs +++ b/QuestPDF/Fluent/ImageExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using QuestPDF.Drawing.Exceptions; using QuestPDF.Elements; using QuestPDF.Infrastructure; using SkiaSharp; @@ -7,12 +9,29 @@ namespace QuestPDF.Fluent { public static class ImageExtensions { - public static void Image(this IContainer parent, byte[] data, ImageScaling scaling = ImageScaling.FitWidth) + public static void Image(this IContainer parent, byte[] imageData, ImageScaling scaling = ImageScaling.FitWidth) { - if (data == null) - return; + var image = SKImage.FromEncodedData(imageData); + parent.Image(image, scaling); + } + + public static void Image(this IContainer parent, string filePath, ImageScaling scaling = ImageScaling.FitWidth) + { + var image = SKImage.FromEncodedData(filePath); + parent.Image(image, scaling); + } + + public static void Image(this IContainer parent, Stream fileStream, ImageScaling scaling = ImageScaling.FitWidth) + { + var image = SKImage.FromEncodedData(fileStream); + parent.Image(image, scaling); + } + + private static void Image(this IContainer parent, SKImage image, ImageScaling scaling = ImageScaling.FitWidth) + { + if (image == null) + throw new DocumentComposeException("Cannot load or decode provided image."); - var image = SKImage.FromEncodedData(data); var aspectRatio = image.Width / (float)image.Height; var imageElement = new Image diff --git a/QuestPDF/Fluent/InlinedExtensions.cs b/QuestPDF/Fluent/InlinedExtensions.cs new file mode 100644 index 0000000..b250c49 --- /dev/null +++ b/QuestPDF/Fluent/InlinedExtensions.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using QuestPDF.Elements; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public class InlinedDescriptor + { + internal Inlined Inlined { get; } = new Inlined(); + + public void Spacing(float value) + { + VerticalSpacing(value); + HorizontalSpacing(value); + } + + public void VerticalSpacing(float value) => Inlined.VerticalSpacing = value; + public void HorizontalSpacing(float value) => Inlined.HorizontalSpacing = value; + + public void BaselineTop() => Inlined.BaselineAlignment = VerticalAlignment.Top; + public void BaselineMiddle() => Inlined.BaselineAlignment = VerticalAlignment.Middle; + public void BaselineBottom() => Inlined.BaselineAlignment = VerticalAlignment.Bottom; + + public void AlignLeft() => Inlined.ElementsAlignment = InlinedAlignment.Left; + public void AlignCenter() => Inlined.ElementsAlignment = InlinedAlignment.Center; + public void AlignRight() => Inlined.ElementsAlignment = InlinedAlignment.Right; + public void AlignJustify() => Inlined.ElementsAlignment = InlinedAlignment.Justify; + public void AlignSpaceAround() => Inlined.ElementsAlignment = InlinedAlignment.SpaceAround; + + public IContainer Item() + { + var container = new InlinedElement(); + Inlined.Elements.Add(container); + return container; + } + } + + public static class InlinedExtensions + { + public static void Inlined(this IContainer element, Action handler) + { + var descriptor = new InlinedDescriptor(); + handler(descriptor); + + element.Element(descriptor.Inlined); + } + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/PageExtensions.cs b/QuestPDF/Fluent/PageExtensions.cs index 24a35a8..82d7dc5 100644 --- a/QuestPDF/Fluent/PageExtensions.cs +++ b/QuestPDF/Fluent/PageExtensions.cs @@ -70,6 +70,16 @@ namespace QuestPDF.Fluent MarginHorizontal(value); } + public void DefaultTextStyle(TextStyle textStyle) + { + Page.DefaultTextStyle = textStyle; + } + + public void Background(string color) + { + Page.BackgroundColor = color; + } + public IContainer Header() { var container = new Container(); diff --git a/QuestPDF/Fluent/TextExtensions.cs b/QuestPDF/Fluent/TextExtensions.cs index dd8eb3c..8282e39 100644 --- a/QuestPDF/Fluent/TextExtensions.cs +++ b/QuestPDF/Fluent/TextExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Elements.Text; using QuestPDF.Elements.Text.Items; @@ -13,7 +14,7 @@ namespace QuestPDF.Fluent { private ICollection TextBlocks { get; } = new List(); private TextStyle DefaultStyle { get; set; } = TextStyle.Default; - private HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; + internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; private float Spacing { get; set; } = 0f; public void DefaultTextStyle(TextStyle style) @@ -49,9 +50,12 @@ namespace QuestPDF.Fluent TextBlocks.Last().Children.Add(item); } - public void Span(string text, TextStyle? style = null) + public void Span(string? text, TextStyle? style = null) { - style ??= DefaultStyle; + if (IsNullOrEmpty(text)) + return; + + style ??= TextStyle.Default; var items = text .Replace("\r", string.Empty) @@ -75,16 +79,12 @@ namespace QuestPDF.Fluent .ForEach(TextBlocks.Add); } - public void Line(string text, TextStyle? style = null) + public void Line(string? text, TextStyle? style = null) { + text ??= string.Empty; Span(text + Environment.NewLine, style); } - - public void Line(string text) - { - Span(text + Environment.NewLine); - } - + public void EmptyLine() { Span(Environment.NewLine); @@ -92,7 +92,10 @@ namespace QuestPDF.Fluent private void PageNumber(string slotName, TextStyle? style = null) { - style ??= DefaultStyle; + if (IsNullOrEmpty(slotName)) + throw new ArgumentException(nameof(slotName)); + + style ??= TextStyle.Default; AddItemToLastTextBlock(new TextBlockPageNumber() { @@ -113,12 +116,21 @@ namespace QuestPDF.Fluent public void PageNumberOfLocation(string locationName, TextStyle? style = null) { + if (IsNullOrEmpty(locationName)) + throw new ArgumentException(nameof(locationName)); + PageNumber(locationName, style); } - public void InternalLocation(string text, string locationName, TextStyle? style = null) + public void InternalLocation(string? text, string locationName, TextStyle? style = null) { - style ??= DefaultStyle; + if (IsNullOrEmpty(text)) + return; + + if (IsNullOrEmpty(locationName)) + throw new ArgumentException(nameof(locationName)); + + style ??= TextStyle.Default; AddItemToLastTextBlock(new TextBlockInternalLink { @@ -128,9 +140,15 @@ namespace QuestPDF.Fluent }); } - public void ExternalLocation(string text, string url, TextStyle? style = null) + public void ExternalLocation(string? text, string url, TextStyle? style = null) { - style ??= DefaultStyle; + if (IsNullOrEmpty(text)) + return; + + if (IsNullOrEmpty(url)) + throw new ArgumentException(nameof(url)); + + style ??= TextStyle.Default; AddItemToLastTextBlock(new TextBlockExternalLink { @@ -156,6 +174,9 @@ namespace QuestPDF.Fluent { TextBlocks.ToList().ForEach(x => x.Alignment = Alignment); + foreach (var textBlockSpan in TextBlocks.SelectMany(x => x.Children).Where(x => x is TextBlockSpan).Cast()) + textBlockSpan.Style.ApplyParentStyle(DefaultStyle); + container.Stack(stack => { stack.Spacing(Spacing); @@ -170,19 +191,18 @@ namespace QuestPDF.Fluent { public static void Text(this IContainer element, Action content) { - var textBlock = new TextBlock(); - - if (element is Alignment alignment) - textBlock.Alignment = alignment.Horizontal; - var descriptor = new TextDescriptor(); + + if (element is Alignment alignment) + descriptor.Alignment = alignment.Horizontal; + content?.Invoke(descriptor); descriptor.Compose(element); } - public static void Text(this IContainer element, object text, TextStyle? style = null) + public static void Text(this IContainer element, object? text, TextStyle? style = null) { - element.Text(x => x.Span(text.ToString(), style)); + element.Text(x => x.Span(text?.ToString(), style)); } } } \ No newline at end of file diff --git a/QuestPDF/Helpers/Placeholders.cs b/QuestPDF/Helpers/Placeholders.cs index d79abd9..bd11a6a 100644 --- a/QuestPDF/Helpers/Placeholders.cs +++ b/QuestPDF/Helpers/Placeholders.cs @@ -7,7 +7,7 @@ namespace QuestPDF.Helpers { public static class Placeholders { - private static Random Random = new Random(); + public static readonly Random Random = new Random(); #region Word Cache diff --git a/QuestPDF/Infrastructure/Size.cs b/QuestPDF/Infrastructure/Size.cs index d7bf147..4baee1a 100644 --- a/QuestPDF/Infrastructure/Size.cs +++ b/QuestPDF/Infrastructure/Size.cs @@ -7,8 +7,8 @@ public readonly float Width; public readonly float Height; - public static Size Zero => new Size(0, 0); - public static Size Max => new Size(14_400, 14_400); + public static Size Zero { get; } = new Size(0, 0); + public static Size Max { get; } = new Size(14_400, 14_400); public Size(float width, float height) { @@ -16,6 +16,6 @@ Height = height; } - public override string ToString() => $"{Width:N2} {Height:N2}"; + public override string ToString() => $"(W: {Width}, H: {Height})"; } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index 76bbd99..640d9d2 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -1,29 +1,68 @@ -using QuestPDF.Helpers; +using System; +using QuestPDF.Helpers; namespace QuestPDF.Infrastructure { public class TextStyle { - internal string Color { get; set; } = Colors.Black; - internal string BackgroundColor { get; set; } = Colors.Transparent; - internal string FontType { get; set; } = "Calibri"; - internal float Size { get; set; } = 12; - internal float LineHeight { get; set; } = 1.2f; - internal FontWeight FontWeight { get; set; } = FontWeight.Normal; - internal bool IsItalic { get; set; } = false; - internal bool HasStrikethrough { get; set; } = false; - internal bool HasUnderline { get; set; } = false; + internal bool HasGlobalStyleApplied { get; private set; } + + internal string? Color { get; set; } + internal string? BackgroundColor { get; set; } + internal string? FontType { get; set; } + internal float? Size { get; set; } + internal float? LineHeight { get; set; } + internal FontWeight? FontWeight { get; set; } + internal bool? IsItalic { get; set; } + internal bool? HasStrikethrough { get; set; } + internal bool? HasUnderline { get; set; } + + internal string? Key { get; private set; } + + internal static TextStyle LibraryDefault => new TextStyle + { + Color = Colors.Black, + BackgroundColor = Colors.Transparent, + FontType = Fonts.Calibri, + Size = 12, + LineHeight = 1.2f, + FontWeight = Infrastructure.FontWeight.Normal, + IsItalic = false, + HasStrikethrough = false, + HasUnderline = false + }; public static TextStyle Default => new TextStyle(); - - private string? KeyCache { get; set; } - public override string ToString() + internal void ApplyGlobalStyle(TextStyle globalStyle) { - KeyCache ??= $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{FontWeight}|{IsItalic}|{HasStrikethrough}|{HasUnderline}"; - return KeyCache; + if (HasGlobalStyleApplied) + return; + + HasGlobalStyleApplied = true; + + ApplyParentStyle(globalStyle); + Key ??= $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{FontWeight}|{IsItalic}|{HasStrikethrough}|{HasUnderline}"; + } + + internal void ApplyParentStyle(TextStyle parentStyle) + { + Color ??= parentStyle.Color; + BackgroundColor ??= parentStyle.BackgroundColor; + FontType ??= parentStyle.FontType; + Size ??= parentStyle.Size; + LineHeight ??= parentStyle.LineHeight; + FontWeight ??= parentStyle.FontWeight; + IsItalic ??= parentStyle.IsItalic; + HasStrikethrough ??= parentStyle.HasStrikethrough; + HasUnderline ??= parentStyle.HasUnderline; } - internal TextStyle Clone() => (TextStyle)MemberwiseClone(); + internal TextStyle Clone() + { + var clone = (TextStyle)MemberwiseClone(); + clone.HasGlobalStyleApplied = false; + return clone; + } } } \ No newline at end of file diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index fcf853c..9019a0c 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -4,9 +4,9 @@ MarcinZiabek CodeFlint QuestPDF - 2021.10.1 + 2021.11.0-beta3 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. - Enhanced text rendering capabilities. Improved rendering performance. + Implemented new elements: SkipOnce and Inlined. Added possibility to define global, page-wide test style. Improved exception handling experience. 8 true Logo.png @@ -15,10 +15,12 @@ https://github.com/QuestPDF/library.git git Marcin Ziąbek, QuestPDF contributors - pdf file export generate generation tool create creation render portable document format quest html library converter open source free standard core + pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core MIT enable net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0 + true + snupkg diff --git a/readme.md b/readme.md index 1b6effb..457fe44 100644 --- a/readme.md +++ b/readme.md @@ -37,8 +37,11 @@ The library is available as a nuget package. You can install it as any other nug ## Documentation **[Release notes and roadmap](https://www.questpdf.com/documentation/releases.html)** - everything that is planned for future library iterations, description of new features and information about potential breaking changes. + **[Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html)** - a short and easy to follow tutorial showing how to design an invoice document under 200 lines of code. + **[API Reference](https://www.questpdf.com/documentation/api-reference.html)** - a detailed description of behavior of all available components and how to use them with C# Fluent API. + **[Patterns and practices](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)** - everything that may help you designing great reports and reusable code that is easy to maintain. ## Example invoice