Compare commits

..

4 Commits

Author SHA1 Message Date
MarcinZiabek 4743768e23 Implemented simple object cache, and updated some fluent api invocations 2022-09-12 17:09:07 +02:00
MarcinZiabek 1ba01a1cf5 Fixed override value 2022-09-09 11:32:05 +02:00
MarcinZiabek d4448437ac Reduced creation of DefaultTextStyle objects 2022-09-08 17:29:52 +02:00
MarcinZiabek bc853f48fc Implemented TextStyleManager to reduce memory usage 2022-09-08 16:49:28 +02:00
28 changed files with 563 additions and 488 deletions
+2 -2
View File
@@ -65,9 +65,9 @@ namespace QuestPDF.Examples.Engine
return this; return this;
} }
public RenderingTest ShowResults() public RenderingTest ShowResults(bool value = true)
{ {
ShowResult = true; ShowResult = value;
return this; return this;
} }
+76 -30
View File
@@ -1,8 +1,13 @@
using System.Linq; using System;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using QuestPDF.Drawing;
using QuestPDF.Elements;
using QuestPDF.Examples.Engine; using QuestPDF.Examples.Engine;
using QuestPDF.Fluent; using QuestPDF.Fluent;
using QuestPDF.Helpers; using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
namespace QuestPDF.Examples namespace QuestPDF.Examples
{ {
@@ -11,36 +16,77 @@ namespace QuestPDF.Examples
[Test] [Test]
public void Benchmark() public void Benchmark()
{ {
RenderingTest GenerateAndCollect();
.Create()
.ProducePdf()
.PageSize(PageSizes.A4)
.ShowResults()
.MaxPages(10_000)
.EnableCaching(true)
.EnableDebugging(false)
.Render(container =>
{
container
.Padding(10)
.MinimalBox()
.Border(1)
.Table(table =>
{
const int numberOfRows = 100_000;
const int numberOfColumns = 10;
table.ColumnsDefinition(columns =>
{
foreach (var _ in Enumerable.Range(0, numberOfColumns))
columns.RelativeColumn();
});
foreach (var row in Enumerable.Range(0, numberOfRows)) var stopwatch = new Stopwatch();
foreach (var column in Enumerable.Range(0, numberOfColumns)) stopwatch.Start();
table.Cell().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}");
}); foreach (var _ in Enumerable.Range(0, 10000))
}); {
GenerateAndCollect();
}
stopwatch.Stop();
Console.WriteLine($"Execution time: {stopwatch.Elapsed:g}");
void GenerateAndCollect()
{
var container = new Container();
container
.Padding(10)
.MinimalBox()
.Border(1)
.Column(column =>
{
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);
}
});
}
});
ElementCacheManager.Collect(container);
}
} }
} }
} }
-39
View File
@@ -2,7 +2,6 @@
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using NUnit.Framework; using NUnit.Framework;
using QuestPDF.Elements.Text;
using QuestPDF.Examples.Engine; using QuestPDF.Examples.Engine;
using QuestPDF.Fluent; using QuestPDF.Fluent;
using QuestPDF.Helpers; using QuestPDF.Helpers;
@@ -619,43 +618,5 @@ namespace QuestPDF.Examples
.FontSize(20); .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: 😊😅🥳👍❤😍👌");
});
});
});
}
} }
} }
+7 -10
View File
@@ -74,6 +74,8 @@ namespace QuestPDF.Drawing
var pageContext = new PageContext(); var pageContext = new PageContext();
RenderPass(pageContext, new FreeCanvas(), content, debuggingState); RenderPass(pageContext, new FreeCanvas(), content, debuggingState);
RenderPass(pageContext, canvas, content, debuggingState); RenderPass(pageContext, canvas, content, debuggingState);
ElementCacheManager.Collect(content);
} }
internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState) internal static void RenderPass<TCanvas>(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState)
@@ -172,7 +174,7 @@ namespace QuestPDF.Drawing
{ {
if (textBlockItem is TextBlockSpan textSpan) if (textBlockItem is TextBlockSpan textSpan)
{ {
textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle); textSpan.Style = textSpan.Style.ApplyGlobalStyle(TextStyle.LibraryDefault);
} }
else if (textBlockItem is TextBlockElement textElement) else if (textBlockItem is TextBlockElement textElement)
{ {
@@ -184,18 +186,13 @@ namespace QuestPDF.Drawing
} }
if (content is DynamicHost dynamicHost) if (content is DynamicHost dynamicHost)
dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle); dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
var targetTextStyle = documentDefaultTextStyle;
if (content is DefaultTextStyle defaultTextStyleElement) if (content is DefaultTextStyle defaultTextStyleElement)
{ documentDefaultTextStyle = defaultTextStyleElement.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
defaultTextStyleElement.TextStyle.ApplyParentStyle(documentDefaultTextStyle);
targetTextStyle = defaultTextStyleElement.TextStyle;
}
foreach (var child in content.GetChildren()) foreach (var child in content.GetChildren())
ApplyDefaultTextStyle(child, targetTextStyle); ApplyDefaultTextStyle(child, documentDefaultTextStyle);
} }
} }
} }
+76
View File
@@ -0,0 +1,76 @@
using System;
using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using QuestPDF.Infrastructure;
namespace QuestPDF.Drawing
{
internal class CircularBuffer
{
private const int BufferSize = 11_000;
private object[] Buffer = new object[BufferSize];
private int WriteIndex { get; set; } = 0;
private int ReadIndex { get; set; } = 0;
public T Get<T>() where T : class, new()
{
lock (this)
{
if (ReadIndex == WriteIndex)
return new T();
var index = ReadIndex;
ReadIndex = (ReadIndex + 1) % BufferSize;
var result = Buffer[index] as T;
Buffer[index] = null;
return result;
}
}
public void Store(object value)
{
lock (this)
{
Buffer[WriteIndex] = value;
WriteIndex = (WriteIndex + 1) % BufferSize;
}
}
}
// performance analysis:
// without: 115s
// ConcurrentQueue: 28s
// ConcurrentBag: 38s
// CircularBuffer: 30s
internal static class ElementCacheManager
{
private static ConcurrentDictionary<Type, ConcurrentQueue<object>> Cache { get; } = new();
public static T Get<T>() where T : class, new()
{
var buffer = Cache.GetOrAdd(typeof(T), _=> new ConcurrentQueue<object>());
return buffer.TryDequeue(out var result) ? result as T : new T();
}
public static void Store<T>(T element)
{
var buffer = Cache.GetOrAdd(element.GetType(), _=> new ConcurrentQueue<object>());
buffer.Enqueue(element);
}
public static void Collect(Element element)
{
foreach (var child in element.GetChildren())
Collect(child);
if (element is ICollectable collectable)
{
collectable.Collect();
Store(element);
}
}
}
}
@@ -4,11 +4,6 @@ namespace QuestPDF.Drawing.Exceptions
{ {
public class DocumentDrawingException : Exception public class DocumentDrawingException : Exception
{ {
internal DocumentDrawingException(string message) : base(message)
{
}
internal DocumentDrawingException(string message, Exception inner) : base(message, inner) internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
{ {
+13 -13
View File
@@ -14,13 +14,13 @@ namespace QuestPDF.Drawing
{ {
public static class FontManager public static class FontManager
{ {
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new(); private static readonly ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new(); private static readonly ConcurrentDictionary<TextStyle, SKFontMetrics> FontMetrics = new();
private static ConcurrentDictionary<object, SKPaint> FontPaints = new(); private static readonly ConcurrentDictionary<TextStyle, SKPaint> FontPaints = new();
private static ConcurrentDictionary<string, SKPaint> ColorPaints = new(); private static readonly ConcurrentDictionary<string, SKPaint> ColorPaints = new();
private static ConcurrentDictionary<object, Font> ShaperFonts = new(); private static readonly ConcurrentDictionary<TextStyle, Font> ShaperFonts = new();
private static ConcurrentDictionary<object, SKFont> Fonts = new(); private static readonly ConcurrentDictionary<TextStyle, SKFont> Fonts = new();
private static ConcurrentDictionary<object, TextShaper> TextShapers = new(); private static readonly ConcurrentDictionary<TextStyle, TextShaper> TextShapers = new();
static FontManager() static FontManager()
{ {
@@ -110,7 +110,7 @@ namespace QuestPDF.Drawing
internal static SKPaint ToPaint(this TextStyle style) 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) static SKPaint Convert(TextStyle style)
{ {
@@ -172,14 +172,14 @@ namespace QuestPDF.Drawing
internal static SKFontMetrics ToFontMetrics(this TextStyle style) 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) 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(); using var harfBuzzBlob = typeface.OpenStream(out var ttcIndex).ToHarfBuzzBlob();
@@ -200,12 +200,12 @@ namespace QuestPDF.Drawing
internal static TextShaper ToTextShaper(this TextStyle style) 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) internal static SKFont ToFont(this TextStyle style)
{ {
return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont()); return Fonts.GetOrAdd(style, key => key.ToPaint().ToFont());
} }
} }
} }
+10
View File
@@ -41,5 +41,15 @@ namespace QuestPDF.Elements
{ {
return $"Border: Top({Top}) Right({Right}) Bottom({Bottom}) Left({Left}) Color({Color})"; return $"Border: Top({Top}) Right({Right}) Bottom({Bottom}) Left({Left}) Color({Color})";
} }
public override void Collect()
{
base.Collect();
Left = 0;
Right = 0;
Bottom = 0;
Top = 0;
}
} }
} }
+6 -1
View File
@@ -19,7 +19,7 @@ namespace QuestPDF.Elements
public Position Offset { get; set; } public Position Offset { get; set; }
} }
internal class Column : Element, ICacheable, IStateResettable internal class Column : Element, ICacheable, IStateResettable, ICollectable
{ {
internal List<ColumnItem> Items { get; } = new(); internal List<ColumnItem> Items { get; } = new();
internal float Spacing { get; set; } internal float Spacing { get; set; }
@@ -124,5 +124,10 @@ namespace QuestPDF.Elements
return commands; return commands;
} }
public void Collect()
{
Items.Clear();
}
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ namespace QuestPDF.Elements
{ {
internal class Container : ContainerElement internal class Container : ContainerElement
{ {
internal Container() public Container()
{ {
} }
+1 -1
View File
@@ -11,7 +11,7 @@ namespace QuestPDF.Elements
private DynamicComponentProxy Child { get; } private DynamicComponentProxy Child { get; }
private object InitialComponentState { get; set; } private object InitialComponentState { get; set; }
internal TextStyle TextStyle { get; } = new(); internal TextStyle TextStyle { get; set; } = TextStyle.Default;
public DynamicHost(DynamicComponentProxy child) public DynamicHost(DynamicComponentProxy child)
{ {
+11 -1
View File
@@ -4,7 +4,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements namespace QuestPDF.Elements
{ {
internal class Padding : ContainerElement, ICacheable internal class Padding : ContainerElement, ICacheable, ICollectable
{ {
public float Top { get; set; } public float Top { get; set; }
public float Right { get; set; } public float Right { get; set; }
@@ -62,5 +62,15 @@ namespace QuestPDF.Elements
{ {
return $"Padding: Top({Top}) Right({Right}) Bottom({Bottom}) Left({Left})"; return $"Padding: Top({Top}) Right({Right}) Bottom({Bottom}) Left({Left})";
} }
public override void Collect()
{
base.Collect();
Left = 0;
Right = 0;
Bottom = 0;
Top = 0;
}
} }
} }
+6 -1
View File
@@ -31,7 +31,7 @@ namespace QuestPDF.Elements
public Position Offset { get; set; } public Position Offset { get; set; }
} }
internal class Row : Element, ICacheable, IStateResettable internal class Row : Element, ICacheable, IStateResettable, ICollectable
{ {
internal List<RowItem> Items { get; } = new(); internal List<RowItem> Items { get; } = new();
internal float Spacing { get; set; } internal float Spacing { get; set; }
@@ -156,5 +156,10 @@ namespace QuestPDF.Elements
return renderingCommands; return renderingCommands;
} }
public void Collect()
{
Items.Clear();
}
} }
} }
-147
View File
@@ -1,147 +0,0 @@
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;
}
var character = char.ConvertFromUtf32(codepoint);
var unicode = $"U-{codepoint:X4}";
var proposedFonts = FindFontsContainingGlyph(codepoint);
var proposedFontsFormatted = proposedFonts.Any() ? string.Join(", ", proposedFonts) : "no fonts available";
throw new DocumentDrawingException(
$"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 TextBlockSpan textBlockSpan)
{
// perform font-fallback operation only when any fallback is available
if (textBlockSpan.Style.Fallback == null)
{
yield return textBlockSpan;
continue;
}
var textRuns = textBlockSpan.Text.SplitWithFontFallback(textBlockSpan.Style);
foreach (var textRun in textRuns)
{
yield return new TextBlockSpan
{
Text = textRun.Content,
Style = textRun.Style
};
}
}
else
{
yield return textBlockItem;
}
}
}
}
}
@@ -14,8 +14,8 @@ namespace QuestPDF.Elements.Text.Items
internal class TextBlockSpan : ITextBlockItem internal class TextBlockSpan : ITextBlockItem
{ {
public string Text { get; set; } public string Text { get; set; }
public TextStyle Style { get; set; } = new(); public TextStyle Style { get; set; } = TextStyle.Default;
private TextShapingResult? TextShapingResult { get; set; } public TextShapingResult? TextShapingResult { get; set; }
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new (); private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
protected virtual bool EnableTextCache => true; protected virtual bool EnableTextCache => true;
+6 -13
View File
@@ -8,7 +8,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text namespace QuestPDF.Elements.Text
{ {
internal class TextBlock : Element, IStateResettable internal class TextBlock : Element, IStateResettable, ICollectable
{ {
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>(); public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
@@ -18,11 +18,8 @@ namespace QuestPDF.Elements.Text
private Queue<ITextBlockItem> RenderingQueue { get; set; } private Queue<ITextBlockItem> RenderingQueue { get; set; }
private int CurrentElementIndex { get; set; } private int CurrentElementIndex { get; set; }
private bool FontFallbackApplied { get; set; } = false;
public void ResetState() public void ResetState()
{ {
ApplyFontFallback();
InitializeQueue(); InitializeQueue();
CurrentElementIndex = 0; CurrentElementIndex = 0;
@@ -40,15 +37,11 @@ namespace QuestPDF.Elements.Text
foreach (var item in Items) foreach (var item in Items)
RenderingQueue.Enqueue(item); RenderingQueue.Enqueue(item);
} }
}
void ApplyFontFallback()
{ public void Collect()
if (FontFallbackApplied) {
return; Items.Clear();
Items = Items.ApplyFontFallback().ToList();
FontFallbackApplied = true;
}
} }
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
+2 -1
View File
@@ -1,4 +1,5 @@
using System; using System;
using QuestPDF.Drawing;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -8,7 +9,7 @@ namespace QuestPDF.Fluent
{ {
private static IContainer Border(this IContainer element, Action<Border> handler) private static IContainer Border(this IContainer element, Action<Border> handler)
{ {
var border = element as Border ?? new Border(); var border = element as Border ?? ElementCacheManager.Get<Border>();
handler(border); handler(border);
return element.Element(border); return element.Element(border);
+8 -10
View File
@@ -1,12 +1,14 @@
using System; using System;
using QuestPDF.Drawing;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
using Container = System.ComponentModel.Container;
namespace QuestPDF.Fluent namespace QuestPDF.Fluent
{ {
public class ColumnDescriptor public class ColumnDescriptor
{ {
internal Column Column { get; } = new(); internal Column Column { get; set; }
public void Spacing(float value, Unit unit = Unit.Point) public void Spacing(float value, Unit unit = Unit.Point)
{ {
@@ -15,14 +17,9 @@ namespace QuestPDF.Fluent
public IContainer Item() public IContainer Item()
{ {
var container = new Container(); var columnItem = ElementCacheManager.Get<ColumnItem>();
Column.Items.Add(columnItem);
Column.Items.Add(new ColumnItem return columnItem;
{
Child = container
});
return container;
} }
} }
@@ -36,7 +33,8 @@ namespace QuestPDF.Fluent
public static void Column(this IContainer element, Action<ColumnDescriptor> handler) public static void Column(this IContainer element, Action<ColumnDescriptor> handler)
{ {
var descriptor = new ColumnDescriptor(); var descriptor = ElementCacheManager.Get<ColumnDescriptor>();
descriptor.Column = ElementCacheManager.Get<Column>();
handler(descriptor); handler(descriptor);
element.Element(descriptor.Column); element.Element(descriptor.Column);
} }
+6 -5
View File
@@ -1,4 +1,5 @@
using System; using System;
using QuestPDF.Drawing;
using QuestPDF.Drawing.Exceptions; using QuestPDF.Drawing.Exceptions;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -52,10 +53,10 @@ namespace QuestPDF.Fluent
public static IContainer Background(this IContainer element, string color) public static IContainer Background(this IContainer element, string color)
{ {
return element.Element(new Background var background = ElementCacheManager.Get<Background>();
{ background.Color = color;
Color = color
}); return element.Element(background);
} }
public static void Placeholder(this IContainer element, string? text = null) public static void Placeholder(this IContainer element, string? text = null)
@@ -162,7 +163,7 @@ namespace QuestPDF.Fluent
public static IContainer MinimalBox(this IContainer element) public static IContainer MinimalBox(this IContainer element)
{ {
return element.Element(new MinimalBox()); return element.Element(ElementCacheManager.Get<MinimalBox>());
} }
public static IContainer Unconstrained(this IContainer element) public static IContainer Unconstrained(this IContainer element)
+2 -1
View File
@@ -1,4 +1,5 @@
using System; using System;
using QuestPDF.Drawing;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -8,7 +9,7 @@ namespace QuestPDF.Fluent
{ {
private static IContainer Padding(this IContainer element, Action<Padding> handler) private static IContainer Padding(this IContainer element, Action<Padding> handler)
{ {
var padding = element as Padding ?? new Padding(); var padding = element as Padding ?? ElementCacheManager.Get<Padding>();
handler(padding); handler(padding);
return element.Element(padding); return element.Element(padding);
+11 -9
View File
@@ -1,4 +1,5 @@
using System; using System;
using QuestPDF.Drawing;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -6,7 +7,7 @@ namespace QuestPDF.Fluent
{ {
public class RowDescriptor public class RowDescriptor
{ {
internal Row Row { get; } = new(); internal Row Row { get; set; }
public void Spacing(float value) public void Spacing(float value)
{ {
@@ -15,14 +16,12 @@ namespace QuestPDF.Fluent
private IContainer Item(RowItemType type, float size = 0) private IContainer Item(RowItemType type, float size = 0)
{ {
var element = new RowItem var rowItem = ElementCacheManager.Get<RowItem>();
{ rowItem.Type = type;
Type = type, rowItem.Size = size;
Size = size
};
Row.Items.Add(element); Row.Items.Add(rowItem);
return element; return rowItem;
} }
[Obsolete("This element has been renamed since version 2022.2. Please use the RelativeItem method.")] [Obsolete("This element has been renamed since version 2022.2. Please use the RelativeItem method.")]
@@ -57,9 +56,12 @@ namespace QuestPDF.Fluent
{ {
public static void Row(this IContainer element, Action<RowDescriptor> handler) public static void Row(this IContainer element, Action<RowDescriptor> handler)
{ {
var descriptor = new RowDescriptor(); var descriptor = ElementCacheManager.Get<RowDescriptor>();
descriptor.Row = ElementCacheManager.Get<Row>();
handler(descriptor); handler(descriptor);
element.Element(descriptor.Row); element.Element(descriptor.Row);
ElementCacheManager.Store(descriptor);
} }
} }
} }
+36 -43
View File
@@ -12,11 +12,18 @@ namespace QuestPDF.Fluent
{ {
public class TextSpanDescriptor 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,16 @@ namespace QuestPDF.Fluent
public class TextPageNumberDescriptor : TextSpanDescriptor public class TextPageNumberDescriptor : TextSpanDescriptor
{ {
internal PageNumberFormatter FormatFunction { get; private set; } = x => x?.ToString() ?? string.Empty; internal Action<PageNumberFormatter> AssignFormatFunction { get; }
internal TextPageNumberDescriptor(TextStyle textStyle) : base(textStyle) internal TextPageNumberDescriptor(Action<TextStyle> assignTextStyle, Action<PageNumberFormatter> assignFormatFunction) : base(assignTextStyle)
{ {
AssignFormatFunction = assignFormatFunction;
} }
public TextPageNumberDescriptor Format(PageNumberFormatter formatter) public TextPageNumberDescriptor Format(PageNumberFormatter formatter)
{ {
FormatFunction = formatter ?? FormatFunction; AssignFormatFunction(formatter);
return this; return this;
} }
} }
@@ -41,7 +48,7 @@ namespace QuestPDF.Fluent
public class TextDescriptor public class TextDescriptor
{ {
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>(); 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; internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
private float Spacing { get; set; } = 0f; private float Spacing { get; set; } = 0f;
@@ -91,19 +98,15 @@ namespace QuestPDF.Fluent
public TextSpanDescriptor Span(string? text) public TextSpanDescriptor Span(string? text)
{ {
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
if (text == null) if (text == null)
return descriptor; return new TextSpanDescriptor(_ => { });
var items = text var items = text
.Replace("\r", string.Empty) .Replace("\r", string.Empty)
.Split(new[] { '\n' }, StringSplitOptions.None) .Split(new[] { '\n' }, StringSplitOptions.None)
.Select(x => new TextBlockSpan .Select(x => new TextBlockSpan
{ {
Text = x, Text = x
Style = style
}) })
.ToList(); .ToList();
@@ -118,7 +121,7 @@ namespace QuestPDF.Fluent
.ToList() .ToList()
.ForEach(TextBlocks.Add); .ForEach(TextBlocks.Add);
return descriptor; return new TextSpanDescriptor(x => items.ForEach(y => y.Style = x));
} }
public TextSpanDescriptor Line(string? text) public TextSpanDescriptor Line(string? text)
@@ -134,16 +137,10 @@ namespace QuestPDF.Fluent
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber) private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
{ {
var style = DefaultStyle.Clone(); var textBlockItem = new TextBlockPageNumber();
var descriptor = new TextPageNumberDescriptor(style); AddItemToLastTextBlock(textBlockItem);
AddItemToLastTextBlock(new TextBlockPageNumber return new TextPageNumberDescriptor(x => textBlockItem.Style = x, x => textBlockItem.Source = context => x(pageNumber(context)));
{
Source = context => descriptor.FormatFunction(pageNumber(context)),
Style = style
});
return descriptor;
} }
public TextPageNumberDescriptor CurrentPageNumber() public TextPageNumberDescriptor CurrentPageNumber()
@@ -187,20 +184,17 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(sectionName)) if (IsNullOrEmpty(sectionName))
throw new ArgumentException("Section name cannot be null or empty", nameof(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)) if (IsNullOrEmpty(text))
return descriptor; return new TextSpanDescriptor(_ => { });
AddItemToLastTextBlock(new TextBlockSectionLink var textBlockItem = new TextBlockSectionLink
{ {
Style = style,
Text = text, Text = text,
SectionName = sectionName 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.")] [Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
@@ -214,20 +208,17 @@ namespace QuestPDF.Fluent
if (IsNullOrEmpty(url)) if (IsNullOrEmpty(url))
throw new ArgumentException("Url cannot be null or empty", nameof(url)); throw new ArgumentException("Url cannot be null or empty", nameof(url));
var style = DefaultStyle.Clone();
var descriptor = new TextSpanDescriptor(style);
if (IsNullOrEmpty(text)) if (IsNullOrEmpty(text))
return descriptor; return new TextSpanDescriptor(_ => { });
AddItemToLastTextBlock(new TextBlockHyperlink var textBlockItem = new TextBlockHyperlink
{ {
Style = style,
Text = text, Text = text,
Url = url 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.")] [Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
@@ -251,7 +242,9 @@ namespace QuestPDF.Fluent
internal void Compose(IContainer container) internal void Compose(IContainer container)
{ {
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment); TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
container = container.DefaultTextStyle(DefaultStyle);
if (DefaultStyle != null)
container = container.DefaultTextStyle(DefaultStyle);
if (TextBlocks.Count == 1) if (TextBlocks.Count == 1)
{ {
+36 -45
View File
@@ -11,131 +11,124 @@ namespace QuestPDF.Fluent
if (style == null) if (style == null)
return descriptor; return descriptor;
descriptor.TextStyle.OverrideStyle(style); descriptor.MutateTextStyle(x => x.OverrideStyle(style));
return descriptor; 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 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; return descriptor;
} }
public static T BackgroundColor<T>(this T descriptor, string value) where T : TextSpanDescriptor 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; return descriptor;
} }
public static T FontFamily<T>(this T descriptor, string value) where T : TextSpanDescriptor 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; return descriptor;
} }
public static T FontSize<T>(this T descriptor, float value) where T : TextSpanDescriptor 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; return descriptor;
} }
public static T LineHeight<T>(this T descriptor, float value) where T : TextSpanDescriptor 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; return descriptor;
} }
public static T Italic<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor 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; return descriptor;
} }
public static T Strikethrough<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor 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; return descriptor;
} }
public static T Underline<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor 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; return descriptor;
} }
public static T WrapAnywhere<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor 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; return descriptor;
} }
#region Weight #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 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 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 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 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 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 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 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 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 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 public static T ExtraBlack<T>(this T descriptor) where T : TextSpanDescriptor
{ {
return descriptor.Weight(FontWeight.ExtraBlack); descriptor.MutateTextStyle(x => x.ExtraBlack());
return descriptor;
} }
#endregion #endregion
@@ -143,24 +136,22 @@ namespace QuestPDF.Fluent
#region Position #region Position
public static T NormalPosition<T>(this T descriptor) where T : TextSpanDescriptor 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 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 public static T Superscript<T>(this T descriptor) where T : TextSpanDescriptor
{ {
return descriptor.Position(FontPosition.Superscript); descriptor.MutateTextStyle(x => x.Superscript());
}
private static T Position<T>(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor
{
descriptor.TextStyle.FontPosition = fontPosition;
return descriptor; return descriptor;
} }
#endregion #endregion
} }
} }
+13 -32
View File
@@ -4,26 +4,10 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent namespace QuestPDF.Fluent
{ {
public static class TextStyleExtensions public static class TextStyleExtensions
{ {
private static TextStyle Mutate(this TextStyle style, Action<TextStyle> handler)
{
style = style.Clone();
handler(style);
return style;
}
public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
{
return style.Mutate(x => x.Fallback = value);
}
public static TextStyle Fallback(this TextStyle style, Func<TextStyle, TextStyle> handler)
{
return style.Fallback(handler(TextStyle.Default));
}
[Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")] [Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")]
public static TextStyle Color(this TextStyle style, string value) public static TextStyle Color(this TextStyle style, string value)
{ {
@@ -32,12 +16,12 @@ namespace QuestPDF.Fluent
public static TextStyle FontColor(this TextStyle style, string value) 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) 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.")] [Obsolete("This element has been renamed since version 2022.3. Please use the FontFamily method.")]
@@ -48,7 +32,7 @@ namespace QuestPDF.Fluent
public static TextStyle FontFamily(this TextStyle style, string value) 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.")] [Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")]
@@ -59,39 +43,39 @@ namespace QuestPDF.Fluent
public static TextStyle FontSize(this TextStyle style, float value) 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) 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) 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) 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) 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) 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 #region Weight
public static TextStyle Weight(this TextStyle style, FontWeight 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) public static TextStyle Thin(this TextStyle style)
@@ -164,10 +148,7 @@ namespace QuestPDF.Fluent
private static TextStyle Position(this TextStyle style, FontPosition fontPosition) private static TextStyle Position(this TextStyle style, FontPosition fontPosition)
{ {
if (style.FontPosition == fontPosition) return style.Mutate(TextStyleProperty.FontPosition, fontPosition);
return style;
return style.Mutate(t => t.FontPosition = fontPosition);
} }
#endregion #endregion
} }
+6 -1
View File
@@ -5,7 +5,7 @@ using QuestPDF.Elements;
namespace QuestPDF.Infrastructure namespace QuestPDF.Infrastructure
{ {
internal abstract class ContainerElement : Element, IContainer internal abstract class ContainerElement : Element, IContainer, ICollectable
{ {
internal Element? Child { get; set; } = Empty.Instance; internal Element? Child { get; set; } = Empty.Instance;
@@ -34,5 +34,10 @@ namespace QuestPDF.Infrastructure
{ {
Child?.Draw(availableSpace); Child?.Draw(availableSpace);
} }
public virtual void Collect()
{
Child = default;
}
} }
} }
+7
View File
@@ -0,0 +1,7 @@
namespace QuestPDF.Infrastructure
{
public interface ICollectable
{
void Collect();
}
}
+4 -75
View File
@@ -1,13 +1,10 @@
using System; using System;
using HarfBuzzSharp;
using QuestPDF.Helpers; using QuestPDF.Helpers;
namespace QuestPDF.Infrastructure namespace QuestPDF.Infrastructure
{ {
public class TextStyle public record TextStyle
{ {
internal bool HasGlobalStyleApplied { get; private set; }
internal string? Color { get; set; } internal string? Color { get; set; }
internal string? BackgroundColor { get; set; } internal string? BackgroundColor { get; set; }
internal string? FontFamily { get; set; } internal string? FontFamily { get; set; }
@@ -20,13 +17,7 @@ namespace QuestPDF.Infrastructure
internal bool? HasUnderline { get; set; } internal bool? HasUnderline { get; set; }
internal bool? WrapAnywhere { get; set; } internal bool? WrapAnywhere { get; set; }
internal TextStyle? Fallback { get; set; } internal static TextStyle LibraryDefault { get; } = new()
// TODO: without cache, this may be an expensive operation
internal object PaintKey => (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color);
internal object FontMetricsKey => (FontFamily, Size, FontWeight, IsItalic);
internal static readonly TextStyle LibraryDefault = new TextStyle
{ {
Color = Colors.Black, Color = Colors.Black,
BackgroundColor = Colors.Transparent, BackgroundColor = Colors.Transparent,
@@ -38,71 +29,9 @@ namespace QuestPDF.Infrastructure
IsItalic = false, IsItalic = false,
HasStrikethrough = false, HasStrikethrough = false,
HasUnderline = 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 { get; } = new();
public static TextStyle Default => new TextStyle();
internal void ApplyGlobalStyle(TextStyle globalStyle)
{
if (HasGlobalStyleApplied)
return;
HasGlobalStyleApplied = true;
ApplyParentStyle(globalStyle);
if (Fallback != null)
ApplyFallbackStyle(this);
}
internal void ApplyFallbackStyle(TextStyle parentStyle)
{
ApplyParentStyle(parentStyle, false);
Fallback?.ApplyFallbackStyle(this);
}
internal void ApplyParentStyle(TextStyle parentStyle, bool mapFallback = true)
{
Color ??= parentStyle.Color;
BackgroundColor ??= parentStyle.BackgroundColor;
FontFamily ??= parentStyle.FontFamily;
Size ??= parentStyle.Size;
LineHeight ??= parentStyle.LineHeight;
FontWeight ??= parentStyle.FontWeight;
FontPosition ??= parentStyle.FontPosition;
IsItalic ??= parentStyle.IsItalic;
HasStrikethrough ??= parentStyle.HasStrikethrough;
HasUnderline ??= parentStyle.HasUnderline;
WrapAnywhere ??= parentStyle.WrapAnywhere;
if (mapFallback)
Fallback ??= parentStyle.Fallback?.Clone();
}
internal void OverrideStyle(TextStyle parentStyle)
{
Color = parentStyle.Color ?? Color;
BackgroundColor = parentStyle.BackgroundColor ?? BackgroundColor;
FontFamily = parentStyle.FontFamily ?? FontFamily;
Size = parentStyle.Size ?? Size;
LineHeight = parentStyle.LineHeight ?? LineHeight;
FontWeight = parentStyle.FontWeight ?? FontWeight;
FontPosition = parentStyle.FontPosition ?? FontPosition;
IsItalic = parentStyle.IsItalic ?? IsItalic;
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere;
Fallback = parentStyle.Fallback?.Clone() ?? Fallback;
}
internal TextStyle Clone()
{
var clone = (TextStyle)MemberwiseClone();
clone.HasGlobalStyleApplied = false;
clone.Fallback = Fallback?.Clone();
return clone;
}
} }
} }
+215
View File
@@ -0,0 +1,215 @@
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
}
internal static class TextStyleManager
{
public static ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new();
public static ConcurrentDictionary<(TextStyle origin, TextStyle parent, bool overrideValue), TextStyle> TextStyleApplyCache = 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 };
}
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, false);
return TextStyleApplyCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, key.overrideValue));
}
internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
{
var cacheKey = (style, parent, true);
return TextStyleApplyCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, key.overrideValue));
}
private static TextStyle ApplyStyle(TextStyle style, TextStyle parent, bool overrideValue)
{
var result = style;
result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideValue);
result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideValue);
result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideValue);
result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideValue);
result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideValue);
result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideValue);
result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideValue);
result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideValue);
result = MutateStyle(result, TextStyleProperty.HasStrikethrough, parent.HasStrikethrough, overrideValue);
result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideValue);
result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideValue);
return result;
}
}
}