Compare commits
53 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 | |||
| 52ad0f5c24 | |||
| 53107073a6 | |||
| c71bb3ea59 | |||
| 6cced3c143 | |||
| 425ea59cfe | |||
| 4d326496c6 | |||
| b7b6488d16 | |||
| f0ba5fc32d | |||
| 04da32e0e7 | |||
| 5bdb338996 |
@@ -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;
|
||||
@@ -129,6 +132,9 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
if (Glyphs.Length == 0)
|
||||
return null;
|
||||
|
||||
if (startIndex > endIndex)
|
||||
return null;
|
||||
|
||||
using var skTextBlobBuilder = new SKTextBlobBuilder();
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
@@ -6,17 +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)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -94,7 +94,12 @@ namespace QuestPDF.Elements
|
||||
if (item.IsRendered)
|
||||
continue;
|
||||
|
||||
var itemSpace = new Size(availableSpace.Width, availableSpace.Height - topOffset);
|
||||
var availableHeight = availableSpace.Height - topOffset;
|
||||
|
||||
if (availableHeight < 0)
|
||||
break;
|
||||
|
||||
var itemSpace = new Size(availableSpace.Width, availableHeight);
|
||||
var measurement = item.Measure(itemSpace);
|
||||
|
||||
if (measurement.Type == SpacePlanType.Wrap)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -1,33 +1,43 @@
|
||||
using System;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
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 SpacePlan.FullRender(availableSpace.Width, availableSpace.Height);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
@@ -9,7 +10,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
return SpacePlan.FullRender(0, 0);
|
||||
return availableSpace.IsNegative()
|
||||
? SpacePlan.Wrap()
|
||||
: SpacePlan.FullRender(0, 0);
|
||||
}
|
||||
|
||||
internal override void Draw(Size availableSpace)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
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()
|
||||
@@ -15,6 +19,12 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
if (availableSpace.IsNegative())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
if (IsRendered && !RepeatContent)
|
||||
return SpacePlan.FullRender(Size.Zero);
|
||||
|
||||
return SpacePlan.FullRender(availableSpace);
|
||||
}
|
||||
|
||||
@@ -23,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,31 +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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Elements
|
||||
@@ -14,6 +15,9 @@ namespace QuestPDF.Elements
|
||||
|
||||
internal override SpacePlan Measure(Size availableSpace)
|
||||
{
|
||||
if (availableSpace.IsNegative())
|
||||
return SpacePlan.Wrap();
|
||||
|
||||
if (IsRendered)
|
||||
return SpacePlan.FullRender(0, 0);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
@@ -66,7 +66,7 @@ namespace QuestPDF.Elements.Text.Items
|
||||
// start breaking text from requested position
|
||||
var endIndex = TextShapingResult.BreakText(startIndex, request.AvailableWidth);
|
||||
|
||||
if (endIndex < 0)
|
||||
if (endIndex < startIndex)
|
||||
return null;
|
||||
|
||||
// break text only on spaces
|
||||
|
||||
@@ -4,12 +4,16 @@ 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 List<ITextBlockItem>();
|
||||
|
||||
@@ -18,8 +22,11 @@ namespace QuestPDF.Elements.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;
|
||||
|
||||
@@ -37,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);
|
||||
|
||||
@@ -68,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())
|
||||
@@ -124,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,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,7 +49,7 @@ namespace QuestPDF.Fluent
|
||||
public class TextDescriptor
|
||||
{
|
||||
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
|
||||
private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
|
||||
private TextStyle? DefaultStyle { get; set; }
|
||||
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
||||
private float Spacing { get; set; } = 0f;
|
||||
|
||||
@@ -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.")]
|
||||
@@ -251,14 +243,23 @@ namespace QuestPDF.Fluent
|
||||
internal void Compose(IContainer container)
|
||||
{
|
||||
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
|
||||
|
||||
if (DefaultStyle != null)
|
||||
container = container.DefaultTextStyle(DefaultStyle);
|
||||
|
||||
container.DefaultTextStyle(DefaultStyle).Column(column =>
|
||||
if (TextBlocks.Count == 1)
|
||||
{
|
||||
container.Element(TextBlocks.First());
|
||||
return;
|
||||
}
|
||||
|
||||
container.Column(column =>
|
||||
{
|
||||
column.Spacing(Spacing);
|
||||
|
||||
foreach (var textBlock in TextBlocks)
|
||||
column.Item().Element(textBlock);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Helpers
|
||||
@@ -54,5 +55,10 @@ namespace QuestPDF.Helpers
|
||||
|
||||
handler(element);
|
||||
}
|
||||
|
||||
internal static bool IsNegative(this Size size)
|
||||
{
|
||||
return size.Width < 0f || size.Height < 0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,61 +31,10 @@ namespace QuestPDF.Infrastructure
|
||||
IsItalic = false,
|
||||
HasStrikethrough = false,
|
||||
HasUnderline = false,
|
||||
WrapAnywhere = false
|
||||
WrapAnywhere = false,
|
||||
Fallback = null
|
||||
};
|
||||
|
||||
// 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)
|
||||
public static readonly 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>2022.8.0</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>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://github.com/QuestPDF/QuestPDF/stargazers)
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://www.nuget.org/packages/QuestPDF/)
|
||||
[](https://github.com/QuestPDF/QuestPDF/blob/main/LICENSE)
|
||||
[](https://github.com/sponsors/QuestPDF)
|
||||
|
||||
QuestPDF is an open-source .NET library for PDF documents generation.
|
||||
|
||||
It offers a layout engine designed with a full paging support in mind. The document consists of many simple elements (e.g. border, background, image, text, padding, table, grid etc.) that are composed together to create more complex structures. This way, as a developer, you can understand the behavior of every element and use them with full confidence. Additionally, the document and all its elements support paging functionality. For example, an element can be moved to the next page (if there is not enough space) or even be split between pages like table's rows.
|
||||
|
||||
## Documentation
|
||||
|
||||
[](https://www.questpdf.com/getting-started.html)
|
||||
A short and easy to follow tutorial showing how to design an invoice document under 200 lines of code.
|
||||
|
||||
|
||||
[](https://www.questpdf.com/api-reference/index.html)
|
||||
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)
|
||||
Everything that may help you designing great reports and create reusable code that is easy to maintain.
|
||||
|
||||
## Simplicity is the key
|
||||
|
||||
How easy it is to start and prototype with QuestPDF? Really easy thanks to its minimal API! Please analyse the code below:
|
||||
|
||||
```#
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Helpers;
|
||||
using QuestPDF.Infrastructure;
|
||||
|
||||
// code in your main method
|
||||
Document.Create(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Size(PageSizes.A4);
|
||||
page.Margin(2, Unit.Centimetre);
|
||||
page.Background(Colors.White);
|
||||
page.DefaultTextStyle(x => x.FontSize(20));
|
||||
|
||||
page.Header()
|
||||
.Text("Hello PDF!")
|
||||
.SemiBold().FontSize(36).FontColor(Colors.Blue.Medium);
|
||||
|
||||
page.Content()
|
||||
.PaddingVertical(1, Unit.Centimetre)
|
||||
.Column(x =>
|
||||
{
|
||||
x.Spacing(20);
|
||||
|
||||
x.Item().Text(Placeholders.LoremIpsum());
|
||||
x.Item().Image(Placeholders.Image(200, 100));
|
||||
});
|
||||
|
||||
page.Footer()
|
||||
.AlignCenter()
|
||||
.Text(x =>
|
||||
{
|
||||
x.Span("Page ");
|
||||
x.CurrentPageNumber();
|
||||
});
|
||||
});
|
||||
})
|
||||
.GeneratePdf("hello.pdf");
|
||||
```
|
||||
|
||||
And compare it to the produced PDF file:
|
||||
|
||||

|
||||
|
||||
## Are you ready for more?
|
||||
|
||||
The Fluent API of QuestPDF scales really well. It is easy to create and maintain even most complex documents. Read [the Getting started tutorial](https://www.questpdf.com/documentation/getting-started.html) to learn QuestPDF basics and implement an invoice under 200 lines of code. You can also investigate and play with the code from [the example repository](https://github.com/QuestPDF/example-invoice).
|
||||
|
||||

|
||||
@@ -1,7 +1,11 @@
|
||||
- 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.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.
|
||||
|
||||
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,12 +17,15 @@ 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/190931857-8ca52ec8-cc7d-4d12-9467-4442b3342fa1.png" width="700" />
|
||||
|
||||
|
||||
## Please share with the community
|
||||
|
||||
As an open-source project without funding, I cannot afford advertising QuestPDF in a typical way. Instead, the library relies on community interactions. Please consider sharing a post about QuestPDF and the value it provides. It really does help!
|
||||
@@ -44,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)
|
||||
|
||||
@@ -59,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.
|
||||
|
||||
|
||||
@@ -74,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