Compare commits

..

37 Commits

Author SHA1 Message Date
MarcinZiabek 69b54eed5d General concept of RTL support 2022-06-12 15:13:26 +02:00
MarcinZiabek 44b3ff8d0d Merge branch 'text-shaping-new' 2022-06-08 02:46:54 +02:00
Marcin Ziąbek df6b88e14a Update readme.md 2022-06-08 02:10:52 +02:00
MarcinZiabek d4103797aa Improved exception message when SkiaSharp throws the TypeInitializationException 2022-06-08 02:01:58 +02:00
MarcinZiabek d894914e7e Improved Previewer stability 2022-06-08 00:09:59 +02:00
MarcinZiabek 90d1ce7af7 2022.6.0-prerelease 2022-05-31 00:52:49 +02:00
MarcinZiabek 5fa32a06cc Text: fixed corner case 2022-05-26 00:42:50 +02:00
MarcinZiabek acdf1ed1aa Stability improvement 2022-05-25 00:21:50 +02:00
MarcinZiabek a0b32e5ecf Fixed Row.AutoItem width calculation 2022-05-25 00:21:50 +02:00
MarcinZiabek 539dc83303 Fixed Row.AutoItem width calculation 2022-05-24 23:50:53 +02:00
MarcinZiabek d97fa2cef1 Minor optimization of SkFont creation 2022-05-23 01:18:21 +02:00
MarcinZiabek 6d122c784d Implemented working text shaping 2022-05-22 20:25:11 +02:00
MarcinZiabek 28743a81d1 Fixed typo 2022-05-21 14:35:13 +02:00
MarcinZiabek 32b94cea43 Updated sponsorship 2022-05-12 00:40:29 +02:00
Marcin Ziąbek 0ca55a91e0 Update readme.md 2022-05-10 21:48:39 +02:00
MarcinZiabek 09e6b5253f Fixed class names 2022-05-10 12:43:13 +02:00
MarcinZiabek bacb136a3f Benchmark: grid element 2022-05-10 12:42:49 +02:00
MarcinZiabek b7bc640e31 Added new example to dynamic component: footer with alternating position 2022-05-09 18:18:06 +02:00
MarcinZiabek 6e6f884a5f Updated sponsorship section 2022-05-09 16:33:12 +02:00
MarcinZiabek b47a8ac409 Added more examples 2022-05-09 16:32:59 +02:00
MarcinZiabek e51082400c Dynamic Component: added more examples 2022-05-09 14:07:50 +02:00
MarcinZiabek 52cfa3ab7e Added note regarding JetBrains OSS Power Ups 2022-05-07 22:18:14 +02:00
MarcinZiabek cd181b1e2e Merge branch '2022.5' 2022-05-07 21:40:10 +02:00
Marcin Ziąbek e71601ad0c Merge pull request #208 from danielchalmers/patch-1
replace obsolete method in readme example
2022-05-07 18:46:09 +02:00
Daniel Chalmers 3041aa53cc replace obsolete method in readme example
Warning	CS0618	'PageDescriptor.Background(string)' is obsolete: 'This element has been renamed since version 2022.3. Please use the PageColor method.'
2022-05-06 19:37:17 -05:00
Marcin Ziąbek 9a7e397db2 Merge pull request #197 from knoxyz/patch-2
Replaced the obsolete methods from 2022.3 (FontType, Color and Size)
2022-05-05 18:23:11 +02:00
Marcin Ziąbek d81f21e658 Merge pull request #196 from knoxyz/patch-1
Update <TargetFramework> from 3.1 to net6.0
2022-05-05 18:11:21 +02:00
Marcin Ziąbek 78909c6edf Merge pull request #199 from knoxyz/patch-4
Replaced the obsolete methods from 2022.3 (InternalLink and Color)
2022-05-05 17:48:28 +02:00
Marcin Ziąbek a2d81b6adf Merge pull request #200 from knoxyz/patch-3
Replaced the obsolete methods from 2022.3 (ExternalLink and Location)
2022-05-05 17:47:40 +02:00
Marcin Ziąbek 4a761c023f Merge pull request #201 from simusr2/main
Replaced PageEnd with PageStart in PageNumberWithinSection
2022-05-05 17:46:30 +02:00
Simone Anile 24bbd06dff Replaced PageEnd with PageStart in PageNumberWithinSection 2022-04-29 15:19:58 +02:00
knoxyz 44bd646bab Replaced the obsolete methods from 2022.3 (InternalLink and Color) 2022-04-29 11:36:28 +02:00
knoxyz 5f0291b2c4 Replaced the obsolete methods from 2022.3 (ExternalLink and Location) 2022-04-29 11:32:20 +02:00
knoxyz 1b91015d8f Replaced the obsolete methods from 2022.3 (FontType, Color and Size) 2022-04-28 16:21:56 +02:00
knoxyz 2b6444463d Update <TargetFramework> from 3.1 to net6.0 2022-04-28 16:13:44 +02:00
Marcin Ziąbek e54e09de15 Merge pull request #185 from mikasius/main
Renamed logo.png
2022-04-23 23:03:37 +02:00
Sergey Metlenkin c49a424f87 Renamed logo.png
Otherwise solution won't build
2022-04-22 10:59:12 +07:00
57 changed files with 1138 additions and 218 deletions
+106
View File
@@ -0,0 +1,106 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public struct FibonacciHeaderState
{
public int Previous { get; set; }
public int Current { get; set; }
}
public class FibonacciHeader : IDynamicComponent<FibonacciHeaderState>
{
public FibonacciHeaderState State { get; set; }
public static readonly string[] ColorsTable =
{
Colors.Red.Lighten2,
Colors.Orange.Lighten2,
Colors.Green.Lighten2,
};
public FibonacciHeader(int previous, int current)
{
State = new FibonacciHeaderState
{
Previous = previous,
Current = current
};
}
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var content = context.CreateElement(container =>
{
var colorIndex = State.Current % ColorsTable.Length;
var color = ColorsTable[colorIndex];
var ratio = (float)State.Current / State.Previous;
container
.Background(color)
.Height(50)
.AlignMiddle()
.AlignCenter()
.Text($"{State.Current} / {State.Previous} = {ratio:N5}");
});
State = new FibonacciHeaderState
{
Previous = State.Current,
Current = State.Previous + State.Current
};
return new DynamicComponentComposeResult
{
Content = content,
HasMoreContent = false
};
}
}
public static class DynamicFibonacci
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.ShowResults()
.MaxPages(100)
.ProduceImages()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A6);
page.PageColor(Colors.White);
page.Margin(1, Unit.Centimetre);
page.DefaultTextStyle(x => x.FontSize(18));
page.Header().Dynamic(new FibonacciHeader(17, 19));
page.Content().Column(column =>
{
foreach (var i in Enumerable.Range(0, 50))
column.Item().PaddingTop(25).Background(Colors.Grey.Lighten2).Height(50);
});
page.Footer().AlignCenter().Text(text =>
{
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
});
});
});
}
}
}
@@ -21,12 +21,12 @@ namespace QuestPDF.Examples
public int ShownItemsCount { get; set; }
}
public class OrdersTable : IDynamicComponent<OrdersTableState>
public class OptimizedOrdersTable : IDynamicComponent<OrdersTableState>
{
private ICollection<OrderItem> Items { get; }
public OrdersTableState State { get; set; }
public OrdersTable(ICollection<OrderItem> items)
public OptimizedOrdersTable(ICollection<OrderItem> items)
{
Items = items;
@@ -69,7 +69,7 @@ namespace QuestPDF.Examples
return new DynamicComponentComposeResult
{
Content = content,
HasMoreContent = State.ShownItemsCount + rows.Count < Items.Count
HasMoreContent = State.ShownItemsCount < Items.Count
};
}
@@ -145,7 +145,7 @@ namespace QuestPDF.Examples
}
}
public static class DynamicExamples
public static class DynamicOptimizedExamples
{
[Test]
public static void Dynamic()
@@ -177,7 +177,7 @@ namespace QuestPDF.Examples
decoration
.Content()
.Dynamic(new OrdersTable(items));
.Dynamic(new OptimizedOrdersTable(items));
});
});
}
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class FooterWithAlternatingAlignment : IDynamicComponent<int>
{
public int State { get; set; }
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var content = context.CreateElement(element =>
{
element
.Element(x => context.PageNumber % 2 == 0 ? x.AlignLeft() : x.AlignRight())
.Text(x =>
{
x.CurrentPageNumber();
x.Span(" / ");
x.TotalPages();
});
});
return new DynamicComponentComposeResult()
{
Content = content,
HasMoreContent = false
};
}
}
public static class DynamicPageNumberLeftRightExamples
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.PageSize(PageSizes.A5)
.MaxPages(100)
.ShowResults()
.ProduceImages()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A6);
page.PageColor(Colors.White);
page.Margin(1, Unit.Centimetre);
page.DefaultTextStyle(x => x.FontSize(18));
page.Content().Column(column =>
{
foreach (var i in Enumerable.Range(0, 50))
column.Item().PaddingTop(25).Background(Colors.Grey.Lighten2).Height(50);
});
page.Footer().Dynamic(new FooterWithAlternatingAlignment());
});
});
}
}
}
@@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class ProgressHeader : IDynamicComponent<int>
{
public int State { get; set; }
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var content = context.CreateElement(container =>
{
var width = context.AvailableSize.Width * context.PageNumber / context.TotalPages;
container
.Background(Colors.Blue.Lighten2)
.Height(25)
.Width(width)
.Background(Colors.Blue.Darken1);
});
return new DynamicComponentComposeResult
{
Content = content,
HasMoreContent = false
};
}
}
public static class DynamicProgressHeader
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.ShowResults()
.MaxPages(100)
.ProduceImages()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A6);
page.Margin(1, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header().Dynamic(new ProgressHeader());
page.Content().Column(column =>
{
foreach (var i in Enumerable.Range(0, 100))
column.Item().PaddingTop(25).Background(Colors.Grey.Lighten2).Height(50);
});
page.Footer().AlignCenter().Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(20));
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
});
});
});
}
}
}
@@ -0,0 +1,157 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class OrdersTable : IDynamicComponent<OrdersTableState>
{
private IList<OrderItem> Items { get; }
public OrdersTableState State { get; set; }
public OrdersTable(IList<OrderItem> items)
{
Items = items;
State = new OrdersTableState
{
ShownItemsCount = 0
};
}
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var possibleItems = Enumerable
.Range(1, Items.Count - State.ShownItemsCount)
.Select(itemsToDisplay => ComposeContent(context, itemsToDisplay))
.TakeWhile(x => x.Size.Height <= context.AvailableSize.Height)
.ToList();
State = new OrdersTableState
{
ShownItemsCount = State.ShownItemsCount + possibleItems.Count
};
return new DynamicComponentComposeResult
{
Content = possibleItems.Last(),
HasMoreContent = State.ShownItemsCount < Items.Count
};
}
private IDynamicElement ComposeContent(DynamicContext context, int itemsToDisplay)
{
var total = Items.Skip(State.ShownItemsCount).Take(itemsToDisplay).Sum(x => x.Count * x.Price);
return context.CreateElement(container =>
{
container
.MinimalBox()
.Width(context.AvailableSize.Width)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(30);
columns.RelativeColumn();
columns.ConstantColumn(50);
columns.ConstantColumn(50);
columns.ConstantColumn(50);
});
table.Header(header =>
{
header.Cell().Element(Style).Text("#");
header.Cell().Element(Style).Text("Item name");
header.Cell().Element(Style).AlignRight().Text("Count");
header.Cell().Element(Style).AlignRight().Text("Price");
header.Cell().Element(Style).AlignRight().Text("Total");
IContainer Style(IContainer container)
{
return container
.DefaultTextStyle(x => x.SemiBold())
.BorderBottom(1)
.BorderColor(Colors.Grey.Darken2)
.Padding(5);
}
});
table.Footer(footer =>
{
footer
.Cell().ColumnSpan(5)
.AlignRight()
.PaddingTop(10)
.Text($"Subtotal: {total}$", TextStyle.Default.Bold());
});
foreach (var index in Enumerable.Range(State.ShownItemsCount, itemsToDisplay))
{
var item = Items[index];
table.Cell().Element(Style).Text(index + 1);
table.Cell().Element(Style).Text(item.ItemName);
table.Cell().Element(Style).AlignRight().Text(item.Count);
table.Cell().Element(Style).AlignRight().Text($"{item.Price}$");
table.Cell().Element(Style).AlignRight().Text($"{item.Count*item.Price}$");
IContainer Style(IContainer container)
{
return container
.BorderBottom(1)
.BorderColor(Colors.Grey.Lighten2)
.Padding(5);
}
}
});
});
}
}
public static class DynamicSimpleTableExample
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.PageSize(PageSizes.A5)
.ShowResults()
.ProduceImages()
.Render(container =>
{
var items = Enumerable.Range(0, 15).Select(x => new OrderItem()).ToList();
container
.Background(Colors.White)
.Padding(25)
.DefaultTextStyle(x => x.FontSize(16))
.Decoration(decoration =>
{
decoration
.Header()
.PaddingBottom(5)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Darken2));
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
});
decoration
.Content()
.Dynamic(new OrdersTable(items));
});
});
}
}
}
+42
View File
@@ -0,0 +1,42 @@
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
namespace QuestPDF.Examples
{
public class GridBenchmark
{
[Test]
public void Benchmark()
{
RenderingTest
.Create()
.ProducePdf()
.PageSize(PageSizes.A4)
.ShowResults()
.MaxPages(10_000)
.EnableCaching(true)
.EnableDebugging(false)
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.Grid(grid =>
{
const int numberOfRows = 100_000;
const int numberOfColumns = 10;
grid.Columns(numberOfColumns);
foreach (var row in Enumerable.Range(0, numberOfRows))
foreach (var column in Enumerable.Range(0, numberOfColumns))
grid.Item().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}");
});
});
}
}
}
+1 -1
View File
@@ -31,7 +31,7 @@ namespace QuestPDF.Examples
{
var chapters = GetBookChapters().ToList();
var results = PerformTest(16).ToList();
var results = PerformTest(128).ToList();
Console.WriteLine($"Min: {results.Min():F}");
Console.WriteLine($"Max: {results.Max():F}");
+115 -43
View File
@@ -12,11 +12,11 @@ namespace QuestPDF.Examples
public class TextExamples
{
[Test]
public void SimpleTextBlock()
public void SimpleText()
{
RenderingTest
.Create()
.PageSize(500, 300)
.PageSize(500, 100)
.ProduceImages()
.ShowResults()
@@ -27,6 +27,27 @@ namespace QuestPDF.Examples
.MinimalBox()
.Border(1)
.Padding(10)
.Text(Placeholders.Paragraph());
});
}
[Test]
public void SimpleTextBlock()
{
RenderingTest
.Create()
.PageSize(600, 300)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(5)
.MinimalBox()
.Border(1)
.MaxWidth(300)
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
@@ -38,48 +59,105 @@ namespace QuestPDF.Examples
}
[Test]
public void SuperscriptSubscript_General()
public void TextWeight()
{
RenderingTest
.Create()
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(5)
.Create()
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.MinimalBox()
.Border(1)
.Padding(10)
.Padding(20)
.Text(text =>
{
text.DefaultTextStyle(x => x.FontFamily(Fonts.Calibri).FontSize(20));
text.Line("Thin").Thin();
text.Line("ExtraLight").ExtraLight();
text.Line("Light").Light();
text.Line("NormalWeight").NormalWeight();
text.Line("Medium").Medium();
text.Line("SemiBold").SemiBold();
text.Line("Bold").Bold();
text.Line("ExtraBold").ExtraBold();
text.Line("Black").Black();
text.Line("ExtraBlack").ExtraBlack();
});
});
}
[Test]
public void LineHeight()
{
RenderingTest
.Create()
.PageSize(500, 700)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.Column(column =>
{
var lineHeights = new[] { 0.8f, 1f, 1.5f };
var paragraph = Placeholders.Paragraph();
foreach (var lineHeight in lineHeights)
{
column
.Item()
.Border(1)
.Padding(10)
.Text(paragraph)
.FontSize(16)
.LineHeight(lineHeight);
}
});
});
}
[Test]
public void SuperscriptSubscript_Simple()
{
RenderingTest
.Create()
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.MinimalBox()
.Border(1)
.Padding(20)
.Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(20));
text.ParagraphSpacing(2);
text.Span("In physics, massenergy equivalence is the relationship between mass and energy in a system's rest frame, where the two values differ only by a constant and the units of measurement.");
text.Span("[1][2]").Superscript();
text.Span(" The principle is described by the physicist Albert Einstein's famous formula: E = mc");
text.Span("2").Superscript();
text.Span(". ");
text.Span("[3]").Superscript();
text.EmptyLine();
text.Span("H");
text.Span("2").Subscript();
text.Span("O is the chemical formula for water, meaning that each of its molecules contains one oxygen and two hydrogen atoms.");
text.ParagraphSpacing(10);
var highlight = TextStyle.Default.BackgroundColor(Colors.Green.Lighten3);
text.Span("E=mc").Style(highlight);
text.Span("2").Superscript().Style(highlight);
text.Span(" is the equation of massenergy equivalence.");
text.EmptyLine();
text.Span("H");
text.Span("2").Subscript();
text.Span("O");
text.Span("H").Style(highlight);
text.Span("2").Subscript().Style(highlight);
text.Span("O").Style(highlight);
text.Span(" is the chemical formula for water.");
});
});
});
}
[Test]
public void SuperscriptSubscript_Effects()
{
@@ -134,7 +212,7 @@ namespace QuestPDF.Examples
{
RenderingTest
.Create()
.PageSize(500, 300)
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
@@ -146,13 +224,7 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.ParagraphSpacing(10);
foreach (var i in Enumerable.Range(1, 3))
{
text.Span($"Paragraph {i}: ").SemiBold();
text.Line(Placeholders.Paragraph());
}
text.Line(Placeholders.Paragraph());
});
});
}
@@ -331,7 +403,7 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
text.DefaultTextStyle(TextStyle.Default.FontSize(20).BackgroundColor(Colors.Red.Lighten4));
text.AlignLeft();
text.ParagraphSpacing(10);
@@ -350,7 +422,7 @@ namespace QuestPDF.Examples
{
text.Line($"{i}: {Placeholders.Paragraph()}");
text.Hyperlink("Please visit QuestPDF website", "https://www.questpdf.com");
text.Hyperlink("Please visit QuestPDF website. ", "https://www.questpdf.com");
text.Span("This is page number ");
text.CurrentPageNumber();
+47
View File
@@ -0,0 +1,47 @@
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
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 = "ينا الألم. في بعض (5000) الأحيان ونظراً للالتزامات التي يفرضها علينا";
container
.Padding(25)
.Text(arabic)
.FontSize(25);
});
}
}
}
+2 -1
View File
@@ -2,13 +2,14 @@
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
using QuestPDF.ReportSample;
using QuestPDF.ReportSample.Layouts;
//ImagePlaceholder.Solid = true;
// var model = DataSource.GetReport();
// var report = new StandardReport(model);
// report.ShowInPreviewer().Wait();
// report.ShowInPreviewer();
//
// return;
@@ -1,5 +1,6 @@
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -29,6 +30,7 @@ class CommunicationService
{
var builder = WebApplication.CreateBuilder();
builder.Services.AddLogging(x => x.ClearProviders());
builder.WebHost.UseKestrel(options => options.Limits.MaxRequestBodySize = null);
Application = builder.Build();
Application.MapGet("ping", HandlePing);
+1 -1
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.5.0</Version>
<Version>2022.6.0</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>questpdf-previewer</ToolCommandName>
<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>
@@ -33,10 +33,11 @@ namespace QuestPDF.ReportSample.Layouts
page.MarginHorizontal(50);
page.Size(PageSizes.A4);
page.ContentDirectionRightToLeft();
page.Header().Element(ComposeHeader);
page.Content().Element(ComposeContent);
page.Footer().AlignCenter().Text(text =>
{
text.CurrentPageNumber().Format(x => x?.FormatAsRomanNumeral() ?? "-----");
@@ -55,7 +56,7 @@ namespace QuestPDF.ReportSample.Layouts
row.Spacing(50);
row.RelativeItem().PaddingTop(-10).Text(Model.Title).Style(Typography.Title);
row.ConstantItem(90).ExternalLink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
row.ConstantItem(90).Hyperlink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
});
column.Item().ShowOnce().PaddingVertical(15).Border(1f).BorderColor(Colors.Grey.Lighten1).ExtendHorizontal();
@@ -88,14 +89,14 @@ namespace QuestPDF.ReportSample.Layouts
column.Item().PageBreak();
foreach (var section in Model.Sections)
column.Item().Location(section.Title).Component(new SectionTemplate(section));
column.Item().Section(section.Title).Component(new SectionTemplate(section));
column.Item().PageBreak();
column.Item().Location("Photos");
column.Item().Section("Photos");
foreach (var photo in Model.Photos)
column.Item().Component(new PhotoTemplate(photo));
});
}
}
}
}
@@ -3,6 +3,7 @@ using System.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.ReportSample.Layouts
{
@@ -41,18 +42,31 @@ namespace QuestPDF.ReportSample.Layouts
private void DrawLink(IContainer container, int number, string locationName)
{
container
.InternalLink(locationName)
.SectionLink(locationName)
.Row(row =>
{
row.ConstantItem(25).Text($"{number}.");
row.RelativeItem().Text(locationName);
row.ConstantItem(150).AlignRight().Text(text =>
row.ConstantItem(20).Text($"{number}.");
row.AutoItem().Text(locationName);
row.RelativeItem().PaddingHorizontal(2).AlignBottom().TranslateY(-3).Height(1).Canvas((canvas, space) =>
{
// best to statically cache
using var paint = new SKPaint
{
StrokeWidth = space.Height,
PathEffect = SKPathEffect.CreateDash(new float[] { 1, 3 }, 0)
};
canvas.DrawLine(0, 0, space.Width, 0, paint);
});
row.AutoItem().Text(text =>
{
text.BeginPageNumberOfSection(locationName);
text.Span(" - ");
text.EndPageNumberOfSection(locationName);
var lengthStyle = TextStyle.Default.Color(Colors.Grey.Medium);
var lengthStyle = TextStyle.Default.FontColor(Colors.Grey.Medium);
text.Span(" (").Style(lengthStyle);
text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x => x == 1 ? "1 page long" : $"{x} pages long");
@@ -61,4 +75,4 @@ namespace QuestPDF.ReportSample.Layouts
});
}
}
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<LangVersion>8</LangVersion>
<RootNamespace>QuestPDF.ReportSample</RootNamespace>

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+4 -4
View File
@@ -6,8 +6,8 @@ namespace QuestPDF.ReportSample
{
public static class Typography
{
public static TextStyle Title => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Darken3).Size(26).Black();
public static TextStyle Headline => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Medium).Size(16).SemiBold();
public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Verdana).Color(Colors.Black).Size(10).LineHeight(1.2f);
public static TextStyle Title => TextStyle.Default.FontFamily(Fonts.Calibri).FontColor(Colors.Blue.Darken3).FontSize(26).Black();
public static TextStyle Headline => TextStyle.Default.FontFamily(Fonts.Calibri).FontColor(Colors.Blue.Medium).FontSize(16).SemiBold();
public static TextStyle Normal => TextStyle.Default.FontFamily(Fonts.Verdana).FontColor(Colors.Black).FontSize(10).LineHeight(1.2f);
}
}
}
+2 -2
View File
@@ -1,4 +1,5 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
using SkiaSharp;
@@ -10,7 +11,6 @@ namespace QuestPDF.UnitTests.TestEngine
public Action<float> RotateFunc { get; set; }
public Action<float, float> ScaleFunc { get; set; }
public Action<SKImage, Position, Size> DrawImageFunc { get; set; }
public Action<string, Position, TextStyle> DrawTextFunc { get; set; }
public Action<Position, Size, string> DrawRectFunc { get; set; }
public void Translate(Position vector) => TranslateFunc(vector);
@@ -18,7 +18,7 @@ namespace QuestPDF.UnitTests.TestEngine
public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
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 DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
using QuestPDF.UnitTests.TestEngine.Operations;
using SkiaSharp;
@@ -15,7 +16,7 @@ namespace QuestPDF.UnitTests.TestEngine
public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY));
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 DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
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();
-18
View File
@@ -82,19 +82,6 @@ namespace QuestPDF.UnitTests.TestEngine
Assert.AreEqual(expected.Color, color, "Draw rectangle: color");
},
DrawTextFunc = (text, position, style) =>
{
var expected = GetExpected<CanvasDrawTextOperation>();
Assert.AreEqual(expected.Text, text);
Assert.AreEqual(expected.Position.X, position.X, "Draw text: X");
Assert.AreEqual(expected.Position.Y, position.Y, "Draw text: Y");
Assert.AreEqual(expected.Style.Color, style.Color, "Draw text: color");
Assert.AreEqual(expected.Style.FontFamily, style.FontFamily, "Draw text: font");
Assert.AreEqual(expected.Style.Size, style.Size, "Draw text: size");
},
DrawImageFunc = (image, position, size) =>
{
var expected = GetExpected<CanvasDrawImageOperation>();
@@ -201,11 +188,6 @@ namespace QuestPDF.UnitTests.TestEngine
return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
}
public TestPlan ExpectCanvasDrawText(string text, Position position, TextStyle style)
{
return AddOperation(new CanvasDrawTextOperation(text, position, style));
}
public TestPlan ExpectCanvasDrawImage(Position position, Size size)
{
return AddOperation(new CanvasDrawImageOperation(position, size));
+22 -5
View File
@@ -65,6 +65,7 @@ namespace QuestPDF.Drawing
document.Compose(container);
var content = container.Compose();
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
ApplyContentDirection(content);
var metadata = document.GetMetadata();
var pageContext = new PageContext();
@@ -135,11 +136,10 @@ namespace QuestPDF.Drawing
$"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " +
$"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
$"Please analyze your documents structure to detect this element and fix its size constraints.";
throw new DocumentLayoutException(message)
{
ElementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode."
};
var elementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode.";
throw new DocumentLayoutException(message, elementTrace);
}
}
@@ -200,5 +200,22 @@ namespace QuestPDF.Drawing
foreach (var child in content.GetChildren())
ApplyDefaultTextStyle(child, targetTextStyle);
}
internal static void ApplyContentDirection(this Element? content, ContentDirectionType contentDirectionType = ContentDirectionType.LeftToRight)
{
if (content == null)
return;
var targetDirection = contentDirectionType;
if (content is ContentDirection contentDirection)
targetDirection = contentDirection.Direction;
if (content is IContentDirectionAware contentDirectionAware)
contentDirectionAware.ContentDirection = targetDirection;
foreach (var child in content.GetChildren())
ApplyContentDirection(child, targetDirection);
}
}
}
@@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions
{
public class DocumentComposeException : Exception
{
public DocumentComposeException()
{
}
public DocumentComposeException(string message) : base(message)
{
}
public DocumentComposeException(string message, Exception inner) : base(message, inner)
internal DocumentComposeException(string message) : base(message)
{
}
@@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions
{
public class DocumentDrawingException : Exception
{
public DocumentDrawingException()
{
}
public DocumentDrawingException(string message) : base(message)
{
}
public DocumentDrawingException(string message, Exception inner) : base(message, inner)
internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
{
}
@@ -4,21 +4,11 @@ namespace QuestPDF.Drawing.Exceptions
{
public class DocumentLayoutException : Exception
{
public string ElementTrace { get; set; }
public DocumentLayoutException()
{
}
public string? ElementTrace { get; }
public DocumentLayoutException(string message) : base(message)
internal DocumentLayoutException(string message, string? elementTrace = null) : base(message)
{
}
public DocumentLayoutException(string message, Exception inner) : base(message, inner)
{
ElementTrace = elementTrace;
}
}
}
@@ -0,0 +1,20 @@
using System;
namespace QuestPDF.Drawing.Exceptions
{
public class InitializationException : Exception
{
internal InitializationException(string documentType, Exception innerException) : base(CreateMessage(documentType), innerException)
{
}
private static string CreateMessage(string documentType)
{
return $"Cannot create the {documentType} document using the SkiaSharp library. " +
$"This exception usually means that, on your operating system where you run the application, SkiaSharp requires installing additional dependencies. " +
$"Such dependencies are available as additional nuget packages, for example SkiaSharp.NativeAssets.Linux. " +
$"Please refer to the SkiaSharp documentation for more details.";
}
}
}
+42 -4
View File
@@ -2,9 +2,11 @@
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using HarfBuzzSharp;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
@@ -12,8 +14,11 @@ 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<string, SKPaint> ColorPaint = new();
private static ConcurrentDictionary<object, SKPaint> FontPaints = new();
private static ConcurrentDictionary<string, SKPaint> ColorPaints = new();
private static ConcurrentDictionary<object, Font> ShaperFonts = new();
private static ConcurrentDictionary<object, SKFont> Fonts = new();
private static ConcurrentDictionary<object, TextShaper> TextShapers = new();
private static void RegisterFontType(SKData fontData, string? customName = null)
{
@@ -47,7 +52,7 @@ namespace QuestPDF.Drawing
internal static SKPaint ColorToPaint(this string color)
{
return ColorPaint.GetOrAdd(color, Convert);
return ColorPaints.GetOrAdd(color, Convert);
static SKPaint Convert(string color)
{
@@ -61,7 +66,7 @@ namespace QuestPDF.Drawing
internal static SKPaint ToPaint(this TextStyle style)
{
return Paints.GetOrAdd(style.PaintKey, key => Convert(style));
return FontPaints.GetOrAdd(style.PaintKey, key => Convert(style));
static SKPaint Convert(TextStyle style)
{
@@ -122,5 +127,38 @@ namespace QuestPDF.Drawing
{
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
}
internal static Font ToShaperFont(this TextStyle style)
{
return ShaperFonts.GetOrAdd(style.PaintKey, _ =>
{
var typeface = style.ToPaint().Typeface;
using var harfBuzzBlob = typeface.OpenStream(out var ttcIndex).ToHarfBuzzBlob();
using var face = new Face(harfBuzzBlob, ttcIndex)
{
Index = ttcIndex,
UnitsPerEm = typeface.UnitsPerEm,
GlyphCount = typeface.GlyphCount
};
var font = new Font(face);
font.SetScale(TextShaper.FontShapingScale, TextShaper.FontShapingScale);
font.SetFunctionsOpenType();
return font;
});
}
internal static TextShaper ToTextShaper(this TextStyle style)
{
return TextShapers.GetOrAdd(style.PaintKey, _ => new TextShaper(style));
}
internal static SKFont FoFont(this TextStyle style)
{
return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont());
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ namespace QuestPDF.Drawing
}
public void DrawText(string text, Position position, TextStyle style)
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
{
}
+17 -3
View File
@@ -1,4 +1,6 @@
using System.IO;
using System;
using System.IO;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Helpers;
using SkiaSharp;
@@ -7,11 +9,23 @@ namespace QuestPDF.Drawing
internal class PdfCanvas : SkiaDocumentCanvasBase
{
public PdfCanvas(Stream stream, DocumentMetadata documentMetadata)
: base(SKDocument.CreatePdf(stream, MapMetadata(documentMetadata)))
: base(CreatePdf(stream, documentMetadata))
{
}
private static SKDocument CreatePdf(Stream stream, DocumentMetadata documentMetadata)
{
try
{
return SKDocument.CreatePdf(stream, MapMetadata(documentMetadata));
}
catch (TypeInitializationException exception)
{
throw new InitializationException("PDF", exception);
}
}
private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata)
{
return new SKDocumentPdfMetadata
+9
View File
@@ -0,0 +1,9 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class DirectionProxy : ContainerElement
{
}
}
+3 -2
View File
@@ -1,5 +1,6 @@
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
@@ -27,9 +28,9 @@ namespace QuestPDF.Drawing
Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint);
}
public void DrawText(string text, Position vector, TextStyle style)
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
{
Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint());
Canvas.DrawText(skTextBlob, position.X, position.Y, style.ToPaint());
}
public void DrawImage(SKImage image, Position vector, Size size)
+156
View File
@@ -0,0 +1,156 @@
using System;
using HarfBuzzSharp;
using QuestPDF.Infrastructure;
using SkiaSharp;
using Buffer = HarfBuzzSharp.Buffer;
namespace QuestPDF.Drawing
{
internal class TextShaper
{
public const int FontShapingScale = 512;
private Font Font { get; }
private SKPaint Paint { get; }
public TextShaper(TextStyle style)
{
Font = style.ToShaperFont();
Paint = style.ToPaint();
}
public TextShapingResult Shape(string text)
{
var buffer = new Buffer();
PopulateBufferWithText(buffer, text);
buffer.GuessSegmentProperties();
//buffer.Direction = Direction.RightToLeft;
Font.Shape(buffer);
var length = buffer.Length;
var glyphInfos = buffer.GlyphInfos;
var glyphPositions = buffer.GlyphPositions;
var scaleY = Paint.TextSize / FontShapingScale;
var scaleX = scaleY * Paint.TextScaleX;
var xOffset = 0f;
var yOffset = 0f;
var glyphs = new ShapedGlyph[length];
for (var i = 0; i < length; i++)
{
glyphs[i] = new ShapedGlyph
{
Codepoint = (ushort)glyphInfos[i].Codepoint,
Position = new SKPoint(xOffset + glyphPositions[i].XOffset * scaleX, yOffset - glyphPositions[i].YOffset * scaleY),
Width = glyphPositions[i].XAdvance * scaleX
};
xOffset += glyphPositions[i].XAdvance * scaleX;
yOffset += glyphPositions[i].YAdvance * scaleY;
}
return new TextShapingResult(glyphs);
}
void PopulateBufferWithText(Buffer buffer, string text)
{
var encoding = Paint.TextEncoding;
if (encoding == SKTextEncoding.Utf8)
buffer.AddUtf8(text);
else if (encoding == SKTextEncoding.Utf16)
buffer.AddUtf16(text);
else if (encoding == SKTextEncoding.Utf32)
buffer.AddUtf32(text);
else
throw new NotSupportedException("TextEncoding of type GlyphId is not supported.");
}
}
internal struct ShapedGlyph
{
public ushort Codepoint;
public SKPoint Position;
public float Width;
}
internal struct DrawTextCommand
{
public SKTextBlob SkTextBlob;
public float TextOffsetX;
}
internal class TextShapingResult
{
public ShapedGlyph[] Glyphs { get; }
public TextShapingResult(ShapedGlyph[] glyphs)
{
Glyphs = glyphs;
}
public int BreakText(int startIndex, float maxWidth)
{
var index = startIndex;
maxWidth += Glyphs[startIndex].Position.X;
while (index < Glyphs.Length)
{
var glyph = Glyphs[index];
if (glyph.Position.X + glyph.Width > maxWidth + Size.Epsilon)
break;
index++;
}
return index - 1;
}
public float MeasureWidth(int startIndex, int endIndex)
{
if (Glyphs.Length == 0)
return 0;
var start = Glyphs[startIndex];
var end = Glyphs[endIndex];
return end.Position.X - start.Position.X + end.Width;
}
public DrawTextCommand? PositionText(int startIndex, int endIndex, TextStyle textStyle)
{
if (Glyphs.Length == 0)
return null;
using var skTextBlobBuilder = new SKTextBlobBuilder();
var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(textStyle.FoFont(), endIndex - startIndex + 1);
var glyphSpan = positionedRunBuffer.GetGlyphSpan();
var positionSpan = positionedRunBuffer.GetPositionSpan();
for (var sourceIndex = startIndex; sourceIndex <= endIndex; sourceIndex++)
{
var runIndex = sourceIndex - startIndex;
glyphSpan[runIndex] = Glyphs[sourceIndex].Codepoint;
positionSpan[runIndex] = Glyphs[sourceIndex].Position;
}
return new DrawTextCommand
{
SkTextBlob = skTextBlobBuilder.Build(),
TextOffsetX = -Glyphs[startIndex].Position.X
};
}
}
}
+16 -2
View File
@@ -1,4 +1,6 @@
using System.IO;
using System;
using System.IO;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Helpers;
using SkiaSharp;
@@ -7,9 +9,21 @@ namespace QuestPDF.Drawing
internal class XpsCanvas : SkiaDocumentCanvasBase
{
public XpsCanvas(Stream stream, DocumentMetadata documentMetadata)
: base(SKDocument.CreateXps(stream, documentMetadata.RasterDpi))
: base(CreateXps(stream, documentMetadata))
{
}
private static SKDocument CreateXps(Stream stream, DocumentMetadata documentMetadata)
{
try
{
return SKDocument.CreateXps(stream, documentMetadata.RasterDpi);
}
catch (TypeInitializationException exception)
{
throw new InitializationException("XPS", exception);
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class ContentDirection : ContainerElement
{
public ContentDirectionType Direction { get; set; }
}
}
+2
View File
@@ -52,6 +52,7 @@ namespace QuestPDF.Elements
var context = new DynamicContext
{
PageNumber = PageContext.CurrentPage,
TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd,
PageContext = PageContext,
Canvas = Canvas,
TextStyle = TextStyle,
@@ -75,6 +76,7 @@ namespace QuestPDF.Elements
internal TextStyle TextStyle { get; set; }
public int PageNumber { get; internal set; }
public int TotalPages { get; internal set; }
public Size AvailableSize { get; internal set; }
public IDynamicElement CreateElement(Action<IContainer> content)
+3
View File
@@ -19,6 +19,8 @@ namespace QuestPDF.Elements
public float MarginBottom { get; set; }
public string BackgroundColor { get; set; } = Colors.Transparent;
public ContentDirectionType ContentDirection { get; set; } = ContentDirectionType.LeftToRight;
public Element Background { get; set; } = Empty.Instance;
public Element Foreground { get; set; } = Empty.Instance;
@@ -30,6 +32,7 @@ namespace QuestPDF.Elements
public void Compose(IContainer container)
{
container
.ContentDirection(ContentDirection)
.Background(BackgroundColor)
.Layers(layers =>
{
+5 -13
View File
@@ -102,13 +102,14 @@ namespace QuestPDF.Elements
private void UpdateItemsWidth(float availableWidth)
{
HandleItemsWithAutoWidth();
foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto))
rowItem.Size = rowItem.Measure(Size.Max).Width;
var constantWidth = Items.Where(x => x.Type == RowItemType.Constant).Sum(x => x.Size);
var constantWidth = Items.Where(x => x.Type != RowItemType.Relative).Sum(x => x.Size);
var relativeWidth = Items.Where(x => x.Type == RowItemType.Relative).Sum(x => x.Size);
var spacingWidth = (Items.Count - 1) * Spacing;
foreach (var item in Items.Where(x => x.Type == RowItemType.Constant))
foreach (var item in Items.Where(x => x.Type != RowItemType.Relative))
item.Width = item.Size;
if (relativeWidth <= 0)
@@ -119,16 +120,7 @@ namespace QuestPDF.Elements
foreach (var item in Items.Where(x => x.Type == RowItemType.Relative))
item.Width = item.Size * widthPerRelativeUnit;
}
private void HandleItemsWithAutoWidth()
{
foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto))
{
rowItem.Size = rowItem.Measure(Size.Max).Width;
rowItem.Type = RowItemType.Constant;
}
}
private ICollection<RowItemRenderingCommand> PlanLayout(Size availableSpace)
{
var leftOffset = 0f;
@@ -5,7 +5,7 @@ using QuestPDF.Fluent;
namespace QuestPDF.Elements.Table
{
static class TableLayoutValidator
static class TableLayoutPlanner
{
public static void PlanCellPositions(this Table table)
{
@@ -6,7 +6,7 @@ using QuestPDF.Fluent;
namespace QuestPDF.Elements.Table
{
static class TableLayoutPlanner
static class TableLayoutValidator
{
public static void ValidateCellPositions(this Table table)
{
@@ -1,4 +1,5 @@
using QuestPDF.Infrastructure;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Calculation
{
@@ -1,4 +1,5 @@
using System;
using QuestPDF.Drawing;
namespace QuestPDF.Elements.Text.Calculation
{
@@ -8,6 +8,7 @@ namespace QuestPDF.Elements.Text.Items
{
public const string PageNumberPlaceholder = "123";
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
protected override bool EnableTextCache => false;
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
@@ -3,7 +3,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSectionlLink : TextBlockSpan
internal class TextBlockSectionLink : TextBlockSpan
{
public string SectionName { get; set; }
+57 -41
View File
@@ -1,20 +1,24 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
using Size = QuestPDF.Infrastructure.Size;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSpan : ITextBlockItem
{
private const char Space = ' ';
public string Text { get; set; }
public TextStyle Style { get; set; } = new();
public TextShapingResult? TextShapingResult { get; set; }
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
protected virtual bool EnableTextCache => true;
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
{
@@ -25,11 +29,17 @@ namespace QuestPDF.Elements.Text.Items
return MeasureCache[cacheKey];
}
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
{
if (!EnableTextCache)
TextShapingResult = null;
TextShapingResult ??= Style.ToTextShaper().Shape(Text);
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
var spaceCodepoint = paint.ToFont().Typeface.GetGlyphs(" ")[0];
var startIndex = request.StartIndex;
@@ -37,11 +47,11 @@ namespace QuestPDF.Elements.Text.Items
// ignore leading spaces
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
{
while (startIndex < Text.Length && Text[startIndex] == Space)
while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint)
startIndex++;
}
if (Text.Length == 0 || startIndex == Text.Length)
if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length)
{
return new TextMeasurementResult
{
@@ -54,27 +64,19 @@ namespace QuestPDF.Elements.Text.Items
}
// start breaking text from requested position
var text = Text.AsSpan().Slice(startIndex);
var textLength = (int)paint.BreakText(text, request.AvailableWidth + Size.Epsilon);
var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
if (textLength <= 0)
if (endIndex < 0)
return null;
// break text only on spaces
var wrappedTextLength = WrapText(text, textLength, request.IsFirstElementInLine);
var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine);
if (wrappedTextLength == null)
if (wrappedText == null)
return null;
textLength = wrappedTextLength.Value.fragmentLength;
text = text.Slice(0, textLength);
var endIndex = startIndex + textLength;
// measure final text
var width = paint.MeasureText(text);
var width = TextShapingResult.MeasureWidth(startIndex, wrappedText.Value.endIndex);
return new TextMeasurementResult
{
@@ -86,56 +88,70 @@ namespace QuestPDF.Elements.Text.Items
LineHeight = Style.LineHeight ?? 1,
StartIndex = startIndex,
EndIndex = endIndex,
NextIndex = startIndex + wrappedTextLength.Value.nextIndex,
TotalIndex = Text.Length
EndIndex = wrappedText.Value.endIndex,
NextIndex = wrappedText.Value.nextIndex,
TotalIndex = TextShapingResult.Glyphs.Length - 1
};
}
// TODO: consider introduce text wrapping abstraction (basic, english-like, asian-like)
private (int fragmentLength, int nextIndex)? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
// TODO: consider introducing text wrapping abstraction (basic, english-like, asian-like)
private (int endIndex, int nextIndex)? WrapText(int startIndex, int endIndex, bool isFirstElementInLine)
{
var spaceCodepoint = Style.ToPaint().ToFont().Typeface.GetGlyphs(" ")[0];
// textLength - length of the part of the text that fits in available width (creating a line)
// entire text fits, no need to wrap
if (textLength == text.Length)
return (textLength, textLength + 1);
if (endIndex == TextShapingResult.Glyphs.Length - 1)
return (endIndex, endIndex);
// breaking anywhere
if (Style.WrapAnywhere ?? false)
return (textLength, textLength);
return (endIndex, endIndex + 1);
// current line ends at word, next character is space, perfect place to wrap
if (text[textLength - 1] != Space && text[textLength] == Space)
return (textLength, textLength + 1);
if (TextShapingResult.Glyphs[endIndex].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex + 1].Codepoint == spaceCodepoint)
return (endIndex, endIndex + 2);
// find last space within the available text to wrap
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(Space);
var lastSpaceIndex = endIndex;
while (lastSpaceIndex >= startIndex)
{
if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint)
break;
lastSpaceIndex--;
}
// text contains space that can be used to wrap
if (lastSpaceIndex > 0)
return (lastSpaceIndex, lastSpaceIndex + 1);
if (lastSpaceIndex >= startIndex)
return (lastSpaceIndex - 1, lastSpaceIndex + 1);
// there is no available space to wrap text
// if the item is first within the line, perform safe mode and chop the word
// otherwise, move the item into the next line
return isFirstElementInLine ? (textLength, textLength) : null;
return isFirstElementInLine ? (endIndex, endIndex + 1) : null;
}
public virtual void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
var glyphOffset = GetGlyphOffset();
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
var glyphOffsetY = GetGlyphOffset();
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
request.Canvas.DrawText(text, new Position(0, glyphOffset), Style);
var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex, Style);
if (Style.BackgroundColor != Colors.Transparent)
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
if (textDrawingCommand.HasValue)
request.Canvas.DrawText(textDrawingCommand.Value.SkTextBlob, new Position(textDrawingCommand.Value.TextOffsetX, glyphOffsetY), Style);
// draw underline
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
{
var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffset;
var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffsetY;
DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1);
}
@@ -145,7 +161,7 @@ namespace QuestPDF.Elements.Text.Items
var strikeoutThickness = fontMetrics.StrikeoutThickness ?? 1;
strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f;
DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffset, strikeoutThickness);
DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffsetY, strikeoutThickness);
}
void DrawLine(float offset, float thickness)
+9 -3
View File
@@ -8,11 +8,13 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text
{
internal class TextBlock : Element, IStateResettable
internal class TextBlock : Element, IContentDirectionAware, IStateResettable
{
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
public ContentDirectionType ContentDirection { get; set; }
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
private Queue<ITextBlockItem> RenderingQueue { get; set; }
@@ -69,8 +71,12 @@ namespace QuestPDF.Elements.Text
Canvas.Translate(new Position(alignmentOffset, 0));
Canvas.Translate(new Position(0, -line.Ascent));
foreach (var item in line.Elements)
var elements = ContentDirection == ContentDirectionType.LeftToRight
? line.Elements
: line.Elements.Reverse();
foreach (var item in elements)
{
var textDrawingRequest = new TextDrawingRequest
{
@@ -0,0 +1,29 @@
using System.Linq.Expressions;
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class ContentDirectionExtensions
{
internal static IContainer ContentDirection(this IContainer element, ContentDirectionType contentDirectionType)
{
var contentDirection = new ContentDirection
{
Direction = contentDirectionType
};
return element.Element(contentDirection);
}
public static IContainer ContentDirectionLeftToRight(this IContainer element)
{
return element.ContentDirection(ContentDirectionType.LeftToRight);
}
public static IContainer ContentDirectionRightToLeft(this IContainer element)
{
return element.ContentDirection(ContentDirectionType.RightToLeft);
}
}
}
+10
View File
@@ -88,6 +88,16 @@ namespace QuestPDF.Fluent
DefaultTextStyle(handler(TextStyle.Default));
}
public void ContentDirectionLeftToRight()
{
Page.ContentDirection = ContentDirectionType.LeftToRight;
}
public void ContentDirectionRightToLeft()
{
Page.ContentDirection = ContentDirectionType.RightToLeft;
}
public void PageColor(string color)
{
Page.BackgroundColor = color;
+2 -2
View File
@@ -174,7 +174,7 @@ namespace QuestPDF.Fluent
public TextPageNumberDescriptor PageNumberWithinSection(string locationName)
{
return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd);
return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageStart);
}
public TextPageNumberDescriptor TotalPagesWithinSection(string locationName)
@@ -193,7 +193,7 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(text))
return descriptor;
AddItemToLastTextBlock(new TextBlockSectionlLink
AddItemToLastTextBlock(new TextBlockSectionLink
{
Style = style,
Text = text,
+1 -1
View File
@@ -7,7 +7,7 @@ namespace QuestPDF.Helpers
{
public static class Placeholders
{
public static readonly Random Random = new();
public static readonly Random Random = new Random(0);
#region Word Cache
@@ -0,0 +1,8 @@
namespace QuestPDF.Infrastructure
{
public enum ContentDirectionType
{
LeftToRight,
RightToLeft
}
}
@@ -4,6 +4,9 @@ namespace QuestPDF.Infrastructure
{
Left,
Center,
Right
Right,
Start,
End
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
using QuestPDF.Drawing;
using SkiaSharp;
namespace QuestPDF.Infrastructure
@@ -7,7 +8,7 @@ namespace QuestPDF.Infrastructure
void Translate(Position vector);
void DrawRectangle(Position vector, Size size, string color);
void DrawText(string text, Position position, TextStyle style);
void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style);
void DrawImage(SKImage image, Position position, Size size);
void DrawHyperlink(string url, Size size);
@@ -0,0 +1,7 @@
namespace QuestPDF.Infrastructure
{
internal interface IContentDirectionAware
{
public ContentDirectionType ContentDirection { get; set; }
}
}
+1 -1
View File
@@ -94,7 +94,7 @@ namespace QuestPDF.Previewer
private void CheckVersionCompatibility(Version version)
{
if (version.Major == 2022 && version.Minor == 5)
if (version.Major == 2022 && version.Minor == 6)
return;
throw new Exception($"Previewer version is not compatible. Possible solutions: " +
+3 -4
View File
@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.5.0</Version>
<Version>2022.6.0-prerelease</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>
@@ -24,7 +23,8 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.*" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.3" />
</ItemGroup>
<ItemGroup>
@@ -45,5 +45,4 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>
+4 -4
View File
@@ -1,4 +1,4 @@
- Implemented the DynamicComponent element (useful for most advanced cases, e.g. per-page totals),
- Extend text rendering capabilities by adding subscript and superscript effects,
- Improved table rendering performance,
- Previewer tool stability fixes.
Implemented support for the text shaping algorithm that fixes rendering more advanced languages such as Arabic.
Improved exception message when SkiaSharp throws the TypeInitializationException (when additional dependencies are needed).
Fixed: a rare case when the Row.AutoItem does not calculate properly the width of its content.
Fixed: the QuestPDF Previewer does not work with content-rich documents.
+1
View File
@@ -0,0 +1 @@
<svg height="180" viewBox="0 0 180 180" width="180" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="32.64" x2="82.77" y1="61.16" y2="85.54"><stop offset=".21" stop-color="#fe2857"/><stop offset="1" stop-color="#293896"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="17.38" x2="82.95" y1="69.86" y2="21.23"><stop offset="0" stop-color="#fe2857"/><stop offset=".01" stop-color="#fe2857"/><stop offset=".86" stop-color="#ff318c"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="74.17" x2="160.27" y1="21.58" y2="99.76"><stop offset=".02" stop-color="#ff318c"/><stop offset=".21" stop-color="#fe2857"/><stop offset=".86" stop-color="#fdb60d"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="155.46" x2="55.07" y1="89.8" y2="158.9"><stop offset=".01" stop-color="#fdb60d"/><stop offset=".86" stop-color="#fcf84a"/></linearGradient><path d="m81.56 83.71-41.35-35a15 15 0 1 0 -14.47 25.7h.15l.39.12 52.16 15.89a3.53 3.53 0 0 0 1.18.21 3.73 3.73 0 0 0 1.93-6.91z" fill="url(#a)"/><path d="m89.85 25.93a10.89 10.89 0 0 0 -16.85-9.18l-50.5 30.66a15 15 0 1 0 17.9 24l45.27-36.89.36-.3a10.93 10.93 0 0 0 3.82-8.29z" fill="url(#b)"/><path d="m163.29 92-76.62-73.79a10.91 10.91 0 1 0 -14.81 16l.14.12 81.4 68.58a7.36 7.36 0 0 0 12.09-5.65 7.39 7.39 0 0 0 -2.2-5.26z" fill="url(#c)"/><path d="m165.5 97.29a7.35 7.35 0 0 0 -11.67-6l-92.71 45.3a15 15 0 1 0 15.48 25.59l85.73-58.84a7.35 7.35 0 0 0 3.17-6.05z" fill="url(#d)"/><path d="m60 60h60v60h-60z"/><g fill="#fff"><path d="m66.53 108.75h22.5v3.75h-22.5z"/><path d="m65.59 75.47 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.45 1.06-1.32v-5.92h2.58v5.94a3.44 3.44 0 0 1 -.92 2.63 3.52 3.52 0 0 1 -2.57 1 3.84 3.84 0 0 1 -3.29-1.62z"/><path d="m73.53 67.52h7.53v2.19h-5v1.43h4.49v2h-4.45v1.49h5v2.2h-7.6z"/><path d="m84.73 69.79h-2.8v-2.27h8.21v2.27h-2.81v7.09h-2.6z"/><path d="m66.63 80.58h4.42a3.47 3.47 0 0 1 2.55.83 2.09 2.09 0 0 1 .61 1.52 2.18 2.18 0 0 1 -1.45 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.69-1.31 2.69-3.55 2.69h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.25c.78 0 1.24-.27 1.24-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86-.02-.53-.4-.87-1.27-.87z"/><path d="m75.45 80.58h4.15a4.14 4.14 0 0 1 3.05 1 2.92 2.92 0 0 1 .83 2.18 3 3 0 0 1 -1.93 2.89l2.24 3.35h-3l-1.89-2.84h-.87v2.84h-2.6zm4 4.5c.87 0 1.4-.43 1.4-1.12 0-.75-.55-1.13-1.41-1.13h-1.39v2.27z"/><path d="m87.09 80.51h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.74h-2.71zm2.28 5.73-1.05-2.65-1.06 2.65z"/><path d="m94 80.55h2.6v9.37h-2.6z"/><path d="m97.56 80.55h2.44l3.37 5v-5h2.57v9.37h-2.27l-3.53-5.14v5.14h-2.58z"/><path d="m106.37 88.53 1.44-1.73a4.86 4.86 0 0 0 3 1.13c.71 0 1.08-.25 1.08-.65 0-.41-.3-.61-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.38-3 3.63-3a5.88 5.88 0 0 1 3.85 1.25l-1.25 1.78a4.56 4.56 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .43.32.62 1.63.91 2.15.47 3.48 1.17 3.48 2.92 0 1.91-1.51 3-3.78 3a6.56 6.56 0 0 1 -4.4-1.45z"/></g><path d="m0 0h180v180h-180z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+25 -3
View File
@@ -37,7 +37,13 @@ As an open-source project without funding, I cannot afford advertising QuestPDF
It doesn't matter if you are a professional developer, creating a startup or work for an established company. All of us care about our tools and dependencies, about stability and security, about time and money we can safe, about quality we can offer. Please consider sponsoring QuestPDF to give me an extra motivational push to develop the next great feature.
> If you represent a company, want to help the entire community and show that you care, please consider sponsoring QuestPDF using one of the higher tiers. Your company logo will be shown here for all developers, building a strong positive relation.
> If you represent a company and want to help the entire community, please consider sponsoring QuestPDF using one of the higher tiers. All developers will see your company logo and the description of your choice. It is a fantastic way to build a strong relationship with the community, show that you care, or even find the best professionals. The truth is, no classical advertisement campaign is as effective as real engagement.
Special thanks to all companies that decided to sponsor QuestPDF development. This makes .NET ecosystem a better place for all developers and businesses!
| Company | Description |
|--------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------|
| <img src="Resources/jetbrains-logo.svg" width="100px"> | [JetBrains](https://www.jetbrains.com/) supports this project as part of the OSS Power-Ups program. Thank you!<br/>100$ / month |
[![Sponsor project](https://img.shields.io/badge/%E2%9D%A4%EF%B8%8F%20sponsor-QuestPDF-red)](https://github.com/sponsors/QuestPDF)
@@ -53,7 +59,7 @@ Install-Package QuestPDF
dotnet add package QuestPDF
// Package reference in .csproj file
<PackageReference Include="QuestPDF" Version="2022.4.1" />
<PackageReference Include="QuestPDF" Version="2022.5.0" />
```
[![Nuget version](https://img.shields.io/badge/package%20details-QuestPDF-blue?logo=nuget)](https://www.nuget.org/packages/QuestPDF/)
@@ -77,6 +83,10 @@ The QuestPDF Previewer is a tool designed to simplify and speed up your developm
[![Learn more](https://img.shields.io/badge/%F0%9F%93%96%20Previewer-learn%20more-blue)](https://www.questpdf.com/documentation/document-previewer.html)
<video src="https://github.com/QuestPDF/QuestPDF-Documentation/blob/a6f54912ee761af14dfbe1f96aa70d7fcf7ff94f/images/previewer/video.mp4?raw=true"></video>
<img src="https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/previewer/animation.gif" width="100%">
## Simplicity is the key
@@ -95,7 +105,7 @@ Document.Create(container =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.Background(Colors.White);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
@@ -133,3 +143,15 @@ And compare it to the produced PDF file:
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).
<img src="https://github.com/QuestPDF/example-invoice/raw/main/images/invoice.png" width="400px">
## QuestPDF on JetBrains OSS Power-Ups
QuestPDF was presented on one of the episodes of OSS Power-Ups hosted by JetBrains. Huge thanks for Matthias Koch and entire JetBrains team for giving me a chance to show QuestPDF. You are the best!
<a href="https://www.youtube.com/watch?v=-iYvZvpLX0g">
<img src="https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/jetbrains-oss-powerups-youtube.png" width="600px">
</a>
[![YouTube video about QuestPDF]( https://img.shields.io/badge/watch%20on-YouTube-red?logo=youtube)](https://www.youtube.com/watch?v=-iYvZvpLX0g)