Compare commits

..

47 Commits

Author SHA1 Message Date
MarcinZiabek bd577b9609 Implemented custom tree view with proper indendation 2022-07-18 10:47:07 +02:00
MarcinZiabek 100afbdc32 Prototype implementation 2022-07-12 15:21:50 +02:00
MarcinZiabek 2a0b133185 Fixed incorrect file name 2022-06-23 20:32:36 +02:00
Marcin Ziąbek 6058dac524 Merge pull request #249 from wieslawsoltes/CreateGlobalJson
Add global.json
2022-06-23 00:41:36 +02:00
Marcin Ziąbek 25b8f62245 Merge pull request #248 from wieslawsoltes/SetApplicationName
Set application name
2022-06-23 00:40:51 +02:00
MarcinZiabek 2c31f3d5b5 2022.6.2 2022-06-23 00:16:13 +02:00
Wiesław Šoltés f7cd89ccf6 Add global.json 2022-06-15 21:55:44 +02:00
Wiesław Šoltés 6914a9063a Set application name 2022-06-15 21:53:34 +02:00
MarcinZiabek b87e8d5d4e Improved exception messages 2022-06-14 17:53:16 +02:00
MarcinZiabek d740d67a64 2022.6.1 2022-06-13 00:17:32 +02:00
MarcinZiabek a14e9ca61f 2022.6 2022-06-12 23:48:38 +02:00
MarcinZiabek 44b3ff8d0d Merge branch 'text-shaping-new' 2022-06-08 02:46:54 +02:00
Marcin Ziąbek df6b88e14a Update readme.md 2022-06-08 02:10:52 +02:00
MarcinZiabek d4103797aa Improved exception message when SkiaSharp throws the TypeInitializationException 2022-06-08 02:01:58 +02:00
MarcinZiabek d894914e7e Improved Previewer stability 2022-06-08 00:09:59 +02:00
MarcinZiabek 90d1ce7af7 2022.6.0-prerelease 2022-05-31 00:52:49 +02:00
MarcinZiabek 5fa32a06cc Text: fixed corner case 2022-05-26 00:42:50 +02:00
MarcinZiabek acdf1ed1aa Stability improvement 2022-05-25 00:21:50 +02:00
MarcinZiabek a0b32e5ecf Fixed Row.AutoItem width calculation 2022-05-25 00:21:50 +02:00
MarcinZiabek 539dc83303 Fixed Row.AutoItem width calculation 2022-05-24 23:50:53 +02:00
MarcinZiabek d97fa2cef1 Minor optimization of SkFont creation 2022-05-23 01:18:21 +02:00
MarcinZiabek 6d122c784d Implemented working text shaping 2022-05-22 20:25:11 +02:00
MarcinZiabek 28743a81d1 Fixed typo 2022-05-21 14:35:13 +02:00
MarcinZiabek 32b94cea43 Updated sponsorship 2022-05-12 00:40:29 +02:00
Marcin Ziąbek 0ca55a91e0 Update readme.md 2022-05-10 21:48:39 +02:00
MarcinZiabek 09e6b5253f Fixed class names 2022-05-10 12:43:13 +02:00
MarcinZiabek bacb136a3f Benchmark: grid element 2022-05-10 12:42:49 +02:00
MarcinZiabek b7bc640e31 Added new example to dynamic component: footer with alternating position 2022-05-09 18:18:06 +02:00
MarcinZiabek 6e6f884a5f Updated sponsorship section 2022-05-09 16:33:12 +02:00
MarcinZiabek b47a8ac409 Added more examples 2022-05-09 16:32:59 +02:00
MarcinZiabek e51082400c Dynamic Component: added more examples 2022-05-09 14:07:50 +02:00
MarcinZiabek 52cfa3ab7e Added note regarding JetBrains OSS Power Ups 2022-05-07 22:18:14 +02:00
MarcinZiabek cd181b1e2e Merge branch '2022.5' 2022-05-07 21:40:10 +02:00
Marcin Ziąbek e71601ad0c Merge pull request #208 from danielchalmers/patch-1
replace obsolete method in readme example
2022-05-07 18:46:09 +02:00
Daniel Chalmers 3041aa53cc replace obsolete method in readme example
Warning	CS0618	'PageDescriptor.Background(string)' is obsolete: 'This element has been renamed since version 2022.3. Please use the PageColor method.'
2022-05-06 19:37:17 -05:00
Marcin Ziąbek 9a7e397db2 Merge pull request #197 from knoxyz/patch-2
Replaced the obsolete methods from 2022.3 (FontType, Color and Size)
2022-05-05 18:23:11 +02:00
Marcin Ziąbek d81f21e658 Merge pull request #196 from knoxyz/patch-1
Update <TargetFramework> from 3.1 to net6.0
2022-05-05 18:11:21 +02:00
Marcin Ziąbek 78909c6edf Merge pull request #199 from knoxyz/patch-4
Replaced the obsolete methods from 2022.3 (InternalLink and Color)
2022-05-05 17:48:28 +02:00
Marcin Ziąbek a2d81b6adf Merge pull request #200 from knoxyz/patch-3
Replaced the obsolete methods from 2022.3 (ExternalLink and Location)
2022-05-05 17:47:40 +02:00
Marcin Ziąbek 4a761c023f Merge pull request #201 from simusr2/main
Replaced PageEnd with PageStart in PageNumberWithinSection
2022-05-05 17:46:30 +02:00
Simone Anile 24bbd06dff Replaced PageEnd with PageStart in PageNumberWithinSection 2022-04-29 15:19:58 +02:00
knoxyz 44bd646bab Replaced the obsolete methods from 2022.3 (InternalLink and Color) 2022-04-29 11:36:28 +02:00
knoxyz 5f0291b2c4 Replaced the obsolete methods from 2022.3 (ExternalLink and Location) 2022-04-29 11:32:20 +02:00
knoxyz 1b91015d8f Replaced the obsolete methods from 2022.3 (FontType, Color and Size) 2022-04-28 16:21:56 +02:00
knoxyz 2b6444463d Update <TargetFramework> from 3.1 to net6.0 2022-04-28 16:13:44 +02:00
Marcin Ziąbek e54e09de15 Merge pull request #185 from mikasius/main
Renamed logo.png
2022-04-23 23:03:37 +02:00
Sergey Metlenkin c49a424f87 Renamed logo.png
Otherwise solution won't build
2022-04-22 10:59:12 +07:00
71 changed files with 2011 additions and 334 deletions
+106
View File
@@ -0,0 +1,106 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public struct FibonacciHeaderState
{
public int Previous { get; set; }
public int Current { get; set; }
}
public class FibonacciHeader : IDynamicComponent<FibonacciHeaderState>
{
public FibonacciHeaderState State { get; set; }
public static readonly string[] ColorsTable =
{
Colors.Red.Lighten2,
Colors.Orange.Lighten2,
Colors.Green.Lighten2,
};
public FibonacciHeader(int previous, int current)
{
State = new FibonacciHeaderState
{
Previous = previous,
Current = current
};
}
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var content = context.CreateElement(container =>
{
var colorIndex = State.Current % ColorsTable.Length;
var color = ColorsTable[colorIndex];
var ratio = (float)State.Current / State.Previous;
container
.Background(color)
.Height(50)
.AlignMiddle()
.AlignCenter()
.Text($"{State.Current} / {State.Previous} = {ratio:N5}");
});
State = new FibonacciHeaderState
{
Previous = State.Current,
Current = State.Previous + State.Current
};
return new DynamicComponentComposeResult
{
Content = content,
HasMoreContent = false
};
}
}
public static class DynamicFibonacci
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.ShowResults()
.MaxPages(100)
.ProduceImages()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A6);
page.PageColor(Colors.White);
page.Margin(1, Unit.Centimetre);
page.DefaultTextStyle(x => x.FontSize(18));
page.Header().Dynamic(new FibonacciHeader(17, 19));
page.Content().Column(column =>
{
foreach (var i in Enumerable.Range(0, 50))
column.Item().PaddingTop(25).Background(Colors.Grey.Lighten2).Height(50);
});
page.Footer().AlignCenter().Text(text =>
{
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
});
});
});
}
}
}
@@ -21,12 +21,12 @@ namespace QuestPDF.Examples
public int ShownItemsCount { get; set; }
}
public class OrdersTable : IDynamicComponent<OrdersTableState>
public class OptimizedOrdersTable : IDynamicComponent<OrdersTableState>
{
private ICollection<OrderItem> Items { get; }
public OrdersTableState State { get; set; }
public OrdersTable(ICollection<OrderItem> items)
public OptimizedOrdersTable(ICollection<OrderItem> items)
{
Items = items;
@@ -69,7 +69,7 @@ namespace QuestPDF.Examples
return new DynamicComponentComposeResult
{
Content = content,
HasMoreContent = State.ShownItemsCount + rows.Count < Items.Count
HasMoreContent = State.ShownItemsCount < Items.Count
};
}
@@ -145,7 +145,7 @@ namespace QuestPDF.Examples
}
}
public static class DynamicExamples
public static class DynamicOptimizedExamples
{
[Test]
public static void Dynamic()
@@ -177,7 +177,7 @@ namespace QuestPDF.Examples
decoration
.Content()
.Dynamic(new OrdersTable(items));
.Dynamic(new OptimizedOrdersTable(items));
});
});
}
@@ -0,0 +1,69 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class FooterWithAlternatingAlignment : IDynamicComponent<int>
{
public int State { get; set; }
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var content = context.CreateElement(element =>
{
element
.Element(x => context.PageNumber % 2 == 0 ? x.AlignLeft() : x.AlignRight())
.Text(x =>
{
x.CurrentPageNumber();
x.Span(" / ");
x.TotalPages();
});
});
return new DynamicComponentComposeResult()
{
Content = content,
HasMoreContent = false
};
}
}
public static class DynamicPageNumberLeftRightExamples
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.PageSize(PageSizes.A5)
.MaxPages(100)
.ShowResults()
.ProduceImages()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A6);
page.PageColor(Colors.White);
page.Margin(1, Unit.Centimetre);
page.DefaultTextStyle(x => x.FontSize(18));
page.Content().Column(column =>
{
foreach (var i in Enumerable.Range(0, 50))
column.Item().PaddingTop(25).Background(Colors.Grey.Lighten2).Height(50);
});
page.Footer().Dynamic(new FooterWithAlternatingAlignment());
});
});
}
}
}
@@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class ProgressHeader : IDynamicComponent<int>
{
public int State { get; set; }
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var content = context.CreateElement(container =>
{
var width = context.AvailableSize.Width * context.PageNumber / context.TotalPages;
container
.Background(Colors.Blue.Lighten2)
.Height(25)
.Width(width)
.Background(Colors.Blue.Darken1);
});
return new DynamicComponentComposeResult
{
Content = content,
HasMoreContent = false
};
}
}
public static class DynamicProgressHeader
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.ShowResults()
.MaxPages(100)
.ProduceImages()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A6);
page.Margin(1, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header().Dynamic(new ProgressHeader());
page.Content().Column(column =>
{
foreach (var i in Enumerable.Range(0, 100))
column.Item().PaddingTop(25).Background(Colors.Grey.Lighten2).Height(50);
});
page.Footer().AlignCenter().Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(20));
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
});
});
});
}
}
}
@@ -0,0 +1,157 @@
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class OrdersTable : IDynamicComponent<OrdersTableState>
{
private IList<OrderItem> Items { get; }
public OrdersTableState State { get; set; }
public OrdersTable(IList<OrderItem> items)
{
Items = items;
State = new OrdersTableState
{
ShownItemsCount = 0
};
}
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var possibleItems = Enumerable
.Range(1, Items.Count - State.ShownItemsCount)
.Select(itemsToDisplay => ComposeContent(context, itemsToDisplay))
.TakeWhile(x => x.Size.Height <= context.AvailableSize.Height)
.ToList();
State = new OrdersTableState
{
ShownItemsCount = State.ShownItemsCount + possibleItems.Count
};
return new DynamicComponentComposeResult
{
Content = possibleItems.Last(),
HasMoreContent = State.ShownItemsCount < Items.Count
};
}
private IDynamicElement ComposeContent(DynamicContext context, int itemsToDisplay)
{
var total = Items.Skip(State.ShownItemsCount).Take(itemsToDisplay).Sum(x => x.Count * x.Price);
return context.CreateElement(container =>
{
container
.MinimalBox()
.Width(context.AvailableSize.Width)
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(30);
columns.RelativeColumn();
columns.ConstantColumn(50);
columns.ConstantColumn(50);
columns.ConstantColumn(50);
});
table.Header(header =>
{
header.Cell().Element(Style).Text("#");
header.Cell().Element(Style).Text("Item name");
header.Cell().Element(Style).AlignRight().Text("Count");
header.Cell().Element(Style).AlignRight().Text("Price");
header.Cell().Element(Style).AlignRight().Text("Total");
IContainer Style(IContainer container)
{
return container
.DefaultTextStyle(x => x.SemiBold())
.BorderBottom(1)
.BorderColor(Colors.Grey.Darken2)
.Padding(5);
}
});
table.Footer(footer =>
{
footer
.Cell().ColumnSpan(5)
.AlignRight()
.PaddingTop(10)
.Text($"Subtotal: {total}$", TextStyle.Default.Bold());
});
foreach (var index in Enumerable.Range(State.ShownItemsCount, itemsToDisplay))
{
var item = Items[index];
table.Cell().Element(Style).Text(index + 1);
table.Cell().Element(Style).Text(item.ItemName);
table.Cell().Element(Style).AlignRight().Text(item.Count);
table.Cell().Element(Style).AlignRight().Text($"{item.Price}$");
table.Cell().Element(Style).AlignRight().Text($"{item.Count*item.Price}$");
IContainer Style(IContainer container)
{
return container
.BorderBottom(1)
.BorderColor(Colors.Grey.Lighten2)
.Padding(5);
}
}
});
});
}
}
public static class DynamicSimpleTableExample
{
[Test]
public static void Dynamic()
{
RenderingTest
.Create()
.PageSize(PageSizes.A5)
.ShowResults()
.ProduceImages()
.Render(container =>
{
var items = Enumerable.Range(0, 15).Select(x => new OrderItem()).ToList();
container
.Background(Colors.White)
.Padding(25)
.DefaultTextStyle(x => x.FontSize(16))
.Decoration(decoration =>
{
decoration
.Header()
.PaddingBottom(5)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.SemiBold().FontColor(Colors.Blue.Darken2));
text.Span("Page ");
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
});
decoration
.Content()
.Dynamic(new OrdersTable(items));
});
});
}
}
}
+42
View File
@@ -0,0 +1,42 @@
using System.Linq;
using NUnit.Framework;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
namespace QuestPDF.Examples
{
public class GridBenchmark
{
[Test]
public void Benchmark()
{
RenderingTest
.Create()
.ProducePdf()
.PageSize(PageSizes.A4)
.ShowResults()
.MaxPages(10_000)
.EnableCaching(true)
.EnableDebugging(false)
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.Grid(grid =>
{
const int numberOfRows = 100_000;
const int numberOfColumns = 10;
grid.Columns(numberOfColumns);
foreach (var row in Enumerable.Range(0, numberOfRows))
foreach (var column in Enumerable.Range(0, numberOfColumns))
grid.Item().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}");
});
});
}
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
@@ -11,7 +11,7 @@
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
<PackageReference Include="SkiaSharp" Version="2.80.3" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
<PackageReference Include="Svg.Skia" Version="0.5.10" />
</ItemGroup>
+1 -1
View File
@@ -31,7 +31,7 @@ namespace QuestPDF.Examples
{
var chapters = GetBookChapters().ToList();
var results = PerformTest(16).ToList();
var results = PerformTest(128).ToList();
Console.WriteLine($"Min: {results.Min():F}");
Console.WriteLine($"Max: {results.Max():F}");
+165 -43
View File
@@ -12,11 +12,11 @@ namespace QuestPDF.Examples
public class TextExamples
{
[Test]
public void SimpleTextBlock()
public void SimpleText()
{
RenderingTest
.Create()
.PageSize(500, 300)
.PageSize(500, 100)
.ProduceImages()
.ShowResults()
@@ -27,6 +27,27 @@ namespace QuestPDF.Examples
.MinimalBox()
.Border(1)
.Padding(10)
.Text(Placeholders.Paragraph());
});
}
[Test]
public void SimpleTextBlock()
{
RenderingTest
.Create()
.PageSize(600, 300)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(5)
.MinimalBox()
.Border(1)
.MaxWidth(300)
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
@@ -38,48 +59,105 @@ namespace QuestPDF.Examples
}
[Test]
public void SuperscriptSubscript_General()
public void TextWeight()
{
RenderingTest
.Create()
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(5)
.Create()
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.MinimalBox()
.Border(1)
.Padding(10)
.Padding(20)
.Text(text =>
{
text.DefaultTextStyle(x => x.FontFamily(Fonts.Calibri).FontSize(20));
text.Line("Thin").Thin();
text.Line("ExtraLight").ExtraLight();
text.Line("Light").Light();
text.Line("NormalWeight").NormalWeight();
text.Line("Medium").Medium();
text.Line("SemiBold").SemiBold();
text.Line("Bold").Bold();
text.Line("ExtraBold").ExtraBold();
text.Line("Black").Black();
text.Line("ExtraBlack").ExtraBlack();
});
});
}
[Test]
public void LineHeight()
{
RenderingTest
.Create()
.PageSize(500, 700)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.Column(column =>
{
var lineHeights = new[] { 0.8f, 1f, 1.5f };
var paragraph = Placeholders.Paragraph();
foreach (var lineHeight in lineHeights)
{
column
.Item()
.Border(1)
.Padding(10)
.Text(paragraph)
.FontSize(16)
.LineHeight(lineHeight);
}
});
});
}
[Test]
public void SuperscriptSubscript_Simple()
{
RenderingTest
.Create()
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.MinimalBox()
.Border(1)
.Padding(20)
.Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(20));
text.ParagraphSpacing(2);
text.Span("In physics, massenergy equivalence is the relationship between mass and energy in a system's rest frame, where the two values differ only by a constant and the units of measurement.");
text.Span("[1][2]").Superscript();
text.Span(" The principle is described by the physicist Albert Einstein's famous formula: E = mc");
text.Span("2").Superscript();
text.Span(". ");
text.Span("[3]").Superscript();
text.EmptyLine();
text.Span("H");
text.Span("2").Subscript();
text.Span("O is the chemical formula for water, meaning that each of its molecules contains one oxygen and two hydrogen atoms.");
text.ParagraphSpacing(10);
var highlight = TextStyle.Default.BackgroundColor(Colors.Green.Lighten3);
text.Span("E=mc").Style(highlight);
text.Span("2").Superscript().Style(highlight);
text.Span(" is the equation of massenergy equivalence.");
text.EmptyLine();
text.Span("H");
text.Span("2").Subscript();
text.Span("O");
text.Span("H").Style(highlight);
text.Span("2").Subscript().Style(highlight);
text.Span("O").Style(highlight);
text.Span(" is the chemical formula for water.");
});
});
});
}
[Test]
public void SuperscriptSubscript_Effects()
{
@@ -134,7 +212,7 @@ namespace QuestPDF.Examples
{
RenderingTest
.Create()
.PageSize(500, 300)
.PageSize(500, 500)
.ProduceImages()
.ShowResults()
.Render(container =>
@@ -146,13 +224,7 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.ParagraphSpacing(10);
foreach (var i in Enumerable.Range(1, 3))
{
text.Span($"Paragraph {i}: ").SemiBold();
text.Line(Placeholders.Paragraph());
}
text.Line(Placeholders.Paragraph());
});
});
}
@@ -331,7 +403,7 @@ namespace QuestPDF.Examples
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(TextStyle.Default.FontSize(20));
text.DefaultTextStyle(TextStyle.Default.FontSize(20).BackgroundColor(Colors.Red.Lighten4));
text.AlignLeft();
text.ParagraphSpacing(10);
@@ -350,7 +422,7 @@ namespace QuestPDF.Examples
{
text.Line($"{i}: {Placeholders.Paragraph()}");
text.Hyperlink("Please visit QuestPDF website", "https://www.questpdf.com");
text.Hyperlink("Please visit QuestPDF website. ", "https://www.questpdf.com");
text.Span("This is page number ");
text.CurrentPageNumber();
@@ -495,5 +567,55 @@ namespace QuestPDF.Examples
});
});
}
[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̸̢̯̪̫͓̭̮̓̀͆͜ț̸̢͉̞̥̤̏̌̓͝").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("ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا")
.FontSize(20);
});
}
}
}
+6 -5
View File
@@ -2,15 +2,16 @@
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
using QuestPDF.ReportSample;
using QuestPDF.ReportSample.Layouts;
//ImagePlaceholder.Solid = true;
// var model = DataSource.GetReport();
// var report = new StandardReport(model);
// report.ShowInPreviewer().Wait();
//
// return;
var model = DataSource.GetReport();
var report = new StandardReport(model);
report.ShowInPreviewer();
return;
Document
.Create(container =>
@@ -1,5 +1,6 @@
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -29,6 +30,7 @@ class CommunicationService
{
var builder = WebApplication.CreateBuilder();
builder.Services.AddLogging(x => x.ClearProviders());
builder.WebHost.UseKestrel(options => options.Limits.MaxRequestBodySize = null);
Application = builder.Build();
Application.MapGet("ping", HandlePing);
+158 -16
View File
@@ -2,6 +2,7 @@
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using DynamicData;
using SkiaSharp;
namespace QuestPDF.Previewer;
@@ -10,9 +11,10 @@ class InteractiveCanvas : ICustomDrawOperation
{
public Rect Bounds { get; set; }
public ICollection<PreviewPage> Pages { get; set; }
public InspectionElement? InspectionElement { get; set; }
private float Width => (float)Bounds.Width;
private float Height => (float)Bounds.Height;
private float ViewportWidth => (float)Bounds.Width;
private float ViewportHeight => (float)Bounds.Height;
public float Scale { get; private set; } = 1;
public float TranslateX { get; set; }
@@ -28,7 +30,7 @@ class InteractiveCanvas : ICustomDrawOperation
public float TotalHeight => TotalPagesHeight + SafeZone * 2 / Scale;
public float MaxWidth => Pages.Any() ? Pages.Max(x => x.Width) : 0;
public float MaxTranslateY => TotalHeight - Height / Scale;
public float MaxTranslateY => TotalHeight - ViewportHeight / Scale;
public float ScrollPercentY
{
@@ -46,7 +48,7 @@ class InteractiveCanvas : ICustomDrawOperation
{
get
{
var viewPortSize = Height / Scale / TotalHeight;
var viewPortSize = ViewportHeight / Scale / TotalHeight;
return Math.Clamp(viewPortSize, 0, 1);
}
}
@@ -61,19 +63,19 @@ class InteractiveCanvas : ICustomDrawOperation
private void LimitTranslate()
{
if (TotalPagesHeight > Height / Scale)
if (TotalPagesHeight > ViewportHeight / Scale)
{
TranslateY = Math.Min(TranslateY, MaxTranslateY);
TranslateY = Math.Max(TranslateY, 0);
}
else
{
TranslateY = (TotalPagesHeight - Height / Scale) / 2;
TranslateY = (TotalPagesHeight - ViewportHeight / Scale) / 2;
}
if (Width / Scale < MaxWidth)
if (ViewportWidth / Scale < MaxWidth)
{
var maxTranslateX = (Width / 2 - SafeZone) / Scale - MaxWidth / 2;
var maxTranslateX = (ViewportWidth / 2 - SafeZone) / Scale - MaxWidth / 2;
TranslateX = Math.Min(TranslateX, -maxTranslateX);
TranslateX = Math.Max(TranslateX, maxTranslateX);
@@ -104,6 +106,72 @@ class InteractiveCanvas : ICustomDrawOperation
LimitTranslate();
}
public int ActivePage { get; set; } = 1;
public IEnumerable<(int pageNumber, float beginY, float endY)> GetPagePosition()
{
var pageNumber = 1;
var currentPagePosition = SafeZone / Scale;
foreach (var page in Pages)
{
yield return (pageNumber, currentPagePosition, currentPagePosition + page.Height);
currentPagePosition += page.Height + PageSpacing;
pageNumber++;
}
}
public void SetActivePage(float x, float y)
{
y /= Scale;
y += TranslateY;
ActivePage = GetPagePosition().FirstOrDefault(p => p.beginY <= y && y <= p.endY).pageNumber;
}
public void ScrollToInspectionElement(InspectionElement element)
{
var location = element.Location.MinBy(x => x.PageNumber);
var pagePosition = GetPagePosition().ElementAt(location.PageNumber - 1);
var page = Pages.ElementAt(location.PageNumber - 1);
var widthScale = ViewportWidth / location.Width;
var heightScale = ViewportHeight / location.Height;
var targetScale = Math.Min(widthScale, heightScale);
targetScale *= 0.7f; // slightly zoom out to show entire element with padding
Scale = targetScale;
TranslateY = pagePosition.beginY + location.Top + location.Height / 2 - ViewportHeight / Scale / 2;
TranslateX = page.Width / 2 - location.Left - location.Width / 2;
}
public (int pageNumber, float x, float y)? FindClickedPointOnThePage(float x, float y)
{
x -= ViewportWidth / 2;
x /= Scale;
x += TranslateX;
y /= Scale;
y += TranslateY;
var location = GetPagePosition().FirstOrDefault(p => p.beginY <= y && y <= p.endY);
if (location == default)
return null;
var page = Pages.ElementAt(location.pageNumber - 1);
x += page.Width / 2;
if (x < 0 || page.Width < x)
return null;
y -= location.beginY;
return (location.pageNumber, x, y);
}
#endregion
@@ -124,20 +192,32 @@ class InteractiveCanvas : ICustomDrawOperation
var originalMatrix = canvas.TotalMatrix;
canvas.Translate(Width / 2, 0);
canvas.Translate(ViewportWidth / 2, 0);
canvas.Scale(Scale);
canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
canvas.Translate(TranslateX, -TranslateY);
var topMatrix = canvas.TotalMatrix;;
var positions = GetPagePosition().ToList();
foreach (var page in Pages)
foreach (var pageIndex in Enumerable.Range(0, Pages.Count))
{
canvas.Translate(-page.Width / 2f, 0);
canvas.SetMatrix(topMatrix);
var page = Pages.ElementAt(pageIndex);
var position = positions.ElementAt(pageIndex);
canvas.Translate(-page.Width / 2f, position.beginY);
DrawBlankPage(canvas, page.Width, page.Height);
canvas.DrawPicture(page.Picture);
canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
DrawInspectionElement(canvas, pageIndex + 1);
}
canvas.SetMatrix(topMatrix);
DrawActivePage(canvas);
canvas.SetMatrix(originalMatrix);
DrawInnerGradient(canvas);
}
@@ -173,7 +253,7 @@ class InteractiveCanvas : ICustomDrawOperation
#region inner viewport gradient
private const int InnerGradientSize = (int)SafeZone;
private static readonly SKColor InnerGradientColor = SKColor.Parse("#666");
private static readonly SKColor InnerGradientColor = SKColor.Parse("#555");
private void DrawInnerGradient(SKCanvas canvas)
{
@@ -195,7 +275,69 @@ class InteractiveCanvas : ICustomDrawOperation
SKShaderTileMode.Clamp)
};
canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint);
canvas.DrawRect(0, 0, ViewportWidth, InnerGradientSize, fogPaint);
}
#endregion
#region Interactivity
private void DrawActivePage(SKCanvas canvas)
{
if (ActivePage == default)
return;
var page = Pages.ElementAt(ActivePage - 1);
var pagePosition = GetPagePosition().ElementAt(ActivePage - 1);
var thickness = 6f / Scale;
using var strokePaint = new SKPaint
{
StrokeWidth = thickness,
IsStroke = true,
Color = SKColor.Parse("#000")
};
canvas.DrawRect(- page.Width / 2 -thickness / 2, pagePosition.beginY -thickness / 2, page.Width + thickness, page.Height + thickness, strokePaint);
}
private void DrawInspectionElement(SKCanvas canvas, int pageNumber)
{
if (InspectionElement == null || InspectionElement.Location == null)
return;
var location = InspectionElement.Location.FirstOrDefault(x => x.PageNumber == pageNumber);
if (location == null)
return;
var size = 6 / Scale;
size = Math.Min(size, 3);
using var strokePaint1 = new SKPaint
{
StrokeWidth = size,
IsStroke = true,
Color = SKColor.Parse("#42A5F5")
};
using var strokePaint2 = new SKPaint
{
StrokeWidth = size,
IsStroke = true,
PathEffect = SKPathEffect.CreateDash(new[] { size * 3, size * 3 }, 0),
Color = SKColor.Parse("#1E88E5")
};
using var backgroundPaint = new SKPaint
{
Color = SKColor.Parse("#442196F3"),
};
canvas.DrawRect(location.Left, location.Top, location.Width, location.Height, backgroundPaint);
canvas.DrawRect(location.Left + size / 2, location.Top + size / 2, location.Width - size, location.Height - size, strokePaint1);
canvas.DrawRect(location.Left + size / 2, location.Top + size / 2, location.Width - size, location.Height - size, strokePaint2);
}
#endregion
+45
View File
@@ -0,0 +1,45 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:previewer="clr-namespace:QuestPDF.Previewer"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="QuestPDF.Previewer.MyHierarchy">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="Auto"></ColumnDefinition>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Panel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Name="Highlight" Background="transparent" DoubleTapped="InputElement_OnDoubleTapped" PointerEnter="Highlight_OnPointerEnter" PointerLeave="Indentation_OnPointerLeave" PointerPressed="Clicked"></Panel>
<Panel Grid.Row="0" Grid.Column="0" Name="Indentation"></Panel>
<Panel Grid.Row="0" Grid.Column="1" Name="Panel" Width="16" Height="16" Margin="0,0,8,0" Background="transparent" PointerPressed="Toggle" PointerEnter="Highlight_OnPointerEnter" PointerLeave="Indentation_OnPointerLeave">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="#AFFF" Name="Icon" />
</Canvas>
</Viewbox>
</Panel>
<Panel Grid.Row="0" Grid.Column="2">
<TextBlock Name="ElementName" FontSize="12" Foreground="#AFFF" Margin="0,6" IsHitTestVisible="False"></TextBlock>
</Panel>
<ItemsRepeater Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Name="Repeater" HorizontalCacheLength="1000000" VerticalCacheLength="100000">
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="previewer:InspectionElement">
<previewer:MyHierarchy Hierarchy="{Binding}" OnSelected="MyHierarchy_OnOnSelected"></previewer:MyHierarchy>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Grid>
</UserControl>
+115
View File
@@ -0,0 +1,115 @@
using System.Windows.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Color = System.Drawing.Color;
namespace QuestPDF.Previewer
{
public partial class MyHierarchy : UserControl
{
public static readonly StyledProperty<InspectionElement> HierarchyProperty =
AvaloniaProperty.Register<MyHierarchy, InspectionElement>(nameof(Hierarchy));
public InspectionElement Hierarchy
{
get => GetValue(HierarchyProperty);
set => SetValue(HierarchyProperty, value);
}
public static readonly StyledProperty<InspectionElement?> SelectionProperty =
AvaloniaProperty.Register<MyHierarchy, InspectionElement?>(nameof(Selection));
public InspectionElement? Selection
{
get => GetValue(SelectionProperty);
set => SetValue(SelectionProperty, value);
}
public event Action<InspectionElement>? OnSelected;
public bool Extended { get; set; }
public MyHierarchy()
{
InitializeComponent();
HierarchyProperty.Changed.Subscribe(x =>
{
Configure();
});
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
Configure();
}
private void Configure()
{
var panel = this.FindControl<Panel>("Panel");
panel.IsVisible = Hierarchy?.Children?.Any() ?? false;
this.FindControl<Panel>("Indentation").Width = (Hierarchy?.Level ?? 0) * 24;
UpdateIcon();
this.FindControl<TextBlock>("ElementName").Text = Hierarchy?.Text;
this.FindControl<TextBlock>("ElementName").Foreground = new SolidColorBrush(Avalonia.Media.Color.Parse(Hierarchy?.FontColor ?? "#FFF"));
}
private const string ExtendedIcon = "M7,15L12,10L17,15H7Z";
private const string CollapsedIcon = "M7,10L12,15L17,10H7Z";
private void Toggle(object? sender, PointerPressedEventArgs e)
{
Extended = !Extended;
UpdateIcon();
this.FindControl<ItemsRepeater>("Repeater").Items = Extended ? Hierarchy?.Children : Array.Empty<InspectionElement>();
}
private void UpdateIcon()
{
var iconControl = this.FindControl<Avalonia.Controls.Shapes.Path>("Icon");
iconControl.Data = new PathGeometry
{
Figures = PathFigures.Parse(Extended ? ExtendedIcon : CollapsedIcon)
};
}
private void InputElement_OnDoubleTapped(object? sender, RoutedEventArgs e)
{
Toggle(null, null);
}
private void Highlight_OnPointerEnter(object? sender, PointerEventArgs e)
{
Cursor = Cursor.Parse("hand");
this.FindControl<Panel>("Highlight").Background = new SolidColorBrush(Avalonia.Media.Color.Parse("#1FFF"));
}
private void Indentation_OnPointerLeave(object? sender, PointerEventArgs e)
{
Cursor = Cursor.Default;
this.FindControl<Panel>("Highlight").Background = new SolidColorBrush(Avalonia.Media.Color.Parse("#0000"));
}
private void Clicked(object? sender, PointerPressedEventArgs e)
{
OnSelected?.Invoke(Hierarchy);
}
private void MyHierarchy_OnOnSelected(InspectionElement selected)
{
OnSelected?.Invoke(selected);
Selection = selected;
}
}
}
+2 -1
View File
@@ -1,6 +1,7 @@
<Application x:Class="QuestPDF.Previewer.PreviewerApp"
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>
<FluentTheme Mode="Dark" />
</Application.Styles>
+81
View File
@@ -35,6 +35,22 @@ namespace QuestPDF.Previewer
set => SetValue(ScrollViewportSizeProperty, value);
}
public static readonly StyledProperty<InspectionElement> HierarchyProperty = AvaloniaProperty.Register<PreviewerControl, InspectionElement>(nameof(Hierarchy));
public InspectionElement Hierarchy
{
get => GetValue(HierarchyProperty);
set => SetValue(HierarchyProperty, value);
}
public static readonly StyledProperty<InspectionElement> CurrentSelectionProperty = AvaloniaProperty.Register<PreviewerControl, InspectionElement>(nameof(CurrentSelection));
public InspectionElement CurrentSelection
{
get => GetValue(CurrentSelectionProperty);
set => SetValue(CurrentSelectionProperty, value);
}
public PreviewerControl()
{
PagesProperty.Changed.Subscribe(x =>
@@ -49,7 +65,72 @@ namespace QuestPDF.Previewer
InvalidateVisual();
});
CurrentSelectionProperty.Changed.Subscribe(x =>
{
InteractiveCanvas.InspectionElement = CurrentSelection;
//InteractiveCanvas.ScrollToInspectionElement(CurrentSelection);
InvalidateVisual();
});
ClipToBounds = true;
PointerPressed += (sender, args) =>
{
var position = args.GetPosition(this);
InteractiveCanvas.SetActivePage((float)position.X, (float)position.Y);
var clickedPosition = InteractiveCanvas.FindClickedPointOnThePage((float)position.X, (float)position.Y);
if (clickedPosition != null)
FindHighlightedElement(clickedPosition.Value.pageNumber, clickedPosition.Value.x, clickedPosition.Value.y);
InvalidateVisual();
};
}
void FindHighlightedElement(int pageNumber, float x, float y)
{
var possible = FlattenHierarchy(Hierarchy, 0)
.Select(x =>
{
var location = x.element.Location.First(y => y.PageNumber == pageNumber);
return new
{
Element = x.element,
Level = x.level,
Size = location.Width * location.Height
};
})
.ToList();
var minSize = possible.Min(x => x.Size);
CurrentSelection = possible
.Where(x => Math.Abs(x.Size - minSize) < 1)
.OrderByDescending(x => x.Level)
.First()
.Element;
IEnumerable<(InspectionElement element, int level)> FlattenHierarchy(InspectionElement element, int level)
{
var location = element.Location.FirstOrDefault(x => x.PageNumber == pageNumber);
if (location == null)
yield break;
if (x < location.Left || location.Left + location.Width < x)
yield break;
if (y < location.Top || location.Top + location.Height < y)
yield break;
yield return (element, level);
foreach (var childIndex in Enumerable.Range(0, element.Children.Count))
foreach (var nestedChild in FlattenHierarchy(element.Children[childIndex], level + childIndex + 1))
yield return nestedChild;
}
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+113 -55
View File
@@ -3,95 +3,153 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:previewer="clr-namespace:QuestPDF.Previewer"
xmlns:visualBasic="clr-namespace:Microsoft.VisualBasic;assembly=Microsoft.VisualBasic.Core"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="QuestPDF.Previewer.PreviewerWindow"
x:DataType="previewer:PreviewerWindowViewModel"
x:CompileBindings="True"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="true"
ExtendClientAreaTitleBarHeightHint="-1"
Background="#666"
ExtendClientAreaTitleBarHeightHint="56"
Background="#555"
Icon="/Resources/Logo.png"
UseLayoutRounding="True"
Title="QuestPDF Document Preview">
<Window.Styles>
<Style Selector="Button.actions">
<Style Selector=".MainMenu TextBlock">
<Setter Property="Foreground" Value="#AFFF" />
<Setter Property="Margin" Value="0,0,16,0" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style Selector=".MainMenu TextBlock:pointerover">
<Setter Property="Foreground" Value="#FFF" />
<Setter Property="TextDecorations" Value="Underline" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<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"/>
<Setter Property="Padding" Value="12" />
<Setter Property="Margin" Value="-1" />
<Setter Property="Background" Value="transparent"/>
</Style>
<Style Selector="Button.actions Path">
<Setter Property="Fill" Value="#8FFF"/>
</Style>
<Style Selector="Button.active Path">
<Setter Property="Fill" Value="#FFFF"/>
</Style>
<Style Selector="Button:pointerover Path">
<Setter Property="Fill" Value="#FFFF"/>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#999"/>
<Setter Property="Background" Value="#333"/>
</Style>
</Window.Styles>
</Window.Styles>
<Panel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="56" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="300" MaxWidth="500" Width="300" />
<ColumnDefinition Width="4" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
VerticalAlignment="Center" HorizontalAlignment="Center"
TextAlignment="Center" Text="QuestPDF Previewer" FontSize="14" Foreground="#DFFF" FontWeight="Regular" IsHitTestVisible="False" />
<previewer:PreviewerControl Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
<Panel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="5" Background="#333">
<StackPanel Orientation="Horizontal">
<Image Source="/Resources/Logo.png" Width="32" Height="32" Margin="12,12,20,12"></Image>
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<TextBlock
VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,0,4"
TextAlignment="Center" Text="QuestPDF Previewer" FontSize="16" Foreground="#FFF" FontWeight="SemiBold" />
<StackPanel Orientation="Horizontal" Classes="MainMenu">
<TextBlock Text="Getting Started" />
<TextBlock Text="Documentation" />
<TextBlock Text="GitHub" />
<TextBlock Text="Sponsor project" />
</StackPanel>
</StackPanel>
</StackPanel>
</Panel>
<Grid Grid.Row="1" Grid.Column="0" Background="#444">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="32" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Panel Grid.Row="0">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Margin="16,8"
TextAlignment="Center" Text="Document hierarchy" Foreground="#DFFF" FontWeight="SemiBold" IsHitTestVisible="False" />
</Panel>
<Panel Grid.Row="1">
<ScrollViewer>
<previewer:MyHierarchy Hierarchy="{Binding Items}" Margin="-8,0,0,0" Selection="{Binding SelectedItem, Mode=TwoWay}"></previewer:MyHierarchy>
</ScrollViewer>
</Panel>
<Panel Grid.Row="3">
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Left" Margin="16,8"
TextAlignment="Center" Text="Selected element properties" Foreground="#DFFF" FontWeight="SemiBold" IsHitTestVisible="False" />
</Panel>
<Panel Grid.Row="4">
<ItemsRepeater Items="{Binding SelectedItem.Metadata}" Margin="16, 2">
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="previewer:Metadata">
<Grid Margin="0, 4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100" />
<ColumnDefinition Width="16" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Label}" Foreground="#AFFF" FontSize="12" />
<TextBlock Grid.Column="2" Text="{Binding Value}" Foreground="#AFFF" FontSize="12" />
</Grid>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</Panel>
</Grid>
<GridSplitter Grid.Column="1" Grid.Row="1" Background="#444" ResizeDirection="Columns" />
<previewer:PreviewerControl Grid.Row="1" Grid.Column="2"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
CurrentSelection="{Binding SelectedItem, Mode=TwoWay}"
Hierarchy="{Binding Items}"
Pages="{Binding Pages, Mode=OneWay}" />
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Vertical" VerticalAlignment="Bottom" Spacing="16" Margin="32">
<Button Classes="actions"
Command="{Binding ShowPdfCommand, Mode=OneTime}"
IsVisible="{Binding !!Pages.Count}"
ToolTip.Tip="Generates PDF file and shows it in the default browser. Useful for testing compatibility and interactive links.">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="White" Data="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H13C12.59,21.75 12.2,21.44 11.86,21.1C11.53,20.77 11.25,20.4 11,20H6V4H13V9H18V10.18C18.71,10.34 19.39,10.61 20,11V8L14,2M20.31,18.9C21.64,16.79 21,14 18.91,12.68C16.8,11.35 14,12 12.69,14.08C11.35,16.19 12,18.97 14.09,20.3C15.55,21.23 17.41,21.23 18.88,20.32L22,23.39L23.39,22L20.31,18.9M16.5,19A2.5,2.5 0 0,1 14,16.5A2.5,2.5 0 0,1 16.5,14A2.5,2.5 0 0,1 19,16.5A2.5,2.5 0 0,1 16.5,19Z" />
</Canvas>
</Viewbox>
</Button>
<Button Classes="actions"
Command="{Binding ShowDocumentationCommand, Mode=OneTime}"
ToolTip.Tip="Opens official QuestPDF documentation">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="White" Data="M19 1L14 6V17L19 12.5V1M21 5V18.5C19.9 18.15 18.7 18 17.5 18C15.8 18 13.35 18.65 12 19.5V6C10.55 4.9 8.45 4.5 6.5 4.5C4.55 4.5 2.45 4.9 1 6V20.65C1 20.9 1.25 21.15 1.5 21.15C1.6 21.15 1.65 21.1 1.75 21.1C3.1 20.45 5.05 20 6.5 20C8.45 20 10.55 20.4 12 21.5C13.35 20.65 15.8 20 17.5 20C19.15 20 20.85 20.3 22.25 21.05C22.35 21.1 22.4 21.1 22.5 21.1C22.75 21.1 23 20.85 23 20.6V6C22.4 5.55 21.75 5.25 21 5M10 18.41C8.75 18.09 7.5 18 6.5 18C5.44 18 4.18 18.19 3 18.5V7.13C3.91 6.73 5.14 6.5 6.5 6.5C7.86 6.5 9.09 6.73 10 7.13V18.41Z" />
</Canvas>
</Viewbox>
</Button>
<Button Classes="actions"
Command="{Binding SponsorProjectCommand, Mode=OneTime}"
ToolTip.Tip="Do you like QuestPDF? Please consider sponsoring the project. It really helps!">
<Viewbox Width="24" Height="24">
<Canvas Width="24" Height="24">
<Path Fill="White" Data="M12,21.1L10.5,22.4C3.9,16.5 0.5,13.4 0.5,9.6C0.5,8.4 0.9,7.3 1.5,6.4C1.5,6.6 1.5,6.8 1.5,7C1.5,11.7 5.4,15.2 12,21.1M13.6,17C18.3,12.7 21.5,9.9 21.6,7C21.6,5 20.1,3.5 18.1,3.5C16.5,3.5 15,4.5 14.5,5.9H12.6C12,4.5 10.5,3.5 9,3.5C7,3.5 5.5,5 5.5,7C5.5,9.9 8.6,12.7 13.4,17L13.5,17.1M18,1.5C21.1,1.5 23.5,3.9 23.5,7C23.5,10.7 20.1,13.8 13.5,19.8C6.9,13.9 3.5,10.8 3.5,7C3.5,3.9 5.9,1.5 9,1.5C10.7,1.5 12.4,2.3 13.5,3.6C14.6,2.3 16.3,1.5 18,1.5Z" />
</Canvas>
</Viewbox>
</Button>
</StackPanel>
<ScrollBar Grid.Row="1" Grid.Column="1"
Orientation="Vertical"
AllowAutoHide="False"
Minimum="0" Maximum="1"
IsVisible="{Binding VerticalScrollbarVisible, Mode=OneWay}"
Value="{Binding CurrentScroll, Mode=TwoWay}"
ViewportSize="{Binding ScrollViewportSize, Mode=OneWay}"></ScrollBar>
<ScrollBar Grid.Row="1" Grid.Column="3"
Orientation="Vertical"
AllowAutoHide="False"
Minimum="0" Maximum="1"
IsVisible="{Binding VerticalScrollbarVisible, Mode=OneWay}"
Value="{Binding CurrentScroll, Mode=TwoWay}"
ViewportSize="{Binding ScrollViewportSize, Mode=OneWay}"></ScrollBar>
</Grid>
</Panel>
</Window>
+2 -1
View File
@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace QuestPDF.Previewer
@@ -1,5 +1,7 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text.Json;
using Avalonia;
using ReactiveUI;
using Unit = System.Reactive.Unit;
using Avalonia.Threading;
@@ -51,6 +53,8 @@ namespace QuestPDF.Previewer
ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/documentation/api-reference.html"));
SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
LoadItems();
}
private void ShowPdf()
@@ -85,5 +89,89 @@ namespace QuestPDF.Previewer
foreach (var page in oldPages)
page.Picture.Dispose();
}
private InspectionElement _selectedItem;
public InspectionElement SelectedItem
{
get => _selectedItem;
set => this.RaiseAndSetIfChanged(ref _selectedItem, value);
}
private InspectionElement items;
public InspectionElement Items
{
get => items;
set => this.RaiseAndSetIfChanged(ref items, value);
}
public void LoadItems()
{
var text = File.ReadAllText("hierarchy.json");
Items = JsonSerializer.Deserialize<InspectionElement>(text);
UpdateLevel(Items);
}
private void UpdateLevel(InspectionElement root)
{
var currentLevel = 1;
Traverse(root);
void Traverse(InspectionElement element)
{
element.Level = currentLevel;
currentLevel++;
element.Children.ForEach(Traverse);
currentLevel--;
}
}
}
public class InspectionElementLocation
{
public int PageNumber { get; set; }
public float Top { get; set; }
public float Left { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}
public class Metadata
{
public string Label { get; set; }
public string Value { get; set; }
public Metadata(string label, string value)
{
Label = label;
Value = value;
}
}
public class InspectionElement
{
public string Element { get; set; }
public List<InspectionElementLocation> Location { get; set; }
public Dictionary<string, string> Properties { get; set; }
public List<InspectionElement> Children { get; set; }
public int Level { get; set; }
public string FontColor => Element == "DebugPointer" ? "#FFF" : "#AFFF";
public string Text => Element == "DebugPointer" ? Properties.First(x => x.Key == "Target").Value : Element;
public bool Expanded { get; set; }
public IList<Metadata> Metadata => ListMetadata().ToList();
public IEnumerable<Metadata> ListMetadata()
{
yield return new Metadata("Element name", Element);
yield return new Metadata("Position left", Location[0].Left.ToString("N2"));
yield return new Metadata("Position top", Location[0].Top.ToString("N2"));
yield return new Metadata("Width", Location[0].Width.ToString("N2"));
yield return new Metadata("Height", Location[0].Height.ToString("N2"));
foreach (var property in Properties)
yield return new Metadata(property.Key, property.Value);
}
}
}
+6 -2
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.5.0</Version>
<Version>2022.6.0</Version>
<PackAsTool>true</PackAsTool>
<ToolCommandName>questpdf-previewer</ToolCommandName>
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
@@ -38,6 +38,9 @@
<Visible>false</Visible>
<PackagePath>\</PackagePath>
</None>
<None Update="hierarchy.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
@@ -48,7 +51,8 @@
<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.3" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
</ItemGroup>
</Project>
File diff suppressed because one or more lines are too long
@@ -55,7 +55,7 @@ namespace QuestPDF.ReportSample.Layouts
row.Spacing(50);
row.RelativeItem().PaddingTop(-10).Text(Model.Title).Style(Typography.Title);
row.ConstantItem(90).ExternalLink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
row.ConstantItem(90).Hyperlink("https://www.questpdf.com").MaxHeight(30).Component<ImagePlaceholder>();
});
column.Item().ShowOnce().PaddingVertical(15).Border(1f).BorderColor(Colors.Grey.Lighten1).ExtendHorizontal();
@@ -88,14 +88,14 @@ namespace QuestPDF.ReportSample.Layouts
column.Item().PageBreak();
foreach (var section in Model.Sections)
column.Item().Location(section.Title).Component(new SectionTemplate(section));
column.Item().Section(section.Title).Component(new SectionTemplate(section));
column.Item().PageBreak();
column.Item().Location("Photos");
column.Item().Section("Photos");
foreach (var photo in Model.Photos)
column.Item().Component(new PhotoTemplate(photo));
});
}
}
}
}
@@ -3,6 +3,7 @@ using System.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.ReportSample.Layouts
{
@@ -41,18 +42,31 @@ namespace QuestPDF.ReportSample.Layouts
private void DrawLink(IContainer container, int number, string locationName)
{
container
.InternalLink(locationName)
.SectionLink(locationName)
.Row(row =>
{
row.ConstantItem(25).Text($"{number}.");
row.RelativeItem().Text(locationName);
row.ConstantItem(150).AlignRight().Text(text =>
row.ConstantItem(20).Text($"{number}.");
row.AutoItem().Text(locationName);
row.RelativeItem().PaddingHorizontal(2).AlignBottom().TranslateY(-3).Height(1).Canvas((canvas, space) =>
{
// best to statically cache
using var paint = new SKPaint
{
StrokeWidth = space.Height,
PathEffect = SKPathEffect.CreateDash(new float[] { 1, 3 }, 0)
};
canvas.DrawLine(0, 0, space.Width, 0, paint);
});
row.AutoItem().Text(text =>
{
text.BeginPageNumberOfSection(locationName);
text.Span(" - ");
text.EndPageNumberOfSection(locationName);
var lengthStyle = TextStyle.Default.Color(Colors.Grey.Medium);
var lengthStyle = TextStyle.Default.FontColor(Colors.Grey.Medium);
text.Span(" (").Style(lengthStyle);
text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x => x == 1 ? "1 page long" : $"{x} pages long");
@@ -61,4 +75,4 @@ namespace QuestPDF.ReportSample.Layouts
});
}
}
}
}
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<LangVersion>8</LangVersion>
<RootNamespace>QuestPDF.ReportSample</RootNamespace>
@@ -12,7 +12,7 @@
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.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>

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

+4 -4
View File
@@ -6,8 +6,8 @@ namespace QuestPDF.ReportSample
{
public static class Typography
{
public static TextStyle Title => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Darken3).Size(26).Black();
public static TextStyle Headline => TextStyle.Default.FontType(Fonts.Calibri).Color(Colors.Blue.Medium).Size(16).SemiBold();
public static TextStyle Normal => TextStyle.Default.FontType(Fonts.Verdana).Color(Colors.Black).Size(10).LineHeight(1.2f);
public static TextStyle Title => TextStyle.Default.FontFamily(Fonts.Calibri).FontColor(Colors.Blue.Darken3).FontSize(26).Black();
public static TextStyle Headline => TextStyle.Default.FontFamily(Fonts.Calibri).FontColor(Colors.Blue.Medium).FontSize(16).SemiBold();
public static TextStyle Normal => TextStyle.Default.FontFamily(Fonts.Verdana).FontColor(Colors.Black).FontSize(10).LineHeight(1.2f);
}
}
}
+1 -1
View File
@@ -10,7 +10,7 @@
<PackageReference Include="nunit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.0.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>
+2 -2
View File
@@ -1,4 +1,5 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
using SkiaSharp;
@@ -10,7 +11,6 @@ namespace QuestPDF.UnitTests.TestEngine
public Action<float> RotateFunc { get; set; }
public Action<float, float> ScaleFunc { get; set; }
public Action<SKImage, Position, Size> DrawImageFunc { get; set; }
public Action<string, Position, TextStyle> DrawTextFunc { get; set; }
public Action<Position, Size, string> DrawRectFunc { get; set; }
public void Translate(Position vector) => TranslateFunc(vector);
@@ -18,7 +18,7 @@ namespace QuestPDF.UnitTests.TestEngine
public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
public void DrawText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
using QuestPDF.UnitTests.TestEngine.Operations;
using SkiaSharp;
@@ -15,7 +16,7 @@ namespace QuestPDF.UnitTests.TestEngine
public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY));
public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style) => throw new NotImplementedException();
public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
public void DrawHyperlink(string url, Size size) => throw new NotImplementedException();
-18
View File
@@ -82,19 +82,6 @@ namespace QuestPDF.UnitTests.TestEngine
Assert.AreEqual(expected.Color, color, "Draw rectangle: color");
},
DrawTextFunc = (text, position, style) =>
{
var expected = GetExpected<CanvasDrawTextOperation>();
Assert.AreEqual(expected.Text, text);
Assert.AreEqual(expected.Position.X, position.X, "Draw text: X");
Assert.AreEqual(expected.Position.Y, position.Y, "Draw text: Y");
Assert.AreEqual(expected.Style.Color, style.Color, "Draw text: color");
Assert.AreEqual(expected.Style.FontFamily, style.FontFamily, "Draw text: font");
Assert.AreEqual(expected.Style.Size, style.Size, "Draw text: size");
},
DrawImageFunc = (image, position, size) =>
{
var expected = GetExpected<CanvasDrawImageOperation>();
@@ -201,11 +188,6 @@ namespace QuestPDF.UnitTests.TestEngine
return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
}
public TestPlan ExpectCanvasDrawText(string text, Position position, TextStyle style)
{
return AddOperation(new CanvasDrawTextOperation(text, position, style));
}
public TestPlan ExpectCanvasDrawImage(Position position, Size size)
{
return AddOperation(new CanvasDrawImageOperation(position, size));
+1
View File
@@ -16,6 +16,7 @@ namespace QuestPDF.Drawing
var container = new Container();
container
.DebugPointer("Document")
.Column(column =>
{
Pages
+226 -6
View File
@@ -1,7 +1,9 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements;
@@ -10,6 +12,8 @@ using QuestPDF.Elements.Text.Items;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace QuestPDF.Drawing
{
@@ -75,9 +79,218 @@ namespace QuestPDF.Drawing
ApplyCaching(content);
RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
if (metadata.ApplyInspection)
ApplyInspection(content);
RenderPass(pageContext, canvas, content, metadata, debuggingState);
if (metadata.ApplyInspection)
{
var x = TraverseStatisticsToJson(content);
var y = JsonSerializer.Serialize(x);
}
}
internal static string TraverseStructure(Element container)
{
var builder = new StringBuilder();
var nestingLevel = 0;
Traverse(container);
return builder.ToString();
void Traverse(Element item)
{
var indent = new string(' ', nestingLevel * 4);
var title = item.GetType().Name;
builder.AppendLine(indent + title);
nestingLevel++;
foreach (var child in item.GetChildren())
Traverse(child);
nestingLevel--;
}
}
internal static string TraverseStatistics(Element container, int pageNumber)
{
var builder = new StringBuilder();
var nestingLevel = 0;
Traverse(container);
return builder.ToString();
void Traverse(Element item)
{
if (item is DebuggingProxy or CacheProxy or Container)
{
Traverse(item.GetChildren().First());
return;
}
var inspectionItem = item as InspectionProxy;
if (inspectionItem == null)
{
var children = item.GetChildren().ToList();
if (children.Count > 1)
nestingLevel++;
children.ForEach(Traverse);
if (children.Count > 1)
nestingLevel--;
return;
}
if (!inspectionItem.Statistics.ContainsKey(pageNumber))
return;
var statistics = inspectionItem.Statistics[pageNumber];
var indent = new string(' ', nestingLevel * 4);
var title = statistics.Element.GetType().Name;
builder.AppendLine(indent + title);
builder.AppendLine(indent + new string('-', title.Length));
builder.AppendLine(indent + "Size: " + statistics.Size);
builder.AppendLine(indent + "Position: " + statistics.Position);
foreach (var configuration in DebuggingState.GetElementConfiguration(statistics.Element))
builder.AppendLine(indent + configuration);
builder.AppendLine();
Traverse(inspectionItem.Child);
}
}
internal static InspectionElement TraverseStatisticsToJson(Element container)
{
return Traverse(container);
InspectionElement? Traverse(Element item)
{
InspectionElement? result = null;
Element currentItem = item;
while (true)
{
if (currentItem is InspectionProxy proxy)
{
if (proxy.Child.GetType() == typeof(Container))
{
currentItem = proxy.Child;
continue;
}
var statistics = GetInspectionElement(proxy);
if (statistics == null)
return null;
if (result == null)
{
result = statistics;
}
else
{
result.Children.Add(statistics);
}
currentItem = proxy.Child;
}
else
{
var children = currentItem.GetChildren().ToList();
if (children.Count == 0)
{
return result;
}
else if (children.Count == 1)
{
currentItem = children.First();
continue;
}
else
{
children
.Select(Traverse)
.Where(x => x != null)
.ToList()
.ForEach(result.Children.Add);
return result;
}
}
}
}
static InspectionElement? GetInspectionElement(InspectionProxy inspectionProxy)
{
var locations = inspectionProxy
.Statistics
.Keys
.Select(x =>
{
var statistics = inspectionProxy.Statistics[x];
return new InspectionElementLocation
{
PageNumber = x,
Top = statistics.Position.Y,
Left = statistics.Position.X,
Width = statistics.Size.Width,
Height = statistics.Size.Height,
};
})
.ToList();
return new InspectionElement
{
Element = inspectionProxy.Child.GetType().Name,
Location = locations,
Properties = GetElementConfiguration(inspectionProxy.Child),
Children = new List<InspectionElement>()
};
}
static Dictionary<string, string> GetElementConfiguration(IElement element)
{
return element
.GetType()
.GetProperties()
.Select(x => new
{
Property = x.Name.PrettifyName(),
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.ToDictionary(x => x.Property, x => FormatValue(x.Value));
string FormatValue(object value)
{
const int maxLength = 100;
var text = value?.ToString() ?? "-";
if (text.Length < maxLength)
return text;
return text.AsSpan(0, maxLength).ToString() + "...";
}
}
}
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DocumentMetadata documentMetadata, DebuggingState? debuggingState)
where TCanvas : ICanvas, IRenderingCanvas
{
@@ -135,11 +348,10 @@ namespace QuestPDF.Drawing
$"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " +
$"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
$"Please analyze your documents structure to detect this element and fix its size constraints.";
throw new DocumentLayoutException(message)
{
ElementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode."
};
var elementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode.";
throw new DocumentLayoutException(message, elementTrace);
}
}
@@ -158,11 +370,19 @@ namespace QuestPDF.Drawing
content.VisitChildren(x =>
{
x.CreateProxy(y => new DebuggingProxy(debuggingState, y));
x.CreateProxy(y => y is ElementProxy ? y : new DebuggingProxy(debuggingState, y));
});
return debuggingState;
}
private static void ApplyInspection(Container content)
{
content.VisitChildren(x =>
{
x.CreateProxy(y => y is ElementProxy ? y : new InspectionProxy(y));
});
}
internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
{
+1
View File
@@ -26,6 +26,7 @@ namespace QuestPDF.Drawing
public bool ApplyCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
public bool ApplyDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
public bool ApplyInspection { get; set; } = System.Diagnostics.Debugger.IsAttached;
public static DocumentMetadata Default => new DocumentMetadata();
}
@@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions
{
public class DocumentComposeException : Exception
{
public DocumentComposeException()
{
}
public DocumentComposeException(string message) : base(message)
{
}
public DocumentComposeException(string message, Exception inner) : base(message, inner)
internal DocumentComposeException(string message) : base(message)
{
}
@@ -4,17 +4,7 @@ namespace QuestPDF.Drawing.Exceptions
{
public class DocumentDrawingException : Exception
{
public DocumentDrawingException()
{
}
public DocumentDrawingException(string message) : base(message)
{
}
public DocumentDrawingException(string message, Exception inner) : base(message, inner)
internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
{
}
@@ -4,21 +4,11 @@ namespace QuestPDF.Drawing.Exceptions
{
public class DocumentLayoutException : Exception
{
public string ElementTrace { get; set; }
public DocumentLayoutException()
{
}
public string? ElementTrace { get; }
public DocumentLayoutException(string message) : base(message)
internal DocumentLayoutException(string message, string? elementTrace = null) : base(message)
{
}
public DocumentLayoutException(string message, Exception inner) : base(message, inner)
{
ElementTrace = elementTrace;
}
}
}
@@ -0,0 +1,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. " +
$"Some operating systems may require installing multiple nugets, e.g. MacOS may need both {nugetConvention}.macOS and {nugetConvention}.Linux." +
$"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.Linux");
if (innerExceptionMessage.Contains("libHarfBuzzSharp"))
return ("HarfBuzzSharp", "HarfBuzzSharp.NativeAssets");
// default
return ("SkiaSharp-related", "*.NativeAssets.Linux");
}
}
}
}
+42 -4
View File
@@ -2,9 +2,11 @@
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using HarfBuzzSharp;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
@@ -12,8 +14,11 @@ namespace QuestPDF.Drawing
{
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
private static ConcurrentDictionary<object, SKPaint> Paints = new();
private static ConcurrentDictionary<string, SKPaint> ColorPaint = new();
private static ConcurrentDictionary<object, SKPaint> FontPaints = new();
private static ConcurrentDictionary<string, SKPaint> ColorPaints = new();
private static ConcurrentDictionary<object, Font> ShaperFonts = new();
private static ConcurrentDictionary<object, SKFont> Fonts = new();
private static ConcurrentDictionary<object, TextShaper> TextShapers = new();
private static void RegisterFontType(SKData fontData, string? customName = null)
{
@@ -47,7 +52,7 @@ namespace QuestPDF.Drawing
internal static SKPaint ColorToPaint(this string color)
{
return ColorPaint.GetOrAdd(color, Convert);
return ColorPaints.GetOrAdd(color, Convert);
static SKPaint Convert(string color)
{
@@ -61,7 +66,7 @@ namespace QuestPDF.Drawing
internal static SKPaint ToPaint(this TextStyle style)
{
return Paints.GetOrAdd(style.PaintKey, key => Convert(style));
return FontPaints.GetOrAdd(style.PaintKey, key => Convert(style));
static SKPaint Convert(TextStyle style)
{
@@ -122,5 +127,38 @@ namespace QuestPDF.Drawing
{
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
}
internal static Font ToShaperFont(this TextStyle style)
{
return ShaperFonts.GetOrAdd(style.PaintKey, _ =>
{
var typeface = style.ToPaint().Typeface;
using var harfBuzzBlob = typeface.OpenStream(out var ttcIndex).ToHarfBuzzBlob();
using var face = new Face(harfBuzzBlob, ttcIndex)
{
Index = ttcIndex,
UnitsPerEm = typeface.UnitsPerEm,
GlyphCount = typeface.GlyphCount
};
var font = new Font(face);
font.SetScale(TextShaper.FontShapingScale, TextShaper.FontShapingScale);
font.SetFunctionsOpenType();
return font;
});
}
internal static TextShaper ToTextShaper(this TextStyle style)
{
return TextShapers.GetOrAdd(style.PaintKey, _ => new TextShaper(style));
}
internal static SKFont FoFont(this TextStyle style)
{
return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont());
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ namespace QuestPDF.Drawing
}
public void DrawText(string text, Position position, TextStyle style)
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
{
}
+17 -3
View File
@@ -1,4 +1,6 @@
using System.IO;
using System;
using System.IO;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Helpers;
using SkiaSharp;
@@ -7,11 +9,23 @@ namespace QuestPDF.Drawing
internal class PdfCanvas : SkiaDocumentCanvasBase
{
public PdfCanvas(Stream stream, DocumentMetadata documentMetadata)
: base(SKDocument.CreatePdf(stream, MapMetadata(documentMetadata)))
: base(CreatePdf(stream, documentMetadata))
{
}
private static SKDocument CreatePdf(Stream stream, DocumentMetadata documentMetadata)
{
try
{
return SKDocument.CreatePdf(stream, MapMetadata(documentMetadata));
}
catch (TypeInitializationException exception)
{
throw new InitializationException("PDF", exception);
}
}
private static SKDocumentPdfMetadata MapMetadata(DocumentMetadata metadata)
{
return new SKDocumentPdfMetadata
+25 -25
View File
@@ -94,36 +94,36 @@ namespace QuestPDF.Drawing.Proxy
item.Stack.ToList().ForEach(Traverse);
nestingLevel--;
}
static IEnumerable<string> GetElementConfiguration(IElement element)
{
if (element is DebugPointer)
return Enumerable.Empty<string>();
}
internal static IEnumerable<string> GetElementConfiguration(IElement element)
{
if (element is DebugPointer)
return Enumerable.Empty<string>();
return element
.GetType()
.GetProperties()
.Select(x => new
{
Property = x.Name.PrettifyName(),
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.Select(x => $"{x.Property}: {FormatValue(x.Value)}");
string FormatValue(object value)
return element
.GetType()
.GetProperties()
.Select(x => new
{
const int maxLength = 100;
Property = x.Name.PrettifyName(),
Value = x.GetValue(element)
})
.Where(x => !(x.Value is IElement))
.Where(x => x.Value is string || !(x.Value is IEnumerable))
.Where(x => !(x.Value is TextStyle))
.Select(x => $"{x.Property}: {FormatValue(x.Value)}");
string FormatValue(object value)
{
const int maxLength = 100;
var text = value?.ToString() ?? "-";
var text = value?.ToString() ?? "-";
if (text.Length < maxLength)
return text;
if (text.Length < maxLength)
return text;
return text.AsSpan(0, maxLength).ToString() + "...";
}
return text.AsSpan(0, maxLength).ToString() + "...";
}
}
}
+35
View File
@@ -0,0 +1,35 @@
using System.Collections;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class InspectionProxy : ElementProxy
{
public Dictionary<int, InspectionStateItem> Statistics { get; set; } = new();
public InspectionProxy(Element child)
{
Child = child;
}
internal override void Draw(Size availableSpace)
{
if (Canvas is SkiaCanvasBase canvas)
{
var matrix = canvas.Canvas.TotalMatrix;
var inspectionItem = new InspectionStateItem
{
Element = Child,
Position = new Position(matrix.TransX, matrix.TransY),
Size = availableSpace
};
Statistics[PageContext.CurrentPage] = inspectionItem;
}
base.Draw(availableSpace);
}
}
}
@@ -0,0 +1,7 @@
namespace QuestPDF.Drawing.Proxy
{
public class InspectionState
{
}
}
@@ -0,0 +1,29 @@
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class InspectionStateItem
{
public Element Element { get; internal set; }
public Size Size { get; internal set; }
public Position Position { get; internal set; }
}
internal class InspectionElementLocation
{
public int PageNumber { get; internal set; }
public float Top { get; set; }
public float Left { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}
internal class InspectionElement
{
public string Element { get; internal set; }
public ICollection<InspectionElementLocation> Location { get; internal set; }
public Dictionary<string, string> Properties { get; internal set; }
public ICollection<InspectionElement> Children { get; set; }
}
}
+3 -2
View File
@@ -1,5 +1,6 @@
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
namespace QuestPDF.Drawing
{
@@ -27,9 +28,9 @@ namespace QuestPDF.Drawing
Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint);
}
public void DrawText(string text, Position vector, TextStyle style)
public void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style)
{
Canvas.DrawText(text, vector.X, vector.Y, style.ToPaint());
Canvas.DrawText(skTextBlob, position.X, position.Y, style.ToPaint());
}
public void DrawImage(SKImage image, Position vector, Size size)
+155
View File
@@ -0,0 +1,155 @@
using System;
using HarfBuzzSharp;
using QuestPDF.Infrastructure;
using SkiaSharp;
using Buffer = HarfBuzzSharp.Buffer;
namespace QuestPDF.Drawing
{
internal class TextShaper
{
public const int FontShapingScale = 512;
private Font Font { get; }
private SKPaint Paint { get; }
public TextShaper(TextStyle style)
{
Font = style.ToShaperFont();
Paint = style.ToPaint();
}
public TextShapingResult Shape(string text)
{
var buffer = new Buffer();
PopulateBufferWithText(buffer, text);
buffer.GuessSegmentProperties();
Font.Shape(buffer);
var length = buffer.Length;
var glyphInfos = buffer.GlyphInfos;
var glyphPositions = buffer.GlyphPositions;
var scaleY = Paint.TextSize / FontShapingScale;
var scaleX = scaleY * Paint.TextScaleX;
var xOffset = 0f;
var yOffset = 0f;
var glyphs = new ShapedGlyph[length];
for (var i = 0; i < length; i++)
{
glyphs[i] = new ShapedGlyph
{
Codepoint = (ushort)glyphInfos[i].Codepoint,
Position = new SKPoint(xOffset + glyphPositions[i].XOffset * scaleX, yOffset - glyphPositions[i].YOffset * scaleY),
Width = glyphPositions[i].XAdvance * scaleX
};
xOffset += glyphPositions[i].XAdvance * scaleX;
yOffset += glyphPositions[i].YAdvance * scaleY;
}
return new TextShapingResult(glyphs);
}
void PopulateBufferWithText(Buffer buffer, string text)
{
var encoding = Paint.TextEncoding;
if (encoding == SKTextEncoding.Utf8)
buffer.AddUtf8(text);
else if (encoding == SKTextEncoding.Utf16)
buffer.AddUtf16(text);
else if (encoding == SKTextEncoding.Utf32)
buffer.AddUtf32(text);
else
throw new NotSupportedException("TextEncoding of type GlyphId is not supported.");
}
}
internal struct ShapedGlyph
{
public ushort Codepoint;
public SKPoint Position;
public float Width;
}
internal struct DrawTextCommand
{
public SKTextBlob SkTextBlob;
public float TextOffsetX;
}
internal class TextShapingResult
{
public ShapedGlyph[] Glyphs { get; }
public TextShapingResult(ShapedGlyph[] glyphs)
{
Glyphs = glyphs;
}
public int BreakText(int startIndex, float maxWidth)
{
var index = startIndex;
maxWidth += Glyphs[startIndex].Position.X;
while (index < Glyphs.Length)
{
var glyph = Glyphs[index];
if (glyph.Position.X + glyph.Width > maxWidth + Size.Epsilon)
break;
index++;
}
return index - 1;
}
public float MeasureWidth(int startIndex, int endIndex)
{
if (Glyphs.Length == 0)
return 0;
var start = Glyphs[startIndex];
var end = Glyphs[endIndex];
return end.Position.X - start.Position.X + end.Width;
}
public DrawTextCommand? PositionText(int startIndex, int endIndex, TextStyle textStyle)
{
if (Glyphs.Length == 0)
return null;
using var skTextBlobBuilder = new SKTextBlobBuilder();
var positionedRunBuffer = skTextBlobBuilder.AllocatePositionedRun(textStyle.FoFont(), endIndex - startIndex + 1);
var glyphSpan = positionedRunBuffer.GetGlyphSpan();
var positionSpan = positionedRunBuffer.GetPositionSpan();
for (var sourceIndex = startIndex; sourceIndex <= endIndex; sourceIndex++)
{
var runIndex = sourceIndex - startIndex;
glyphSpan[runIndex] = Glyphs[sourceIndex].Codepoint;
positionSpan[runIndex] = Glyphs[sourceIndex].Position;
}
return new DrawTextCommand
{
SkTextBlob = skTextBlobBuilder.Build(),
TextOffsetX = -Glyphs[startIndex].Position.X
};
}
}
}
+16 -2
View File
@@ -1,4 +1,6 @@
using System.IO;
using System;
using System.IO;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Helpers;
using SkiaSharp;
@@ -7,9 +9,21 @@ namespace QuestPDF.Drawing
internal class XpsCanvas : SkiaDocumentCanvasBase
{
public XpsCanvas(Stream stream, DocumentMetadata documentMetadata)
: base(SKDocument.CreateXps(stream, documentMetadata.RasterDpi))
: base(CreateXps(stream, documentMetadata))
{
}
private static SKDocument CreateXps(Stream stream, DocumentMetadata documentMetadata)
{
try
{
return SKDocument.CreateXps(stream, documentMetadata.RasterDpi);
}
catch (TypeInitializationException exception)
{
throw new InitializationException("XPS", exception);
}
}
}
}
+2
View File
@@ -52,6 +52,7 @@ namespace QuestPDF.Elements
var context = new DynamicContext
{
PageNumber = PageContext.CurrentPage,
TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd,
PageContext = PageContext,
Canvas = Canvas,
TextStyle = TextStyle,
@@ -75,6 +76,7 @@ namespace QuestPDF.Elements
internal TextStyle TextStyle { get; set; }
public int PageNumber { get; internal set; }
public int TotalPages { get; internal set; }
public Size AvailableSize { get; internal set; }
public IDynamicElement CreateElement(Action<IContainer> content)
+1 -1
View File
@@ -19,7 +19,7 @@ namespace QuestPDF.Elements
{
return Children;
}
internal override SpacePlan Measure(Size availableSpace)
{
return Children
+2
View File
@@ -30,6 +30,7 @@ namespace QuestPDF.Elements
public void Compose(IContainer container)
{
container
.Container()
.Background(BackgroundColor)
.Layers(layers =>
{
@@ -40,6 +41,7 @@ namespace QuestPDF.Elements
layers
.PrimaryLayer()
.DebugPointer("Page content")
.MinWidth(MinSize.Width)
.MinHeight(MinSize.Height)
+5 -18
View File
@@ -46,11 +46,6 @@ namespace QuestPDF.Elements
return Items;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Items.ForEach(x => x.Child = create(x.Child));
}
internal override SpacePlan Measure(Size availableSpace)
{
if (!Items.Any())
@@ -102,13 +97,14 @@ namespace QuestPDF.Elements
private void UpdateItemsWidth(float availableWidth)
{
HandleItemsWithAutoWidth();
foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto))
rowItem.Size = rowItem.Measure(Size.Max).Width;
var constantWidth = Items.Where(x => x.Type == RowItemType.Constant).Sum(x => x.Size);
var constantWidth = Items.Where(x => x.Type != RowItemType.Relative).Sum(x => x.Size);
var relativeWidth = Items.Where(x => x.Type == RowItemType.Relative).Sum(x => x.Size);
var spacingWidth = (Items.Count - 1) * Spacing;
foreach (var item in Items.Where(x => x.Type == RowItemType.Constant))
foreach (var item in Items.Where(x => x.Type != RowItemType.Relative))
item.Width = item.Size;
if (relativeWidth <= 0)
@@ -119,16 +115,7 @@ namespace QuestPDF.Elements
foreach (var item in Items.Where(x => x.Type == RowItemType.Relative))
item.Width = item.Size * widthPerRelativeUnit;
}
private void HandleItemsWithAutoWidth()
{
foreach (var rowItem in Items.Where(x => x.Type == RowItemType.Auto))
{
rowItem.Size = rowItem.Measure(Size.Max).Width;
rowItem.Type = RowItemType.Constant;
}
}
private ICollection<RowItemRenderingCommand> PlanLayout(Size availableSpace)
{
var leftOffset = 0f;
@@ -5,7 +5,7 @@ using QuestPDF.Fluent;
namespace QuestPDF.Elements.Table
{
static class TableLayoutValidator
static class TableLayoutPlanner
{
public static void PlanCellPositions(this Table table)
{
@@ -6,7 +6,7 @@ using QuestPDF.Fluent;
namespace QuestPDF.Elements.Table
{
static class TableLayoutPlanner
static class TableLayoutValidator
{
public static void ValidateCellPositions(this Table table)
{
@@ -1,4 +1,5 @@
using QuestPDF.Infrastructure;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Calculation
{
@@ -1,4 +1,5 @@
using System;
using QuestPDF.Drawing;
namespace QuestPDF.Elements.Text.Calculation
{
@@ -8,6 +8,7 @@ namespace QuestPDF.Elements.Text.Items
{
public const string PageNumberPlaceholder = "123";
public Func<IPageContext, string> Source { get; set; } = _ => PageNumberPlaceholder;
protected override bool EnableTextCache => false;
public override TextMeasurementResult? Measure(TextMeasurementRequest request)
{
@@ -3,7 +3,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSectionlLink : TextBlockSpan
internal class TextBlockSectionLink : TextBlockSpan
{
public string SectionName { get; set; }
+57 -41
View File
@@ -1,20 +1,24 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
using SkiaSharp.HarfBuzz;
using Size = QuestPDF.Infrastructure.Size;
namespace QuestPDF.Elements.Text.Items
{
internal class TextBlockSpan : ITextBlockItem
{
private const char Space = ' ';
public string Text { get; set; }
public TextStyle Style { get; set; } = new();
public TextShapingResult? TextShapingResult { get; set; }
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
protected virtual bool EnableTextCache => true;
public virtual TextMeasurementResult? Measure(TextMeasurementRequest request)
{
@@ -25,11 +29,17 @@ namespace QuestPDF.Elements.Text.Items
return MeasureCache[cacheKey];
}
internal TextMeasurementResult? MeasureWithoutCache(TextMeasurementRequest request)
{
if (!EnableTextCache)
TextShapingResult = null;
TextShapingResult ??= Style.ToTextShaper().Shape(Text);
var paint = Style.ToPaint();
var fontMetrics = Style.ToFontMetrics();
var spaceCodepoint = paint.ToFont().Typeface.GetGlyphs(" ")[0];
var startIndex = request.StartIndex;
@@ -37,11 +47,11 @@ namespace QuestPDF.Elements.Text.Items
// ignore leading spaces
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
{
while (startIndex < Text.Length && Text[startIndex] == Space)
while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint)
startIndex++;
}
if (Text.Length == 0 || startIndex == Text.Length)
if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length)
{
return new TextMeasurementResult
{
@@ -54,27 +64,19 @@ namespace QuestPDF.Elements.Text.Items
}
// start breaking text from requested position
var text = Text.AsSpan().Slice(startIndex);
var textLength = (int)paint.BreakText(text, request.AvailableWidth + Size.Epsilon);
var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
if (textLength <= 0)
if (endIndex < 0)
return null;
// break text only on spaces
var wrappedTextLength = WrapText(text, textLength, request.IsFirstElementInLine);
var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine);
if (wrappedTextLength == null)
if (wrappedText == null)
return null;
textLength = wrappedTextLength.Value.fragmentLength;
text = text.Slice(0, textLength);
var endIndex = startIndex + textLength;
// measure final text
var width = paint.MeasureText(text);
var width = TextShapingResult.MeasureWidth(startIndex, wrappedText.Value.endIndex);
return new TextMeasurementResult
{
@@ -86,56 +88,70 @@ namespace QuestPDF.Elements.Text.Items
LineHeight = Style.LineHeight ?? 1,
StartIndex = startIndex,
EndIndex = endIndex,
NextIndex = startIndex + wrappedTextLength.Value.nextIndex,
TotalIndex = Text.Length
EndIndex = wrappedText.Value.endIndex,
NextIndex = wrappedText.Value.nextIndex,
TotalIndex = TextShapingResult.Glyphs.Length - 1
};
}
// TODO: consider introduce text wrapping abstraction (basic, english-like, asian-like)
private (int fragmentLength, int nextIndex)? WrapText(ReadOnlySpan<char> text, int textLength, bool isFirstElementInLine)
// TODO: consider introducing text wrapping abstraction (basic, english-like, asian-like)
private (int endIndex, int nextIndex)? WrapText(int startIndex, int endIndex, bool isFirstElementInLine)
{
var spaceCodepoint = Style.ToPaint().ToFont().Typeface.GetGlyphs(" ")[0];
// textLength - length of the part of the text that fits in available width (creating a line)
// entire text fits, no need to wrap
if (textLength == text.Length)
return (textLength, textLength + 1);
if (endIndex == TextShapingResult.Glyphs.Length - 1)
return (endIndex, endIndex);
// breaking anywhere
if (Style.WrapAnywhere ?? false)
return (textLength, textLength);
return (endIndex, endIndex + 1);
// current line ends at word, next character is space, perfect place to wrap
if (text[textLength - 1] != Space && text[textLength] == Space)
return (textLength, textLength + 1);
if (TextShapingResult.Glyphs[endIndex].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex + 1].Codepoint == spaceCodepoint)
return (endIndex, endIndex + 2);
// find last space within the available text to wrap
var lastSpaceIndex = text.Slice(0, textLength).LastIndexOf(Space);
var lastSpaceIndex = endIndex;
while (lastSpaceIndex >= startIndex)
{
if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint)
break;
lastSpaceIndex--;
}
// text contains space that can be used to wrap
if (lastSpaceIndex > 0)
return (lastSpaceIndex, lastSpaceIndex + 1);
if (lastSpaceIndex >= startIndex)
return (lastSpaceIndex - 1, lastSpaceIndex + 1);
// there is no available space to wrap text
// if the item is first within the line, perform safe mode and chop the word
// otherwise, move the item into the next line
return isFirstElementInLine ? (textLength, textLength) : null;
return isFirstElementInLine ? (endIndex, endIndex + 1) : null;
}
public virtual void Draw(TextDrawingRequest request)
{
var fontMetrics = Style.ToFontMetrics();
var glyphOffset = GetGlyphOffset();
var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
var glyphOffsetY = GetGlyphOffset();
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
request.Canvas.DrawText(text, new Position(0, glyphOffset), Style);
var textDrawingCommand = TextShapingResult.PositionText(request.StartIndex, request.EndIndex, Style);
if (Style.BackgroundColor != Colors.Transparent)
request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
if (textDrawingCommand.HasValue)
request.Canvas.DrawText(textDrawingCommand.Value.SkTextBlob, new Position(textDrawingCommand.Value.TextOffsetX, glyphOffsetY), Style);
// draw underline
if ((Style.HasUnderline ?? false) && fontMetrics.UnderlinePosition.HasValue)
{
var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffset;
var underlineOffset = Style.FontPosition == FontPosition.Superscript ? 0 : glyphOffsetY;
DrawLine(fontMetrics.UnderlinePosition.Value + underlineOffset, fontMetrics.UnderlineThickness ?? 1);
}
@@ -145,7 +161,7 @@ namespace QuestPDF.Elements.Text.Items
var strikeoutThickness = fontMetrics.StrikeoutThickness ?? 1;
strikeoutThickness *= Style.FontPosition == FontPosition.Normal ? 1f : 0.625f;
DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffset, strikeoutThickness);
DrawLine(fontMetrics.StrikeoutPosition.Value + glyphOffsetY, strikeoutThickness);
}
void DrawLine(float offset, float thickness)
+1 -1
View File
@@ -22,7 +22,7 @@ namespace QuestPDF.Fluent
Child = container
});
return container;
return container.DebugPointer("Column Item");;
}
}
+3 -3
View File
@@ -12,7 +12,7 @@ namespace QuestPDF.Fluent
{
var container = new Container();
Decoration.Before = container;
return container;
return container.DebugPointer("Decoration Before");
}
public void Before(Action<IContainer> handler)
@@ -24,7 +24,7 @@ namespace QuestPDF.Fluent
{
var container = new Container();
Decoration.Content = container;
return container;
return container.DebugPointer("Decoration Content");
}
public void Content(Action<IContainer> handler)
@@ -36,7 +36,7 @@ namespace QuestPDF.Fluent
{
var container = new Container();
Decoration.After = container;
return container;
return container.DebugPointer("Decoration After");
}
public void After(Action<IContainer> handler)
+1 -1
View File
@@ -22,7 +22,7 @@ namespace QuestPDF.Fluent
};
Row.Items.Add(element);
return element;
return element.DebugPointer("Row Item");
}
[Obsolete("This element has been renamed since version 2022.2. Please use the RelativeItem method.")]
+2 -2
View File
@@ -174,7 +174,7 @@ namespace QuestPDF.Fluent
public TextPageNumberDescriptor PageNumberWithinSection(string locationName)
{
return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageEnd);
return PageNumber(x => x.CurrentPage + 1 - x.GetLocation(locationName)?.PageStart);
}
public TextPageNumberDescriptor TotalPagesWithinSection(string locationName)
@@ -193,7 +193,7 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(text))
return descriptor;
AddItemToLastTextBlock(new TextBlockSectionlLink
AddItemToLastTextBlock(new TextBlockSectionLink
{
Style = style,
Text = text,
+1 -1
View File
@@ -7,7 +7,7 @@ namespace QuestPDF.Helpers
{
public static class Placeholders
{
public static readonly Random Random = new();
public static readonly Random Random = new Random(0);
#region Word Cache
+2 -1
View File
@@ -1,3 +1,4 @@
using QuestPDF.Drawing;
using SkiaSharp;
namespace QuestPDF.Infrastructure
@@ -7,7 +8,7 @@ namespace QuestPDF.Infrastructure
void Translate(Position vector);
void DrawRectangle(Position vector, Size size, string color);
void DrawText(string text, Position position, TextStyle style);
void DrawText(SKTextBlob skTextBlob, Position position, TextStyle style);
void DrawImage(SKImage image, Position position, Size size);
void DrawHyperlink(string url, Size size);
+5 -2
View File
@@ -17,6 +17,9 @@ namespace QuestPDF.Previewer
private HttpClient HttpClient { get; }
public event Action? OnPreviewerStopped;
private const int RequiredPreviewerVersionMajor = 2022;
private const int RequiredPreviewerVersionMinor = 6;
public PreviewerService(int port)
{
@@ -94,12 +97,12 @@ namespace QuestPDF.Previewer
private void CheckVersionCompatibility(Version version)
{
if (version.Major == 2022 && version.Minor == 5)
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 2022.5'");
$"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version {RequiredPreviewerVersionMajor}.{RequiredPreviewerVersionMinor}'");
}
private async Task WaitForConnection()
+4 -5
View File
@@ -1,10 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.5.0</Version>
<Version>2022.6.2</Version>
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
<PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>9</LangVersion>
@@ -18,13 +17,14 @@
<PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Nullable>enable</Nullable>
<TargetFrameworks>net462;netstandard2.0;netcoreapp2.0;netcoreapp3.0;net6.0</TargetFrameworks>
<TargetFramework>net6.0</TargetFramework>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.*" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.4" />
</ItemGroup>
<ItemGroup>
@@ -45,5 +45,4 @@
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>
+9 -4
View File
@@ -1,4 +1,9 @@
- Implemented the DynamicComponent element (useful for most advanced cases, e.g. per-page totals),
- Extend text rendering capabilities by adding subscript and superscript effects,
- Improved table rendering performance,
- Previewer tool stability fixes.
Integrated the text-shaping algorithm. This change significantly improves the Unicode compatibility. Also, it extends support for more advanced languages (e.g. Arabic) that:
1) Combine multiple text characters and display them as a single visual glyph.
2) Are displayed in the right-to-left order.
Improved the exception message when SkiaSharp throws the TypeInitializationException. On some operating systems, SkiaSharp requires additional dependencies installed as nuget packages. This change should help developers determine how to choose and install them correctly.
Fixed: a rare case when the Row.AutoItem() does not correctly calculate the width of its content.
Fixed: the QuestPDF Previewer does not work with content-rich documents.
+1
View File
@@ -0,0 +1 @@
<svg height="180" viewBox="0 0 180 180" width="180" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="32.64" x2="82.77" y1="61.16" y2="85.54"><stop offset=".21" stop-color="#fe2857"/><stop offset="1" stop-color="#293896"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="17.38" x2="82.95" y1="69.86" y2="21.23"><stop offset="0" stop-color="#fe2857"/><stop offset=".01" stop-color="#fe2857"/><stop offset=".86" stop-color="#ff318c"/></linearGradient><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="74.17" x2="160.27" y1="21.58" y2="99.76"><stop offset=".02" stop-color="#ff318c"/><stop offset=".21" stop-color="#fe2857"/><stop offset=".86" stop-color="#fdb60d"/></linearGradient><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="155.46" x2="55.07" y1="89.8" y2="158.9"><stop offset=".01" stop-color="#fdb60d"/><stop offset=".86" stop-color="#fcf84a"/></linearGradient><path d="m81.56 83.71-41.35-35a15 15 0 1 0 -14.47 25.7h.15l.39.12 52.16 15.89a3.53 3.53 0 0 0 1.18.21 3.73 3.73 0 0 0 1.93-6.91z" fill="url(#a)"/><path d="m89.85 25.93a10.89 10.89 0 0 0 -16.85-9.18l-50.5 30.66a15 15 0 1 0 17.9 24l45.27-36.89.36-.3a10.93 10.93 0 0 0 3.82-8.29z" fill="url(#b)"/><path d="m163.29 92-76.62-73.79a10.91 10.91 0 1 0 -14.81 16l.14.12 81.4 68.58a7.36 7.36 0 0 0 12.09-5.65 7.39 7.39 0 0 0 -2.2-5.26z" fill="url(#c)"/><path d="m165.5 97.29a7.35 7.35 0 0 0 -11.67-6l-92.71 45.3a15 15 0 1 0 15.48 25.59l85.73-58.84a7.35 7.35 0 0 0 3.17-6.05z" fill="url(#d)"/><path d="m60 60h60v60h-60z"/><g fill="#fff"><path d="m66.53 108.75h22.5v3.75h-22.5z"/><path d="m65.59 75.47 1.67-1.58a1.88 1.88 0 0 0 1.47.87c.64 0 1.06-.45 1.06-1.32v-5.92h2.58v5.94a3.44 3.44 0 0 1 -.92 2.63 3.52 3.52 0 0 1 -2.57 1 3.84 3.84 0 0 1 -3.29-1.62z"/><path d="m73.53 67.52h7.53v2.19h-5v1.43h4.49v2h-4.45v1.49h5v2.2h-7.6z"/><path d="m84.73 69.79h-2.8v-2.27h8.21v2.27h-2.81v7.09h-2.6z"/><path d="m66.63 80.58h4.42a3.47 3.47 0 0 1 2.55.83 2.09 2.09 0 0 1 .61 1.52 2.18 2.18 0 0 1 -1.45 2.09 2.27 2.27 0 0 1 1.86 2.29c0 1.69-1.31 2.69-3.55 2.69h-4.44zm5 2.89c0-.52-.42-.8-1.18-.8h-1.29v1.64h1.25c.78 0 1.24-.27 1.24-.81zm-.9 2.66h-1.57v1.73h1.62c.8 0 1.24-.31 1.24-.86-.02-.53-.4-.87-1.27-.87z"/><path d="m75.45 80.58h4.15a4.14 4.14 0 0 1 3.05 1 2.92 2.92 0 0 1 .83 2.18 3 3 0 0 1 -1.93 2.89l2.24 3.35h-3l-1.89-2.84h-.87v2.84h-2.6zm4 4.5c.87 0 1.4-.43 1.4-1.12 0-.75-.55-1.13-1.41-1.13h-1.39v2.27z"/><path d="m87.09 80.51h2.5l4 9.44h-2.79l-.67-1.69h-3.63l-.67 1.74h-2.71zm2.28 5.73-1.05-2.65-1.06 2.65z"/><path d="m94 80.55h2.6v9.37h-2.6z"/><path d="m97.56 80.55h2.44l3.37 5v-5h2.57v9.37h-2.27l-3.53-5.14v5.14h-2.58z"/><path d="m106.37 88.53 1.44-1.73a4.86 4.86 0 0 0 3 1.13c.71 0 1.08-.25 1.08-.65 0-.41-.3-.61-1.59-.91-2-.46-3.53-1-3.53-2.93 0-1.74 1.38-3 3.63-3a5.88 5.88 0 0 1 3.85 1.25l-1.25 1.78a4.56 4.56 0 0 0 -2.62-.92c-.63 0-.94.25-.94.6 0 .43.32.62 1.63.91 2.15.47 3.48 1.17 3.48 2.92 0 1.91-1.51 3-3.78 3a6.56 6.56 0 0 1 -4.4-1.45z"/></g><path d="m0 0h180v180h-180z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

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