diff --git a/QuestPDF.Examples/BarcodeExamples.cs b/QuestPDF.Examples/BarcodeExamples.cs index 0ce3beb..ad43734 100644 --- a/QuestPDF.Examples/BarcodeExamples.cs +++ b/QuestPDF.Examples/BarcodeExamples.cs @@ -13,11 +13,11 @@ namespace QuestPDF.Examples [Test] public void Example() { - FontManager.RegisterFontType("LibreBarcode39", File.OpenRead("LibreBarcode39-Regular.ttf")); + FontManager.RegisterFont(File.OpenRead("LibreBarcode39-Regular.ttf")); RenderingTest .Create() - .PageSize(400, 100) + .PageSize(400, 200) .ShowResults() .Render(container => { @@ -25,7 +25,9 @@ namespace QuestPDF.Examples .Background(Colors.White) .AlignCenter() .AlignMiddle() - .Text("*QuestPDF*", TextStyle.Default.FontType("LibreBarcode39").Size(64)); + .Text("*QuestPDF*") + .FontFamily("Libre Barcode 39") + .FontSize(64); }); } } diff --git a/QuestPDF.Examples/CanvasExamples.cs b/QuestPDF.Examples/CanvasExamples.cs index a37d41f..cb9bd2e 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") + .FontSize(16) + .FontColor(Colors.Blue.Darken2) + .SemiBold(); }); }); } diff --git a/QuestPDF.Examples/ChartExamples.cs b/QuestPDF.Examples/ChartExamples.cs index a8eac35..beae613 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") + .FontSize(20) + .SemiBold() + .FontColor(Colors.Blue.Medium); column .Item() diff --git a/QuestPDF.Examples/ContinousPage.cs b/QuestPDF.Examples/ContinousPage.cs index 1e5c545..eb73ed9 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}").FontColor(Placeholders.Color()); }); page.Footer().Text("Footer"); diff --git a/QuestPDF.Examples/DefaultTextStyleExample.cs b/QuestPDF.Examples/DefaultTextStyleExample.cs index e7db9eb..9efce3f 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()).FontColor(Colors.Red.Medium); }); }); }); diff --git a/QuestPDF.Examples/DefaultTextStyleExamples.cs b/QuestPDF.Examples/DefaultTextStyleExamples.cs index 4165a39..c02536b 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).FontColor(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) + .FontSize(16 + i / 4); } }); }); diff --git a/QuestPDF.Examples/ElementExamples.cs b/QuestPDF.Examples/ElementExamples.cs index 635961c..45d67ea 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") + .FontSize(16) + .FontColor("#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") + .FontSize(48) + .Bold() + .FontColor(Colors.Green.Lighten3); layers .Layer() .AlignBottom() - .Text(text => text.CurrentPageNumber(TextStyle.Default.Size(16).Color(Colors.Green.Medium))); + .Text(text => text.CurrentPageNumber().FontSize(16).FontColor(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) + .FontFamily(font) + .FontSize(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!").FontSize(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").FontSize(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") + .FontSize(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}°") + .FontSize(16); } }); }); @@ -689,7 +700,8 @@ namespace QuestPDF.Examples .MinimalBox() .Background(Colors.White) .Padding(10) - .Text($"Flipped {turns}", TextStyle.Default.Size(16)); + .Text($"Flipped {turns}") + .FontSize(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..8f4adda 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) + .FontSize(15); }); } }); diff --git a/QuestPDF.Examples/MinimalApiExamples.cs b/QuestPDF.Examples/MinimalApiExamples.cs new file mode 100644 index 0000000..4d7e3ba --- /dev/null +++ b/QuestPDF.Examples/MinimalApiExamples.cs @@ -0,0 +1,95 @@ +using System.Diagnostics; +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class MinimalApiExamples + { + [Test] + public void MinimalApi() + { + Document + .Create(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().FontSize(36).FontColor(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(); + }); + }); + }) + .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() + .FontSize(36) + .FontColor(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..d8dddcd 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") + .FontFamily("Segoe UI emoji"); }); } diff --git a/QuestPDF.Examples/PageBackgroundForeground.cs b/QuestPDF.Examples/PageBackgroundForeground.cs new file mode 100644 index 0000000..23a3fd0 --- /dev/null +++ b/QuestPDF.Examples/PageBackgroundForeground.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using NUnit.Framework; +using QuestPDF.Examples.Engine; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + public class PageBackgroundForeground + { + [Test] + public void Test() + { + RenderingTest + .Create() + .PageSize(550, 400) + .ProducePdf() + .ShowResults() + .RenderDocument(document => + { + document.Page(page => + { + page.Size(PageSizes.A4); + page.Margin(1, Unit.Inch); + page.DefaultTextStyle(TextStyle.Default.FontSize(16)); + + page.Foreground() + .AlignMiddle() + .AlignCenter() + .Text("Watermark") + .FontSize(64) + .FontColor(Colors.Blue.Lighten3); + + page.Header().Text("Background and foreground").Bold().FontColor(Colors.Blue.Medium).FontSize(24); + + page.Content().PaddingVertical(25).Column(column => + { + column.Spacing(25); + + foreach (var i in Enumerable.Range(0, 100)) + column.Item().Background(Colors.Grey.Lighten2).Height(75); + }); + }); + }); + } + } +} \ No newline at end of file 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..182a65e 100644 --- a/QuestPDF.Examples/SkipOnceExample.cs +++ b/QuestPDF.Examples/SkipOnceExample.cs @@ -31,7 +31,8 @@ namespace QuestPDF.Examples page.Content() .PaddingVertical(10) - .Text(Placeholders.Paragraphs(), TextStyle.Default.Color(Colors.Grey.Medium)); + .Text(Placeholders.Paragraphs()) + .FontColor(Colors.Grey.Medium); page.Footer().Text(text => { diff --git a/QuestPDF.Examples/TextBenchmark.cs b/QuestPDF.Examples/TextBenchmark.cs index 607fab5..fb4c105 100644 --- a/QuestPDF.Examples/TextBenchmark.cs +++ b/QuestPDF.Examples/TextBenchmark.cs @@ -134,8 +134,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").FontSize(72).Bold().FontColor(Colors.Blue.Darken2); + column.Item().Text("Henryk Sienkiewicz").FontSize(24).FontColor(Colors.Grey.Darken2); }); } @@ -149,8 +149,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)); }); } }); @@ -173,7 +173,7 @@ namespace QuestPDF.Examples column.Item().Text(text => { text.ParagraphSpacing(5); - text.Span(content, normalStyle); + text.Span(content).Style(normalStyle); }); column.Item().PageBreak(); @@ -191,7 +191,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/").FontColor(Colors.Blue.Medium).Underline(); text.Span(". Dziękuję za wspieranie polskiego czytelnictwa!"); }); }); @@ -199,7 +199,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 027e320..998c111 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -29,9 +29,9 @@ namespace QuestPDF.Examples .Padding(10) .Text(text => { - text.DefaultTextStyle(TextStyle.Default.Size(20)); + text.DefaultTextStyle(TextStyle.Default.FontSize(20)); text.Span("This is a normal text, followed by an "); - text.Span("underlined red text", TextStyle.Default.Color(Colors.Red.Medium).Underline()); + text.Span("underlined red text").FontColor(Colors.Red.Medium).Underline(); text.Span("."); }); }); @@ -58,7 +58,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()); } }); @@ -121,7 +121,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(); }); }); } @@ -174,6 +174,8 @@ namespace QuestPDF.Examples .Padding(10) .Text(text => { + text.DefaultTextStyle(x => x.Bold()); + text.DefaultTextStyle(TextStyle.Default); text.AlignLeft(); text.ParagraphSpacing(10); @@ -183,9 +185,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, ").FontColor(Colors.Red.Medium).Underline(); + text.Span("and this is slightly bigger text.").FontSize(16); text.EmptyLine(); @@ -211,7 +213,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()); }); @@ -242,9 +244,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, ").FontColor(Colors.Red.Medium).Underline(); + text.Span("and this is slightly bigger text.").FontSize(16); text.Span("The new text element also supports injecting custom content between words: "); text.Element().PaddingBottom(-4).Height(16).Width(32).Image(Placeholders.Image); @@ -314,7 +316,7 @@ namespace QuestPDF.Examples page.Content().Text( " ", - TextStyle.Default.Size(11).BackgroundColor(Colors.Red.Lighten3)); + TextStyle.Default.FontSize(11).BackgroundColor(Colors.Red.Lighten3)); }); }); } @@ -339,7 +341,7 @@ namespace QuestPDF.Examples page.Content().Text( " x ", - TextStyle.Default.Size(11).BackgroundColor(Colors.Red.Lighten3)); + TextStyle.Default.FontSize(11).BackgroundColor(Colors.Red.Lighten3)); }); }); } diff --git a/QuestPDF.ReportSample/Helpers.cs b/QuestPDF.ReportSample/Helpers.cs index cb9af62..572284f 100644 --- a/QuestPDF.ReportSample/Helpers.cs +++ b/QuestPDF.ReportSample/Helpers.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.IO; using SkiaSharp; @@ -24,5 +25,32 @@ namespace QuestPDF.ReportSample Latitude = Helpers.Random.NextDouble() * 180f - 90f }; } + + private static readonly ConcurrentDictionary RomanNumeralCache = new ConcurrentDictionary(); + + public static string FormatAsRomanNumeral(this int number) + { + if (number < 0 || number > 3999) + throw new ArgumentOutOfRangeException("Number should be in range from 1 to 3999"); + + return RomanNumeralCache.GetOrAdd(number, x => + { + if (x >= 1000) return "M" + FormatAsRomanNumeral(x - 1000); + if (x >= 900) return "CM" + FormatAsRomanNumeral(x - 900); + if (x >= 500) return "D" + FormatAsRomanNumeral(x - 500); + if (x >= 400) return "CD" + FormatAsRomanNumeral(x - 400); + if (x >= 100) return "C" + FormatAsRomanNumeral(x - 100); + if (x >= 90) return "XC" + FormatAsRomanNumeral(x - 90); + if (x >= 50) return "L" + FormatAsRomanNumeral(x - 50); + if (x >= 40) return "XL" + FormatAsRomanNumeral(x - 40); + if (x >= 10) return "X" + FormatAsRomanNumeral(x - 10); + if (x >= 9) return "IX" + FormatAsRomanNumeral(x - 9); + if (x >= 5) return "V" + FormatAsRomanNumeral(x - 5); + if (x >= 4) return "IV" + FormatAsRomanNumeral(x - 4); + if (x >= 1) return "I" + FormatAsRomanNumeral(x - 1); + + return string.Empty; + }); + } } } \ No newline at end of file diff --git a/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs b/QuestPDF.ReportSample/Layouts/DifferentHeadersTemplate.cs index e67fb58..9b72d86 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").FontColor(Colors.Grey.Darken3).FontSize(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").FontColor(Colors.Grey.Darken3).FontSize(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 2bba69a..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(); + text.CurrentPageNumber().Format(x => x?.FormatAsRomanNumeral() ?? "-----"); text.Span(" / "); - text.TotalPages(); + 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 3f93f93..8788edd 100644 --- a/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs +++ b/QuestPDF.ReportSample/Layouts/TableOfContentsTemplate.cs @@ -1,5 +1,7 @@ using System.Collections.Generic; +using System.Drawing; using QuestPDF.Fluent; +using QuestPDF.Helpers; using QuestPDF.Infrastructure; namespace QuestPDF.ReportSample.Layouts @@ -21,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 => { @@ -43,7 +46,18 @@ namespace QuestPDF.ReportSample.Layouts { row.ConstantItem(25).Text($"{number}."); row.RelativeItem().Text(locationName); - row.ConstantItem(150).AlignRight().Text(text => text.PageNumberOfLocation(locationName)); + row.ConstantItem(150).AlignRight().Text(text => + { + text.BeginPageNumberOfSection(locationName); + text.Span(" - "); + text.EndPageNumberOfSection(locationName); + + var lengthStyle = TextStyle.Default.Color(Colors.Grey.Medium); + + 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.UnitTests/ExternalLinkTests.cs b/QuestPDF.UnitTests/ExternalLinkTests.cs index cd6853d..5461cf2 100644 --- a/QuestPDF.UnitTests/ExternalLinkTests.cs +++ b/QuestPDF.UnitTests/ExternalLinkTests.cs @@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests public class ExternalLinkTests { [Test] - public void Measure() => SimpleContainerTests.Measure(); + public void Measure() => SimpleContainerTests.Measure(); // TODO: consider tests for the Draw method } diff --git a/QuestPDF.UnitTests/FontStyleSetTests.cs b/QuestPDF.UnitTests/FontStyleSetTests.cs new file mode 100644 index 0000000..9743888 --- /dev/null +++ b/QuestPDF.UnitTests/FontStyleSetTests.cs @@ -0,0 +1,146 @@ +using FluentAssertions; +using NUnit.Framework; +using QuestPDF.Drawing; +using SkiaSharp; +using static SkiaSharp.SKFontStyleSlant; + +namespace QuestPDF.UnitTests +{ + [TestFixture] + public class FontStyleSetTests + { + private void ExpectComparisonOrder(SKFontStyle target, SKFontStyle[] styles) + { + for (var i = 0; i < styles.Length - 1; i++) + { + var currentStyle = styles[i]; + var nextStyle = styles[i + 1]; + + FontStyleSet.IsBetterMatch(target, currentStyle, nextStyle).Should().BeTrue(); + FontStyleSet.IsBetterMatch(target, nextStyle, currentStyle).Should().BeFalse(); + } + } + + [Test] + public void FontStyleSet_IsBetterMatch_CondensedWidth() + { + var styles = new[] + { + new SKFontStyle(500, 5, Upright), + new SKFontStyle(500, 4, Upright), + new SKFontStyle(500, 3, Upright), + new SKFontStyle(500, 6, Upright) + }; + + ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles); + } + + [Test] + public void FontStyleSet_IsBetterMatch_ExpandedWidth() + { + var styles = new[] + { + new SKFontStyle(500, 6, Upright), + new SKFontStyle(500, 7, Upright), + new SKFontStyle(500, 8, Upright), + new SKFontStyle(500, 5, Upright) + }; + + ExpectComparisonOrder(new SKFontStyle(500, 6, Upright), styles); + } + + [Test] + public void FontStyleSet_IsBetterMatch_ItalicSlant() + { + var styles = new[] + { + new SKFontStyle(500, 5, Italic), + new SKFontStyle(500, 5, Oblique), + new SKFontStyle(500, 5, Upright) + }; + + ExpectComparisonOrder(new SKFontStyle(500, 5, Italic), styles); + } + + [Test] + public void FontStyleSet_IsBetterMatch_ObliqueSlant() + { + var styles = new[] + { + new SKFontStyle(500, 5, Oblique), + new SKFontStyle(500, 5, Italic), + new SKFontStyle(500, 5, Upright) + }; + + ExpectComparisonOrder(new SKFontStyle(500, 5, Oblique), styles); + } + + [Test] + public void FontStyleSet_IsBetterMatch_UprightSlant() + { + var styles = new[] + { + new SKFontStyle(500, 5, Upright), + new SKFontStyle(500, 5, Oblique), + new SKFontStyle(500, 5, Italic) + }; + + ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles); + } + + [Test] + public void FontStyleSet_IsBetterMatch_ThinWeight() + { + var styles = new[] + { + new SKFontStyle(300, 5, Upright), + new SKFontStyle(200, 5, Upright), + new SKFontStyle(100, 5, Upright), + new SKFontStyle(400, 5, Upright) + }; + + ExpectComparisonOrder(new SKFontStyle(300, 5, Upright), styles); + } + + [Test] + public void FontStyleSet_IsBetterMatch_RegularWeight() + { + var styles = new[] + { + new SKFontStyle(500, 5, Upright), + new SKFontStyle(300, 5, Upright), + new SKFontStyle(100, 5, Upright), + new SKFontStyle(600, 5, Upright) + }; + + ExpectComparisonOrder(new SKFontStyle(400, 5, Upright), styles); + } + + [Test] + public void FontStyleSet_IsBetterMatch_BoldWeight() + { + var styles = new[] + { + new SKFontStyle(600, 5, Upright), + new SKFontStyle(700, 5, Upright), + new SKFontStyle(800, 5, Upright), + new SKFontStyle(500, 5, Upright) + }; + + ExpectComparisonOrder(new SKFontStyle(600, 5, Upright), styles); + } + + [Test] + public void FontStyleSet_RespectsPriority() + { + var styles = new[] + { + new SKFontStyle(600, 5, Italic), + new SKFontStyle(600, 6, Upright), + new SKFontStyle(500, 6, Italic) + }; + + ExpectComparisonOrder(new SKFontStyle(500, 5, Upright), styles); + } + } +} \ No newline at end of file diff --git a/QuestPDF.UnitTests/InternalLinkTests.cs b/QuestPDF.UnitTests/InternalLinkTests.cs index bd52d1c..1ad9432 100644 --- a/QuestPDF.UnitTests/InternalLinkTests.cs +++ b/QuestPDF.UnitTests/InternalLinkTests.cs @@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests public class InternalLinkTests { [Test] - public void Measure() => SimpleContainerTests.Measure(); + public void Measure() => SimpleContainerTests.Measure(); // TODO: consider tests for the Draw method } diff --git a/QuestPDF.UnitTests/InternalLocationTests.cs b/QuestPDF.UnitTests/InternalLocationTests.cs index 8a21fcb..5966c36 100644 --- a/QuestPDF.UnitTests/InternalLocationTests.cs +++ b/QuestPDF.UnitTests/InternalLocationTests.cs @@ -8,7 +8,7 @@ namespace QuestPDF.UnitTests public class InternalLocationTests { [Test] - public void Measure() => SimpleContainerTests.Measure(); + public void Measure() => SimpleContainerTests.Measure(); // TODO: consider tests for the Draw method } diff --git a/QuestPDF.UnitTests/TestEngine/MockCanvas.cs b/QuestPDF.UnitTests/TestEngine/MockCanvas.cs index fbed591..6e4926e 100644 --- a/QuestPDF.UnitTests/TestEngine/MockCanvas.cs +++ b/QuestPDF.UnitTests/TestEngine/MockCanvas.cs @@ -20,9 +20,9 @@ namespace QuestPDF.UnitTests.TestEngine public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color); public void DrawText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style); public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size); - - public void DrawExternalLink(string url, Size size) => throw new NotImplementedException(); - public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException(); - public void DrawLocation(string locationName) => throw new NotImplementedException(); + + public void DrawHyperlink(string url, Size size) => throw new NotImplementedException(); + public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException(); + public void DrawSection(string sectionName) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs b/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs index 3617052..4fbedfe 100644 --- a/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs +++ b/QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs @@ -18,8 +18,8 @@ namespace QuestPDF.UnitTests.TestEngine public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style)); public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size)); - public void DrawExternalLink(string url, Size size) => throw new NotImplementedException(); - public void DrawLocationLink(string locationName, Size size) => throw new NotImplementedException(); - public void DrawLocation(string locationName) => throw new NotImplementedException(); + public void DrawHyperlink(string url, Size size) => throw new NotImplementedException(); + public void DrawSectionLink(string sectionName, Size size) => throw new NotImplementedException(); + public void DrawSection(string sectionName) => throw new NotImplementedException(); } } \ No newline at end of file diff --git a/QuestPDF.UnitTests/TestEngine/TestPlan.cs b/QuestPDF.UnitTests/TestEngine/TestPlan.cs index 00136fc..1ec5fe9 100644 --- a/QuestPDF.UnitTests/TestEngine/TestPlan.cs +++ b/QuestPDF.UnitTests/TestEngine/TestPlan.cs @@ -92,7 +92,7 @@ namespace QuestPDF.UnitTests.TestEngine Assert.AreEqual(expected.Position.Y, position.Y, "Draw text: Y"); Assert.AreEqual(expected.Style.Color, style.Color, "Draw text: color"); - Assert.AreEqual(expected.Style.FontType, style.FontType, "Draw text: font"); + Assert.AreEqual(expected.Style.FontFamily, style.FontFamily, "Draw text: font"); Assert.AreEqual(expected.Style.Size, style.Size, "Draw text: size"); }, DrawImageFunc = (image, position, size) => diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index cd600e4..e4d2fca 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Concurrent; using System.IO; +using System.Linq; using QuestPDF.Infrastructure; using SkiaSharp; @@ -8,16 +9,41 @@ namespace QuestPDF.Drawing { public static class FontManager { - private static ConcurrentDictionary Typefaces = new ConcurrentDictionary(); - private static ConcurrentDictionary FontMetrics = new ConcurrentDictionary(); - private static ConcurrentDictionary Paints = new ConcurrentDictionary(); - private static ConcurrentDictionary ColorPaint = new ConcurrentDictionary(); + private static ConcurrentDictionary StyleSets = new(); + private static ConcurrentDictionary FontMetrics = new(); + private static ConcurrentDictionary Paints = new(); + private static ConcurrentDictionary ColorPaint = new(); + private static void RegisterFontType(SKData fontData, string? customName = null) + { + foreach (var index in Enumerable.Range(0, 256)) + { + var typeface = SKTypeface.FromData(fontData, index); + + if (typeface == null) + break; + + var typefaceName = customName ?? typeface.FamilyName; + + var fontStyleSet = StyleSets.GetOrAdd(typefaceName, _ => new FontStyleSet()); + fontStyleSet.Add(typeface); + } + } + + [Obsolete("Since version 2022.3, the FontManager class offers better font type matching support. Please use the RegisterFont(Stream stream) method.")] public static void RegisterFontType(string fontName, Stream stream) { - Typefaces.TryAdd(fontName, SKTypeface.FromStream(stream)); + using var fontData = SKData.Create(stream); + RegisterFontType(fontData); + RegisterFontType(fontData, customName: fontName); } - + + public static void RegisterFont(Stream stream) + { + using var fontData = SKData.Create(stream); + RegisterFontType(fontData); + } + internal static SKPaint ColorToPaint(this string color) { return ColorPaint.GetOrAdd(color, Convert); @@ -30,37 +56,47 @@ namespace QuestPDF.Drawing }; } } - + internal static SKPaint ToPaint(this TextStyle style) { - return Paints.GetOrAdd(style.Key, key => Convert(style)); - + return Paints.GetOrAdd(style.PaintKey, key => Convert(style)); + static SKPaint Convert(TextStyle style) { return new SKPaint { Color = SKColor.Parse(style.Color), Typeface = GetTypeface(style), - TextSize = (style.Size ?? 12), - TextEncoding = SKTextEncoding.Utf32 + TextSize = style.Size ?? 12 }; } static SKTypeface GetTypeface(TextStyle style) { - if (Typefaces.TryGetValue(style.FontType, out var result)) - return result; - + var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal); var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright; + + var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant); + + if (StyleSets.TryGetValue(style.FontFamily, out var fontStyleSet)) + return fontStyleSet.Match(fontStyle); + + var fontFromDefaultSource = SKFontManager.Default.MatchFamily(style.FontFamily, fontStyle); - 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."); + if (fontFromDefaultSource != null) + return fontFromDefaultSource; + + throw new ArgumentException( + $"The typeface '{style.FontFamily}' could not be found. " + + $"Please consider the following options: " + + $"1) install the font on your operating system or execution environment. " + + $"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method."); } } internal static SKFontMetrics ToFontMetrics(this TextStyle style) { - return FontMetrics.GetOrAdd(style.Key, key => style.ToPaint().FontMetrics); + return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics); } } } \ No newline at end of file diff --git a/QuestPDF/Drawing/FontStyleSet.cs b/QuestPDF/Drawing/FontStyleSet.cs new file mode 100644 index 0000000..fd70aa6 --- /dev/null +++ b/QuestPDF/Drawing/FontStyleSet.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Collections.Concurrent; +using SkiaSharp; + +namespace QuestPDF.Drawing +{ + internal class FontStyleSet + { + private ConcurrentDictionary Styles { get; } = new(); + + public void Add(SKTypeface typeface) + { + var style = typeface.FontStyle; + Styles.AddOrUpdate(style, _ => typeface, (_, _) => typeface); + } + + public SKTypeface? Match(SKFontStyle target) + { + SKFontStyle? bestStyle = null; + SKTypeface? bestTypeface = null; + + foreach (var entry in Styles) + { + if (IsBetterMatch(target, entry.Key, bestStyle)) + { + bestStyle = entry.Key; + bestTypeface = entry.Value; + } + } + + return bestTypeface; + } + + private static Dictionary> SlantFallbacks = new() + { + { SKFontStyleSlant.Italic, new() { SKFontStyleSlant.Italic, SKFontStyleSlant.Oblique, SKFontStyleSlant.Upright } }, + { SKFontStyleSlant.Oblique, new() { SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic, SKFontStyleSlant.Upright } }, + { SKFontStyleSlant.Upright, new() { SKFontStyleSlant.Upright, SKFontStyleSlant.Oblique, SKFontStyleSlant.Italic } }, + }; + + // Checks whether style a is a better match for the target then style b. Uses the CSS font style matching algorithm + internal static bool IsBetterMatch(SKFontStyle? target, SKFontStyle? a, SKFontStyle? b) + { + // A font is better than no font + if (b == null) + return true; + + if (a == null) + return false; + + // First check font width + // For normal and condensed widths prefer smaller widths + // For expanded widths prefer larger widths + if (target.Width <= (int)SKFontStyleWidth.Normal) + { + if (a.Width <= target.Width && b.Width > target.Width) + return true; + + if (a.Width > target.Width && b.Width <= target.Width) + return false; + } + else + { + if (a.Width >= target.Width && b.Width < target.Width) + return true; + + if (a.Width < target.Width && b.Width >= target.Width) + return false; + } + + // Prefer closest match + var widthDifferenceA = Math.Abs(a.Width - target.Width); + var widthDifferenceB = Math.Abs(b.Width - target.Width); + + if (widthDifferenceA < widthDifferenceB) + return true; + + if (widthDifferenceB < widthDifferenceA) + return false; + + // Prefer closest slant based on provided fallback list + var slantFallback = SlantFallbacks[target.Slant]; + var slantIndexA = slantFallback.IndexOf(a.Slant); + var slantIndexB = slantFallback.IndexOf(b.Slant); + + if (slantIndexA < slantIndexB) + return true; + + if (slantIndexB < slantIndexA) + return false; + + // Check weight last + // For thin (<400) weights, prefer thinner weights + // For regular (400-500) weights, prefer other regular weights, then use rule for thin or bold + // For bold (>500) weights, prefer thicker weights + // Behavior for values other than multiples of 100 is not given in the specification + + if (target.Weight >= 400 && target.Weight <= 500) + { + if ((a.Weight >= 400 && a.Weight <= 500) && !(b.Weight >= 400 && b.Weight <= 500)) + return true; + + if (!(a.Weight >= 400 && a.Weight <= 500) && (b.Weight >= 400 && b.Weight <= 500)) + return false; + } + + if (target.Weight < 450) + { + if (a.Weight <= target.Weight && b.Weight > target.Weight) + return true; + + if (a.Weight > target.Weight && b.Weight <= target.Weight) + return false; + } + else + { + if (a.Weight >= target.Weight && b.Weight < target.Weight) + return true; + + if (a.Weight < target.Weight && b.Weight >= target.Weight) + return false; + } + + // Prefer closest weight + var weightDifferenceA = Math.Abs(a.Weight - target.Weight); + var weightDifferenceB = Math.Abs(b.Weight - target.Weight); + + return weightDifferenceA < weightDifferenceB; + } + } +} \ No newline at end of file diff --git a/QuestPDF/Drawing/FreeCanvas.cs b/QuestPDF/Drawing/FreeCanvas.cs index 6716e9e..251c9d1 100644 --- a/QuestPDF/Drawing/FreeCanvas.cs +++ b/QuestPDF/Drawing/FreeCanvas.cs @@ -51,17 +51,17 @@ namespace QuestPDF.Drawing } - public void DrawExternalLink(string url, Size size) + public void DrawHyperlink(string url, Size size) { } - public void DrawLocationLink(string locationName, Size size) + public void DrawSectionLink(string sectionName, Size size) { } - public void DrawLocation(string locationName) + public void DrawSection(string sectionName) { } diff --git a/QuestPDF/Drawing/SkiaCanvasBase.cs b/QuestPDF/Drawing/SkiaCanvasBase.cs index 9ec37a8..aaf8e25 100644 --- a/QuestPDF/Drawing/SkiaCanvasBase.cs +++ b/QuestPDF/Drawing/SkiaCanvasBase.cs @@ -37,19 +37,19 @@ namespace QuestPDF.Drawing Canvas.DrawImage(image, new SKRect(vector.X, vector.Y, size.Width, size.Height)); } - public void DrawExternalLink(string url, Size size) + public void DrawHyperlink(string url, Size size) { Canvas.DrawUrlAnnotation(new SKRect(0, 0, size.Width, size.Height), url); } - public void DrawLocationLink(string locationName, Size size) + public void DrawSectionLink(string sectionName, Size size) { - Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), locationName); + Canvas.DrawLinkDestinationAnnotation(new SKRect(0, 0, size.Width, size.Height), sectionName); } - public void DrawLocation(string locationName) + public void DrawSection(string sectionName) { - Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), locationName); + Canvas.DrawNamedDestinationAnnotation(new SKPoint(0, 0), sectionName); } public void Rotate(float angle) diff --git a/QuestPDF/Elements/DebugArea.cs b/QuestPDF/Elements/DebugArea.cs index cd93da2..7b20236 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) + .FontColor(Color) + .FontFamily(Fonts.Consolas) + .FontSize(8); }); } } diff --git a/QuestPDF/Elements/ExternalLink.cs b/QuestPDF/Elements/Hyperlink.cs similarity index 80% rename from QuestPDF/Elements/ExternalLink.cs rename to QuestPDF/Elements/Hyperlink.cs index 67c2d84..73e9bce 100644 --- a/QuestPDF/Elements/ExternalLink.cs +++ b/QuestPDF/Elements/Hyperlink.cs @@ -3,7 +3,7 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Elements { - internal class ExternalLink : ContainerElement + internal class Hyperlink : ContainerElement { public string Url { get; set; } = "https://www.questpdf.com"; @@ -14,7 +14,7 @@ namespace QuestPDF.Elements if (targetSize.Type == SpacePlanType.Wrap) return; - Canvas.DrawExternalLink(Url, targetSize); + Canvas.DrawHyperlink(Url, targetSize); base.Draw(availableSpace); } } diff --git a/QuestPDF/Elements/Page.cs b/QuestPDF/Elements/Page.cs index 7d06bbe..be8e678 100644 --- a/QuestPDF/Elements/Page.cs +++ b/QuestPDF/Elements/Page.cs @@ -20,6 +20,9 @@ namespace QuestPDF.Elements public string BackgroundColor { get; set; } = Colors.Transparent; + public Element Background { get; set; } = Empty.Instance; + public Element Foreground { get; set; } = Empty.Instance; + public Element Header { get; set; } = Empty.Instance; public Element Content { get; set; } = Empty.Instance; public Element Footer { get; set; } = Empty.Instance; @@ -27,39 +30,54 @@ 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) - .PaddingTop(MarginTop) - .PaddingBottom(MarginBottom) - - .DefaultTextStyle(DefaultTextStyle) - - .Decoration(decoration => + .Layers(layers => { - decoration - .Before() - .DebugPointer("Page header") - .Element(Header); + layers + .Layer() + .DebugPointer("Page background layer") + .Element(Background); - decoration - .Content() - .Element(x => IsClose(MinSize.Width, MaxSize.Width) ? x.ExtendHorizontal() : x) - .Element(x => IsClose(MinSize.Height, MaxSize.Height) ? x.ExtendVertical() : x) - .DebugPointer("Page content") - .Element(Content); + layers + .PrimaryLayer() + .MinWidth(MinSize.Width) + .MinHeight(MinSize.Height) + + .MaxWidth(MaxSize.Width) + .MaxHeight(MaxSize.Height) + + .Background(BackgroundColor) + + .PaddingLeft(MarginLeft) + .PaddingRight(MarginRight) + .PaddingTop(MarginTop) + .PaddingBottom(MarginBottom) + + .DefaultTextStyle(DefaultTextStyle) + + .Decoration(decoration => + { + decoration + .Before() + .DebugPointer("Page header") + .Element(Header); + + decoration + .Content() + .Element(x => IsClose(MinSize.Width, MaxSize.Width) ? x.ExtendHorizontal() : x) + .Element(x => IsClose(MinSize.Height, MaxSize.Height) ? x.ExtendVertical() : x) + .DebugPointer("Page content") + .Element(Content); + + decoration + .After() + .DebugPointer("Page footer") + .Element(Footer); + }); - decoration - .After() - .DebugPointer("Page footer") - .Element(Footer); + layers + .Layer() + .DebugPointer("Page foreground layer") + .Element(Foreground); }); bool IsClose(float x, float y) diff --git a/QuestPDF/Elements/Placeholder.cs b/QuestPDF/Elements/Placeholder.cs index cf33087..b1020d0 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).FontSize(14); }); } } diff --git a/QuestPDF/Elements/InternalLocation.cs b/QuestPDF/Elements/Section.cs similarity index 71% rename from QuestPDF/Elements/InternalLocation.cs rename to QuestPDF/Elements/Section.cs index 99c122d..46fe3c0 100644 --- a/QuestPDF/Elements/InternalLocation.cs +++ b/QuestPDF/Elements/Section.cs @@ -2,7 +2,7 @@ namespace QuestPDF.Elements { - internal class InternalLocation : ContainerElement, IStateResettable + internal class Section : ContainerElement, IStateResettable { public string LocationName { get; set; } private bool IsRendered { get; set; } @@ -16,12 +16,11 @@ namespace QuestPDF.Elements { if (!IsRendered) { - PageContext.SetLocationPage(LocationName); - - Canvas.DrawLocation(LocationName); + Canvas.DrawSection(LocationName); IsRendered = true; } + PageContext.SetSectionPage(LocationName); base.Draw(availableSpace); } } diff --git a/QuestPDF/Elements/InternalLink.cs b/QuestPDF/Elements/SectionLink.cs similarity index 69% rename from QuestPDF/Elements/InternalLink.cs rename to QuestPDF/Elements/SectionLink.cs index c6e2aba..f19713a 100644 --- a/QuestPDF/Elements/InternalLink.cs +++ b/QuestPDF/Elements/SectionLink.cs @@ -3,9 +3,9 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Elements { - internal class InternalLink : ContainerElement + internal class SectionLink : ContainerElement { - public string LocationName { get; set; } + public string SectionName { get; set; } internal override void Draw(Size availableSpace) { @@ -14,7 +14,7 @@ namespace QuestPDF.Elements if (targetSize.Type == SpacePlanType.Wrap) return; - Canvas.DrawLocationLink(LocationName, targetSize); + Canvas.DrawSectionLink(SectionName, targetSize); base.Draw(availableSpace); } } diff --git a/QuestPDF/Elements/Text/Items/TextBlockExternalLink.cs b/QuestPDF/Elements/Text/Items/TextBlockHyperlink.cs similarity index 78% rename from QuestPDF/Elements/Text/Items/TextBlockExternalLink.cs rename to QuestPDF/Elements/Text/Items/TextBlockHyperlink.cs index 5337fca..bd8228b 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockExternalLink.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockHyperlink.cs @@ -3,7 +3,7 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Text.Items { - internal class TextBlockExternalLink : TextBlockSpan + internal class TextBlockHyperlink : TextBlockSpan { public string Url { get; set; } @@ -15,7 +15,7 @@ namespace QuestPDF.Elements.Text.Items 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.DrawHyperlink(Url, new Size(request.TextSize.Width, request.TextSize.Height)); request.Canvas.Translate(new Position(0, -request.TotalAscent)); base.Draw(request); diff --git a/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs b/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs index 85a829d..b48b65d 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockPageNumber.cs @@ -1,11 +1,13 @@ -using QuestPDF.Elements.Text.Calculation; +using System; +using QuestPDF.Elements.Text.Calculation; using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Text.Items { internal class TextBlockPageNumber : TextBlockSpan { - public string SlotName { get; set; } + public const string PageNumberPlaceholder = "123"; + public Func Source { get; set; } = _ => PageNumberPlaceholder; public override TextMeasurementResult? Measure(TextMeasurementRequest request) { @@ -21,13 +23,7 @@ namespace QuestPDF.Elements.Text.Items private void SetPageNumber(IPageContext context) { - var pageNumberPlaceholder = 123; - - var pageNumber = context.GetRegisteredLocations().Contains(SlotName) - ? context.GetLocationPage(SlotName) - : pageNumberPlaceholder; - - Text = pageNumber.ToString(); + Text = Source(context) ?? PageNumberPlaceholder; } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Items/TextBlockInternalLink.cs b/QuestPDF/Elements/Text/Items/TextBlockSectionlLink.cs similarity index 72% rename from QuestPDF/Elements/Text/Items/TextBlockInternalLink.cs rename to QuestPDF/Elements/Text/Items/TextBlockSectionlLink.cs index 6b28113..5c41ca0 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockInternalLink.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSectionlLink.cs @@ -3,9 +3,9 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Text.Items { - internal class TextBlockInternalLink : TextBlockSpan + internal class TextBlockSectionlLink : TextBlockSpan { - public string LocationName { get; set; } + public string SectionName { get; set; } public override TextMeasurementResult? Measure(TextMeasurementRequest request) { @@ -15,7 +15,7 @@ namespace QuestPDF.Elements.Text.Items 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.DrawSectionLink(SectionName, new Size(request.TextSize.Width, request.TextSize.Height)); request.Canvas.Translate(new Position(0, -request.TotalAscent)); base.Draw(request); diff --git a/QuestPDF/Fluent/ElementExtensions.cs b/QuestPDF/Fluent/ElementExtensions.cs index d9ce5fb..804dc84 100644 --- a/QuestPDF/Fluent/ElementExtensions.cs +++ b/QuestPDF/Fluent/ElementExtensions.cs @@ -60,7 +60,7 @@ namespace QuestPDF.Fluent public static void Placeholder(this IContainer element, string? text = null) { - element.Component(new Elements.Placeholder + element.Component(new Placeholder { Text = text ?? string.Empty }); @@ -99,27 +99,45 @@ namespace QuestPDF.Fluent return element.Element(new Container()); } + [Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")] public static IContainer ExternalLink(this IContainer element, string url) { - return element.Element(new ExternalLink + return element.Hyperlink(url); + } + + public static IContainer Hyperlink(this IContainer element, string url) + { + return element.Element(new Hyperlink { Url = url }); } + [Obsolete("This element has been renamed since version 2022.3. Please use the Section method.")] public static IContainer Location(this IContainer element, string locationName) { - return element.Element(new InternalLocation + return element.Section(locationName); + } + + public static IContainer Section(this IContainer element, string sectionName) + { + return element.Element(new Section { - LocationName = locationName + LocationName = sectionName }); } + [Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")] public static IContainer InternalLink(this IContainer element, string locationName) { - return element.Element(new InternalLink + return element.SectionLink(locationName); + } + + public static IContainer SectionLink(this IContainer element, string sectionName) + { + return element.Element(new SectionLink { - LocationName = locationName + SectionName = sectionName }); } @@ -159,7 +177,15 @@ namespace QuestPDF.Fluent TextStyle = textStyle }); } - + + public static IContainer DefaultTextStyle(this IContainer element, Func handler) + { + return element.Element(new DefaultTextStyle + { + TextStyle = handler(TextStyle.Default) + }); + } + public static IContainer StopPaging(this IContainer element) { return element.Element(new StopPaging()); diff --git a/QuestPDF/Fluent/MinimalApi.cs b/QuestPDF/Fluent/MinimalApi.cs new file mode 100644 index 0000000..a37110d --- /dev/null +++ b/QuestPDF/Fluent/MinimalApi.cs @@ -0,0 +1,35 @@ +using System; +using QuestPDF.Drawing; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Fluent +{ + public class Document : IDocument + { + private Action ContentSource { get; } + private DocumentMetadata Metadata { get; set; } = DocumentMetadata.Default; + + private Document(Action contentSource) + { + ContentSource = contentSource; + } + + public static Document Create(Action handler) + { + return new Document(handler); + } + + public Document WithMetadata(DocumentMetadata metadata) + { + Metadata = metadata ?? Metadata; + return this; + } + + #region IDocument + + public DocumentMetadata GetMetadata() => Metadata; + public void Compose(IDocumentContainer container) => ContentSource(container); + + #endregion + } +} \ No newline at end of file diff --git a/QuestPDF/Fluent/PageExtensions.cs b/QuestPDF/Fluent/PageExtensions.cs index ab968ee..e043ed3 100644 --- a/QuestPDF/Fluent/PageExtensions.cs +++ b/QuestPDF/Fluent/PageExtensions.cs @@ -83,11 +83,30 @@ namespace QuestPDF.Fluent Page.DefaultTextStyle = textStyle; } + public void DefaultTextStyle(Func handler) + { + DefaultTextStyle(handler(TextStyle.Default)); + } + public void Background(string color) { Page.BackgroundColor = color; } + public IContainer Background() + { + var container = new Container(); + Page.Background = container; + return container; + } + + public IContainer Foreground() + { + var container = new Container(); + Page.Foreground = container; + return container; + } + public IContainer Header() { var container = new Container(); diff --git a/QuestPDF/Fluent/TextExtensions.cs b/QuestPDF/Fluent/TextExtensions.cs index 2d688b4..3819d40 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,15 @@ namespace QuestPDF.Fluent TextBlocks.Last().Items.Add(item); } - public void Span(string? text, TextStyle? style = null) + [Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")] + public void Span(string? text, TextStyle style) { - if (IsNullOrEmpty(text)) - return; - - style ??= TextStyle.Default; + Span(text).Style(style); + } + + public TextSpanDescriptor Span(string? text) + { + var style = DefaultStyle.Clone(); var items = text .Replace("\r", string.Empty) @@ -77,85 +113,123 @@ 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 void PageNumber(string slotName, TextStyle? style = null) + + private TextPageNumberDescriptor PageNumber(Func pageNumber) { - if (IsNullOrEmpty(slotName)) - throw new ArgumentException(nameof(slotName)); + var style = DefaultStyle.Clone(); + var descriptor = new TextPageNumberDescriptor(DefaultStyle); - style ??= TextStyle.Default; - - AddItemToLastTextBlock(new TextBlockPageNumber() + AddItemToLastTextBlock(new TextBlockPageNumber { - Style = style, - SlotName = slotName + Source = context => descriptor.FormatFunction(pageNumber(context)), + Style = style }); + + return descriptor; } - - public void CurrentPageNumber(TextStyle? style = null) + + public TextPageNumberDescriptor CurrentPageNumber() { - PageNumber(PageContext.CurrentPageSlot, style); + return PageNumber(x => x.CurrentPage); } - public void TotalPages(TextStyle? style = null) + public TextPageNumberDescriptor TotalPages() { - PageNumber(PageContext.TotalPagesSlot, style); + 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) { - if (IsNullOrEmpty(locationName)) - throw new ArgumentException(nameof(locationName)); - - PageNumber(locationName, style); + BeginPageNumberOfSection(locationName).Style(style); } - public void InternalLocation(string? text, string locationName, TextStyle? style = null) + public TextPageNumberDescriptor BeginPageNumberOfSection(string locationName) { - if (IsNullOrEmpty(text)) - return; - - if (IsNullOrEmpty(locationName)) - throw new ArgumentException(nameof(locationName)); + return PageNumber(x => x.GetLocation(locationName)?.PageStart); + } + + public TextPageNumberDescriptor EndPageNumberOfSection(string locationName) + { + return PageNumber(x => x.GetLocation(locationName)?.PageEnd); + } + + public TextPageNumberDescriptor PageNumberWithinSection(string locationName) + { + return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd); + } + + public TextPageNumberDescriptor TotalPagesWithinSection(string locationName) + { + return PageNumber(x => x.GetLocation(locationName)?.Length); + } + + public TextSpanDescriptor SectionLink(string? text, string sectionName) + { + if (IsNullOrEmpty(sectionName)) + throw new ArgumentException(nameof(sectionName)); - style ??= TextStyle.Default; + var style = DefaultStyle.Clone(); + var descriptor = new TextSpanDescriptor(style); - AddItemToLastTextBlock(new TextBlockInternalLink + if (IsNullOrEmpty(text)) + return descriptor; + + AddItemToLastTextBlock(new TextBlockSectionlLink { Style = style, Text = text, - LocationName = locationName + SectionName = sectionName }); + + return descriptor; } - public void ExternalLocation(string? text, string url, TextStyle? style = null) + [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(style); + } + + public TextSpanDescriptor Hyperlink(string? text, string url) { - if (IsNullOrEmpty(text)) - return; - if (IsNullOrEmpty(url)) throw new ArgumentException(nameof(url)); + + var style = DefaultStyle.Clone(); + var descriptor = new TextSpanDescriptor(style); + + if (IsNullOrEmpty(text)) + return descriptor; - style ??= TextStyle.Default; - - AddItemToLastTextBlock(new TextBlockExternalLink + AddItemToLastTextBlock(new TextBlockHyperlink { Style = style, 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(style); } public IContainer Element() @@ -197,9 +271,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 overload that returns a TextSpanDescriptor object which allows to specify text style.")] + public static void Text(this IContainer element, object? text, TextStyle style) { - element.Text(x => x.Span(text?.ToString(), style)); + element.Text(text).Style(style); + } + + public static TextSpanDescriptor Text(this IContainer element, object? text) + { + 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..9af8e9a --- /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 FontColor(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 FontFamily(this T descriptor, string value) where T : TextSpanDescriptor + { + descriptor.TextStyle.FontFamily = value; + return descriptor; + } + + public static T FontSize(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/Fluent/TextStyleExtensions.cs b/QuestPDF/Fluent/TextStyleExtensions.cs index 64cae02..a748225 100644 --- a/QuestPDF/Fluent/TextStyleExtensions.cs +++ b/QuestPDF/Fluent/TextStyleExtensions.cs @@ -13,7 +13,13 @@ namespace QuestPDF.Fluent return style; } + [Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")] public static TextStyle Color(this TextStyle style, string value) + { + return style.FontColor(value); + } + + public static TextStyle FontColor(this TextStyle style, string value) { return style.Mutate(x => x.Color = value); } @@ -23,12 +29,24 @@ namespace QuestPDF.Fluent return style.Mutate(x => x.BackgroundColor = value); } + [Obsolete("This element has been renamed since version 2022.3. Please use the FontFamily method.")] public static TextStyle FontType(this TextStyle style, string value) { - return style.Mutate(x => x.FontType = value); + return style.FontFamily(value); } + public static TextStyle FontFamily(this TextStyle style, string value) + { + return style.Mutate(x => x.FontFamily = value); + } + + [Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")] public static TextStyle Size(this TextStyle style, float value) + { + return style.FontSize(value); + } + + public static TextStyle FontSize(this TextStyle style, float value) { return style.Mutate(x => x.Size = value); } diff --git a/QuestPDF/Infrastructure/ICanvas.cs b/QuestPDF/Infrastructure/ICanvas.cs index e2bb3fb..65b470b 100644 --- a/QuestPDF/Infrastructure/ICanvas.cs +++ b/QuestPDF/Infrastructure/ICanvas.cs @@ -10,9 +10,9 @@ namespace QuestPDF.Infrastructure void DrawText(string text, Position position, TextStyle style); void DrawImage(SKImage image, Position position, Size size); - void DrawExternalLink(string url, Size size); - void DrawLocationLink(string locationName, Size size); - void DrawLocation(string locationName); + void DrawHyperlink(string url, Size size); + void DrawSectionLink(string sectionName, Size size); + void DrawSection(string sectionName); void Rotate(float angle); void Scale(float scaleX, float scaleY); diff --git a/QuestPDF/Infrastructure/IPageContext.cs b/QuestPDF/Infrastructure/IPageContext.cs index b445994..73b2c0a 100644 --- a/QuestPDF/Infrastructure/IPageContext.cs +++ b/QuestPDF/Infrastructure/IPageContext.cs @@ -2,10 +2,18 @@ namespace QuestPDF.Infrastructure { - public interface IPageContext + internal class DocumentLocation { - void SetLocationPage(string key); - int GetLocationPage(string key); - ICollection GetRegisteredLocations(); + public string Name { get; set; } + public int PageStart { get; set; } + public int PageEnd { get; set; } + public int Length => PageEnd - PageStart + 1; + } + + internal interface IPageContext + { + int CurrentPage { get; } + void SetSectionPage(string name); + DocumentLocation? GetLocation(string name); } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/PageContext.cs b/QuestPDF/Infrastructure/PageContext.cs index bc2597b..385dcfb 100644 --- a/QuestPDF/Infrastructure/PageContext.cs +++ b/QuestPDF/Infrastructure/PageContext.cs @@ -1,44 +1,45 @@ using System; using System.Collections.Generic; +using System.Linq; namespace QuestPDF.Infrastructure { - public class PageContext : IPageContext + internal class PageContext : IPageContext { - public const string CurrentPageSlot = "currentPage"; - public const string TotalPagesSlot = "totalPages"; + public const string DocumentLocation = "document"; - private Dictionary Locations { get; } = new Dictionary(); - private int PageNumber { get; set; } + private List Locations { get; } = new(); + public int CurrentPage { get; private set; } internal void SetPageNumber(int number) { - PageNumber = number; - Locations[CurrentPageSlot] = number; - - if (!Locations.ContainsKey(TotalPagesSlot) || Locations[TotalPagesSlot] < number) - Locations[TotalPagesSlot] = number; + CurrentPage = number; + SetSectionPage(DocumentLocation); } - public void SetLocationPage(string key) + public void SetSectionPage(string name) { - if (Locations.ContainsKey(key)) - return; - - Locations[key] = PageNumber; + var location = GetLocation(name); + + if (location == null) + { + location = new DocumentLocation + { + Name = name, + PageStart = CurrentPage, + PageEnd = CurrentPage + }; + + Locations.Add(location); + } + + if (location.PageEnd < CurrentPage) + location.PageEnd = CurrentPage; } - public int GetLocationPage(string key) + public DocumentLocation? GetLocation(string name) { - if (!Locations.ContainsKey(key)) - throw new ArgumentException($"The location '{key}' does not exists."); - - return Locations[key]; - } - - public ICollection GetRegisteredLocations() - { - return Locations.Keys; + return Locations.FirstOrDefault(x => x.Name == name); } } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index 640d9d2..4556648 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -9,7 +9,7 @@ namespace QuestPDF.Infrastructure internal string? Color { get; set; } internal string? BackgroundColor { get; set; } - internal string? FontType { get; set; } + internal string? FontFamily { get; set; } internal float? Size { get; set; } internal float? LineHeight { get; set; } internal FontWeight? FontWeight { get; set; } @@ -17,13 +17,14 @@ namespace QuestPDF.Infrastructure internal bool? HasStrikethrough { get; set; } internal bool? HasUnderline { get; set; } - internal string? Key { get; private set; } + internal object PaintKey { get; private set; } + internal object FontMetricsKey { get; private set; } internal static TextStyle LibraryDefault => new TextStyle { Color = Colors.Black, BackgroundColor = Colors.Transparent, - FontType = Fonts.Calibri, + FontFamily = Fonts.Calibri, Size = 12, LineHeight = 1.2f, FontWeight = Infrastructure.FontWeight.Normal, @@ -42,14 +43,15 @@ namespace QuestPDF.Infrastructure HasGlobalStyleApplied = true; ApplyParentStyle(globalStyle); - Key ??= $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{FontWeight}|{IsItalic}|{HasStrikethrough}|{HasUnderline}"; + PaintKey ??= (FontFamily, Size, FontWeight, IsItalic, Color); + FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic); } internal void ApplyParentStyle(TextStyle parentStyle) { Color ??= parentStyle.Color; BackgroundColor ??= parentStyle.BackgroundColor; - FontType ??= parentStyle.FontType; + FontFamily ??= parentStyle.FontFamily; Size ??= parentStyle.Size; LineHeight ??= parentStyle.LineHeight; FontWeight ??= parentStyle.FontWeight; @@ -58,6 +60,19 @@ namespace QuestPDF.Infrastructure HasUnderline ??= parentStyle.HasUnderline; } + internal void OverrideStyle(TextStyle parentStyle) + { + Color = parentStyle.Color ?? Color; + BackgroundColor = parentStyle.BackgroundColor ?? BackgroundColor; + FontFamily = parentStyle.FontFamily ?? FontFamily; + 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/QuestPDF/Infrastructure/Unit.cs b/QuestPDF/Infrastructure/Unit.cs index a4d55fc..af4af4a 100644 --- a/QuestPDF/Infrastructure/Unit.cs +++ b/QuestPDF/Infrastructure/Unit.cs @@ -13,6 +13,10 @@ namespace QuestPDF.Infrastructure Feet, Inch, + + /// + /// 1/1000th of inch + /// Mill } diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index 59ff516..57f541e 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -4,7 +4,7 @@ MarcinZiabek CodeFlint QuestPDF - 2022.2.7 + 2022.3.0 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. $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt")) 9 diff --git a/readme.md b/readme.md index a12c970..de23b1d 100644 --- a/readme.md +++ b/readme.md @@ -16,215 +16,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