From fcff989ab2763bb54ce057b02788fbc87e114be1 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Tue, 8 Mar 2022 17:27:30 +0100 Subject: [PATCH] Improved text API --- QuestPDF.Examples/BarcodeExamples.cs | 4 +- QuestPDF.Examples/CanvasExamples.cs | 5 +- QuestPDF.Examples/ChartExamples.cs | 5 +- QuestPDF.Examples/ContinousPage.cs | 2 +- QuestPDF.Examples/DefaultTextStyleExample.cs | 2 +- QuestPDF.Examples/DefaultTextStyleExamples.cs | 7 +- QuestPDF.Examples/ElementExamples.cs | 34 ++- QuestPDF.Examples/EnsureSpaceExample.cs | 2 +- QuestPDF.Examples/FrameExample.cs | 4 +- QuestPDF.Examples/InlinedExamples.cs | 3 +- QuestPDF.Examples/MinimalApiExamples.cs | 63 ++++- QuestPDF.Examples/Padding.cs | 3 +- QuestPDF.Examples/ShowOnceExample.cs | 2 +- QuestPDF.Examples/SkipOnceExample.cs | 2 +- QuestPDF.Examples/TextBenchmark.cs | 14 +- QuestPDF.Examples/TextExamples.cs | 22 +- .../Layouts/DifferentHeadersTemplate.cs | 4 +- .../Layouts/SectionTemplate.cs | 5 +- .../Layouts/StandardReport.cs | 8 +- .../Layouts/TableOfContentsTemplate.cs | 9 +- QuestPDF/Elements/DebugArea.cs | 5 +- QuestPDF/Elements/Placeholder.cs | 2 +- QuestPDF/Fluent/TextExtensions.cs | 134 ++++++---- .../Fluent/TextSpanDescriptorExtensions.cs | 126 ++++++++++ QuestPDF/Infrastructure/TextStyle.cs | 13 + readme.md | 237 ++++-------------- 26 files changed, 426 insertions(+), 291 deletions(-) create mode 100644 QuestPDF/Fluent/TextSpanDescriptorExtensions.cs diff --git a/QuestPDF.Examples/BarcodeExamples.cs b/QuestPDF.Examples/BarcodeExamples.cs index f4fa0dc..762ad20 100644 --- a/QuestPDF.Examples/BarcodeExamples.cs +++ b/QuestPDF.Examples/BarcodeExamples.cs @@ -25,7 +25,9 @@ namespace QuestPDF.Examples .Background(Colors.White) .AlignCenter() .AlignMiddle() - .Text("*QuestPDF*", TextStyle.Default.FontType("Libre Barcode 39").Size(64)); + .Text("*QuestPDF*") + .FontType("Libre Barcode 39") + .Size(64); }); } } diff --git a/QuestPDF.Examples/CanvasExamples.cs b/QuestPDF.Examples/CanvasExamples.cs index a37d41f..aff03cf 100644 --- a/QuestPDF.Examples/CanvasExamples.cs +++ b/QuestPDF.Examples/CanvasExamples.cs @@ -49,7 +49,10 @@ namespace QuestPDF.Examples .PrimaryLayer() .PaddingVertical(10) .PaddingHorizontal(20) - .Text("Sample text", TextStyle.Default.Size(16).Color(Colors.Blue.Darken2).SemiBold()); + .Text("Sample text") + .Size(16) + .Color(Colors.Blue.Darken2) + .SemiBold(); }); }); } diff --git a/QuestPDF.Examples/ChartExamples.cs b/QuestPDF.Examples/ChartExamples.cs index a8eac35..6cb8abe 100644 --- a/QuestPDF.Examples/ChartExamples.cs +++ b/QuestPDF.Examples/ChartExamples.cs @@ -57,7 +57,10 @@ namespace QuestPDF.Examples column .Item() .PaddingBottom(10) - .Text("Chart example", TextStyle.Default.Size(20).SemiBold().Color(Colors.Blue.Medium)); + .Text("Chart example") + .Size(20) + .SemiBold() + .Color(Colors.Blue.Medium); column .Item() diff --git a/QuestPDF.Examples/ContinousPage.cs b/QuestPDF.Examples/ContinousPage.cs index 1e5c545..37e3e47 100644 --- a/QuestPDF.Examples/ContinousPage.cs +++ b/QuestPDF.Examples/ContinousPage.cs @@ -25,7 +25,7 @@ namespace QuestPDF.Examples page.Content().PaddingVertical(10).Border(1).Padding(10).Column(column => { foreach (var index in Enumerable.Range(1, 100)) - column.Item().Text($"Line {index}", TextStyle.Default.Color(Placeholders.Color())); + column.Item().Text($"Line {index}").Color(Placeholders.Color()); }); page.Footer().Text("Footer"); diff --git a/QuestPDF.Examples/DefaultTextStyleExample.cs b/QuestPDF.Examples/DefaultTextStyleExample.cs index e7db9eb..acf176e 100644 --- a/QuestPDF.Examples/DefaultTextStyleExample.cs +++ b/QuestPDF.Examples/DefaultTextStyleExample.cs @@ -38,7 +38,7 @@ namespace QuestPDF.Examples text.Line(Placeholders.Sentence()); // this text has size 20 but also semibold and red - text.Span(Placeholders.Sentence(), TextStyle.Default.Color(Colors.Red.Medium)); + text.Span(Placeholders.Sentence()).Color(Colors.Red.Medium); }); }); }); diff --git a/QuestPDF.Examples/DefaultTextStyleExamples.cs b/QuestPDF.Examples/DefaultTextStyleExamples.cs index 4165a39..9db6bc7 100644 --- a/QuestPDF.Examples/DefaultTextStyleExamples.cs +++ b/QuestPDF.Examples/DefaultTextStyleExamples.cs @@ -26,8 +26,8 @@ namespace QuestPDF.Examples .DefaultTextStyle(TextStyle.Default.Bold().Underline()) .Column(column => { - column.Item().Text("Default style applies to all children", TextStyle.Default); - column.Item().Text("You can override certain styles", TextStyle.Default.Underline(false).Color(Colors.Green.Darken2)); + column.Item().Text("Default style applies to all children"); + column.Item().Text("You can override certain styles").Underline(false).Color(Colors.Green.Darken2); column.Item().PaddingTop(10).Border(1).Grid(grid => { @@ -43,7 +43,8 @@ namespace QuestPDF.Examples .Height(50) .AlignCenter() .AlignMiddle() - .Text(i, TextStyle.Default.Size(16 + i / 4)); + .Text(i) + .Size(16 + i / 4); } }); }); diff --git a/QuestPDF.Examples/ElementExamples.cs b/QuestPDF.Examples/ElementExamples.cs index 635961c..3874f78 100644 --- a/QuestPDF.Examples/ElementExamples.cs +++ b/QuestPDF.Examples/ElementExamples.cs @@ -43,7 +43,9 @@ namespace QuestPDF.Examples .Before() .Background(Colors.Grey.Medium) .Padding(10) - .Text("Notes", TextStyle.Default.Size(16).Color("#FFF")); + .Text("Notes") + .Size(16) + .Color("#FFF"); decoration .Content() @@ -280,12 +282,15 @@ namespace QuestPDF.Examples .Layer() .AlignCenter() .AlignMiddle() - .Text("Watermark", TextStyle.Default.Size(48).Bold().Color(Colors.Green.Lighten3)); + .Text("Watermark") + .Size(48) + .Bold() + .Color(Colors.Green.Lighten3); layers .Layer() .AlignBottom() - .Text(text => text.CurrentPageNumber(TextStyle.Default.Size(16).Color(Colors.Green.Medium))); + .Text(text => text.CurrentPageNumber().Size(16).Color(Colors.Green.Medium)); }); }); } @@ -416,7 +421,9 @@ namespace QuestPDF.Examples .Border(1) .BorderColor(Colors.Grey.Medium) .Padding(10) - .Text(font, TextStyle.Default.FontType(font).Size(16)); + .Text(font) + .FontType(font) + .Size(16); } }); }); @@ -456,7 +463,7 @@ namespace QuestPDF.Examples }); layers.Layer().Background("#8F00").Extend(); - layers.Layer().PaddingTop(40).Text("It works!", TextStyle.Default.Size(24)); + layers.Layer().PaddingTop(40).Text("It works!").Size(24); }); }); } @@ -477,7 +484,7 @@ namespace QuestPDF.Examples //.MinimalBox() .Background(Colors.Grey.Lighten2) .Padding(15) - .Text("Test of the \n box element", TextStyle.Default.Size(20)); + .Text("Test of the \n box element").Size(20); }); } @@ -503,7 +510,8 @@ namespace QuestPDF.Examples decoration .Before() .PaddingBottom(10) - .Text("Example: scale component", headerFontStyle); + .Text("Example: scale component") + .Style(headerFontStyle); decoration .Content() @@ -525,7 +533,8 @@ namespace QuestPDF.Examples .Background(fontColor) .Scale(scale) .Padding(5) - .Text($"Content with {scale} scale.", fontStyle); + .Text($"Content with {scale} scale.") + .Style(fontStyle); } }); }); @@ -555,7 +564,8 @@ namespace QuestPDF.Examples .BorderColor(Colors.Green.Darken1) .Padding(50) - .Text("Moved text", TextStyle.Default.Size(25)); + .Text("Moved text") + .Size(25); }); } @@ -591,7 +601,8 @@ namespace QuestPDF.Examples .MinimalBox() .Background(Colors.White) .Padding(10) - .Text($"Rotated {turns * 90}°", TextStyle.Default.Size(16)); + .Text($"Rotated {turns * 90}°") + .Size(16); } }); }); @@ -689,7 +700,8 @@ namespace QuestPDF.Examples .MinimalBox() .Background(Colors.White) .Padding(10) - .Text($"Flipped {turns}", TextStyle.Default.Size(16)); + .Text($"Flipped {turns}") + .Size(16); } }); }); diff --git a/QuestPDF.Examples/EnsureSpaceExample.cs b/QuestPDF.Examples/EnsureSpaceExample.cs index 0425f1b..27de8ad 100644 --- a/QuestPDF.Examples/EnsureSpaceExample.cs +++ b/QuestPDF.Examples/EnsureSpaceExample.cs @@ -23,7 +23,7 @@ namespace QuestPDF.Examples page.Size(PageSizes.A7.Landscape()); page.Background(Colors.White); - page.Header().Text("With ensure space", TextStyle.Default.SemiBold()); + page.Header().Text("With ensure space").SemiBold(); page.Content().Column(column => { diff --git a/QuestPDF.Examples/FrameExample.cs b/QuestPDF.Examples/FrameExample.cs index e8f89b8..95fee4e 100644 --- a/QuestPDF.Examples/FrameExample.cs +++ b/QuestPDF.Examples/FrameExample.cs @@ -17,9 +17,9 @@ namespace QuestPDF.Examples .Padding(5); } - public static void LabelCell(this IContainer container, string text) => container.Cell(true).Text(text, TextStyle.Default.SemiBold()); + public static void LabelCell(this IContainer container, string text) => container.Cell(true).Text(text).SemiBold(); 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 static void ValueCell(this IContainer container, string text) => container.ValueCell().Text(text); } public class FrameExample diff --git a/QuestPDF.Examples/InlinedExamples.cs b/QuestPDF.Examples/InlinedExamples.cs index fce4c85..cef4d1e 100644 --- a/QuestPDF.Examples/InlinedExamples.cs +++ b/QuestPDF.Examples/InlinedExamples.cs @@ -74,7 +74,8 @@ namespace QuestPDF.Examples .PrimaryLayer() .AlignCenter() .AlignMiddle() - .Text(sizeText, TextStyle.Default.Size(15)); + .Text(sizeText) + .Size(15); }); } }); diff --git a/QuestPDF.Examples/MinimalApiExamples.cs b/QuestPDF.Examples/MinimalApiExamples.cs index cad0c98..0a4112e 100644 --- a/QuestPDF.Examples/MinimalApiExamples.cs +++ b/QuestPDF.Examples/MinimalApiExamples.cs @@ -4,8 +4,6 @@ using QuestPDF.Examples.Engine; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; -using ShimSkiaSharp; -using SKTypeface = SkiaSharp.SKTypeface; namespace QuestPDF.Examples { @@ -19,26 +17,79 @@ namespace QuestPDF.Examples { container.Page(page => { - page.Size(PageSizes.A5); - page.Margin(1.5f, Unit.Centimetre); + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.Background(Colors.White); + page.DefaultTextStyle(TextStyle.Default.Size(20)); page.Header() - .Text("Hello PDF!", TextStyle.Default.SemiBold().Size(20)); + .Text("Hello PDF!").SemiBold().Size(36).Color(Colors.Blue.Medium); page.Content() .PaddingVertical(1, Unit.Centimetre) .Column(x => { - x.Spacing(10); + x.Spacing(20); x.Item().Text(Placeholders.LoremIpsum()); x.Item().Image(Placeholders.Image(200, 100)); }); + + page.Footer() + .AlignCenter() + .Text(x => + { + x.Span("Page "); + x.CurrentPageNumber(); + }); }); }) .GeneratePdf("hello.pdf"); Process.Start("explorer.exe", "hello.pdf"); } + + [Test] + public void MinimalApi2() + { + RenderingTest + .Create() + .ProduceImages() + .ShowResults() + .RenderDocument(container => + { + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.Background(Colors.White); + page.DefaultTextStyle(TextStyle.Default.Size(20)); + + page.Header() + .Text("Hello PDF!") + .SemiBold() + .Size(36) + .Color(Colors.Blue.Medium); + + page.Content() + .PaddingVertical(1, Unit.Centimetre) + .Column(x => + { + x.Spacing(20); + + x.Item().Text(Placeholders.LoremIpsum()); + x.Item().Image(Placeholders.Image(200, 100)); + }); + + page.Footer() + .AlignCenter() + .Text(x => + { + x.Span("Page "); + x.CurrentPageNumber(); + }); + }); + }); + } } } \ No newline at end of file diff --git a/QuestPDF.Examples/Padding.cs b/QuestPDF.Examples/Padding.cs index a544765..68f5576 100644 --- a/QuestPDF.Examples/Padding.cs +++ b/QuestPDF.Examples/Padding.cs @@ -50,7 +50,8 @@ namespace QuestPDF.Examples .Background("FFF") .Padding(5) .AlignCenter() - .Text("Sample text", TextStyle.Default.FontType("Segoe UI emoji")); + .Text("Sample text") + .FontType("Segoe UI emoji"); }); } diff --git a/QuestPDF.Examples/ShowOnceExample.cs b/QuestPDF.Examples/ShowOnceExample.cs index 8d88fe8..2fa5f29 100644 --- a/QuestPDF.Examples/ShowOnceExample.cs +++ b/QuestPDF.Examples/ShowOnceExample.cs @@ -23,7 +23,7 @@ namespace QuestPDF.Examples page.Size(PageSizes.A7.Landscape()); page.Background(Colors.White); - page.Header().Text("With show once", TextStyle.Default.SemiBold()); + page.Header().Text("With show once").SemiBold(); page.Content().PaddingVertical(5).Row(row => { diff --git a/QuestPDF.Examples/SkipOnceExample.cs b/QuestPDF.Examples/SkipOnceExample.cs index 634fee5..942e730 100644 --- a/QuestPDF.Examples/SkipOnceExample.cs +++ b/QuestPDF.Examples/SkipOnceExample.cs @@ -31,7 +31,7 @@ namespace QuestPDF.Examples page.Content() .PaddingVertical(10) - .Text(Placeholders.Paragraphs(), TextStyle.Default.Color(Colors.Grey.Medium)); + .Text(Placeholders.Paragraphs()).Color(Colors.Grey.Medium); page.Footer().Text(text => { diff --git a/QuestPDF.Examples/TextBenchmark.cs b/QuestPDF.Examples/TextBenchmark.cs index 47645a3..5470664 100644 --- a/QuestPDF.Examples/TextBenchmark.cs +++ b/QuestPDF.Examples/TextBenchmark.cs @@ -141,8 +141,8 @@ namespace QuestPDF.Examples .AlignBottom() .Column(column => { - column.Item().Text("Quo Vadis", TextStyle.Default.Size(72).Bold().Color(Colors.Blue.Darken2)); - column.Item().Text("Henryk Sienkiewicz", TextStyle.Default.Size(24).Color(Colors.Grey.Darken2)); + column.Item().Text("Quo Vadis").Size(72).Bold().Color(Colors.Blue.Darken2); + column.Item().Text("Henryk Sienkiewicz").Size(24).Color(Colors.Grey.Darken2); }); } @@ -156,8 +156,8 @@ namespace QuestPDF.Examples { column.Item().InternalLink(chapter.Title).Row(row => { - row.RelativeItem().Text(chapter.Title, normalStyle); - row.ConstantItem(100).AlignRight().Text(text => text.PageNumberOfLocation(chapter.Title, normalStyle)); + row.RelativeItem().Text(chapter.Title).Style(normalStyle); + row.ConstantItem(100).AlignRight().Text(text => text.BeginPageNumberOfSection(chapter.Title).Style(normalStyle)); }); } }); @@ -180,7 +180,7 @@ namespace QuestPDF.Examples column.Item().Text(text => { text.ParagraphSpacing(5); - text.Span(content, normalStyle); + text.Span(content).Style(normalStyle); }); column.Item().PageBreak(); @@ -198,7 +198,7 @@ namespace QuestPDF.Examples text.DefaultTextStyle(normalStyle); text.Span("Ten dokument został wygenerowany na podstawie książki w formacie TXT opublikowanej w serwisie "); - text.ExternalLocation("wolnelektury.pl", "https://wolnelektury.pl/", normalStyle.Color(Colors.Blue.Medium).Underline()); + text.Hyperlink("wolnelektury.pl", "https://wolnelektury.pl/").Color(Colors.Blue.Medium).Underline(); text.Span(". Dziękuję za wspieranie polskiego czytelnictwa!"); }); }); @@ -206,7 +206,7 @@ namespace QuestPDF.Examples void SectionTitle(ColumnDescriptor column, string text) { - column.Item().Location(text).Text(text, subtitleStyle); + column.Item().Location(text).Text(text).Style(subtitleStyle); column.Item().PaddingTop(10).PaddingBottom(50).BorderBottom(1).BorderColor(Colors.Grey.Lighten2).ExtendHorizontal(); } diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index ef12e96..347996f 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -31,7 +31,7 @@ namespace QuestPDF.Examples { text.DefaultTextStyle(TextStyle.Default.Size(20)); text.Span("This is a normal text, followed by an "); - text.Span("underlined red text.", TextStyle.Default.Size(20).Color(Colors.Red.Medium).Underline()); + text.Span("underlined red text.").Size(20).Color(Colors.Red.Medium).Underline(); }); }); } @@ -57,7 +57,7 @@ namespace QuestPDF.Examples foreach (var i in Enumerable.Range(1, 3)) { - text.Span($"Paragraph {i}: ", TextStyle.Default.SemiBold()); + text.Span($"Paragraph {i}: ").SemiBold(); text.Line(Placeholders.Paragraph()); } }); @@ -120,7 +120,7 @@ namespace QuestPDF.Examples text.Line(Placeholders.LoremIpsum()); - text.Span($"This is target text that should show up. {DateTime.UtcNow:T} > This is a short sentence that will be wrapped into second line hopefully, right? <", TextStyle.Default.Underline()); + text.Span($"This is target text that should show up. {DateTime.UtcNow:T} > This is a short sentence that will be wrapped into second line hopefully, right? <").Underline(); }); }); } @@ -173,6 +173,8 @@ namespace QuestPDF.Examples .Padding(10) .Text(text => { + text.DefaultTextStyle(x => x.Bold()); + text.DefaultTextStyle(TextStyle.Default); text.AlignLeft(); text.ParagraphSpacing(10); @@ -182,9 +184,9 @@ namespace QuestPDF.Examples text.EmptyLine(); text.Span("This text is a normal text, "); - text.Span("this is a bold text, ", TextStyle.Default.Bold()); - text.Span("this is a red and underlined text, ", TextStyle.Default.Color(Colors.Red.Medium).Underline()); - text.Span("and this is slightly bigger text.", TextStyle.Default.Size(16)); + text.Span("this is a bold text, ").Bold(); + text.Span("this is a red and underlined text, ").Color(Colors.Red.Medium).Underline(); + text.Span("and this is slightly bigger text.").Size(16); text.EmptyLine(); @@ -210,7 +212,7 @@ namespace QuestPDF.Examples text.EmptyLine(); - text.Span(Placeholders.Paragraphs(), TextStyle.Default.Italic()); + text.Span(Placeholders.Paragraphs()).Italic(); text.Line("This is target text that does not show up. " + Placeholders.Paragraph()); }); @@ -241,9 +243,9 @@ namespace QuestPDF.Examples text.ParagraphSpacing(10); text.Span("This text is a normal text, "); - text.Span("this is a bold text, ", TextStyle.Default.Bold()); - text.Span("this is a red and underlined text, ", TextStyle.Default.Color(Colors.Red.Medium).Underline()); - text.Span("and this is slightly bigger text.", TextStyle.Default.Size(16)); + text.Span("this is a bold text, ").Bold(); + text.Span("this is a red and underlined text, ").Color(Colors.Red.Medium).Underline(); + text.Span("and this is slightly bigger text.").Size(16); text.Span("The new text element also supports injecting custom content between words: "); text.Element().PaddingBottom(-10).Height(16).Width(32).Image(Placeholders.Image); diff --git a/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs b/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs index e67fb58..a70c3bb 100644 --- a/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs +++ b/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs @@ -30,12 +30,12 @@ namespace QuestPDF.ReportSample.Layouts { column.Item().ShowOnce().Padding(5).AlignMiddle().Row(row => { - row.RelativeItem(2).AlignMiddle().Text("PRIMARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold()); + row.RelativeItem(2).AlignMiddle().Text("PRIMARY HEADER").Color(Colors.Grey.Darken3).Size(30).Bold(); row.RelativeItem(1).AlignRight().MinimalBox().AlignMiddle().Background(Colors.Blue.Darken2).Padding(30); }); column.Item().SkipOnce().Padding(5).Row(row => { - row.RelativeItem(2).Text("SECONDARY HEADER", TextStyle.Default.Color(Colors.Grey.Darken3).Size(30).Bold()); + row.RelativeItem(2).Text("SECONDARY HEADER").Color(Colors.Grey.Darken3).Size(30).Bold(); row.RelativeItem(1).AlignRight().MinimalBox().Background(Colors.Blue.Lighten4).Padding(15); }); }); diff --git a/QuestPDF.ReportSample/Layouts/SectionTemplate.cs b/QuestPDF.ReportSample/Layouts/SectionTemplate.cs index 2e37ec3..83c55cf 100644 --- a/QuestPDF.ReportSample/Layouts/SectionTemplate.cs +++ b/QuestPDF.ReportSample/Layouts/SectionTemplate.cs @@ -23,7 +23,8 @@ namespace QuestPDF.ReportSample.Layouts decoration .Before() .PaddingBottom(5) - .Text(Model.Title, Typography.Headline); + .Text(Model.Title) + .Style(Typography.Headline); decoration.Content().Border(0.75f).BorderColor(Colors.Grey.Medium).Column(column => { @@ -69,7 +70,7 @@ namespace QuestPDF.ReportSample.Layouts { if (model.PhotoCount == 0) { - container.Text("No photos", Typography.Normal); + container.Text("No photos").Style(Typography.Normal); return; } diff --git a/QuestPDF.ReportSample/Layouts/StandardReport.cs b/QuestPDF.ReportSample/Layouts/StandardReport.cs index 143d5c9..87adf0f 100644 --- a/QuestPDF.ReportSample/Layouts/StandardReport.cs +++ b/QuestPDF.ReportSample/Layouts/StandardReport.cs @@ -39,9 +39,9 @@ namespace QuestPDF.ReportSample.Layouts page.Footer().AlignCenter().Text(text => { - text.CurrentPageNumber(format: x => x?.FormatAsRomanNumeral() ?? "-----"); + text.CurrentPageNumber().Format(x => x?.FormatAsRomanNumeral() ?? "-----"); text.Span(" / "); - text.TotalPages(format: x => x?.FormatAsRomanNumeral() ?? "-----"); + text.TotalPages().Format(x => x?.FormatAsRomanNumeral() ?? "-----"); }); }); } @@ -54,7 +54,7 @@ namespace QuestPDF.ReportSample.Layouts { row.Spacing(50); - row.RelativeItem().PaddingTop(-10).Text(Model.Title, Typography.Title); + row.RelativeItem().PaddingTop(-10).Text(Model.Title).Style(Typography.Title); row.ConstantItem(90).ExternalLink("https://www.questpdf.com").MaxHeight(30).Component(); }); @@ -69,7 +69,7 @@ namespace QuestPDF.ReportSample.Layouts { grid.Item().Text(text => { - text.Span($"{field.Label}: ", TextStyle.Default.SemiBold()); + text.Span($"{field.Label}: ").SemiBold(); text.Span(field.Value); }); } diff --git a/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs b/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs index 62c02e3..8788edd 100644 --- a/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs +++ b/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs @@ -23,7 +23,8 @@ namespace QuestPDF.ReportSample.Layouts decoration .Before() .PaddingBottom(5) - .Text("Table of contents", Typography.Headline); + .Text("Table of contents") + .Style(Typography.Headline); decoration.Content().Column(column => { @@ -53,9 +54,9 @@ namespace QuestPDF.ReportSample.Layouts var lengthStyle = TextStyle.Default.Color(Colors.Grey.Medium); - text.Span(" (", lengthStyle); - text.TotalPagesWithinSection(locationName, lengthStyle, x => x == 1 ? "1 page long" : $"{x} pages long"); - text.Span(")", lengthStyle); + text.Span(" (").Style(lengthStyle); + text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x => x == 1 ? "1 page long" : $"{x} pages long"); + text.Span(")").Style(lengthStyle); }); }); } diff --git a/QuestPDF/Elements/DebugArea.cs b/QuestPDF/Elements/DebugArea.cs index cd93da2..be9b2a5 100644 --- a/QuestPDF/Elements/DebugArea.cs +++ b/QuestPDF/Elements/DebugArea.cs @@ -30,7 +30,10 @@ namespace QuestPDF.Elements .MinimalBox() .Background(Colors.White) .Padding(2) - .Text(Text, TextStyle.Default.Color(Color).FontType(Fonts.Consolas).Size(8)); + .Text(Text) + .Color(Color) + .FontType(Fonts.Consolas) + .Size(8); }); } } diff --git a/QuestPDF/Elements/Placeholder.cs b/QuestPDF/Elements/Placeholder.cs index cf33087..943ecc0 100644 --- a/QuestPDF/Elements/Placeholder.cs +++ b/QuestPDF/Elements/Placeholder.cs @@ -26,7 +26,7 @@ namespace QuestPDF.Elements if (string.IsNullOrWhiteSpace(Text)) x.MaxHeight(32).Image(ImageData, ImageScaling.FitArea); else - x.Text(Text, TextStyle.Default.Size(14)); + x.Text(Text).Size(14); }); } } diff --git a/QuestPDF/Fluent/TextExtensions.cs b/QuestPDF/Fluent/TextExtensions.cs index e069871..4e510d5 100644 --- a/QuestPDF/Fluent/TextExtensions.cs +++ b/QuestPDF/Fluent/TextExtensions.cs @@ -10,6 +10,34 @@ using static System.String; namespace QuestPDF.Fluent { + public class TextSpanDescriptor + { + internal TextStyle TextStyle { get; } + + internal TextSpanDescriptor(TextStyle textStyle) + { + TextStyle = textStyle; + } + } + + public delegate string PageNumberFormatter(int? pageNumber); + + public class TextPageNumberDescriptor : TextSpanDescriptor + { + internal PageNumberFormatter FormatFunction { get; private set; } = x => (x ?? 123).ToString(); + + internal TextPageNumberDescriptor(TextStyle textStyle) : base(textStyle) + { + + } + + public TextPageNumberDescriptor Format(PageNumberFormatter formatter) + { + FormatFunction = formatter ?? FormatFunction; + return this; + } + } + public class TextDescriptor { private ICollection TextBlocks { get; } = new List(); @@ -22,6 +50,11 @@ namespace QuestPDF.Fluent DefaultStyle = style; } + public void DefaultTextStyle(Func style) + { + DefaultStyle = style(TextStyle.Default); + } + public void AlignLeft() { Alignment = HorizontalAlignment.Left; @@ -50,12 +83,9 @@ namespace QuestPDF.Fluent TextBlocks.Last().Items.Add(item); } - public void Span(string? text, TextStyle? style = null) + public TextSpanDescriptor Span(string? text) { - if (IsNullOrEmpty(text)) - return; - - style ??= TextStyle.Default; + var style = DefaultStyle.Clone(); var items = text .Replace("\r", string.Empty) @@ -77,78 +107,81 @@ namespace QuestPDF.Fluent }) .ToList() .ForEach(TextBlocks.Add); + + return new TextSpanDescriptor(style); } - public void Line(string? text, TextStyle? style = null) + public TextSpanDescriptor Line(string? text) { text ??= string.Empty; - Span(text + Environment.NewLine, style); + return Span(text + Environment.NewLine); } - public void EmptyLine() + public TextSpanDescriptor EmptyLine() { - Span(Environment.NewLine); + return Span(Environment.NewLine); } - - private static Func DefaultTextFormat = x => (x ?? 123).ToString(); - private void PageNumber(Func pageNumber, TextStyle? style, Func? format) + private TextPageNumberDescriptor PageNumber(Func pageNumber) { - style ??= TextStyle.Default; - format ??= DefaultTextFormat; + var style = DefaultStyle.Clone(); + var descriptor = new TextPageNumberDescriptor(DefaultStyle); - AddItemToLastTextBlock(new TextBlockPageNumber() + AddItemToLastTextBlock(new TextBlockPageNumber { - Source = context => format(pageNumber(context)), + Source = context => descriptor.FormatFunction(pageNumber(context)), Style = style }); + + return descriptor; } - public void CurrentPageNumber(TextStyle? style = null, Func? format = null) + public TextPageNumberDescriptor CurrentPageNumber() { - PageNumber(x => x.CurrentPage, style, format); + return PageNumber(x => x.CurrentPage); } - public void TotalPages(TextStyle? style = null, Func? format = null) + public TextPageNumberDescriptor TotalPages() { - PageNumber(x => x.GetLocation(PageContext.DocumentLocation)?.Length, style, format); + return PageNumber(x => x.GetLocation(PageContext.DocumentLocation)?.Length); } [Obsolete("This element has been renamed since version 2022.3. Please use the BeginPageNumberOfSection method.")] public void PageNumberOfLocation(string locationName, TextStyle? style = null) { - BeginPageNumberOfSection(locationName); + BeginPageNumberOfSection(locationName).Style(style); } - public void BeginPageNumberOfSection(string locationName, TextStyle? style = null, Func? format = null) + public TextPageNumberDescriptor BeginPageNumberOfSection(string locationName) { - PageNumber(x => x.GetLocation(locationName)?.PageStart, style, format); + return PageNumber(x => x.GetLocation(locationName)?.PageStart); } - public void EndPageNumberOfSection(string locationName, TextStyle? style = null, Func? format = null) + public TextPageNumberDescriptor EndPageNumberOfSection(string locationName) { - PageNumber(x => x.GetLocation(locationName)?.PageEnd, style, format); + return PageNumber(x => x.GetLocation(locationName)?.PageEnd); } - public void PageNumberWithinSection(string locationName, TextStyle? style = null, Func? format = null) + public TextPageNumberDescriptor PageNumberWithinSection(string locationName) { - PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd, style, format); + return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd); } - public void TotalPagesWithinSection(string locationName, TextStyle? style = null, Func? format = null) + public TextPageNumberDescriptor TotalPagesWithinSection(string locationName) { - PageNumber(x => x.GetLocation(locationName)?.Length, style, format); + return PageNumber(x => x.GetLocation(locationName)?.Length); } - public void SectionLink(string? text, string sectionName, TextStyle? style = null) + public TextSpanDescriptor SectionLink(string? text, string sectionName) { - if (IsNullOrEmpty(text)) - return; - if (IsNullOrEmpty(sectionName)) throw new ArgumentException(nameof(sectionName)); - style ??= TextStyle.Default; + var style = DefaultStyle.Clone(); + var descriptor = new TextSpanDescriptor(style); + + if (IsNullOrEmpty(text)) + return descriptor; AddItemToLastTextBlock(new TextBlockSectionlLink { @@ -156,23 +189,26 @@ namespace QuestPDF.Fluent Text = text, SectionName = sectionName }); + + return descriptor; } [Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")] public void InternalLocation(string? text, string locationName, TextStyle? style = null) { - SectionLink(text, locationName, style); + SectionLink(text, locationName).Style(style); } - public void Hyperlink(string? text, string url, TextStyle? style = null) + public TextSpanDescriptor Hyperlink(string? text, string url) { - if (IsNullOrEmpty(text)) - return; - if (IsNullOrEmpty(url)) throw new ArgumentException(nameof(url)); - - style ??= TextStyle.Default; + + var style = DefaultStyle.Clone(); + var descriptor = new TextSpanDescriptor(style); + + if (IsNullOrEmpty(text)) + return descriptor; AddItemToLastTextBlock(new TextBlockHyperlink { @@ -180,12 +216,14 @@ namespace QuestPDF.Fluent Text = text, Url = url }); + + return descriptor; } [Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")] public void ExternalLocation(string? text, string url, TextStyle? style = null) { - Hyperlink(text, url, style); + Hyperlink(text, url).Style(style); } public IContainer Element() @@ -227,9 +265,17 @@ namespace QuestPDF.Fluent descriptor.Compose(element); } - public static void Text(this IContainer element, object? text, TextStyle? style = null) + //[Obsolete("This element has been renamed since version 2022.3. Please use the ")] + // public static void Text(this IContainer element, object? text, TextStyle style) + // { + // element.Text(text).Style(style); + // } + + public static TextSpanDescriptor Text(this IContainer element, object? text) { - element.Text(x => x.Span(text?.ToString(), style)); + var descriptor = (TextSpanDescriptor) null; + element.Text(x => descriptor = x.Span(text?.ToString())); + return descriptor; } } } \ No newline at end of file diff --git a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs new file mode 100644 index 0000000..f04e207 --- /dev/null +++ b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs @@ -0,0 +1,126 @@ +using System; +using System.Runtime.CompilerServices; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public static class TextSpanDescriptorExtensions + { + public static T Style(this T descriptor, TextStyle style) where T : TextSpanDescriptor + { + if (style == null) + return descriptor; + + descriptor.TextStyle.OverrideStyle(style); + return descriptor; + } + + public static T Color(this T descriptor, string value) where T : TextSpanDescriptor + { + descriptor.TextStyle.Color = value; + return descriptor; + } + + public static T BackgroundColor(this T descriptor, string value) where T : TextSpanDescriptor + { + descriptor.TextStyle.BackgroundColor = value; + return descriptor; + } + + public static T FontType(this T descriptor, string value) where T : TextSpanDescriptor + { + descriptor.TextStyle.FontType = value; + return descriptor; + } + + public static T Size(this T descriptor, float value) where T : TextSpanDescriptor + { + descriptor.TextStyle.Size = value; + return descriptor; + } + + public static T LineHeight(this T descriptor, float value) where T : TextSpanDescriptor + { + descriptor.TextStyle.LineHeight = value; + return descriptor; + } + + public static T Italic(this T descriptor, bool value = true) where T : TextSpanDescriptor + { + descriptor.TextStyle.IsItalic = value; + return descriptor; + } + + public static T Strikethrough(this T descriptor, bool value = true) where T : TextSpanDescriptor + { + descriptor.TextStyle.HasStrikethrough = value; + return descriptor; + } + + public static T Underline(this T descriptor, bool value = true) where T : TextSpanDescriptor + { + descriptor.TextStyle.HasUnderline = value; + return descriptor; + } + + #region Weight + + public static T Weight(this T descriptor, FontWeight weight) where T : TextSpanDescriptor + { + descriptor.TextStyle.FontWeight = weight; + return descriptor; + } + + public static T Thin(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.Thin); + } + + public static T ExtraLight(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.ExtraLight); + } + + public static T Light(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.Light); + } + + public static T NormalWeight(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.Normal); + } + + public static T Medium(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.Medium); + } + + public static T SemiBold(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.SemiBold); + } + + public static T Bold(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.Bold); + } + + public static T ExtraBold(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.ExtraBold); + } + + public static T Black(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.Black); + } + + public static T ExtraBlack(this T descriptor) where T : TextSpanDescriptor + { + return descriptor.Weight(FontWeight.ExtraBlack); + } + + #endregion + } +} \ No newline at end of file diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index 552f54f..13dde4e 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -60,6 +60,19 @@ namespace QuestPDF.Infrastructure HasUnderline ??= parentStyle.HasUnderline; } + internal void OverrideStyle(TextStyle parentStyle) + { + Color = parentStyle.Color ?? Color; + BackgroundColor = parentStyle.BackgroundColor ?? BackgroundColor; + FontType = parentStyle.FontType ?? FontType; + Size = parentStyle.Size ?? Size; + LineHeight = parentStyle.LineHeight ?? LineHeight; + FontWeight = parentStyle.FontWeight ?? FontWeight; + IsItalic = parentStyle.IsItalic ?? IsItalic; + HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough; + HasUnderline = parentStyle.HasUnderline ?? HasUnderline; + } + internal TextStyle Clone() { var clone = (TextStyle)MemberwiseClone(); diff --git a/readme.md b/readme.md index 187a195..494e112 100644 --- a/readme.md +++ b/readme.md @@ -8,215 +8,84 @@ I have designed this layouting engine with full paging support in mind. The docu ## Support QuestPDF -All great frameworks and libraries started from zero. Please help me make QuestPDF a commonly known library and an obvious choice in case of generating PDF documents. Please give it a start ⭐ and share with your colleagues 💬👨‍💻. +All great frameworks and libraries started from zero. Please help me make QuestPDF a commonly known library and an obvious choice for generating PDF documents. + +- ⭐ Give this repository a star, +- 💬 Share it with your team members. ## Installation The library is available as a nuget package. You can install it as any other nuget package from your IDE, try to search by `QuestPDF`. You can find package details [on this webpage](https://www.nuget.org/packages/QuestPDF/). -``` +```c# +// Package Manager Install-Package QuestPDF + +// .NET CLI +dotnet add package QuestPDF + +// Package reference in .csproj file + ``` ## 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. -**[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. -**[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 create reusable code that is easy to maintain. -**[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. +## Simplicity is the key -## Example invoice - -Do you believe that creating a complete invoice document can take less than 200 lines of code? We have prepared for you a step-by-step instruction that shows every detail of this implementation and describes the best patterns and practices. - -For tutorial, documentation and API reference, please visit [the QuestPDF documentation](https://www.questpdf.com/documentation/getting-started.html). - - - - - -Here you can find an example code showing how easy is to write and understand the fluent API. - -**General document structure** with header, content and footer: +How easy it is to start and prototype with QuestPDF? Really easy thanks to its minimal API! Please analyse the code below: ```csharp -public void Compose(IDocumentContainer container) +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +// code in your main method +Document.Create(container => { - container - .Page(page => - { - page.Margin(50); - - page.Header().Element(ComposeHeader); - page.Content().Element(ComposeContent); - - page.Footer().AlignCenter().Text(x => + container.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(2, Unit.Centimetre); + page.Background(Colors.White); + page.DefaultTextStyle(TextStyle.Default.Size(20)); + + page.Header() + .Text("Hello PDF!", TextStyle.Default.SemiBold().Size(36).Color(Colors.Blue.Medium)); + + page.Content() + .PaddingVertical(1, Unit.Centimetre) + .Column(x => { + x.Spacing(20); + + x.Item().Text(Placeholders.LoremIpsum()); + x.Item().Image(Placeholders.Image(200, 100)); + }); + + page.Footer() + .AlignCenter() + .Text(x => + { + x.Span("Page "); x.CurrentPageNumber(); - x.Span(" / "); - x.TotalPages(); }); - }); -} -``` - -**The header area** consists of basic invoice information along with a logo placeholder. - -```csharp -void ComposeHeader(IContainer container) -{ - var titleTextStyle = TextStyle.Default.Size(20).SemiBold().Color(Colors.Blue.Medium); - - container.Row(row => - { - row.RelativeColumn().Stack(stack => - { - stack.Item().Text($"Invoice #{Model.InvoiceNumber}", titleStyle); - - stack.Item().Text(text => - { - text.Span("Issue date: ", TextStyle.Default.SemiBold()); - text.Span($"{Model.IssueDate:d}"); - }); - - stack.Item().Text(text => - { - text.Span("Due date: ", TextStyle.Default.SemiBold()); - text.Span($"{Model.DueDate:d}"); - }); - }); - - row.ConstantColumn(100).Height(50).Placeholder(); }); -} +}) +.GeneratePdf("hello.pdf"); ``` -Implementation of **the content area** that contains seller and customer details, then listing of all bought products, then a comments section. +And compare it to the produced PDF file: -```csharp -void ComposeContent(IContainer container) -{ - container.PaddingVertical(40).Stack(column => - { - column.Spacing(20); - - column.Item().Row(row => - { - row.RelativeColumn().Component(new AddressComponent("From", Model.SellerAddress)); - row.ConstantColumn(50); - row.RelativeColumn().Component(new AddressComponent("For", Model.CustomerAddress)); - }); - column.Item().Element(ComposeTable); - var totalPrice = Model.Items.Sum(x => x.Price * x.Quantity); - - column - .Item() - .PaddingRight(5) - .AlignRight() - .Text($"Grand total: {totalPrice}$", TextStyle.Default.SemiBold()); +## Are you ready for more? - if (!string.IsNullOrWhiteSpace(Model.Comments)) - column.Item().PaddingTop(25).Element(ComposeComments); - }); -} -``` +The Fluent API of QuestPDF scales really well. It is easy to create and maintain even most complex documents. Read [the Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html) to learn QuestPDF basics and implement an invoice under 200 lines of code. You can also investigate and play with the code from [the example repository](https://github.com/QuestPDF/example-invoice). -**The table and comments** codes are extracted into separate methods to increase clarity: - -```csharp -void ComposeTable(IContainer container) -{ - var headerStyle = TextStyle.Default.SemiBold(); - - container.Table(table => - { - table.ColumnsDefinition(columns => - { - columns.ConstantColumn(25); - columns.RelativeColumn(3); - columns.RelativeColumn(); - columns.RelativeColumn(); - columns.RelativeColumn(); - }); - - table.Header(header => - { - header.Cell().Text("#", headerStyle); - header.Cell().Text("Product", headerStyle); - header.Cell().AlignRight().Text("Unit price", headerStyle); - header.Cell().AlignRight().Text("Quantity", headerStyle); - header.Cell().AlignRight().Text("Total", headerStyle); - - header.Cell().ColumnSpan(5) - .PaddingVertical(5).BorderBottom(1).BorderColor(Colors.Black); - }); - - foreach (var item in Model.Items) - { - table.Cell().Element(CellStyle).Text(Model.Items.IndexOf(item) + 1); - table.Cell().Element(CellStyle).Text(item.Name); - table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price}$"); - table.Cell().Element(CellStyle).AlignRight().Text(item.Quantity); - table.Cell().Element(CellStyle).AlignRight().Text($"{item.Price * item.Quantity}$"); - - static IContainer CellStyle(IContainer container) - { - container.BorderBottom(1).BorderColor(Colors.Grey.Lighten2).PaddingVertical(5); - } - } - }); -} -``` - -```csharp -void ComposeComments(IContainer container) -{ - container.ShowEntire().Background(Colors.Grey.Lighten3).Padding(10).Stack(message => - { - message.Spacing(5); - message.Item().Text("Comments", TextStyle.Default.Size(14).SemiBold()); - message.Item().Text(Model.Comments); - }); -} -``` - -**The address details section** is implemented using components. This way the code can be easily reused for both seller and customer: - -```csharp -public class AddressComponent : IComponent -{ - private string Title { get; } - private Address Address { get; } - - public AddressComponent(string title, Address address) - { - Title = title; - Address = address; - } - - public void Compose(IContainer container) - { - container.ShowEntire().Stack(column => - { - column.Spacing(5); - - column - .Item() - .BorderBottom(1) - .PaddingBottom(5) - .Text(Title, TextStyle.Default.SemiBold()); - - column.Item().Text(Address.CompanyName); - column.Item().Text(Address.Street); - column.Item().Text($"{Address.City}, {Address.State}"); - column.Item().Text(Address.Email); - column.Item().Text(Address.Phone); - }); - } -} -``` + \ No newline at end of file