Compare commits

..

93 Commits

Author SHA1 Message Date
MarcinZiabek 4c679978be Fixes 2023-05-14 23:50:18 +02:00
MarcinZiabek 4fcd955238 Testing project adjustments 2022-12-22 00:33:11 +01:00
MarcinZiabek a9c06db2e0 Improved hierarchy extraction 2022-12-12 00:42:24 +01:00
MarcinZiabek 2f55b8d386 Document hierarchy collection fixes 2022-12-11 00:53:04 +01:00
MarcinZiabek e4a5c471ee Preview: do not collect information about simple containers 2022-12-10 16:45:03 +01:00
MarcinZiabek 4e3d462e8d Simplified document hierarchy collection 2022-12-08 23:53:16 +01:00
MarcinZiabek 423ca330f7 Improved collecting layout information when the InfiniteLayoutException is thrown 2022-12-08 23:48:17 +01:00
MarcinZiabek 57c2a8cb38 Added test project 2022-12-06 22:33:05 +01:00
MarcinZiabek 7c51649415 Fixed incompatible version notification 2022-12-06 22:32:53 +01:00
MarcinZiabek 423c630a47 Previewer ConnectionService refactoring 2022-12-06 20:54:12 +01:00
MarcinZiabek 42c6c75862 Layout debugging fixes, minor code refactoring 2022-12-01 01:11:29 +01:00
MarcinZiabek 559de4520a Improved debug pointers 2022-11-25 21:28:12 +01:00
MarcinZiabek 73e66c293b Previewer: added presence notification 2022-11-25 21:28:03 +01:00
MarcinZiabek 164bdd76d8 Added detector that checks if code is being run via unit test 2022-11-24 22:44:31 +01:00
MarcinZiabek d62cec4f66 Implemented gathering document hierarchy structure 2022-11-20 18:27:34 +01:00
MarcinZiabek baa0feb5e6 Previewer: sending details about layout overflow exception 2022-11-20 15:28:56 +01:00
MarcinZiabek 57e68e293d Communication is working, showing generic error, showing simple preview 2022-11-19 22:26:25 +01:00
MarcinZiabek 320a6e221d Deleted old previewer 2022-11-19 11:48:45 +01:00
MarcinZiabek d92231f5d4 Updated dependencies 2022-11-19 11:47:42 +01:00
MarcinZiabek 1060e4a44a Updated QuestPDF.Previewer version to 2022.11.1 2022-11-11 13:02:43 +01:00
MarcinZiabek 983edd5f76 Added example for Arabic support 2022-11-07 12:44:00 +01:00
Marcin Ziąbek 5315165cf5 Update readme.md 2022-11-07 12:31:41 +01:00
MarcinZiabek ee3d87010e Added example: forcing text direction 2022-11-07 12:22:38 +01:00
MarcinZiabek 3ac91c4971 Improved content-direction examples 2022-11-06 01:18:13 +01:00
MarcinZiabek 8e450934ab 2022.11.0 2022-11-06 00:26:33 +01:00
Marcin Ziąbek bf193eec23 Update readme.md 2022-11-05 19:57:35 +01:00
Marcin Ziąbek c9104540aa Merge pull request #349 from QuestPDF/content-direction
Right-to-left content support
2022-11-05 19:54:41 +01:00
Marcin Ziąbek af60d84abe Merge branch 'main' into content-direction 2022-11-05 19:54:20 +01:00
MarcinZiabek d56117647b 2022.11.0-alpha1 2022-11-01 21:51:45 +01:00
MarcinZiabek 117913693c Fixed build (missing file) 2022-11-01 21:49:58 +01:00
MarcinZiabek 2ba716a5ba Text: fixed alignment when in RTL mode, added support for default alignment 2022-11-01 21:49:46 +01:00
MarcinZiabek be2108f6fe Inlined: fixed alignment when in RTL mode 2022-11-01 21:49:26 +01:00
MarcinZiabek c08bb963a6 Improved tests for alignment for Text and Inlined elements 2022-11-01 21:49:07 +01:00
MarcinZiabek 8ed8897fd1 Improved: the Inlined element should display children aligned to right (by default) when in RTL mode 2022-11-01 21:32:37 +01:00
MarcinZiabek 2a0e91af68 Fixed: the Decoration element is not aligned to right when in RTL mode 2022-11-01 21:32:04 +01:00
MarcinZiabek f647b79a0a Fixed: the Column element incorrectly displays children when in RTL mode 2022-11-01 21:31:46 +01:00
MarcinZiabek b1286bb536 Improved LTR and RTL tests 2022-11-01 21:26:47 +01:00
MarcinZiabek 43d1a55df0 Fixed: positioning elements in RTL mode in the Column element 2022-11-01 12:55:32 +01:00
MarcinZiabek d1cd7b06db Fixed: Page.Size should use points instead of inches (API consistency) 2022-11-01 12:55:13 +01:00
MarcinZiabek 7bce01d6e6 Fixed: table rendering in the RTL mode 2022-10-29 00:40:36 +02:00
MarcinZiabek e3ff2960e1 Fixed: table initialization process 2022-10-29 00:39:07 +02:00
MarcinZiabek 5d9e6f327b Version adjustments 2022-10-28 23:36:15 +02:00
MarcinZiabek b895931eb9 2022.11.0-alpha0 2022-10-28 23:30:23 +02:00
MarcinZiabek 86a35b7cd3 Deprecated the Grid element 2022-10-28 23:25:10 +02:00
MarcinZiabek 65990ebf59 Added the WordWrappingStability test 2022-10-21 12:05:45 +02:00
MarcinZiabek c348106b3f Page fluent: added possibility to select content direction 2022-10-19 21:45:16 +02:00
MarcinZiabek 9d86e9efd3 DynamicImage: simplified implementation 2022-10-19 20:08:41 +02:00
MarcinZiabek 9770575f9a Column: added content direction support 2022-10-19 20:04:31 +02:00
MarcinZiabek e6e6febf8b Inlined: added content direction support 2022-10-19 20:04:24 +02:00
Marcin Ziąbek 5cbacc7ea1 Update readme.md 2022-10-19 19:15:25 +02:00
MarcinZiabek 8214a8429d 2022.9.1 2022-10-15 23:03:25 +02:00
MarcinZiabek 389b8ce304 Inlined: improved behaviour predictability 2022-10-15 22:49:50 +02:00
MarcinZiabek e1f7cff4aa Fix: performance regression of the Table element #320 2022-10-11 23:19:13 +02:00
MarcinZiabek 6801425082 Fix: cells with RowSpan are not always displayed properly #320 2022-10-11 22:39:31 +02:00
MarcinZiabek ec31c9b063 Fix: CheckIfAllTextGlyphsAreAvailable and Fallback break hyperlinks 2022-10-11 10:31:31 +02:00
MarcinZiabek 6c8867e1b3 Added test: ReusingTheSameImageFileShouldBePossible 2022-10-06 23:20:17 +02:00
Marcin Ziąbek 20d86cbfde Update readme.md 2022-10-05 22:49:03 +02:00
MarcinZiabek 16a164e5b8 Example: achieve proportional height division 2022-10-05 01:15:36 +02:00
Marcin Ziąbek 261f087b46 Update readme.md 2022-10-04 12:30:36 +02:00
Marcin Ziąbek c876350b05 Merge pull request #354 from CollinAlpert/fix_broken_links
Fix links in README
2022-10-03 22:10:19 +02:00
Collin Alpert 58f3932241 Update readme.md 2022-10-03 15:28:48 +02:00
MarcinZiabek 350abc2838 Improved RTL, fixed line breaking algorithm 2022-10-02 19:50:50 +02:00
MarcinZiabek 5d4c9a5dd2 Added example for RTL text 2022-10-01 00:18:31 +02:00
MarcinZiabek b31d097b61 Added possibility to force text direction 2022-09-30 20:45:28 +02:00
MarcinZiabek 9f6c091706 RTL: text 2022-09-30 20:33:36 +02:00
MarcinZiabek af2229eabe Fixed unit tests 2022-09-29 23:27:45 +02:00
MarcinZiabek b86946b396 Removed the Initialize method 2022-09-29 22:50:08 +02:00
MarcinZiabek e1ae97d3ca RTL: grid (example) 2022-09-29 21:31:21 +02:00
MarcinZiabek a7ac747776 RTL: dynamic 2022-09-29 21:28:22 +02:00
MarcinZiabek fab0f5bfe1 RTL: constrained 2022-09-29 21:21:25 +02:00
MarcinZiabek f5f90bd235 RTL: aspect ratio (fix) 2022-09-29 21:21:17 +02:00
MarcinZiabek 603dc13d0f RTL: aspect ratio 2022-09-29 21:12:46 +02:00
MarcinZiabek e277c6a8dc RTL: table, decoration, minimal box 2022-09-29 14:46:56 +02:00
MarcinZiabek 5ff9d03f20 Prototype for the RTL mode 2022-09-29 14:14:00 +02:00
MarcinZiabek 9ce9ca85be Added GenerationBenchmark to test async performance 2022-09-22 22:37:15 +02:00
MarcinZiabek 553c8ab719 2022.9.1 2022-09-19 12:05:44 +02:00
Marcin Ziąbek e768e9b06d Update readme.md 2022-09-19 00:58:55 +02:00
Marcin Ziąbek fd913a777c Update readme.md 2022-09-19 00:57:32 +02:00
MarcinZiabek ed9e6daec5 2022.9.0 2022-09-19 00:45:14 +02:00
Marcin Ziąbek ee6249a658 Merge pull request #341 from QuestPDF/2022.9
2022.9 Release
2022-09-19 00:43:29 +02:00
MarcinZiabek b307304f46 Fixed typo 2022-09-18 20:15:41 +02:00
MarcinZiabek f028f82e11 Removed unsued file 2022-09-17 20:42:14 +02:00
MarcinZiabek 719a3385f6 Updated Previewer to 2022.9 2022-09-16 19:52:10 +02:00
MarcinZiabek 2adff11400 2022.9.0-alpha1 2022-09-16 19:46:32 +02:00
MarcinZiabek 34fed6d547 Settings.CheckIfAllTextGlyphsAreAvailable improvements 2022-09-16 16:08:02 +02:00
MarcinZiabek db2df75624 Text example: fixed visuals 2022-09-15 22:19:28 +02:00
MarcinZiabek 6b535752df Fixed formatting page numbers 2022-09-15 22:12:06 +02:00
MarcinZiabek 556f87ff25 Settings renaming 2022-09-15 21:20:25 +02:00
MarcinZiabek fbebbd85eb Fixed build, adapted fallback process for cached TextStyle 2022-09-15 15:09:30 +02:00
MarcinZiabek 4650c2a4ea Merge branch 'font-fallback' into 2022.9
# Conflicts:
#	QuestPDF/Elements/Text/Items/TextBlockSpan.cs
#	QuestPDF/Fluent/TextStyleExtensions.cs
#	QuestPDF/Infrastructure/TextStyle.cs
2022-09-14 23:28:50 +02:00
MarcinZiabek 1ba01a1cf5 Fixed override value 2022-09-09 11:32:05 +02:00
MarcinZiabek d4448437ac Reduced creation of DefaultTextStyle objects 2022-09-08 17:29:52 +02:00
MarcinZiabek bc853f48fc Implemented TextStyleManager to reduce memory usage 2022-09-08 16:49:28 +02:00
106 changed files with 2660 additions and 1735 deletions
@@ -0,0 +1,408 @@
using System;
using System.Linq;
using NUnit.Framework;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples
{
public class ContentDirectionExamples
{
private Action<IContainer> ContentDirectionTemplate(Action<IContainer> content)
{
return container =>
{
container
.MinimalBox()
.ExtendHorizontal()
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.ConstantColumn(1);
columns.RelativeColumn();
});
table.Header(header =>
{
header.Cell().ContentFromLeftToRight().Element(HeaderCell("Left-to-right"));
header.Cell().LineVertical(1).LineColor(Colors.Grey.Medium);
header.Cell().ContentFromRightToLeft().Element(HeaderCell("Right-to-left"));
static Action<IContainer> HeaderCell(string label)
{
return container => container
.BorderColor(Colors.Grey.Medium)
.Background(Colors.Grey.Lighten2)
.PaddingHorizontal(15)
.PaddingVertical(5)
.Text(label)
.FontSize(18)
.SemiBold();
}
});
table.Cell().Element(TestCell).ContentFromLeftToRight().Element(content);
table.Cell().LineVertical(1).LineColor(Colors.Grey.Medium);
table.Cell().Element(TestCell).ContentFromRightToLeft().Element(content);
static IContainer TestCell(IContainer container)
{
return container.Padding(15);
}
});
};
}
[Test]
public void Page()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.EnableDebugging()
.RenderDocument(document =>
{
document.Page(page =>
{
page.Size(PageSizes.A5);
page.Margin(20);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontFamily("Calibri").FontSize(20));
page.ContentFromRightToLeft();
page.Content().Column(column =>
{
column.Spacing(20);
column.Item()
.Text("مثال على الفاتورة") // example invoice
.FontSize(32).FontColor(Colors.Blue.Darken2).SemiBold();
column.Item().Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.ConstantColumn(75);
columns.ConstantColumn(100);
});
table.Cell().Element(HeaderStyle).Text("وصف السلعة"); // item description
table.Cell().Element(HeaderStyle).Text("كمية"); // quantity
table.Cell().Element(HeaderStyle).Text("سعر"); // price
var items = new[]
{
"دورة البرمجة", // programming course
"دورة تصميم الرسومات", // graphics design course
"تحليل وتصميم الخوارزميات", // analysis and design of algorithms
};
foreach (var item in items)
{
var price = Placeholders.Random.NextDouble() * 100;
table.Cell().Text(item);
table.Cell().Text(Placeholders.Random.Next(1, 10));
table.Cell().Text($"USD${price:F2}");
}
static IContainer HeaderStyle(IContainer x) => x.BorderBottom(1).PaddingVertical(5);
});
});
});
});
}
[Test]
public void Column()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(ContentDirectionTemplate(Content));
void Content(IContainer container)
{
container.Column(column =>
{
column.Spacing(5);
column.Item().Height(50).Width(50).Background(Colors.Red.Lighten1);
column.Item().Height(50).Width(100).Background(Colors.Green.Lighten1);
column.Item().Height(50).Width(150).Background(Colors.Blue.Lighten1);
});
}
}
[Test]
public void Row()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(ContentDirectionTemplate(Content));
void Content(IContainer container)
{
container.Row(row =>
{
row.Spacing(5);
row.AutoItem().Height(50).Width(50).Background(Colors.Red.Lighten1);
row.AutoItem().Height(50).Width(50).Background(Colors.Green.Lighten1);
row.AutoItem().Height(50).Width(75).Background(Colors.Blue.Lighten1);
});
}
}
[Test]
public void Table()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(ContentDirectionTemplate(Content));
void Content(IContainer container)
{
container.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
table.Cell().Height(50).Background(Colors.Red.Lighten1);
table.Cell().Height(50).Background(Colors.Green.Lighten1);
table.Cell().Height(50).Background(Colors.Blue.Lighten1);
table.Cell().ColumnSpan(2).Height(50).Background(Colors.Orange.Lighten1);
});
}
}
[Test]
public void Constrained()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(ContentDirectionTemplate(Content));
void Content(IContainer container)
{
container.Width(50).Height(50).Background(Colors.Red.Lighten1);
}
}
[Test]
public void Unconstrained()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(ContentDirectionTemplate(Content));
void Content(IContainer container)
{
container
.Width(100)
.Height(100)
.Background(Colors.Grey.Lighten3)
.AlignCenter()
.AlignMiddle()
.Unconstrained()
.Width(50)
.Height(50)
.Background(Colors.Red.Lighten1);
}
}
[Test]
public void Inlined()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(ContentDirectionTemplate(Content));
void Content(IContainer container)
{
container.Column(column =>
{
column.Spacing(10);
column.Item().Text("Default alignment").FontSize(14).SemiBold();
column.Item().Element(ContentWithAlignment(null));
column.Item().Text("Left alignment").FontSize(14).SemiBold();
column.Item().Element(ContentWithAlignment(InlinedAlignment.Left));
column.Item().Text("Center alignment").FontSize(14).SemiBold();
column.Item().Element(ContentWithAlignment(InlinedAlignment.Center));
column.Item().Text("Right alignment").FontSize(14).SemiBold();
column.Item().Element(ContentWithAlignment(InlinedAlignment.Right));
});
static Action<IContainer> ContentWithAlignment(InlinedAlignment? alignment)
{
return container =>
{
container.Inlined(inlined =>
{
inlined.Spacing(5);
inlined.Alignment(alignment);
inlined.Item().Height(50).Width(50).Background(Colors.Red.Lighten1);
inlined.Item().Height(50).Width(75).Background(Colors.Green.Lighten1);
inlined.Item().Height(50).Width(100).Background(Colors.Blue.Lighten1);
inlined.Item().Height(50).Width(125).Background(Colors.Orange.Lighten1);
});
};
}
}
}
[Test]
public void Text()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(ContentDirectionTemplate(Content));
void Content(IContainer container)
{
container.Column(column =>
{
column.Spacing(10);
column.Item().Text("Default alignment").FontSize(14).SemiBold();
column.Item().Element(ContentWithAlignment(null));
column.Item().Text("Left alignment").FontSize(14).SemiBold();
column.Item().Element(ContentWithAlignment(HorizontalAlignment.Left));
column.Item().Text("Center alignment").FontSize(14).SemiBold();
column.Item().Element(ContentWithAlignment(HorizontalAlignment.Center));
column.Item().Text("Right alignment").FontSize(14).SemiBold();
column.Item().Element(ContentWithAlignment(HorizontalAlignment.Right));
});
static Action<IContainer> ContentWithAlignment(HorizontalAlignment? alignment)
{
return container =>
{
container.Text(text =>
{
text.Alignment = alignment; // internal API
text.Span("Lorem ipsum").Bold().FontColor(Colors.Red.Medium);
text.Element().Width(5);
text.Span(Placeholders.LoremIpsum());
});
};
}
}
}
[Test]
public void Decoration()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(ContentDirectionTemplate(Content));
void Content(IContainer container)
{
container.Decoration(decoration =>
{
decoration.Before().Background(Colors.Green.Lighten1).Padding(5).Text("Before").FontSize(16);
decoration.Content().Background(Colors.Green.Lighten2).Padding(5).Text("Content").FontSize(16);
decoration.After().Background(Colors.Green.Lighten3).Padding(5).Text("After").FontSize(16);
});
}
}
[Test]
public void Dynamic()
{
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProduceImages()
.ShowResults()
.EnableDebugging()
.Render(ContentDirectionTemplate(Content));
void Content(IContainer container)
{
container.Dynamic(new SimpleDynamic());
}
}
class SimpleDynamic : IDynamicComponent<int>
{
public int State { get; set; }
public DynamicComponentComposeResult Compose(DynamicContext context)
{
var content = context.CreateElement(container =>
{
container.Row(row =>
{
row.ConstantItem(50).Background(Colors.Red.Lighten2).Height(50);
row.ConstantItem(75).Background(Colors.Green.Lighten2).Height(50);
row.ConstantItem(100).Background(Colors.Blue.Lighten2).Height(50);
});
});
return new DynamicComponentComposeResult
{
Content = content,
HasMoreContent = false
};
}
}
}
}
+146
View File
@@ -0,0 +1,146 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using QuestPDF.Elements;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
namespace QuestPDF.Examples
{
public class ProcessRunningTime
{
public TimeSpan FluentTime { get; set; }
public TimeSpan GenerationTime { get; set; }
public float Size { get; set; }
}
public class GenerationBenchmark
{
public const int TestSize = 4096;
[Test]
public void BenchmarkAsync()
{
RunTest(() => Enumerable
.Range(0, TestSize)
.AsParallel() // difference
.Select(GenerateAndCollect)
.ToList());
}
[Test]
public void BenchmarkSync()
{
RunTest(() => Enumerable
.Range(0, TestSize)
.Select(GenerateAndCollect)
.ToList());
}
public void RunTest(Func<IEnumerable<ProcessRunningTime>> handler)
{
var totalFluentTime = TimeSpan.Zero;
var totalGenerationTime = TimeSpan.Zero;
var stopWatch = new Stopwatch();
stopWatch.Start();
var results = handler();
stopWatch.Stop();
foreach (var result in results)
{
totalFluentTime += result.FluentTime;
totalGenerationTime += result.GenerationTime;
}
Console.WriteLine($"Fluent: {totalFluentTime:g}");
Console.WriteLine($"Generation: {totalGenerationTime:g}");
Console.WriteLine($"Total: {stopWatch.Elapsed:g}");
}
static ProcessRunningTime GenerateAndCollect(int attemptNumber)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
var container = new Container();
container
.Padding(10)
.MinimalBox()
.Border(1)
.Column(column =>
{
column.Item().Text($"Attempts {attemptNumber}");
const int numberOfRows = 100;
const int numberOfColumns = 10;
for (var y = 0; y < numberOfRows; y++)
{
column.Item().Row(row =>
{
for (var x = 0; x < numberOfColumns; x++)
{
row.RelativeItem()
.Background(Colors.Red.Lighten5)
.Padding(3)
.Background(Colors.Red.Lighten4)
.Padding(3)
.Background(Colors.Red.Lighten3)
.Padding(3)
.Background(Colors.Red.Lighten2)
.Padding(3)
.Background(Colors.Red.Lighten1)
.Padding(3)
.Background(Colors.Red.Medium)
.Padding(3)
.Background(Colors.Red.Darken1)
.Padding(3)
.Background(Colors.Red.Darken2)
.Padding(3)
.Background(Colors.Red.Darken3)
.Padding(3)
.Background(Colors.Red.Darken4)
.Height(3);
}
});
}
});
var fluentTime = stopwatch.Elapsed;
stopwatch.Reset();
stopwatch.Start();
var size = Document
.Create(x => x.Page(page => page.Content().Element(container)))
.GeneratePdf()
.Length;
var generationTime = stopwatch.Elapsed;
return new ProcessRunningTime
{
FluentTime = fluentTime,
GenerationTime = generationTime,
Size = size
};
}
}
}
+55 -1
View File
@@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using NUnit.Framework;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Examples.Engine;
@@ -34,6 +35,21 @@ namespace QuestPDF.Examples
});
}
[Test]
public void DynamicImage()
{
RenderingTest
.Create()
.PageSize(450, 350)
.ProducePdf()
.ShowResults()
.Render(page =>
{
page.Padding(25)
.Image(Placeholders.Image);
});
}
[Test]
public void Exception()
{
@@ -47,5 +63,43 @@ namespace QuestPDF.Examples
.Render(page => page.Image("non_existent.png"));
});
}
[Test]
public void ReusingTheSameImageFileShouldBePossible()
{
var fileName = Path.GetTempFileName() + ".jpg";
try
{
var image = Placeholders.Image(300, 100);
using var file = File.Create(fileName);
file.Write(image);
file.Dispose();
RenderingTest
.Create()
.ProducePdf()
.PageSize(PageSizes.A4)
.ShowResults()
.Render(container =>
{
container
.Padding(20)
.Column(column =>
{
column.Spacing(20);
column.Item().Image(fileName);
column.Item().Image(fileName);
column.Item().Image(fileName);
});
});
}
finally
{
File.Delete(fileName);
}
}
}
}
+5 -5
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
@@ -9,10 +9,10 @@
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="microcharts" Version="0.9.5.9" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
<PackageReference Include="Svg.Skia" Version="0.5.10" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="Svg.Skia" Version="0.5.18" />
</ItemGroup>
<ItemGroup>
+36
View File
@@ -105,5 +105,41 @@ namespace QuestPDF.Examples
.Row(row => { });
});
}
[Test]
public void RowElementForRelativeHeightDivision()
{
RenderingTest
.Create()
.ProduceImages()
.ShowResults()
.MaxPages(100)
.PageSize(250, 400)
.Render(container =>
{
container
.Padding(25)
.AlignLeft()
.RotateRight()
.Row(row =>
{
row.Spacing(20);
row.RelativeItem(1).Element(Content);
row.RelativeItem(2).Element(Content);
row.RelativeItem(3).Element(Content);
void Content(IContainer container)
{
container
.RotateLeft()
.Border(1)
.Background(Placeholders.BackgroundColor())
.Padding(5)
.Text(Placeholders.Label());
}
});
});
}
}
}
+1 -1
View File
@@ -111,7 +111,7 @@ namespace QuestPDF.Examples
{
page.Margin(50);
page.Content().Column(column =>
page.Content().PaddingVertical(10).Column(column =>
{
column.Item().Element(Title);
column.Item().PageBreak();
+133 -4
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Text;
using NUnit.Framework;
@@ -604,7 +604,7 @@ namespace QuestPDF.Examples
{
RenderingTest
.Create()
.PageSize(500, 100)
.PageSize(250, 100)
.ProduceImages()
.ShowResults()
@@ -614,9 +614,9 @@ namespace QuestPDF.Examples
.Padding(25)
.MinimalBox()
.Background(Colors.Grey.Lighten2)
.Text("ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا")
.Text("خوارزمية ترتيب")
.FontFamily(Fonts.Calibri)
.FontSize(20);
.FontSize(30);
});
}
@@ -657,5 +657,134 @@ namespace QuestPDF.Examples
});
});
}
[Test]
public void WordWrappingStability()
{
// instruction: check if any characters repeat when performing the word-wrapping algorithm
RenderingTest
.Create()
.PageSize(PageSizes.A4)
.ProducePdf()
.ShowResults()
.Render(container =>
{
var text = "Lorem ipsum dolor sit amet consectetuer";
container
.Padding(20)
.Column(column =>
{
column.Spacing(10);
foreach (var width in Enumerable.Range(25, 200))
{
column
.Item()
.MaxWidth(width)
.Background(Colors.Grey.Lighten3)
.Text(text);
}
});
});
}
[Test]
public void AdvancedLanguagesSupport()
{
RenderingTest
.Create()
.PageSize(new PageSize(400, 400))
.ProduceImages()
.ShowResults()
.Render(container =>
{
var text = "في المعلوماتية أو الرياضيات، خوارزمية الترتيب هي خوارزمية تمكن من تنظيم مجموعة عناصر حسب ترتيب محدد.";
container
.Padding(20)
.ContentFromRightToLeft()
.Text(text)
.FontFamily(Fonts.Calibri)
.FontSize(22);
});
}
[Test]
public void WordWrappingWhenRightToLeft()
{
RenderingTest
.Create()
.PageSize(new PageSize(1000, 500))
.ProducePdf()
.ShowResults()
.Render(container =>
{
var text = "في المعلوماتية أو الرياضيات، خوارزمية الترتيب هي خوارزمية تمكن من تنظيم مجموعة عناصر حسب ترتيب محدد.";
container
.Padding(25)
.ContentFromRightToLeft()
.Column(column =>
{
column.Spacing(20);
foreach (var size in new[] { 36, 34, 32, 30, 15 })
{
column
.Item()
.ShowEntire()
.MaxWidth(size * 25)
.Background(Colors.Grey.Lighten3)
.MinimalBox()
.Background(Colors.Grey.Lighten2)
.Text(text)
.FontSize(20)
.FontFamily("Segoe UI");
}
});
});
}
[Test]
public void ForcingTextDirection()
{
RenderingTest
.Create()
.PageSize(new PageSize(1000, 500))
.ProduceImages()
.ShowResults()
.Render(container =>
{
container
.Padding(10)
.DefaultTextStyle(x => x.FontSize(24).FontFamily("Calibri"))
.Column(column =>
{
column.Spacing(10);
var word = "الجوريتم";
var definition = "algorithm in Arabic";
var text = $"{word} - {definition}";
// text direction is automatically detected using the first word
column.Item().Text(text);
// it is possible to force specific content direction
column.Item().Text(text).DirectionFromLeftToRight();
column.Item().Text(text).DirectionFromRightToLeft();
// to combine text in various content directions, split it into segments
column.Item().Text(text =>
{
text.Span(word);
text.Span(" - ");
text.Span(definition);
});
});
});
}
}
}
-62
View File
@@ -1,62 +0,0 @@
using QuestPDF.Fluent;
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();
//
// return;
Document
.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hot Reload!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Darken2);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
x.Item().Table(t =>
{
t.ColumnsDefinition(c =>
{
c.RelativeColumn();
c.RelativeColumn(3);
});
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Visual Studio");
t.Cell().Border(1).Padding(5).Text("Start in debug mode with 'Hot Reload on Save' enabled.");
t.Cell().Border(1).Background(Colors.Grey.Lighten3).Padding(5).Text("Command line");
t.Cell().Border(1).Padding(5).Text("Run 'dotnet watch'.");
});
x.Item().Text("Modify this line and the preview should show your changes instantly.");
});
page.Footer()
.AlignCenter()
.Text(x =>
{
x.Span("Page ");
x.CurrentPageNumber();
});
});
})
.ShowInPreviewer();
@@ -1,15 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\QuestPDF.ReportSample\QuestPDF.ReportSample.csproj" />
<ProjectReference Include="..\QuestPDF\QuestPDF.csproj" />
</ItemGroup>
</Project>
@@ -1,79 +0,0 @@
using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SkiaSharp;
namespace QuestPDF.Previewer;
class CommunicationService
{
public static CommunicationService Instance { get; } = new ();
public event Action<ICollection<PreviewPage>>? OnDocumentRefreshed;
private WebApplication? Application { get; set; }
private readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true
};
private CommunicationService()
{
}
public Task Start(int port)
{
var builder = WebApplication.CreateBuilder();
builder.Services.AddLogging(x => x.ClearProviders());
builder.WebHost.UseKestrel(options => options.Limits.MaxRequestBodySize = null);
Application = builder.Build();
Application.MapGet("ping", HandlePing);
Application.MapGet("version", HandleVersion);
Application.MapPost("update/preview", HandleUpdatePreview);
return Application.RunAsync($"http://localhost:{port}/");
}
public async Task Stop()
{
await Application.StopAsync();
await Application.DisposeAsync();
}
private async Task<IResult> HandlePing()
{
return OnDocumentRefreshed == null
? Results.StatusCode(StatusCodes.Status503ServiceUnavailable)
: Results.Ok();
}
private async Task<IResult> HandleVersion()
{
return Results.Json(GetType().Assembly.GetName().Version);
}
private async Task<IResult> HandleUpdatePreview(HttpRequest request)
{
var command = JsonSerializer.Deserialize<DocumentSnapshot>(request.Form["command"], JsonSerializerOptions);
var pages = command
.Pages
.Select(page =>
{
using var stream = request.Form.Files[page.Id].OpenReadStream();
var picture = SKPicture.Deserialize(stream);
return new PreviewPage(picture, page.Width, page.Height);
})
.ToList();
Task.Run(() => OnDocumentRefreshed(pages));
return Results.Ok();
}
}
-20
View File
@@ -1,20 +0,0 @@
using SkiaSharp;
namespace QuestPDF.Previewer;
class Helpers
{
public static void GeneratePdfFromDocumentSnapshots(string filePath, ICollection<PreviewPage> pages)
{
using var stream = File.Create(filePath);
using var document = SKDocument.CreatePdf(stream);
foreach (var page in pages)
{
using var canvas = document.BeginPage(page.Width, page.Height);
canvas.DrawPicture(page.Picture);
document.EndPage();
}
}
}
-202
View File
@@ -1,202 +0,0 @@
using Avalonia;
using Avalonia.Platform;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Skia;
using SkiaSharp;
namespace QuestPDF.Previewer;
class InteractiveCanvas : ICustomDrawOperation
{
public Rect Bounds { get; set; }
public ICollection<PreviewPage> Pages { get; set; }
private float Width => (float)Bounds.Width;
private float Height => (float)Bounds.Height;
public float Scale { get; private set; } = 1;
public float TranslateX { get; set; }
public float TranslateY { get; set; }
private const float MinScale = 0.1f;
private const float MaxScale = 10f;
private const float PageSpacing = 25f;
private const float SafeZone = 25f;
public float TotalPagesHeight => Pages.Sum(x => x.Height) + (Pages.Count - 1) * PageSpacing;
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 ScrollPercentY
{
get
{
return TranslateY / MaxTranslateY;
}
set
{
TranslateY = value * MaxTranslateY;
}
}
public float ScrollViewportSizeY
{
get
{
var viewPortSize = Height / Scale / TotalHeight;
return Math.Clamp(viewPortSize, 0, 1);
}
}
#region transformations
private void LimitScale()
{
Scale = Math.Max(Scale, MinScale);
Scale = Math.Min(Scale, MaxScale);
}
private void LimitTranslate()
{
if (TotalPagesHeight > Height / Scale)
{
TranslateY = Math.Min(TranslateY, MaxTranslateY);
TranslateY = Math.Max(TranslateY, 0);
}
else
{
TranslateY = (TotalPagesHeight - Height / Scale) / 2;
}
if (Width / Scale < MaxWidth)
{
var maxTranslateX = (Width / 2 - SafeZone) / Scale - MaxWidth / 2;
TranslateX = Math.Min(TranslateX, -maxTranslateX);
TranslateX = Math.Max(TranslateX, maxTranslateX);
}
else
{
TranslateX = 0;
}
}
public void TranslateWithCurrentScale(float x, float y)
{
TranslateX += x / Scale;
TranslateY += y / Scale;
LimitTranslate();
}
public void ZoomToPoint(float x, float y, float factor)
{
var oldScale = Scale;
Scale *= factor;
LimitScale();
TranslateX -= x / Scale - x / oldScale;
TranslateY -= y / Scale - y / oldScale;
LimitTranslate();
}
#endregion
#region rendering
public void Render(IDrawingContextImpl context)
{
if (Pages.Count <= 0)
return;
LimitScale();
LimitTranslate();
var canvas = (context as ISkiaDrawingContextImpl)?.SkCanvas;
if (canvas == null)
throw new InvalidOperationException($"Context needs to be ISkiaDrawingContextImpl but got {nameof(context)}");
var originalMatrix = canvas.TotalMatrix;
canvas.Translate(Width / 2, 0);
canvas.Scale(Scale);
canvas.Translate(TranslateX, -TranslateY + SafeZone / Scale);
foreach (var page in Pages)
{
canvas.Translate(-page.Width / 2f, 0);
DrawBlankPage(canvas, page.Width, page.Height);
canvas.DrawPicture(page.Picture);
canvas.Translate(page.Width / 2f, page.Height + PageSpacing);
}
canvas.SetMatrix(originalMatrix);
DrawInnerGradient(canvas);
}
public void Dispose() { }
public bool Equals(ICustomDrawOperation? other) => false;
public bool HitTest(Point p) => true;
#endregion
#region blank page
private static SKPaint BlankPagePaint = new SKPaint
{
Color = SKColors.White
};
private static SKPaint BlankPageShadowPaint = new SKPaint
{
ImageFilter = SKImageFilter.CreateBlendMode(
SKBlendMode.Overlay,
SKImageFilter.CreateDropShadowOnly(0, 6, 6, 6, SKColors.Black.WithAlpha(64)),
SKImageFilter.CreateDropShadowOnly(0, 10, 14, 14, SKColors.Black.WithAlpha(32)))
};
private void DrawBlankPage(SKCanvas canvas, float width, float height)
{
canvas.DrawRect(0, 0, width, height, BlankPageShadowPaint);
canvas.DrawRect(0, 0, width, height, BlankPagePaint);
}
#endregion
#region inner viewport gradient
private const int InnerGradientSize = (int)SafeZone;
private static readonly SKColor InnerGradientColor = SKColor.Parse("#666");
private void DrawInnerGradient(SKCanvas canvas)
{
// gamma correction
var colors = Enumerable
.Range(0, InnerGradientSize)
.Select(x => 1f - x / (float) InnerGradientSize)
.Select(x => Math.Pow(x, 2f))
.Select(x => (byte)(x * 255))
.Select(x => InnerGradientColor.WithAlpha(x))
.ToArray();
using var fogPaint = new SKPaint
{
Shader = SKShader.CreateLinearGradient(
new SKPoint(0, 0),
new SKPoint(0, InnerGradientSize),
colors,
SKShaderTileMode.Clamp)
};
canvas.DrawRect(0, 0, Width, InnerGradientSize, fogPaint);
}
#endregion
}
-6
View File
@@ -1,6 +0,0 @@
using SkiaSharp;
namespace QuestPDF.Previewer
{
record PreviewPage(SKPicture Picture, float Width, float Height);
}
-8
View File
@@ -1,8 +0,0 @@
<Application x:Class="QuestPDF.Previewer.PreviewerApp"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="QuestPDF Document Preview">
<Application.Styles>
<FluentTheme Mode="Dark" />
</Application.Styles>
</Application>
-27
View File
@@ -1,27 +0,0 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
namespace QuestPDF.Previewer
{
internal class PreviewerApp : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new PreviewerWindow()
{
DataContext = new PreviewerWindowViewModel()
};
}
base.OnFrameworkInitializationCompleted();
}
}
}
-118
View File
@@ -1,118 +0,0 @@
using System.Collections.ObjectModel;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
namespace QuestPDF.Previewer
{
class PreviewerControl : Control
{
private InteractiveCanvas InteractiveCanvas { get; set; } = new ();
public static readonly StyledProperty<ObservableCollection<PreviewPage>> PagesProperty =
AvaloniaProperty.Register<PreviewerControl, ObservableCollection<PreviewPage>>(nameof(Pages));
public ObservableCollection<PreviewPage>? Pages
{
get => GetValue(PagesProperty);
set => SetValue(PagesProperty, value);
}
public static readonly StyledProperty<float> CurrentScrollProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(CurrentScroll));
public float CurrentScroll
{
get => GetValue(CurrentScrollProperty);
set => SetValue(CurrentScrollProperty, value);
}
public static readonly StyledProperty<float> ScrollViewportSizeProperty = AvaloniaProperty.Register<PreviewerControl, float>(nameof(ScrollViewportSize));
public float ScrollViewportSize
{
get => GetValue(ScrollViewportSizeProperty);
set => SetValue(ScrollViewportSizeProperty, value);
}
public PreviewerControl()
{
PagesProperty.Changed.Subscribe(x =>
{
InteractiveCanvas.Pages = x.NewValue.Value;
InvalidateVisual();
});
CurrentScrollProperty.Changed.Subscribe(x =>
{
InteractiveCanvas.ScrollPercentY = x.NewValue.Value;
InvalidateVisual();
});
ClipToBounds = true;
}
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
base.OnPointerWheelChanged(e);
if ((e.KeyModifiers & KeyModifiers.Control) != 0)
{
var scaleFactor = 1 + e.Delta.Y / 10f;
var point = new Point(Bounds.Center.X, Bounds.Top) - e.GetPosition(this);
InteractiveCanvas.ZoomToPoint((float)point.X, -(float)point.Y, (float)scaleFactor);
}
if (e.KeyModifiers == KeyModifiers.None)
{
var translation = (float)e.Delta.Y * 25;
InteractiveCanvas.TranslateWithCurrentScale(0, -translation);
}
InvalidateVisual();
}
private bool IsMousePressed { get; set; }
private Vector MousePosition { get; set; }
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (IsMousePressed)
{
var currentPosition = e.GetPosition(this);
var translation = currentPosition - MousePosition;
InteractiveCanvas.TranslateWithCurrentScale((float)translation.X, -(float)translation.Y);
InvalidateVisual();
}
MousePosition = e.GetPosition(this);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
IsMousePressed = true;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
IsMousePressed = false;
}
public override void Render(DrawingContext context)
{
CurrentScroll = InteractiveCanvas.ScrollPercentY;
ScrollViewportSize = InteractiveCanvas.ScrollViewportSizeY;
InteractiveCanvas.Bounds = new Rect(0, 0, Bounds.Width, Bounds.Height);
context.Custom(InteractiveCanvas);
base.Render(context);
}
}
}
@@ -1,18 +0,0 @@
using SkiaSharp;
namespace QuestPDF.Previewer;
internal class DocumentSnapshot
{
public ICollection<PageSnapshot> Pages { get; set; }
public class PageSnapshot
{
public string Id { get; set; }
public float Width { get; set; }
public float Height { get; set; }
public SKPicture Picture { get; set; }
}
}
-97
View File
@@ -1,97 +0,0 @@
<Window 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.PreviewerWindow"
x:DataType="previewer:PreviewerWindowViewModel"
x:CompileBindings="True"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="true"
ExtendClientAreaTitleBarHeightHint="-1"
Background="#666"
Icon="/Resources/Logo.png"
UseLayoutRounding="True"
Title="QuestPDF Document Preview">
<Window.Styles>
<Style Selector="Button.actions">
<Setter Property="VerticalAlignment" Value="Bottom"/>
<Setter Property="HorizontalAlignment" Value="Left"/>
<Setter Property="Padding" Value="10"/>
<Setter Property="CornerRadius" Value="100"/>
<Setter Property="Background" Value="#888"/>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#999"/>
</Style>
</Window.Styles>
<Panel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<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"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
CurrentScroll="{Binding CurrentScroll, Mode=TwoWay}"
ScrollViewportSize="{Binding ScrollViewportSize, Mode=OneWayToSource}"
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>
</Grid>
</Panel>
</Window>
@@ -1,23 +0,0 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace QuestPDF.Previewer
{
class PreviewerWindow : Window
{
public PreviewerWindow()
{
InitializeComponent();
}
protected override void OnClosed(EventArgs e)
{
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}
@@ -1,89 +0,0 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using ReactiveUI;
using Unit = System.Reactive.Unit;
using Avalonia.Threading;
namespace QuestPDF.Previewer
{
internal class PreviewerWindowViewModel : ReactiveObject
{
private ObservableCollection<PreviewPage> _pages = new();
public ObservableCollection<PreviewPage> Pages
{
get => _pages;
set => this.RaiseAndSetIfChanged(ref _pages, value);
}
private float _currentScroll;
public float CurrentScroll
{
get => _currentScroll;
set => this.RaiseAndSetIfChanged(ref _currentScroll, value);
}
private float _scrollViewportSize;
public float ScrollViewportSize
{
get => _scrollViewportSize;
set
{
this.RaiseAndSetIfChanged(ref _scrollViewportSize, value);
VerticalScrollbarVisible = value < 1;
}
}
private bool _verticalScrollbarVisible;
public bool VerticalScrollbarVisible
{
get => _verticalScrollbarVisible;
private set => Dispatcher.UIThread.Post(() => this.RaiseAndSetIfChanged(ref _verticalScrollbarVisible, value));
}
public ReactiveCommand<Unit, Unit> ShowPdfCommand { get; }
public ReactiveCommand<Unit, Unit> ShowDocumentationCommand { get; }
public ReactiveCommand<Unit, Unit> SponsorProjectCommand { get; }
public PreviewerWindowViewModel()
{
CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview;
ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/api-reference/index.html"));
SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
}
private void ShowPdf()
{
var filePath = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.pdf");
Helpers.GeneratePdfFromDocumentSnapshots(filePath, Pages);
OpenLink(filePath);
}
private void OpenLink(string path)
{
using var openBrowserProcess = new Process
{
StartInfo = new()
{
UseShellExecute = true,
FileName = path
}
};
openBrowserProcess.Start();
}
private void HandleUpdatePreview(ICollection<PreviewPage> pages)
{
var oldPages = Pages;
Pages.Clear();
Pages = new ObservableCollection<PreviewPage>(pages);
foreach (var page in oldPages)
page.Picture.Dispose();
}
}
}
-38
View File
@@ -1,38 +0,0 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.ReactiveUI;
using QuestPDF.Previewer;
var applicationPort = GetCommunicationPort();
CommunicationService.Instance.Start(applicationPort);
if(Application.Current?.ApplicationLifetime is ClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new PreviewerWindow()
{
DataContext = new PreviewerWindowViewModel()
};
desktop.MainWindow.Show();
desktop.Start(Array.Empty<string>());
return;
}
AppBuilder
.Configure(() => new PreviewerApp())
.UsePlatformDetect()
.UseReactiveUI()
.StartWithClassicDesktopLifetime(Array.Empty<string>());
static int GetCommunicationPort()
{
const int defaultApplicationPort = 12500;
var arguments = Environment.GetCommandLineArgs();
if (arguments.Length < 2)
return defaultApplicationPort;
return int.TryParse(arguments[1], out var port) ? port : defaultApplicationPort;
}
@@ -1,55 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.8.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>
<PackageReleaseNotes>Initial release.</PackageReleaseNotes>
<LangVersion>10</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageIcon>Logo.png</PackageIcon>
<PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl>
<PackageProjectUrl>https://www.questpdf.com/</PackageProjectUrl>
<RepositoryUrl>https://github.com/QuestPDF/library.git</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Copyright>Marcin Ziąbek, QuestPDF contributors</Copyright>
<PackageTags>pdf report file export generate generation tool create creation render portable document format quest html library converter open source free standard core previewer</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<OutputType>exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Resources\Logo.png" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Logo.png" />
<None Include="Resources\Logo.png">
<Pack>true</Pack>
<Visible>false</Visible>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Avalonia" Version="0.10.10" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.10" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.10" />
<PackageReference Include="Avalonia.Markup.Xaml.Loader" Version="0.10.10" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.10" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="2.80.4" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="SkiaSharp" Version="2.80.4" />
</ItemGroup>
</Project>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

-23
View File
@@ -1,23 +0,0 @@
Install nuget locally (in directory where nupkg file is located)
```
dotnet tool install --global --add-source . QuestPDF.Previewer --global
```
Run on default port
```
questpdf-previewer
```
Run on custom port
```
questpdf-previewer 12500
```
Remove nuget locally
```
dotnet tool uninstall QuestPDF.Previewer --global
```
@@ -83,6 +83,8 @@ namespace QuestPDF.ReportSample.Layouts
.Range(0, model.PhotoCount)
.ToList()
.ForEach(x => grid.Item().AspectRatio(4 / 3f).Component<ImagePlaceholder>());
//grid.Item().AspectRatio(4 / 3f).Width(100000).Component<ImagePlaceholder>();
});
}
}
@@ -68,9 +68,11 @@ namespace QuestPDF.ReportSample.Layouts
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");
text.Span(")").Style(lengthStyle);
text.TotalPagesWithinSection(locationName).Style(lengthStyle).Format(x =>
{
var formatted = x == 1 ? "1 page long" : $"{x} pages long";
return $" ({formatted})";
});
});
});
}
+2 -2
View File
@@ -51,7 +51,7 @@ namespace QuestPDF.ReportSample
Content = documentContainer.Compose();
PageContext = new PageContext();
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content);
var sw = new Stopwatch();
sw.Start();
@@ -69,7 +69,7 @@ namespace QuestPDF.ReportSample
[Benchmark]
public void GenerationTest()
{
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content);
}
}
}
@@ -9,10 +9,10 @@
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<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.4" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
</ItemGroup>
<ItemGroup>
+9 -6
View File
@@ -6,6 +6,7 @@ using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
using QuestPDF.ReportSample.Layouts;
namespace QuestPDF.ReportSample
@@ -24,11 +25,13 @@ namespace QuestPDF.ReportSample
[Test]
public void GenerateAndShowPdf()
{
Report.ShowInPreviewer();
//ImagePlaceholder.Solid = true;
var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf");
Report.GeneratePdf(path);
Process.Start("explorer.exe", path);
//
// var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf");
// Report.GeneratePdf(path);
// Process.Start("explorer.exe", path);
}
[Test]
@@ -50,8 +53,8 @@ namespace QuestPDF.ReportSample
var pageContext = new PageContext();
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content);
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content);
}
}
}
+40
View File
@@ -153,7 +153,9 @@ namespace QuestPDF.UnitTests
Ratio = 2f
})
.DrawElement(new Size(500, 200))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(400, 200))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
@@ -168,7 +170,45 @@ namespace QuestPDF.UnitTests
Ratio = 2f
})
.DrawElement(new Size(400, 300))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(400, 200))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
[Test]
public void DrawChild_PerWidth_RightToLeft()
{
TestPlan
.For(x => new AspectRatio
{
Child = x.CreateChild(),
Option = AspectRatioOption.FitArea,
Ratio = 2f,
ContentDirection = ContentDirection.RightToLeft
})
.DrawElement(new Size(500, 200))
.ExpectCanvasTranslate(100, 0)
.ExpectChildDraw(new Size(400, 200))
.ExpectCanvasTranslate(-100, 0)
.CheckDrawResult();
}
[Test]
public void DrawChild_PerHeight_RightToLeft()
{
TestPlan
.For(x => new AspectRatio
{
Child = x.CreateChild(),
Option = AspectRatioOption.FitArea,
Ratio = 2f,
ContentDirection = ContentDirection.RightToLeft
})
.DrawElement(new Size(400, 300))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(400, 200))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
}
+38
View File
@@ -35,7 +35,9 @@ namespace QuestPDF.UnitTests
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.PartialRender(200, 100))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(200, 100))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
@@ -49,7 +51,43 @@ namespace QuestPDF.UnitTests
})
.MeasureElement(new Size(500, 400))
.ExpectChildMeasure(expectedInput: new Size(500, 400), returns: SpacePlan.FullRender(300, 200))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(300, 200))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
[Test]
public void Measure_PartialRender_RightToLeft()
{
TestPlan
.For(x => new MinimalBox
{
Child = x.CreateChild(),
ContentDirection = ContentDirection.RightToLeft
})
.MeasureElement(new Size(400, 300))
.ExpectChildMeasure(expectedInput: new Size(400, 300), returns: SpacePlan.PartialRender(200, 100))
.ExpectCanvasTranslate(200, 0)
.ExpectChildDraw(new Size(200, 100))
.ExpectCanvasTranslate(-200, 0)
.CheckDrawResult();
}
[Test]
public void Measure_FullRender_RightToLeft()
{
TestPlan
.For(x => new MinimalBox
{
Child = x.CreateChild(),
ContentDirection = ContentDirection.RightToLeft
})
.MeasureElement(new Size(500, 400))
.ExpectChildMeasure(expectedInput: new Size(500, 400), returns: SpacePlan.FullRender(350, 200))
.ExpectCanvasTranslate(150, 0)
.ExpectChildDraw(new Size(350, 200))
.ExpectCanvasTranslate(-150, 0)
.CheckDrawResult();
}
}
+54
View File
@@ -0,0 +1,54 @@
using System;
using NUnit.Framework;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
namespace QuestPDF.UnitTests
{
public class PreviewerTests
{
[Test]
public void SendError()
{
Document
.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.PageColor(Colors.White);
page.DefaultTextStyle(x => x.FontSize(20));
page.Header()
.Text("Hello PDF!")
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
page.Content()
.PaddingVertical(1, Unit.Centimetre)
.Column(x =>
{
x.Spacing(20);
x.Item().Text(Placeholders.LoremIpsum());
x.Item().Image(Placeholders.Image(200, 100));
});
page.Footer()
.AlignCenter()
.Text(x =>
{
//throw new Exception("Dupa!");
x.Span("Page ");
x.CurrentPageNumber();
});
});
})
.ShowInPreviewer();
}
}
}
+6 -6
View File
@@ -1,16 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.7.0" />
<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.4" />
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
</ItemGroup>
<ItemGroup>
+6 -6
View File
@@ -195,7 +195,7 @@ namespace QuestPDF.UnitTests.TestEngine
public TestPlan CheckMeasureResult(SpacePlan expected)
{
Element.VisitChildren(x => x?.Initialize(null, Canvas));
Element.InjectDependencies(null, Canvas);
var actual = Element.Measure(OperationInput);
@@ -210,7 +210,7 @@ namespace QuestPDF.UnitTests.TestEngine
public TestPlan CheckDrawResult()
{
Element.VisitChildren(x => x?.Initialize(null, Canvas));
Element.InjectDependencies(null, Canvas);
Element.Draw(OperationInput);
return this;
}
@@ -253,10 +253,10 @@ namespace QuestPDF.UnitTests.TestEngine
availableSpace ??= new Size(400, 300);
var canvas = new FreeCanvas();
value.VisitChildren(x => x.Initialize(null, canvas));
value.InjectDependencies(null, canvas);
var valueMeasure = value.Measure(availableSpace.Value);
expected.VisitChildren(x => x.Initialize(null, canvas));
expected.InjectDependencies(null, canvas);
var expectedMeasure = expected.Measure(availableSpace.Value);
valueMeasure.Should().BeEquivalentTo(expectedMeasure);
@@ -267,11 +267,11 @@ namespace QuestPDF.UnitTests.TestEngine
availableSpace ??= new Size(400, 300);
var valueCanvas = new OperationRecordingCanvas();
value.VisitChildren(x => x.Initialize(null, valueCanvas));
value.InjectDependencies(null, valueCanvas);
value.Draw(availableSpace.Value);
var expectedCanvas = new OperationRecordingCanvas();
expected.VisitChildren(x => x.Initialize(null, expectedCanvas));
expected.InjectDependencies(null, expectedCanvas);
expected.Draw(availableSpace.Value);
valueCanvas.Operations.Should().BeEquivalentTo(expectedCanvas.Operations);
+38
View File
@@ -77,7 +77,9 @@ namespace QuestPDF.UnitTests
})
.DrawElement(new Size(900, 800))
.ExpectChildMeasure(Size.Max, SpacePlan.PartialRender(1200, 1600))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(1200, 1600))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
@@ -91,7 +93,43 @@ namespace QuestPDF.UnitTests
})
.DrawElement(new Size(900, 800))
.ExpectChildMeasure(Size.Max, SpacePlan.FullRender(1600, 1000))
.ExpectCanvasTranslate(0, 0)
.ExpectChildDraw(new Size(1600, 1000))
.ExpectCanvasTranslate(0, 0)
.CheckDrawResult();
}
[Test]
public void Draw_WhenChildPartiallyRenders_RightToLeft()
{
TestPlan
.For(x => new Unconstrained
{
Child = x.CreateChild(),
ContentDirection = ContentDirection.RightToLeft
})
.DrawElement(new Size(900, 800))
.ExpectChildMeasure(Size.Max, SpacePlan.PartialRender(1200, 1600))
.ExpectCanvasTranslate(-1200, 0)
.ExpectChildDraw(new Size(1200, 1600))
.ExpectCanvasTranslate(1200, 0)
.CheckDrawResult();
}
[Test]
public void Draw_WhenChildFullyRenders_RightToLeft()
{
TestPlan
.For(x => new Unconstrained
{
Child = x.CreateChild(),
ContentDirection = ContentDirection.RightToLeft
})
.DrawElement(new Size(900, 800))
.ExpectChildMeasure(Size.Max, SpacePlan.FullRender(1600, 1000))
.ExpectCanvasTranslate(-1600, 0)
.ExpectChildDraw(new Size(1600, 1000))
.ExpectCanvasTranslate(1600, 0)
.CheckDrawResult();
}
+5 -11
View File
@@ -8,14 +8,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.UnitTests", "Quest
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Examples", "QuestPDF.Examples\QuestPDF.Examples.csproj", "{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer.Examples", "QuestPDF.Previewer.Examples\QuestPDF.Previewer.Examples.csproj", "{CA413A39-038F-4A9F-B56F-0E5413B6B158}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configuration", "Configuration", "{73123649-216A-47B4-BFB2-DADF13FBE75D}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuestPDF.Previewer", "QuestPDF.Previewer\QuestPDF.Previewer.csproj", "{B2FF6003-3A45-4A78-A85D-B86C7F01D054}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SimpleApp", "SimpleApp\SimpleApp.csproj", "{B361D452-80A8-493A-BEA6-AB268E99153E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -39,13 +37,9 @@ Global
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BD0A2B4-2DC1-47BA-9724-C158320D9CAE}.Release|Any CPU.Build.0 = Release|Any CPU
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CA413A39-038F-4A9F-B56F-0E5413B6B158}.Release|Any CPU.Build.0 = Release|Any CPU
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2FF6003-3A45-4A78-A85D-B86C7F01D054}.Release|Any CPU.Build.0 = Release|Any CPU
{B361D452-80A8-493A-BEA6-AB268E99153E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B361D452-80A8-493A-BEA6-AB268E99153E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B361D452-80A8-493A-BEA6-AB268E99153E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B361D452-80A8-493A-BEA6-AB268E99153E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+2 -1
View File
@@ -16,13 +16,14 @@ namespace QuestPDF.Drawing
var container = new Container();
container
.DebugPointer("Document")
.Column(column =>
{
Pages
.SelectMany(x => new List<Action>()
{
() => column.Item().PageBreak(),
() => column.Item().Component(x)
() => column.Item().DebugPointer("Page").Component(x)
})
.Skip(1)
.ToList()
+67 -28
View File
@@ -10,6 +10,7 @@ using QuestPDF.Elements.Text.Items;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer.Inspection;
namespace QuestPDF.Drawing
{
@@ -51,35 +52,48 @@ namespace QuestPDF.Drawing
return canvas.Images;
}
internal static ICollection<PreviewerPicture> GeneratePreviewerPictures(IDocument document)
internal static DocumentPreviewResult GeneratePreviewerPictures(IDocument document)
{
var canvas = new SkiaPictureCanvas();
RenderDocument(canvas, document);
return canvas.Pictures;
InspectionElement documentHierarchy = null;
RenderDocument(canvas, document, true, x => documentHierarchy = x);
return new DocumentPreviewResult
{
PageSnapshots = canvas.Pictures.ToList(),
DocumentHierarchy = documentHierarchy
};
}
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document)
internal static void RenderDocument<TCanvas>(TCanvas canvas, IDocument document, bool applyInspection = false, Action<InspectionElement> inspectionResultHandler = null)
where TCanvas : ICanvas, IRenderingCanvas
{
var container = new DocumentContainer();
document.Compose(container);
var content = container.Compose();
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
ApplyContentDirection(content, ContentDirection.LeftToRight);
if (Settings.EnableCaching)
ApplyCaching(content);
var pageContext = new PageContext();
RenderPass(pageContext, new FreeCanvas(), content, debuggingState);
RenderPass(pageContext, canvas, content, debuggingState);
RenderPass(pageContext, new FreeCanvas(), content);
if (applyInspection)
ApplyInspection(content);
RenderPass(pageContext, canvas, content);
if (applyInspection)
inspectionResultHandler(content.ExtractDocumentHierarchy());
}
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState)
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content)
where TCanvas : ICanvas, IRenderingCanvas
{
content.VisitChildren(x => x?.Initialize(pageContext, canvas));
InjectDependencies(content, pageContext, canvas);
content.VisitChildren(x => (x as IStateResettable)?.ResetState());
canvas.BeginDocument();
@@ -89,7 +103,6 @@ namespace QuestPDF.Drawing
while(true)
{
pageContext.SetPageNumber(currentPage);
debuggingState?.Reset();
var spacePlan = content.Measure(Size.Max);
@@ -134,12 +147,29 @@ namespace QuestPDF.Drawing
$"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.";
var elementTrace = debuggingState?.BuildTrace() ?? "Debug trace is available only in the DEBUG mode.";
content.RemoveExistingProxies();
content.RemoveExistingContainers();
content.ApplyLayoutDebugging();
content.Measure(Size.Max);
var layoutTrace = content.CollectLayoutErrorTrace();
throw new DocumentLayoutException(message, elementTrace);
throw new DocumentLayoutException(message, layoutTrace);
}
}
internal static void InjectDependencies(this Element content, IPageContext pageContext, ICanvas canvas)
{
content.VisitChildren(x =>
{
if (x == null)
return;
x.PageContext = pageContext;
x.Canvas = canvas;
});
}
private static void ApplyCaching(Container content)
{
content.VisitChildren(x =>
@@ -149,16 +179,30 @@ namespace QuestPDF.Drawing
});
}
private static DebuggingState ApplyDebugging(Container content)
private static void ApplyInspection(Container content)
{
var debuggingState = new DebuggingState();
content.VisitChildren(x =>
{
x.CreateProxy(y => new DebuggingProxy(debuggingState, y));
x.CreateProxy(y => y is ElementProxy or Container ? y : new InspectionProxy(y));
});
}
internal static void ApplyContentDirection(this Element? content, ContentDirection direction)
{
if (content == null)
return;
return debuggingState;
if (content is ContentDirectionSetter contentDirectionSetter)
{
ApplyContentDirection(contentDirectionSetter.Child, contentDirectionSetter.ContentDirection);
return;
}
if (content is IContentDirectionAware contentDirectionAware)
contentDirectionAware.ContentDirection = direction;
foreach (var child in content.GetChildren())
ApplyContentDirection(child, direction);
}
internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
@@ -172,7 +216,7 @@ namespace QuestPDF.Drawing
{
if (textBlockItem is TextBlockSpan textSpan)
{
textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle);
textSpan.Style = textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle);
}
else if (textBlockItem is TextBlockElement textElement)
{
@@ -184,18 +228,13 @@ namespace QuestPDF.Drawing
}
if (content is DynamicHost dynamicHost)
dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
var targetTextStyle = documentDefaultTextStyle;
dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
if (content is DefaultTextStyle defaultTextStyleElement)
{
defaultTextStyleElement.TextStyle.ApplyParentStyle(documentDefaultTextStyle);
targetTextStyle = defaultTextStyleElement.TextStyle;
}
documentDefaultTextStyle = defaultTextStyleElement.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
foreach (var child in content.GetChildren())
ApplyDefaultTextStyle(child, targetTextStyle);
ApplyDefaultTextStyle(child, documentDefaultTextStyle);
}
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ namespace QuestPDF.Drawing
set => Settings.EnableCaching = value;
}
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableDebugging static property.")]
[Obsolete("The new implementation for debugging layout issues does not introduce additional performance overhead. Therefore, this setting is no longer used since the 2023.1 release. Please remove this setter from your code.")]
public bool ApplyDebugging
{
get => Settings.EnableDebugging;
@@ -1,12 +1,13 @@
using System;
using QuestPDF.Previewer;
namespace QuestPDF.Drawing.Exceptions
{
public class DocumentLayoutException : Exception
{
public string? ElementTrace { get; }
internal LayoutErrorTrace? ElementTrace { get; }
internal DocumentLayoutException(string message, string? elementTrace = null) : base(message)
internal DocumentLayoutException(string message, LayoutErrorTrace? elementTrace = null) : base(message)
{
ElementTrace = elementTrace;
}
+13 -13
View File
@@ -14,13 +14,13 @@ namespace QuestPDF.Drawing
{
public static class FontManager
{
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = 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 readonly ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
private static readonly ConcurrentDictionary<TextStyle, SKFontMetrics> FontMetrics = new();
private static readonly ConcurrentDictionary<TextStyle, SKPaint> FontPaints = new();
private static readonly ConcurrentDictionary<string, SKPaint> ColorPaints = new();
private static readonly ConcurrentDictionary<TextStyle, Font> ShaperFonts = new();
private static readonly ConcurrentDictionary<TextStyle, SKFont> Fonts = new();
private static readonly ConcurrentDictionary<TextStyle, TextShaper> TextShapers = new();
static FontManager()
{
@@ -110,7 +110,7 @@ namespace QuestPDF.Drawing
internal static SKPaint ToPaint(this TextStyle style)
{
return FontPaints.GetOrAdd(style.PaintKey, key => Convert(style));
return FontPaints.GetOrAdd(style, Convert);
static SKPaint Convert(TextStyle style)
{
@@ -172,14 +172,14 @@ namespace QuestPDF.Drawing
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
{
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
return FontMetrics.GetOrAdd(style, key => key.NormalPosition().ToPaint().FontMetrics);
}
internal static Font ToShaperFont(this TextStyle style)
{
return ShaperFonts.GetOrAdd(style.PaintKey, _ =>
return ShaperFonts.GetOrAdd(style, key =>
{
var typeface = style.ToPaint().Typeface;
var typeface = key.ToPaint().Typeface;
using var harfBuzzBlob = typeface.OpenStream(out var ttcIndex).ToHarfBuzzBlob();
@@ -200,12 +200,12 @@ namespace QuestPDF.Drawing
internal static TextShaper ToTextShaper(this TextStyle style)
{
return TextShapers.GetOrAdd(style.PaintKey, _ => new TextShaper(style));
return TextShapers.GetOrAdd(style, key => new TextShaper(key));
}
internal static SKFont ToFont(this TextStyle style)
{
return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont());
return Fonts.GetOrAdd(style, key => key.ToPaint().ToFont());
}
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ namespace QuestPDF.Drawing.Proxy
base.Draw(availableSpace);
}
private bool IsClose(float x, float y)
private static bool IsClose(float x, float y)
{
return Math.Abs(x - y) < Size.Epsilon;
}
-14
View File
@@ -1,14 +0,0 @@
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
public class DebugStackItem
{
public IElement Element { get; internal set; }
public Size AvailableSpace { get; internal set; }
public SpacePlan SpacePlan { get; internal set; }
public ICollection<DebugStackItem> Stack { get; internal set; } = new List<DebugStackItem>();
}
}
+18 -8
View File
@@ -1,24 +1,34 @@
using QuestPDF.Infrastructure;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class DebuggingProxy : ElementProxy
{
private DebuggingState DebuggingState { get; }
internal List<MeasureDetails> Measurements { get; } = new();
public DebuggingProxy(DebuggingState debuggingState, Element child)
public DebuggingProxy(Element child)
{
DebuggingState = debuggingState;
Child = child;
}
internal override SpacePlan Measure(Size availableSpace)
{
DebuggingState.RegisterMeasure(Child, availableSpace);
var spacePlan = base.Measure(availableSpace);
DebuggingState.RegisterMeasureResult(Child, spacePlan);
var spacePlan = Child.Measure(availableSpace);
Measurements.Add(new MeasureDetails(availableSpace, spacePlan));
return spacePlan;
}
}
internal struct MeasureDetails
{
public Size AvailableSpace { get; }
public SpacePlan SpacePlan { get;}
public MeasureDetails(Size availableSpace, SpacePlan spacePlan)
{
AvailableSpace = availableSpace;
SpacePlan = spacePlan;
}
}
}
-130
View File
@@ -1,130 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing.Proxy
{
internal class DebuggingState
{
private DebugStackItem Root { get; set; }
private Stack<DebugStackItem> Stack { get; set; }
public DebuggingState()
{
Reset();
}
public void Reset()
{
Root = null;
Stack = new Stack<DebugStackItem>();
}
public void RegisterMeasure(IElement element, Size availableSpace)
{
if (element.GetType() == typeof(Container))
return;
var item = new DebugStackItem
{
Element = element,
AvailableSpace = availableSpace
};
Root ??= item;
if (Stack.Any())
Stack.Peek().Stack.Add(item);
Stack.Push(item);
}
public void RegisterMeasureResult(IElement element, SpacePlan spacePlan)
{
if (element.GetType() == typeof(Container))
return;
var item = Stack.Pop();
if (item.Element != element)
throw new Exception();
item.SpacePlan = spacePlan;
}
public string BuildTrace()
{
var builder = new StringBuilder();
var nestingLevel = 0;
Traverse(Root);
return builder.ToString();
void Traverse(DebugStackItem item)
{
var indent = new string(' ', nestingLevel * 4);
var title = item.Element.GetType().Name;
if (item.Element is DebugPointer debugPointer)
{
title = debugPointer.Target;
title += debugPointer.Highlight ? " 🌟" : string.Empty;
}
if (item.SpacePlan.Type != SpacePlanType.FullRender)
title = "🔥 " + title;
builder.AppendLine(indent + title);
builder.AppendLine(indent + new string('-', title.Length));
builder.AppendLine(indent + "Available space: " + item.AvailableSpace);
builder.AppendLine(indent + "Requested space: " + item.SpacePlan);
foreach (var configuration in GetElementConfiguration(item.Element))
builder.AppendLine(indent + configuration);
builder.AppendLine();
nestingLevel++;
item.Stack.ToList().ForEach(Traverse);
nestingLevel--;
}
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)
{
const int maxLength = 100;
var text = value?.ToString() ?? "-";
if (text.Length < maxLength)
return text;
return text.AsSpan(0, maxLength).ToString() + "...";
}
}
}
}
}
+45
View File
@@ -0,0 +1,45 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
namespace QuestPDF.Drawing.Proxy;
internal static class Helpers
{
public static IReadOnlyCollection<DocumentElementProperty> GetElementConfiguration(this 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))
.Select(x => new DocumentElementProperty
{
Label = x.Property,
Value = FormatValue(x.Value)
})
.ToList();
string FormatValue(object value)
{
const int maxLength = 100;
var text = value?.ToString() ?? "-";
if (text.Length < maxLength)
return text;
return text.AsSpan(0, maxLength).ToString() + "...";
}
}
}
+36
View File
@@ -0,0 +1,36 @@
using System.Collections;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer.Inspection;
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,74 @@
using System.Linq;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer;
namespace QuestPDF.Drawing.Proxy
{
internal static class LayoutErrorAnalyzer
{
public static void ApplyLayoutDebugging(this Container content)
{
content.VisitChildren(x =>
{
x.CreateProxy(y => y is ElementProxy ? y : new DebuggingProxy(y));
});
}
public static void RemoveExistingProxies(this Container content)
{
content.VisitChildren(x =>
{
x.CreateProxy(y => y is ElementProxy proxy ? proxy.Child : y);
});
}
public static void RemoveExistingContainers(this Container content)
{
content.VisitChildren(x =>
{
x.CreateProxy(y => y is Container container ? container.Child : y);
});
}
public static LayoutErrorTrace CollectLayoutErrorTrace(this Container content)
{
var proxies = content.ExtractProxyOfType<DebuggingProxy>();
return Map(proxies);
LayoutErrorTrace Map(TreeNode<DebuggingProxy> proxy)
{
var element = proxy.Value.Child;
return new LayoutErrorTrace
{
ElementType = element.GetType().Name,
IsSingleChildContainer = element is ContainerElement,
ElementProperties = element.GetElementConfiguration().ToList(),
Measurements = proxy
.Value
.Measurements
.Select(x => new LayoutErrorMeasurement
{
AvailableSpace = new QuestPDF.Previewer.Size()
{
Width = x.AvailableSpace.Width,
Height = x.AvailableSpace.Height
},
SpacePlan = new QuestPDF.Previewer.SpacePlan()
{
Width = x.SpacePlan.Width,
Height = x.SpacePlan.Height,
Type = x.SpacePlan.Type
}
})
.ToList(),
Children = proxy.Children.Select(Map).ToList()
};
}
}
}
}
-8
View File
@@ -1,8 +0,0 @@
namespace QuestPDF.Drawing
{
internal struct TextMeasurement
{
public int LineIndex { get; set; }
public float FragmentWidth { get; set; }
}
}
+72 -38
View File
@@ -29,6 +29,12 @@ namespace QuestPDF.Drawing
PopulateBufferWithText(buffer, text);
buffer.GuessSegmentProperties();
if (TextStyle.Direction == TextDirection.LeftToRight)
buffer.Direction = Direction.LeftToRight;
if (TextStyle.Direction == TextDirection.RightToLeft)
buffer.Direction = Direction.RightToLeft;
ShaperFont.Shape(buffer);
var length = buffer.Length;
@@ -55,11 +61,8 @@ namespace QuestPDF.Drawing
xOffset += glyphPositions[i].XAdvance * scaleX;
yOffset += glyphPositions[i].YAdvance * scaleY;
}
if (Settings.CheckIfAllTextGlyphsAreAvailableInSpecifiedFont)
CheckIfAllGlyphsAreAvailable(glyphs, text);
return new TextShapingResult(glyphs);
return new TextShapingResult(buffer.Direction, glyphs);
}
void PopulateBufferWithText(Buffer buffer, string text)
@@ -78,20 +81,6 @@ namespace QuestPDF.Drawing
else
throw new NotSupportedException("TextEncoding of type GlyphId is not supported.");
}
void CheckIfAllGlyphsAreAvailable(ShapedGlyph[] glyphs, string originalText)
{
var containsMissingGlyphs = glyphs.Any(x => x.Codepoint == default);
if (!containsMissingGlyphs)
return;
throw new ArgumentException(
$"Detected missing font glyphs while rendering text. " +
$"This means that the document contains text with characters not present in the assigned font. " +
$"Such characters are replaced by placeholders, usually visible as empty rectangles. " +
$"Font family used: {TextStyle.FontFamily}. Issue detected in text: '{originalText}'");
}
}
internal struct ShapedGlyph
@@ -109,29 +98,65 @@ namespace QuestPDF.Drawing
internal class TextShapingResult
{
public ShapedGlyph[] Glyphs { get; }
private Direction Direction { get; }
private ShapedGlyph[] Glyphs { get; }
public TextShapingResult(ShapedGlyph[] glyphs)
public int Length => Glyphs.Length;
public ShapedGlyph this[int index] =>
Direction == Direction.LeftToRight
? Glyphs[index]
: Glyphs[Glyphs.Length - 1 - index];
public TextShapingResult(Direction direction, ShapedGlyph[] glyphs)
{
Direction = direction;
Glyphs = glyphs;
}
public int BreakText(int startIndex, float maxWidth)
{
var index = startIndex;
maxWidth += Glyphs[startIndex].Position.X;
while (index < Glyphs.Length)
return Direction switch
{
var glyph = Glyphs[index];
if (glyph.Position.X + glyph.Width > maxWidth + Size.Epsilon)
break;
index++;
}
Direction.LeftToRight => BreakTextLeftToRight(),
Direction.RightToLeft => BreakTextRightToLeft(),
_ => throw new ArgumentOutOfRangeException()
};
return index - 1;
int BreakTextLeftToRight()
{
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;
}
int BreakTextRightToLeft()
{
var index = startIndex;
var startOffset = this[startIndex].Position.X + this[startIndex].Width;
while (index < Glyphs.Length)
{
if (startOffset - this[index].Position.X > maxWidth + Size.Epsilon)
break;
index++;
}
return index - 1;
}
}
public float MeasureWidth(int startIndex, int endIndex)
@@ -139,10 +164,15 @@ namespace QuestPDF.Drawing
if (Glyphs.Length == 0)
return 0;
var start = Glyphs[startIndex];
var end = Glyphs[endIndex];
var start = this[startIndex];
var end = this[endIndex];
return end.Position.X - start.Position.X + end.Width;
return Direction switch
{
Direction.LeftToRight => end.Position.X - start.Position.X + end.Width,
Direction.RightToLeft => start.Position.X - end.Position.X + start.Width,
_ => throw new NotSupportedException()
};
}
public DrawTextCommand? PositionText(int startIndex, int endIndex, TextStyle textStyle)
@@ -163,14 +193,18 @@ namespace QuestPDF.Drawing
{
var runIndex = sourceIndex - startIndex;
glyphSpan[runIndex] = Glyphs[sourceIndex].Codepoint;
positionSpan[runIndex] = Glyphs[sourceIndex].Position;
glyphSpan[runIndex] = this[sourceIndex].Codepoint;
positionSpan[runIndex] = this[sourceIndex].Position;
}
var firstVisualCharacterIndex = Direction == Direction.LeftToRight
? startIndex
: endIndex;
return new DrawTextCommand
{
SkTextBlob = skTextBlobBuilder.Build(),
TextOffsetX = -Glyphs[startIndex].Position.X
TextOffsetX = -this[firstVisualCharacterIndex].Position.X
};
}
}
+10 -1
View File
@@ -4,8 +4,10 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class AspectRatio : ContainerElement, ICacheable
internal class AspectRatio : ContainerElement, ICacheable, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
public float Ratio { get; set; } = 1;
public AspectRatioOption Option { get; set; } = AspectRatioOption.FitWidth;
@@ -42,7 +44,14 @@ namespace QuestPDF.Elements
return;
var size = GetTargetSize(availableSpace);
var offset = ContentDirection == ContentDirection.LeftToRight
? Position.Zero
: new Position(availableSpace.Width - size.Width, 0);
Canvas.Translate(offset);
base.Draw(size);
Canvas.Translate(offset.Reverse());
}
private Size GetTargetSize(Size availableSpace)
+2 -2
View File
@@ -6,7 +6,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class ColumnItem : Container
internal class ColumnItem : ContainerElement
{
public bool IsRendered { get; set; }
}
@@ -74,7 +74,7 @@ namespace QuestPDF.Elements
command.ColumnItem.IsRendered = true;
var targetSize = new Size(availableSpace.Width, command.Size.Height);
Canvas.Translate(command.Offset);
command.ColumnItem.Draw(targetSize);
Canvas.Translate(command.Offset.Reverse());
+11 -3
View File
@@ -5,8 +5,10 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class Constrained : ContainerElement, ICacheable
internal class Constrained : ContainerElement, ICacheable, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
public float? MinWidth { get; set; }
public float? MaxWidth { get; set; }
@@ -45,11 +47,17 @@ namespace QuestPDF.Elements
internal override void Draw(Size availableSpace)
{
var available = new Size(
var size = new Size(
Min(MaxWidth, availableSpace.Width),
Min(MaxHeight, availableSpace.Height));
Child?.Draw(available);
var offset = ContentDirection == ContentDirection.LeftToRight
? Position.Zero
: new Position(availableSpace.Width - size.Width, 0);
Canvas.Translate(offset);
base.Draw(size);
Canvas.Translate(offset.Reverse());
}
private static float Min(float? x, float y)
@@ -0,0 +1,9 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class ContentDirectionSetter : ContainerElement
{
public ContentDirection ContentDirection { get; set; }
}
}
+2 -1
View File
@@ -1,8 +1,9 @@
using System.ComponentModel;
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class DebugPointer : Container
internal class DebugPointer : ContainerElement
{
public string Target { get; set; }
public bool Highlight { get; set; }
+9 -3
View File
@@ -14,8 +14,10 @@ namespace QuestPDF.Elements
public Position Offset { get; set; }
}
internal class Decoration : Element, ICacheable
internal class Decoration : Element, ICacheable, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
internal Element Before { get; set; } = new Empty();
internal Element Content { get; set; } = new Empty();
internal Element After { get; set; } = new Empty();
@@ -64,9 +66,13 @@ namespace QuestPDF.Elements
{
var elementSize = new Size(width, command.Measurement.Height);
Canvas.Translate(command.Offset);
var offset = ContentDirection == ContentDirection.LeftToRight
? command.Offset
: new Position(availableSpace.Width - width, command.Offset.Y);
Canvas.Translate(offset);
command.Element.Draw(elementSize);
Canvas.Translate(command.Offset.Reverse());
Canvas.Translate(offset.Reverse());
}
}
+14 -7
View File
@@ -6,13 +6,14 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class DynamicHost : Element, IStateResettable
internal class DynamicHost : Element, IStateResettable, IContentDirectionAware
{
private DynamicComponentProxy Child { get; }
private object InitialComponentState { get; set; }
internal TextStyle TextStyle { get; } = new();
internal TextStyle TextStyle { get; set; } = TextStyle.Default;
public ContentDirection ContentDirection { get; set; }
public DynamicHost(DynamicComponentProxy child)
{
Child = child;
@@ -51,12 +52,14 @@ namespace QuestPDF.Elements
var context = new DynamicContext
{
PageNumber = PageContext.CurrentPage,
TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd,
PageContext = PageContext,
Canvas = Canvas,
TextStyle = TextStyle,
TextStyle = TextStyle,
ContentDirection = ContentDirection,
PageNumber = PageContext.CurrentPage,
TotalPages = PageContext.GetLocation(Infrastructure.PageContext.DocumentLocation).PageEnd,
AvailableSize = availableSize
};
@@ -73,7 +76,9 @@ namespace QuestPDF.Elements
{
internal IPageContext PageContext { get; set; }
internal ICanvas Canvas { get; set; }
internal TextStyle TextStyle { get; set; }
internal ContentDirection ContentDirection { get; set; }
public int PageNumber { get; internal set; }
public int TotalPages { get; internal set; }
@@ -85,7 +90,9 @@ namespace QuestPDF.Elements
content(container);
container.ApplyDefaultTextStyle(TextStyle);
container.VisitChildren(x => x?.Initialize(PageContext, Canvas));
container.ApplyContentDirection(ContentDirection);
container.InjectDependencies(PageContext, Canvas);
container.VisitChildren(x => (x as IStateResettable)?.ResetState());
container.Size = container.Measure(Size.Max);
+2 -7
View File
@@ -24,13 +24,8 @@ namespace QuestPDF.Elements
if (imageData == null)
return;
var imageElement = new Image
{
InternalImage = SKImage.FromEncodedData(imageData)
};
imageElement.Initialize(PageContext, Canvas);
imageElement.Draw(availableSpace);
using var image = SKImage.FromEncodedData(imageData);
Canvas.DrawImage(image, Position.Zero, availableSpace);
}
}
}
+35 -14
View File
@@ -6,7 +6,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class InlinedElement : Container
internal class InlinedElement : ContainerElement
{
}
@@ -26,15 +26,17 @@ namespace QuestPDF.Elements
public SpacePlan Size { get; set; }
}
internal class Inlined : Element, IStateResettable
internal class Inlined : Element, IStateResettable, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
public List<InlinedElement> Elements { get; internal set; } = new List<InlinedElement>();
private Queue<InlinedElement> ChildrenQueue { get; set; }
internal float VerticalSpacing { get; set; }
internal float HorizontalSpacing { get; set; }
internal InlinedAlignment ElementsAlignment { get; set; }
internal InlinedAlignment? ElementsAlignment { get; set; }
internal VerticalAlignment BaselineAlignment { get; set; }
public void ResetState()
@@ -47,8 +49,15 @@ namespace QuestPDF.Elements
return Elements;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Elements.ForEach(x => x.Child = create(x.Child));
}
internal override SpacePlan Measure(Size availableSpace)
{
SetDefaultAlignment();
if (!ChildrenQueue.Any())
return SpacePlan.FullRender(Size.Zero);
@@ -81,6 +90,8 @@ namespace QuestPDF.Elements
internal override void Draw(Size availableSpace)
{
SetDefaultAlignment();
var lines = Compose(availableSpace);
var topOffset = 0f;
@@ -103,7 +114,6 @@ namespace QuestPDF.Elements
var elementOffset = ElementOffset();
var leftOffset = AlignOffset();
Canvas.Translate(new Position(leftOffset, 0));
foreach (var measurement in lineMeasurements)
{
@@ -113,15 +123,16 @@ namespace QuestPDF.Elements
if (size.Height == 0)
size = new Size(size.Width, lineSize.Height);
Canvas.Translate(new Position(0, baselineOffset));
var offset = ContentDirection == ContentDirection.LeftToRight
? new Position(leftOffset, baselineOffset)
: new Position(availableSpace.Width - size.Width - leftOffset, baselineOffset);
Canvas.Translate(offset);
measurement.Element.Draw(size);
Canvas.Translate(new Position(0, -baselineOffset));
Canvas.Translate(offset.Reverse());
leftOffset += size.Width + elementOffset;
Canvas.Translate(new Position(size.Width + elementOffset, 0));
}
Canvas.Translate(new Position(-leftOffset, 0));
float ElementOffset()
{
@@ -140,15 +151,15 @@ namespace QuestPDF.Elements
float AlignOffset()
{
var difference = availableSpace.Width - lineSize.Width - (lineMeasurements.Count - 1) * HorizontalSpacing;
var emptySpace = availableSpace.Width - lineSize.Width - (lineMeasurements.Count - 1) * HorizontalSpacing;
return ElementsAlignment switch
{
InlinedAlignment.Left => 0,
InlinedAlignment.Left => ContentDirection == ContentDirection.LeftToRight ? 0 : emptySpace,
InlinedAlignment.Justify => 0,
InlinedAlignment.SpaceAround => elementOffset,
InlinedAlignment.Center => difference / 2,
InlinedAlignment.Right => difference,
InlinedAlignment.Center => emptySpace / 2,
InlinedAlignment.Right => ContentDirection == ContentDirection.LeftToRight ? emptySpace : 0,
_ => 0
};
}
@@ -167,6 +178,16 @@ namespace QuestPDF.Elements
}
}
void SetDefaultAlignment()
{
if (ElementsAlignment.HasValue)
return;
ElementsAlignment = ContentDirection == ContentDirection.LeftToRight
? InlinedAlignment.Left
: InlinedAlignment.Right;
}
Size GetLineSize(ICollection<InlinedMeasurement> measurements)
{
var width = measurements.Sum(x => x.Size.Width);
@@ -212,7 +233,7 @@ namespace QuestPDF.Elements
break;
var element = queue.Peek();
var size = element.Measure(Size.Max);
var size = element.Measure(new Size(availableSize.Width, Size.Max.Height));
if (size.Type == SpacePlanType.Wrap)
break;
+5
View File
@@ -20,6 +20,11 @@ namespace QuestPDF.Elements
return Children;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Children.ForEach(x => x.Child = create(x.Child));
}
internal override SpacePlan Measure(Size availableSpace)
{
return Children
+9 -1
View File
@@ -3,8 +3,10 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class MinimalBox : ContainerElement
internal class MinimalBox : ContainerElement, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
internal override void Draw(Size availableSpace)
{
var targetSize = base.Measure(availableSpace);
@@ -12,7 +14,13 @@ namespace QuestPDF.Elements
if (targetSize.Type == SpacePlanType.Wrap)
return;
var translate = ContentDirection == ContentDirection.RightToLeft
? new Position(availableSpace.Width - targetSize.Width, 0)
: Position.Zero;
Canvas.Translate(translate);
base.Draw(targetSize);
Canvas.Translate(translate.Reverse());
}
}
}
+5 -2
View File
@@ -8,7 +8,8 @@ namespace QuestPDF.Elements
{
internal class Page : IComponent
{
public TextStyle DefaultTextStyle { get; set; } = new TextStyle();
public ContentDirection ContentDirection { get; set; }
public TextStyle DefaultTextStyle { get; set; } = TextStyle.Default;
public Size MinSize { get; set; } = PageSizes.A4;
public Size MaxSize { get; set; } = PageSizes.A4;
@@ -30,6 +31,7 @@ namespace QuestPDF.Elements
public void Compose(IContainer container)
{
container
.ContentDirection(ContentDirection)
.Background(BackgroundColor)
.Layers(layers =>
{
@@ -40,6 +42,7 @@ namespace QuestPDF.Elements
layers
.PrimaryLayer()
.DebugPointer("Page primary layer")
.MinWidth(MinSize.Width)
.MinHeight(MinSize.Height)
@@ -62,9 +65,9 @@ namespace QuestPDF.Elements
decoration
.Content()
.DebugPointer("Page content")
.Element(x => IsClose(MinSize.Width, MaxSize.Width) ? x.ExtendHorizontal() : x)
.Element(x => IsClose(MinSize.Height, MaxSize.Height) ? x.ExtendVertical() : x)
.DebugPointer("Page content")
.Element(Content);
decoration
+1
View File
@@ -25,6 +25,7 @@ namespace QuestPDF.Elements
{
if (string.IsNullOrWhiteSpace(Text))
x.MaxHeight(32).Image(ImageData, ImageScaling.FitArea);
else
x.Text(Text).FontSize(14);
});
+10 -4
View File
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements
Relative
}
internal class RowItem : Container
internal class RowItem : ContainerElement
{
public bool IsRendered { get; set; }
public float Width { get; set; }
@@ -31,8 +31,10 @@ namespace QuestPDF.Elements
public Position Offset { get; set; }
}
internal class Row : Element, ICacheable, IStateResettable
internal class Row : Element, ICacheable, IStateResettable, IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
internal List<RowItem> Items { get; } = new();
internal float Spacing { get; set; }
@@ -91,9 +93,13 @@ namespace QuestPDF.Elements
if (command.Measurement.Type == SpacePlanType.Wrap)
continue;
Canvas.Translate(command.Offset);
var offset = ContentDirection == ContentDirection.LeftToRight
? command.Offset
: new Position(availableSpace.Width - command.Offset.X - command.Size.Width, 0);
Canvas.Translate(offset);
command.RowItem.Draw(command.Size);
Canvas.Translate(command.Offset.Reverse());
Canvas.Translate(offset.Reverse());
}
if (Items.All(x => x.IsRendered))
+40 -20
View File
@@ -9,12 +9,15 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
internal class Table : Element, IStateResettable
internal class Table : Element, IStateResettable, IContentDirectionAware
{
public List<TableColumnDefinition> Columns { get; set; } = new List<TableColumnDefinition>();
public List<TableCell> Cells { get; set; } = new List<TableCell>();
public ContentDirection ContentDirection { get; set; }
public List<TableColumnDefinition> Columns { get; set; } = new();
public List<TableCell> Cells { get; set; } = new();
public bool ExtendLastCellsToTableBottom { get; set; }
private bool CacheInitialized { get; set; }
private int StartingRowsCount { get; set; }
private int RowsCount { get; set; }
private int CurrentRow { get; set; }
@@ -24,29 +27,40 @@ namespace QuestPDF.Elements.Table
// inner table: list of all cells that ends at the corresponding row
private TableCell[][] CellsCache { get; set; }
private int MaxRow { get; set; }
private int MaxRowSpan { get; set; }
internal override void Initialize(IPageContext pageContext, ICanvas canvas)
{
StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
BuildCache();
base.Initialize(pageContext, canvas);
}
internal override IEnumerable<Element?> GetChildren()
{
return Cells;
}
internal override void CreateProxy(Func<Element?, Element?> create)
{
Cells.ForEach(x => x.Child = create(x.Child));
}
public void ResetState()
{
Initialize();
foreach (var x in Cells)
x.IsRendered = false;
CurrentRow = 1;
}
private void Initialize()
{
if (CacheInitialized)
return;
StartingRowsCount = Cells.Select(x => x.Row).DefaultIfEmpty(0).Max();
RowsCount = Cells.Select(x => x.Row + x.RowSpan - 1).DefaultIfEmpty(0).Max();
Cells = Cells.OrderBy(x => x.Row).ThenBy(x => x.Column).ToList();
BuildCache();
CacheInitialized = true;
}
private void BuildCache()
{
@@ -56,6 +70,7 @@ namespace QuestPDF.Elements.Table
if (Cells.Count == 0)
{
MaxRow = 0;
MaxRowSpan = 1;
CellsCache = Array.Empty<TableCell[]>();
return;
@@ -66,6 +81,7 @@ namespace QuestPDF.Elements.Table
.ToDictionary(x => x.Key, x => x.OrderBy(x => x.Column).ToArray());
MaxRow = groups.Max(x => x.Key);
MaxRowSpan = Cells.Max(x => x.RowSpan);
CellsCache = Enumerable
.Range(0, MaxRow + 1)
@@ -109,9 +125,13 @@ namespace QuestPDF.Elements.Table
if (command.Measurement.Type == SpacePlanType.Wrap)
continue;
Canvas.Translate(command.Offset);
var offset = ContentDirection == ContentDirection.LeftToRight
? command.Offset
: new Position(availableSpace.Width - command.Offset.X - command.Size.Width, command.Offset.Y);
Canvas.Translate(offset);
command.Cell.Draw(command.Size);
Canvas.Translate(command.Offset.Reverse());
Canvas.Translate(offset.Reverse());
}
CurrentRow = FindLastRenderedRow(renderingCommands) + 1;
@@ -158,7 +178,7 @@ namespace QuestPDF.Elements.Table
if (ExtendLastCellsToTableBottom)
AdjustLastCellSizes(tableHeight, commands);
return commands;
static float[] GetColumnLeftOffsets(IList<TableColumnDefinition> columns)
@@ -199,9 +219,9 @@ namespace QuestPDF.Elements.Table
currentRow = cell.Row;
}
// cell visibility optimizations
if (cell.Row > maxRenderingRow)
if (cell.Row > maxRenderingRow + MaxRowSpan)
break;
// calculate cell position / size
@@ -218,14 +238,14 @@ namespace QuestPDF.Elements.Table
{
maxRenderingRow = Math.Min(maxRenderingRow, cell.Row + cell.RowSpan - 1);
}
// corner case: if cell within the row want to wrap to the next page, do not attempt to render this row
if (cellSize.Type == SpacePlanType.Wrap)
{
maxRenderingRow = Math.Min(maxRenderingRow, cell.Row - 1);
continue;
}
// update position of the last row that cell occupies
var bottomRow = cell.Row + cell.RowSpan - 1;
rowBottomOffsets[bottomRow] = Math.Max(rowBottomOffsets[bottomRow], topOffset + cellSize.Height);
+3 -1
View File
@@ -1,6 +1,8 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Table
{
internal class TableCell : Container, ITableCellContainer
internal class TableCell : ContainerElement, ITableCellContainer
{
public int Row { get; set; } = 0;
public int RowSpan { get; set; } = 1;
+25 -12
View File
@@ -85,22 +85,26 @@ namespace QuestPDF.Elements.Text
if (fallbackOption.Font.ContainsGlyph(codepoint))
return fallbackOption;
}
throw CreateNotMatchingFontException(codepoint);
}
static Exception CreateNotMatchingFontException(int codepoint)
{
var character = char.ConvertFromUtf32(codepoint);
var unicode = $"U-{codepoint:X4}";
var proposedFonts = FindFontsContainingGlyph(codepoint);
var proposedFontsFormatted = proposedFonts.Any() ? string.Join(", ", proposedFonts) : "no fonts available";
throw new DocumentDrawingException(
return new DocumentDrawingException(
$"Could not find an appropriate font fallback for glyph: {unicode} '{character}'. " +
$"Font families available on current environment that contain this glyph: {proposedFontsFormatted}. " +
$"Possible solutions: " +
$"1) Use one of the listed fonts as the primary font in your document. " +
$"2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. ");
}
static IEnumerable<string> FindFontsContainingGlyph(int codepoint)
{
var fontManager = SKFontManager.Default;
@@ -117,29 +121,38 @@ namespace QuestPDF.Elements.Text
{
foreach (var textBlockItem in textBlockItems)
{
if (textBlockItem is TextBlockSpan textBlockSpan)
if (textBlockItem is TextBlockPageNumber or TextBlockElement)
{
// perform font-fallback operation only when any fallback is available
if (textBlockSpan.Style.Fallback == null)
yield return textBlockItem;
}
else if (textBlockItem is TextBlockSpan textBlockSpan)
{
if (!Settings.CheckIfAllTextGlyphsAreAvailable && textBlockSpan.Style.Fallback == null)
{
yield return textBlockSpan;
continue;
}
var textRuns = textBlockSpan.Text.SplitWithFontFallback(textBlockSpan.Style);
foreach (var textRun in textRuns)
{
yield return new TextBlockSpan
var newElement = textBlockSpan switch
{
Text = textRun.Content,
Style = textRun.Style
TextBlockHyperlink hyperlink => new TextBlockHyperlink { Url = hyperlink.Url },
TextBlockSectionLink sectionLink => new TextBlockSectionLink { SectionName = sectionLink.SectionName },
TextBlockSpan => new TextBlockSpan()
};
newElement.Text = textRun.Content;
newElement.Style = textRun.Style;
yield return newElement;
}
}
else
{
yield return textBlockItem;
throw new NotSupportedException();
}
}
}
@@ -12,7 +12,7 @@ namespace QuestPDF.Elements.Text.Items
public TextMeasurementResult? Measure(TextMeasurementRequest request)
{
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
Element.InjectDependencies(request.PageContext, request.Canvas);
var measurement = Element.Measure(new Size(request.AvailableWidth, Size.Max.Height));
@@ -37,7 +37,7 @@ namespace QuestPDF.Elements.Text.Items
public void Draw(TextDrawingRequest request)
{
Element.VisitChildren(x => (x as IStateResettable)?.ResetState());
Element.VisitChildren(x => x.Initialize(request.PageContext, request.Canvas));
Element.InjectDependencies(request.PageContext, request.Canvas);
request.Canvas.Translate(new Position(0, request.TotalAscent));
Element.Draw(new Size(request.TextSize.Width, -request.TotalAscent));
@@ -14,7 +14,7 @@ namespace QuestPDF.Elements.Text.Items
internal class TextBlockSpan : ITextBlockItem
{
public string Text { get; set; }
public TextStyle Style { get; set; } = new();
public TextStyle Style { get; set; } = TextStyle.Default;
private TextShapingResult? TextShapingResult { get; set; }
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
@@ -47,11 +47,11 @@ namespace QuestPDF.Elements.Text.Items
// ignore leading spaces
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
{
while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint)
while (startIndex < TextShapingResult.Length && Text[startIndex] == spaceCodepoint)
startIndex++;
}
if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length)
if (TextShapingResult.Length == 0 || startIndex == TextShapingResult.Length)
{
return new TextMeasurementResult
{
@@ -90,7 +90,7 @@ namespace QuestPDF.Elements.Text.Items
StartIndex = startIndex,
EndIndex = wrappedText.Value.endIndex,
NextIndex = wrappedText.Value.nextIndex,
TotalIndex = TextShapingResult.Glyphs.Length - 1
TotalIndex = TextShapingResult.Length - 1
};
}
@@ -102,7 +102,7 @@ namespace QuestPDF.Elements.Text.Items
// textLength - length of the part of the text that fits in available width (creating a line)
// entire text fits, no need to wrap
if (endIndex == TextShapingResult.Glyphs.Length - 1)
if (endIndex == TextShapingResult.Length - 1)
return (endIndex, endIndex);
// breaking anywhere
@@ -110,7 +110,7 @@ namespace QuestPDF.Elements.Text.Items
return (endIndex, endIndex + 1);
// current line ends at word, next character is space, perfect place to wrap
if (TextShapingResult.Glyphs[endIndex].Codepoint != spaceCodepoint && TextShapingResult.Glyphs[endIndex + 1].Codepoint == spaceCodepoint)
if (TextShapingResult[endIndex].Codepoint != spaceCodepoint && TextShapingResult[endIndex + 1].Codepoint == spaceCodepoint)
return (endIndex, endIndex + 2);
// find last space within the available text to wrap
@@ -118,7 +118,7 @@ namespace QuestPDF.Elements.Text.Items
while (lastSpaceIndex >= startIndex)
{
if (TextShapingResult.Glyphs[lastSpaceIndex].Codepoint == spaceCodepoint)
if (TextShapingResult[lastSpaceIndex].Codepoint == spaceCodepoint)
break;
lastSpaceIndex--;
+39 -33
View File
@@ -8,9 +8,11 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text
{
internal class TextBlock : Element, IStateResettable
internal class TextBlock : Element, IStateResettable, IContentDirectionAware
{
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public ContentDirection ContentDirection { get; set; }
public HorizontalAlignment? Alignment { get; set; }
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
@@ -19,7 +21,7 @@ namespace QuestPDF.Elements.Text
private int CurrentElementIndex { get; set; }
private bool FontFallbackApplied { get; set; } = false;
public void ResetState()
{
ApplyFontFallback();
@@ -50,9 +52,21 @@ namespace QuestPDF.Elements.Text
FontFallbackApplied = true;
}
}
void SetDefaultAlignment()
{
if (Alignment.HasValue)
return;
Alignment = ContentDirection == ContentDirection.LeftToRight
? HorizontalAlignment.Left
: HorizontalAlignment.Right;
}
internal override SpacePlan Measure(Size availableSpace)
{
SetDefaultAlignment();
if (!RenderingQueue.Any())
return SpacePlan.FullRender(Size.Zero);
@@ -80,23 +94,19 @@ namespace QuestPDF.Elements.Text
internal override void Draw(Size availableSpace)
{
SetDefaultAlignment();
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
if (!lines.Any())
return;
var heightOffset = 0f;
var widthOffset = 0f;
var topOffset = 0f;
foreach (var line in lines)
{
widthOffset = 0f;
var leftOffset = GetAlignmentOffset(line.Width);
var alignmentOffset = GetAlignmentOffset(line.Width);
Canvas.Translate(new Position(alignmentOffset, 0));
Canvas.Translate(new Position(0, -line.Ascent));
foreach (var item in line.Elements)
{
var textDrawingRequest = new TextDrawingRequest
@@ -111,21 +121,20 @@ namespace QuestPDF.Elements.Text
TotalAscent = line.Ascent
};
var canvasOffset = ContentDirection == ContentDirection.LeftToRight
? new Position(leftOffset, topOffset - line.Ascent)
: new Position(availableSpace.Width - leftOffset - item.Measurement.Width, topOffset - line.Ascent);
Canvas.Translate(canvasOffset);
item.Item.Draw(textDrawingRequest);
Canvas.Translate(new Position(item.Measurement.Width, 0));
widthOffset += item.Measurement.Width;
Canvas.Translate(canvasOffset.Reverse());
leftOffset += item.Measurement.Width;
}
Canvas.Translate(new Position(-alignmentOffset, 0));
Canvas.Translate(new Position(-line.Width, line.Ascent));
Canvas.Translate(new Position(0, line.LineHeight));
heightOffset += line.LineHeight;
topOffset += line.LineHeight;
}
Canvas.Translate(new Position(0, -heightOffset));
lines
.SelectMany(x => x.Elements)
.GroupBy(x => x.Item)
@@ -142,18 +151,15 @@ namespace QuestPDF.Elements.Text
float GetAlignmentOffset(float lineWidth)
{
if (Alignment == HorizontalAlignment.Left)
return 0;
var emptySpace = availableSpace.Width - lineWidth;
if (Alignment == HorizontalAlignment.Right)
return emptySpace;
if (Alignment == HorizontalAlignment.Center)
return emptySpace / 2;
throw new ArgumentException();
return Alignment switch
{
HorizontalAlignment.Left => ContentDirection == ContentDirection.LeftToRight ? 0 : emptySpace,
HorizontalAlignment.Center => emptySpace / 2,
HorizontalAlignment.Right => ContentDirection == ContentDirection.LeftToRight ? emptySpace : 0,
_ => 0
};
}
}
+9 -1
View File
@@ -3,8 +3,10 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class Unconstrained : ContainerElement, ICacheable
internal class Unconstrained : ContainerElement, IContentDirectionAware, ICacheable
{
public ContentDirection ContentDirection { get; set; }
internal override SpacePlan Measure(Size availableSpace)
{
var childSize = base.Measure(Size.Max);
@@ -25,7 +27,13 @@ namespace QuestPDF.Elements
if (measurement.Type == SpacePlanType.Wrap)
return;
var translate = ContentDirection == ContentDirection.RightToLeft
? new Position(-measurement.Width, 0)
: Position.Zero;
Canvas.Translate(translate);
base.Draw(measurement);
Canvas.Translate(translate.Reverse());
}
}
}
@@ -0,0 +1,26 @@
using QuestPDF.Elements;
using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent
{
public static class ContentDirectionExtensions
{
internal static IContainer ContentDirection(this IContainer element, ContentDirection direction)
{
return element.Element(new ContentDirectionSetter
{
ContentDirection = direction
});
}
public static IContainer ContentFromLeftToRight(this IContainer element)
{
return element.ContentDirection(Infrastructure.ContentDirection.LeftToRight);
}
public static IContainer ContentFromRightToLeft(this IContainer element)
{
return element.ContentDirection(Infrastructure.ContentDirection.RightToLeft);
}
}
}
+1
View File
@@ -55,6 +55,7 @@ namespace QuestPDF.Fluent
public static class GridExtensions
{
[Obsolete("This element has been deprecated since version 2022.11. Please use the Table element, or the combination of the Row and Column elements.")]
public static void Grid(this IContainer element, Action<GridDescriptor> handler)
{
var descriptor = new GridDescriptor();
+1
View File
@@ -30,6 +30,7 @@ namespace QuestPDF.Fluent
public void BaselineMiddle() => Inlined.BaselineAlignment = VerticalAlignment.Middle;
public void BaselineBottom() => Inlined.BaselineAlignment = VerticalAlignment.Bottom;
internal void Alignment(InlinedAlignment? alignment) => Inlined.ElementsAlignment = alignment;
public void AlignLeft() => Inlined.ElementsAlignment = InlinedAlignment.Left;
public void AlignCenter() => Inlined.ElementsAlignment = InlinedAlignment.Center;
public void AlignRight() => Inlined.ElementsAlignment = InlinedAlignment.Right;
+27 -1
View File
@@ -10,7 +10,9 @@ namespace QuestPDF.Fluent
{
internal Page Page { get; } = new Page();
public void Size(float width, float height, Unit unit = Unit.Inch)
#region Size
public void Size(float width, float height, Unit unit = Unit.Point)
{
var pageSize = new PageSize(width, height, unit);
@@ -39,6 +41,10 @@ namespace QuestPDF.Fluent
{
Page.MaxSize = pageSize;
}
#endregion
#region Margin
public void MarginLeft(float value, Unit unit = Unit.Point)
{
@@ -78,6 +84,10 @@ namespace QuestPDF.Fluent
MarginHorizontal(value, unit);
}
#endregion
#region Properties
public void DefaultTextStyle(TextStyle textStyle)
{
Page.DefaultTextStyle = textStyle;
@@ -88,6 +98,16 @@ namespace QuestPDF.Fluent
DefaultTextStyle(handler(TextStyle.Default));
}
public void ContentFromLeftToRight()
{
Page.ContentDirection = ContentDirection.LeftToRight;
}
public void ContentFromRightToLeft()
{
Page.ContentDirection = ContentDirection.RightToLeft;
}
public void PageColor(string color)
{
Page.BackgroundColor = color;
@@ -99,6 +119,10 @@ namespace QuestPDF.Fluent
PageColor(color);
}
#endregion
#region Slots
public IContainer Background()
{
var container = new Container();
@@ -133,6 +157,8 @@ namespace QuestPDF.Fluent
Page.Footer = container;
return container;
}
#endregion
}
public static class PageExtensions
+40 -46
View File
@@ -12,11 +12,18 @@ namespace QuestPDF.Fluent
{
public class TextSpanDescriptor
{
internal TextStyle TextStyle { get; }
internal TextStyle TextStyle = TextStyle.Default;
internal Action<TextStyle> AssignTextStyle { get; }
internal TextSpanDescriptor(TextStyle textStyle)
internal TextSpanDescriptor(Action<TextStyle> assignTextStyle)
{
TextStyle = textStyle;
AssignTextStyle = assignTextStyle;
}
internal void MutateTextStyle(Func<TextStyle, TextStyle> handler)
{
TextStyle = handler(TextStyle);
AssignTextStyle(TextStyle);
}
}
@@ -24,16 +31,17 @@ namespace QuestPDF.Fluent
public class TextPageNumberDescriptor : TextSpanDescriptor
{
internal PageNumberFormatter FormatFunction { get; private set; } = x => x?.ToString() ?? string.Empty;
internal TextPageNumberDescriptor(TextStyle textStyle) : base(textStyle)
internal Action<PageNumberFormatter> AssignFormatFunction { get; }
internal TextPageNumberDescriptor(Action<TextStyle> assignTextStyle, Action<PageNumberFormatter> assignFormatFunction) : base(assignTextStyle)
{
AssignFormatFunction = assignFormatFunction;
AssignFormatFunction(x => x?.ToString());
}
public TextPageNumberDescriptor Format(PageNumberFormatter formatter)
{
FormatFunction = formatter ?? FormatFunction;
AssignFormatFunction(formatter);
return this;
}
}
@@ -41,8 +49,8 @@ namespace QuestPDF.Fluent
public class TextDescriptor
{
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
private TextStyle? DefaultStyle { get; set; }
internal HorizontalAlignment? Alignment { get; set; }
private float Spacing { get; set; } = 0f;
public void DefaultTextStyle(TextStyle style)
@@ -54,7 +62,7 @@ namespace QuestPDF.Fluent
{
DefaultStyle = style(TextStyle.Default);
}
public void AlignLeft()
{
Alignment = HorizontalAlignment.Left;
@@ -91,19 +99,15 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor Span(string? text)
{
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
if (text == null)
return descriptor;
return new TextSpanDescriptor(_ => { });
var items = text
.Replace("\r", string.Empty)
.Split(new[] { '\n' }, StringSplitOptions.None)
.Select(x => new TextBlockSpan
{
Text = x,
Style = style
Text = x
})
.ToList();
@@ -118,7 +122,7 @@ namespace QuestPDF.Fluent
.ToList()
.ForEach(TextBlocks.Add);
return descriptor;
return new TextSpanDescriptor(x => items.ForEach(y => y.Style = x));
}
public TextSpanDescriptor Line(string? text)
@@ -134,16 +138,10 @@ namespace QuestPDF.Fluent
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
{
var style = DefaultStyle.Clone();
var descriptor = new TextPageNumberDescriptor(style);
var textBlockItem = new TextBlockPageNumber();
AddItemToLastTextBlock(textBlockItem);
AddItemToLastTextBlock(new TextBlockPageNumber
{
Source = context => descriptor.FormatFunction(pageNumber(context)),
Style = style
});
return descriptor;
return new TextPageNumberDescriptor(x => textBlockItem.Style = x, x => textBlockItem.Source = context => x(pageNumber(context)));
}
public TextPageNumberDescriptor CurrentPageNumber()
@@ -187,20 +185,17 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(sectionName))
throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
if (IsNullOrEmpty(text))
return descriptor;
AddItemToLastTextBlock(new TextBlockSectionLink
return new TextSpanDescriptor(_ => { });
var textBlockItem = new TextBlockSectionLink
{
Style = style,
Text = text,
SectionName = sectionName
});
};
return descriptor;
AddItemToLastTextBlock(textBlockItem);
return new TextSpanDescriptor(x => textBlockItem.Style = x);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
@@ -214,20 +209,17 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(url))
throw new ArgumentException("Url cannot be null or empty", nameof(url));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
if (IsNullOrEmpty(text))
return descriptor;
return new TextSpanDescriptor(_ => { });
AddItemToLastTextBlock(new TextBlockHyperlink
var textBlockItem = new TextBlockHyperlink
{
Style = style,
Text = text,
Url = url
});
};
return descriptor;
AddItemToLastTextBlock(textBlockItem);
return new TextSpanDescriptor(x => textBlockItem.Style = x);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
@@ -250,8 +242,10 @@ namespace QuestPDF.Fluent
internal void Compose(IContainer container)
{
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
container = container.DefaultTextStyle(DefaultStyle);
TextBlocks.ToList().ForEach(x => x.Alignment ??= Alignment);
if (DefaultStyle != null)
container = container.DefaultTextStyle(DefaultStyle);
if (TextBlocks.Count == 1)
{
+58 -34
View File
@@ -11,7 +11,7 @@ namespace QuestPDF.Fluent
if (style == null)
return descriptor;
descriptor.TextStyle.OverrideStyle(style);
descriptor.MutateTextStyle(x => x.OverrideStyle(style));
return descriptor;
}
@@ -28,114 +28,118 @@ namespace QuestPDF.Fluent
public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
{
descriptor.TextStyle.Color = value;
descriptor.MutateTextStyle(x => x.FontColor(value));
return descriptor;
}
public static T BackgroundColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
{
descriptor.TextStyle.BackgroundColor = value;
descriptor.MutateTextStyle(x => x.BackgroundColor(value));
return descriptor;
}
public static T FontFamily<T>(this T descriptor, string value) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontFamily = value;
descriptor.MutateTextStyle(x => x.FontFamily(value));
return descriptor;
}
public static T FontSize<T>(this T descriptor, float value) where T : TextSpanDescriptor
{
descriptor.TextStyle.Size = value;
descriptor.MutateTextStyle(x => x.FontSize(value));
return descriptor;
}
public static T LineHeight<T>(this T descriptor, float value) where T : TextSpanDescriptor
{
descriptor.TextStyle.LineHeight = value;
descriptor.MutateTextStyle(x => x.LineHeight(value));
return descriptor;
}
public static T Italic<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.IsItalic = value;
descriptor.MutateTextStyle(x => x.Italic(value));
return descriptor;
}
public static T Strikethrough<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.HasStrikethrough = value;
descriptor.MutateTextStyle(x => x.Strikethrough(value));
return descriptor;
}
public static T Underline<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.HasUnderline = value;
descriptor.MutateTextStyle(x => x.Underline(value));
return descriptor;
}
public static T WrapAnywhere<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.WrapAnywhere = value;
descriptor.MutateTextStyle(x => x.WrapAnywhere(value));
return descriptor;
}
#region Weight
public static T Weight<T>(this T descriptor, FontWeight weight) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontWeight = weight;
return descriptor;
}
public static T Thin<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Thin);
descriptor.MutateTextStyle(x => x.Thin());
return descriptor;
}
public static T ExtraLight<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.ExtraLight);
descriptor.MutateTextStyle(x => x.ExtraLight());
return descriptor;
}
public static T Light<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Light);
descriptor.MutateTextStyle(x => x.Light());
return descriptor;
}
public static T NormalWeight<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Normal);
descriptor.MutateTextStyle(x => x.NormalWeight());
return descriptor;
}
public static T Medium<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Medium);
descriptor.MutateTextStyle(x => x.Medium());
return descriptor;
}
public static T SemiBold<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.SemiBold);
descriptor.MutateTextStyle(x => x.SemiBold());
return descriptor;
}
public static T Bold<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Bold);
descriptor.MutateTextStyle(x => x.Bold());
return descriptor;
}
public static T ExtraBold<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.ExtraBold);
descriptor.MutateTextStyle(x => x.ExtraBold());
return descriptor;
}
public static T Black<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.Black);
descriptor.MutateTextStyle(x => x.Black());
return descriptor;
}
public static T ExtraBlack<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Weight(FontWeight.ExtraBlack);
descriptor.MutateTextStyle(x => x.ExtraBlack());
return descriptor;
}
#endregion
@@ -143,24 +147,44 @@ namespace QuestPDF.Fluent
#region Position
public static T NormalPosition<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Normal);
descriptor.MutateTextStyle(x => x.NormalPosition());
return descriptor;
}
public static T Subscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Subscript);
descriptor.MutateTextStyle(x => x.Subscript());
return descriptor;
}
public static T Superscript<T>(this T descriptor) where T : TextSpanDescriptor
{
return descriptor.Position(FontPosition.Superscript);
}
private static T Position<T>(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontPosition = fontPosition;
descriptor.MutateTextStyle(x => x.Superscript());
return descriptor;
}
#endregion
#region Direction
public static T DirectionAuto<T>(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(x => x.DirectionAuto());
return descriptor;
}
public static T DirectionFromLeftToRight<T>(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(x => x.DirectionFromLeftToRight());
return descriptor;
}
public static T DirectionFromRightToLeft<T>(this T descriptor) where T : TextSpanDescriptor
{
descriptor.MutateTextStyle(x => x.DirectionFromRightToLeft());
return descriptor;
}
#endregion
}
}
+51 -32
View File
@@ -6,24 +6,6 @@ namespace QuestPDF.Fluent
{
public static class TextStyleExtensions
{
private static TextStyle Mutate(this TextStyle style, Action<TextStyle> handler)
{
style = style.Clone();
handler(style);
return style;
}
public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
{
return style.Mutate(x => x.Fallback = value);
}
public static TextStyle Fallback(this TextStyle style, Func<TextStyle, TextStyle> handler)
{
return style.Fallback(handler(TextStyle.Default));
}
[Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")]
public static TextStyle Color(this TextStyle style, string value)
{
@@ -32,12 +14,12 @@ namespace QuestPDF.Fluent
public static TextStyle FontColor(this TextStyle style, string value)
{
return style.Mutate(x => x.Color = value);
return style.Mutate(TextStyleProperty.Color, value);
}
public static TextStyle BackgroundColor(this TextStyle style, string value)
{
return style.Mutate(x => x.BackgroundColor = value);
return style.Mutate(TextStyleProperty.BackgroundColor, value);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the FontFamily method.")]
@@ -48,7 +30,7 @@ namespace QuestPDF.Fluent
public static TextStyle FontFamily(this TextStyle style, string value)
{
return style.Mutate(x => x.FontFamily = value);
return style.Mutate(TextStyleProperty.FontFamily, value);
}
[Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")]
@@ -59,39 +41,39 @@ namespace QuestPDF.Fluent
public static TextStyle FontSize(this TextStyle style, float value)
{
return style.Mutate(x => x.Size = value);
return style.Mutate(TextStyleProperty.Size, value);
}
public static TextStyle LineHeight(this TextStyle style, float value)
{
return style.Mutate(x => x.LineHeight = value);
return style.Mutate(TextStyleProperty.LineHeight, value);
}
public static TextStyle Italic(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.IsItalic = value);
return style.Mutate(TextStyleProperty.IsItalic, value);
}
public static TextStyle Strikethrough(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.HasStrikethrough = value);
return style.Mutate(TextStyleProperty.HasStrikethrough, value);
}
public static TextStyle Underline(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.HasUnderline = value);
return style.Mutate(TextStyleProperty.HasUnderline, value);
}
public static TextStyle WrapAnywhere(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.WrapAnywhere = value);
return style.Mutate(TextStyleProperty.WrapAnywhere, value);
}
#region Weight
public static TextStyle Weight(this TextStyle style, FontWeight weight)
{
return style.Mutate(x => x.FontWeight = weight);
return style.Mutate(TextStyleProperty.FontWeight, weight);
}
public static TextStyle Thin(this TextStyle style)
@@ -147,6 +129,7 @@ namespace QuestPDF.Fluent
#endregion
#region Position
public static TextStyle NormalPosition(this TextStyle style)
{
return style.Position(FontPosition.Normal);
@@ -164,11 +147,47 @@ namespace QuestPDF.Fluent
private static TextStyle Position(this TextStyle style, FontPosition fontPosition)
{
if (style.FontPosition == fontPosition)
return style;
return style.Mutate(t => t.FontPosition = fontPosition);
return style.Mutate(TextStyleProperty.FontPosition, fontPosition);
}
#endregion
#region Fallback
public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
{
return style.Mutate(TextStyleProperty.Fallback, value);
}
public static TextStyle Fallback(this TextStyle style, Func<TextStyle, TextStyle> handler)
{
return style.Fallback(handler(TextStyle.Default));
}
#endregion
#region Direction
private static TextStyle TextDirection(this TextStyle style, TextDirection textDirection)
{
return style.Mutate(TextStyleProperty.Direction, textDirection);
}
public static TextStyle DirectionAuto(this TextStyle style)
{
return style.TextDirection(Infrastructure.TextDirection.Auto);
}
public static TextStyle DirectionFromLeftToRight(this TextStyle style)
{
return style.TextDirection(Infrastructure.TextDirection.LeftToRight);
}
public static TextStyle DirectionFromRightToLeft(this TextStyle style)
{
return style.TextDirection(Infrastructure.TextDirection.RightToLeft);
}
#endregion
}
}
+1 -1
View File
@@ -7,7 +7,7 @@ namespace QuestPDF.Helpers
{
public static class Placeholders
{
public static readonly Random Random = new Random(0);
public static readonly Random Random = new Random();
#region Word Cache
@@ -0,0 +1,8 @@
namespace QuestPDF.Infrastructure
{
internal enum ContentDirection
{
LeftToRight,
RightToLeft
}
}
-6
View File
@@ -15,12 +15,6 @@ namespace QuestPDF.Infrastructure
yield break;
}
internal virtual void Initialize(IPageContext pageContext, ICanvas canvas)
{
PageContext = pageContext;
Canvas = canvas;
}
internal virtual void CreateProxy(Func<Element?, Element?> create)
{
+1 -1
View File
@@ -7,7 +7,7 @@ namespace QuestPDF.Infrastructure
}
class Slot : Container, ISlot
class Slot : ContainerElement, ISlot
{
}
@@ -0,0 +1,7 @@
namespace QuestPDF.Infrastructure
{
internal interface IContentDirectionAware
{
public ContentDirection ContentDirection { get; set; }
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace QuestPDF.Infrastructure
{
internal enum TextDirection
{
Auto,
LeftToRight,
RightToLeft
}
}
+6 -72
View File
@@ -1,13 +1,10 @@
using System;
using HarfBuzzSharp;
using QuestPDF.Helpers;
namespace QuestPDF.Infrastructure
{
public class TextStyle
public record TextStyle
{
internal bool HasGlobalStyleApplied { get; private set; }
internal string? Color { get; set; }
internal string? BackgroundColor { get; set; }
internal string? FontFamily { get; set; }
@@ -19,14 +16,11 @@ namespace QuestPDF.Infrastructure
internal bool? HasStrikethrough { get; set; }
internal bool? HasUnderline { get; set; }
internal bool? WrapAnywhere { get; set; }
internal TextDirection? Direction { get; set; }
internal TextStyle? Fallback { get; set; }
// TODO: without cache, this may be an expensive operation
internal object PaintKey => (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
internal object FontMetricsKey => (FontFamily, Size, FontWeight, IsItalic);
internal static readonly TextStyle LibraryDefault = new TextStyle
internal static TextStyle LibraryDefault { get; } = new()
{
Color = Colors.Black,
BackgroundColor = Colors.Transparent,
@@ -39,70 +33,10 @@ namespace QuestPDF.Infrastructure
HasStrikethrough = false,
HasUnderline = false,
WrapAnywhere = false,
Direction = TextDirection.Auto,
Fallback = null
};
// it is important to create new instances for the DefaultTextStyle element to work correctly
public static TextStyle Default => new TextStyle();
internal void ApplyGlobalStyle(TextStyle globalStyle)
{
if (HasGlobalStyleApplied)
return;
HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle);
if (Fallback != null)
ApplyFallbackStyle(this);
}
internal void ApplyFallbackStyle(TextStyle parentStyle)
{
ApplyParentStyle(parentStyle, false);
Fallback?.ApplyFallbackStyle(this);
}
internal void ApplyParentStyle(TextStyle parentStyle, bool mapFallback = true)
{
Color ??= parentStyle.Color;
BackgroundColor ??= parentStyle.BackgroundColor;
FontFamily ??= parentStyle.FontFamily;
Size ??= parentStyle.Size;
LineHeight ??= parentStyle.LineHeight;
FontWeight ??= parentStyle.FontWeight;
FontPosition ??= parentStyle.FontPosition;
IsItalic ??= parentStyle.IsItalic;
HasStrikethrough ??= parentStyle.HasStrikethrough;
HasUnderline ??= parentStyle.HasUnderline;
WrapAnywhere ??= parentStyle.WrapAnywhere;
if (mapFallback)
Fallback ??= parentStyle.Fallback?.Clone();
}
internal void OverrideStyle(TextStyle parentStyle)
{
Color = parentStyle.Color ?? Color;
BackgroundColor = parentStyle.BackgroundColor ?? BackgroundColor;
FontFamily = parentStyle.FontFamily ?? FontFamily;
Size = parentStyle.Size ?? Size;
LineHeight = parentStyle.LineHeight ?? LineHeight;
FontWeight = parentStyle.FontWeight ?? FontWeight;
FontPosition = parentStyle.FontPosition ?? FontPosition;
IsItalic = parentStyle.IsItalic ?? IsItalic;
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere;
Fallback = parentStyle.Fallback?.Clone() ?? Fallback;
}
internal TextStyle Clone()
{
var clone = (TextStyle)MemberwiseClone();
clone.HasGlobalStyleApplied = false;
clone.Fallback = Fallback?.Clone();
return clone;
}
public static TextStyle Default { get; } = new();
}
}
+263
View File
@@ -0,0 +1,263 @@
using System;
using System.Collections.Concurrent;
using QuestPDF.Fluent;
namespace QuestPDF.Infrastructure
{
internal enum TextStyleProperty
{
Color,
BackgroundColor,
FontFamily,
Size,
LineHeight,
FontWeight,
FontPosition,
IsItalic,
HasStrikethrough,
HasUnderline,
WrapAnywhere,
Fallback,
Direction
}
internal static class TextStyleManager
{
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new();
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleApplyGlobalCache = new();
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleOverrideCache = new();
public static TextStyle Mutate(this TextStyle origin, TextStyleProperty property, object value)
{
var cacheKey = (origin, property, value);
return TextStyleMutateCache.GetOrAdd(cacheKey, x => MutateStyle(x.origin, x.property, x.value));
}
private static TextStyle MutateStyle(TextStyle origin, TextStyleProperty property, object? value, bool overrideValue = true)
{
if (overrideValue && value == null)
return origin;
if (property == TextStyleProperty.Color)
{
if (!overrideValue && origin.Color != null)
return origin;
var castedValue = (string?)value;
if (origin.Color == castedValue)
return origin;
return origin with { Color = castedValue };
}
if (property == TextStyleProperty.BackgroundColor)
{
if (!overrideValue && origin.BackgroundColor != null)
return origin;
var castedValue = (string?)value;
if (origin.BackgroundColor == castedValue)
return origin;
return origin with { BackgroundColor = castedValue };
}
if (property == TextStyleProperty.FontFamily)
{
if (!overrideValue && origin.FontFamily != null)
return origin;
var castedValue = (string?)value;
if (origin.FontFamily == castedValue)
return origin;
return origin with { FontFamily = castedValue };
}
if (property == TextStyleProperty.Size)
{
if (!overrideValue && origin.Size != null)
return origin;
var castedValue = (float?)value;
if (origin.Size == castedValue)
return origin;
return origin with { Size = castedValue };
}
if (property == TextStyleProperty.LineHeight)
{
if (!overrideValue && origin.LineHeight != null)
return origin;
var castedValue = (float?)value;
if (origin.LineHeight == castedValue)
return origin;
return origin with { LineHeight = castedValue };
}
if (property == TextStyleProperty.FontWeight)
{
if (!overrideValue && origin.FontWeight != null)
return origin;
var castedValue = (FontWeight?)value;
if (origin.FontWeight == castedValue)
return origin;
return origin with { FontWeight = castedValue };
}
if (property == TextStyleProperty.FontPosition)
{
if (!overrideValue && origin.FontPosition != null)
return origin;
var castedValue = (FontPosition?)value;
if (origin.FontPosition == castedValue)
return origin;
return origin with { FontPosition = castedValue };
}
if (property == TextStyleProperty.IsItalic)
{
if (!overrideValue && origin.IsItalic != null)
return origin;
var castedValue = (bool?)value;
if (origin.IsItalic == castedValue)
return origin;
return origin with { IsItalic = castedValue };
}
if (property == TextStyleProperty.HasStrikethrough)
{
if (!overrideValue && origin.HasStrikethrough != null)
return origin;
var castedValue = (bool?)value;
if (origin.HasStrikethrough == castedValue)
return origin;
return origin with { HasStrikethrough = castedValue };
}
if (property == TextStyleProperty.HasUnderline)
{
if (!overrideValue && origin.HasUnderline != null)
return origin;
var castedValue = (bool?)value;
if (origin.HasUnderline == castedValue)
return origin;
return origin with { HasUnderline = castedValue };
}
if (property == TextStyleProperty.WrapAnywhere)
{
if (!overrideValue && origin.WrapAnywhere != null)
return origin;
var castedValue = (bool?)value;
if (origin.WrapAnywhere == castedValue)
return origin;
return origin with { WrapAnywhere = castedValue };
}
if (property == TextStyleProperty.Fallback)
{
if (!overrideValue && origin.Fallback != null)
return origin;
var castedValue = (TextStyle?)value;
if (origin.Fallback == castedValue)
return origin;
return origin with { Fallback = castedValue };
}
if (property == TextStyleProperty.Direction)
{
if (!overrideValue && origin.Direction != null)
return origin;
var castedValue = (TextDirection?)value;
if (origin.Direction == castedValue)
return origin;
return origin with { Direction = castedValue };
}
throw new ArgumentOutOfRangeException(nameof(property), property, "Expected to mutate the TextStyle object. Provided property type is not supported.");
}
internal static TextStyle ApplyGlobalStyle(this TextStyle style, TextStyle parent)
{
var cacheKey = (style, parent);
return TextStyleApplyGlobalCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, overrideStyle: false).ApplyFontFallback());
}
private static TextStyle ApplyFontFallback(this TextStyle style)
{
var targetFallbackStyle = style
?.Fallback
?.ApplyStyle(style, overrideStyle: false, applyFallback: false)
?.ApplyFontFallback();
return MutateStyle(style, TextStyleProperty.Fallback, targetFallbackStyle);
}
internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
{
var cacheKey = (style, parent);
return TextStyleOverrideCache.GetOrAdd(cacheKey, key =>
{
var result = ApplyStyle(key.origin, key.parent);
return MutateStyle(result, TextStyleProperty.Fallback, key.parent.Fallback);
});
}
private static TextStyle ApplyStyle(this TextStyle style, TextStyle parent, bool overrideStyle = true, bool applyFallback = true)
{
var result = style;
result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideStyle);
result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideStyle);
result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle);
result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideStyle);
result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideStyle);
result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideStyle);
result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideStyle);
result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideStyle);
result = MutateStyle(result, TextStyleProperty.HasStrikethrough, parent.HasStrikethrough, overrideStyle);
result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideStyle);
result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideStyle);
result = MutateStyle(result, TextStyleProperty.Direction, parent.Direction, overrideStyle);
if (applyFallback)
result = MutateStyle(result, TextStyleProperty.Fallback, parent.Fallback, overrideStyle);
return result;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using QuestPDF.Previewer.Inspection;
namespace QuestPDF.Previewer;
internal sealed class IncompatibleVersionApiRequest
{
}
internal sealed class NotifyPresenceApiRequest
{
public string ClientId { get; set; }
public string LibraryVersion { get; set; }
public bool IsDotnet6OrBeyond { get; set; }
public bool IsDotnet3OrBeyond { get; set; }
public bool IsExecutedInUnitTest { get; set; }
}
internal sealed class DocumentPreviewApiRequest
{
public ICollection<PageSnapshot> PageSnapshots { get; set; }
public InspectionElement DocumentHierarchy { get; set; }
public class PageSnapshot
{
public string ResourceId { get; } = Guid.NewGuid().ToString("N");
public int PageNumber { get; set; }
public float Width { get; set; }
public float Height { get; set; }
}
}
internal sealed class GenericErrorApiRequest
{
public GenericError? Error { get; set; }
}
internal sealed class LayoutErrorApiRequest
{
public LayoutErrorTrace? Trace { get; set; }
}
-108
View File
@@ -1,108 +0,0 @@
using System;
using QuestPDF.Drawing;
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using SkiaSharp;
namespace QuestPDF.Previewer
{
public class ExceptionDocument : IDocument
{
private Exception Exception { get; }
public ExceptionDocument(Exception exception)
{
Exception = exception;
}
public DocumentMetadata GetMetadata()
{
return DocumentMetadata.Default;
}
public void Compose(IDocumentContainer document)
{
document.Page(page =>
{
page.ContinuousSize(PageSizes.A4.Width);
page.Margin(50);
page.DefaultTextStyle(x => x.FontSize(16));
page.Foreground().PaddingTop(5).Border(10).BorderColor(Colors.Red.Medium);
page.Header()
.ShowOnce()
.PaddingBottom(5)
.Row(row =>
{
row.Spacing(15);
row.AutoItem()
.PaddingTop(15)
.Width(48)
.AspectRatio(1)
.Canvas((canvas, size) =>
{
const float iconSize = 24;
using var iconPath = SKPath.ParseSvgPathData("M23,12L20.56,14.78L20.9,18.46L17.29,19.28L15.4,22.46L12,21L8.6,22.47L6.71,19.29L3.1,18.47L3.44,14.78L1,12L3.44,9.21L3.1,5.53L6.71,4.72L8.6,1.54L12,3L15.4,1.54L17.29,4.72L20.9,5.54L20.56,9.22L23,12M20.33,12L18.5,9.89L18.74,7.1L16,6.5L14.58,4.07L12,5.18L9.42,4.07L8,6.5L5.26,7.09L5.5,9.88L3.67,12L5.5,14.1L5.26,16.9L8,17.5L9.42,19.93L12,18.81L14.58,19.92L16,17.5L18.74,16.89L18.5,14.1L20.33,12M11,15H13V17H11V15M11,7H13V13H11V7");
using var paint = new SKPaint()
{
Color = SKColors.Red,
IsAntialias = true
};
canvas.Scale(size.Width / iconSize);
canvas.DrawPath(iconPath, paint);
});
row.RelativeItem()
.Column(column =>
{
column.Item().Text("Exception").FontSize(36).FontColor(Colors.Red.Medium).Bold();
column.Item().PaddingTop(-10).Text("Don't panic! Just analyze what's happened...").FontSize(18).FontColor(Colors.Red.Medium).Bold();
});
});
page.Content().PaddingVertical(20).Column(column =>
{
var currentException = Exception;
while (currentException != null)
{
column.Item()
.PaddingTop(25)
.PaddingBottom(15)
.Padding(-10)
.Background(Colors.Grey.Lighten3)
.Padding(10)
.Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(16));
text.Span(currentException.GetType().Name + ": ").Bold();
text.Span(currentException.Message);
});
foreach (var trace in currentException.StackTrace.Split('\n'))
{
column
.Item()
.ShowEntire()
.BorderBottom(1)
.BorderColor(Colors.Grey.Lighten2)
.PaddingVertical(5)
.Text(trace.Trim())
.FontSize(12);
}
currentException = currentException.InnerException;
}
});
});
}
}
}
+22
View File
@@ -0,0 +1,22 @@
#if NETCOREAPP3_0_OR_GREATER
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace QuestPDF.Previewer;
internal static class Helpers
{
public static Task<HttpResponseMessage> PostAsJsonAsync<TValue>(this HttpClient client, string? requestUri, TValue value, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
{
var json = JsonSerializer.Serialize(value, options);
var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
return client.PostAsync(requestUri, jsonContent, cancellationToken);
}
}
#endif
@@ -0,0 +1,60 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Elements;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer.Inspection;
public static class DocumentHierarchyProcessor
{
internal static InspectionElement ExtractDocumentHierarchy(this Container content)
{
content.RemoveExistingContainers();
var proxies = content.ExtractProxyOfType<InspectionProxy>();
return Map(proxies);
InspectionElement Map(TreeNode<InspectionProxy> treeNode)
{
var proxy = treeNode.Value;
var element = proxy.Child;
var locations = proxy
.Statistics
.Keys
.Select(x =>
{
var statistics = proxy.Statistics[x];
return new InspectionElementLocation
{
PageNumber = x,
Position = new MyPosition
{
Top = statistics.Position.Y,
Left = statistics.Position.X
},
Size = new Size
{
Width = statistics.Size.Width,
Height = statistics.Size.Height
}
};
})
.ToList();
return new InspectionElement
{
ElementType = element.GetType().Name,
IsSingleChildContainer = element is ContainerElement,
Location = locations,
Properties = element.GetElementConfiguration().ToList(),
Children = treeNode.Children.Select(Map).ToList()
};
}
}
}
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using QuestPDF.Drawing;
namespace QuestPDF.Previewer.Inspection;
internal class DocumentPreviewResult
{
public IList<PreviewerPicture> PageSnapshots { get; set; }
public InspectionElement DocumentHierarchy { get; set; }
}
@@ -0,0 +1,34 @@
using System.Collections.Generic;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer.Inspection
{
internal class InspectionStateItem
{
public Element Element { get; internal set; }
public Infrastructure.Size Size { get; internal set; }
public Position Position { get; internal set; }
}
internal class MyPosition
{
public float Top { get; set; }
public float Left { get; set; }
}
internal class InspectionElementLocation
{
public int PageNumber { get; internal set; }
public MyPosition Position { get; internal set; }
public Size Size { get; internal set; }
}
internal class InspectionElement
{
public string ElementType { get; set; }
public bool IsSingleChildContainer { get; set; }
public IReadOnlyCollection<InspectionElementLocation> Location { get; set; }
public IReadOnlyCollection<DocumentElementProperty> Properties { get; set; }
public ICollection<InspectionElement> Children { get; set; }
}
}
+63
View File
@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using QuestPDF.Drawing;
using QuestPDF.Infrastructure;
using QuestPDF.Previewer.Inspection;
namespace QuestPDF.Previewer;
internal sealed class DocumentElementProperty
{
public string Label { get; set; }
public string Value { get; set; }
}
internal sealed class Size
{
public float Width { get; set; }
public float Height { get; set; }
}
internal sealed class SpacePlan
{
public float Width { get; set; }
public float Height { get; set; }
public SpacePlanType Type { get; set; }
}
internal sealed class LayoutErrorTrace
{
public string ElementType { get; set; }
public bool IsSingleChildContainer { get; set; }
public IReadOnlyCollection<DocumentElementProperty> ElementProperties { get; set; }
public IReadOnlyCollection<LayoutErrorMeasurement> Measurements { get; set; }
public IReadOnlyCollection<LayoutErrorTrace> Children { get; set; }
}
internal sealed class LayoutErrorMeasurement
{
public Size AvailableSpace { get; set; }
public SpacePlan SpacePlan { get; set; }
}
internal sealed class GenericError
{
public string ExceptionType { get; set; }
public string Message { get; set; }
public string StackTrace { get; set; }
public GenericError? InnerException { get; set; }
}
+25 -28
View File
@@ -1,10 +1,11 @@
#if NET6_0_OR_GREATER
#if NETCOREAPP3_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer
@@ -16,47 +17,43 @@ namespace QuestPDF.Previewer
document.ShowInPreviewerAsync(port).ConfigureAwait(true).GetAwaiter().GetResult();
}
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500)
public static async Task ShowInPreviewerAsync(this IDocument document, int port = 12500, CancellationToken cancellationToken = default)
{
var previewerService = new PreviewerService(port);
using var cancellationTokenSource = new CancellationTokenSource();
previewerService.OnPreviewerStopped += () => cancellationTokenSource.Cancel();
previewerService.OnLostConnection += () => cancellationTokenSource.Cancel();
await previewerService.Connect();
await RefreshPreview();
#if NET6_0_OR_GREATER
HotReloadManager.UpdateApplicationRequested += (_, _) => RefreshPreview();
await WaitForPreviewerExit(cancellationTokenSource.Token);
Task RefreshPreview()
#endif
while (true)
{
var pictures = GetPictures();
return previewerService.RefreshPreview(pictures);
ICollection<PreviewerPicture> GetPictures()
{
try
{
return DocumentGenerator.GeneratePreviewerPictures(document);
}
catch (Exception exception)
{
var exceptionDocument = new ExceptionDocument(exception);
return DocumentGenerator.GeneratePreviewerPictures(exceptionDocument);
}
}
if (cancellationToken.IsCancellationRequested)
break;
// ReSharper disable once MethodSupportsCancellation
await Task.Delay(TimeSpan.FromMilliseconds(1000));
}
async Task WaitForPreviewerExit(CancellationToken cancellationToken)
Task RefreshPreview()
{
while (true)
try
{
if (cancellationToken.IsCancellationRequested)
return;
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
var documentPreviewResult = DocumentGenerator.GeneratePreviewerPictures(document);
return previewerService.ShowDocumentPreview(documentPreviewResult);
}
catch (DocumentLayoutException exception)
{
return previewerService.ShowLayoutError(exception);
}
catch (Exception exception)
{
return previewerService.ShowGenericError(exception);
}
}
}
@@ -1,23 +0,0 @@
using System;
using System.Collections.Generic;
using QuestPDF.Infrastructure;
#if NET6_0_OR_GREATER
namespace QuestPDF.Previewer
{
internal class PreviewerRefreshCommand
{
public ICollection<Page> Pages { get; set; }
public class Page
{
public string Id { get; } = Guid.NewGuid().ToString("N");
public float Width { get; init; }
public float Height { get; init; }
}
}
}
#endif
+156 -53
View File
@@ -1,25 +1,32 @@
#if NET6_0_OR_GREATER
#if NETCOREAPP3_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions;
using QuestPDF.Previewer.Inspection;
namespace QuestPDF.Previewer
{
internal class PreviewerService
{
private string ClientId { get; } = Guid.NewGuid().ToString("D");
private string LibraryVersion { get; } = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version?.ToString();
private int Port { get; }
private HttpClient HttpClient { get; }
public event Action? OnPreviewerStopped;
private const int RequiredPreviewerVersionMajor = 2022;
private const int RequiredPreviewerVersionMinor = 8;
public event Action? OnLostConnection;
private const int RequiredPreviewerVersionMajor = 2023;
private const int RequiredPreviewerVersionMinor = 1;
public PreviewerService(int port)
{
@@ -30,11 +37,23 @@ namespace QuestPDF.Previewer
Timeout = TimeSpan.FromSeconds(1)
};
}
internal static readonly JsonSerializerOptions JsonSerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
MaxDepth = 256,
Converters =
{
new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
}
};
#region Connection
public async Task Connect()
{
var isAvailable = await IsPreviewerAvailable();
if (!isAvailable)
{
StartPreviewer();
@@ -42,9 +61,11 @@ namespace QuestPDF.Previewer
}
var previewerVersion = await GetPreviewerVersion();
CheckVersionCompatibility(previewerVersion);
}
await CheckVersionCompatibility(previewerVersion);
StartNotifyPresenceTask();
}
private async Task<bool> IsPreviewerAvailable()
{
try
@@ -58,12 +79,6 @@ namespace QuestPDF.Previewer
}
}
private async Task<Version> GetPreviewerVersion()
{
using var result = await HttpClient.GetAsync("/version");
return await result.Content.ReadFromJsonAsync<Version>();
}
private void StartPreviewer()
{
try
@@ -80,13 +95,6 @@ namespace QuestPDF.Previewer
};
process.Start();
Task.Run(async () =>
{
await process.WaitForExitAsync();
process.Dispose();
OnPreviewerStopped?.Invoke();
});
}
catch
{
@@ -94,16 +102,6 @@ namespace QuestPDF.Previewer
"Please install it by executing in terminal the following command: 'dotnet tool install --global QuestPDF.Previewer'.");
}
}
private void CheckVersionCompatibility(Version version)
{
if (version.Major == RequiredPreviewerVersionMajor && version.Minor == RequiredPreviewerVersionMinor)
return;
throw new Exception($"Previewer version is not compatible. Possible solutions: " +
$"1) Update the QuestPDF library to newer version. " +
$"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version {RequiredPreviewerVersionMajor}.{RequiredPreviewerVersionMinor}'");
}
private async Task WaitForConnection()
{
@@ -114,50 +112,155 @@ namespace QuestPDF.Previewer
while (true)
{
// ReSharper disable once MethodSupportsCancellation
await Task.Delay(TimeSpan.FromMilliseconds(250));
if (cancellationToken.IsCancellationRequested)
throw new Exception($"Cannot connect to the QuestPDF Previewer tool. Please make sure that your Operating System does not block HTTP connections on port {Port}.");
var isConnected = await IsPreviewerAvailable();
if (isConnected)
break;
}
}
public async Task RefreshPreview(ICollection<PreviewerPicture> pictures)
#endregion
#region Checking compatibility
private async Task<Version> GetPreviewerVersion()
{
using var result = await HttpClient.GetAsync("/version");
var response = await result.Content.ReadFromJsonAsync<string>();
return Version.Parse(response);
}
private async Task CheckVersionCompatibility(Version version)
{
if (version is { Major: RequiredPreviewerVersionMajor, Minor: RequiredPreviewerVersionMinor })
return;
await ShowIncompatibleVersion();
throw new Exception($"Previewer version is not compatible. Possible solutions: " +
$"1) Update the QuestPDF library to newer version. " +
$"2) Update the QuestPDF previewer tool using the following command: 'dotnet tool update --global QuestPDF.Previewer --version {RequiredPreviewerVersionMajor}.{RequiredPreviewerVersionMinor}'");
}
private async Task ShowIncompatibleVersion()
{
var payload = new IncompatibleVersionApiRequest();
await HttpClient.PostAsJsonAsync("/update/incompatibleVersion", payload, JsonSerializerOptions);
}
#endregion
#region Reporting presence
private void StartNotifyPresenceTask()
{
Task.Run(async () =>
{
while (true)
{
await NotifyPresence();
await Task.Delay(1000);
}
});
}
private async Task NotifyPresence()
{
var payload = new NotifyPresenceApiRequest
{
ClientId = ClientId,
LibraryVersion = LibraryVersion,
IsDotnet6OrBeyond = RuntimeDetector.IsNet6OrGreater,
IsDotnet3OrBeyond = RuntimeDetector.IsNet3OrGreater,
IsExecutedInUnitTest = UnitTestDetector.RunningInUnitTest
};
await HttpClient.PostAsJsonAsync("/update/notifyPresence", payload);
}
#endregion
#region Updating state
public async Task ShowDocumentPreview(DocumentPreviewResult documentPreviewResult)
{
using var multipartContent = new MultipartFormDataContent();
var pages = new List<PreviewerRefreshCommand.Page>();
foreach (var picture in pictures)
var pages = new List<DocumentPreviewApiRequest.PageSnapshot>();
foreach (var snapshot in documentPreviewResult.PageSnapshots)
{
var page = new PreviewerRefreshCommand.Page
var page = new DocumentPreviewApiRequest.PageSnapshot
{
Width = picture.Size.Width,
Height = picture.Size.Height
PageNumber = documentPreviewResult.PageSnapshots.IndexOf(snapshot) + 1,
Width = snapshot.Size.Width,
Height = snapshot.Size.Height
};
pages.Add(page);
var pictureStream = picture.Picture.Serialize().AsStream();
multipartContent.Add(new StreamContent(pictureStream), page.Id, page.Id);
var pictureStream = snapshot.Picture.Serialize().AsStream();
multipartContent.Add(new StreamContent(pictureStream), "snapshots", page.ResourceId);
}
var command = new PreviewerRefreshCommand
var request = new DocumentPreviewApiRequest
{
Pages = pages
PageSnapshots = pages,
DocumentHierarchy = documentPreviewResult.DocumentHierarchy
};
multipartContent.Add(JsonContent.Create(command), "command");
using var _ = await HttpClient.PostAsync("/update/preview", multipartContent);
foreach (var picture in pictures)
var json = JsonSerializer.Serialize(request, JsonSerializerOptions);
var jsonContent = new StringContent(json, Encoding.UTF8, "application/json");
multipartContent.Add(jsonContent, "request");
using var response = await HttpClient.PostAsync("/update/documentPreview", multipartContent);
response.EnsureSuccessStatusCode();
foreach (var picture in documentPreviewResult.PageSnapshots)
picture.Picture.Dispose();
}
public async Task ShowGenericError(Exception exception)
{
var payload = new GenericErrorApiRequest
{
Error = MapException(exception)
};
await HttpClient.PostAsJsonAsync("/update/genericError", payload, JsonSerializerOptions);
static GenericError? MapException(Exception? exception)
{
if (exception == null)
return null;
return new GenericError
{
ExceptionType = exception.GetType().FullName,
Message = exception.Message,
StackTrace = exception.StackTrace,
InnerException = MapException(exception.InnerException)
};
}
}
public async Task ShowLayoutError(DocumentLayoutException exception)
{
var payload = new LayoutErrorApiRequest
{
Trace = exception.ElementTrace
};
await HttpClient.PostAsJsonAsync("/update/layoutError", payload, JsonSerializerOptions);
}
#endregion
}
}
+16
View File
@@ -0,0 +1,16 @@
namespace QuestPDF.Previewer;
public static class RuntimeDetector
{
#if NET6_0_OR_GREATER
public static bool IsNet6OrGreater => true;
#else
public static bool IsNet6OrGreater => false;
#endif
#if NETCOREAPP3_0_OR_GREATER
public static bool IsNet3OrGreater => true;
#else
public static bool IsNet6OrGreater => false;
#endif
}
+38
View File
@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing.Proxy;
using QuestPDF.Infrastructure;
namespace QuestPDF.Previewer;
internal record TreeNode<T>(T Value, ICollection<TreeNode<T>> Children);
internal static class TreeTraversal
{
public static TreeNode<T> ExtractProxyOfType<T>(this Element root) where T : ElementProxy
{
return Traverse(root).FirstOrDefault();
IEnumerable<TreeNode<T>> Traverse(Element element)
{
if (element is T proxy)
{
var result = new TreeNode<T>(proxy, new List<TreeNode<T>>());
proxy
.Child!
.GetChildren()
.SelectMany(Traverse)
.ToList()
.ForEach(result.Children.Add);
yield return result;
}
else
{
foreach (var treeNode in element.GetChildren().SelectMany(Traverse))
yield return treeNode;
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
using System;
using System.Linq;
namespace QuestPDF.Previewer;
internal static class UnitTestDetector
{
public static bool RunningInUnitTest { get; }
private static string[] UnitTestAssemblies = { "xunit", "nunit", "mstest" };
static UnitTestDetector()
{
RunningInUnitTest = AppDomain
.CurrentDomain
.GetAssemblies()
.Select(x => x.FullName.ToLowerInvariant())
.Any(x => UnitTestAssemblies.Any(x.Contains));
}
}
+9 -5
View File
@@ -3,10 +3,10 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.8.2</Version>
<Version>2022.11.0</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>
<LangVersion>10</LangVersion>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageIcon>Logo.png</PackageIcon>
<PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl>
@@ -17,14 +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>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="2.80.4" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.80.4" />
<PackageReference Include="SkiaSharp" Version="2.88.3" />
<PackageReference Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
</ItemGroup>
<ItemGroup>
@@ -65,4 +65,8 @@
<None Remove="Resources\DefaultFont\Lato-ThinItalic.ttf" />
<EmbeddedResource Include="Resources\DefaultFont\Lato-ThinItalic.ttf" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Previewer\Models\GenericError.cs" />
</ItemGroup>
</Project>

Some files were not shown because too many files have changed in this diff Show More