Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4103797aa | |||
| d894914e7e | |||
| 90d1ce7af7 | |||
| 5fa32a06cc | |||
| acdf1ed1aa | |||
| a0b32e5ecf | |||
| d97fa2cef1 | |||
| 6d122c784d | |||
| 28743a81d1 | |||
| 32b94cea43 | |||
| 0ca55a91e0 | |||
| 09e6b5253f | |||
| bacb136a3f | |||
| b7bc640e31 | |||
| 6e6f884a5f | |||
| b47a8ac409 | |||
| e51082400c | |||
| 52cfa3ab7e | |||
| cd181b1e2e | |||
| e71601ad0c | |||
| 3041aa53cc | |||
| 0ebf2833af | |||
| 25e6d9fbca | |||
| f80149792d | |||
| 2beee1d22e | |||
| 1af7c0a144 | |||
| eeca67e5bc | |||
| 4da3ef5dfa | |||
| f3d4151273 | |||
| 9a7e397db2 | |||
| d81f21e658 | |||
| 78909c6edf | |||
| a2d81b6adf | |||
| 4a761c023f | |||
| 24bbd06dff | |||
| 44bd646bab | |||
| 5f0291b2c4 | |||
| 1b91015d8f | |||
| 2b6444463d | |||
| a2629575e8 | |||
| c8824021cc | |||
| e1101d870d | |||
| 152304b467 | |||
| 8a1be90c4f | |||
| bc8a9e5dc5 | |||
| e54e09de15 | |||
| c49a424f87 | |||
| c63b0658f3 | |||
| 82cd1d7835 | |||
| 3ffab6e933 | |||
| e4d55e0298 | |||
| a6f4fd7f49 | |||
| 048a4748b2 | |||
| 8cc2fb8696 | |||
| 8e7b528cd9 | |||
| 26193313b8 | |||
| 555825e8be | |||
| ddde8e7517 | |||
| d2c82b5988 | |||
| d82aae95b5 | |||
| 5c11a2ab6a |
@@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}");
|
||||
|
||||
@@ -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 mass–energy 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
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 = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
|
||||
//
|
||||
// var text = arabic;
|
||||
// var metrics = textPaint.FontMetrics;
|
||||
//
|
||||
// container
|
||||
// .Padding(25)
|
||||
// .Canvas((canvas, space) =>
|
||||
// {
|
||||
// canvas.Translate(0, 20);
|
||||
//
|
||||
// var width = MeasureText(text, textPaint);
|
||||
// var widthReal = textPaint.MeasureText(text);
|
||||
// canvas.DrawRect(0, metrics.Descent, width, metrics.Ascent - metrics.Descent, backgroundPaint);
|
||||
//
|
||||
// canvas.DrawShapedText(text, 0, 0, textPaint);
|
||||
//
|
||||
// canvas.Translate(0, 40);
|
||||
// canvas.DrawText(text, 0, 0, textPaint);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
[Test]
|
||||
public void MeasureTest()
|
||||
{
|
||||
using var textPaint = new SKPaint
|
||||
{
|
||||
Color = SKColors.Black,
|
||||
Typeface = SKTypeface.CreateDefault(),
|
||||
IsAntialias = true,
|
||||
TextSize = 20
|
||||
};
|
||||
|
||||
var lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec odio ipsum, aliquam a neque a, lacinia vehicula lectus.";
|
||||
var arabic = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
|
||||
// 012345678901234567890123456789012345678901234567890123456
|
||||
var shaper = new SKShaper(textPaint.Typeface);
|
||||
var result = shaper.Shape(lorem, textPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -55,7 +55,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 +88,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 |
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -135,11 +135,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 +163,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 +185,9 @@ namespace QuestPDF.Drawing
|
||||
return;
|
||||
}
|
||||
|
||||
if (content is DynamicHost dynamicHost)
|
||||
dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
var targetTextStyle = documentDefaultTextStyle;
|
||||
|
||||
if (content is DefaultTextStyle defaultTextStyleElement)
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
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();
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
+1
-1
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,9 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
internal enum FontPosition
|
||||
{
|
||||
Normal,
|
||||
Subscript,
|
||||
Superscript,
|
||||
}
|
||||
}
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 |
@@ -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 |
|
||||
|
||||
[](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" />
|
||||
```
|
||||
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
@@ -68,11 +74,16 @@ 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.
|
||||
|
||||
|
||||
[](https://www.questpdf.com/documentation/patterns-and-practices.html#document-metadata)
|
||||
[](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!
|
||||
|
||||
[](https://www.questpdf.com/documentation/document-previewer.html)
|
||||
|
||||
<img src="https://raw.githubusercontent.com/QuestPDF/QuestPDF-Documentation/main/images/previewer/animation.gif" width="100%">
|
||||
|
||||
## Simplicity is the key
|
||||
|
||||
@@ -90,7 +101,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 +139,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>
|
||||
|
||||
|
||||
[](https://www.youtube.com/watch?v=-iYvZvpLX0g)
|
||||
|
||||
Reference in New Issue
Block a user