Compare commits

..

18 Commits

Author SHA1 Message Date
MarcinZiabek b658344a46 Experimental version of controlling content repeatability 2022-09-21 19:15:11 +02:00
MarcinZiabek 553c8ab719 2022.9.1 2022-09-19 12:05:44 +02:00
Marcin Ziąbek e768e9b06d Update readme.md 2022-09-19 00:58:55 +02:00
Marcin Ziąbek fd913a777c Update readme.md 2022-09-19 00:57:32 +02:00
MarcinZiabek ed9e6daec5 2022.9.0 2022-09-19 00:45:14 +02:00
Marcin Ziąbek ee6249a658 Merge pull request #341 from QuestPDF/2022.9
2022.9 Release
2022-09-19 00:43:29 +02:00
MarcinZiabek b307304f46 Fixed typo 2022-09-18 20:15:41 +02:00
MarcinZiabek f028f82e11 Removed unsued file 2022-09-17 20:42:14 +02:00
MarcinZiabek 719a3385f6 Updated Previewer to 2022.9 2022-09-16 19:52:10 +02:00
MarcinZiabek 2adff11400 2022.9.0-alpha1 2022-09-16 19:46:32 +02:00
MarcinZiabek 34fed6d547 Settings.CheckIfAllTextGlyphsAreAvailable improvements 2022-09-16 16:08:02 +02:00
MarcinZiabek db2df75624 Text example: fixed visuals 2022-09-15 22:19:28 +02:00
MarcinZiabek 6b535752df Fixed formatting page numbers 2022-09-15 22:12:06 +02:00
MarcinZiabek 556f87ff25 Settings renaming 2022-09-15 21:20:25 +02:00
MarcinZiabek fbebbd85eb Fixed build, adapted fallback process for cached TextStyle 2022-09-15 15:09:30 +02:00
MarcinZiabek 4650c2a4ea Merge branch 'font-fallback' into 2022.9
# Conflicts:
#	QuestPDF/Elements/Text/Items/TextBlockSpan.cs
#	QuestPDF/Fluent/TextStyleExtensions.cs
#	QuestPDF/Infrastructure/TextStyle.cs
2022-09-14 23:28:50 +02:00
MarcinZiabek 5390fc3f1b Implemented font-fallback as required configuration 2022-09-10 23:27:27 +02:00
MarcinZiabek 0468dd5f02 Font-fallback implementation 2022-09-08 13:23:59 +02:00
42 changed files with 418 additions and 318 deletions
+2 -2
View File
@@ -65,9 +65,9 @@ namespace QuestPDF.Examples.Engine
return this; return this;
} }
public RenderingTest ShowResults(bool value = true) public RenderingTest ShowResults()
{ {
ShowResult = value; ShowResult = true;
return this; return this;
} }
+27 -73
View File
@@ -1,13 +1,8 @@
using System; using System.Linq;
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
{ {
@@ -16,77 +11,36 @@ namespace QuestPDF.Examples
[Test] [Test]
public void Benchmark() public void Benchmark()
{ {
GenerateAndCollect(); RenderingTest
.Create()
var stopwatch = new Stopwatch(); .ProducePdf()
stopwatch.Start(); .PageSize(PageSizes.A4)
.ShowResults()
foreach (var _ in Enumerable.Range(0, 10000)) .MaxPages(10_000)
{ .EnableCaching(true)
GenerateAndCollect(); .EnableDebugging(false)
} .Render(container =>
{
stopwatch.Stop(); container
.Padding(10)
Console.WriteLine($"Execution time: {stopwatch.Elapsed:g}"); .MinimalBox()
.Border(1)
void GenerateAndCollect() .Table(table =>
{
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 => const int numberOfRows = 100_000;
const int numberOfColumns = 10;
table.ColumnsDefinition(columns =>
{ {
for (var x = 0; x < numberOfColumns; x++) foreach (var _ in Enumerable.Range(0, numberOfColumns))
{ columns.RelativeColumn();
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); foreach (var row in Enumerable.Range(0, numberOfRows))
} foreach (var column in Enumerable.Range(0, numberOfColumns))
table.Cell().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}");
});
});
} }
} }
} }
+1 -1
View File
@@ -111,7 +111,7 @@ namespace QuestPDF.Examples
{ {
page.Margin(50); page.Margin(50);
page.Content().Column(column => page.Content().PaddingVertical(10).Column(column =>
{ {
column.Item().Element(Title); column.Item().Element(Title);
column.Item().PageBreak(); column.Item().PageBreak();
+39
View File
@@ -2,6 +2,7 @@
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;
@@ -618,5 +619,43 @@ 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: 😊😅🥳👍❤😍👌");
});
});
});
}
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@
<Authors>MarcinZiabek</Authors> <Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company> <Company>CodeFlint</Company>
<PackageId>QuestPDF.Previewer</PackageId> <PackageId>QuestPDF.Previewer</PackageId>
<Version>2022.8.0</Version> <Version>2022.9.1</Version>
<PackAsTool>true</PackAsTool> <PackAsTool>true</PackAsTool>
<ToolCommandName>questpdf-previewer</ToolCommandName> <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> <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>
+21 -3
View File
@@ -64,6 +64,8 @@ namespace QuestPDF.Drawing
var container = new DocumentContainer(); var container = new DocumentContainer();
document.Compose(container); document.Compose(container);
var content = container.Compose(); var content = container.Compose();
ApplyRepeatContent(content);
ApplyDefaultTextStyle(content, TextStyle.LibraryDefault); ApplyDefaultTextStyle(content, TextStyle.LibraryDefault);
var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null; var debuggingState = Settings.EnableDebugging ? ApplyDebugging(content) : null;
@@ -74,8 +76,6 @@ 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)
@@ -174,7 +174,7 @@ namespace QuestPDF.Drawing
{ {
if (textBlockItem is TextBlockSpan textSpan) if (textBlockItem is TextBlockSpan textSpan)
{ {
textSpan.Style = textSpan.Style.ApplyGlobalStyle(TextStyle.LibraryDefault); textSpan.Style = textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle);
} }
else if (textBlockItem is TextBlockElement textElement) else if (textBlockItem is TextBlockElement textElement)
{ {
@@ -194,5 +194,23 @@ namespace QuestPDF.Drawing
foreach (var child in content.GetChildren()) foreach (var child in content.GetChildren())
ApplyDefaultTextStyle(child, documentDefaultTextStyle); ApplyDefaultTextStyle(child, documentDefaultTextStyle);
} }
internal static void ApplyRepeatContent(this Element? content, bool enabled = false)
{
if (content == null)
return;
if (content is RepeatContent repeatContent)
{
ApplyRepeatContent(repeatContent.Child, repeatContent.Repeat);
return;
}
foreach (var child in content.GetChildren())
ApplyRepeatContent(child, enabled);
if (!enabled)
content.CreateProxy(y => y is IContent ? new ShowOnce { Child = y } : y);
}
} }
} }
-76
View File
@@ -1,76 +0,0 @@
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,6 +4,11 @@ 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)
{ {
-8
View File
@@ -1,8 +0,0 @@
namespace QuestPDF.Drawing
{
internal struct TextMeasurement
{
public int LineIndex { get; set; }
public float FragmentWidth { get; set; }
}
}
-17
View File
@@ -55,9 +55,6 @@ namespace QuestPDF.Drawing
xOffset += glyphPositions[i].XAdvance * scaleX; xOffset += glyphPositions[i].XAdvance * scaleX;
yOffset += glyphPositions[i].YAdvance * scaleY; yOffset += glyphPositions[i].YAdvance * scaleY;
} }
if (Settings.CheckIfAllTextGlyphsAreAvailableInSpecifiedFont)
CheckIfAllGlyphsAreAvailable(glyphs, text);
return new TextShapingResult(glyphs); return new TextShapingResult(glyphs);
} }
@@ -78,20 +75,6 @@ namespace QuestPDF.Drawing
else else
throw new NotSupportedException("TextEncoding of type GlyphId is not supported."); throw new NotSupportedException("TextEncoding of type GlyphId is not supported.");
} }
void CheckIfAllGlyphsAreAvailable(ShapedGlyph[] glyphs, string originalText)
{
var containsMissingGlyphs = glyphs.Any(x => x.Codepoint == default);
if (!containsMissingGlyphs)
return;
throw new ArgumentException(
$"Detected missing font glyphs while rendering text. " +
$"This means that the document contains text with characters not present in the assigned font. " +
$"Such characters are replaced by placeholders, usually visible as empty rectangles. " +
$"Font family used: {TextStyle.FontFamily}. Issue detected in text: '{originalText}'");
}
} }
internal struct ShapedGlyph internal struct ShapedGlyph
-10
View File
@@ -41,15 +41,5 @@ 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;
}
} }
} }
+1 -1
View File
@@ -7,7 +7,7 @@ namespace QuestPDF.Elements
{ {
public delegate void DrawOnCanvas(SKCanvas canvas, Size availableSpace); public delegate void DrawOnCanvas(SKCanvas canvas, Size availableSpace);
internal class Canvas : Element, ICacheable internal class Canvas : Element, ICacheable, IContent
{ {
public DrawOnCanvas Handler { get; set; } public DrawOnCanvas Handler { get; set; }
+1 -6
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, ICollectable internal class Column : Element, ICacheable, IStateResettable
{ {
internal List<ColumnItem> Items { get; } = new(); internal List<ColumnItem> Items { get; } = new();
internal float Spacing { get; set; } internal float Spacing { get; set; }
@@ -124,10 +124,5 @@ 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
{ {
public Container() internal Container()
{ {
} }
+1 -1
View File
@@ -6,7 +6,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements namespace QuestPDF.Elements
{ {
internal class DynamicHost : Element, IStateResettable internal class DynamicHost : Element, IStateResettable, IContent
{ {
private DynamicComponentProxy Child { get; } private DynamicComponentProxy Child { get; }
private object InitialComponentState { get; set; } private object InitialComponentState { get; set; }
+1 -1
View File
@@ -6,7 +6,7 @@ using SkiaSharp;
namespace QuestPDF.Elements namespace QuestPDF.Elements
{ {
internal class DynamicImage : Element internal class DynamicImage : Element, IContent
{ {
public Func<Size, byte[]>? Source { get; set; } public Func<Size, byte[]>? Source { get; set; }
+1 -1
View File
@@ -5,7 +5,7 @@ using SkiaSharp;
namespace QuestPDF.Elements namespace QuestPDF.Elements
{ {
internal class Image : Element, ICacheable internal class Image : Element, ICacheable, IContent
{ {
public SKImage? InternalImage { get; set; } public SKImage? InternalImage { get; set; }
+1 -1
View File
@@ -15,7 +15,7 @@ namespace QuestPDF.Elements
Horizontal Horizontal
} }
internal class Line : Element, ILine, ICacheable internal class Line : Element, ILine, ICacheable, IContent
{ {
public LineType Type { get; set; } = LineType.Vertical; public LineType Type { get; set; } = LineType.Vertical;
public string Color { get; set; } = Colors.Black; public string Color { get; set; } = Colors.Black;
+1 -11
View File
@@ -4,7 +4,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements namespace QuestPDF.Elements
{ {
internal class Padding : ContainerElement, ICacheable, ICollectable internal class Padding : ContainerElement, ICacheable
{ {
public float Top { get; set; } public float Top { get; set; }
public float Right { get; set; } public float Right { get; set; }
@@ -62,15 +62,5 @@ 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;
}
} }
} }
+9
View File
@@ -0,0 +1,9 @@
using QuestPDF.Infrastructure;
namespace QuestPDF.Elements
{
internal class RepeatContent : ContainerElement
{
public bool Repeat { get; set; }
}
}
+1 -6
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, ICollectable internal class Row : Element, ICacheable, IStateResettable
{ {
internal List<RowItem> Items { get; } = new(); internal List<RowItem> Items { get; } = new();
internal float Spacing { get; set; } internal float Spacing { get; set; }
@@ -156,10 +156,5 @@ namespace QuestPDF.Elements
return renderingCommands; return renderingCommands;
} }
public void Collect()
{
Items.Clear();
}
} }
} }
+150
View File
@@ -0,0 +1,150 @@
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 TextBlockSpan textBlockSpan and not TextBlockPageNumber)
{
if (!Settings.CheckIfAllTextGlyphsAreAvailable && textBlockSpan.Style.Fallback == null)
{
yield return textBlockSpan;
continue;
}
var textRuns = textBlockSpan.Text.SplitWithFontFallback(textBlockSpan.Style);
foreach (var textRun in textRuns)
{
yield return new TextBlockSpan
{
Text = textRun.Content,
Style = textRun.Style
};
}
}
else
{
yield return textBlockItem;
}
}
}
}
}
+13 -6
View File
@@ -8,7 +8,7 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Elements.Text namespace QuestPDF.Elements.Text
{ {
internal class TextBlock : Element, IStateResettable, ICollectable internal class TextBlock : Element, IStateResettable, IContent
{ {
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,8 +18,11 @@ 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;
@@ -37,11 +40,15 @@ 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)
Items.Clear(); return;
Items = Items.ApplyFontFallback().ToList();
FontFallbackApplied = true;
}
} }
internal override SpacePlan Measure(Size availableSpace) internal override SpacePlan Measure(Size availableSpace)
+1 -2
View File
@@ -1,5 +1,4 @@
using System; using System;
using QuestPDF.Drawing;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -9,7 +8,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 ?? ElementCacheManager.Get<Border>(); var border = element as Border ?? new Border();
handler(border); handler(border);
return element.Element(border); return element.Element(border);
+10 -8
View File
@@ -1,14 +1,12 @@
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; set; } internal Column Column { get; } = new();
public void Spacing(float value, Unit unit = Unit.Point) public void Spacing(float value, Unit unit = Unit.Point)
{ {
@@ -17,9 +15,14 @@ namespace QuestPDF.Fluent
public IContainer Item() public IContainer Item()
{ {
var columnItem = ElementCacheManager.Get<ColumnItem>(); var container = new Container();
Column.Items.Add(columnItem);
return columnItem; Column.Items.Add(new ColumnItem
{
Child = container
});
return container;
} }
} }
@@ -33,8 +36,7 @@ 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 = ElementCacheManager.Get<ColumnDescriptor>(); var descriptor = new ColumnDescriptor();
descriptor.Column = ElementCacheManager.Get<Column>();
handler(descriptor); handler(descriptor);
element.Element(descriptor.Column); element.Element(descriptor.Column);
} }
+2 -2
View File
@@ -12,7 +12,7 @@ namespace QuestPDF.Fluent
{ {
var container = new Container(); var container = new Container();
Decoration.Before = container; Decoration.Before = container;
return container; return container.RepeatContent();
} }
public void Before(Action<IContainer> handler) public void Before(Action<IContainer> handler)
@@ -36,7 +36,7 @@ namespace QuestPDF.Fluent
{ {
var container = new Container(); var container = new Container();
Decoration.After = container; Decoration.After = container;
return container; return container.RepeatContent();
} }
public void After(Action<IContainer> handler) public void After(Action<IContainer> handler)
+13 -6
View File
@@ -1,5 +1,4 @@
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;
@@ -53,10 +52,10 @@ namespace QuestPDF.Fluent
public static IContainer Background(this IContainer element, string color) public static IContainer Background(this IContainer element, string color)
{ {
var background = ElementCacheManager.Get<Background>(); return element.Element(new 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)
@@ -163,7 +162,7 @@ namespace QuestPDF.Fluent
public static IContainer MinimalBox(this IContainer element) public static IContainer MinimalBox(this IContainer element)
{ {
return element.Element(ElementCacheManager.Get<MinimalBox>()); return element.Element(new MinimalBox());
} }
public static IContainer Unconstrained(this IContainer element) public static IContainer Unconstrained(this IContainer element)
@@ -196,5 +195,13 @@ namespace QuestPDF.Fluent
{ {
return element.Element(new ScaleToFit()); return element.Element(new ScaleToFit());
} }
public static IContainer RepeatContent(this IContainer element, bool enabled = true)
{
return element.Element(new RepeatContent
{
Repeat = enabled
});
}
} }
} }
+1 -2
View File
@@ -1,5 +1,4 @@
using System; using System;
using QuestPDF.Drawing;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -9,7 +8,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 ?? ElementCacheManager.Get<Padding>(); var padding = element as Padding ?? new Padding();
handler(padding); handler(padding);
return element.Element(padding); return element.Element(padding);
+9 -11
View File
@@ -1,5 +1,4 @@
using System; using System;
using QuestPDF.Drawing;
using QuestPDF.Elements; using QuestPDF.Elements;
using QuestPDF.Infrastructure; using QuestPDF.Infrastructure;
@@ -7,7 +6,7 @@ namespace QuestPDF.Fluent
{ {
public class RowDescriptor public class RowDescriptor
{ {
internal Row Row { get; set; } internal Row Row { get; } = new();
public void Spacing(float value) public void Spacing(float value)
{ {
@@ -16,12 +15,14 @@ namespace QuestPDF.Fluent
private IContainer Item(RowItemType type, float size = 0) private IContainer Item(RowItemType type, float size = 0)
{ {
var rowItem = ElementCacheManager.Get<RowItem>(); var element = new RowItem
rowItem.Type = type; {
rowItem.Size = size; Type = type,
Size = size
};
Row.Items.Add(rowItem); Row.Items.Add(element);
return rowItem; return element;
} }
[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.")]
@@ -56,12 +57,9 @@ 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 = ElementCacheManager.Get<RowDescriptor>(); var descriptor = new RowDescriptor();
descriptor.Row = ElementCacheManager.Get<Row>();
handler(descriptor); handler(descriptor);
element.Element(descriptor.Row); element.Element(descriptor.Row);
ElementCacheManager.Store(descriptor);
} }
} }
} }
+1
View File
@@ -36,6 +36,7 @@ namespace QuestPDF.Fluent
internal TextPageNumberDescriptor(Action<TextStyle> assignTextStyle, Action<PageNumberFormatter> assignFormatFunction) : base(assignTextStyle) internal TextPageNumberDescriptor(Action<TextStyle> assignTextStyle, Action<PageNumberFormatter> assignFormatFunction) : base(assignTextStyle)
{ {
AssignFormatFunction = assignFormatFunction; AssignFormatFunction = assignFormatFunction;
AssignFormatFunction(x => x?.ToString());
} }
public TextPageNumberDescriptor Format(PageNumberFormatter formatter) public TextPageNumberDescriptor Format(PageNumberFormatter formatter)
@@ -15,6 +15,17 @@ namespace QuestPDF.Fluent
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.MutateTextStyle(x => x.FontColor(value)); descriptor.MutateTextStyle(x => x.FontColor(value));
+16 -2
View File
@@ -4,8 +4,6 @@ using QuestPDF.Infrastructure;
namespace QuestPDF.Fluent namespace QuestPDF.Fluent
{ {
public static class TextStyleExtensions public static class TextStyleExtensions
{ {
[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.")]
@@ -131,6 +129,7 @@ namespace QuestPDF.Fluent
#endregion #endregion
#region Position #region Position
public static TextStyle NormalPosition(this TextStyle style) public static TextStyle NormalPosition(this TextStyle style)
{ {
return style.Position(FontPosition.Normal); return style.Position(FontPosition.Normal);
@@ -150,6 +149,21 @@ namespace QuestPDF.Fluent
{ {
return style.Mutate(TextStyleProperty.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 #endregion
} }
} }
+1 -6
View File
@@ -5,7 +5,7 @@ using QuestPDF.Elements;
namespace QuestPDF.Infrastructure namespace QuestPDF.Infrastructure
{ {
internal abstract class ContainerElement : Element, IContainer, ICollectable internal abstract class ContainerElement : Element, IContainer
{ {
internal Element? Child { get; set; } = Empty.Instance; internal Element? Child { get; set; } = Empty.Instance;
@@ -34,10 +34,5 @@ namespace QuestPDF.Infrastructure
{ {
Child?.Draw(availableSpace); Child?.Draw(availableSpace);
} }
public virtual void Collect()
{
Child = default;
}
} }
} }
-7
View File
@@ -1,7 +0,0 @@
namespace QuestPDF.Infrastructure
{
public interface ICollectable
{
void Collect();
}
}
+7
View File
@@ -0,0 +1,7 @@
namespace QuestPDF.Infrastructure
{
internal interface IContent
{
}
}
+4 -1
View File
@@ -17,6 +17,8 @@ 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() internal static TextStyle LibraryDefault { get; } = new()
{ {
Color = Colors.Black, Color = Colors.Black,
@@ -29,7 +31,8 @@ namespace QuestPDF.Infrastructure
IsItalic = false, IsItalic = false,
HasStrikethrough = false, HasStrikethrough = false,
HasUnderline = false, HasUnderline = false,
WrapAnywhere = false WrapAnywhere = false,
Fallback = null
}; };
public static TextStyle Default { get; } = new(); public static TextStyle Default { get; } = new();
+53 -20
View File
@@ -16,13 +16,15 @@ namespace QuestPDF.Infrastructure
IsItalic, IsItalic,
HasStrikethrough, HasStrikethrough,
HasUnderline, HasUnderline,
WrapAnywhere WrapAnywhere,
Fallback
} }
internal static class TextStyleManager internal static class TextStyleManager
{ {
public static ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new(); private static readonly ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new();
public static ConcurrentDictionary<(TextStyle origin, TextStyle parent, bool overrideValue), TextStyle> TextStyleApplyCache = 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) public static TextStyle Mutate(this TextStyle origin, TextStyleProperty property, object value)
{ {
@@ -30,7 +32,7 @@ namespace QuestPDF.Infrastructure
return TextStyleMutateCache.GetOrAdd(cacheKey, x => MutateStyle(x.origin, x.property, x.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) private static TextStyle MutateStyle(TextStyle origin, TextStyleProperty property, object? value, bool overrideValue = true)
{ {
if (overrideValue && value == null) if (overrideValue && value == null)
return origin; return origin;
@@ -177,38 +179,69 @@ namespace QuestPDF.Infrastructure
return origin with { WrapAnywhere = castedValue }; 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."); 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) internal static TextStyle ApplyGlobalStyle(this TextStyle style, TextStyle parent)
{ {
var cacheKey = (style, parent, false); var cacheKey = (style, parent);
return TextStyleApplyCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, key.overrideValue)); 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) internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
{ {
var cacheKey = (style, parent, true); var cacheKey = (style, parent);
return TextStyleApplyCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, key.overrideValue));
return TextStyleOverrideCache.GetOrAdd(cacheKey, key =>
{
var result = ApplyStyle(key.origin, key.parent);
return MutateStyle(result, TextStyleProperty.Fallback, key.parent.Fallback);
});
} }
private static TextStyle ApplyStyle(TextStyle style, TextStyle parent, bool overrideValue) private static TextStyle ApplyStyle(this TextStyle style, TextStyle parent, bool overrideStyle = true, bool applyFallback = true)
{ {
var result = style; var result = style;
result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideValue); result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideStyle);
result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideValue); result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideStyle);
result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideValue); result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle);
result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideValue); result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideStyle);
result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideValue); result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideStyle);
result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideValue); result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideStyle);
result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideValue); result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideStyle);
result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideValue); result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideStyle);
result = MutateStyle(result, TextStyleProperty.HasStrikethrough, parent.HasStrikethrough, overrideValue); result = MutateStyle(result, TextStyleProperty.HasStrikethrough, parent.HasStrikethrough, overrideStyle);
result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideValue); result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideStyle);
result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideValue); result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideStyle);
if (applyFallback)
result = MutateStyle(result, TextStyleProperty.Fallback, parent.Fallback, overrideStyle);
return result; return result;
} }
} }
+1 -1
View File
@@ -19,7 +19,7 @@ namespace QuestPDF.Previewer
public event Action? OnPreviewerStopped; public event Action? OnPreviewerStopped;
private const int RequiredPreviewerVersionMajor = 2022; private const int RequiredPreviewerVersionMajor = 2022;
private const int RequiredPreviewerVersionMinor = 8; private const int RequiredPreviewerVersionMinor = 9;
public PreviewerService(int port) public PreviewerService(int port)
{ {
+1 -1
View File
@@ -3,7 +3,7 @@
<Authors>MarcinZiabek</Authors> <Authors>MarcinZiabek</Authors>
<Company>CodeFlint</Company> <Company>CodeFlint</Company>
<PackageId>QuestPDF</PackageId> <PackageId>QuestPDF</PackageId>
<Version>2022.8.2</Version> <Version>2022.9.0</Version>
<PackageDescription>QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API.</PackageDescription> <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> <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
<LangVersion>9</LangVersion> <LangVersion>9</LangVersion>
+6 -20
View File
@@ -1,20 +1,6 @@
2022.8.0: 2022.9.0
- Implemented font-fallback algorithm,
- Improved library performance, - Introduced new Settings API,
- Breaking change: changed default font from Calibri to an open-source Lato, - Significantly reduced memory allocation cost for TextStyle objects,
- Default font files are included with the nuget package, making it safe to deploy on any environment, - Implemented optional checking if all font glyphs are available,
- Default font files are significantly smaller, so output document files should be smaller too (up to 20x reduction in size), - Minor text-rendering optimizations.
- When requested font is not available on the runtime environment, library provides list of available fonts,
- Fixed a rare layout overflow exception with the Inlined element,
- Fixed a memory leak connected to the HarfBuzz library.
2022.8.1:
- Fixed: default text style does not always work
- Fixed: page breaking rendering does not work in very specific corner cases
- Stability improvements for text wrapping
- Updated stability of rendering elements in negative space
- Optimization for the Column element: do not measure child when available height is negative
2022.8.2
- Fixed: the Column element incorrectly renders zero-height elements.
+2 -2
View File
@@ -3,7 +3,7 @@
public static class Settings public static class Settings
{ {
/// <summary> /// <summary>
/// This value represents the maximum lenght of the document that the library produces. /// This value represents the maximum length of the document that the library produces.
/// This is useful when layout constraints are too strong, e.g. one element does not fit in another. /// 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. /// 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. /// To break the algorithm and save the environment, the library breaks the rendering process after reaching specified length of document.
@@ -35,6 +35,6 @@
/// However, it provides hints that used fonts are not sufficient to produce correct results. /// However, it provides hints that used fonts are not sufficient to produce correct results.
/// </summary> /// </summary>
/// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks> /// <remarks>By default, this flag is enabled only when the debugger IS attached.</remarks>
public static bool CheckIfAllTextGlyphsAreAvailableInSpecifiedFont { get; set; } = System.Diagnostics.Debugger.IsAttached; public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached;
} }
} }
+2 -1
View File
@@ -23,7 +23,8 @@ Choosing a project dependency could be difficult. We need to ensure stability an
⭐ Please give this repository a star. It takes seconds and help thousands of developers! ⭐ ⭐ Please give this repository a star. It takes seconds and help thousands of developers! ⭐
<img src="https://user-images.githubusercontent.com/9263853/184642026-27dd7567-a46a-45d4-9594-e6a70a7193e9.png" width="700" /> <img src="https://user-images.githubusercontent.com/9263853/190931857-8ca52ec8-cc7d-4d12-9467-4442b3342fa1.png" width="700" />
## Please share with the community ## Please share with the community