Compare commits

...

66 Commits

Author SHA1 Message Date
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
53 changed files with 1708 additions and 423 deletions
@@ -0,0 +1,411 @@
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
.Padding(20)
.MinimalBox()
.Border(1)
.ExtendHorizontal()
.Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
});
table.Header(header =>
{
header.Cell().ContentFromLeftToRight().Element(HeaderCell("Left-to-right"));
header.Cell().ContentFromRightToLeft().Element(HeaderCell("Right-to-left"));
static Action<IContainer> HeaderCell(string label)
{
return container => container
.Border(1)
.BorderColor(Colors.Grey.Medium)
.Background(Colors.Grey.Lighten3)
.PaddingHorizontal(10)
.PaddingVertical(5)
.Text(label)
.FontSize(18)
.SemiBold();
}
});
table.Cell().Element(TestCell).ContentFromLeftToRight().Element(content);
table.Cell()
.Element(TestCell)
.ContentFromRightToLeft()
.Element(content);
static IContainer TestCell(IContainer container)
{
return container.Border(1).BorderColor(Colors.Grey.Medium).Padding(10);
}
});
};
}
[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.ContentFromRightToLeft();
page.Content().Column(column =>
{
column.Spacing(20);
column.Item().Row(row =>
{
row.Spacing(10);
row.AutoItem().AlignMiddle().Width(20).Height(20).Image(Placeholders.Image);
row.RelativeItem()
.Text("Document title")
.FontSize(24).FontColor(Colors.Blue.Accent1).SemiBold();
});
column.Item().Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
columns.RelativeColumn();
});
foreach (var i in Enumerable.Range(0, 9))
{
var width = (i % 4 == 0) ? 2 : 1;
table
.Cell()
.ColumnSpan((uint)width)
.Background(i % 4 == 0 ? Colors.Grey.Lighten1 : Colors.Grey.Lighten2)
.Padding(5)
.AlignCenter()
.Text(i)
.FontSize(20);
}
});
});
});
});
}
[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().Background(Colors.Grey.Lighten3).Text("Default alignment").FontSize(13);
column.Item().Element(ContentWithAlignment(null));
column.Item().Background(Colors.Grey.Lighten3).Text("Left alignment").FontSize(14);
column.Item().Element(ContentWithAlignment(InlinedAlignment.Left));
column.Item().Background(Colors.Grey.Lighten3).Text("Center alignment").FontSize(14);
column.Item().Element(ContentWithAlignment(InlinedAlignment.Center));
column.Item().Background(Colors.Grey.Lighten3).Text("Right alignment").FontSize(14);
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().Background(Colors.Grey.Lighten3).Text("Default alignment").FontSize(13);
column.Item().Element(ContentWithAlignment(null));
column.Item().Background(Colors.Grey.Lighten3).Text("Left alignment").FontSize(14);
column.Item().Element(ContentWithAlignment(HorizontalAlignment.Left));
column.Item().Background(Colors.Grey.Lighten3).Text("Center alignment").FontSize(14);
column.Item().Element(ContentWithAlignment(HorizontalAlignment.Center));
column.Item().Background(Colors.Grey.Lighten3).Text("Right alignment").FontSize(14);
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);
}
}
}
}
+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();
+72 -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,73 @@ 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 TextDirectionality()
{
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");
}
});
});
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.8.0</Version>
<Version>2022.11.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>
@@ -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})";
});
});
});
}
+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();
}
}
+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();
}
+37 -11
View File
@@ -65,6 +65,7 @@ namespace QuestPDF.Drawing
document.Compose(container);
var content = container.Compose();
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
ApplyContentDirection(content, ContentDirection.LeftToRight);
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
@@ -79,7 +80,7 @@ namespace QuestPDF.Drawing
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState)
where TCanvas : ICanvas, IRenderingCanvas
{
content.VisitChildren(x => x?.Initialize(pageContext, canvas));
InjectDependencies(content, pageContext, canvas);
content.VisitChildren(x => (x as IStateResettable)?.ResetState());
canvas.BeginDocument();
@@ -140,6 +141,18 @@ namespace QuestPDF.Drawing
}
}
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 =>
@@ -160,6 +173,24 @@ namespace QuestPDF.Drawing
return debuggingState;
}
internal static void ApplyContentDirection(this Element? content, ContentDirection direction)
{
if (content == null)
return;
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 +203,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 +215,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);
}
}
}
+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());
}
}
}
-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)
+1 -1
View File
@@ -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; }
}
}
+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);
}
}
}
+29 -13
View File
@@ -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()
@@ -49,6 +51,8 @@ namespace QuestPDF.Elements
internal override SpacePlan Measure(Size availableSpace)
{
SetDefaultAlignment();
if (!ChildrenQueue.Any())
return SpacePlan.FullRender(Size.Zero);
@@ -81,6 +85,8 @@ namespace QuestPDF.Elements
internal override void Draw(Size availableSpace)
{
SetDefaultAlignment();
var lines = Compose(availableSpace);
var topOffset = 0f;
@@ -103,7 +109,6 @@ namespace QuestPDF.Elements
var elementOffset = ElementOffset();
var leftOffset = AlignOffset();
Canvas.Translate(new Position(leftOffset, 0));
foreach (var measurement in lineMeasurements)
{
@@ -113,15 +118,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 +146,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 +173,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 +228,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;
+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());
}
}
}
+3 -1
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 =>
{
+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);
});
+9 -3
View File
@@ -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))
+35 -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,17 +27,8 @@ 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;
@@ -42,11 +36,26 @@ namespace QuestPDF.Elements.Table
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 +65,7 @@ namespace QuestPDF.Elements.Table
if (Cells.Count == 0)
{
MaxRow = 0;
MaxRowSpan = 1;
CellsCache = Array.Empty<TableCell[]>();
return;
@@ -66,6 +76,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 +120,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 +173,7 @@ namespace QuestPDF.Elements.Table
if (ExtendLastCellsToTableBottom)
AdjustLastCellSizes(tableHeight, commands);
return commands;
static float[] GetColumnLeftOffsets(IList<TableColumnDefinition> columns)
@@ -199,9 +214,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 +233,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);
+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
}
}
@@ -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)
{
@@ -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;
}
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ namespace QuestPDF.Previewer
public event Action? OnPreviewerStopped;
private const int RequiredPreviewerVersionMajor = 2022;
private const int RequiredPreviewerVersionMinor = 8;
private const int RequiredPreviewerVersionMinor = 11;
public PreviewerService(int port)
{
+1 -1
View File
@@ -3,7 +3,7 @@
<Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId>
<Version>2022.8.2</Version>
<Version>2022.11.0-alpha1</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>
+1 -1
View File
@@ -21,7 +21,7 @@ Install-Package QuestPDF
dotnet add package QuestPDF
// Package reference in .csproj file
<PackageReference Include="QuestPDF" Version="2022.3.0" />
<PackageReference Include="QuestPDF" Version="2022.11.0" />
```
## Documentation
+3 -20
View File
@@ -1,20 +1,3 @@
2022.8.0:
- Improved library performance,
- Breaking change: changed default font from Calibri to an open-source Lato,
- Default font files are included with the nuget package, making it safe to deploy on any environment,
- Default font files are significantly smaller, so output document files should be smaller too (up to 20x reduction in size),
- When requested font is not available on the runtime environment, library provides list of available fonts,
- Fixed a rare layout overflow exception with the Inlined element,
- Fixed a memory leak connected to the HarfBuzz library.
2022.8.1:
- Fixed: default text style does not always work
- Fixed: page breaking rendering does not work in very specific corner cases
- Stability improvements for text wrapping
- Updated stability of rendering elements in negative space
- Optimization for the Column element: do not measure child when available height is negative
2022.8.2
- Fixed: the Column element incorrectly renders zero-height elements.
Added support for the right-to-left content direction.
Fixed: word-wrapping algorithm does not work correctly with right-to-left languages.
Fixed: Page.Size() API incorrectly uses Unit.Inch as default unit. Replaced with Unit.Point for consistency.
+2 -2
View File
@@ -3,7 +3,7 @@
public static class Settings
{
/// <summary>
/// This value represents the maximum lenght of the document that the library produces.
/// This value represents the maximum length of the document that the library produces.
/// This is useful when layout constraints are too strong, e.g. one element does not fit in another.
/// In such cases, the library would produce document of infinite length, consuming all available resources.
/// To break the algorithm and save the environment, the library breaks the rendering process after reaching specified length of document.
@@ -35,6 +35,6 @@
/// However, it provides hints that used fonts are not sufficient to produce correct results.
/// </summary>
/// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
public static bool CheckIfAllTextGlyphsAreAvailableInSpecifiedFont { get; set; } = System.Diagnostics.Debugger.IsAttached;
public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached;
}
}
+8 -6
View File
@@ -17,13 +17,14 @@ It offers a layouting engine designed with a full paging support in mind. The do
Unlike other libraries, it does not rely on the HTML-to-PDF conversion which in many cases is not reliable. Instead, it implements its own layouting engine that is optimized to cover all paging-related requirements.
## Please show the value
## Please help by giving a star
Choosing a project dependency could be difficult. We need to ensure stability and maintainability of our projects. Surveys show that GitHub stars count play an important factor when assessing library quality.
⭐ Please give this repository a star. It takes seconds and help thousands of developers! ⭐
<img src="https://user-images.githubusercontent.com/9263853/184642026-27dd7567-a46a-45d4-9594-e6a70a7193e9.png" width="700" />
<img src="https://user-images.githubusercontent.com/9263853/190931857-8ca52ec8-cc7d-4d12-9467-4442b3342fa1.png" width="700" />
## Please share with the community
@@ -46,6 +47,7 @@ Special thanks to all companies that decided to sponsor QuestPDF development. Th
| Company | Description |
|--------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------|
| <img src="Resources/jetbrains-logo.svg" width="100px"> | [JetBrains](https://www.jetbrains.com/) supports this project as part of the OSS Power-Ups program. Thank you!<br/>100$ / month |
| <img src="https://avatars.githubusercontent.com/u/2712328?v=4" width="100px"> | [Mark Gould](https://github.com/markgould) supports this project. Thank you!<br/>100$ / month |
[![Sponsor project](https://img.shields.io/badge/%E2%9D%A4%EF%B8%8F%20sponsor-QuestPDF-red)](https://github.com/sponsors/QuestPDF)
@@ -61,14 +63,14 @@ Install-Package QuestPDF
dotnet add package QuestPDF
// Package reference in .csproj file
<PackageReference Include="QuestPDF" Version="2022.6.0" />
<PackageReference Include="QuestPDF" Version="2022.9.0" />
```
[![Nuget version](https://img.shields.io/badge/package%20details-QuestPDF-blue?logo=nuget)](https://www.nuget.org/packages/QuestPDF/)
## Documentation
[![Getting started tutorial]( https://img.shields.io/badge/%F0%9F%9A%80%20read-getting%20started-blue)](https://www.questpdf.com/getting-started.html)
[![Getting started tutorial]( https://img.shields.io/badge/%F0%9F%9A%80%20read-getting%20started-blue)](https://www.questpdf.com/getting-started)
A short and easy to follow tutorial showing how to design an invoice document under 200 lines of code.
@@ -76,14 +78,14 @@ A short and easy to follow tutorial showing how to design an invoice document un
A detailed description of behavior of all available components and how to use them with C# Fluent API.
[![Patterns and Practices](https://img.shields.io/badge/%E2%9C%A8%20read-patterns%20and%20practices-blue)](https://www.questpdf.com/design-patterns.html)
[![Patterns and Practices](https://img.shields.io/badge/%E2%9C%A8%20read-patterns%20and%20practices-blue)](https://www.questpdf.com/design-patterns)
Everything that may help you designing great reports and create reusable code that is easy to maintain.
## QuestPDF Previewer
The QuestPDF Previewer is a tool designed to simplify and speed up your development lifecycle. First, it shows a preview of your document. But the real magic starts with the hot-reload capability! It observes your code and updates the preview every time you change the implementation. Get real-time results without the need of code recompilation. Save time and enjoy the task!
[![Learn more](https://img.shields.io/badge/%F0%9F%93%96%20Previewer-learn%20more-blue)](https://www.questpdf.com/document-previewer.html)
[![Learn more](https://img.shields.io/badge/%F0%9F%93%96%20Previewer-learn%20more-blue)](https://www.questpdf.com/document-previewer)
<img src="https://github.com/QuestPDF/QuestPDF-Documentation/blob/main/docs/public/previewer/animation.gif?raw=true" width="100%">