Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b658344a46 | |||
| 553c8ab719 | |||
| e768e9b06d | |||
| fd913a777c | |||
| ed9e6daec5 | |||
| ee6249a658 | |||
| b307304f46 | |||
| f028f82e11 | |||
| 719a3385f6 | |||
| 2adff11400 | |||
| 34fed6d547 | |||
| db2df75624 | |||
| 6b535752df | |||
| 556f87ff25 | |||
| fbebbd85eb | |||
| 4650c2a4ea | |||
| 5390fc3f1b | |||
| 1ba01a1cf5 | |||
| d4448437ac | |||
| bc853f48fc | |||
| 0468dd5f02 | |||
| 3ba09ea826 | |||
| 92abd32aae | |||
| e17867d1f3 | |||
| bd71f30c78 | |||
| 6a78a4ebdb | |||
| 9fc721d66b | |||
| 7d62dead86 | |||
| 52ad0f5c24 | |||
| 53107073a6 | |||
| c71bb3ea59 | |||
| 6cced3c143 | |||
| 425ea59cfe | |||
| 4d326496c6 | |||
| b7b6488d16 | |||
| f0ba5fc32d | |||
| 04da32e0e7 | |||
| 5bdb338996 | |||
| 06b30f2ad6 | |||
| d39f68a974 | |||
| db9d8b5fcf | |||
| a31a588b24 | |||
| cb2a3c6363 | |||
| e775f62fab | |||
| 056e3f244c | |||
| cd3f7f5a25 | |||
| a5805f6e43 | |||
| abe7b2f14c | |||
| 3e485a572c | |||
| fd0bb5b089 | |||
| cc51b62b73 | |||
| 8e16b5ad4f | |||
| 71e2743409 | |||
| ba9d3aa04b | |||
| 2a0b133185 | |||
| 6058dac524 | |||
| 25b8f62245 | |||
| 2c31f3d5b5 | |||
| f7cd89ccf6 | |||
| 6914a9063a | |||
| b87e8d5d4e | |||
| d740d67a64 | |||
| a14e9ca61f | |||
| 44b3ff8d0d | |||
| df6b88e14a | |||
| d4103797aa | |||
| d894914e7e | |||
| 90d1ce7af7 | |||
| 5fa32a06cc | |||
| acdf1ed1aa | |||
| a0b32e5ecf | |||
| 539dc83303 | |||
| d97fa2cef1 | |||
| 6d122c784d | |||
| 28743a81d1 | |||
| 1047854f77 | |||
| c5fbf7263c | |||
| fae2205ff8 | |||
| 1cbe4b1dc8 | |||
| 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 | |||
| 03e3a80fd9 | |||
| 4692c5ce62 | |||
| 919f916219 | |||
| 7a32f774a1 | |||
| fba71e6392 | |||
| e27585a504 | |||
| 9be497ee41 | |||
| 0fc0db55d2 | |||
| 9e7a1ed49d | |||
| c137bcfcde | |||
| b80b3414d8 | |||
| 5f7cee3420 | |||
| 435c42ab9e | |||
| 7b4923b45d | |||
| 8732b7e50b | |||
| a0909eb057 | |||
| 11ff53946d | |||
| 0711bca1e1 |
@@ -0,0 +1 @@
|
|||||||
|
github: QuestPDF
|
||||||
@@ -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}");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,12 +6,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
|
||||||
<PackageReference Include="microcharts" Version="0.9.5.9" />
|
<PackageReference Include="microcharts" Version="0.9.5.9" />
|
||||||
<PackageReference Include="nunit" Version="3.13.2" />
|
<PackageReference Include="nunit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
|
||||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
<PackageReference Include="SkiaSharp" Version="2.80.4" />
|
||||||
<PackageReference Include="Svg.Skia" Version="0.5.10" />
|
<PackageReference Include="Svg.Skia" Version="0.5.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -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 chapters = GetBookChapters().ToList();
|
||||||
|
|
||||||
var results = PerformTest(16).ToList();
|
var results = PerformTest(128).ToList();
|
||||||
|
|
||||||
Console.WriteLine($"Min: {results.Min():F}");
|
Console.WriteLine($"Min: {results.Min():F}");
|
||||||
Console.WriteLine($"Max: {results.Max():F}");
|
Console.WriteLine($"Max: {results.Max():F}");
|
||||||
@@ -111,7 +111,7 @@ namespace QuestPDF.Examples
|
|||||||
{
|
{
|
||||||
page.Margin(50);
|
page.Margin(50);
|
||||||
|
|
||||||
page.Content().Column(column =>
|
page.Content().PaddingVertical(10).Column(column =>
|
||||||
{
|
{
|
||||||
column.Item().Element(Title);
|
column.Item().Element(Title);
|
||||||
column.Item().PageBreak();
|
column.Item().PageBreak();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using QuestPDF.Elements.Text;
|
||||||
using QuestPDF.Examples.Engine;
|
using QuestPDF.Examples.Engine;
|
||||||
using QuestPDF.Fluent;
|
using QuestPDF.Fluent;
|
||||||
using QuestPDF.Helpers;
|
using QuestPDF.Helpers;
|
||||||
@@ -12,11 +13,11 @@ namespace QuestPDF.Examples
|
|||||||
public class TextExamples
|
public class TextExamples
|
||||||
{
|
{
|
||||||
[Test]
|
[Test]
|
||||||
public void SimpleTextBlock()
|
public void SimpleText()
|
||||||
{
|
{
|
||||||
RenderingTest
|
RenderingTest
|
||||||
.Create()
|
.Create()
|
||||||
.PageSize(500, 300)
|
.PageSize(500, 100)
|
||||||
|
|
||||||
.ProduceImages()
|
.ProduceImages()
|
||||||
.ShowResults()
|
.ShowResults()
|
||||||
@@ -27,6 +28,27 @@ namespace QuestPDF.Examples
|
|||||||
.MinimalBox()
|
.MinimalBox()
|
||||||
.Border(1)
|
.Border(1)
|
||||||
.Padding(10)
|
.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(text =>
|
||||||
{
|
{
|
||||||
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
|
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
|
||||||
@@ -36,13 +58,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]
|
[Test]
|
||||||
public void ParagraphSpacing()
|
public void ParagraphSpacing()
|
||||||
{
|
{
|
||||||
RenderingTest
|
RenderingTest
|
||||||
.Create()
|
.Create()
|
||||||
.PageSize(500, 300)
|
.PageSize(500, 500)
|
||||||
.ProduceImages()
|
.ProduceImages()
|
||||||
.ShowResults()
|
.ShowResults()
|
||||||
.Render(container =>
|
.Render(container =>
|
||||||
@@ -54,13 +225,7 @@ namespace QuestPDF.Examples
|
|||||||
.Padding(10)
|
.Padding(10)
|
||||||
.Text(text =>
|
.Text(text =>
|
||||||
{
|
{
|
||||||
text.ParagraphSpacing(10);
|
text.Line(Placeholders.Paragraph());
|
||||||
|
|
||||||
foreach (var i in Enumerable.Range(1, 3))
|
|
||||||
{
|
|
||||||
text.Span($"Paragraph {i}: ").SemiBold();
|
|
||||||
text.Line(Placeholders.Paragraph());
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -239,7 +404,7 @@ namespace QuestPDF.Examples
|
|||||||
.Padding(10)
|
.Padding(10)
|
||||||
.Text(text =>
|
.Text(text =>
|
||||||
{
|
{
|
||||||
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
|
text.DefaultTextStyle(TextStyle.Default.FontSize(20).BackgroundColor(Colors.Red.Lighten4));
|
||||||
text.AlignLeft();
|
text.AlignLeft();
|
||||||
text.ParagraphSpacing(10);
|
text.ParagraphSpacing(10);
|
||||||
|
|
||||||
@@ -258,7 +423,7 @@ namespace QuestPDF.Examples
|
|||||||
{
|
{
|
||||||
text.Line($"{i}: {Placeholders.Paragraph()}");
|
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.Span("This is page number ");
|
||||||
text.CurrentPageNumber();
|
text.CurrentPageNumber();
|
||||||
@@ -371,5 +536,126 @@ namespace QuestPDF.Examples
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void BreakingLongWord()
|
||||||
|
{
|
||||||
|
RenderingTest
|
||||||
|
.Create()
|
||||||
|
.ProduceImages()
|
||||||
|
.ShowResults()
|
||||||
|
.RenderDocument(container =>
|
||||||
|
{
|
||||||
|
container.Page(page =>
|
||||||
|
{
|
||||||
|
page.Margin(50);
|
||||||
|
page.PageColor(Colors.White);
|
||||||
|
|
||||||
|
page.Size(PageSizes.A4);
|
||||||
|
|
||||||
|
page.Content().Column(column =>
|
||||||
|
{
|
||||||
|
column.Item().Text(null);
|
||||||
|
|
||||||
|
column.Item().Text(text =>
|
||||||
|
{
|
||||||
|
text.DefaultTextStyle(x => x.BackgroundColor(Colors.Red.Lighten3).FontSize(24));
|
||||||
|
|
||||||
|
text.Span(" " + Placeholders.LoremIpsum());
|
||||||
|
text.Span(" 012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789 ").WrapAnywhere();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TextShaping_Unicode()
|
||||||
|
{
|
||||||
|
RenderingTest
|
||||||
|
.Create()
|
||||||
|
.PageSize(600, 100)
|
||||||
|
|
||||||
|
.ProduceImages()
|
||||||
|
.ShowResults()
|
||||||
|
.Render(container =>
|
||||||
|
{
|
||||||
|
container
|
||||||
|
.Padding(35)
|
||||||
|
.MinimalBox()
|
||||||
|
.Background(Colors.Grey.Lighten2)
|
||||||
|
.Text(text =>
|
||||||
|
{
|
||||||
|
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
|
||||||
|
|
||||||
|
text.Span("Complex Unicode structure: ");
|
||||||
|
|
||||||
|
|
||||||
|
text.Span("T̶̖̔͆͆̽̔ḩ̷̼̫̐̈́̀͜͝͝ì̶͇̤͓̱̣͇͓͉̎s̵̡̟̹͍̜͉̗̾͛̈̐́͋͂͝͠ͅ ̴̨͙͍͇̭̒͗̀́͝ì̷̡̺͉̼̏̏̉̌͝s̷͍͙̗̰̖͙̈̑̂̔͑͊̌̓̊̇͜ ̶̛̼͚͊̅͘ṭ̷̨̘̣̙̖͉͌̏̂̅͑̄̽̕͝ȅ̶̲̲̙̭͈̬̣͔̝͔̈́͝s̸̢̯̪̫͓̭̮̓̀͆͜ț̸̢͉̞̥̤̏̌̓͝").FontFamily(Fonts.Calibri).FontColor(Colors.Red.Medium);
|
||||||
|
|
||||||
|
|
||||||
|
text.Span(".");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TextShaping_Arabic()
|
||||||
|
{
|
||||||
|
RenderingTest
|
||||||
|
.Create()
|
||||||
|
.PageSize(500, 100)
|
||||||
|
|
||||||
|
.ProduceImages()
|
||||||
|
.ShowResults()
|
||||||
|
.Render(container =>
|
||||||
|
{
|
||||||
|
container
|
||||||
|
.Padding(25)
|
||||||
|
.MinimalBox()
|
||||||
|
.Background(Colors.Grey.Lighten2)
|
||||||
|
.Text("ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا")
|
||||||
|
.FontFamily(Fonts.Calibri)
|
||||||
|
.FontSize(20);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void FontFallback()
|
||||||
|
{
|
||||||
|
RenderingTest
|
||||||
|
.Create()
|
||||||
|
.ProduceImages()
|
||||||
|
.ShowResults()
|
||||||
|
.RenderDocument(container =>
|
||||||
|
{
|
||||||
|
container.Page(page =>
|
||||||
|
{
|
||||||
|
page.Margin(50);
|
||||||
|
page.PageColor(Colors.White);
|
||||||
|
page.DefaultTextStyle(x => x
|
||||||
|
.Fallback(y => y.FontFamily("Segoe UI Emoji")
|
||||||
|
.Fallback(y => y.FontFamily("Microsoft YaHei"))));
|
||||||
|
|
||||||
|
page.Size(PageSizes.A4);
|
||||||
|
|
||||||
|
page.Content().Text(t =>
|
||||||
|
{
|
||||||
|
t.Line("This is normal text.");
|
||||||
|
t.EmptyLine();
|
||||||
|
|
||||||
|
t.Line("Following line should use font fallback:");
|
||||||
|
t.Line("中文文本");
|
||||||
|
t.EmptyLine();
|
||||||
|
|
||||||
|
t.Line("The following line contains a mix of known and unknown characters.");
|
||||||
|
t.Line("Mixed line: This 中文 is 文文 a mixed 本 本 line 本 中文文本!");
|
||||||
|
t.EmptyLine();
|
||||||
|
|
||||||
|
t.Line("Emojis work out of the box because of font fallback: 😊😅🥳👍❤😍👌");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
using Avalonia.Media;
|
using QuestPDF.Fluent;
|
||||||
using QuestPDF.Fluent;
|
|
||||||
using QuestPDF.Helpers;
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using QuestPDF.Previewer;
|
using QuestPDF.Previewer;
|
||||||
using QuestPDF.ReportSample;
|
using QuestPDF.ReportSample;
|
||||||
using QuestPDF.ReportSample.Layouts;
|
using QuestPDF.ReportSample.Layouts;
|
||||||
using Colors = QuestPDF.Helpers.Colors;
|
|
||||||
|
|
||||||
var model = DataSource.GetReport();
|
//ImagePlaceholder.Solid = true;
|
||||||
var report = new StandardReport(model);
|
|
||||||
report.ShowInPreviewer();
|
|
||||||
|
|
||||||
return;
|
// var model = DataSource.GetReport();
|
||||||
|
// var report = new StandardReport(model);
|
||||||
|
// report.ShowInPreviewer();
|
||||||
|
//
|
||||||
|
// return;
|
||||||
|
|
||||||
Document
|
Document
|
||||||
.Create(container =>
|
.Create(container =>
|
||||||
@@ -58,23 +58,5 @@ Document
|
|||||||
x.CurrentPageNumber();
|
x.CurrentPageNumber();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
container.Page(page =>
|
|
||||||
{
|
|
||||||
page.Size(PageSizes.A4);
|
|
||||||
page.Margin(2, Unit.Centimetre);
|
|
||||||
page.PageColor(Colors.Red.Medium);
|
|
||||||
page.DefaultTextStyle(x => x.FontSize(20));
|
|
||||||
|
|
||||||
page.Content()
|
|
||||||
.PaddingVertical(1, Unit.Centimetre)
|
|
||||||
.Column(x =>
|
|
||||||
{
|
|
||||||
x.Spacing(20);
|
|
||||||
|
|
||||||
foreach (var i in Enumerable.Range(0, 10))
|
|
||||||
x.Item().Background(Colors.Grey.Lighten2).Height(80);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.ShowInPreviewer();
|
.ShowInPreviewer();
|
||||||
@@ -8,7 +8,6 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\QuestPDF.Previewer\QuestPDF.Previewer.csproj" />
|
|
||||||
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
|
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
|
||||||
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
|
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace QuestPDF.Previewer;
|
||||||
|
|
||||||
|
class CommunicationService
|
||||||
|
{
|
||||||
|
public static CommunicationService Instance { get; } = new ();
|
||||||
|
|
||||||
|
public event Action<ICollection<PreviewPage>>? OnDocumentRefreshed;
|
||||||
|
|
||||||
|
private WebApplication? Application { get; set; }
|
||||||
|
|
||||||
|
private readonly JsonSerializerOptions JsonSerializerOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
|
||||||
|
private CommunicationService()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Start(int port)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
Application.MapGet("version", HandleVersion);
|
||||||
|
Application.MapPost("update/preview", HandleUpdatePreview);
|
||||||
|
|
||||||
|
return Application.RunAsync($"http://localhost:{port}/");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Stop()
|
||||||
|
{
|
||||||
|
await Application.StopAsync();
|
||||||
|
await Application.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IResult> HandlePing()
|
||||||
|
{
|
||||||
|
return OnDocumentRefreshed == null
|
||||||
|
? Results.StatusCode(StatusCodes.Status503ServiceUnavailable)
|
||||||
|
: Results.Ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IResult> HandleVersion()
|
||||||
|
{
|
||||||
|
return Results.Json(GetType().Assembly.GetName().Version);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IResult> HandleUpdatePreview(HttpRequest request)
|
||||||
|
{
|
||||||
|
var command = JsonSerializer.Deserialize<DocumentSnapshot>(request.Form["command"], JsonSerializerOptions);
|
||||||
|
|
||||||
|
var pages = command
|
||||||
|
.Pages
|
||||||
|
.Select(page =>
|
||||||
|
{
|
||||||
|
using var stream = request.Form.Files[page.Id].OpenReadStream();
|
||||||
|
var picture = SKPicture.Deserialize(stream);
|
||||||
|
|
||||||
|
return new PreviewPage(picture, page.Width, page.Height);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Task.Run(() => OnDocumentRefreshed(pages));
|
||||||
|
return Results.Ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
using Avalonia;
|
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
|
||||||
using Avalonia.ReactiveUI;
|
|
||||||
using QuestPDF.Infrastructure;
|
|
||||||
|
|
||||||
namespace QuestPDF.Previewer
|
|
||||||
{
|
|
||||||
public static class DocumentPreviewerExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Opens document in the QuestPDF previewer tool.
|
|
||||||
/// Improves development speed by supporting hot reloading.
|
|
||||||
/// Shows document preview and refreshes it after each code change.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// Intended for development only. Do not use in production environment.
|
|
||||||
/// </remarks>
|
|
||||||
public static void ShowInPreviewer(this IDocument document)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(document);
|
|
||||||
|
|
||||||
// currently there is no way to utilize a previously run Avalonia app.
|
|
||||||
// so we need to check if the previewer was already run and show the window again.
|
|
||||||
if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime desktop)
|
|
||||||
{
|
|
||||||
desktop.MainWindow = new PreviewerWindow()
|
|
||||||
{
|
|
||||||
DataContext = new PreviewerWindowViewModel()
|
|
||||||
{
|
|
||||||
Document = document,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
desktop.MainWindow.Show();
|
|
||||||
desktop.Start(Array.Empty<string>());
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AppBuilder
|
|
||||||
.Configure(() => new PreviewerApp()
|
|
||||||
{
|
|
||||||
Document = document,
|
|
||||||
})
|
|
||||||
.UsePlatformDetect()
|
|
||||||
.UseReactiveUI()
|
|
||||||
.StartWithClassicDesktopLifetime(Array.Empty<string>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using QuestPDF.Drawing;
|
|
||||||
using QuestPDF.Infrastructure;
|
|
||||||
|
|
||||||
namespace QuestPDF.Previewer
|
|
||||||
{
|
|
||||||
internal class DocumentRenderer : INotifyPropertyChanged
|
|
||||||
{
|
|
||||||
public IDocument? Document { get; private set; }
|
|
||||||
|
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
|
||||||
|
|
||||||
private ObservableCollection<PreviewPage> _pages = new();
|
|
||||||
public ObservableCollection<PreviewPage> Pages
|
|
||||||
{
|
|
||||||
get => _pages;
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (_pages != value)
|
|
||||||
{
|
|
||||||
_pages = value;
|
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Pages)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdateDocument(IDocument? document)
|
|
||||||
{
|
|
||||||
Document = document;
|
|
||||||
|
|
||||||
if (document == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
RenderDocument(document);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
var exceptionDocument = new ExceptionDocument(exception);
|
|
||||||
RenderDocument(exceptionDocument);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RenderDocument(IDocument document)
|
|
||||||
{
|
|
||||||
var canvas = new PreviewerCanvas();
|
|
||||||
|
|
||||||
DocumentGenerator.RenderDocument(canvas, document);
|
|
||||||
|
|
||||||
foreach (var pages in Pages)
|
|
||||||
pages?.Picture?.Dispose();
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
|
||||||
{
|
|
||||||
Pages.Clear();
|
|
||||||
Pages = new ObservableCollection<PreviewPage>(canvas.Pictures);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
using QuestPDF.Drawing;
|
|
||||||
using QuestPDF.Fluent;
|
|
||||||
using QuestPDF.Helpers;
|
|
||||||
using QuestPDF.Infrastructure;
|
|
||||||
|
|
||||||
namespace QuestPDF.Previewer;
|
|
||||||
|
|
||||||
public class ExceptionDocument : IDocument
|
|
||||||
{
|
|
||||||
private Exception Exception { get; }
|
|
||||||
|
|
||||||
public ExceptionDocument(Exception exception)
|
|
||||||
{
|
|
||||||
Exception = exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentMetadata GetMetadata()
|
|
||||||
{
|
|
||||||
return DocumentMetadata.Default;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Compose(IDocumentContainer document)
|
|
||||||
{
|
|
||||||
document.Page(page =>
|
|
||||||
{
|
|
||||||
page.Size(PageSizes.A4);
|
|
||||||
page.Margin(1, Unit.Inch);
|
|
||||||
page.PageColor(Colors.Red.Lighten4);
|
|
||||||
page.DefaultTextStyle(x => x.FontSize(16));
|
|
||||||
|
|
||||||
page.Header()
|
|
||||||
.BorderBottom(2)
|
|
||||||
.BorderColor(Colors.Red.Medium)
|
|
||||||
.PaddingBottom(5)
|
|
||||||
.Text("Ooops! Something went wrong...").FontSize(28).FontColor(Colors.Red.Medium).Bold();
|
|
||||||
|
|
||||||
page.Content().PaddingVertical(20).Column(column =>
|
|
||||||
{
|
|
||||||
var currentException = Exception;
|
|
||||||
|
|
||||||
while (currentException != null)
|
|
||||||
{
|
|
||||||
column.Item().Text(currentException.GetType().Name).FontSize(20).SemiBold();
|
|
||||||
column.Item().Text(currentException.Message).FontSize(14);
|
|
||||||
column.Item().PaddingTop(10).Text(currentException.StackTrace).FontSize(10).Light();
|
|
||||||
|
|
||||||
currentException = currentException.InnerException;
|
|
||||||
|
|
||||||
if (currentException != null)
|
|
||||||
column.Item().PaddingVertical(15).LineHorizontal(2).LineColor(Colors.Red.Medium);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace QuestPDF.Previewer;
|
||||||
|
|
||||||
|
class Helpers
|
||||||
|
{
|
||||||
|
public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<PreviewPage> pages)
|
||||||
|
{
|
||||||
|
using var stream = File.Create(filePath);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics;
|
using Avalonia;
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
using Avalonia.Rendering.SceneGraph;
|
using Avalonia.Rendering.SceneGraph;
|
||||||
using Avalonia.Skia;
|
using Avalonia.Skia;
|
||||||
@@ -25,9 +24,9 @@ class InteractiveCanvas : ICustomDrawOperation
|
|||||||
private const float PageSpacing = 25f;
|
private const float PageSpacing = 25f;
|
||||||
private const float SafeZone = 25f;
|
private const float SafeZone = 25f;
|
||||||
|
|
||||||
public float TotalPagesHeight => Pages.Sum(x => x.Size.Height) + (Pages.Count - 1) * PageSpacing;
|
public float TotalPagesHeight => Pages.Sum(x => x.Height) + (Pages.Count - 1) * PageSpacing;
|
||||||
public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
|
public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
|
||||||
public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Size.Width) : 0;
|
public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
|
||||||
|
|
||||||
public float MaxTranslateY => TotalHeight - Height / Scale;
|
public float MaxTranslateY => TotalHeight - Height / Scale;
|
||||||
|
|
||||||
@@ -99,11 +98,9 @@ class InteractiveCanvas : ICustomDrawOperation
|
|||||||
Scale *= factor;
|
Scale *= factor;
|
||||||
|
|
||||||
LimitScale();
|
LimitScale();
|
||||||
|
|
||||||
factor = Scale / oldScale;
|
TranslateX -= x / Scale - x / oldScale;
|
||||||
|
TranslateY -= y / Scale - y / oldScale;
|
||||||
TranslateX -= x / (oldScale * factor) - x / oldScale;
|
|
||||||
TranslateY -= y / (oldScale * factor) - y / oldScale;
|
|
||||||
|
|
||||||
LimitTranslate();
|
LimitTranslate();
|
||||||
}
|
}
|
||||||
@@ -134,10 +131,10 @@ class InteractiveCanvas : ICustomDrawOperation
|
|||||||
|
|
||||||
foreach (var page in Pages)
|
foreach (var page in Pages)
|
||||||
{
|
{
|
||||||
canvas.Translate(-page.Size.Width / 2f, 0);
|
canvas.Translate(-page.Width / 2f, 0);
|
||||||
DrawBlankPage(canvas, page.Size);
|
DrawBlankPage(canvas, page.Width, page.Height);
|
||||||
canvas.DrawPicture(page.Picture);
|
canvas.DrawPicture(page.Picture);
|
||||||
canvas.Translate(page.Size.Width / 2f, page.Size.Height + PageSpacing);
|
canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
|
||||||
}
|
}
|
||||||
|
|
||||||
canvas.SetMatrix(originalMatrix);
|
canvas.SetMatrix(originalMatrix);
|
||||||
@@ -165,10 +162,10 @@ class InteractiveCanvas : ICustomDrawOperation
|
|||||||
SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32)))
|
SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32)))
|
||||||
};
|
};
|
||||||
|
|
||||||
private void DrawBlankPage(SKCanvas canvas, QuestPDF.Infrastructure.Size size)
|
private void DrawBlankPage(SKCanvas canvas, float width, float height)
|
||||||
{
|
{
|
||||||
canvas.DrawRect(0, 0, size.Width, size.Height, BlankPageShadowPaint);
|
canvas.DrawRect(0, 0, width, height, BlankPageShadowPaint);
|
||||||
canvas.DrawRect(0, 0, size.Width, size.Height, BlankPagePaint);
|
canvas.DrawRect(0, 0, width, height, BlankPagePaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace QuestPDF.Previewer
|
||||||
|
{
|
||||||
|
record PreviewPage(SKPicture Picture, float Width, float Height);
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<Application x:Class="QuestPDF.Previewer.PreviewerApp"
|
<Application x:Class="QuestPDF.Previewer.PreviewerApp"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Name="QuestPDF Document Preview">
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<FluentTheme Mode="Dark" />
|
<FluentTheme Mode="Dark" />
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using QuestPDF.Infrastructure;
|
|
||||||
|
|
||||||
namespace QuestPDF.Previewer
|
namespace QuestPDF.Previewer
|
||||||
{
|
{
|
||||||
internal class PreviewerApp : Application
|
internal class PreviewerApp : Application
|
||||||
{
|
{
|
||||||
public IDocument? Document { get; init; }
|
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
@@ -21,9 +18,6 @@ namespace QuestPDF.Previewer
|
|||||||
desktop.MainWindow = new PreviewerWindow()
|
desktop.MainWindow = new PreviewerWindow()
|
||||||
{
|
{
|
||||||
DataContext = new PreviewerWindowViewModel()
|
DataContext = new PreviewerWindowViewModel()
|
||||||
{
|
|
||||||
Document = Document,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace QuestPDF.Previewer
|
namespace QuestPDF.Previewer
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace QuestPDF.Previewer;
|
||||||
|
|
||||||
|
internal class DocumentSnapshot
|
||||||
|
{
|
||||||
|
public ICollection<PageSnapshot> Pages { get; set; }
|
||||||
|
|
||||||
|
public class PageSnapshot
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
public float Width { get; set; }
|
||||||
|
public float Height { get; set; }
|
||||||
|
|
||||||
|
public SKPicture Picture { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,34 @@
|
|||||||
<Window xmlns="https://github.com/avaloniaui"
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:previewer="clr-namespace:QuestPDF.Previewer"
|
xmlns:previewer="clr-namespace:QuestPDF.Previewer"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="QuestPDF.Previewer.PreviewerWindow"
|
x:Class="QuestPDF.Previewer.PreviewerWindow"
|
||||||
WindowStartupLocation="CenterScreen"
|
x:DataType="previewer:PreviewerWindowViewModel"
|
||||||
ExtendClientAreaToDecorationsHint="true"
|
x:CompileBindings="True"
|
||||||
ExtendClientAreaTitleBarHeightHint="-1"
|
WindowStartupLocation="CenterScreen"
|
||||||
Background="#666"
|
ExtendClientAreaToDecorationsHint="true"
|
||||||
Icon="/Images/Logo.png"
|
ExtendClientAreaTitleBarHeightHint="-1"
|
||||||
UseLayoutRounding="True"
|
Background="#666"
|
||||||
Title="QuestPDF Document Preview">
|
Icon="/Resources/Logo.png"
|
||||||
|
UseLayoutRounding="True"
|
||||||
|
Title="QuestPDF Document Preview">
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="Button.actions">
|
||||||
|
<Setter Property="VerticalAlignment" Value="Bottom"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left"/>
|
||||||
|
<Setter Property="Padding" Value="10"/>
|
||||||
|
<Setter Property="CornerRadius" Value="100"/>
|
||||||
|
<Setter Property="Background" Value="#888"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="#999"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
<Panel>
|
<Panel>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
@@ -33,12 +50,12 @@
|
|||||||
VerticalAlignment="Stretch"
|
VerticalAlignment="Stretch"
|
||||||
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
|
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
|
||||||
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
|
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
|
||||||
Pages="{Binding DocumentRenderer.Pages, Mode=OneWay}" />
|
Pages="{Binding Pages, Mode=OneWay}" />
|
||||||
|
|
||||||
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32" ZIndex="100">
|
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
|
||||||
<Button VerticalAlignment="Bottom" HorizontalAlignment="Left"
|
<Button Classes="actions"
|
||||||
Padding="10" CornerRadius="100"
|
|
||||||
Command="{Binding ShowPdfCommand, Mode=OneTime}"
|
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.">
|
ToolTip.Tip="Generates PDF file and shows it in the default browser. Useful for testing compatibility and interactive links.">
|
||||||
<Viewbox Width="24" Height="24">
|
<Viewbox Width="24" Height="24">
|
||||||
<Canvas Width="24" Height="24">
|
<Canvas Width="24" Height="24">
|
||||||
@@ -47,8 +64,7 @@
|
|||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button VerticalAlignment="Bottom" HorizontalAlignment="Left"
|
<Button Classes="actions"
|
||||||
Padding="10" CornerRadius="100"
|
|
||||||
Command="{Binding ShowDocumentationCommand, Mode=OneTime}"
|
Command="{Binding ShowDocumentationCommand, Mode=OneTime}"
|
||||||
ToolTip.Tip="Opens official QuestPDF documentation">
|
ToolTip.Tip="Opens official QuestPDF documentation">
|
||||||
<Viewbox Width="24" Height="24">
|
<Viewbox Width="24" Height="24">
|
||||||
@@ -58,8 +74,7 @@
|
|||||||
</Viewbox>
|
</Viewbox>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button VerticalAlignment="Bottom" HorizontalAlignment="Left"
|
<Button Classes="actions"
|
||||||
Padding="10" CornerRadius="100"
|
|
||||||
Command="{Binding SponsorProjectCommand, Mode=OneTime}"
|
Command="{Binding SponsorProjectCommand, Mode=OneTime}"
|
||||||
ToolTip.Tip="Do you like QuestPDF? Please consider sponsoring the project. It really helps!">
|
ToolTip.Tip="Do you like QuestPDF? Please consider sponsoring the project. It really helps!">
|
||||||
<Viewbox Width="24" Height="24">
|
<Viewbox Width="24" Height="24">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace QuestPDF.Previewer
|
|||||||
|
|
||||||
protected override void OnClosed(EventArgs e)
|
protected override void OnClosed(EventArgs e)
|
||||||
{
|
{
|
||||||
(DataContext as PreviewerWindowViewModel)?.UnregisterHotReloadHandler();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using QuestPDF.Fluent;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using QuestPDF.Infrastructure;
|
|
||||||
using Unit = System.Reactive.Unit;
|
using Unit = System.Reactive.Unit;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
@@ -9,19 +8,13 @@ namespace QuestPDF.Previewer
|
|||||||
{
|
{
|
||||||
internal class PreviewerWindowViewModel : ReactiveObject
|
internal class PreviewerWindowViewModel : ReactiveObject
|
||||||
{
|
{
|
||||||
public DocumentRenderer DocumentRenderer { get; } = new();
|
private ObservableCollection<PreviewPage> _pages = new();
|
||||||
|
public ObservableCollection<PreviewPage> Pages
|
||||||
private IDocument? _document;
|
|
||||||
public IDocument? Document
|
|
||||||
{
|
{
|
||||||
get => _document;
|
get => _pages;
|
||||||
set
|
set => this.RaiseAndSetIfChanged(ref _pages, value);
|
||||||
{
|
|
||||||
this.RaiseAndSetIfChanged(ref _document, value);
|
|
||||||
UpdateDocument(value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float _currentScroll;
|
private float _currentScroll;
|
||||||
public float CurrentScroll
|
public float CurrentScroll
|
||||||
{
|
{
|
||||||
@@ -53,47 +46,24 @@ namespace QuestPDF.Previewer
|
|||||||
|
|
||||||
public PreviewerWindowViewModel()
|
public PreviewerWindowViewModel()
|
||||||
{
|
{
|
||||||
HotReloadManager.UpdateApplicationRequested += InvalidateDocument;
|
CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview;
|
||||||
|
|
||||||
ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
|
ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
|
||||||
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/documentation/api-reference.html"));
|
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/api-reference/index.html"));
|
||||||
SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
|
SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UnregisterHotReloadHandler()
|
|
||||||
{
|
|
||||||
HotReloadManager.UpdateApplicationRequested -= InvalidateDocument;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InvalidateDocument(object? sender, EventArgs e)
|
|
||||||
{
|
|
||||||
UpdateDocument(Document);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task UpdateDocument(IDocument? document)
|
|
||||||
{
|
|
||||||
return Task.Run(() => DocumentRenderer.UpdateDocument(document));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowPdf()
|
private void ShowPdf()
|
||||||
{
|
{
|
||||||
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.pdf");
|
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.pdf");
|
||||||
|
Helpers.GeneratePdfFromDocumentSnapshots(filePath, Pages);
|
||||||
try
|
|
||||||
{
|
|
||||||
Document?.GeneratePdf(filePath);
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
new ExceptionDocument(exception).GeneratePdf(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
OpenLink(filePath);
|
OpenLink(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OpenLink(string path)
|
private void OpenLink(string path)
|
||||||
{
|
{
|
||||||
var openBrowserProcess = new Process
|
using var openBrowserProcess = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new()
|
StartInfo = new()
|
||||||
{
|
{
|
||||||
@@ -104,5 +74,16 @@ namespace QuestPDF.Previewer
|
|||||||
|
|
||||||
openBrowserProcess.Start();
|
openBrowserProcess.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleUpdatePreview(ICollection<PreviewPage> pages)
|
||||||
|
{
|
||||||
|
var oldPages = Pages;
|
||||||
|
|
||||||
|
Pages.Clear();
|
||||||
|
Pages = new ObservableCollection<PreviewPage>(pages);
|
||||||
|
|
||||||
|
foreach (var page in oldPages)
|
||||||
|
page.Picture.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using QuestPDF.Previewer;
|
||||||
|
|
||||||
|
var applicationPort = GetCommunicationPort();
|
||||||
|
CommunicationService.Instance.Start(applicationPort);
|
||||||
|
|
||||||
|
if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
desktop.MainWindow = new PreviewerWindow()
|
||||||
|
{
|
||||||
|
DataContext = new PreviewerWindowViewModel()
|
||||||
|
};
|
||||||
|
|
||||||
|
desktop.MainWindow.Show();
|
||||||
|
desktop.Start(Array.Empty<string>());
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AppBuilder
|
||||||
|
.Configure(() => new PreviewerApp())
|
||||||
|
.UsePlatformDetect()
|
||||||
|
.UseReactiveUI()
|
||||||
|
.StartWithClassicDesktopLifetime(Array.Empty<string>());
|
||||||
|
|
||||||
|
static int GetCommunicationPort()
|
||||||
|
{
|
||||||
|
const int defaultApplicationPort = 12500;
|
||||||
|
|
||||||
|
var arguments = Environment.GetCommandLineArgs();
|
||||||
|
|
||||||
|
if (arguments.Length < 2)
|
||||||
|
return defaultApplicationPort;
|
||||||
|
|
||||||
|
return int.TryParse(arguments[1], out var port) ? port : defaultApplicationPort;
|
||||||
|
}
|
||||||
@@ -1,27 +1,55 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<Authors>MarcinZiabek</Authors>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<Company>CodeFlint</Company>
|
||||||
<Nullable>enable</Nullable>
|
<PackageId>QuestPDF.Previewer</PackageId>
|
||||||
</PropertyGroup>
|
<Version>2022.9.1</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>
|
||||||
|
<PackageReleaseNotes>Initial release.</PackageReleaseNotes>
|
||||||
|
<LangVersion>10</LangVersion>
|
||||||
|
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||||
|
<PackageIcon>Logo.png</PackageIcon>
|
||||||
|
<PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl>
|
||||||
|
<PackageProjectUrl>https://www.questpdf.com/</PackageProjectUrl>
|
||||||
|
<RepositoryUrl>https://github.com/QuestPDF/library.git</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
|
||||||
|
<PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core previewer</PackageTags>
|
||||||
|
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
<OutputType>exe</OutputType>
|
||||||
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Images\Logo.png" />
|
<AvaloniaResource Include="Resources\Logo.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Avalonia" Version="0.10.10" />
|
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
|
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
|
|
||||||
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.10" />
|
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
|
|
||||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
|
||||||
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<EmbeddedResource Include="Resources\Logo.png" />
|
||||||
|
<None Include="Resources\Logo.png">
|
||||||
|
<Pack>true</Pack>
|
||||||
|
<Visible>false</Visible>
|
||||||
|
<PackagePath>\</PackagePath>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||||
|
<PackageReference Include="Avalonia" Version="0.10.10" />
|
||||||
|
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
|
||||||
|
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
|
||||||
|
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.10" />
|
||||||
|
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
|
||||||
|
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||||
|
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.80.4" />
|
||||||
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
|
<PackageReference Include="SkiaSharp" Version="2.80.4" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
Install nuget locally (in directory where nupkg file is located)
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet tool install --global --add-source . QuestPDF.Previewer --global
|
||||||
|
```
|
||||||
|
|
||||||
|
Run on default port
|
||||||
|
|
||||||
|
```
|
||||||
|
questpdf-previewer
|
||||||
|
```
|
||||||
|
|
||||||
|
Run on custom port
|
||||||
|
|
||||||
|
```
|
||||||
|
questpdf-previewer 12500
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove nuget locally
|
||||||
|
|
||||||
|
```
|
||||||
|
dotnet tool uninstall QuestPDF.Previewer --global
|
||||||
|
```
|
||||||
@@ -55,7 +55,7 @@ namespace QuestPDF.ReportSample.Layouts
|
|||||||
row.Spacing(50);
|
row.Spacing(50);
|
||||||
|
|
||||||
row.RelativeItem().PaddingTop(-10).Text(Model.Title).Style(Typography.Title);
|
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();
|
column.Item().ShowOnce().PaddingVertical(15).Border(1f).BorderColor(Colors.Grey.Lighten1).ExtendHorizontal();
|
||||||
@@ -88,14 +88,14 @@ namespace QuestPDF.ReportSample.Layouts
|
|||||||
column.Item().PageBreak();
|
column.Item().PageBreak();
|
||||||
|
|
||||||
foreach (var section in Model.Sections)
|
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().PageBreak();
|
||||||
column.Item().Location("Photos");
|
column.Item().Section("Photos");
|
||||||
|
|
||||||
foreach (var photo in Model.Photos)
|
foreach (var photo in Model.Photos)
|
||||||
column.Item().Component(new PhotoTemplate(photo));
|
column.Item().Component(new PhotoTemplate(photo));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Drawing;
|
|||||||
using QuestPDF.Fluent;
|
using QuestPDF.Fluent;
|
||||||
using QuestPDF.Helpers;
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace QuestPDF.ReportSample.Layouts
|
namespace QuestPDF.ReportSample.Layouts
|
||||||
{
|
{
|
||||||
@@ -41,18 +42,31 @@ namespace QuestPDF.ReportSample.Layouts
|
|||||||
private void DrawLink(IContainer container, int number, string locationName)
|
private void DrawLink(IContainer container, int number, string locationName)
|
||||||
{
|
{
|
||||||
container
|
container
|
||||||
.InternalLink(locationName)
|
.SectionLink(locationName)
|
||||||
.Row(row =>
|
.Row(row =>
|
||||||
{
|
{
|
||||||
row.ConstantItem(25).Text($"{number}.");
|
row.ConstantItem(20).Text($"{number}.");
|
||||||
row.RelativeItem().Text(locationName);
|
row.AutoItem().Text(locationName);
|
||||||
row.ConstantItem(150).AlignRight().Text(text =>
|
|
||||||
|
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.BeginPageNumberOfSection(locationName);
|
||||||
text.Span(" - ");
|
text.Span(" - ");
|
||||||
text.EndPageNumberOfSection(locationName);
|
text.EndPageNumberOfSection(locationName);
|
||||||
|
|
||||||
var lengthStyle = TextStyle.Default.Color(Colors.Grey.Medium);
|
var lengthStyle = TextStyle.Default.FontColor(Colors.Grey.Medium);
|
||||||
|
|
||||||
text.Span(" (").Style(lengthStyle);
|
text.Span(" (").Style(lengthStyle);
|
||||||
text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x => x == 1 ? "1 page long" : $"{x} pages long");
|
text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x => x == 1 ? "1 page long" : $"{x} pages long");
|
||||||
@@ -61,4 +75,4 @@ namespace QuestPDF.ReportSample.Layouts
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ namespace QuestPDF.ReportSample
|
|||||||
Content = documentContainer.Compose();
|
Content = documentContainer.Compose();
|
||||||
|
|
||||||
PageContext = new PageContext();
|
PageContext = new PageContext();
|
||||||
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
|
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
|
||||||
|
|
||||||
var sw = new Stopwatch();
|
var sw = new Stopwatch();
|
||||||
sw.Start();
|
sw.Start();
|
||||||
@@ -69,7 +69,7 @@ namespace QuestPDF.ReportSample
|
|||||||
[Benchmark]
|
[Benchmark]
|
||||||
public void GenerationTest()
|
public void GenerationTest()
|
||||||
{
|
{
|
||||||
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
|
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<LangVersion>8</LangVersion>
|
<LangVersion>8</LangVersion>
|
||||||
<RootNamespace>QuestPDF.ReportSample</RootNamespace>
|
<RootNamespace>QuestPDF.ReportSample</RootNamespace>
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<PackageReference Include="nunit" Version="3.13.2" />
|
<PackageReference Include="nunit" Version="3.13.2" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
<PackageReference Include="SkiaSharp" Version="2.80.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
@@ -48,11 +48,10 @@ namespace QuestPDF.ReportSample
|
|||||||
Report.Compose(container);
|
Report.Compose(container);
|
||||||
var content = container.Compose();
|
var content = container.Compose();
|
||||||
|
|
||||||
var metadata = Report.GetMetadata();
|
|
||||||
var pageContext = new PageContext();
|
var pageContext = new PageContext();
|
||||||
|
|
||||||
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, metadata, null);
|
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
|
||||||
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, metadata, null);
|
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,8 +6,8 @@ namespace QuestPDF.ReportSample
|
|||||||
{
|
{
|
||||||
public static class Typography
|
public static class Typography
|
||||||
{
|
{
|
||||||
public static TextStyle Title => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Darken3).Size(26).Black();
|
public static TextStyle Title => TextStyle.Default.FontFamily(Fonts.Lato).FontColor(Colors.Blue.Darken3).FontSize(26).Black();
|
||||||
public static TextStyle Headline => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Medium).Size(16).SemiBold();
|
public static TextStyle Headline => TextStyle.Default.FontFamily(Fonts.Lato).FontColor(Colors.Blue.Medium).FontSize(16).SemiBold();
|
||||||
public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Verdana).Color(Colors.Black).Size(10).LineHeight(1.2f);
|
public static TextStyle Normal => TextStyle.Default.FontFamily(Fonts.Lato).FontColor(Colors.Black).FontSize(10).LineHeight(1.2f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||||
<PackageReference Include="nunit" Version="3.13.2" />
|
<PackageReference Include="nunit" Version="3.13.2" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
|
||||||
<PackageReference Include="SkiaSharp" Version="2.80.3" />
|
<PackageReference Include="SkiaSharp" Version="2.80.4" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using QuestPDF.Drawing;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
@@ -10,7 +11,6 @@ namespace QuestPDF.UnitTests.TestEngine
|
|||||||
public Action<float> RotateFunc { get; set; }
|
public Action<float> RotateFunc { get; set; }
|
||||||
public Action<float, float> ScaleFunc { get; set; }
|
public Action<float, float> ScaleFunc { get; set; }
|
||||||
public Action<SKImage, Position, Size> DrawImageFunc { 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 Action<Position, Size, string> DrawRectFunc { get; set; }
|
||||||
|
|
||||||
public void Translate(Position vector) => TranslateFunc(vector);
|
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 Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
|
||||||
|
|
||||||
public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
|
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 DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
|
||||||
|
|
||||||
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
|
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using QuestPDF.Drawing;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using QuestPDF.UnitTests.TestEngine.Operations;
|
using QuestPDF.UnitTests.TestEngine.Operations;
|
||||||
using SkiaSharp;
|
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 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 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 DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
|
||||||
|
|
||||||
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
|
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");
|
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) =>
|
DrawImageFunc = (image, position, size) =>
|
||||||
{
|
{
|
||||||
var expected = GetExpected<CanvasDrawImageOperation>();
|
var expected = GetExpected<CanvasDrawImageOperation>();
|
||||||
@@ -201,11 +188,6 @@ namespace QuestPDF.UnitTests.TestEngine
|
|||||||
return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
|
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)
|
public TestPlan ExpectCanvasDrawImage(Position position, Size size)
|
||||||
{
|
{
|
||||||
return AddOperation(new CanvasDrawImageOperation(position, size));
|
return AddOperation(new CanvasDrawImageOperation(position, size));
|
||||||
|
|||||||
+6
-6
@@ -8,8 +8,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.UnitTests", "Quest
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}"
|
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer.Examples", "QuestPDF.Previewer.Examples\QuestPDF.Previewer.Examples.csproj", "{CA413A39-038F-4A9F-B56F-0E5413B6B158}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer.Examples", "QuestPDF.Previewer.Examples\QuestPDF.Previewer.Examples.csproj", "{CA413A39-038F-4A9F-B56F-0E5413B6B158}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{73123649-216A-47B4-BFB2-DADF13FBE75D}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{73123649-216A-47B4-BFB2-DADF13FBE75D}"
|
||||||
@@ -17,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configurat
|
|||||||
.editorconfig = .editorconfig
|
.editorconfig = .editorconfig
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{B2FF6003-3A45-4A78-A85D-B86C7F01D054}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -39,13 +39,13 @@ Global
|
|||||||
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.Build.0 = Release|Any CPU
|
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{D63255E7-7043-4AD4-A4C9-7ACFEDBE8225}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.Build.0 = Release|Any CPU
|
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
|
|
||||||
[assembly: InternalsVisibleTo("QuestPDF.Previewer")]
|
|
||||||
[assembly: InternalsVisibleTo("QuestPDF.UnitTests")]
|
|
||||||
[assembly: InternalsVisibleTo("QuestPDF.Examples")]
|
|
||||||
[assembly: InternalsVisibleTo("QuestPDF.ReportSample")]
|
|
||||||
@@ -51,27 +51,34 @@ namespace QuestPDF.Drawing
|
|||||||
return canvas.Images;
|
return canvas.Images;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
|
||||||
|
{
|
||||||
|
var canvas = new SkiaPictureCanvas();
|
||||||
|
RenderDocument(canvas, document);
|
||||||
|
return canvas.Pictures;
|
||||||
|
}
|
||||||
|
|
||||||
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
|
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
|
||||||
where TCanvas : ICanvas, IRenderingCanvas
|
where TCanvas : ICanvas, IRenderingCanvas
|
||||||
{
|
{
|
||||||
var container = new DocumentContainer();
|
var container = new DocumentContainer();
|
||||||
document.Compose(container);
|
document.Compose(container);
|
||||||
var content = container.Compose();
|
var content = container.Compose();
|
||||||
|
|
||||||
|
ApplyRepeatContent(content);
|
||||||
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
|
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
|
||||||
|
|
||||||
var metadata = document.GetMetadata();
|
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
|
||||||
var pageContext = new PageContext();
|
|
||||||
|
|
||||||
var debuggingState = metadata.ApplyDebugging ? ApplyDebugging(content) : null;
|
|
||||||
|
|
||||||
if (metadata.ApplyCaching)
|
if (Settings.EnableCaching)
|
||||||
ApplyCaching(content);
|
ApplyCaching(content);
|
||||||
|
|
||||||
RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
|
var pageContext = new PageContext();
|
||||||
RenderPass(pageContext, canvas, content, metadata, debuggingState);
|
RenderPass(pageContext, new FreeCanvas(), content, debuggingState);
|
||||||
|
RenderPass(pageContext, canvas, content, debuggingState);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DocumentMetadata documentMetadata, DebuggingState? debuggingState)
|
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState)
|
||||||
where TCanvas : ICanvas, IRenderingCanvas
|
where TCanvas : ICanvas, IRenderingCanvas
|
||||||
{
|
{
|
||||||
content.VisitChildren(x => x?.Initialize(pageContext, canvas));
|
content.VisitChildren(x => x?.Initialize(pageContext, canvas));
|
||||||
@@ -107,7 +114,7 @@ namespace QuestPDF.Drawing
|
|||||||
|
|
||||||
canvas.EndPage();
|
canvas.EndPage();
|
||||||
|
|
||||||
if (currentPage >= documentMetadata.DocumentLayoutExceptionThreshold)
|
if (currentPage >= Settings.DocumentLayoutExceptionThreshold)
|
||||||
{
|
{
|
||||||
canvas.EndDocument();
|
canvas.EndDocument();
|
||||||
ThrowLayoutException();
|
ThrowLayoutException();
|
||||||
@@ -124,15 +131,14 @@ namespace QuestPDF.Drawing
|
|||||||
void ThrowLayoutException()
|
void ThrowLayoutException()
|
||||||
{
|
{
|
||||||
var message = $"Composed layout generates infinite document. This may happen in two cases. " +
|
var message = $"Composed layout generates infinite document. This may happen in two cases. " +
|
||||||
$"1) Your document and its layout configuration is correct but the content takes more than {documentMetadata.DocumentLayoutExceptionThreshold} pages. " +
|
$"1) Your document and its layout configuration is correct but the content takes more than {Settings.DocumentLayoutExceptionThreshold} pages. " +
|
||||||
$"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " +
|
$"In this case, please increase the value {nameof(QuestPDF)}.{nameof(Settings)}.{nameof(Settings.DocumentLayoutExceptionThreshold)} static property. " +
|
||||||
$"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
|
$"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.";
|
$"Please analyze your documents structure to detect this element and fix its size constraints.";
|
||||||
|
|
||||||
throw new DocumentLayoutException(message)
|
var elementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode.";
|
||||||
{
|
|
||||||
ElementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode."
|
throw new DocumentLayoutException(message, elementTrace);
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +163,7 @@ namespace QuestPDF.Drawing
|
|||||||
return debuggingState;
|
return debuggingState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
|
internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
|
||||||
{
|
{
|
||||||
if (content == null)
|
if (content == null)
|
||||||
return;
|
return;
|
||||||
@@ -168,7 +174,7 @@ namespace QuestPDF.Drawing
|
|||||||
{
|
{
|
||||||
if (textBlockItem is TextBlockSpan textSpan)
|
if (textBlockItem is TextBlockSpan textSpan)
|
||||||
{
|
{
|
||||||
textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle);
|
textSpan.Style = textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||||
}
|
}
|
||||||
else if (textBlockItem is TextBlockElement textElement)
|
else if (textBlockItem is TextBlockElement textElement)
|
||||||
{
|
{
|
||||||
@@ -179,16 +185,32 @@ namespace QuestPDF.Drawing
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetTextStyle = documentDefaultTextStyle;
|
if (content is DynamicHost dynamicHost)
|
||||||
|
dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||||
|
|
||||||
if (content is DefaultTextStyle defaultTextStyleElement)
|
if (content is DefaultTextStyle defaultTextStyleElement)
|
||||||
|
documentDefaultTextStyle = defaultTextStyleElement.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||||
|
|
||||||
|
foreach (var child in content.GetChildren())
|
||||||
|
ApplyDefaultTextStyle(child, documentDefaultTextStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ApplyRepeatContent(this Element? content, bool enabled = false)
|
||||||
|
{
|
||||||
|
if (content == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (content is RepeatContent repeatContent)
|
||||||
{
|
{
|
||||||
defaultTextStyleElement.TextStyle.ApplyParentStyle(documentDefaultTextStyle);
|
ApplyRepeatContent(repeatContent.Child, repeatContent.Repeat);
|
||||||
targetTextStyle = defaultTextStyleElement.TextStyle;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var child in content.GetChildren())
|
foreach (var child in content.GetChildren())
|
||||||
ApplyDefaultTextStyle(child, targetTextStyle);
|
ApplyRepeatContent(child, enabled);
|
||||||
|
|
||||||
|
if (!enabled)
|
||||||
|
content.CreateProxy(y => y is IContent ? new ShowOnce { Child = y } : y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
namespace QuestPDF.Drawing
|
namespace QuestPDF.Drawing
|
||||||
{
|
{
|
||||||
@@ -18,14 +19,26 @@ namespace QuestPDF.Drawing
|
|||||||
public DateTime CreationDate { get; set; } = DateTime.Now;
|
public DateTime CreationDate { get; set; } = DateTime.Now;
|
||||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||||
|
|
||||||
/// <summary>
|
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.DocumentLayoutExceptionThreshold static property.")]
|
||||||
/// If the number of generated pages exceeds this threshold
|
public int DocumentLayoutExceptionThreshold
|
||||||
/// (likely due to infinite layout), the exception is thrown.
|
{
|
||||||
/// </summary>
|
get => Settings.DocumentLayoutExceptionThreshold;
|
||||||
public int DocumentLayoutExceptionThreshold { get; set; } = 250;
|
set => Settings.DocumentLayoutExceptionThreshold = value;
|
||||||
|
}
|
||||||
|
|
||||||
public bool ApplyCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
|
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableCaching static property.")]
|
||||||
public bool ApplyDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
|
public bool ApplyCaching
|
||||||
|
{
|
||||||
|
get => Settings.EnableCaching;
|
||||||
|
set => Settings.EnableCaching = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableDebugging static property.")]
|
||||||
|
public bool ApplyDebugging
|
||||||
|
{
|
||||||
|
get => Settings.EnableDebugging;
|
||||||
|
set => Settings.EnableDebugging = value;
|
||||||
|
}
|
||||||
|
|
||||||
public static DocumentMetadata Default => new DocumentMetadata();
|
public static DocumentMetadata Default => new DocumentMetadata();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions
|
|||||||
{
|
{
|
||||||
public class DocumentComposeException : Exception
|
public class DocumentComposeException : Exception
|
||||||
{
|
{
|
||||||
public DocumentComposeException()
|
internal DocumentComposeException(string message) : base(message)
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentComposeException(string message) : base(message)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentComposeException(string message, Exception inner) : base(message, inner)
|
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,12 @@ namespace QuestPDF.Drawing.Exceptions
|
|||||||
{
|
{
|
||||||
public class DocumentDrawingException : Exception
|
public class DocumentDrawingException : Exception
|
||||||
{
|
{
|
||||||
public DocumentDrawingException()
|
internal DocumentDrawingException(string message) : base(message)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DocumentDrawingException(string message) : base(message)
|
internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentDrawingException(string message, Exception inner) : base(message, inner)
|
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,11 @@ namespace QuestPDF.Drawing.Exceptions
|
|||||||
{
|
{
|
||||||
public class DocumentLayoutException : Exception
|
public class DocumentLayoutException : Exception
|
||||||
{
|
{
|
||||||
public string ElementTrace { get; set; }
|
public string? ElementTrace { get; }
|
||||||
|
|
||||||
public DocumentLayoutException()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentLayoutException(string message) : base(message)
|
internal DocumentLayoutException(string message, string? elementTrace = null) : base(message)
|
||||||
{
|
{
|
||||||
|
ElementTrace = elementTrace;
|
||||||
}
|
|
||||||
|
|
||||||
public DocumentLayoutException(string message, Exception inner) : base(message, inner)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace QuestPDF.Drawing.Exceptions
|
||||||
|
{
|
||||||
|
public class InitializationException : Exception
|
||||||
|
{
|
||||||
|
internal InitializationException(string documentType, Exception innerException) : base(CreateMessage(documentType, innerException.Message), innerException)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string CreateMessage(string documentType, string innerExceptionMessage)
|
||||||
|
{
|
||||||
|
var (libraryName, nugetConvention) = GetLibraryName();
|
||||||
|
|
||||||
|
return $"Cannot create the {documentType} document using the {libraryName} library. " +
|
||||||
|
$"This exception usually means that, on your operating system where you run the application, {libraryName} requires installing additional dependencies. " +
|
||||||
|
$"Such dependencies are available as additional nuget packages, for example {nugetConvention}.Linux.NoDependencies. " +
|
||||||
|
$"Some operating systems may require installing multiple nugets, e.g. MacOS may need both {nugetConvention}.macOS.NoDependencies and {nugetConvention}.Linux.NoDependencies." +
|
||||||
|
$"Please refer to the {libraryName} documentation for more details. " +
|
||||||
|
$"Also, please consult the inner exception that has been originally thrown by the dependency library.";
|
||||||
|
|
||||||
|
(string GetLibraryName, string nugetConvention) GetLibraryName()
|
||||||
|
{
|
||||||
|
if (innerExceptionMessage.Contains("libSkiaSharp"))
|
||||||
|
return ("SkiaSharp", "SkiaSharp.NativeAssets");
|
||||||
|
|
||||||
|
if (innerExceptionMessage.Contains("libHarfBuzzSharp"))
|
||||||
|
return ("HarfBuzzSharp", "HarfBuzzSharp.NativeAssets");
|
||||||
|
|
||||||
|
// default
|
||||||
|
return ("SkiaSharp-related", "*.NativeAssets");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+117
-10
@@ -1,19 +1,32 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using HarfBuzzSharp;
|
||||||
|
using QuestPDF.Fluent;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
using SkiaSharp.HarfBuzz;
|
||||||
|
|
||||||
namespace QuestPDF.Drawing
|
namespace QuestPDF.Drawing
|
||||||
{
|
{
|
||||||
public static class FontManager
|
public static class FontManager
|
||||||
{
|
{
|
||||||
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
|
private static readonly ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
|
||||||
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
|
private static readonly ConcurrentDictionary<TextStyle, SKFontMetrics> FontMetrics = new();
|
||||||
private static ConcurrentDictionary<object, SKPaint> Paints = new();
|
private static readonly ConcurrentDictionary<TextStyle, SKPaint> FontPaints = new();
|
||||||
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new();
|
private static readonly ConcurrentDictionary<string, SKPaint> ColorPaints = new();
|
||||||
|
private static readonly ConcurrentDictionary<TextStyle, Font> ShaperFonts = new();
|
||||||
|
private static readonly ConcurrentDictionary<TextStyle, SKFont> Fonts = new();
|
||||||
|
private static readonly ConcurrentDictionary<TextStyle, TextShaper> TextShapers = new();
|
||||||
|
|
||||||
|
static FontManager()
|
||||||
|
{
|
||||||
|
RegisterLibraryDefaultFonts();
|
||||||
|
}
|
||||||
|
|
||||||
private static void RegisterFontType(SKData fontData, string? customName = null)
|
private static void RegisterFontType(SKData fontData, string? customName = null)
|
||||||
{
|
{
|
||||||
foreach (var index in Enumerable.Range(0, 256))
|
foreach (var index in Enumerable.Range(0, 256))
|
||||||
@@ -30,8 +43,13 @@ namespace QuestPDF.Drawing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("Since version 2022.3, the FontManager class offers better font type matching support. Please use the RegisterFont(Stream stream) method.")]
|
[Obsolete("Since version 2022.8 this method has been renamed. Please use the RegisterFontWithCustomName method.")]
|
||||||
public static void RegisterFontType(string fontName, Stream stream)
|
public static void RegisterFontType(string fontName, Stream stream)
|
||||||
|
{
|
||||||
|
RegisterFontWithCustomName(fontName, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RegisterFontWithCustomName(string fontName, Stream stream)
|
||||||
{
|
{
|
||||||
using var fontData = SKData.Create(stream);
|
using var fontData = SKData.Create(stream);
|
||||||
RegisterFontType(fontData);
|
RegisterFontType(fontData);
|
||||||
@@ -43,10 +61,42 @@ namespace QuestPDF.Drawing
|
|||||||
using var fontData = SKData.Create(stream);
|
using var fontData = SKData.Create(stream);
|
||||||
RegisterFontType(fontData);
|
RegisterFontType(fontData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void RegisterFontFromEmbeddedResource(string pathName)
|
||||||
|
{
|
||||||
|
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(pathName);
|
||||||
|
RegisterFont(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterLibraryDefaultFonts()
|
||||||
|
{
|
||||||
|
var fontFileNames = new[]
|
||||||
|
{
|
||||||
|
"Lato-Black.ttf",
|
||||||
|
"Lato-BlackItalic.ttf",
|
||||||
|
|
||||||
|
"Lato-Bold.ttf",
|
||||||
|
"Lato-BoldItalic.ttf",
|
||||||
|
|
||||||
|
"Lato-Regular.ttf",
|
||||||
|
"Lato-Italic.ttf",
|
||||||
|
|
||||||
|
"Lato-Light.ttf",
|
||||||
|
"Lato-LightItalic.ttf",
|
||||||
|
|
||||||
|
"Lato-Thin.ttf",
|
||||||
|
"Lato-ThinItalic.ttf"
|
||||||
|
};
|
||||||
|
|
||||||
|
fontFileNames
|
||||||
|
.Select(x => $"QuestPDF.Resources.DefaultFont.{x}")
|
||||||
|
.ToList()
|
||||||
|
.ForEach(RegisterFontFromEmbeddedResource);
|
||||||
|
}
|
||||||
|
|
||||||
internal static SKPaint ColorToPaint(this string color)
|
internal static SKPaint ColorToPaint(this string color)
|
||||||
{
|
{
|
||||||
return ColorPaint.GetOrAdd(color, Convert);
|
return ColorPaints.GetOrAdd(color, Convert);
|
||||||
|
|
||||||
static SKPaint Convert(string color)
|
static SKPaint Convert(string color)
|
||||||
{
|
{
|
||||||
@@ -60,7 +110,7 @@ namespace QuestPDF.Drawing
|
|||||||
|
|
||||||
internal static SKPaint ToPaint(this TextStyle style)
|
internal static SKPaint ToPaint(this TextStyle style)
|
||||||
{
|
{
|
||||||
return Paints.GetOrAdd(style.PaintKey, key => Convert(style));
|
return FontPaints.GetOrAdd(style, Convert);
|
||||||
|
|
||||||
static SKPaint Convert(TextStyle style)
|
static SKPaint Convert(TextStyle style)
|
||||||
{
|
{
|
||||||
@@ -68,7 +118,7 @@ namespace QuestPDF.Drawing
|
|||||||
{
|
{
|
||||||
Color = SKColor.Parse(style.Color),
|
Color = SKColor.Parse(style.Color),
|
||||||
Typeface = GetTypeface(style),
|
Typeface = GetTypeface(style),
|
||||||
TextSize = style.Size ?? 12,
|
TextSize = (style.Size ?? 12) * GetTextScale(style),
|
||||||
IsAntialias = true,
|
IsAntialias = true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -76,6 +126,16 @@ namespace QuestPDF.Drawing
|
|||||||
static SKTypeface GetTypeface(TextStyle style)
|
static SKTypeface GetTypeface(TextStyle style)
|
||||||
{
|
{
|
||||||
var weight = (SKFontStyleWeight)(style.FontWeight ?? FontWeight.Normal);
|
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 slant = (style.IsItalic ?? false) ? SKFontStyleSlant.Italic : SKFontStyleSlant.Upright;
|
||||||
|
|
||||||
var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
|
var fontStyle = new SKFontStyle(weight, SKFontStyleWidth.Normal, slant);
|
||||||
@@ -87,18 +147,65 @@ namespace QuestPDF.Drawing
|
|||||||
|
|
||||||
if (fontFromDefaultSource != null)
|
if (fontFromDefaultSource != null)
|
||||||
return fontFromDefaultSource;
|
return fontFromDefaultSource;
|
||||||
|
|
||||||
|
var availableFontNames = string.Join(", ", SKFontManager.Default.GetFontFamilies());
|
||||||
|
|
||||||
throw new ArgumentException(
|
throw new ArgumentException(
|
||||||
$"The typeface '{style.FontFamily}' could not be found. " +
|
$"The typeface '{style.FontFamily}' could not be found. " +
|
||||||
$"Please consider the following options: " +
|
$"Please consider the following options: " +
|
||||||
$"1) install the font on your operating system or execution environment. " +
|
$"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.");
|
$"2) load a font file specifically for QuestPDF usage via the QuestPDF.Drawing.FontManager.RegisterFontType(Stream fileContentStream) static method. " +
|
||||||
|
$"Available font family names: [{availableFontNames}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
|
||||||
{
|
{
|
||||||
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.ToPaint().FontMetrics);
|
return FontMetrics.GetOrAdd(style, key => key.NormalPosition().ToPaint().FontMetrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Font ToShaperFont(this TextStyle style)
|
||||||
|
{
|
||||||
|
return ShaperFonts.GetOrAdd(style, key =>
|
||||||
|
{
|
||||||
|
var typeface = key.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, key => new TextShaper(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static SKFont ToFont(this TextStyle style)
|
||||||
|
{
|
||||||
|
return Fonts.GetOrAdd(style, key => key.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 QuestPDF.Helpers;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
@@ -7,11 +9,23 @@ namespace QuestPDF.Drawing
|
|||||||
internal class PdfCanvas : SkiaDocumentCanvasBase
|
internal class PdfCanvas : SkiaDocumentCanvasBase
|
||||||
{
|
{
|
||||||
public PdfCanvas(Stream stream, DocumentMetadata documentMetadata)
|
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)
|
private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata)
|
||||||
{
|
{
|
||||||
return new SKDocumentPdfMetadata
|
return new SKDocumentPdfMetadata
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
using SkiaSharp.HarfBuzz;
|
||||||
|
|
||||||
namespace QuestPDF.Drawing
|
namespace QuestPDF.Drawing
|
||||||
{
|
{
|
||||||
@@ -27,9 +28,9 @@ namespace QuestPDF.Drawing
|
|||||||
Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint);
|
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)
|
public void DrawImage(SKImage image, Position vector, Size size)
|
||||||
|
|||||||
@@ -1,17 +1,30 @@
|
|||||||
using QuestPDF.Drawing;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace QuestPDF.Previewer
|
namespace QuestPDF.Drawing
|
||||||
{
|
{
|
||||||
record PreviewPage(SKPicture Picture, Size Size);
|
public class PreviewerPicture
|
||||||
|
{
|
||||||
|
public SKPicture Picture { get; set; }
|
||||||
|
public Size Size { get; set; }
|
||||||
|
|
||||||
|
public PreviewerPicture(SKPicture picture, Size size)
|
||||||
|
{
|
||||||
|
Picture = picture;
|
||||||
|
Size = size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sealed class PreviewerCanvas : SkiaCanvasBase, IRenderingCanvas
|
internal class SkiaPictureCanvas : SkiaCanvasBase
|
||||||
{
|
{
|
||||||
private SKPictureRecorder? PictureRecorder { get; set; }
|
private SKPictureRecorder? PictureRecorder { get; set; }
|
||||||
private Size? CurrentPageSize { get; set; }
|
private Size? CurrentPageSize { get; set; }
|
||||||
|
|
||||||
public ICollection<PreviewPage> Pictures { get; } = new List<PreviewPage>();
|
public ICollection<PreviewerPicture> Pictures { get; } = new List<PreviewerPicture>();
|
||||||
|
|
||||||
public override void BeginDocument()
|
public override void BeginDocument()
|
||||||
{
|
{
|
||||||
@@ -22,7 +35,7 @@ namespace QuestPDF.Previewer
|
|||||||
{
|
{
|
||||||
CurrentPageSize = size;
|
CurrentPageSize = size;
|
||||||
PictureRecorder = new SKPictureRecorder();
|
PictureRecorder = new SKPictureRecorder();
|
||||||
|
|
||||||
Canvas = PictureRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height));
|
Canvas = PictureRecorder.BeginRecording(new SKRect(0, 0, size.Width, size.Height));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +44,7 @@ namespace QuestPDF.Previewer
|
|||||||
var picture = PictureRecorder?.EndRecording();
|
var picture = PictureRecorder?.EndRecording();
|
||||||
|
|
||||||
if (picture != null && CurrentPageSize.HasValue)
|
if (picture != null && CurrentPageSize.HasValue)
|
||||||
Pictures.Add(new PreviewPage(picture, CurrentPageSize.Value));
|
Pictures.Add(new PreviewerPicture(picture, CurrentPageSize.Value));
|
||||||
|
|
||||||
PictureRecorder?.Dispose();
|
PictureRecorder?.Dispose();
|
||||||
PictureRecorder = null;
|
PictureRecorder = null;
|
||||||
@@ -39,4 +52,4 @@ namespace QuestPDF.Previewer
|
|||||||
|
|
||||||
public override void EndDocument() { }
|
public override void EndDocument() { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
namespace QuestPDF.Drawing
|
|
||||||
{
|
|
||||||
internal struct TextMeasurement
|
|
||||||
{
|
|
||||||
public int LineIndex { get; set; }
|
|
||||||
public float FragmentWidth { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using HarfBuzzSharp;
|
||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
using SkiaSharp;
|
||||||
|
using Buffer = HarfBuzzSharp.Buffer;
|
||||||
|
|
||||||
|
namespace QuestPDF.Drawing
|
||||||
|
{
|
||||||
|
internal class TextShaper
|
||||||
|
{
|
||||||
|
public const int FontShapingScale = 512;
|
||||||
|
|
||||||
|
private TextStyle TextStyle { get; }
|
||||||
|
|
||||||
|
private SKFont Font => TextStyle.ToFont();
|
||||||
|
private Font ShaperFont => TextStyle.ToShaperFont();
|
||||||
|
private SKPaint Paint => TextStyle.ToPaint();
|
||||||
|
|
||||||
|
public TextShaper(TextStyle textStyle)
|
||||||
|
{
|
||||||
|
TextStyle = textStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TextShapingResult Shape(string text)
|
||||||
|
{
|
||||||
|
using var buffer = new Buffer();
|
||||||
|
|
||||||
|
PopulateBufferWithText(buffer, text);
|
||||||
|
buffer.GuessSegmentProperties();
|
||||||
|
|
||||||
|
ShaperFont.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;
|
||||||
|
|
||||||
|
if (startIndex > endIndex)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
using var skTextBlobBuilder = new SKTextBlobBuilder();
|
||||||
|
|
||||||
|
var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(textStyle.ToFont(), 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 QuestPDF.Helpers;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
@@ -7,9 +9,21 @@ namespace QuestPDF.Drawing
|
|||||||
internal class XpsCanvas : SkiaDocumentCanvasBase
|
internal class XpsCanvas : SkiaDocumentCanvasBase
|
||||||
{
|
{
|
||||||
public XpsCanvas(Stream stream, DocumentMetadata documentMetadata)
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using QuestPDF.Drawing;
|
using QuestPDF.Drawing;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
@@ -6,13 +7,15 @@ namespace QuestPDF.Elements
|
|||||||
{
|
{
|
||||||
public delegate void DrawOnCanvas(SKCanvas canvas, Size availableSpace);
|
public delegate void DrawOnCanvas(SKCanvas canvas, Size availableSpace);
|
||||||
|
|
||||||
internal class Canvas : Element, ICacheable
|
internal class Canvas : Element, ICacheable, IContent
|
||||||
{
|
{
|
||||||
public DrawOnCanvas Handler { get; set; }
|
public DrawOnCanvas Handler { get; set; }
|
||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
return SpacePlan.FullRender(availableSpace);
|
return availableSpace.IsNegative()
|
||||||
|
? SpacePlan.Wrap()
|
||||||
|
: SpacePlan.FullRender(availableSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override void Draw(Size availableSpace)
|
internal override void Draw(Size availableSpace)
|
||||||
|
|||||||
@@ -94,7 +94,12 @@ namespace QuestPDF.Elements
|
|||||||
if (item.IsRendered)
|
if (item.IsRendered)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
var itemSpace = new Size(availableSpace.Width, availableSpace.Height - topOffset);
|
var availableHeight = availableSpace.Height - topOffset;
|
||||||
|
|
||||||
|
if (availableHeight < 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var itemSpace = new Size(availableSpace.Width, availableHeight);
|
||||||
var measurement = item.Measure(itemSpace);
|
var measurement = item.Measure(itemSpace);
|
||||||
|
|
||||||
if (measurement.Type == SpacePlanType.Wrap)
|
if (measurement.Type == SpacePlanType.Wrap)
|
||||||
|
|||||||
@@ -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, IContent
|
||||||
|
{
|
||||||
|
private DynamicComponentProxy Child { get; }
|
||||||
|
private object InitialComponentState { get; set; }
|
||||||
|
|
||||||
|
internal TextStyle TextStyle { get; set; } = TextStyle.Default;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
using System;
|
using System;
|
||||||
using QuestPDF.Drawing;
|
using QuestPDF.Drawing;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace QuestPDF.Elements
|
namespace QuestPDF.Elements
|
||||||
{
|
{
|
||||||
internal class DynamicImage : Element
|
internal class DynamicImage : Element, IContent
|
||||||
{
|
{
|
||||||
public Func<Size, byte[]>? Source { get; set; }
|
public Func<Size, byte[]>? Source { get; set; }
|
||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
return SpacePlan.FullRender(availableSpace.Width, availableSpace.Height);
|
return availableSpace.IsNegative()
|
||||||
|
? SpacePlan.Wrap()
|
||||||
|
: SpacePlan.FullRender(availableSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override void Draw(Size availableSpace)
|
internal override void Draw(Size availableSpace)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using QuestPDF.Drawing;
|
using QuestPDF.Drawing;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
namespace QuestPDF.Elements
|
namespace QuestPDF.Elements
|
||||||
@@ -9,7 +10,9 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
return SpacePlan.FullRender(0, 0);
|
return availableSpace.IsNegative()
|
||||||
|
? SpacePlan.Wrap()
|
||||||
|
: SpacePlan.FullRender(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override void Draw(Size availableSpace)
|
internal override void Draw(Size availableSpace)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
using QuestPDF.Drawing;
|
using QuestPDF.Drawing;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
using SkiaSharp;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace QuestPDF.Elements
|
namespace QuestPDF.Elements
|
||||||
{
|
{
|
||||||
internal class Image : Element, ICacheable
|
internal class Image : Element, ICacheable, IContent
|
||||||
{
|
{
|
||||||
public SKImage? InternalImage { get; set; }
|
public SKImage? InternalImage { get; set; }
|
||||||
|
|
||||||
@@ -15,7 +16,9 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
return SpacePlan.FullRender(availableSpace);
|
return availableSpace.IsNegative()
|
||||||
|
? SpacePlan.Wrap()
|
||||||
|
: SpacePlan.FullRender(availableSpace);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override void Draw(Size availableSpace)
|
internal override void Draw(Size availableSpace)
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ namespace QuestPDF.Elements
|
|||||||
Justify,
|
Justify,
|
||||||
SpaceAround
|
SpaceAround
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal struct InlinedMeasurement
|
||||||
|
{
|
||||||
|
public Element Element { get; set; }
|
||||||
|
public SpacePlan Size { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
internal class Inlined : Element, IStateResettable
|
internal class Inlined : Element, IStateResettable
|
||||||
{
|
{
|
||||||
@@ -80,10 +86,7 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
foreach (var line in lines)
|
foreach (var line in lines)
|
||||||
{
|
{
|
||||||
var height = line
|
var height = line.Max(x => x.Size.Height);
|
||||||
.Select(x => x.Measure(Size.Max))
|
|
||||||
.Where(x => x.Type != SpacePlanType.Wrap)
|
|
||||||
.Max(x => x.Height);
|
|
||||||
|
|
||||||
DrawLine(line);
|
DrawLine(line);
|
||||||
|
|
||||||
@@ -94,24 +97,24 @@ namespace QuestPDF.Elements
|
|||||||
Canvas.Translate(new Position(0, -topOffset));
|
Canvas.Translate(new Position(0, -topOffset));
|
||||||
lines.SelectMany(x => x).ToList().ForEach(x => ChildrenQueue.Dequeue());
|
lines.SelectMany(x => x).ToList().ForEach(x => ChildrenQueue.Dequeue());
|
||||||
|
|
||||||
void DrawLine(ICollection<InlinedElement> elements)
|
void DrawLine(ICollection<InlinedMeasurement> lineMeasurements)
|
||||||
{
|
{
|
||||||
var lineSize = GetLineSize(elements);
|
var lineSize = GetLineSize(lineMeasurements);
|
||||||
|
|
||||||
var elementOffset = ElementOffset();
|
var elementOffset = ElementOffset();
|
||||||
var leftOffset = AlignOffset();
|
var leftOffset = AlignOffset();
|
||||||
Canvas.Translate(new Position(leftOffset, 0));
|
Canvas.Translate(new Position(leftOffset, 0));
|
||||||
|
|
||||||
foreach (var element in elements)
|
foreach (var measurement in lineMeasurements)
|
||||||
{
|
{
|
||||||
var size = (Size)element.Measure(Size.Max);
|
var size = (Size)measurement.Size;
|
||||||
var baselineOffset = BaselineOffset(size, lineSize.Height);
|
var baselineOffset = BaselineOffset(size, lineSize.Height);
|
||||||
|
|
||||||
if (size.Height == 0)
|
if (size.Height == 0)
|
||||||
size = new Size(size.Width, lineSize.Height);
|
size = new Size(size.Width, lineSize.Height);
|
||||||
|
|
||||||
Canvas.Translate(new Position(0, baselineOffset));
|
Canvas.Translate(new Position(0, baselineOffset));
|
||||||
element.Draw(size);
|
measurement.Element.Draw(size);
|
||||||
Canvas.Translate(new Position(0, -baselineOffset));
|
Canvas.Translate(new Position(0, -baselineOffset));
|
||||||
|
|
||||||
leftOffset += size.Width + elementOffset;
|
leftOffset += size.Width + elementOffset;
|
||||||
@@ -124,20 +127,20 @@ namespace QuestPDF.Elements
|
|||||||
{
|
{
|
||||||
var difference = availableSpace.Width - lineSize.Width;
|
var difference = availableSpace.Width - lineSize.Width;
|
||||||
|
|
||||||
if (elements.Count == 1)
|
if (lineMeasurements.Count == 1)
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return ElementsAlignment switch
|
return ElementsAlignment switch
|
||||||
{
|
{
|
||||||
InlinedAlignment.Justify => difference / (elements.Count - 1),
|
InlinedAlignment.Justify => difference / (lineMeasurements.Count - 1),
|
||||||
InlinedAlignment.SpaceAround => difference / (elements.Count + 1),
|
InlinedAlignment.SpaceAround => difference / (lineMeasurements.Count + 1),
|
||||||
_ => HorizontalSpacing
|
_ => HorizontalSpacing
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
float AlignOffset()
|
float AlignOffset()
|
||||||
{
|
{
|
||||||
var difference = availableSpace.Width - lineSize.Width - (elements.Count - 1) * HorizontalSpacing;
|
var difference = availableSpace.Width - lineSize.Width - (lineMeasurements.Count - 1) * HorizontalSpacing;
|
||||||
|
|
||||||
return ElementsAlignment switch
|
return ElementsAlignment switch
|
||||||
{
|
{
|
||||||
@@ -164,24 +167,19 @@ namespace QuestPDF.Elements
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Size GetLineSize(ICollection<InlinedElement> elements)
|
Size GetLineSize(ICollection<InlinedMeasurement> measurements)
|
||||||
{
|
{
|
||||||
var sizes = elements
|
var width = measurements.Sum(x => x.Size.Width);
|
||||||
.Select(x => x.Measure(Size.Max))
|
var height = measurements.Max(x => x.Size.Height);
|
||||||
.Where(x => x.Type != SpacePlanType.Wrap)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
var width = sizes.Sum(x => x.Width);
|
|
||||||
var height = sizes.Max(x => x.Height);
|
|
||||||
|
|
||||||
return new Size(width, height);
|
return new Size(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// list of lines, each line is a list of elements
|
// list of lines, each line is a list of elements
|
||||||
private ICollection<ICollection<InlinedElement>> Compose(Size availableSize)
|
private ICollection<ICollection<InlinedMeasurement>> Compose(Size availableSize)
|
||||||
{
|
{
|
||||||
var queue = new Queue<InlinedElement>(ChildrenQueue);
|
var queue = new Queue<InlinedElement>(ChildrenQueue);
|
||||||
var result = new List<ICollection<InlinedElement>>();
|
var result = new List<ICollection<InlinedMeasurement>>();
|
||||||
|
|
||||||
var topOffset = 0f;
|
var topOffset = 0f;
|
||||||
|
|
||||||
@@ -192,10 +190,7 @@ namespace QuestPDF.Elements
|
|||||||
if (!line.Any())
|
if (!line.Any())
|
||||||
break;
|
break;
|
||||||
|
|
||||||
var height = line
|
var height = line.Max(x => x.Size.Height);
|
||||||
.Select(x => x.Measure(availableSize))
|
|
||||||
.Where(x => x.Type != SpacePlanType.Wrap)
|
|
||||||
.Max(x => x.Height);
|
|
||||||
|
|
||||||
if (topOffset + height > availableSize.Height + Size.Epsilon)
|
if (topOffset + height > availableSize.Height + Size.Epsilon)
|
||||||
break;
|
break;
|
||||||
@@ -206,9 +201,9 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
ICollection<InlinedElement> GetNextLine()
|
ICollection<InlinedMeasurement> GetNextLine()
|
||||||
{
|
{
|
||||||
var result = new List<InlinedElement>();
|
var result = new List<InlinedMeasurement>();
|
||||||
var leftOffset = GetInitialAlignmentOffset();
|
var leftOffset = GetInitialAlignmentOffset();
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
@@ -227,7 +222,12 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
queue.Dequeue();
|
queue.Dequeue();
|
||||||
leftOffset += size.Width + HorizontalSpacing;
|
leftOffset += size.Width + HorizontalSpacing;
|
||||||
result.Add(element);
|
|
||||||
|
result.Add(new InlinedMeasurement
|
||||||
|
{
|
||||||
|
Element = element,
|
||||||
|
Size = size
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace QuestPDF.Elements
|
|||||||
Horizontal
|
Horizontal
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class Line : Element, ILine, ICacheable
|
internal class Line : Element, ILine, ICacheable, IContent
|
||||||
{
|
{
|
||||||
public LineType Type { get; set; } = LineType.Vertical;
|
public LineType Type { get; set; } = LineType.Vertical;
|
||||||
public string Color { get; set; } = Colors.Black;
|
public string Color { get; set; } = Colors.Black;
|
||||||
@@ -23,6 +23,9 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
|
if (availableSpace.IsNegative())
|
||||||
|
return SpacePlan.Wrap();
|
||||||
|
|
||||||
return Type switch
|
return Type switch
|
||||||
{
|
{
|
||||||
LineType.Vertical when availableSpace.Width + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(Size, 0),
|
LineType.Vertical when availableSpace.Width + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(Size, 0),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using QuestPDF.Drawing;
|
using QuestPDF.Drawing;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
namespace QuestPDF.Elements
|
namespace QuestPDF.Elements
|
||||||
@@ -14,6 +15,9 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
|
if (availableSpace.IsNegative())
|
||||||
|
return SpacePlan.Wrap();
|
||||||
|
|
||||||
if (IsRendered)
|
if (IsRendered)
|
||||||
return SpacePlan.FullRender(0, 0);
|
return SpacePlan.FullRender(0, 0);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
|
namespace QuestPDF.Elements
|
||||||
|
{
|
||||||
|
internal class RepeatContent : ContainerElement
|
||||||
|
{
|
||||||
|
public bool Repeat { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -102,13 +102,14 @@ namespace QuestPDF.Elements
|
|||||||
|
|
||||||
private void UpdateItemsWidth(float availableWidth)
|
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 relativeWidth = Items.Where(x => x.Type == RowItemType.Relative).Sum(x => x.Size);
|
||||||
var spacingWidth = (Items.Count - 1) * Spacing;
|
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;
|
item.Width = item.Size;
|
||||||
|
|
||||||
if (relativeWidth <= 0)
|
if (relativeWidth <= 0)
|
||||||
@@ -119,16 +120,7 @@ namespace QuestPDF.Elements
|
|||||||
foreach (var item in Items.Where(x => x.Type == RowItemType.Relative))
|
foreach (var item in Items.Where(x => x.Type == RowItemType.Relative))
|
||||||
item.Width = item.Size * widthPerRelativeUnit;
|
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)
|
private ICollection<RowItemRenderingCommand> PlanLayout(Size availableSpace)
|
||||||
{
|
{
|
||||||
var leftOffset = 0f;
|
var leftOffset = 0f;
|
||||||
|
|||||||
@@ -19,15 +19,22 @@ namespace QuestPDF.Elements.Table
|
|||||||
private int RowsCount { get; set; }
|
private int RowsCount { get; set; }
|
||||||
private int CurrentRow { 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)
|
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
|
||||||
{
|
{
|
||||||
StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
|
StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
|
||||||
RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).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();
|
Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
|
||||||
|
BuildCache();
|
||||||
|
|
||||||
base.Initialize(pageContext, canvas);
|
base.Initialize(pageContext, canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override IEnumerable<Element?> GetChildren()
|
internal override IEnumerable<Element?> GetChildren()
|
||||||
{
|
{
|
||||||
return Cells;
|
return Cells;
|
||||||
@@ -35,9 +42,36 @@ namespace QuestPDF.Elements.Table
|
|||||||
|
|
||||||
public void ResetState()
|
public void ResetState()
|
||||||
{
|
{
|
||||||
Cells.ForEach(x => x.IsRendered = false);
|
foreach (var x in Cells)
|
||||||
|
x.IsRendered = false;
|
||||||
|
|
||||||
CurrentRow = 1;
|
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)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
{
|
{
|
||||||
@@ -143,7 +177,10 @@ namespace QuestPDF.Elements.Table
|
|||||||
var rowBottomOffsets = new DynamicDictionary<int, float>();
|
var rowBottomOffsets = new DynamicDictionary<int, float>();
|
||||||
var commands = new List<TableCellRenderingCommand>();
|
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 currentRow = CurrentRow;
|
||||||
var maxRenderingRow = RowsCount;
|
var maxRenderingRow = RowsCount;
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using QuestPDF.Fluent;
|
|||||||
|
|
||||||
namespace QuestPDF.Elements.Table
|
namespace QuestPDF.Elements.Table
|
||||||
{
|
{
|
||||||
static class TableLayoutValidator
|
static class TableLayoutPlanner
|
||||||
{
|
{
|
||||||
public static void PlanCellPositions(this Table table)
|
public static void PlanCellPositions(this Table table)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using QuestPDF.Fluent;
|
|||||||
|
|
||||||
namespace QuestPDF.Elements.Table
|
namespace QuestPDF.Elements.Table
|
||||||
{
|
{
|
||||||
static class TableLayoutPlanner
|
static class TableLayoutValidator
|
||||||
{
|
{
|
||||||
public static void ValidateCellPositions(this Table table)
|
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
|
namespace QuestPDF.Elements.Text.Calculation
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ namespace QuestPDF.Elements.Text.Calculation
|
|||||||
|
|
||||||
public int StartIndex { get; set; }
|
public int StartIndex { get; set; }
|
||||||
public float AvailableWidth { get; set; }
|
public float AvailableWidth { get; set; }
|
||||||
|
|
||||||
|
public bool IsFirstElementInBlock { get; set; }
|
||||||
public bool IsFirstElementInLine { get; set; }
|
public bool IsFirstElementInLine { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using QuestPDF.Drawing;
|
||||||
|
|
||||||
namespace QuestPDF.Elements.Text.Calculation
|
namespace QuestPDF.Elements.Text.Calculation
|
||||||
{
|
{
|
||||||
@@ -17,6 +18,6 @@ namespace QuestPDF.Elements.Text.Calculation
|
|||||||
public int NextIndex { get; set; }
|
public int NextIndex { get; set; }
|
||||||
public int TotalIndex { get; set; }
|
public int TotalIndex { get; set; }
|
||||||
|
|
||||||
public bool IsLast => NextIndex == EndIndex;
|
public bool IsLast => EndIndex == TotalIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using QuestPDF.Drawing;
|
||||||
|
using QuestPDF.Drawing.Exceptions;
|
||||||
|
using QuestPDF.Elements.Text.Items;
|
||||||
|
using QuestPDF.Fluent;
|
||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace QuestPDF.Elements.Text
|
||||||
|
{
|
||||||
|
internal static class FontFallback
|
||||||
|
{
|
||||||
|
public struct TextRun
|
||||||
|
{
|
||||||
|
public string Content { get; set; }
|
||||||
|
public TextStyle Style { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FallbackOption
|
||||||
|
{
|
||||||
|
public TextStyle Style { get; set; }
|
||||||
|
public SKFont Font { get; set; }
|
||||||
|
public SKTypeface Typeface { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SKFontManager FontManager => SKFontManager.Default;
|
||||||
|
|
||||||
|
public static IEnumerable<TextRun> SplitWithFontFallback(this string text, TextStyle textStyle)
|
||||||
|
{
|
||||||
|
var fallbackOptions = GetFallbackOptions(textStyle).ToArray();
|
||||||
|
|
||||||
|
var spanStartIndex = 0;
|
||||||
|
var spanFallbackOption = fallbackOptions[0];
|
||||||
|
|
||||||
|
for (var i = 0; i < text.Length; i += char.IsSurrogatePair(text, i) ? 2 : 1)
|
||||||
|
{
|
||||||
|
var codepoint = char.ConvertToUtf32(text, i);
|
||||||
|
var newFallbackOption = MatchFallbackOption(fallbackOptions, codepoint);
|
||||||
|
|
||||||
|
if (newFallbackOption == spanFallbackOption)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
yield return new TextRun
|
||||||
|
{
|
||||||
|
Content = text.Substring(spanStartIndex, i - spanStartIndex),
|
||||||
|
Style = spanFallbackOption.Style
|
||||||
|
};
|
||||||
|
|
||||||
|
spanStartIndex = i;
|
||||||
|
spanFallbackOption = newFallbackOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spanStartIndex > text.Length)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
yield return new TextRun
|
||||||
|
{
|
||||||
|
Content = text.Substring(spanStartIndex, text.Length - spanStartIndex),
|
||||||
|
Style = spanFallbackOption.Style
|
||||||
|
};
|
||||||
|
|
||||||
|
static IEnumerable<FallbackOption> GetFallbackOptions(TextStyle? textStyle)
|
||||||
|
{
|
||||||
|
while (textStyle != null)
|
||||||
|
{
|
||||||
|
var font = textStyle.ToFont();
|
||||||
|
|
||||||
|
yield return new FallbackOption
|
||||||
|
{
|
||||||
|
Style = textStyle,
|
||||||
|
Font = font,
|
||||||
|
Typeface = font.Typeface
|
||||||
|
};
|
||||||
|
|
||||||
|
textStyle = textStyle.Fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static FallbackOption MatchFallbackOption(ICollection<FallbackOption> fallbackOptions, int codepoint)
|
||||||
|
{
|
||||||
|
foreach (var fallbackOption in fallbackOptions)
|
||||||
|
{
|
||||||
|
if (fallbackOption.Font.ContainsGlyph(codepoint))
|
||||||
|
return fallbackOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw CreateNotMatchingFontException(codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Exception CreateNotMatchingFontException(int codepoint)
|
||||||
|
{
|
||||||
|
var character = char.ConvertFromUtf32(codepoint);
|
||||||
|
var unicode = $"U-{codepoint:X4}";
|
||||||
|
|
||||||
|
var proposedFonts = FindFontsContainingGlyph(codepoint);
|
||||||
|
var proposedFontsFormatted = proposedFonts.Any() ? string.Join(", ", proposedFonts) : "no fonts available";
|
||||||
|
|
||||||
|
return new DocumentDrawingException(
|
||||||
|
$"Could not find an appropriate font fallback for glyph: {unicode} '{character}'. " +
|
||||||
|
$"Font families available on current environment that contain this glyph: {proposedFontsFormatted}. " +
|
||||||
|
$"Possible solutions: " +
|
||||||
|
$"1) Use one of the listed fonts as the primary font in your document. " +
|
||||||
|
$"2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. ");
|
||||||
|
}
|
||||||
|
|
||||||
|
static IEnumerable<string> FindFontsContainingGlyph(int codepoint)
|
||||||
|
{
|
||||||
|
var fontManager = SKFontManager.Default;
|
||||||
|
|
||||||
|
return fontManager
|
||||||
|
.GetFontFamilies()
|
||||||
|
.Select(fontManager.MatchFamily)
|
||||||
|
.Where(x => x.ContainsGlyph(codepoint))
|
||||||
|
.Select(x => x.FamilyName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<ITextBlockItem> ApplyFontFallback(this ICollection<ITextBlockItem> textBlockItems)
|
||||||
|
{
|
||||||
|
foreach (var textBlockItem in textBlockItems)
|
||||||
|
{
|
||||||
|
if (textBlockItem is TextBlockSpan textBlockSpan and not TextBlockPageNumber)
|
||||||
|
{
|
||||||
|
if (!Settings.CheckIfAllTextGlyphsAreAvailable && textBlockSpan.Style.Fallback == null)
|
||||||
|
{
|
||||||
|
yield return textBlockSpan;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textRuns = textBlockSpan.Text.SplitWithFontFallback(textBlockSpan.Style);
|
||||||
|
|
||||||
|
foreach (var textRun in textRuns)
|
||||||
|
{
|
||||||
|
yield return new TextBlockSpan
|
||||||
|
{
|
||||||
|
Text = textRun.Content,
|
||||||
|
Style = textRun.Style
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
yield return textBlockItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ namespace QuestPDF.Elements.Text.Items
|
|||||||
{
|
{
|
||||||
public const string PageNumberPlaceholder = "123";
|
public const string PageNumberPlaceholder = "123";
|
||||||
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
|
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
|
||||||
|
protected override bool EnableTextCache => false;
|
||||||
|
|
||||||
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
|
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ using QuestPDF.Infrastructure;
|
|||||||
|
|
||||||
namespace QuestPDF.Elements.Text.Items
|
namespace QuestPDF.Elements.Text.Items
|
||||||
{
|
{
|
||||||
internal class TextBlockSectionlLink : TextBlockSpan
|
internal class TextBlockSectionLink : TextBlockSpan
|
||||||
{
|
{
|
||||||
public string SectionName { get; set; }
|
public string SectionName { get; set; }
|
||||||
|
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using QuestPDF.Drawing;
|
using QuestPDF.Drawing;
|
||||||
using QuestPDF.Elements.Text.Calculation;
|
using QuestPDF.Elements.Text.Calculation;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
using SkiaSharp;
|
||||||
|
using SkiaSharp.HarfBuzz;
|
||||||
using Size = QuestPDF.Infrastructure.Size;
|
using Size = QuestPDF.Infrastructure.Size;
|
||||||
|
|
||||||
namespace QuestPDF.Elements.Text.Items
|
namespace QuestPDF.Elements.Text.Items
|
||||||
@@ -10,9 +14,11 @@ namespace QuestPDF.Elements.Text.Items
|
|||||||
internal class TextBlockSpan : ITextBlockItem
|
internal class TextBlockSpan : ITextBlockItem
|
||||||
{
|
{
|
||||||
public string Text { get; set; }
|
public string Text { get; set; }
|
||||||
public TextStyle Style { get; set; } = new TextStyle();
|
public TextStyle Style { get; set; } = TextStyle.Default;
|
||||||
|
public TextShapingResult? TextShapingResult { get; set; }
|
||||||
|
|
||||||
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
|
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
|
||||||
|
protected virtual bool EnableTextCache => true;
|
||||||
|
|
||||||
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
|
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
|
||||||
{
|
{
|
||||||
@@ -23,25 +29,29 @@ namespace QuestPDF.Elements.Text.Items
|
|||||||
|
|
||||||
return MeasureCache[cacheKey];
|
return MeasureCache[cacheKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
|
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
|
||||||
{
|
{
|
||||||
const char space = ' ';
|
if (!EnableTextCache)
|
||||||
|
TextShapingResult = null;
|
||||||
|
|
||||||
|
TextShapingResult ??= Style.ToTextShaper().Shape(Text);
|
||||||
|
|
||||||
var paint = Style.ToPaint();
|
var paint = Style.ToPaint();
|
||||||
var fontMetrics = Style.ToFontMetrics();
|
var fontMetrics = Style.ToFontMetrics();
|
||||||
|
var spaceCodepoint = paint.ToFont().Typeface.GetGlyphs(" ")[0];
|
||||||
|
|
||||||
var startIndex = request.StartIndex;
|
var startIndex = request.StartIndex;
|
||||||
|
|
||||||
// if the element is the first one within the line,
|
// if the element is the first one within the line,
|
||||||
// ignore leading spaces
|
// ignore leading spaces
|
||||||
if (request.IsFirstElementInLine)
|
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
|
||||||
{
|
{
|
||||||
while (startIndex < Text.Length && Text[startIndex] == space)
|
while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint)
|
||||||
startIndex++;
|
startIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Text.Length == 0 || startIndex == Text.Length)
|
if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length)
|
||||||
{
|
{
|
||||||
return new TextMeasurementResult
|
return new TextMeasurementResult
|
||||||
{
|
{
|
||||||
@@ -54,32 +64,19 @@ namespace QuestPDF.Elements.Text.Items
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start breaking text from requested position
|
// start breaking text from requested position
|
||||||
var text = Text.AsSpan().Slice(startIndex);
|
var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
|
||||||
|
|
||||||
var textLength = (int)paint.BreakText(text, request.AvailableWidth + Size.Epsilon);
|
|
||||||
|
|
||||||
if (textLength <= 0)
|
if (endIndex < startIndex)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
// break text only on spaces
|
// 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;
|
return null;
|
||||||
|
|
||||||
textLength = wrappedTextLength.Value;
|
|
||||||
|
|
||||||
text = text.Slice(0, textLength);
|
|
||||||
|
|
||||||
var endIndex = startIndex + textLength;
|
|
||||||
var nextIndex = endIndex;
|
|
||||||
|
|
||||||
// when breaking text, omit spaces at the end of the line
|
|
||||||
while (nextIndex < Text.Length && Text[nextIndex] == space)
|
|
||||||
nextIndex++;
|
|
||||||
|
|
||||||
// measure final text
|
// measure final text
|
||||||
var width = paint.MeasureText(text);
|
var width = TextShapingResult.MeasureWidth(startIndex, wrappedText.Value.endIndex);
|
||||||
|
|
||||||
return new TextMeasurementResult
|
return new TextMeasurementResult
|
||||||
{
|
{
|
||||||
@@ -91,57 +88,100 @@ namespace QuestPDF.Elements.Text.Items
|
|||||||
LineHeight = Style.LineHeight ?? 1,
|
LineHeight = Style.LineHeight ?? 1,
|
||||||
|
|
||||||
StartIndex = startIndex,
|
StartIndex = startIndex,
|
||||||
EndIndex = endIndex,
|
EndIndex = wrappedText.Value.endIndex,
|
||||||
NextIndex = nextIndex,
|
NextIndex = wrappedText.Value.nextIndex,
|
||||||
TotalIndex = Text.Length
|
TotalIndex = TextShapingResult.Glyphs.Length - 1
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
|
||||||
static int? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
|
// entire text fits, no need to wrap
|
||||||
|
if (endIndex == TextShapingResult.Glyphs.Length - 1)
|
||||||
|
return (endIndex, endIndex);
|
||||||
|
|
||||||
|
// breaking anywhere
|
||||||
|
if (Style.WrapAnywhere ?? false)
|
||||||
|
return (endIndex, endIndex + 1);
|
||||||
|
|
||||||
|
// current line ends at word, next character is space, perfect place to wrap
|
||||||
|
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 = endIndex;
|
||||||
|
|
||||||
|
while (lastSpaceIndex >= startIndex)
|
||||||
{
|
{
|
||||||
// textLength - length of the part of the text that fits in available width (creating a line)
|
if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint)
|
||||||
|
break;
|
||||||
// entire text fits, no need to wrap
|
|
||||||
if (textLength == text.Length)
|
|
||||||
return textLength;
|
|
||||||
|
|
||||||
// current line ends at word, next character is space, perfect place to wrap
|
lastSpaceIndex--;
|
||||||
if (text[textLength - 1] != space && text[textLength] == space)
|
|
||||||
return textLength;
|
|
||||||
|
|
||||||
// find last space within the available text to wrap
|
|
||||||
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(space);
|
|
||||||
|
|
||||||
// text contains space that can be used to wrap
|
|
||||||
if (lastSpaceIndex > 0)
|
|
||||||
return lastSpaceIndex;
|
|
||||||
|
|
||||||
// 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 : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// text contains space that can be used to wrap
|
||||||
|
if (lastSpaceIndex > 1 && 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 ? (endIndex, endIndex + 1) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Draw(TextDrawingRequest request)
|
public virtual void Draw(TextDrawingRequest request)
|
||||||
{
|
{
|
||||||
var fontMetrics = Style.ToFontMetrics();
|
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);
|
var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex, Style);
|
||||||
request.Canvas.DrawText(text, Position.Zero, 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
|
// draw underline
|
||||||
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
|
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
|
// draw stroke
|
||||||
if ((Style.HasStrikethrough ?? false) && fontMetrics.StrikeoutPosition.HasValue)
|
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)
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,47 @@ using QuestPDF.Infrastructure;
|
|||||||
|
|
||||||
namespace QuestPDF.Elements.Text
|
namespace QuestPDF.Elements.Text
|
||||||
{
|
{
|
||||||
internal class TextBlock : Element, IStateResettable
|
internal class TextBlock : Element, IStateResettable, IContent
|
||||||
{
|
{
|
||||||
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
||||||
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
|
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
|
||||||
|
|
||||||
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
|
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
|
||||||
|
|
||||||
private Queue<ITextBlockItem> RenderingQueue { get; set; }
|
private Queue<ITextBlockItem> RenderingQueue { get; set; }
|
||||||
private int CurrentElementIndex { get; set; }
|
private int CurrentElementIndex { get; set; }
|
||||||
|
|
||||||
|
private bool FontFallbackApplied { get; set; } = false;
|
||||||
|
|
||||||
public void ResetState()
|
public void ResetState()
|
||||||
{
|
{
|
||||||
RenderingQueue = new Queue<ITextBlockItem>(Items);
|
ApplyFontFallback();
|
||||||
|
InitializeQueue();
|
||||||
CurrentElementIndex = 0;
|
CurrentElementIndex = 0;
|
||||||
|
|
||||||
|
void InitializeQueue()
|
||||||
|
{
|
||||||
|
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
||||||
|
if (RenderingQueue == null)
|
||||||
|
{
|
||||||
|
RenderingQueue = new Queue<ITextBlockItem>(Items);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderingQueue.Clear();
|
||||||
|
|
||||||
|
foreach (var item in Items)
|
||||||
|
RenderingQueue.Enqueue(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyFontFallback()
|
||||||
|
{
|
||||||
|
if (FontFallbackApplied)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Items = Items.ApplyFontFallback().ToList();
|
||||||
|
FontFallbackApplied = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override SpacePlan Measure(Size availableSpace)
|
internal override SpacePlan Measure(Size availableSpace)
|
||||||
@@ -170,6 +197,8 @@ namespace QuestPDF.Elements.Text
|
|||||||
|
|
||||||
StartIndex = currentItemIndex,
|
StartIndex = currentItemIndex,
|
||||||
AvailableWidth = availableWidth - currentWidth,
|
AvailableWidth = availableWidth - currentWidth,
|
||||||
|
|
||||||
|
IsFirstElementInBlock = currentElement == Items.First(),
|
||||||
IsFirstElementInLine = !currentLineElements.Any()
|
IsFirstElementInLine = !currentLineElements.Any()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace QuestPDF.Fluent
|
|||||||
{
|
{
|
||||||
var container = new Container();
|
var container = new Container();
|
||||||
Decoration.Before = container;
|
Decoration.Before = container;
|
||||||
return container;
|
return container.RepeatContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Before(Action<IContainer> handler)
|
public void Before(Action<IContainer> handler)
|
||||||
@@ -36,7 +36,7 @@ namespace QuestPDF.Fluent
|
|||||||
{
|
{
|
||||||
var container = new Container();
|
var container = new Container();
|
||||||
Decoration.After = container;
|
Decoration.After = container;
|
||||||
return container;
|
return container.RepeatContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void After(Action<IContainer> handler)
|
public void After(Action<IContainer> handler)
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -195,5 +195,13 @@ namespace QuestPDF.Fluent
|
|||||||
{
|
{
|
||||||
return element.Element(new ScaleToFit());
|
return element.Element(new ScaleToFit());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static IContainer RepeatContent(this IContainer element, bool enabled = true)
|
||||||
|
{
|
||||||
|
return element.Element(new RepeatContent
|
||||||
|
{
|
||||||
|
Repeat = enabled
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,18 @@ namespace QuestPDF.Fluent
|
|||||||
{
|
{
|
||||||
public class TextSpanDescriptor
|
public class TextSpanDescriptor
|
||||||
{
|
{
|
||||||
internal TextStyle TextStyle { get; }
|
internal TextStyle TextStyle = TextStyle.Default;
|
||||||
|
internal Action<TextStyle> AssignTextStyle { get; }
|
||||||
|
|
||||||
internal TextSpanDescriptor(TextStyle textStyle)
|
internal TextSpanDescriptor(Action<TextStyle> assignTextStyle)
|
||||||
{
|
{
|
||||||
TextStyle = textStyle;
|
AssignTextStyle = assignTextStyle;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void MutateTextStyle(Func<TextStyle, TextStyle> handler)
|
||||||
|
{
|
||||||
|
TextStyle = handler(TextStyle);
|
||||||
|
AssignTextStyle(TextStyle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,16 +31,17 @@ namespace QuestPDF.Fluent
|
|||||||
|
|
||||||
public class TextPageNumberDescriptor : TextSpanDescriptor
|
public class TextPageNumberDescriptor : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
internal PageNumberFormatter FormatFunction { get; private set; } = x => x?.ToString() ?? string.Empty;
|
internal Action<PageNumberFormatter> AssignFormatFunction { get; }
|
||||||
|
|
||||||
internal TextPageNumberDescriptor(TextStyle textStyle) : base(textStyle)
|
internal TextPageNumberDescriptor(Action<TextStyle> assignTextStyle, Action<PageNumberFormatter> assignFormatFunction) : base(assignTextStyle)
|
||||||
{
|
{
|
||||||
|
AssignFormatFunction = assignFormatFunction;
|
||||||
|
AssignFormatFunction(x => x?.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public TextPageNumberDescriptor Format(PageNumberFormatter formatter)
|
public TextPageNumberDescriptor Format(PageNumberFormatter formatter)
|
||||||
{
|
{
|
||||||
FormatFunction = formatter ?? FormatFunction;
|
AssignFormatFunction(formatter);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,7 +49,7 @@ namespace QuestPDF.Fluent
|
|||||||
public class TextDescriptor
|
public class TextDescriptor
|
||||||
{
|
{
|
||||||
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
|
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
|
||||||
private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
|
private TextStyle? DefaultStyle { get; set; }
|
||||||
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
||||||
private float Spacing { get; set; } = 0f;
|
private float Spacing { get; set; } = 0f;
|
||||||
|
|
||||||
@@ -91,19 +99,15 @@ namespace QuestPDF.Fluent
|
|||||||
|
|
||||||
public TextSpanDescriptor Span(string? text)
|
public TextSpanDescriptor Span(string? text)
|
||||||
{
|
{
|
||||||
var style = DefaultStyle.Clone();
|
|
||||||
var descriptor = new TextSpanDescriptor(style);
|
|
||||||
|
|
||||||
if (text == null)
|
if (text == null)
|
||||||
return descriptor;
|
return new TextSpanDescriptor(_ => { });
|
||||||
|
|
||||||
var items = text
|
var items = text
|
||||||
.Replace("\r", string.Empty)
|
.Replace("\r", string.Empty)
|
||||||
.Split(new[] { '\n' }, StringSplitOptions.None)
|
.Split(new[] { '\n' }, StringSplitOptions.None)
|
||||||
.Select(x => new TextBlockSpan
|
.Select(x => new TextBlockSpan
|
||||||
{
|
{
|
||||||
Text = x,
|
Text = x
|
||||||
Style = style
|
|
||||||
})
|
})
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
@@ -118,7 +122,7 @@ namespace QuestPDF.Fluent
|
|||||||
.ToList()
|
.ToList()
|
||||||
.ForEach(TextBlocks.Add);
|
.ForEach(TextBlocks.Add);
|
||||||
|
|
||||||
return descriptor;
|
return new TextSpanDescriptor(x => items.ForEach(y => y.Style = x));
|
||||||
}
|
}
|
||||||
|
|
||||||
public TextSpanDescriptor Line(string? text)
|
public TextSpanDescriptor Line(string? text)
|
||||||
@@ -134,16 +138,10 @@ namespace QuestPDF.Fluent
|
|||||||
|
|
||||||
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
|
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
|
||||||
{
|
{
|
||||||
var style = DefaultStyle.Clone();
|
var textBlockItem = new TextBlockPageNumber();
|
||||||
var descriptor = new TextPageNumberDescriptor(DefaultStyle);
|
AddItemToLastTextBlock(textBlockItem);
|
||||||
|
|
||||||
AddItemToLastTextBlock(new TextBlockPageNumber
|
return new TextPageNumberDescriptor(x => textBlockItem.Style = x, x => textBlockItem.Source = context => x(pageNumber(context)));
|
||||||
{
|
|
||||||
Source = context => descriptor.FormatFunction(pageNumber(context)),
|
|
||||||
Style = style
|
|
||||||
});
|
|
||||||
|
|
||||||
return descriptor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public TextPageNumberDescriptor CurrentPageNumber()
|
public TextPageNumberDescriptor CurrentPageNumber()
|
||||||
@@ -174,7 +172,7 @@ namespace QuestPDF.Fluent
|
|||||||
|
|
||||||
public TextPageNumberDescriptor PageNumberWithinSection(string locationName)
|
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)
|
public TextPageNumberDescriptor TotalPagesWithinSection(string locationName)
|
||||||
@@ -185,22 +183,19 @@ namespace QuestPDF.Fluent
|
|||||||
public TextSpanDescriptor SectionLink(string? text, string sectionName)
|
public TextSpanDescriptor SectionLink(string? text, string sectionName)
|
||||||
{
|
{
|
||||||
if (IsNullOrEmpty(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);
|
|
||||||
|
|
||||||
if (IsNullOrEmpty(text))
|
if (IsNullOrEmpty(text))
|
||||||
return descriptor;
|
return new TextSpanDescriptor(_ => { });
|
||||||
|
|
||||||
AddItemToLastTextBlock(new TextBlockSectionlLink
|
var textBlockItem = new TextBlockSectionLink
|
||||||
{
|
{
|
||||||
Style = style,
|
|
||||||
Text = text,
|
Text = text,
|
||||||
SectionName = sectionName
|
SectionName = sectionName
|
||||||
});
|
};
|
||||||
|
|
||||||
return descriptor;
|
AddItemToLastTextBlock(textBlockItem);
|
||||||
|
return new TextSpanDescriptor(x => textBlockItem.Style = x);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
|
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
|
||||||
@@ -212,22 +207,19 @@ namespace QuestPDF.Fluent
|
|||||||
public TextSpanDescriptor Hyperlink(string? text, string url)
|
public TextSpanDescriptor Hyperlink(string? text, string url)
|
||||||
{
|
{
|
||||||
if (IsNullOrEmpty(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);
|
|
||||||
|
|
||||||
if (IsNullOrEmpty(text))
|
if (IsNullOrEmpty(text))
|
||||||
return descriptor;
|
return new TextSpanDescriptor(_ => { });
|
||||||
|
|
||||||
AddItemToLastTextBlock(new TextBlockHyperlink
|
var textBlockItem = new TextBlockHyperlink
|
||||||
{
|
{
|
||||||
Style = style,
|
|
||||||
Text = text,
|
Text = text,
|
||||||
Url = url
|
Url = url
|
||||||
});
|
};
|
||||||
|
|
||||||
return descriptor;
|
AddItemToLastTextBlock(textBlockItem);
|
||||||
|
return new TextSpanDescriptor(x => textBlockItem.Style = x);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
|
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
|
||||||
@@ -251,14 +243,23 @@ namespace QuestPDF.Fluent
|
|||||||
internal void Compose(IContainer container)
|
internal void Compose(IContainer container)
|
||||||
{
|
{
|
||||||
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
|
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
|
||||||
|
|
||||||
|
if (DefaultStyle != null)
|
||||||
|
container = container.DefaultTextStyle(DefaultStyle);
|
||||||
|
|
||||||
container.DefaultTextStyle(DefaultStyle).Column(column =>
|
if (TextBlocks.Count == 1)
|
||||||
|
{
|
||||||
|
container.Element(TextBlocks.First());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.Column(column =>
|
||||||
{
|
{
|
||||||
column.Spacing(Spacing);
|
column.Spacing(Spacing);
|
||||||
|
|
||||||
foreach (var textBlock in TextBlocks)
|
foreach (var textBlock in TextBlocks)
|
||||||
column.Item().Element(textBlock);
|
column.Item().Element(textBlock);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,114 +11,156 @@ namespace QuestPDF.Fluent
|
|||||||
if (style == null)
|
if (style == null)
|
||||||
return descriptor;
|
return descriptor;
|
||||||
|
|
||||||
descriptor.TextStyle.OverrideStyle(style);
|
descriptor.MutateTextStyle(x => x.OverrideStyle(style));
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static T Fallback<T>(this T descriptor, TextStyle? value = null) where T : TextSpanDescriptor
|
||||||
|
{
|
||||||
|
descriptor.TextStyle.Fallback = value;
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Fallback<T>(this T descriptor, Func<TextStyle, TextStyle> handler) where T : TextSpanDescriptor
|
||||||
|
{
|
||||||
|
return descriptor.Fallback(handler(TextStyle.Default));
|
||||||
|
}
|
||||||
|
|
||||||
public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
descriptor.TextStyle.Color = value;
|
descriptor.MutateTextStyle(x => x.FontColor(value));
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T BackgroundColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
public static T BackgroundColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
descriptor.TextStyle.BackgroundColor = value;
|
descriptor.MutateTextStyle(x => x.BackgroundColor(value));
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T FontFamily<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
public static T FontFamily<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
descriptor.TextStyle.FontFamily = value;
|
descriptor.MutateTextStyle(x => x.FontFamily(value));
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T FontSize<T>(this T descriptor, float value) where T : TextSpanDescriptor
|
public static T FontSize<T>(this T descriptor, float value) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
descriptor.TextStyle.Size = value;
|
descriptor.MutateTextStyle(x => x.FontSize(value));
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T LineHeight<T>(this T descriptor, float value) where T : TextSpanDescriptor
|
public static T LineHeight<T>(this T descriptor, float value) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
descriptor.TextStyle.LineHeight = value;
|
descriptor.MutateTextStyle(x => x.LineHeight(value));
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T Italic<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
public static T Italic<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
descriptor.TextStyle.IsItalic = value;
|
descriptor.MutateTextStyle(x => x.Italic(value));
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T Strikethrough<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
public static T Strikethrough<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
descriptor.TextStyle.HasStrikethrough = value;
|
descriptor.MutateTextStyle(x => x.Strikethrough(value));
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T Underline<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
public static T Underline<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
descriptor.TextStyle.HasUnderline = value;
|
descriptor.MutateTextStyle(x => x.Underline(value));
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T WrapAnywhere<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||||
|
{
|
||||||
|
descriptor.MutateTextStyle(x => x.WrapAnywhere(value));
|
||||||
return descriptor;
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Weight
|
#region Weight
|
||||||
|
|
||||||
public static T Weight<T>(this T descriptor, FontWeight weight) where T : TextSpanDescriptor
|
|
||||||
{
|
|
||||||
descriptor.TextStyle.FontWeight = weight;
|
|
||||||
return descriptor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T Thin<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T Thin<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.Thin);
|
descriptor.MutateTextStyle(x => x.Thin());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T ExtraLight<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T ExtraLight<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.ExtraLight);
|
descriptor.MutateTextStyle(x => x.ExtraLight());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T Light<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T Light<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.Light);
|
descriptor.MutateTextStyle(x => x.Light());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T NormalWeight<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T NormalWeight<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.Normal);
|
descriptor.MutateTextStyle(x => x.NormalWeight());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T Medium<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T Medium<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.Medium);
|
descriptor.MutateTextStyle(x => x.Medium());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T SemiBold<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T SemiBold<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.SemiBold);
|
descriptor.MutateTextStyle(x => x.SemiBold());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T Bold<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T Bold<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.Bold);
|
descriptor.MutateTextStyle(x => x.Bold());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T ExtraBold<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T ExtraBold<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.ExtraBold);
|
descriptor.MutateTextStyle(x => x.ExtraBold());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T Black<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T Black<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.Black);
|
descriptor.MutateTextStyle(x => x.Black());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T ExtraBlack<T>(this T descriptor) where T : TextSpanDescriptor
|
public static T ExtraBlack<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
{
|
{
|
||||||
return descriptor.Weight(FontWeight.ExtraBlack);
|
descriptor.MutateTextStyle(x => x.ExtraBlack());
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Position
|
||||||
|
public static T NormalPosition<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
|
{
|
||||||
|
descriptor.MutateTextStyle(x => x.NormalPosition());
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Subscript<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
|
{
|
||||||
|
descriptor.MutateTextStyle(x => x.Subscript());
|
||||||
|
return descriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Superscript<T>(this T descriptor) where T : TextSpanDescriptor
|
||||||
|
{
|
||||||
|
descriptor.MutateTextStyle(x => x.Superscript());
|
||||||
|
return descriptor;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|||||||
@@ -6,14 +6,6 @@ namespace QuestPDF.Fluent
|
|||||||
{
|
{
|
||||||
public static class TextStyleExtensions
|
public static class TextStyleExtensions
|
||||||
{
|
{
|
||||||
private static TextStyle Mutate(this TextStyle style, Action<TextStyle> handler)
|
|
||||||
{
|
|
||||||
style = style.Clone();
|
|
||||||
|
|
||||||
handler(style);
|
|
||||||
return style;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")]
|
[Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")]
|
||||||
public static TextStyle Color(this TextStyle style, string value)
|
public static TextStyle Color(this TextStyle style, string value)
|
||||||
{
|
{
|
||||||
@@ -22,12 +14,12 @@ namespace QuestPDF.Fluent
|
|||||||
|
|
||||||
public static TextStyle FontColor(this TextStyle style, string value)
|
public static TextStyle FontColor(this TextStyle style, string value)
|
||||||
{
|
{
|
||||||
return style.Mutate(x => x.Color = value);
|
return style.Mutate(TextStyleProperty.Color, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TextStyle BackgroundColor(this TextStyle style, string value)
|
public static TextStyle BackgroundColor(this TextStyle style, string value)
|
||||||
{
|
{
|
||||||
return style.Mutate(x => x.BackgroundColor = value);
|
return style.Mutate(TextStyleProperty.BackgroundColor, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontFamily method.")]
|
[Obsolete("This element has been renamed since version 2022.3. Please use the FontFamily method.")]
|
||||||
@@ -38,7 +30,7 @@ namespace QuestPDF.Fluent
|
|||||||
|
|
||||||
public static TextStyle FontFamily(this TextStyle style, string value)
|
public static TextStyle FontFamily(this TextStyle style, string value)
|
||||||
{
|
{
|
||||||
return style.Mutate(x => x.FontFamily = value);
|
return style.Mutate(TextStyleProperty.FontFamily, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")]
|
[Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")]
|
||||||
@@ -49,34 +41,39 @@ namespace QuestPDF.Fluent
|
|||||||
|
|
||||||
public static TextStyle FontSize(this TextStyle style, float value)
|
public static TextStyle FontSize(this TextStyle style, float value)
|
||||||
{
|
{
|
||||||
return style.Mutate(x => x.Size = value);
|
return style.Mutate(TextStyleProperty.Size, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TextStyle LineHeight(this TextStyle style, float value)
|
public static TextStyle LineHeight(this TextStyle style, float value)
|
||||||
{
|
{
|
||||||
return style.Mutate(x => x.LineHeight = value);
|
return style.Mutate(TextStyleProperty.LineHeight, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TextStyle Italic(this TextStyle style, bool value = true)
|
public static TextStyle Italic(this TextStyle style, bool value = true)
|
||||||
{
|
{
|
||||||
return style.Mutate(x => x.IsItalic = value);
|
return style.Mutate(TextStyleProperty.IsItalic, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TextStyle Strikethrough(this TextStyle style, bool value = true)
|
public static TextStyle Strikethrough(this TextStyle style, bool value = true)
|
||||||
{
|
{
|
||||||
return style.Mutate(x => x.HasStrikethrough = value);
|
return style.Mutate(TextStyleProperty.HasStrikethrough, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TextStyle Underline(this TextStyle style, bool value = true)
|
public static TextStyle Underline(this TextStyle style, bool value = true)
|
||||||
{
|
{
|
||||||
return style.Mutate(x => x.HasUnderline = value);
|
return style.Mutate(TextStyleProperty.HasUnderline, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TextStyle WrapAnywhere(this TextStyle style, bool value = true)
|
||||||
|
{
|
||||||
|
return style.Mutate(TextStyleProperty.WrapAnywhere, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Weight
|
#region Weight
|
||||||
|
|
||||||
public static TextStyle Weight(this TextStyle style, FontWeight weight)
|
public static TextStyle Weight(this TextStyle style, FontWeight weight)
|
||||||
{
|
{
|
||||||
return style.Mutate(x => x.FontWeight = weight);
|
return style.Mutate(TextStyleProperty.FontWeight, weight);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static TextStyle Thin(this TextStyle style)
|
public static TextStyle Thin(this TextStyle style)
|
||||||
@@ -128,7 +125,45 @@ namespace QuestPDF.Fluent
|
|||||||
{
|
{
|
||||||
return style.Weight(FontWeight.ExtraBlack);
|
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)
|
||||||
|
{
|
||||||
|
return style.Mutate(TextStyleProperty.FontPosition, fontPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Fallback
|
||||||
|
|
||||||
|
public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
|
||||||
|
{
|
||||||
|
return style.Mutate(TextStyleProperty.Fallback, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TextStyle Fallback(this TextStyle style, Func<TextStyle, TextStyle> handler)
|
||||||
|
{
|
||||||
|
return style.Fallback(handler(TextStyle.Default));
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
public const string CourierNew = "Courier New";
|
public const string CourierNew = "Courier New";
|
||||||
public const string Georgia = "Georgia";
|
public const string Georgia = "Georgia";
|
||||||
public const string Impact = "Impact";
|
public const string Impact = "Impact";
|
||||||
|
public const string Lato = "Lato";
|
||||||
public const string LucidaConsole = "Lucida Console";
|
public const string LucidaConsole = "Lucida Console";
|
||||||
public const string SegoeSD = "Segoe SD";
|
public const string SegoeSD = "Segoe SD";
|
||||||
public const string SegoeUI = "Segoe UI";
|
public const string SegoeUI = "Segoe UI";
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Linq;
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using QuestPDF.Drawing;
|
||||||
using QuestPDF.Infrastructure;
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
namespace QuestPDF.Helpers
|
namespace QuestPDF.Helpers
|
||||||
@@ -41,15 +42,23 @@ namespace QuestPDF.Helpers
|
|||||||
|
|
||||||
internal static string PrettifyName(this string text)
|
internal static string PrettifyName(this string text)
|
||||||
{
|
{
|
||||||
return Regex.Replace(text, @"([a-z])([A-Z])", "$1 $2");
|
return Regex.Replace(text, @"([a-z])([A-Z])", "$1 $2", RegexOptions.Compiled);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void VisitChildren(this Element? element, Action<Element?> handler)
|
internal static void VisitChildren(this Element? element, Action<Element?> handler)
|
||||||
{
|
{
|
||||||
foreach (var child in element.GetChildren().Where(x => x != null))
|
if (element == null)
|
||||||
VisitChildren(child, handler);
|
return;
|
||||||
|
|
||||||
|
foreach (var child in element.GetChildren())
|
||||||
|
VisitChildren(child, handler);
|
||||||
|
|
||||||
handler(element);
|
handler(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool IsNegative(this Size size)
|
||||||
|
{
|
||||||
|
return size.Width < 0f || size.Height < 0f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ namespace QuestPDF.Helpers
|
|||||||
{
|
{
|
||||||
public static class Placeholders
|
public static class Placeholders
|
||||||
{
|
{
|
||||||
public static readonly Random Random = new();
|
public static readonly Random Random = new Random(0);
|
||||||
|
|
||||||
#region Word Cache
|
#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;
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace QuestPDF.Infrastructure
|
namespace QuestPDF.Infrastructure
|
||||||
@@ -7,7 +8,7 @@ namespace QuestPDF.Infrastructure
|
|||||||
void Translate(Position vector);
|
void Translate(Position vector);
|
||||||
|
|
||||||
void DrawRectangle(Position vector, Size size, string color);
|
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 DrawImage(SKImage image, Position position, Size size);
|
||||||
|
|
||||||
void DrawHyperlink(string url, Size size);
|
void DrawHyperlink(string url, Size size);
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace QuestPDF.Infrastructure
|
||||||
|
{
|
||||||
|
internal interface IContent
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,81 +3,38 @@ using QuestPDF.Helpers;
|
|||||||
|
|
||||||
namespace QuestPDF.Infrastructure
|
namespace QuestPDF.Infrastructure
|
||||||
{
|
{
|
||||||
public class TextStyle
|
public record TextStyle
|
||||||
{
|
{
|
||||||
internal bool HasGlobalStyleApplied { get; private set; }
|
|
||||||
|
|
||||||
internal string? Color { get; set; }
|
internal string? Color { get; set; }
|
||||||
internal string? BackgroundColor { get; set; }
|
internal string? BackgroundColor { get; set; }
|
||||||
internal string? FontFamily { get; set; }
|
internal string? FontFamily { get; set; }
|
||||||
internal float? Size { get; set; }
|
internal float? Size { get; set; }
|
||||||
internal float? LineHeight { get; set; }
|
internal float? LineHeight { get; set; }
|
||||||
internal FontWeight? FontWeight { get; set; }
|
internal FontWeight? FontWeight { get; set; }
|
||||||
|
internal FontPosition? FontPosition { get; set; }
|
||||||
internal bool? IsItalic { get; set; }
|
internal bool? IsItalic { get; set; }
|
||||||
internal bool? HasStrikethrough { get; set; }
|
internal bool? HasStrikethrough { get; set; }
|
||||||
internal bool? HasUnderline { get; set; }
|
internal bool? HasUnderline { get; set; }
|
||||||
|
internal bool? WrapAnywhere { get; set; }
|
||||||
|
|
||||||
internal object PaintKey { get; private set; }
|
internal TextStyle? Fallback { get; set; }
|
||||||
internal object FontMetricsKey { get; private set; }
|
|
||||||
|
internal static TextStyle LibraryDefault { get; } = new()
|
||||||
internal static TextStyle LibraryDefault => new TextStyle
|
|
||||||
{
|
{
|
||||||
Color = Colors.Black,
|
Color = Colors.Black,
|
||||||
BackgroundColor = Colors.Transparent,
|
BackgroundColor = Colors.Transparent,
|
||||||
FontFamily = Fonts.Calibri,
|
FontFamily = Fonts.Lato,
|
||||||
Size = 12,
|
Size = 12,
|
||||||
LineHeight = 1.2f,
|
LineHeight = 1.2f,
|
||||||
FontWeight = Infrastructure.FontWeight.Normal,
|
FontWeight = Infrastructure.FontWeight.Normal,
|
||||||
|
FontPosition = Infrastructure.FontPosition.Normal,
|
||||||
IsItalic = false,
|
IsItalic = false,
|
||||||
HasStrikethrough = false,
|
HasStrikethrough = false,
|
||||||
HasUnderline = false
|
HasUnderline = false,
|
||||||
|
WrapAnywhere = false,
|
||||||
|
Fallback = null
|
||||||
};
|
};
|
||||||
|
|
||||||
public static TextStyle Default => new TextStyle();
|
public static TextStyle Default { get; } = new();
|
||||||
|
|
||||||
internal void ApplyGlobalStyle(TextStyle globalStyle)
|
|
||||||
{
|
|
||||||
if (HasGlobalStyleApplied)
|
|
||||||
return;
|
|
||||||
|
|
||||||
HasGlobalStyleApplied = true;
|
|
||||||
|
|
||||||
ApplyParentStyle(globalStyle);
|
|
||||||
PaintKey ??= (FontFamily, Size, FontWeight, IsItalic, Color);
|
|
||||||
FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void ApplyParentStyle(TextStyle parentStyle)
|
|
||||||
{
|
|
||||||
Color ??= parentStyle.Color;
|
|
||||||
BackgroundColor ??= parentStyle.BackgroundColor;
|
|
||||||
FontFamily ??= parentStyle.FontFamily;
|
|
||||||
Size ??= parentStyle.Size;
|
|
||||||
LineHeight ??= parentStyle.LineHeight;
|
|
||||||
FontWeight ??= parentStyle.FontWeight;
|
|
||||||
IsItalic ??= parentStyle.IsItalic;
|
|
||||||
HasStrikethrough ??= parentStyle.HasStrikethrough;
|
|
||||||
HasUnderline ??= parentStyle.HasUnderline;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void OverrideStyle(TextStyle parentStyle)
|
|
||||||
{
|
|
||||||
Color = parentStyle.Color ?? Color;
|
|
||||||
BackgroundColor = parentStyle.BackgroundColor ?? BackgroundColor;
|
|
||||||
FontFamily = parentStyle.FontFamily ?? FontFamily;
|
|
||||||
Size = parentStyle.Size ?? Size;
|
|
||||||
LineHeight = parentStyle.LineHeight ?? LineHeight;
|
|
||||||
FontWeight = parentStyle.FontWeight ?? FontWeight;
|
|
||||||
IsItalic = parentStyle.IsItalic ?? IsItalic;
|
|
||||||
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
|
|
||||||
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal TextStyle Clone()
|
|
||||||
{
|
|
||||||
var clone = (TextStyle)MemberwiseClone();
|
|
||||||
clone.HasGlobalStyleApplied = false;
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using QuestPDF.Fluent;
|
||||||
|
|
||||||
|
namespace QuestPDF.Infrastructure
|
||||||
|
{
|
||||||
|
internal enum TextStyleProperty
|
||||||
|
{
|
||||||
|
Color,
|
||||||
|
BackgroundColor,
|
||||||
|
FontFamily,
|
||||||
|
Size,
|
||||||
|
LineHeight,
|
||||||
|
FontWeight,
|
||||||
|
FontPosition,
|
||||||
|
IsItalic,
|
||||||
|
HasStrikethrough,
|
||||||
|
HasUnderline,
|
||||||
|
WrapAnywhere,
|
||||||
|
Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class TextStyleManager
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new();
|
||||||
|
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleApplyGlobalCache = new();
|
||||||
|
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleOverrideCache = new();
|
||||||
|
|
||||||
|
public static TextStyle Mutate(this TextStyle origin, TextStyleProperty property, object value)
|
||||||
|
{
|
||||||
|
var cacheKey = (origin, property, value);
|
||||||
|
return TextStyleMutateCache.GetOrAdd(cacheKey, x => MutateStyle(x.origin, x.property, x.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextStyle MutateStyle(TextStyle origin, TextStyleProperty property, object? value, bool overrideValue = true)
|
||||||
|
{
|
||||||
|
if (overrideValue && value == null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.Color)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.Color != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (string?)value;
|
||||||
|
|
||||||
|
if (origin.Color == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { Color = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.BackgroundColor)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.BackgroundColor != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (string?)value;
|
||||||
|
|
||||||
|
if (origin.BackgroundColor == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { BackgroundColor = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.FontFamily)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.FontFamily != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (string?)value;
|
||||||
|
|
||||||
|
if (origin.FontFamily == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { FontFamily = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.Size)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.Size != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (float?)value;
|
||||||
|
|
||||||
|
if (origin.Size == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { Size = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.LineHeight)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.LineHeight != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (float?)value;
|
||||||
|
|
||||||
|
if (origin.LineHeight == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { LineHeight = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.FontWeight)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.FontWeight != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (FontWeight?)value;
|
||||||
|
|
||||||
|
if (origin.FontWeight == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { FontWeight = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.FontPosition)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.FontPosition != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (FontPosition?)value;
|
||||||
|
|
||||||
|
if (origin.FontPosition == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { FontPosition = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.IsItalic)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.IsItalic != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (bool?)value;
|
||||||
|
|
||||||
|
if (origin.IsItalic == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { IsItalic = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.HasStrikethrough)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.HasStrikethrough != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (bool?)value;
|
||||||
|
|
||||||
|
if (origin.HasStrikethrough == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { HasStrikethrough = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.HasUnderline)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.HasUnderline != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (bool?)value;
|
||||||
|
|
||||||
|
if (origin.HasUnderline == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { HasUnderline = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.WrapAnywhere)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.WrapAnywhere != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (bool?)value;
|
||||||
|
|
||||||
|
if (origin.WrapAnywhere == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { WrapAnywhere = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property == TextStyleProperty.Fallback)
|
||||||
|
{
|
||||||
|
if (!overrideValue && origin.Fallback != null)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
var castedValue = (TextStyle?)value;
|
||||||
|
|
||||||
|
if (origin.Fallback == castedValue)
|
||||||
|
return origin;
|
||||||
|
|
||||||
|
return origin with { Fallback = castedValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(property), property, "Expected to mutate the TextStyle object. Provided property type is not supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static TextStyle ApplyGlobalStyle(this TextStyle style, TextStyle parent)
|
||||||
|
{
|
||||||
|
var cacheKey = (style, parent);
|
||||||
|
return TextStyleApplyGlobalCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, overrideStyle: false).ApplyFontFallback());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextStyle ApplyFontFallback(this TextStyle style)
|
||||||
|
{
|
||||||
|
var targetFallbackStyle = style
|
||||||
|
?.Fallback
|
||||||
|
?.ApplyStyle(style, overrideStyle: false, applyFallback: false)
|
||||||
|
?.ApplyFontFallback();
|
||||||
|
|
||||||
|
return MutateStyle(style, TextStyleProperty.Fallback, targetFallbackStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
|
||||||
|
{
|
||||||
|
var cacheKey = (style, parent);
|
||||||
|
|
||||||
|
return TextStyleOverrideCache.GetOrAdd(cacheKey, key =>
|
||||||
|
{
|
||||||
|
var result = ApplyStyle(key.origin, key.parent);
|
||||||
|
return MutateStyle(result, TextStyleProperty.Fallback, key.parent.Fallback);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TextStyle ApplyStyle(this TextStyle style, TextStyle parent, bool overrideStyle = true, bool applyFallback = true)
|
||||||
|
{
|
||||||
|
var result = style;
|
||||||
|
|
||||||
|
result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.HasStrikethrough, parent.HasStrikethrough, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideStyle);
|
||||||
|
result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideStyle);
|
||||||
|
|
||||||
|
if (applyFallback)
|
||||||
|
result = MutateStyle(result, TextStyleProperty.Fallback, parent.Fallback, overrideStyle);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using System;
|
||||||
|
using QuestPDF.Drawing;
|
||||||
|
using QuestPDF.Fluent;
|
||||||
|
using QuestPDF.Helpers;
|
||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace QuestPDF.Previewer
|
||||||
|
{
|
||||||
|
public class ExceptionDocument : IDocument
|
||||||
|
{
|
||||||
|
private Exception Exception { get; }
|
||||||
|
|
||||||
|
public ExceptionDocument(Exception exception)
|
||||||
|
{
|
||||||
|
Exception = exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DocumentMetadata GetMetadata()
|
||||||
|
{
|
||||||
|
return DocumentMetadata.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Compose(IDocumentContainer document)
|
||||||
|
{
|
||||||
|
document.Page(page =>
|
||||||
|
{
|
||||||
|
page.ContinuousSize(PageSizes.A4.Width);
|
||||||
|
page.Margin(50);
|
||||||
|
page.DefaultTextStyle(x => x.FontSize(16));
|
||||||
|
|
||||||
|
page.Foreground().PaddingTop(5).Border(10).BorderColor(Colors.Red.Medium);
|
||||||
|
|
||||||
|
page.Header()
|
||||||
|
.ShowOnce()
|
||||||
|
.PaddingBottom(5)
|
||||||
|
.Row(row =>
|
||||||
|
{
|
||||||
|
row.Spacing(15);
|
||||||
|
|
||||||
|
row.AutoItem()
|
||||||
|
.PaddingTop(15)
|
||||||
|
.Width(48)
|
||||||
|
.AspectRatio(1)
|
||||||
|
.Canvas((canvas, size) =>
|
||||||
|
{
|
||||||
|
const float iconSize = 24;
|
||||||
|
using var iconPath = SKPath.ParseSvgPathData("M23,12L20.56,14.78L20.9,18.46L17.29,19.28L15.4,22.46L12,21L8.6,22.47L6.71,19.29L3.1,18.47L3.44,14.78L1,12L3.44,9.21L3.1,5.53L6.71,4.72L8.6,1.54L12,3L15.4,1.54L17.29,4.72L20.9,5.54L20.56,9.22L23,12M20.33,12L18.5,9.89L18.74,7.1L16,6.5L14.58,4.07L12,5.18L9.42,4.07L8,6.5L5.26,7.09L5.5,9.88L3.67,12L5.5,14.1L5.26,16.9L8,17.5L9.42,19.93L12,18.81L14.58,19.92L16,17.5L18.74,16.89L18.5,14.1L20.33,12M11,15H13V17H11V15M11,7H13V13H11V7");
|
||||||
|
|
||||||
|
using var paint = new SKPaint()
|
||||||
|
{
|
||||||
|
Color = SKColors.Red,
|
||||||
|
IsAntialias = true
|
||||||
|
};
|
||||||
|
|
||||||
|
canvas.Scale(size.Width / iconSize);
|
||||||
|
canvas.DrawPath(iconPath, paint);
|
||||||
|
});
|
||||||
|
|
||||||
|
row.RelativeItem()
|
||||||
|
.Column(column =>
|
||||||
|
{
|
||||||
|
column.Item().Text("Exception").FontSize(36).FontColor(Colors.Red.Medium).Bold();
|
||||||
|
column.Item().PaddingTop(-10).Text("Don't panic! Just analyze what's happened...").FontSize(18).FontColor(Colors.Red.Medium).Bold();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
page.Content().PaddingVertical(20).Column(column =>
|
||||||
|
{
|
||||||
|
var currentException = Exception;
|
||||||
|
|
||||||
|
while (currentException != null)
|
||||||
|
{
|
||||||
|
column.Item()
|
||||||
|
.PaddingTop(25)
|
||||||
|
.PaddingBottom(15)
|
||||||
|
|
||||||
|
.Padding(-10)
|
||||||
|
.Background(Colors.Grey.Lighten3)
|
||||||
|
.Padding(10)
|
||||||
|
|
||||||
|
.Text(text =>
|
||||||
|
{
|
||||||
|
text.DefaultTextStyle(x => x.FontSize(16));
|
||||||
|
|
||||||
|
text.Span(currentException.GetType().Name + ": ").Bold();
|
||||||
|
text.Span(currentException.Message);
|
||||||
|
});
|
||||||
|
|
||||||
|
foreach (var trace in currentException.StackTrace.Split('\n'))
|
||||||
|
{
|
||||||
|
column
|
||||||
|
.Item()
|
||||||
|
.ShowEntire()
|
||||||
|
.BorderBottom(1)
|
||||||
|
.BorderColor(Colors.Grey.Lighten2)
|
||||||
|
.PaddingVertical(5)
|
||||||
|
.Text(trace.Trim())
|
||||||
|
.FontSize(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentException = currentException.InnerException;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(QuestPDF.Previewer.HotReloadManager))]
|
#if NET6_0_OR_GREATER
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
[assembly: System.Reflection.Metadata.MetadataUpdateHandler(typeof(QuestPDF.Previewer.HotReloadManager))]
|
||||||
|
|
||||||
namespace QuestPDF.Previewer
|
namespace QuestPDF.Previewer
|
||||||
{
|
{
|
||||||
@@ -15,3 +19,5 @@ namespace QuestPDF.Previewer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using QuestPDF.Drawing;
|
||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
|
namespace QuestPDF.Previewer
|
||||||
|
{
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
public static void ShowInPreviewer(this IDocument document, int port = 12500)
|
||||||
|
{
|
||||||
|
document.ShowInPreviewerAsync(port).ConfigureAwait(true).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500)
|
||||||
|
{
|
||||||
|
var previewerService = new PreviewerService(port);
|
||||||
|
|
||||||
|
using var cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
|
||||||
|
|
||||||
|
await previewerService.Connect();
|
||||||
|
await RefreshPreview();
|
||||||
|
|
||||||
|
HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();
|
||||||
|
|
||||||
|
await WaitForPreviewerExit(cancellationTokenSource.Token);
|
||||||
|
|
||||||
|
Task RefreshPreview()
|
||||||
|
{
|
||||||
|
var pictures = GetPictures();
|
||||||
|
return previewerService.RefreshPreview(pictures);
|
||||||
|
|
||||||
|
ICollection<PreviewerPicture> GetPictures()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return DocumentGenerator.GeneratePreviewerPictures(document);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
var exceptionDocument = new ExceptionDocument(exception);
|
||||||
|
return DocumentGenerator.GeneratePreviewerPictures(exceptionDocument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task WaitForPreviewerExit(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using QuestPDF.Infrastructure;
|
||||||
|
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
|
||||||
|
namespace QuestPDF.Previewer
|
||||||
|
{
|
||||||
|
internal class PreviewerRefreshCommand
|
||||||
|
{
|
||||||
|
public ICollection<Page> Pages { get; set; }
|
||||||
|
|
||||||
|
public class Page
|
||||||
|
{
|
||||||
|
public string Id { get; } = Guid.NewGuid().ToString("N");
|
||||||
|
|
||||||
|
public float Width { get; init; }
|
||||||
|
public float Height { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
|
||||||
|
using System;
|
||||||
|
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;
|
||||||
|
|
||||||
|
namespace QuestPDF.Previewer
|
||||||
|
{
|
||||||
|
internal class PreviewerService
|
||||||
|
{
|
||||||
|
private int Port { get; }
|
||||||
|
private HttpClient HttpClient { get; }
|
||||||
|
|
||||||
|
public event Action? OnPreviewerStopped;
|
||||||
|
|
||||||
|
private const int RequiredPreviewerVersionMajor = 2022;
|
||||||
|
private const int RequiredPreviewerVersionMinor = 9;
|
||||||
|
|
||||||
|
public PreviewerService(int port)
|
||||||
|
{
|
||||||
|
Port = port;
|
||||||
|
HttpClient = new()
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri($"http://localhost:{port}/"),
|
||||||
|
Timeout = TimeSpan.FromSeconds(1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Connect()
|
||||||
|
{
|
||||||
|
var isAvailable = await IsPreviewerAvailable();
|
||||||
|
|
||||||
|
if (!isAvailable)
|
||||||
|
{
|
||||||
|
StartPreviewer();
|
||||||
|
await WaitForConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
var previewerVersion = await GetPreviewerVersion();
|
||||||
|
CheckVersionCompatibility(previewerVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsPreviewerAvailable()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var result = await HttpClient.GetAsync("/ping");
|
||||||
|
return result.IsSuccessStatusCode;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Version> GetPreviewerVersion()
|
||||||
|
{
|
||||||
|
using var result = await HttpClient.GetAsync("/version");
|
||||||
|
return await result.Content.ReadFromJsonAsync<Version>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartPreviewer()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var process = new Process
|
||||||
|
{
|
||||||
|
StartInfo = new()
|
||||||
|
{
|
||||||
|
FileName = "questpdf-previewer",
|
||||||
|
Arguments = $"{Port}",
|
||||||
|
UseShellExecute = false,
|
||||||
|
CreateNoWindow = true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await process.WaitForExitAsync();
|
||||||
|
process.Dispose();
|
||||||
|
OnPreviewerStopped?.Invoke();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new Exception("Cannot start the QuestPDF Previewer tool. " +
|
||||||
|
"Please install it by executing in terminal the following command: 'dotnet tool install --global QuestPDF.Previewer'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckVersionCompatibility(Version version)
|
||||||
|
{
|
||||||
|
if (version.Major == RequiredPreviewerVersionMajor && version.Minor == RequiredPreviewerVersionMinor)
|
||||||
|
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 {RequiredPreviewerVersionMajor}.{RequiredPreviewerVersionMinor}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
|
||||||
|
{
|
||||||
|
using var multipartContent = new MultipartFormDataContent();
|
||||||
|
|
||||||
|
var pages = new List<PreviewerRefreshCommand.Page>();
|
||||||
|
|
||||||
|
foreach (var picture in pictures)
|
||||||
|
{
|
||||||
|
var page = new PreviewerRefreshCommand.Page
|
||||||
|
{
|
||||||
|
Width = picture.Size.Width,
|
||||||
|
Height = picture.Size.Height
|
||||||
|
};
|
||||||
|
|
||||||
|
pages.Add(page);
|
||||||
|
|
||||||
|
var pictureStream = picture.Picture.Serialize().AsStream();
|
||||||
|
multipartContent.Add(new StreamContent(pictureStream), page.Id, page.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
var command = new PreviewerRefreshCommand
|
||||||
|
{
|
||||||
|
Pages = pages
|
||||||
|
};
|
||||||
|
|
||||||
|
multipartContent.Add(JsonContent.Create(command), "command");
|
||||||
|
|
||||||
|
using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
|
||||||
|
|
||||||
|
foreach (var picture in pictures)
|
||||||
|
picture.Picture.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user