Compare commits

..

5 Commits

Author SHA1 Message Date
MarcinZiabek 1b9b164583 DefaultTextStyle API improvements 2022-03-12 16:11:03 +01:00
MarcinZiabek 2c14f91ebe Added support for background and foreground in page API 2022-03-11 18:04:30 +01:00
MarcinZiabek b533a32b61 Text API naming adjustments 2022-03-11 13:23:36 +01:00
MarcinZiabek 262bd3c182 Improved backwards compatibility 2022-03-09 22:13:51 +01:00
MarcinZiabek fcff989ab2 Improved text API 2022-03-08 17:27:30 +01:00
52 changed files with 836 additions and 729 deletions
+4 -2
View File
@@ -13,7 +13,7 @@ namespace QuestPDF.Examples
[Test]
public void Example()
{
FontManager.RegisterFontType(File.OpenRead("LibreBarcode39-Regular.ttf"));
FontManager.RegisterFont(File.OpenRead("LibreBarcode39-Regular.ttf"));
RenderingTest
.Create()
@@ -25,7 +25,9 @@ namespace QuestPDF.Examples
.Background(Colors.White)
.AlignCenter()
.AlignMiddle()
.Text("*QuestPDF*", TextStyle.Default.FontType("Libre Barcode 39").Size(64));
.Text("*QuestPDF*")
.FontFamily("Libre Barcode 39")
.FontSize(64);
});
}
}
+4 -1
View File
@@ -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();
});
});
}
+4 -1
View File
@@ -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()
+1 -1
View File
@@ -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");
+1 -1
View File
@@ -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);
});
});
});
@@ -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);
}
});
});
+23 -11
View File
@@ -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);
}
});
});
+1 -1
View File
@@ -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 =>
{
+2 -2
View File
@@ -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
+2 -1
View File
@@ -74,7 +74,8 @@ namespace QuestPDF.Examples
.PrimaryLayer()
.AlignCenter()
.AlignMiddle()
.Text(sizeText, TextStyle.Default.Size(15));
.Text(sizeText)
.FontSize(15);
});
}
});
+57 -6
View File
@@ -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().FontSize(36).FontColor(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()
.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();
});
});
});
}
}
}
+2 -1
View File
@@ -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");
});
}
@@ -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);
});
});
});
}
}
}
+1 -1
View File
@@ -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 =>
{
+2 -1
View File
@@ -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 =>
{
+7 -7
View File
@@ -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").FontSize(72).Bold().FontColor(Colors.Blue.Darken2);
column.Item().Text("Henryk Sienkiewicz").FontSize(24).FontColor(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/").FontColor(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();
}
+14 -12
View File
@@ -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.").FontSize(20).FontColor(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, ").FontColor(Colors.Red.Medium).Underline();
text.Span("and this is slightly bigger text.").FontSize(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());
});
@@ -236,14 +238,14 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.BackgroundColor(Colors.Grey.Lighten3));
text.DefaultTextStyle(TextStyle.Default);
text.AlignLeft();
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(-10).Height(16).Width(32).Image(Placeholders.Image);
@@ -251,7 +253,7 @@ namespace QuestPDF.Examples
text.EmptyLine();
foreach (var i in Enumerable.Range(1, 1000))
foreach (var i in Enumerable.Range(1, 100))
{
text.Line($"{i}: {Placeholders.Paragraph()}");
-161
View File
@@ -1,161 +0,0 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Examples
{
public class TextShapingTests
{
[Test]
public void ShapeText()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
using var backgroundPaint = new SKPaint
{
Color = SKColors.LightGray
};
RenderingTest
.Create()
.PageSize(550, 250)
.ProduceImages()
.ShowResults()
.Render(container =>
{
//var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus.";
//var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا الواجب والعمل سنتنازل غالباً ونرفض الشعور";
var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit.";
var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
var text = arabic;
var metrics = textPaint.FontMetrics;
container
.Padding(25)
.Canvas((canvas, space) =>
{
canvas.Translate(0, 20);
var width = MeasureText(text, textPaint);
var widthReal = textPaint.MeasureText(text);
canvas.DrawRect(0, metrics.Descent, width, metrics.Ascent - metrics.Descent, backgroundPaint);
canvas.DrawShapedText(text, 0, 0, textPaint);
canvas.Translate(0, 40);
canvas.DrawText(text, 0, 0, textPaint);
});
});
}
float MeasureText(string text, SKPaint paint)
{
var font = paint.ToFont();
using var shaper = new SKShaper(paint.Typeface);
var result = shaper.Shape(text + " ", paint);
return Math.Max(result.Points.First().X, result.Points.Last().X);
var glyphCount = result.Codepoints.Length;
var glyphIds = new ushort[glyphCount];
var glyphWidths = new float[glyphCount];
var glyphBounds = new SKRect[glyphCount];
font.GetGlyphs(result.Codepoints.Select(x => (int)x).ToArray(), glyphIds);
font.GetGlyphWidths(glyphIds, glyphWidths, glyphBounds);
return glyphWidths.Sum();
return result.Points.Max(p => p.X);
using var skTextBlobBuilder = new SKTextBlobBuilder();
var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(font, result.Codepoints.Length);
var glyphSpan = positionedRunBuffer.GetGlyphSpan();
var positionSpan = positionedRunBuffer.GetPositionSpan();
for (int index = 0; index < result.Codepoints.Length; ++index)
{
glyphSpan[index] = (ushort) result.Codepoints[index];
positionSpan[index] = result.Points[index];
}
using var text1 = skTextBlobBuilder.Build();
return text1.Bounds.Width;
}
[Test]
public void ShapeSingle()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
var text = string.Join(" ", Enumerable.Range(0, 10_000).Select(x => Placeholders.Sentence()));
using var shaper = new SKShaper(textPaint.Typeface);
var result = shaper.Shape(text + " ", textPaint);
}
[Test]
public void ShapeMany()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
var text = string.Join(" ", Enumerable.Range(0, 1_000).Select(x => Placeholders.Sentence()));
foreach (var part in Regex.Split(text, "( )|(\\w+)"))
{
using var shaper = new SKShaper(textPaint.Typeface);
var result = shaper.Shape(part + " ", textPaint);
}
}
[Test]
public void ShapeMany2()
{
using var textPaint = new SKPaint
{
Color = SKColors.Black,
Typeface = SKTypeface.CreateDefault(),
IsAntialias = true,
TextSize = 20
};
using var shaper = new SKShaper(textPaint.Typeface);
foreach (var i in Enumerable.Range(0, 10_000))
{
var text = Placeholders.Paragraph();
textPaint.MeasureText(text);
var result = shaper.Shape(text + " ", textPaint);
}
}
}
}
@@ -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);
});
});
@@ -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;
}
@@ -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<ImagePlaceholder>();
});
@@ -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);
});
}
@@ -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);
});
});
}
@@ -19,7 +19,6 @@ 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 DrawShapedText(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 DrawHyperlink(string url, Size size) => throw new NotImplementedException();
@@ -16,7 +16,6 @@ namespace QuestPDF.UnitTests.TestEngine
public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
public void DrawShapedText(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 DrawHyperlink(string url, Size size) => throw new NotImplementedException();
+1 -1
View File
@@ -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) =>
+5 -18
View File
@@ -4,7 +4,6 @@ using System.IO;
using System.Linq;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
@@ -13,8 +12,6 @@ namespace QuestPDF.Drawing
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
private static ConcurrentDictionary<object, SKPaint> Paints = new();
private static ConcurrentDictionary<object, SKFont> Fonts = new();
private static ConcurrentDictionary<object, SKShaper> Shapers = new();
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new();
private static void RegisterFontType(SKData fontData, string? customName = null)
@@ -33,7 +30,7 @@ namespace QuestPDF.Drawing
}
}
[Obsolete("Since version 2022.3, the FontManager class offers better font type matching support. Please use the RegisterFontType(Stream stream) overload.")]
[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)
{
using var fontData = SKData.Create(stream);
@@ -41,7 +38,7 @@ namespace QuestPDF.Drawing
RegisterFontType(fontData, customName: fontName);
}
public static void RegisterFontType(Stream stream)
public static void RegisterFont(Stream stream)
{
using var fontData = SKData.Create(stream);
RegisterFontType(fontData);
@@ -81,35 +78,25 @@ namespace QuestPDF.Drawing
var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
if (StyleSets.TryGetValue(style.FontType, out var fontStyleSet))
if (StyleSets.TryGetValue(style.FontFamily, out var fontStyleSet))
return fontStyleSet.Match(fontStyle);
var fontFromDefaultSource = SKFontManager.Default.MatchFamily(style.FontType, fontStyle);
var fontFromDefaultSource = SKFontManager.Default.MatchFamily(style.FontFamily, fontStyle);
if (fontFromDefaultSource != null)
return fontFromDefaultSource;
throw new ArgumentException(
$"The typeface '{style.FontType}' could not be found. " +
$"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 SKFont ToFont(this TextStyle style)
{
return Fonts.GetOrAdd(style.FontMetricsKey, _ => style.ToPaint().Typeface.ToFont());
}
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
{
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
}
internal static SKShaper ToShaper(this TextStyle style)
{
return Shapers.GetOrAdd(style.FontMetricsKey, _ => new SKShaper(style.ToFont().Typeface));
}
}
}
-5
View File
@@ -44,11 +44,6 @@ namespace QuestPDF.Drawing
public void DrawText(string text, Position position, TextStyle style)
{
}
public void DrawShapedText(string text, Position position, TextStyle style)
{
}
public void DrawImage(SKImage image, Position position, Size size)
-6
View File
@@ -1,6 +1,5 @@
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
@@ -33,11 +32,6 @@ namespace QuestPDF.Drawing
Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint());
}
public void DrawShapedText(string text, Position vector, TextStyle style)
{
Canvas.DrawShapedText(text, vector.X, vector.Y, style.ToPaint());
}
public void DrawImage(SKImage image, Position vector, Size size)
{
Canvas.DrawImage(image, new SKRect(vector.X, vector.Y, size.Width, size.Height));
+4 -1
View File
@@ -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);
});
}
}
+48 -30
View File
@@ -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)
+1 -1
View File
@@ -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);
});
}
}
@@ -2,8 +2,14 @@
namespace QuestPDF.Elements.Text.Calculation
{
internal struct TextDrawingRequest
internal class TextDrawingRequest
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public float TotalAscent { get; set; }
public Size TextSize { get; set; }
}
@@ -5,6 +5,6 @@ namespace QuestPDF.Elements.Text.Calculation
internal class TextLineElement
{
public ITextBlockItem Item { get; set; }
public TextBlockSize Measurement { get; set; }
public TextMeasurementResult Measurement { get; set; }
}
}
@@ -0,0 +1,14 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextMeasurementRequest
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public int StartIndex { get; set; }
public float AvailableWidth { get; set; }
public bool IsFirstLineElement { get; set; }
}
}
@@ -2,7 +2,7 @@
namespace QuestPDF.Elements.Text.Calculation
{
internal class TextBlockSize
internal class TextMeasurementResult
{
public float Width { get; set; }
public float Height => Math.Abs(Descent) + Math.Abs(Ascent);
@@ -11,5 +11,12 @@ namespace QuestPDF.Elements.Text.Calculation
public float Descent { get; set; }
public float LineHeight { get; set; }
public int StartIndex { get; set; }
public int EndIndex { get; set; }
public int NextIndex { get; set; }
public int TotalIndex { get; set; }
public bool IsLast => EndIndex == TotalIndex;
}
}
@@ -5,10 +5,7 @@ namespace QuestPDF.Elements.Text.Items
{
internal interface ITextBlockItem
{
ICanvas Canvas { get; set; }
IPageContext PageContext { get; set; }
TextBlockSize? Measure();
TextMeasurementResult? Measure(TextMeasurementRequest request);
void Draw(TextDrawingRequest request);
}
}
@@ -7,40 +7,41 @@ namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockElement : ITextBlockItem
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public Element Element { get; set; } = Empty.Instance;
public TextBlockSize? Measure()
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
var measurement = Element.Measure(Size.Max);
var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
if (measurement.Type != SpacePlanType.FullRender)
return null;
return new TextBlockSize
return new TextMeasurementResult
{
Width = measurement.Width,
Ascent = -measurement.Height,
Descent = 0,
LineHeight = 1
LineHeight = 1,
StartIndex = 0,
EndIndex = 0,
TotalIndex = 0
};
}
public void Draw(TextDrawingRequest request)
{
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.VisitChildren(x => x.Initialize(PageContext, Canvas));
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.Translate(new Position(0, request.TotalAscent));
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
Canvas.Translate(new Position(0, -request.TotalAscent));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
}
}
}
@@ -6,12 +6,17 @@ namespace QuestPDF.Elements.Text.Items
internal class TextBlockHyperlink : TextBlockSpan
{
public string Url { get; set; }
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return MeasureWithoutCache(request);
}
public override void Draw(TextDrawingRequest request)
{
Canvas.Translate(new Position(0, request.TotalAscent));
Canvas.DrawHyperlink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
Canvas.Translate(new Position(0, -request.TotalAscent));
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawHyperlink(Url, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
@@ -9,21 +9,21 @@ namespace QuestPDF.Elements.Text.Items
public const string PageNumberPlaceholder = "123";
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
public override TextBlockSize? Measure()
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
SetPageNumber();
return base.Measure();
SetPageNumber(request.PageContext);
return MeasureWithoutCache(request);
}
public override void Draw(TextDrawingRequest request)
{
SetPageNumber();
SetPageNumber(request.PageContext);
base.Draw(request);
}
private void SetPageNumber()
private void SetPageNumber(IPageContext context)
{
Text = Source(PageContext) ?? PageNumberPlaceholder;
Text = Source(context) ?? PageNumberPlaceholder;
}
}
}
@@ -1,19 +0,0 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSectionLink : TextBlockSpan
{
public string SectionName { get; set; }
public override void Draw(TextDrawingRequest request)
{
Canvas.Translate(new Position(0, request.TotalAscent));
Canvas.DrawSectionLink(SectionName, new Size(request.TextSize.Width, request.TextSize.Height));
Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
@@ -0,0 +1,24 @@
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSectionlLink : TextBlockSpan
{
public string SectionName { get; set; }
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
return MeasureWithoutCache(request);
}
public override void Draw(TextDrawingRequest request)
{
request.Canvas.Translate(new Position(0, request.TotalAscent));
request.Canvas.DrawSectionLink(SectionName, new Size(request.TextSize.Width, request.TextSize.Height));
request.Canvas.Translate(new Position(0, -request.TotalAscent));
base.Draw(request);
}
}
}
+85 -55
View File
@@ -1,88 +1,118 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
using SkiaSharp.HarfBuzz;
using Size = QuestPDF.Infrastructure.Size;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSpan : ITextBlockItem
{
public ICanvas Canvas { get; set; }
public IPageContext PageContext { get; set; }
public string Text { get; set; }
public TextStyle Style { get; set; }
protected TextBlockSize? Size { get; set; }
protected bool RequiresShaping { get; set; }
public virtual TextBlockSize? Measure()
{
Size ??= Text == " " ? GetSizeForSpace() : GetSizeForWord();
return Size;
}
public TextStyle Style { get; set; } = new TextStyle();
private TextBlockSize GetSizeForWord()
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache =
new Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?>();
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
{
var cacheKey = (request.StartIndex, request.AvailableWidth);
if (!MeasureCache.ContainsKey(cacheKey))
MeasureCache[cacheKey] = MeasureWithoutCache(request);
return MeasureCache[cacheKey];
}
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
{
const char space = ' ';
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
var shaper = Style.ToShaper();
var startIndex = request.StartIndex;
// shaper returns positions of all glyphs,
// by adding a space, it is possible to capture width of the last original character
var result = shaper.Shape(Text + " ", paint);
// when text is left-to-right: last value corresponds to text width
// when text is right-to-left: glyphs are in the reverse order, first value represents text width
var width = Math.Max(result.Points.First().X, result.Points.Last().X);
if (request.IsFirstLineElement)
{
while (startIndex + 1 < Text.Length && Text[startIndex] == space)
startIndex++;
}
RequiresShaping = result.Points.Length != Text.Length + 1;
if (Text.Length == 0)
{
return new TextMeasurementResult
{
Width = 0,
LineHeight = Style.LineHeight ?? 1,
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent
};
}
return new TextBlockSize
// start breaking text from requested position
var text = Text.Substring(startIndex);
var textLength = (int)paint.BreakText(text, request.AvailableWidth);
if (textLength <= 0)
return null;
if (textLength < text.Length && text[textLength] == space)
textLength++;
// break text only on spaces
if (textLength < text.Length)
{
var lastSpaceIndex = text.Substring(0, textLength).LastIndexOf(space) - 1;
if (lastSpaceIndex <= 0)
{
if (!request.IsFirstLineElement)
return null;
}
else
{
textLength = lastSpaceIndex + 1;
}
}
text = text.Substring(0, textLength);
var endIndex = startIndex + textLength;
var nextIndex = endIndex;
while (nextIndex + 1 < Text.Length && Text[nextIndex] == space)
nextIndex++;
// measure final text
var width = paint.MeasureText(text);
return new TextMeasurementResult
{
Width = width,
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent,
LineHeight = Style.LineHeight ?? 1
LineHeight = Style.LineHeight ?? 1,
StartIndex = startIndex,
EndIndex = endIndex,
NextIndex = nextIndex,
TotalIndex = Text.Length
};
}
private TextBlockSize GetSizeForSpace()
{
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
return new TextBlockSize
{
Width = paint.MeasureText(" "),
Ascent = fontMetrics.Ascent,
Descent = fontMetrics.Descent,
LineHeight = Style.LineHeight ?? 1
};
}
public virtual void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
if (!string.IsNullOrWhiteSpace(Text))
{
if (RequiresShaping)
Canvas.DrawShapedText(Text, Position.Zero, Style);
else
Canvas.DrawText(Text, Position.Zero, Style);
}
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
request.Canvas.DrawText(text, Position.Zero, Style);
// draw underline
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
@@ -94,7 +124,7 @@ namespace QuestPDF.Elements.Text.Items
void DrawLine(float offset, float thickness)
{
Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
}
}
}
+77 -100
View File
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Elements.Text.Items;
@@ -12,145 +11,108 @@ namespace QuestPDF.Elements.Text
internal class TextBlock : Element, IStateResettable
{
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<ITextBlockItem> Items { get; set; } = new();
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
private Queue<ITextBlockItem> RenderingQueue { get; set; }
private int CurrentElementIndex { get; set; }
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
{
Items = SplitElementsBySpace().ToList();
Items.ForEach(x =>
{
x.PageContext = pageContext;
x.Canvas = canvas;
});
base.Initialize(pageContext, canvas);
}
public void ResetState()
{
RenderingQueue = new Queue<ITextBlockItem>(Items);
CurrentElementIndex = 0;
}
private IEnumerable<ITextBlockItem> SplitElementsBySpace()
{
foreach (var item in Items)
{
if (item is TextBlockSpan span)
{
span.Text ??= "";
foreach (var spanItem in Regex.Split(span.Text, "( )|([^ ]+)"))
{
yield return new TextBlockSpan
{
Text = spanItem,
Style = span.Style
};
}
}
}
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!Items.Any())
if (!RenderingQueue.Any())
return SpacePlan.FullRender(Size.Zero);
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
if (!lines.Any())
return SpacePlan.Wrap();
var width = lines.Max(x => x.Width);
var height = lines.Sum(x => x.LineHeight);
if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
return SpacePlan.Wrap();
if (CurrentElementIndex + lines.Sum(x => x.Elements.Count) == Items.Count)
return SpacePlan.FullRender(width, height);
var fullyRenderedItemsCount = lines
.SelectMany(x => x.Elements)
.GroupBy(x => x.Item)
.Count(x => x.Any(y => y.Measurement.IsLast));
if (fullyRenderedItemsCount == RenderingQueue.Count)
return SpacePlan.FullRender(width, height);
return SpacePlan.PartialRender(width, height);
}
internal override void Draw(Size availableSpace)
{
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
if (!lines.Any())
return;
var heightOffset = 0f;
var widthOffset = 0f;
var isLastPart = lines.Sum(x => x.Elements.Count) + CurrentElementIndex == Items.Count;
foreach (var sourceLine in lines)
foreach (var line in lines)
{
widthOffset = 0f;
var isLastLine = sourceLine == lines.Last();
var line = sourceLine;
if (!(isLastPart && isLastLine))
{
var spans = sourceLine.Elements
.Where(x => x.Item is TextBlockSpan)
.SkipWhile(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
.Reverse()
.SkipWhile(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
.Reverse()
.ToList();
var wordsWidth = spans
.Where(x => !string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text))
.Sum(x => x.Measurement.Width);
var spaceSpans = spans.Where(x => string.IsNullOrWhiteSpace((x.Item as TextBlockSpan).Text)).ToList();
var spaceWidth = (availableSpace.Width - wordsWidth) / spaceSpans.Count;
spaceSpans.ForEach(x => x.Measurement.Width = spaceWidth);
line = TextLine.From(spans);
}
var alignmentOffset = GetAlignmentOffset(line.Width);
Canvas.Translate(new Position(alignmentOffset, 0));
Canvas.Translate(new Position(0, -line.Ascent));
foreach (var item in line.Elements)
{
var textDrawingRequest = new TextDrawingRequest
{
Canvas = Canvas,
PageContext = PageContext,
StartIndex = item.Measurement.StartIndex,
EndIndex = item.Measurement.EndIndex,
TextSize = new Size(item.Measurement.Width, line.LineHeight),
TotalAscent = line.Ascent
};
item.Item.Draw(textDrawingRequest);
Canvas.Translate(new Position(item.Measurement.Width, 0));
widthOffset += item.Measurement.Width;
}
Canvas.Translate(new Position(-alignmentOffset, 0));
Canvas.Translate(new Position(-line.Width, line.Ascent));
Canvas.Translate(new Position(0, line.LineHeight));
heightOffset += line.LineHeight;
}
Canvas.Translate(new Position(0, -heightOffset));
CurrentElementIndex += lines.Sum(x => x.Elements.Count);
if (CurrentElementIndex == Items.Count)
ResetState();
Canvas.Translate(new Position(0, -heightOffset));
lines
.SelectMany(x => x.Elements)
.GroupBy(x => x.Item)
.Where(x => x.Any(y => y.Measurement.IsLast))
.Select(x => x.Key)
.ToList()
.ForEach(x => RenderingQueue.Dequeue());
var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
if (!RenderingQueue.Any())
ResetState();
float GetAlignmentOffset(float lineWidth)
{
if (Alignment == HorizontalAlignment.Left)
@@ -168,18 +130,19 @@ namespace QuestPDF.Elements.Text
}
}
private IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
{
var queue = new Queue<ITextBlockItem>(RenderingQueue);
var currentItemIndex = CurrentElementIndex;
var currentHeight = 0f;
while (true)
while (queue.Any())
{
var line = GetNextLine();
if (!line.Elements.Any())
yield break;
if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
yield break;
@@ -192,29 +155,43 @@ namespace QuestPDF.Elements.Text
var currentWidth = 0f;
var currentLineElements = new List<TextLineElement>();
while (true)
{
if (currentItemIndex == Items.Count)
if (!queue.Any())
break;
var currentElement = Items[currentItemIndex];
var textBlockSize = currentElement.Measure();
if (textBlockSize == null)
var currentElement = queue.Peek();
var measurementRequest = new TextMeasurementRequest
{
Canvas = Canvas,
PageContext = PageContext,
StartIndex = currentItemIndex,
AvailableWidth = availableWidth - currentWidth,
IsFirstLineElement = !currentLineElements.Any()
};
var measurementResponse = currentElement.Measure(measurementRequest);
if (measurementResponse == null)
break;
if (currentWidth + textBlockSize.Width > availableWidth + Size.Epsilon)
break;
currentLineElements.Add(new TextLineElement
{
Item = currentElement,
Measurement = textBlockSize
Measurement = measurementResponse
});
currentWidth += textBlockSize.Width;
currentItemIndex ++;
currentWidth += measurementResponse.Width;
currentItemIndex = measurementResponse.NextIndex;
if (!measurementResponse.IsLast)
break;
currentItemIndex = 0;
queue.Dequeue();
}
return TextLine.From(currentLineElements);
+9 -1
View File
@@ -177,7 +177,15 @@ namespace QuestPDF.Fluent
TextStyle = textStyle
});
}
public static IContainer DefaultTextStyle(this IContainer element, Func<TextStyle, TextStyle> handler)
{
return element.Element(new DefaultTextStyle
{
TextStyle = handler(TextStyle.Default)
});
}
public static IContainer StopPaging(this IContainer element)
{
return element.Element(new StopPaging());
+19
View File
@@ -83,11 +83,30 @@ namespace QuestPDF.Fluent
Page.DefaultTextStyle = textStyle;
}
public void DefaultTextStyle(Func<TextStyle, TextStyle> 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();
+97 -45
View File
@@ -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<TextBlock> TextBlocks { get; } = new List<TextBlock>();
@@ -22,6 +50,11 @@ namespace QuestPDF.Fluent
DefaultStyle = style;
}
public void DefaultTextStyle(Func<TextStyle, TextStyle> 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,102 +113,108 @@ 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<int?, string> DefaultTextFormat = x => (x ?? 123).ToString();
private void PageNumber(Func<IPageContext, int?> pageNumber, TextStyle? style, Func<int?, string>? format)
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> 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<int?, string>? format = null)
public TextPageNumberDescriptor CurrentPageNumber()
{
PageNumber(x => x.CurrentPage, style, format);
return PageNumber(x => x.CurrentPage);
}
public void TotalPages(TextStyle? style = null, Func<int?, string>? 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<int?, string>? 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<int?, string>? 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<int?, string>? 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<int?, string>? 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);
AddItemToLastTextBlock(new TextBlockSectionLink
if (IsNullOrEmpty(text))
return descriptor;
AddItemToLastTextBlock(new TextBlockSectionlLink
{
Style = style,
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 +222,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 +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;
}
}
}
@@ -0,0 +1,126 @@
using System;
using System.Runtime.CompilerServices;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class TextSpanDescriptorExtensions
{
public static T Style<T>(this T descriptor, TextStyle style) where T : TextSpanDescriptor
{
if (style == null)
return descriptor;
descriptor.TextStyle.OverrideStyle(style);
return descriptor;
}
public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
{
descriptor.TextStyle.Color = value;
return descriptor;
}
public static T BackgroundColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
{
descriptor.TextStyle.BackgroundColor = value;
return descriptor;
}
public static T FontFamily<T>(this T descriptor, string value) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontFamily = value;
return descriptor;
}
public static T FontSize<T>(this T descriptor, float value) where T : TextSpanDescriptor
{
descriptor.TextStyle.Size = value;
return descriptor;
}
public static T LineHeight<T>(this T descriptor, float value) where T : TextSpanDescriptor
{
descriptor.TextStyle.LineHeight = value;
return descriptor;
}
public static T Italic<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.IsItalic = value;
return descriptor;
}
public static T Strikethrough<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.HasStrikethrough = value;
return descriptor;
}
public static T Underline<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.HasUnderline = value;
return descriptor;
}
#region Weight
public static T Weight<T>(this T descriptor, FontWeight weight) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontWeight = weight;
return descriptor;
}
public static T Thin<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Thin);
}
public static T ExtraLight<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.ExtraLight);
}
public static T Light<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Light);
}
public static T NormalWeight<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Normal);
}
public static T Medium<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Medium);
}
public static T SemiBold<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.SemiBold);
}
public static T Bold<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Bold);
}
public static T ExtraBold<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.ExtraBold);
}
public static T Black<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Black);
}
public static T ExtraBlack<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.ExtraBlack);
}
#endregion
}
}
+19 -1
View File
@@ -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);
}
-1
View File
@@ -8,7 +8,6 @@ namespace QuestPDF.Infrastructure
void DrawRectangle(Position vector, Size size, string color);
void DrawText(string text, Position position, TextStyle style);
void DrawShapedText(string text, Position position, TextStyle style);
void DrawImage(SKImage image, Position position, Size size);
void DrawHyperlink(string url, Size size);
+18 -5
View File
@@ -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; }
@@ -24,7 +24,7 @@ namespace QuestPDF.Infrastructure
{
Color = Colors.Black,
BackgroundColor = Colors.Transparent,
FontType = Fonts.Calibri,
FontFamily = Fonts.Calibri,
Size = 12,
LineHeight = 1.2f,
FontWeight = Infrastructure.FontWeight.Normal,
@@ -43,15 +43,15 @@ namespace QuestPDF.Infrastructure
HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle);
PaintKey ??= (FontType, Size, FontWeight, IsItalic, Color);
FontMetricsKey ??= (FontType, Size, FontWeight, IsItalic);
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;
@@ -60,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();
+1 -2
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.2.3</Version>
<Version>2022.3.0</Version>
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>9</LangVersion>
@@ -25,7 +25,6 @@
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.3" />
</ItemGroup>
<ItemGroup>
+53 -184
View File
@@ -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
<PackageReference Include="QuestPDF" Version="2022.2.5" />
```
## 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).
<a href="https://github.com/QuestPDF/example-invoice">
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/invoice.png" width="595px">
</a>
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);
});
}
}
```
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/invoice.png" width="595px">