Compare commits

..

65 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
MarcinZiabek 0ebf2833af Updated release notes 2022-05-06 21:17:16 +02:00
MarcinZiabek 25e6d9fbca Adjusted library versions 2022-05-06 21:09:16 +02:00
Marcin Ziąbek f80149792d Merge pull request #178 from jnyrup/Dispose
Explicit disposal of disposables
2022-05-06 20:59:35 +02:00
Marcin Ziąbek 2beee1d22e Update PreviewerService.cs 2022-05-06 20:59:06 +02:00
MarcinZiabek 1af7c0a144 Adjusted code related to subscript/superscript 2022-05-06 20:55:18 +02:00
Marcin Ziąbek eeca67e5bc Merge pull request #179 from Bebo-Maker/main
Implement subscript/superscript text
2022-05-06 00:09:31 +02:00
MarcinZiabek 4da3ef5dfa Fixed breaking long words 2022-05-05 23:52:17 +02:00
MarcinZiabek f3d4151273 Improved performance of the table rendering algorithm (square to linear complexity) 2022-05-05 23:20:23 +02: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
MarcinZiabek a2629575e8 Dynamic: simplified container composition process 2022-04-27 09:50:17 +02:00
MarcinZiabek c8824021cc Dynamic: generalized dynamic element size measurement 2022-04-27 09:24:44 +02:00
MarcinZiabek e1101d870d Dynamic: improve state management 2022-04-27 06:56:13 +02:00
MarcinZiabek 152304b467 Dynamic: improved TextStyle handling 2022-04-26 01:11:40 +02:00
MarcinZiabek 8a1be90c4f Ported dynamic component implementation prototype 2022-04-26 00:36:50 +02:00
MarcinZiabek bc8a9e5dc5 Fixed mutating text style for page-related text items 2022-04-23 23:13:38 +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
Bebo-Maker c63b0658f3 Ignore FontPosition when calculating font metrics. 2022-04-12 19:14:00 +02:00
Bebo-Maker 82cd1d7835 Add example for superscript/subscript. 2022-04-12 18:49:34 +02:00
Bebo-Maker 3ffab6e933 Implement superscript and subscript. 2022-04-12 18:48:28 +02:00
Marcin Ziąbek e4d55e0298 Merge pull request #177 from jnyrup/ArgumentException
Use `paramName` overload of `ArgumentException`
2022-04-11 22:03:40 +02:00
Jonas Nyrup a6f4fd7f49 explicit disposal 2022-04-11 21:36:50 +02:00
Jonas Nyrup 048a4748b2 Use paramName overload of ArgumentException 2022-04-11 21:33:33 +02:00
Marcin Ziąbek 8cc2fb8696 Merge pull request #169 from fredericoregateiro/AvaloniaCompiledBindings
Enable compiled bindings on Previewer
2022-04-08 22:17:41 +02:00
MarcinZiabek 8e7b528cd9 Updated install command in readme 2022-04-08 22:15:12 +02:00
MarcinZiabek 26193313b8 Previewer: hide "Show in default browser" button when no document available 2022-04-08 22:14:19 +02:00
MarcinZiabek 555825e8be Previewer: throwing exception when cannot connect with frontend 2022-04-08 22:14:19 +02:00
Frederico Regateiro ddde8e7517 enable compiled bindings on PreviewerWindow 2022-04-06 14:08:53 +01:00
Marcin Ziąbek d2c82b5988 Update readme.md 2022-04-05 14:54:33 +02:00
Marcin Ziąbek d82aae95b5 Update readme.md 2022-04-05 14:34:00 +02:00
Marcin Ziąbek 5c11a2ab6a Update readme.md 2022-04-05 14:30:51 +02:00
69 changed files with 1766 additions and 212 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();
});
});
});
}
}
}
@@ -0,0 +1,185 @@
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 OrderItem
{
public string ItemName { get; set; } = Placeholders.Label();
public int Price { get; set; } = Placeholders.Random.Next(1, 11) * 10;
public int Count { get; set; } = Placeholders.Random.Next(1, 11);
}
public struct OrdersTableState
{
public int ShownItemsCount { get; set; }
}
public class OptimizedOrdersTable : IDynamicComponent<OrdersTableState>
{
private ICollection<OrderItem> Items { get; }
public OrdersTableState State { get; set; }
public OptimizedOrdersTable(ICollection<OrderItem> items)
{
Items = items;
State = new OrdersTableState
{
ShownItemsCount = 0
};
}
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var header = ComposeHeader(context);
var sampleFooter = ComposeFooter(context, Enumerable.Empty<OrderItem>());
var decorationHeight = header.Size.Height + sampleFooter.Size.Height;
var rows = GetItemsForPage(context, decorationHeight).ToList();
var footer = ComposeFooter(context, rows.Select(x => x.Item));
var content = context.CreateElement(container =>
{
container.MinimalBox().Decoration(decoration =>
{
decoration.Header().Element(header);
decoration.Content().Box().Stack(stack =>
{
foreach (var row in rows)
stack.Item().Element(row.Element);
});
decoration.Footer().Element(footer);
});
});
State = new OrdersTableState
{
ShownItemsCount = State.ShownItemsCount + rows.Count
};
return new DynamicComponentComposeResult
{
Content = content,
HasMoreContent = State.ShownItemsCount < Items.Count
};
}
private IDynamicElement ComposeHeader(DynamicContext context)
{
return context.CreateElement(element =>
{
element
.Width(context.AvailableSize.Width)
.BorderBottom(1)
.BorderColor(Colors.Grey.Darken2)
.Padding(5)
.Row(row =>
{
var textStyle = TextStyle.Default.SemiBold();
row.ConstantItem(30).Text("#", textStyle);
row.RelativeItem().Text("Item name", textStyle);
row.ConstantItem(50).AlignRight().Text("Count", textStyle);
row.ConstantItem(50).AlignRight().Text("Price", textStyle);
row.ConstantItem(50).AlignRight().Text("Total", textStyle);
});
});
}
private IDynamicElement ComposeFooter(DynamicContext context, IEnumerable<OrderItem> items)
{
var total = items.Sum(x => x.Count * x.Price);
return context.CreateElement(element =>
{
element
.Width(context.AvailableSize.Width)
.Padding(5)
.AlignRight()
.Text($"Subtotal: {total}$", TextStyle.Default.Size(14).SemiBold());
});
}
private IEnumerable<(OrderItem Item, IDynamicElement Element)> GetItemsForPage(DynamicContext context, float decorationHeight)
{
var totalHeight = decorationHeight;
foreach (var index in Enumerable.Range(State.ShownItemsCount, Items.Count - State.ShownItemsCount))
{
var item = Items.ElementAt(index);
var element = context.CreateElement(content =>
{
content
.Width(context.AvailableSize.Width)
.BorderBottom(1)
.BorderColor(Colors.Grey.Lighten2)
.Padding(5)
.Row(row =>
{
row.ConstantItem(30).Text(index + 1);
row.RelativeItem().Text(item.ItemName);
row.ConstantItem(50).AlignRight().Text(item.Count);
row.ConstantItem(50).AlignRight().Text($"{item.Price}$");
row.ConstantItem(50).AlignRight().Text($"{item.Count*item.Price}$");
});
});
var elementHeight = element.Size.Height;
if (totalHeight + elementHeight > context.AvailableSize.Height)
break;
totalHeight += elementHeight;
yield return (item, element);
}
}
}
public static class DynamicOptimizedExamples
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.PageSize(PageSizes.A5)
.ShowResults()
.Render(container =>
{
var items = Enumerable.Range(0, 25).Select(x => new OrderItem()).ToList();
container
.Background(Colors.White)
.Padding(25)
.Decoration(decoration =>
{
decoration
.Header()
.PaddingBottom(5)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Darken2).FontSize(16));
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" of ");
text.TotalPages();
});
decoration
.Content()
.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}");
});
});
}
}
}
+46
View File
@@ -0,0 +1,46 @@
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
namespace QuestPDF.Examples
{
public class TableBenchmark
{
[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)
.Table(table =>
{
const int numberOfRows = 100_000;
const int numberOfColumns = 10;
table.ColumnsDefinition(columns =>
{
foreach (var _ in Enumerable.Range(0, numberOfColumns))
columns.RelativeColumn();
});
foreach (var row in Enumerable.Range(0, numberOfRows))
foreach (var column in Enumerable.Range(0, numberOfColumns))
table.Cell().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}");
+177 -13
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));
@@ -36,13 +57,162 @@ namespace QuestPDF.Examples
});
});
}
[Test]
public void TextWeight()
{
RenderingTest
.Create()
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.MinimalBox()
.Border(1)
.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(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").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()
{
RenderingTest
.Create()
.PageSize(800, 400)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(25)
.DefaultTextStyle(x => x.FontSize(30))
.Column(column =>
{
column.Spacing(25);
column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.Underline());
text.Span("Underline of the superscript (E = mc");
text.Span("2").Superscript();
text.Span(") should be at the same height as for normal text.");
});
column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.Underline());
text.Span("Underline of the subscript(H");
text.Span("2").Subscript();
text.Span("O) should be slightly lower than a normal text.");
});
column.Item().Text(text =>
{
text.DefaultTextStyle(x => x.Strikethrough());
text.Span("Strikethrough of both superscript (E=mc");
text.Span("2").Superscript();
text.Span(") and subscript(H");
text.Span("2").Subscript();
text.Span("O) should be visible in the middle of the text.");
});
});
});
}
[Test]
public void ParagraphSpacing()
{
RenderingTest
.Create()
.PageSize(500, 300)
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
@@ -54,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());
});
});
}
@@ -239,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);
@@ -258,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();
@@ -397,7 +561,7 @@ namespace QuestPDF.Examples
text.DefaultTextStyle(x => x.BackgroundColor(Colors.Red.Lighten3).FontSize(24));
text.Span(" " + Placeholders.LoremIpsum());
text.Span(" 0123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere();
text.Span(" 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere();
});
});
});
+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 -4
View File
@@ -8,16 +8,13 @@ class Helpers
{
using var stream = File.Create(filePath);
var document = SKDocument.CreatePdf(stream);
using var document = SKDocument.CreatePdf(stream);
foreach (var page in pages)
{
using var canvas = document.BeginPage(page.Width, page.Height);
canvas.DrawPicture(page.Picture);
document.EndPage();
canvas.Dispose();
}
document.Close();
}
}
+3
View File
@@ -5,6 +5,8 @@
xmlns:previewer="clr-namespace:QuestPDF.Previewer"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="QuestPDF.Previewer.PreviewerWindow"
x:DataType="previewer:PreviewerWindowViewModel"
x:CompileBindings="True"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="true"
ExtendClientAreaTitleBarHeightHint="-1"
@@ -53,6 +55,7 @@
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
<Button Classes="actions"
Command="{Binding ShowPdfCommand, Mode=OneTime}"
IsVisible="{Binding !!Pages.Count}"
ToolTip.Tip="Generates PDF file and shows it in the default browser. Useful for testing compatibility and interactive links.">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
@@ -63,7 +63,7 @@ namespace QuestPDF.Previewer
private void OpenLink(string path)
{
var openBrowserProcess = new Process
using var openBrowserProcess = new Process
{
StartInfo = new()
{
+1 -1
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.4.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));
+26 -6
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);
}
}
@@ -164,7 +164,7 @@ namespace QuestPDF.Drawing
return debuggingState;
}
private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
{
if (content == null)
return;
@@ -186,6 +186,9 @@ namespace QuestPDF.Drawing
return;
}
if (content is DynamicHost dynamicHost)
dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
var targetTextStyle = documentDefaultTextStyle;
if (content is DefaultTextStyle defaultTextStyleElement)
@@ -197,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.";
}
}
}
+66 -6
View File
@@ -2,8 +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
{
@@ -11,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)
{
@@ -46,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)
{
@@ -60,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)
{
@@ -68,7 +74,7 @@ namespace QuestPDF.Drawing
{
Color = SKColor.Parse(style.Color),
Typeface = GetTypeface(style),
TextSize = style.Size ?? 12,
TextSize = (style.Size ?? 12) * GetTextScale(style),
IsAntialias = true,
};
}
@@ -76,6 +82,16 @@ namespace QuestPDF.Drawing
static SKTypeface GetTypeface(TextStyle style)
{
var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
// superscript and subscript use slightly bolder font to match visually line thickness
if (style.FontPosition is FontPosition.Superscript or FontPosition.Subscript)
{
var weightValue = (int)weight;
weightValue = Math.Min(weightValue + 100, 1000);
weight = (SKFontStyleWeight) (weightValue);
}
var slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
@@ -94,11 +110,55 @@ namespace QuestPDF.Drawing
$"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.");
}
static float GetTextScale(TextStyle style)
{
return style.FontPosition switch
{
FontPosition.Normal => 1f,
FontPosition.Subscript => 0.625f,
FontPosition.Superscript => 0.625f,
_ => throw new ArgumentOutOfRangeException()
};
}
}
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
{
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
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; }
}
}
+106
View File
@@ -0,0 +1,106 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class DynamicHost : Element, IStateResettable
{
private DynamicComponentProxy Child { get; }
private object InitialComponentState { get; set; }
internal TextStyle TextStyle { get; } = new();
public DynamicHost(DynamicComponentProxy child)
{
Child = child;
InitialComponentState = Child.GetState();
}
public void ResetState()
{
Child.SetState(InitialComponentState);
}
internal override SpacePlan Measure(Size availableSpace)
{
var result = GetContent(availableSpace, acceptNewState: false);
var content = result.Content as Element ?? Empty.Instance;
var measurement = content.Measure(availableSpace);
if (measurement.Type != SpacePlanType.FullRender)
throw new DocumentLayoutException("Dynamic component generated content that does not fit on a single page.");
return result.HasMoreContent
? SpacePlan.PartialRender(measurement)
: SpacePlan.FullRender(measurement);
}
internal override void Draw(Size availableSpace)
{
var content = GetContent(availableSpace, acceptNewState: true).Content as Element;
content?.Draw(availableSpace);
}
private DynamicComponentComposeResult GetContent(Size availableSize, bool acceptNewState)
{
var componentState = Child.GetState();
var context = new DynamicContext
{
PageNumber = PageContext.CurrentPage,
TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd,
PageContext = PageContext,
Canvas = Canvas,
TextStyle = TextStyle,
AvailableSize = availableSize
};
var result = Child.Compose(context);
if (!acceptNewState)
Child.SetState(componentState);
return result;
}
}
public class DynamicContext
{
internal IPageContext PageContext { get; set; }
internal ICanvas Canvas { get; set; }
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)
{
var container = new DynamicElement();
content(container);
container.ApplyDefaultTextStyle(TextStyle);
container.VisitChildren(x => x?.Initialize(PageContext, Canvas));
container.VisitChildren(x => (x as IStateResettable)?.ResetState());
container.Size = container.Measure(Size.Max);
return container;
}
}
public interface IDynamicElement : IElement
{
Size Size { get; }
}
internal class DynamicElement : ContainerElement, IDynamicElement
{
public Size Size { get; internal set; }
}
}
+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;
+37 -2
View File
@@ -19,15 +19,22 @@ namespace QuestPDF.Elements.Table
private int RowsCount { get; set; }
private int CurrentRow { get; set; }
// cache that stores all cells
// first index: row number
// inner table: list of all cells that ends at the corresponding row
private TableCell[][] CellsCache { get; set; }
private int MaxRow { get; set; }
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
{
StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
BuildCache();
base.Initialize(pageContext, canvas);
}
internal override IEnumerable<Element?> GetChildren()
{
return Cells;
@@ -38,6 +45,31 @@ namespace QuestPDF.Elements.Table
Cells.ForEach(x => x.IsRendered = false);
CurrentRow = 1;
}
private void BuildCache()
{
if (CellsCache != null)
return;
if (Cells.Count == 0)
{
MaxRow = 0;
CellsCache = Array.Empty<TableCell[]>();
return;
}
var groups = Cells
.GroupBy(x => x.Row + x.RowSpan - 1)
.ToDictionary(x => x.Key, x => x.OrderBy(x => x.Column).ToArray());
MaxRow = groups.Max(x => x.Key);
CellsCache = Enumerable
.Range(0, MaxRow + 1)
.Select(x => groups.ContainsKey(x) ? groups[x] : Array.Empty<TableCell>())
.ToArray();
}
internal override SpacePlan Measure(Size availableSpace)
{
@@ -143,7 +175,10 @@ namespace QuestPDF.Elements.Table
var rowBottomOffsets = new DynamicDictionary<int, float>();
var commands = new List<TableCellRenderingCommand>();
var cellsToTry = Cells.Where(x => x.Row + x.RowSpan - 1 >= CurrentRow);
var cellsToTry = Enumerable
.Range(CurrentRow, MaxRow - CurrentRow + 1)
.SelectMany(x => CellsCache[x]);
var currentRow = CurrentRow;
var maxRenderingRow = RowsCount;
@@ -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; }
+83 -43
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 TextStyle();
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,62 +88,100 @@ 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 + 1) : null;
return isFirstElementInLine ? (endIndex, endIndex + 1) : null;
}
public virtual void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
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, Position.Zero, 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)
DrawLine(fontMetrics.UnderlinePosition.Value, fontMetrics.UnderlineThickness ?? 1);
{
var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffsetY;
DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1);
}
// draw stroke
if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue)
DrawLine(fontMetrics.StrikeoutPosition.Value, fontMetrics.StrikeoutThickness ?? 1);
{
var strikeoutThickness = fontMetrics.StrikeoutThickness ?? 1;
strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f;
DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffsetY, strikeoutThickness);
}
void DrawLine(float offset, float thickness)
{
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
request.Canvas.DrawRectangle(new Position(0, offset), new Size(request.TextSize.Width, thickness), Style.Color);
}
float GetGlyphOffset()
{
var fontSize = Style.Size ?? 12f;
var offsetFactor = Style.FontPosition switch
{
FontPosition.Normal => 0,
FontPosition.Subscript => 0.1f,
FontPosition.Superscript => -0.35f,
_ => throw new ArgumentOutOfRangeException()
};
return fontSize * offsetFactor;
}
}
}
+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);
}
}
}
@@ -0,0 +1,14 @@
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class DynamicComponentExtensions
{
public static void Dynamic<TState>(this IContainer element, IDynamicComponent<TState> dynamicElement) where TState : struct
{
var componentProxy = DynamicComponentProxy.CreateFrom(dynamicElement);
element.Element(new DynamicHost(componentProxy));
}
}
}
+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;
+5 -5
View File
@@ -135,7 +135,7 @@ namespace QuestPDF.Fluent
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
{
var style = DefaultStyle.Clone();
var descriptor = new TextPageNumberDescriptor(DefaultStyle);
var descriptor = new TextPageNumberDescriptor(style);
AddItemToLastTextBlock(new TextBlockPageNumber
{
@@ -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)
@@ -185,7 +185,7 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor SectionLink(string? text, string sectionName)
{
if (IsNullOrEmpty(sectionName))
throw new ArgumentException(nameof(sectionName));
throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
@@ -193,7 +193,7 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(text))
return descriptor;
AddItemToLastTextBlock(new TextBlockSectionlLink
AddItemToLastTextBlock(new TextBlockSectionLink
{
Style = style,
Text = text,
@@ -212,7 +212,7 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor Hyperlink(string? text, string url)
{
if (IsNullOrEmpty(url))
throw new ArgumentException(nameof(url));
throw new ArgumentException("Url cannot be null or empty", nameof(url));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
@@ -126,7 +126,30 @@ namespace QuestPDF.Fluent
{
return descriptor.Weight(FontWeight.ExtraBlack);
}
#endregion
#region Position
public static T NormalPosition<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Normal);
}
public static T Subscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Subscript);
}
public static T Superscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Superscript);
}
private static T Position<T>(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontPosition = fontPosition;
return descriptor;
}
#endregion
}
}
+26 -1
View File
@@ -133,7 +133,32 @@ namespace QuestPDF.Fluent
{
return style.Weight(FontWeight.ExtraBlack);
}
#endregion
#region Position
public static TextStyle NormalPosition(this TextStyle style)
{
return style.Position(FontPosition.Normal);
}
public static TextStyle Subscript(this TextStyle style)
{
return style.Position(FontPosition.Subscript);
}
public static TextStyle Superscript(this TextStyle style)
{
return style.Position(FontPosition.Superscript);
}
private static TextStyle Position(this TextStyle style, FontPosition fontPosition)
{
if (style.FontPosition == fontPosition)
return style;
return style.Mutate(t => t.FontPosition = fontPosition);
}
#endregion
}
}
+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
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace QuestPDF.Infrastructure
{
internal enum FontPosition
{
Normal,
Subscript,
Superscript,
}
}
@@ -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; }
}
}
@@ -0,0 +1,35 @@
using System;
using QuestPDF.Elements;
using QuestPDF.Helpers;
namespace QuestPDF.Infrastructure
{
internal class DynamicComponentProxy
{
internal Action<object> SetState { get; private set; }
internal Func<object> GetState { get; private set; }
internal Func<DynamicContext, DynamicComponentComposeResult> Compose { get; private set; }
internal static DynamicComponentProxy CreateFrom<TState>(IDynamicComponent<TState> component) where TState : struct
{
return new DynamicComponentProxy
{
GetState = () => component.State,
SetState = x => component.State = (TState)x,
Compose = component.Compose
};
}
}
public class DynamicComponentComposeResult
{
public IElement Content { get; set; }
public bool HasMoreContent { get; set; }
}
public interface IDynamicComponent<TState> where TState : struct
{
TState State { get; set; }
DynamicComponentComposeResult Compose(DynamicContext context);
}
}
+5 -1
View File
@@ -13,6 +13,7 @@ namespace QuestPDF.Infrastructure
internal float? Size { get; set; }
internal float? LineHeight { get; set; }
internal FontWeight? FontWeight { get; set; }
internal FontPosition? FontPosition { get; set; }
internal bool? IsItalic { get; set; }
internal bool? HasStrikethrough { get; set; }
internal bool? HasUnderline { get; set; }
@@ -29,6 +30,7 @@ namespace QuestPDF.Infrastructure
Size = 12,
LineHeight = 1.2f,
FontWeight = Infrastructure.FontWeight.Normal,
FontPosition = Infrastructure.FontPosition.Normal,
IsItalic = false,
HasStrikethrough = false,
HasUnderline = false,
@@ -45,7 +47,7 @@ namespace QuestPDF.Infrastructure
HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle);
PaintKey ??= (FontFamily, Size, FontWeight, IsItalic, Color);
PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic);
}
@@ -57,6 +59,7 @@ namespace QuestPDF.Infrastructure
Size ??= parentStyle.Size;
LineHeight ??= parentStyle.LineHeight;
FontWeight ??= parentStyle.FontWeight;
FontPosition ??= parentStyle.FontPosition;
IsItalic ??= parentStyle.IsItalic;
HasStrikethrough ??= parentStyle.HasStrikethrough;
HasUnderline ??= parentStyle.HasUnderline;
@@ -71,6 +74,7 @@ namespace QuestPDF.Infrastructure
Size = parentStyle.Size ?? Size;
LineHeight = parentStyle.LineHeight ?? LineHeight;
FontWeight = parentStyle.FontWeight ?? FontWeight;
FontPosition = parentStyle.FontPosition ?? FontPosition;
IsItalic = parentStyle.IsItalic ?? IsItalic;
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
+1 -1
View File
@@ -20,7 +20,7 @@ namespace QuestPDF.Previewer
{
var previewerService = new PreviewerService(port);
var cancellationTokenSource = new CancellationTokenSource();
using var cancellationTokenSource = new CancellationTokenSource();
previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
await previewerService.Connect();
+17 -7
View File
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
@@ -23,7 +24,7 @@ namespace QuestPDF.Previewer
HttpClient = new()
{
BaseAddress = new Uri($"http://localhost:{port}/"),
Timeout = TimeSpan.FromMilliseconds(250)
Timeout = TimeSpan.FromSeconds(1)
};
}
@@ -45,7 +46,7 @@ namespace QuestPDF.Previewer
{
try
{
var result = await HttpClient.GetAsync("/ping");
using var result = await HttpClient.GetAsync("/ping");
return result.IsSuccessStatusCode;
}
catch
@@ -56,7 +57,7 @@ namespace QuestPDF.Previewer
private async Task<Version> GetPreviewerVersion()
{
var result = await HttpClient.GetAsync("/version");
using var result = await HttpClient.GetAsync("/version");
return await result.Content.ReadFromJsonAsync<Version>();
}
@@ -80,6 +81,7 @@ namespace QuestPDF.Previewer
Task.Run(async () =>
{
await process.WaitForExitAsync();
process.Dispose();
OnPreviewerStopped?.Invoke();
});
}
@@ -92,20 +94,28 @@ namespace QuestPDF.Previewer
private void CheckVersionCompatibility(Version version)
{
if (version.Major == 2022 && version.Minor == 4)
if (version.Major == 2022 && version.Minor == 6)
return;
throw new Exception($"Previewer version is not compatible. Possible solutions: " +
$"1) Update the QuestPDF library to newer version. " +
$"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version 2022.4'");
$"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version 2022.5'");
}
private async Task WaitForConnection()
{
using var cancellationTokenSource = new CancellationTokenSource();
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(10));
var cancellationToken = cancellationTokenSource.Token;
while (true)
{
await Task.Delay(TimeSpan.FromMilliseconds(250));
if (cancellationToken.IsCancellationRequested)
throw new Exception($"Cannot connect to the QuestPDF Previewer tool. Please make sure that your Operating System does not block HTTP connections on port {Port}.");
var isConnected = await IsPreviewerAvailable();
if (isConnected)
@@ -115,7 +125,7 @@ namespace QuestPDF.Previewer
public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
{
var multipartContent = new MultipartFormDataContent();
using var multipartContent = new MultipartFormDataContent();
var pages = new List<PreviewerRefreshCommand.Page>();
@@ -140,7 +150,7 @@ namespace QuestPDF.Previewer
multipartContent.Add(JsonContent.Create(command), "command");
await HttpClient.PostAsync("/update/preview", multipartContent);
using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
foreach (var picture in pictures)
picture.Picture.Dispose();
+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.4.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 -6
View File
@@ -1,6 +1,4 @@
Release theme:
Introduced the QuestPDF Previewer tool - a hot-reload powered, cross-platform program that visualizes your PDF document and updates its preview every time you make a code change. You don't need to recompile your code after every small adjustment. Save time and enjoy the design process! (available only for dotnet 6 and beyond)
Other changes:
- Improved default word-wrapping algorithm to better handle words which do not fit on the available width,
- Introduced new word-wrapping option 'WrapAnywhere' that wraps word at the last possible character instead of moving it into new line.
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

+32 -5
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.3.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/)
@@ -68,11 +74,20 @@ A short and easy to follow tutorial showing how to design an invoice document un
A detailed description of behavior of all available components and how to use them with C# Fluent API.
[![Patterns and Practices](https://img.shields.io/badge/%F0%9F%94%8D%20read-patterns%20and%20practices-blue)](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)
[![Patterns and Practices](https://img.shields.io/badge/%E2%9C%A8%20read-patterns%20and%20practices-blue)](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.
## QuestPDF Previewer
<!-- 🚀 📖 ️-->
The QuestPDF Previewer is a tool designed to simplify and speed up your development lifecycle. First, it shows a preview of your document. But the real magic starts with the hot-reload capability! It observes your code and updates the preview every time you change the implementation. Get real-time results without the need of code recompilation. Save time and enjoy the task!
[![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
@@ -90,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()
@@ -128,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)