Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09e642295f | |||
| 65990ebf59 | |||
| 9d86e9efd3 | |||
| 5cbacc7ea1 | |||
| 8214a8429d | |||
| 389b8ce304 | |||
| e1f7cff4aa | |||
| 6801425082 | |||
| ec31c9b063 | |||
| 6c8867e1b3 | |||
| 20d86cbfde | |||
| 16a164e5b8 | |||
| 261f087b46 | |||
| c876350b05 | |||
| 58f3932241 | |||
| 9ce9ca85be | |||
| 553c8ab719 | |||
| e768e9b06d | |||
| fd913a777c | |||
| ed9e6daec5 | |||
| ee6249a658 | |||
| b307304f46 | |||
| f028f82e11 | |||
| 719a3385f6 | |||
| 2adff11400 | |||
| 34fed6d547 | |||
| db2df75624 | |||
| 6b535752df | |||
| 556f87ff25 | |||
| fbebbd85eb | |||
| 4650c2a4ea | |||
| 5390fc3f1b | |||
| 1ba01a1cf5 | |||
| d4448437ac | |||
| bc853f48fc | |||
| 0468dd5f02 | |||
| 3ba09ea826 | |||
| 92abd32aae | |||
| e17867d1f3 | |||
| bd71f30c78 | |||
| 6a78a4ebdb | |||
| 9fc721d66b | |||
| 7d62dead86 |
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
|
||||
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
|
||||
<PackageReference Include="microcharts" Version="0.9.5.9" />
|
||||
<PackageReference Include="nunit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.4.0" />
|
||||
<PackageReference Include="nunit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.1" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.80.4" />
|
||||
<PackageReference Include="Svg.Skia" Version="0.5.10" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
|
||||
namespace QuestPDF.Examples
|
||||
{
|
||||
public class RepeatContentExamples
|
||||
{
|
||||
[Test]
|
||||
public void ItemTypes()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProducePdf()
|
||||
.PageSize(PageSizes.A4)
|
||||
.ShowResults()
|
||||
.Render(container =>
|
||||
{
|
||||
container
|
||||
.Padding(25)
|
||||
.Decoration(decoration =>
|
||||
{
|
||||
decoration.Before().Text("Test").FontSize(22);
|
||||
|
||||
decoration.Content().Column(column =>
|
||||
{
|
||||
column.Spacing(20);
|
||||
|
||||
foreach (var _ in Enumerable.Range(0, 10))
|
||||
column.Item().Background(Colors.Grey.Medium).ExtendHorizontal().Height(80);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NUnit.Framework;
|
||||
using QuestPDF.Elements.Text;
|
||||
using QuestPDF.Examples.Engine;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
@@ -618,5 +619,75 @@ namespace QuestPDF.Examples
|
||||
.FontSize(20);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontFallback()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Margin(50);
|
||||
page.PageColor(Colors.White);
|
||||
page.DefaultTextStyle(x => x
|
||||
.Fallback(y => y.FontFamily("Segoe UI Emoji")
|
||||
.Fallback(y => y.FontFamily("Microsoft YaHei"))));
|
||||
|
||||
page.Size(PageSizes.A4);
|
||||
|
||||
page.Content().Text(t =>
|
||||
{
|
||||
t.Line("This is normal text.");
|
||||
t.EmptyLine();
|
||||
|
||||
t.Line("Following line should use font fallback:");
|
||||
t.Line("中文文本");
|
||||
t.EmptyLine();
|
||||
|
||||
t.Line("The following line contains a mix of known and unknown characters.");
|
||||
t.Line("Mixed line: This 中文 is 文文 a mixed 本 本 line 本 中文文本!");
|
||||
t.EmptyLine();
|
||||
|
||||
t.Line("Emojis work out of the box because of font fallback: 😊😅🥳👍❤😍👌");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@ namespace QuestPDF.Previewer
|
||||
CommunicationService.Instance.OnDocumentRefreshed += HandleUpdatePreview;
|
||||
|
||||
ShowPdfCommand = ReactiveCommand.Create(ShowPdf);
|
||||
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/documentation/api-reference.html"));
|
||||
ShowDocumentationCommand = ReactiveCommand.Create(() => OpenLink("https://www.questpdf.com/api-reference/index.html"));
|
||||
SponsorProjectCommand = ReactiveCommand.Create(() => OpenLink("https://github.com/sponsors/QuestPDF"));
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<PackageId>QuestPDF.Previewer</PackageId>
|
||||
<Version>2022.8.0</Version>
|
||||
<Version>2022.9.1</Version>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<ToolCommandName>questpdf-previewer</ToolCommandName>
|
||||
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription>
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace QuestPDF.ReportSample
|
||||
Content = documentContainer.Compose();
|
||||
|
||||
PageContext = new PageContext();
|
||||
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
|
||||
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
|
||||
|
||||
var sw = new Stopwatch();
|
||||
sw.Start();
|
||||
@@ -69,7 +69,7 @@ namespace QuestPDF.ReportSample
|
||||
[Benchmark]
|
||||
public void GenerationTest()
|
||||
{
|
||||
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null);
|
||||
DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,11 +48,10 @@ namespace QuestPDF.ReportSample
|
||||
Report.Compose(container);
|
||||
var content = container.Compose();
|
||||
|
||||
var metadata = Report.GetMetadata();
|
||||
var pageContext = new PageContext();
|
||||
|
||||
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, metadata, null);
|
||||
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, metadata, null);
|
||||
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
|
||||
DocumentGenerator.RenderPass(pageContext, new FreeCanvas(), content, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.1.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.7.0" />
|
||||
<PackageReference Include="nunit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.0" />
|
||||
|
||||
@@ -65,24 +65,25 @@ namespace QuestPDF.Drawing
|
||||
document.Compose(container);
|
||||
var content = container.Compose();
|
||||
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
|
||||
|
||||
var metadata = document.GetMetadata();
|
||||
var pageContext = new PageContext();
|
||||
ApplyContentRepeatState(content, false);
|
||||
|
||||
var debuggingState = metadata.ApplyDebugging ? ApplyDebugging(content) : null;
|
||||
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
|
||||
|
||||
if (metadata.ApplyCaching)
|
||||
if (Settings.EnableCaching)
|
||||
ApplyCaching(content);
|
||||
|
||||
RenderPass(pageContext, new FreeCanvas(), content, metadata, debuggingState);
|
||||
RenderPass(pageContext, canvas, content, metadata, debuggingState);
|
||||
var pageContext = new PageContext();
|
||||
RenderPass(pageContext, new FreeCanvas(), content, debuggingState);
|
||||
RenderPass(pageContext, canvas, content, debuggingState);
|
||||
}
|
||||
|
||||
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DocumentMetadata documentMetadata, DebuggingState? debuggingState)
|
||||
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState)
|
||||
where TCanvas : ICanvas, IRenderingCanvas
|
||||
{
|
||||
content.VisitChildren(x => x?.Initialize(pageContext, canvas));
|
||||
content.VisitChildren(x => (x as IStateResettable)?.ResetState());
|
||||
|
||||
ResetIsRenderedState(content);
|
||||
|
||||
canvas.BeginDocument();
|
||||
|
||||
@@ -114,7 +115,7 @@ namespace QuestPDF.Drawing
|
||||
|
||||
canvas.EndPage();
|
||||
|
||||
if (currentPage >= documentMetadata.DocumentLayoutExceptionThreshold)
|
||||
if (currentPage >= Settings.DocumentLayoutExceptionThreshold)
|
||||
{
|
||||
canvas.EndDocument();
|
||||
ThrowLayoutException();
|
||||
@@ -131,8 +132,8 @@ namespace QuestPDF.Drawing
|
||||
void ThrowLayoutException()
|
||||
{
|
||||
var message = $"Composed layout generates infinite document. This may happen in two cases. " +
|
||||
$"1) Your document and its layout configuration is correct but the content takes more than {documentMetadata.DocumentLayoutExceptionThreshold} pages. " +
|
||||
$"In this case, please increase the value {nameof(DocumentMetadata)}.{nameof(DocumentMetadata.DocumentLayoutExceptionThreshold)} property configured in the {nameof(IDocument.GetMetadata)} method. " +
|
||||
$"1) Your document and its layout configuration is correct but the content takes more than {Settings.DocumentLayoutExceptionThreshold} pages. " +
|
||||
$"In this case, please increase the value {nameof(QuestPDF)}.{nameof(Settings)}.{nameof(Settings.DocumentLayoutExceptionThreshold)} static property. " +
|
||||
$"2) The layout configuration of your document is invalid. Some of the elements require more space than is provided." +
|
||||
$"Please analyze your documents structure to detect this element and fix its size constraints.";
|
||||
|
||||
@@ -162,6 +163,50 @@ namespace QuestPDF.Drawing
|
||||
|
||||
return debuggingState;
|
||||
}
|
||||
|
||||
private static void ApplyContentRepeatState(Element? content, bool repeatContent)
|
||||
{
|
||||
if (content == null)
|
||||
return;
|
||||
|
||||
if (content is IVisual visual)
|
||||
visual.RepeatContent = repeatContent;
|
||||
|
||||
if (content is TextBlock textBlock)
|
||||
{
|
||||
foreach (var textBlockItem in textBlock.Items)
|
||||
{
|
||||
if (textBlockItem is TextBlockElement textElement)
|
||||
{
|
||||
ApplyContentRepeatState(textElement.Element, true);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: apply RepeatState in dynamic content
|
||||
//if (content is DynamicHost dynamicHost)
|
||||
// dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
if (content is RepeatContentSetter repeatContentSetter)
|
||||
repeatContent = repeatContentSetter.RepeatContent;
|
||||
|
||||
foreach (var child in content.GetChildren())
|
||||
ApplyContentRepeatState(child, repeatContent);
|
||||
}
|
||||
|
||||
private static void ResetIsRenderedState(Element? content)
|
||||
{
|
||||
if (content == null)
|
||||
return;
|
||||
|
||||
if (content is IVisual visual)
|
||||
visual.IsRendered = false;
|
||||
|
||||
foreach (var child in content.GetChildren())
|
||||
ResetIsRenderedState(child);
|
||||
}
|
||||
|
||||
internal static void ApplyDefaultTextStyle(this Element? content, TextStyle documentDefaultTextStyle)
|
||||
{
|
||||
@@ -174,7 +219,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)
|
||||
{
|
||||
@@ -186,18 +231,13 @@ namespace QuestPDF.Drawing
|
||||
}
|
||||
|
||||
if (content is DynamicHost dynamicHost)
|
||||
dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
var targetTextStyle = documentDefaultTextStyle;
|
||||
dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
if (content is DefaultTextStyle defaultTextStyleElement)
|
||||
{
|
||||
defaultTextStyleElement.TextStyle.ApplyParentStyle(documentDefaultTextStyle);
|
||||
targetTextStyle = defaultTextStyleElement.TextStyle;
|
||||
}
|
||||
|
||||
documentDefaultTextStyle = defaultTextStyleElement.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
foreach (var child in content.GetChildren())
|
||||
ApplyDefaultTextStyle(child, targetTextStyle);
|
||||
ApplyDefaultTextStyle(child, documentDefaultTextStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
@@ -18,14 +19,26 @@ namespace QuestPDF.Drawing
|
||||
public DateTime CreationDate { get; set; } = DateTime.Now;
|
||||
public DateTime ModifiedDate { get; set; } = DateTime.Now;
|
||||
|
||||
/// <summary>
|
||||
/// If the number of generated pages exceeds this threshold
|
||||
/// (likely due to infinite layout), the exception is thrown.
|
||||
/// </summary>
|
||||
public int DocumentLayoutExceptionThreshold { get; set; } = 250;
|
||||
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.DocumentLayoutExceptionThreshold static property.")]
|
||||
public int DocumentLayoutExceptionThreshold
|
||||
{
|
||||
get => Settings.DocumentLayoutExceptionThreshold;
|
||||
set => Settings.DocumentLayoutExceptionThreshold = value;
|
||||
}
|
||||
|
||||
public bool ApplyCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
|
||||
public bool ApplyDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
|
||||
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableCaching static property.")]
|
||||
public bool ApplyCaching
|
||||
{
|
||||
get => Settings.EnableCaching;
|
||||
set => Settings.EnableCaching = value;
|
||||
}
|
||||
|
||||
[Obsolete("This API has been moved since version 2022.9. Please use the QuestPDF.Settings.EnableDebugging static property.")]
|
||||
public bool ApplyDebugging
|
||||
{
|
||||
get => Settings.EnableDebugging;
|
||||
set => Settings.EnableDebugging = value;
|
||||
}
|
||||
|
||||
public static DocumentMetadata Default => new DocumentMetadata();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ namespace QuestPDF.Drawing.Exceptions
|
||||
{
|
||||
public class DocumentDrawingException : Exception
|
||||
{
|
||||
internal DocumentDrawingException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
public static class FontManager
|
||||
{
|
||||
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
|
||||
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
|
||||
private static ConcurrentDictionary<object, SKPaint> FontPaints = new();
|
||||
private static ConcurrentDictionary<string, SKPaint> ColorPaints = new();
|
||||
private static ConcurrentDictionary<object, Font> ShaperFonts = new();
|
||||
private static ConcurrentDictionary<object, SKFont> Fonts = new();
|
||||
private static ConcurrentDictionary<object, TextShaper> TextShapers = new();
|
||||
private static readonly ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, SKFontMetrics> FontMetrics = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, SKPaint> FontPaints = new();
|
||||
private static readonly ConcurrentDictionary<string, SKPaint> ColorPaints = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, Font> ShaperFonts = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, SKFont> Fonts = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, TextShaper> TextShapers = new();
|
||||
|
||||
static FontManager()
|
||||
{
|
||||
@@ -110,7 +110,7 @@ namespace QuestPDF.Drawing
|
||||
|
||||
internal static SKPaint ToPaint(this TextStyle style)
|
||||
{
|
||||
return FontPaints.GetOrAdd(style.PaintKey, key => Convert(style));
|
||||
return FontPaints.GetOrAdd(style, Convert);
|
||||
|
||||
static SKPaint Convert(TextStyle style)
|
||||
{
|
||||
@@ -172,14 +172,14 @@ namespace QuestPDF.Drawing
|
||||
|
||||
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
|
||||
{
|
||||
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
|
||||
return FontMetrics.GetOrAdd(style, key => key.NormalPosition().ToPaint().FontMetrics);
|
||||
}
|
||||
|
||||
internal static Font ToShaperFont(this TextStyle style)
|
||||
{
|
||||
return ShaperFonts.GetOrAdd(style.PaintKey, _ =>
|
||||
return ShaperFonts.GetOrAdd(style, key =>
|
||||
{
|
||||
var typeface = style.ToPaint().Typeface;
|
||||
var typeface = key.ToPaint().Typeface;
|
||||
|
||||
using var harfBuzzBlob = typeface.OpenStream(out var ttcIndex).ToHarfBuzzBlob();
|
||||
|
||||
@@ -200,12 +200,12 @@ namespace QuestPDF.Drawing
|
||||
|
||||
internal static TextShaper ToTextShaper(this TextStyle style)
|
||||
{
|
||||
return TextShapers.GetOrAdd(style.PaintKey, _ => new TextShaper(style));
|
||||
return TextShapers.GetOrAdd(style, key => new TextShaper(key));
|
||||
}
|
||||
|
||||
internal static SKFont ToFont(this TextStyle style)
|
||||
{
|
||||
return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont());
|
||||
return Fonts.GetOrAdd(style, key => key.ToPaint().ToFont());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace QuestPDF.Drawing
|
||||
{
|
||||
internal struct TextMeasurement
|
||||
{
|
||||
public int LineIndex { get; set; }
|
||||
public float FragmentWidth { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using HarfBuzzSharp;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
@@ -9,14 +10,16 @@ namespace QuestPDF.Drawing
|
||||
internal class TextShaper
|
||||
{
|
||||
public const int FontShapingScale = 512;
|
||||
|
||||
private Font Font { get; }
|
||||
private SKPaint Paint { get; }
|
||||
|
||||
public TextShaper(TextStyle style)
|
||||
private TextStyle TextStyle { get; }
|
||||
|
||||
private SKFont Font => TextStyle.ToFont();
|
||||
private Font ShaperFont => TextStyle.ToShaperFont();
|
||||
private SKPaint Paint => TextStyle.ToPaint();
|
||||
|
||||
public TextShaper(TextStyle textStyle)
|
||||
{
|
||||
Font = style.ToShaperFont();
|
||||
Paint = style.ToPaint();
|
||||
TextStyle = textStyle;
|
||||
}
|
||||
|
||||
public TextShapingResult Shape(string text)
|
||||
@@ -26,7 +29,7 @@ namespace QuestPDF.Drawing
|
||||
PopulateBufferWithText(buffer, text);
|
||||
buffer.GuessSegmentProperties();
|
||||
|
||||
Font.Shape(buffer);
|
||||
ShaperFont.Shape(buffer);
|
||||
|
||||
var length = buffer.Length;
|
||||
var glyphInfos = buffer.GlyphInfos;
|
||||
|
||||
@@ -7,19 +7,28 @@ namespace QuestPDF.Elements
|
||||
{
|
||||
public delegate void DrawOnCanvas(SKCanvas canvas, Size availableSpace);
|
||||
|
||||
internal class Canvas : Element, ICacheable
|
||||
internal class Canvas : Element, IVisual, ICacheable
|
||||
{
|
||||
public bool IsRendered { get; set; }
|
||||
public bool RepeatContent { get; set; }
|
||||
|
||||
public DrawOnCanvas Handler { get; set; }
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
return availableSpace.IsNegative()
|
||||
? SpacePlan.Wrap()
|
||||
: SpacePlan.FullRender(availableSpace);
|
||||
if (availableSpace.IsNegative())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
if (IsRendered && !RepeatContent)
|
||||
return SpacePlan.FullRender(Size.Zero);
|
||||
|
||||
return SpacePlan.FullRender(availableSpace);
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
IsRendered = true;
|
||||
|
||||
var skiaCanvas = (Canvas as Drawing.SkiaCanvasBase)?.Canvas;
|
||||
|
||||
if (Handler == null || skiaCanvas == null)
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace QuestPDF.Elements
|
||||
private DynamicComponentProxy Child { get; }
|
||||
private object InitialComponentState { get; set; }
|
||||
|
||||
internal TextStyle TextStyle { get; } = new();
|
||||
internal TextStyle TextStyle { get; set; } = TextStyle.Default;
|
||||
|
||||
public DynamicHost(DynamicComponentProxy child)
|
||||
{
|
||||
|
||||
@@ -6,31 +6,38 @@ using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
internal class DynamicImage : Element
|
||||
internal class DynamicImage : Element, IVisual
|
||||
{
|
||||
public bool IsRendered { get; set; }
|
||||
public bool RepeatContent { get; set; }
|
||||
|
||||
public Func<Size, byte[]>? Source { get; set; }
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
return availableSpace.IsNegative()
|
||||
? SpacePlan.Wrap()
|
||||
: SpacePlan.FullRender(availableSpace);
|
||||
if (availableSpace.IsNegative())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
if (IsRendered && !RepeatContent)
|
||||
return SpacePlan.FullRender(Size.Zero);
|
||||
|
||||
return SpacePlan.FullRender(availableSpace);
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
IsRendered = true;
|
||||
|
||||
if (availableSpace.Width < Size.Epsilon || availableSpace.Height < Size.Epsilon)
|
||||
return;
|
||||
|
||||
var imageData = Source?.Invoke(availableSpace);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,11 @@ using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
internal class Image : Element, ICacheable
|
||||
internal class Image : Element, IVisual, ICacheable
|
||||
{
|
||||
public bool IsRendered { get; set; }
|
||||
public bool RepeatContent { get; set; }
|
||||
|
||||
public SKImage? InternalImage { get; set; }
|
||||
|
||||
~Image()
|
||||
@@ -16,9 +19,13 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
return availableSpace.IsNegative()
|
||||
? SpacePlan.Wrap()
|
||||
: SpacePlan.FullRender(availableSpace);
|
||||
if (availableSpace.IsNegative())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
if (IsRendered && !RepeatContent)
|
||||
return SpacePlan.FullRender(Size.Zero);
|
||||
|
||||
return SpacePlan.FullRender(availableSpace);
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
@@ -26,6 +33,7 @@ namespace QuestPDF.Elements
|
||||
if (InternalImage == null)
|
||||
return;
|
||||
|
||||
IsRendered = true;
|
||||
Canvas.DrawImage(InternalImage, Position.Zero, availableSpace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +212,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;
|
||||
|
||||
@@ -15,34 +15,42 @@ namespace QuestPDF.Elements
|
||||
Horizontal
|
||||
}
|
||||
|
||||
internal class Line : Element, ILine, ICacheable
|
||||
internal class Line : Element, ILine, IVisual, ICacheable
|
||||
{
|
||||
public bool IsRendered { get; set; }
|
||||
public bool RepeatContent { get; set; }
|
||||
|
||||
public LineType Type { get; set; } = LineType.Vertical;
|
||||
public string Color { get; set; } = Colors.Black;
|
||||
public float Size { get; set; } = 1;
|
||||
public float Thickness { get; set; } = 1;
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
if (availableSpace.IsNegative())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
if (IsRendered && !RepeatContent)
|
||||
return SpacePlan.FullRender(Size.Zero);
|
||||
|
||||
return Type switch
|
||||
{
|
||||
LineType.Vertical when availableSpace.Width + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(Size, 0),
|
||||
LineType.Horizontal when availableSpace.Height + Infrastructure.Size.Epsilon >= Size => SpacePlan.FullRender(0, Size),
|
||||
LineType.Vertical when availableSpace.Width + Infrastructure.Size.Epsilon >= Thickness => SpacePlan.FullRender(Thickness, 0),
|
||||
LineType.Horizontal when availableSpace.Height + Infrastructure.Size.Epsilon >= Thickness => SpacePlan.FullRender(0, Thickness),
|
||||
_ => SpacePlan.Wrap()
|
||||
};
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
IsRendered = true;
|
||||
|
||||
if (Type == LineType.Vertical)
|
||||
{
|
||||
Canvas.DrawRectangle(new Position(-Size/2, 0), new Size(Size, availableSpace.Height), Color);
|
||||
Canvas.DrawRectangle(new Position(-Thickness/2, 0), new Size(Thickness, availableSpace.Height), Color);
|
||||
}
|
||||
else if (Type == LineType.Horizontal)
|
||||
{
|
||||
Canvas.DrawRectangle(new Position(0, -Size/2), new Size(availableSpace.Width, Size), Color);
|
||||
Canvas.DrawRectangle(new Position(0, -Thickness/2), new Size(availableSpace.Width, Thickness), Color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
{
|
||||
internal class RepeatContentSetter : ContainerElement
|
||||
{
|
||||
public bool RepeatContent { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ 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)
|
||||
{
|
||||
@@ -56,6 +57,7 @@ namespace QuestPDF.Elements.Table
|
||||
if (Cells.Count == 0)
|
||||
{
|
||||
MaxRow = 0;
|
||||
MaxRowSpan = 1;
|
||||
CellsCache = Array.Empty<TableCell[]>();
|
||||
|
||||
return;
|
||||
@@ -66,6 +68,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)
|
||||
@@ -199,9 +202,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 +221,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);
|
||||
|
||||
@@ -19,6 +19,5 @@ namespace QuestPDF.Elements.Text.Calculation
|
||||
public int TotalIndex { get; set; }
|
||||
|
||||
public bool IsLast => EndIndex == TotalIndex;
|
||||
public bool IsNewLine { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Elements.Text.Items;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Elements.Text
|
||||
{
|
||||
internal static class FontFallback
|
||||
{
|
||||
public struct TextRun
|
||||
{
|
||||
public string Content { get; set; }
|
||||
public TextStyle Style { get; set; }
|
||||
}
|
||||
|
||||
public class FallbackOption
|
||||
{
|
||||
public TextStyle Style { get; set; }
|
||||
public SKFont Font { get; set; }
|
||||
public SKTypeface Typeface { get; set; }
|
||||
}
|
||||
|
||||
private static SKFontManager FontManager => SKFontManager.Default;
|
||||
|
||||
public static IEnumerable<TextRun> SplitWithFontFallback(this string text, TextStyle textStyle)
|
||||
{
|
||||
var fallbackOptions = GetFallbackOptions(textStyle).ToArray();
|
||||
|
||||
var spanStartIndex = 0;
|
||||
var spanFallbackOption = fallbackOptions[0];
|
||||
|
||||
for (var i = 0; i < text.Length; i += char.IsSurrogatePair(text, i) ? 2 : 1)
|
||||
{
|
||||
var codepoint = char.ConvertToUtf32(text, i);
|
||||
var newFallbackOption = MatchFallbackOption(fallbackOptions, codepoint);
|
||||
|
||||
if (newFallbackOption == spanFallbackOption)
|
||||
continue;
|
||||
|
||||
yield return new TextRun
|
||||
{
|
||||
Content = text.Substring(spanStartIndex, i - spanStartIndex),
|
||||
Style = spanFallbackOption.Style
|
||||
};
|
||||
|
||||
spanStartIndex = i;
|
||||
spanFallbackOption = newFallbackOption;
|
||||
}
|
||||
|
||||
if (spanStartIndex > text.Length)
|
||||
yield break;
|
||||
|
||||
yield return new TextRun
|
||||
{
|
||||
Content = text.Substring(spanStartIndex, text.Length - spanStartIndex),
|
||||
Style = spanFallbackOption.Style
|
||||
};
|
||||
|
||||
static IEnumerable<FallbackOption> GetFallbackOptions(TextStyle? textStyle)
|
||||
{
|
||||
while (textStyle != null)
|
||||
{
|
||||
var font = textStyle.ToFont();
|
||||
|
||||
yield return new FallbackOption
|
||||
{
|
||||
Style = textStyle,
|
||||
Font = font,
|
||||
Typeface = font.Typeface
|
||||
};
|
||||
|
||||
textStyle = textStyle.Fallback;
|
||||
}
|
||||
}
|
||||
|
||||
static FallbackOption MatchFallbackOption(ICollection<FallbackOption> fallbackOptions, int codepoint)
|
||||
{
|
||||
foreach (var fallbackOption in fallbackOptions)
|
||||
{
|
||||
if (fallbackOption.Font.ContainsGlyph(codepoint))
|
||||
return fallbackOption;
|
||||
}
|
||||
|
||||
throw CreateNotMatchingFontException(codepoint);
|
||||
}
|
||||
|
||||
static Exception CreateNotMatchingFontException(int codepoint)
|
||||
{
|
||||
var character = char.ConvertFromUtf32(codepoint);
|
||||
var unicode = $"U-{codepoint:X4}";
|
||||
|
||||
var proposedFonts = FindFontsContainingGlyph(codepoint);
|
||||
var proposedFontsFormatted = proposedFonts.Any() ? string.Join(", ", proposedFonts) : "no fonts available";
|
||||
|
||||
return new DocumentDrawingException(
|
||||
$"Could not find an appropriate font fallback for glyph: {unicode} '{character}'. " +
|
||||
$"Font families available on current environment that contain this glyph: {proposedFontsFormatted}. " +
|
||||
$"Possible solutions: " +
|
||||
$"1) Use one of the listed fonts as the primary font in your document. " +
|
||||
$"2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. ");
|
||||
}
|
||||
|
||||
static IEnumerable<string> FindFontsContainingGlyph(int codepoint)
|
||||
{
|
||||
var fontManager = SKFontManager.Default;
|
||||
|
||||
return fontManager
|
||||
.GetFontFamilies()
|
||||
.Select(fontManager.MatchFamily)
|
||||
.Where(x => x.ContainsGlyph(codepoint))
|
||||
.Select(x => x.FamilyName);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<ITextBlockItem> ApplyFontFallback(this ICollection<ITextBlockItem> textBlockItems)
|
||||
{
|
||||
foreach (var textBlockItem in textBlockItems)
|
||||
{
|
||||
if (textBlockItem is TextBlockPageNumber or TextBlockElement)
|
||||
{
|
||||
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)
|
||||
{
|
||||
var newElement = textBlockSpan switch
|
||||
{
|
||||
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
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ namespace QuestPDF.Elements.Text.Items
|
||||
internal class TextBlockSpan : ITextBlockItem
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public TextStyle Style { get; set; } = new();
|
||||
public TextShapingResult? TextShapingResult { get; set; }
|
||||
public TextStyle Style { get; set; } = TextStyle.Default;
|
||||
private TextShapingResult? TextShapingResult { get; set; }
|
||||
|
||||
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
|
||||
protected virtual bool EnableTextCache => true;
|
||||
@@ -39,34 +39,18 @@ namespace QuestPDF.Elements.Text.Items
|
||||
|
||||
var paint = Style.ToPaint();
|
||||
var fontMetrics = Style.ToFontMetrics();
|
||||
var spaceCodepoint = paint.ToFont().Typeface.GetGlyphs(" ")[0];
|
||||
|
||||
// if the element is the first one within the line,
|
||||
// ignore leading spaces and new lines
|
||||
var spaceCodepoint = paint.ToFont().Typeface.GetGlyph(' ');
|
||||
var newLineCodepoint = paint.ToFont().Typeface.GetGlyph('\n');
|
||||
var returnCodepoint = paint.ToFont().Typeface.GetGlyph('\r');
|
||||
|
||||
var startIndex = request.StartIndex;
|
||||
|
||||
// if the element is the first one within the line,
|
||||
// ignore leading spaces
|
||||
if (!request.IsFirstElementInBlock && request.IsFirstElementInLine)
|
||||
{
|
||||
while (startIndex < TextShapingResult.Glyphs.Length && (Text[startIndex] == spaceCodepoint || Text[startIndex] == newLineCodepoint || Text[startIndex] == returnCodepoint))
|
||||
while (startIndex < TextShapingResult.Glyphs.Length && Text[startIndex] == spaceCodepoint)
|
||||
startIndex++;
|
||||
}
|
||||
|
||||
// calculate max index (new new line)
|
||||
var newLineIndex = startIndex;
|
||||
|
||||
while (newLineIndex < TextShapingResult.Glyphs.Length)
|
||||
{
|
||||
var glyphCodepoint = TextShapingResult.Glyphs[newLineIndex].Codepoint;
|
||||
|
||||
if (glyphCodepoint == newLineCodepoint || glyphCodepoint == returnCodepoint)
|
||||
break;
|
||||
|
||||
newLineIndex++;
|
||||
}
|
||||
|
||||
if (TextShapingResult.Glyphs.Length == 0 || startIndex == TextShapingResult.Glyphs.Length)
|
||||
{
|
||||
return new TextMeasurementResult
|
||||
@@ -84,8 +68,6 @@ namespace QuestPDF.Elements.Text.Items
|
||||
|
||||
if (endIndex < startIndex)
|
||||
return null;
|
||||
|
||||
endIndex = Math.Min(endIndex, newLineIndex);
|
||||
|
||||
// break text only on spaces
|
||||
var wrappedText = WrapText(startIndex, endIndex, request.IsFirstElementInLine);
|
||||
@@ -108,9 +90,7 @@ namespace QuestPDF.Elements.Text.Items
|
||||
StartIndex = startIndex,
|
||||
EndIndex = wrappedText.Value.endIndex,
|
||||
NextIndex = wrappedText.Value.nextIndex,
|
||||
TotalIndex = TextShapingResult.Glyphs.Length - 1,
|
||||
|
||||
IsNewLine = endIndex == newLineIndex
|
||||
TotalIndex = TextShapingResult.Glyphs.Length - 1
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,23 +4,29 @@ using System.Linq;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Elements.Text.Calculation;
|
||||
using QuestPDF.Elements.Text.Items;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements.Text
|
||||
{
|
||||
internal class TextBlock : Element, IStateResettable
|
||||
internal class TextBlock : Element, IVisual, IStateResettable
|
||||
{
|
||||
public bool IsRendered { get; set; }
|
||||
public bool RepeatContent { get; set; }
|
||||
|
||||
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
||||
public List<ITextBlockItem> Items { get; set; } = new();
|
||||
public float ParagraphSpacing { get; set; } = 0;
|
||||
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));
|
||||
|
||||
private Queue<ITextBlockItem> RenderingQueue { get; set; }
|
||||
private int CurrentElementIndex { get; set; }
|
||||
|
||||
private bool FontFallbackApplied { get; set; } = false;
|
||||
|
||||
public void ResetState()
|
||||
{
|
||||
ApplyFontFallback();
|
||||
InitializeQueue();
|
||||
CurrentElementIndex = 0;
|
||||
|
||||
@@ -38,10 +44,25 @@ namespace QuestPDF.Elements.Text
|
||||
foreach (var item in Items)
|
||||
RenderingQueue.Enqueue(item);
|
||||
}
|
||||
|
||||
void ApplyFontFallback()
|
||||
{
|
||||
if (FontFallbackApplied)
|
||||
return;
|
||||
|
||||
Items = Items.ApplyFontFallback().ToList();
|
||||
FontFallbackApplied = true;
|
||||
}
|
||||
}
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
if (availableSpace.IsNegative())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
if (IsRendered && !RepeatContent)
|
||||
return SpacePlan.FullRender(Size.Zero);
|
||||
|
||||
if (!RenderingQueue.Any())
|
||||
return SpacePlan.FullRender(Size.Zero);
|
||||
|
||||
@@ -69,6 +90,9 @@ namespace QuestPDF.Elements.Text
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
{
|
||||
if (IsRendered && !RepeatContent)
|
||||
return;
|
||||
|
||||
var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
|
||||
|
||||
if (!lines.Any())
|
||||
@@ -111,12 +135,6 @@ namespace QuestPDF.Elements.Text
|
||||
Canvas.Translate(new Position(0, line.LineHeight));
|
||||
|
||||
heightOffset += line.LineHeight;
|
||||
|
||||
if (line.Elements.Last().Measurement.IsNewLine)
|
||||
{
|
||||
heightOffset += ParagraphSpacing;
|
||||
Canvas.Translate(new Position(0, ParagraphSpacing));
|
||||
}
|
||||
}
|
||||
|
||||
Canvas.Translate(new Position(0, -heightOffset));
|
||||
@@ -131,10 +149,13 @@ namespace QuestPDF.Elements.Text
|
||||
|
||||
var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
|
||||
CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
|
||||
|
||||
|
||||
if (!RenderingQueue.Any())
|
||||
{
|
||||
ResetState();
|
||||
|
||||
IsRendered = true;
|
||||
}
|
||||
|
||||
float GetAlignmentOffset(float lineWidth)
|
||||
{
|
||||
if (Alignment == HorizontalAlignment.Left)
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
var container = new Container();
|
||||
Decoration.Before = container;
|
||||
return container;
|
||||
return container.RepeatContentWhenPaging();
|
||||
}
|
||||
|
||||
public void Before(Action<IContainer> handler)
|
||||
@@ -36,7 +36,7 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
var container = new Container();
|
||||
Decoration.After = container;
|
||||
return container;
|
||||
return container.RepeatContentWhenPaging();
|
||||
}
|
||||
|
||||
public void After(Action<IContainer> handler)
|
||||
@@ -49,9 +49,7 @@ namespace QuestPDF.Fluent
|
||||
[Obsolete("This element has been renamed since version 2022.2. Please use the 'Before' method.")]
|
||||
public IContainer Header()
|
||||
{
|
||||
var container = new Container();
|
||||
Decoration.Before = container;
|
||||
return container;
|
||||
return Before();
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.2. Please use the 'Before' method.")]
|
||||
@@ -63,9 +61,7 @@ namespace QuestPDF.Fluent
|
||||
[Obsolete("This element has been renamed since version 2022.2. Please use the 'After' method.")]
|
||||
public IContainer Footer()
|
||||
{
|
||||
var container = new Container();
|
||||
Decoration.After = container;
|
||||
return container;
|
||||
return After();
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.2. Please use the 'After' method.")]
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace QuestPDF.Fluent
|
||||
return container;
|
||||
}
|
||||
|
||||
public IContainer Layer() => Layer(false);
|
||||
public IContainer Layer() => Layer(false).RepeatContentWhenPaging();
|
||||
public IContainer PrimaryLayer() => Layer(true);
|
||||
|
||||
internal void Validate()
|
||||
|
||||
@@ -6,11 +6,11 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
public static class LineExtensions
|
||||
{
|
||||
private static ILine Line(this IContainer element, LineType type, float size)
|
||||
private static ILine Line(this IContainer element, LineType type, float thickness)
|
||||
{
|
||||
var line = new Line
|
||||
{
|
||||
Size = size,
|
||||
Thickness = thickness,
|
||||
Type = type
|
||||
};
|
||||
|
||||
@@ -18,14 +18,14 @@ namespace QuestPDF.Fluent
|
||||
return line;
|
||||
}
|
||||
|
||||
public static ILine LineVertical(this IContainer element, float size, Unit unit = Unit.Point)
|
||||
public static ILine LineVertical(this IContainer element, float thickness, Unit unit = Unit.Point)
|
||||
{
|
||||
return element.Line(LineType.Vertical, size.ToPoints(unit));
|
||||
return element.Line(LineType.Vertical, thickness.ToPoints(unit));
|
||||
}
|
||||
|
||||
public static ILine LineHorizontal(this IContainer element, float size, Unit unit = Unit.Point)
|
||||
public static ILine LineHorizontal(this IContainer element, float thickness, Unit unit = Unit.Point)
|
||||
{
|
||||
return element.Line(LineType.Horizontal, size.ToPoints(unit));
|
||||
return element.Line(LineType.Horizontal, thickness.ToPoints(unit));
|
||||
}
|
||||
|
||||
public static void LineColor(this ILine descriptor, string value)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using QuestPDF.Elements;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
public static class RepeatContentExtensions
|
||||
{
|
||||
private static IContainer RepeatContent(this IContainer element, Action<RepeatContentSetter> handler)
|
||||
{
|
||||
var repeatContentSetter = element as RepeatContentSetter ?? new RepeatContentSetter();
|
||||
handler(repeatContentSetter);
|
||||
|
||||
return element.Element(repeatContentSetter);
|
||||
}
|
||||
|
||||
public static IContainer RepeatContentWhenPaging(this IContainer element)
|
||||
{
|
||||
return element.RepeatContent(x => x.RepeatContent = true);
|
||||
}
|
||||
|
||||
public static IContainer DoNotRepeatContentWhenPaging(this IContainer element)
|
||||
{
|
||||
return element.RepeatContent(x => x.RepeatContent = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,25 +31,27 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
public class TextDescriptor
|
||||
{
|
||||
internal TextBlock TextBlock = new();
|
||||
private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
|
||||
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
|
||||
private TextStyle? DefaultStyle { get; set; }
|
||||
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
||||
private float Spacing { get; set; } = 0f;
|
||||
|
||||
public void DefaultTextStyle(TextStyle style)
|
||||
{
|
||||
@@ -71,9 +80,17 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public void ParagraphSpacing(float value, Unit unit = Unit.Point)
|
||||
{
|
||||
TextBlock.ParagraphSpacing = value.ToPoints(unit);
|
||||
Spacing = value.ToPoints(unit);
|
||||
}
|
||||
|
||||
private void AddItemToLastTextBlock(ITextBlockItem item)
|
||||
{
|
||||
if (!TextBlocks.Any())
|
||||
TextBlocks.Add(new TextBlock());
|
||||
|
||||
TextBlocks.Last().Items.Add(item);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
|
||||
public void Span(string? text, TextStyle style)
|
||||
{
|
||||
@@ -82,19 +99,30 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public TextSpanDescriptor Span(string? text)
|
||||
{
|
||||
var style = DefaultStyle.Clone();
|
||||
var descriptor = new TextSpanDescriptor(style);
|
||||
|
||||
if (text == null)
|
||||
return descriptor;
|
||||
|
||||
TextBlock.Items.Add(new TextBlockSpan
|
||||
{
|
||||
Text = text,
|
||||
Style = style
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
return new TextSpanDescriptor(_ => { });
|
||||
|
||||
var items = text
|
||||
.Replace("\r", string.Empty)
|
||||
.Split(new[] { '\n' }, StringSplitOptions.None)
|
||||
.Select(x => new TextBlockSpan
|
||||
{
|
||||
Text = x
|
||||
})
|
||||
.ToList();
|
||||
|
||||
AddItemToLastTextBlock(items.First());
|
||||
|
||||
items
|
||||
.Skip(1)
|
||||
.Select(x => new TextBlock
|
||||
{
|
||||
Items = new List<ITextBlockItem> { x }
|
||||
})
|
||||
.ToList()
|
||||
.ForEach(TextBlocks.Add);
|
||||
|
||||
return new TextSpanDescriptor(x => items.ForEach(y => y.Style = x));
|
||||
}
|
||||
|
||||
public TextSpanDescriptor Line(string? text)
|
||||
@@ -110,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);
|
||||
|
||||
TextBlock.Items.Add(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()
|
||||
@@ -163,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;
|
||||
|
||||
TextBlock.Items.Add(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.")]
|
||||
@@ -190,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(_ => { });
|
||||
|
||||
TextBlock.Items.Add(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.")]
|
||||
@@ -216,13 +232,35 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
var container = new Container();
|
||||
|
||||
TextBlock.Items.Add(new TextBlockElement
|
||||
AddItemToLastTextBlock(new TextBlockElement
|
||||
{
|
||||
Element = container
|
||||
});
|
||||
|
||||
return container.AlignBottom().MinimalBox();
|
||||
}
|
||||
|
||||
internal void Compose(IContainer container)
|
||||
{
|
||||
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
|
||||
|
||||
if (DefaultStyle != null)
|
||||
container = container.DefaultTextStyle(DefaultStyle);
|
||||
|
||||
if (TextBlocks.Count == 1)
|
||||
{
|
||||
container.Element(TextBlocks.First());
|
||||
return;
|
||||
}
|
||||
|
||||
container.Column(column =>
|
||||
{
|
||||
column.Spacing(Spacing);
|
||||
|
||||
foreach (var textBlock in TextBlocks)
|
||||
column.Item().Element(textBlock);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static class TextExtensions
|
||||
@@ -235,7 +273,7 @@ namespace QuestPDF.Fluent
|
||||
descriptor.Alignment = alignment.Horizontal;
|
||||
|
||||
content?.Invoke(descriptor);
|
||||
element.Element(descriptor.TextBlock);
|
||||
descriptor.Compose(element);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
|
||||
|
||||
@@ -11,120 +11,135 @@ namespace QuestPDF.Fluent
|
||||
if (style == null)
|
||||
return descriptor;
|
||||
|
||||
descriptor.TextStyle.OverrideStyle(style);
|
||||
descriptor.MutateTextStyle(x => x.OverrideStyle(style));
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Fallback<T>(this T descriptor, TextStyle? value = null) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.Fallback = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Fallback<T>(this T descriptor, Func<TextStyle, TextStyle> handler) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Fallback(handler(TextStyle.Default));
|
||||
}
|
||||
|
||||
public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||
{
|
||||
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
|
||||
@@ -132,24 +147,22 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -6,14 +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;
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")]
|
||||
public static TextStyle Color(this TextStyle style, string value)
|
||||
{
|
||||
@@ -22,12 +14,12 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public static TextStyle FontColor(this TextStyle style, string value)
|
||||
{
|
||||
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.")]
|
||||
@@ -38,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.")]
|
||||
@@ -49,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)
|
||||
@@ -137,6 +129,7 @@ namespace QuestPDF.Fluent
|
||||
#endregion
|
||||
|
||||
#region Position
|
||||
|
||||
public static TextStyle NormalPosition(this TextStyle style)
|
||||
{
|
||||
return style.Position(FontPosition.Normal);
|
||||
@@ -154,11 +147,23 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
internal interface IVisual
|
||||
{
|
||||
public bool IsRendered { get; set; }
|
||||
public bool RepeatContent { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,8 @@ 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,13 +17,9 @@ namespace QuestPDF.Infrastructure
|
||||
internal bool? HasUnderline { get; set; }
|
||||
internal bool? WrapAnywhere { get; set; }
|
||||
|
||||
internal object PaintKey { get; private set; }
|
||||
internal object FontMetricsKey { get; private set; }
|
||||
|
||||
// REVIEW: Should this be a method call that news up a TextStyle,
|
||||
// or can it be a static variable?
|
||||
// (style mutations seem to create a clone anyway)
|
||||
internal static readonly TextStyle LibraryDefault = new TextStyle
|
||||
internal TextStyle? Fallback { get; set; }
|
||||
|
||||
internal static TextStyle LibraryDefault { get; } = new()
|
||||
{
|
||||
Color = Colors.Black,
|
||||
BackgroundColor = Colors.Transparent,
|
||||
@@ -37,59 +31,10 @@ namespace QuestPDF.Infrastructure
|
||||
IsItalic = false,
|
||||
HasStrikethrough = false,
|
||||
HasUnderline = false,
|
||||
WrapAnywhere = false
|
||||
WrapAnywhere = false,
|
||||
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);
|
||||
PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
|
||||
FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic);
|
||||
}
|
||||
|
||||
internal void ApplyParentStyle(TextStyle parentStyle)
|
||||
{
|
||||
Color ??= parentStyle.Color;
|
||||
BackgroundColor ??= parentStyle.BackgroundColor;
|
||||
FontFamily ??= parentStyle.FontFamily;
|
||||
Size ??= parentStyle.Size;
|
||||
LineHeight ??= parentStyle.LineHeight;
|
||||
FontWeight ??= parentStyle.FontWeight;
|
||||
FontPosition ??= parentStyle.FontPosition;
|
||||
IsItalic ??= parentStyle.IsItalic;
|
||||
HasStrikethrough ??= parentStyle.HasStrikethrough;
|
||||
HasUnderline ??= parentStyle.HasUnderline;
|
||||
WrapAnywhere ??= parentStyle.WrapAnywhere;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
internal TextStyle Clone()
|
||||
{
|
||||
var clone = (TextStyle)MemberwiseClone();
|
||||
clone.HasGlobalStyleApplied = false;
|
||||
return clone;
|
||||
}
|
||||
public static TextStyle Default { get; } = new();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using QuestPDF.Fluent;
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
internal enum TextStyleProperty
|
||||
{
|
||||
Color,
|
||||
BackgroundColor,
|
||||
FontFamily,
|
||||
Size,
|
||||
LineHeight,
|
||||
FontWeight,
|
||||
FontPosition,
|
||||
IsItalic,
|
||||
HasStrikethrough,
|
||||
HasUnderline,
|
||||
WrapAnywhere,
|
||||
Fallback
|
||||
}
|
||||
|
||||
internal static class TextStyleManager
|
||||
{
|
||||
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new();
|
||||
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleApplyGlobalCache = new();
|
||||
private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleOverrideCache = new();
|
||||
|
||||
public static TextStyle Mutate(this TextStyle origin, TextStyleProperty property, object value)
|
||||
{
|
||||
var cacheKey = (origin, property, value);
|
||||
return TextStyleMutateCache.GetOrAdd(cacheKey, x => MutateStyle(x.origin, x.property, x.value));
|
||||
}
|
||||
|
||||
private static TextStyle MutateStyle(TextStyle origin, TextStyleProperty property, object? value, bool overrideValue = true)
|
||||
{
|
||||
if (overrideValue && value == null)
|
||||
return origin;
|
||||
|
||||
if (property == TextStyleProperty.Color)
|
||||
{
|
||||
if (!overrideValue && origin.Color != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (string?)value;
|
||||
|
||||
if (origin.Color == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { Color = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.BackgroundColor)
|
||||
{
|
||||
if (!overrideValue && origin.BackgroundColor != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (string?)value;
|
||||
|
||||
if (origin.BackgroundColor == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { BackgroundColor = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.FontFamily)
|
||||
{
|
||||
if (!overrideValue && origin.FontFamily != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (string?)value;
|
||||
|
||||
if (origin.FontFamily == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { FontFamily = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.Size)
|
||||
{
|
||||
if (!overrideValue && origin.Size != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (float?)value;
|
||||
|
||||
if (origin.Size == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { Size = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.LineHeight)
|
||||
{
|
||||
if (!overrideValue && origin.LineHeight != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (float?)value;
|
||||
|
||||
if (origin.LineHeight == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { LineHeight = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.FontWeight)
|
||||
{
|
||||
if (!overrideValue && origin.FontWeight != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (FontWeight?)value;
|
||||
|
||||
if (origin.FontWeight == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { FontWeight = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.FontPosition)
|
||||
{
|
||||
if (!overrideValue && origin.FontPosition != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (FontPosition?)value;
|
||||
|
||||
if (origin.FontPosition == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { FontPosition = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.IsItalic)
|
||||
{
|
||||
if (!overrideValue && origin.IsItalic != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (bool?)value;
|
||||
|
||||
if (origin.IsItalic == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { IsItalic = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.HasStrikethrough)
|
||||
{
|
||||
if (!overrideValue && origin.HasStrikethrough != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (bool?)value;
|
||||
|
||||
if (origin.HasStrikethrough == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { HasStrikethrough = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.HasUnderline)
|
||||
{
|
||||
if (!overrideValue && origin.HasUnderline != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (bool?)value;
|
||||
|
||||
if (origin.HasUnderline == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { HasUnderline = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.WrapAnywhere)
|
||||
{
|
||||
if (!overrideValue && origin.WrapAnywhere != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (bool?)value;
|
||||
|
||||
if (origin.WrapAnywhere == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { WrapAnywhere = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.Fallback)
|
||||
{
|
||||
if (!overrideValue && origin.Fallback != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (TextStyle?)value;
|
||||
|
||||
if (origin.Fallback == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { Fallback = castedValue };
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(property), property, "Expected to mutate the TextStyle object. Provided property type is not supported.");
|
||||
}
|
||||
|
||||
internal static TextStyle ApplyGlobalStyle(this TextStyle style, TextStyle parent)
|
||||
{
|
||||
var cacheKey = (style, parent);
|
||||
return TextStyleApplyGlobalCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, overrideStyle: false).ApplyFontFallback());
|
||||
}
|
||||
|
||||
private static TextStyle ApplyFontFallback(this TextStyle style)
|
||||
{
|
||||
var targetFallbackStyle = style
|
||||
?.Fallback
|
||||
?.ApplyStyle(style, overrideStyle: false, applyFallback: false)
|
||||
?.ApplyFontFallback();
|
||||
|
||||
return MutateStyle(style, TextStyleProperty.Fallback, targetFallbackStyle);
|
||||
}
|
||||
|
||||
internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
|
||||
{
|
||||
var cacheKey = (style, parent);
|
||||
|
||||
return TextStyleOverrideCache.GetOrAdd(cacheKey, key =>
|
||||
{
|
||||
var result = ApplyStyle(key.origin, key.parent);
|
||||
return MutateStyle(result, TextStyleProperty.Fallback, key.parent.Fallback);
|
||||
});
|
||||
}
|
||||
|
||||
private static TextStyle ApplyStyle(this TextStyle style, TextStyle parent, bool overrideStyle = true, bool applyFallback = true)
|
||||
{
|
||||
var result = style;
|
||||
|
||||
result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.HasStrikethrough, parent.HasStrikethrough, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideStyle);
|
||||
result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideStyle);
|
||||
|
||||
if (applyFallback)
|
||||
result = MutateStyle(result, TextStyleProperty.Fallback, parent.Fallback, overrideStyle);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = 9;
|
||||
|
||||
public PreviewerService(int port)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<Authors>MarcinZiabek</Authors>
|
||||
<Company>CodeFlint</Company>
|
||||
<PackageId>QuestPDF</PackageId>
|
||||
<Version>2202.8.2</Version>
|
||||
<Version>2022.9.1</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,20 +1,11 @@
|
||||
2022.8.0:
|
||||
2022.9.0
|
||||
- Implemented font-fallback algorithm,
|
||||
- Introduced new Settings API,
|
||||
- Significantly reduced memory allocation cost for TextStyle objects,
|
||||
- Implemented optional checking if all font glyphs are available,
|
||||
- Minor text-rendering optimizations.
|
||||
|
||||
- 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
|
||||
|
||||
2202.8.2
|
||||
- Fixed: the Column element incorrectly renders zero-height elements.
|
||||
2022.9.1
|
||||
- Fixed: text hyperlinks do not work when the CheckIfAllTextGlyphsAreAvailable option or text fallback are used,
|
||||
- Fixed: cells with RowSpan (greater than 1) are not always displayed properly,
|
||||
- Improved predictability of the Inlined element when measuring its children.
|
||||
@@ -0,0 +1,40 @@
|
||||
namespace QuestPDF
|
||||
{
|
||||
public static class Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// If your content requires generating longer documents, please assign the most reasonable value.
|
||||
/// </summary>
|
||||
public static int DocumentLayoutExceptionThreshold { get; set; } = 250;
|
||||
|
||||
/// <summary>
|
||||
/// This flag generates additional document elements to cache layout calculation results.
|
||||
/// In the vast majority of cases, this significantly improves performance, while slightly increasing memory consumption.
|
||||
/// </summary>
|
||||
/// <remarks>By default, this flag is enabled only when the debugger is NOT attached.</remarks>
|
||||
public static bool EnableCaching { get; set; } = !System.Diagnostics.Debugger.IsAttached;
|
||||
|
||||
/// <summary>
|
||||
/// This flag generates additional document elements to improve layout debugging experience.
|
||||
/// When the DocumentLayoutException is thrown, the library is able to provide additional execution context.
|
||||
/// It includes layout calculation results and path to the problematic area.
|
||||
/// </summary>
|
||||
/// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
|
||||
public static bool EnableDebugging { get; set; } = System.Diagnostics.Debugger.IsAttached;
|
||||
|
||||
/// <summary>
|
||||
/// This flag enables checking the font glyph availability.
|
||||
/// If your text contains glyphs that are not present in the specified font,
|
||||
/// 1) when this flag is enabled: the DocumentDrawingException is thrown. OR
|
||||
/// 2) when this flag is disabled: placeholder characters are visible in the produced PDF file.
|
||||
/// Enabling this flag may slightly decrease document generation performance.
|
||||
/// 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 CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached;
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
|
||||
[](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" />
|
||||
```
|
||||
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
|
||||
## Documentation
|
||||
|
||||
[](https://www.questpdf.com/getting-started.html)
|
||||
[](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.
|
||||
|
||||
|
||||
[](https://www.questpdf.com/design-patterns.html)
|
||||
[](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!
|
||||
|
||||
[](https://www.questpdf.com/document-previewer.html)
|
||||
[](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%">
|
||||
|
||||
Reference in New Issue
Block a user