From 0468dd5f02cd2f6abe8e5daf108a4759f464b6b9 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 8 Sep 2022 13:23:59 +0200 Subject: [PATCH 01/14] Font-fallback implementation --- QuestPDF.Examples/TextExamples.cs | 36 ++++++++ .../Exceptions/DocumentDrawingException.cs | 5 ++ QuestPDF/Elements/Text/FontFallback.cs | 84 +++++++++++++++++++ QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 2 +- QuestPDF/Elements/Text/TextBlock.cs | 12 +++ QuestPDF/Infrastructure/TextStyle.cs | 11 +-- 6 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 QuestPDF/Elements/Text/FontFallback.cs diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index c68252a..8acda8b 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Text; using NUnit.Framework; +using QuestPDF.Elements.Text; using QuestPDF.Examples.Engine; using QuestPDF.Fluent; using QuestPDF.Helpers; @@ -618,5 +619,40 @@ namespace QuestPDF.Examples .FontSize(20); }); } + + [Test] + public void FontFallback() + { + RenderingTest + .Create() + .ProducePdf() + .ShowResults() + .RenderDocument(container => + { + container.Page(page => + { + page.Margin(50); + page.PageColor(Colors.White); + + 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: 😊😅🥳👍❤😍👌"); + }); + }); + }); + } } } \ No newline at end of file diff --git a/QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs b/QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs index a650d90..01a63bc 100644 --- a/QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs +++ b/QuestPDF/Drawing/Exceptions/DocumentDrawingException.cs @@ -4,6 +4,11 @@ namespace QuestPDF.Drawing.Exceptions { public class DocumentDrawingException : Exception { + internal DocumentDrawingException(string message) : base(message) + { + + } + internal DocumentDrawingException(string message, Exception inner) : base(message, inner) { diff --git a/QuestPDF/Elements/Text/FontFallback.cs b/QuestPDF/Elements/Text/FontFallback.cs new file mode 100644 index 0000000..387d794 --- /dev/null +++ b/QuestPDF/Elements/Text/FontFallback.cs @@ -0,0 +1,84 @@ +using System.Collections.Generic; +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; } + } + + private static SKFontManager FontManager => SKFontManager.Default; + + public static IEnumerable SplitWithFontFallback(this string text, TextStyle textStyle) + { + var partStartIndex = 0; + var partTextStyle = textStyle; + + for (var i = 0; i < text.Length; i += char.IsSurrogatePair(text, i) ? 2 : 1) + { + var codepoint = char.ConvertToUtf32(text, i); + var font = partTextStyle.ToFont(); + var typeface = font.Typeface; + + if (font.ContainsGlyph(codepoint)) + continue; + + var fallbackTypeface = FontManager.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codepoint); + + if (fallbackTypeface == null) + throw new DocumentDrawingException($"Could not find an appropriate font fallback for text: '{text}'"); + + yield return new TextRun + { + Content = text.Substring(partStartIndex, i - partStartIndex), + Style = partTextStyle + }; + + partStartIndex = i; + partTextStyle = textStyle.FontFamily(fallbackTypeface.FamilyName).Weight((FontWeight)fallbackTypeface.FontWeight); + } + + if (partStartIndex > text.Length) + yield break; + + yield return new TextRun + { + Content = text.Substring(partStartIndex, text.Length - partStartIndex), + Style = partTextStyle + }; + } + + public static IEnumerable ApplyFontFallback(this ICollection textBlockItems) + { + foreach (var textBlockItem in textBlockItems) + { + if (textBlockItem is TextBlockSpan textBlockSpan) + { + 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; + } + } + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 033931e..701e60a 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -15,7 +15,7 @@ namespace QuestPDF.Elements.Text.Items { public string Text { get; set; } public TextStyle Style { get; set; } = new(); - public TextShapingResult? TextShapingResult { get; set; } + private TextShapingResult? TextShapingResult { get; set; } private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new (); protected virtual bool EnableTextCache => true; diff --git a/QuestPDF/Elements/Text/TextBlock.cs b/QuestPDF/Elements/Text/TextBlock.cs index 6420a7d..c14f8cc 100644 --- a/QuestPDF/Elements/Text/TextBlock.cs +++ b/QuestPDF/Elements/Text/TextBlock.cs @@ -18,8 +18,11 @@ namespace QuestPDF.Elements.Text private Queue RenderingQueue { get; set; } private int CurrentElementIndex { get; set; } + private bool FontFallbackApplied { get; set; } = false; + public void ResetState() { + ApplyFontFallback(); InitializeQueue(); CurrentElementIndex = 0; @@ -37,6 +40,15 @@ namespace QuestPDF.Elements.Text foreach (var item in Items) RenderingQueue.Enqueue(item); } + + void ApplyFontFallback() + { + if (FontFallbackApplied) + return; + + Items = Items.ApplyFontFallback().ToList(); + FontFallbackApplied = true; + } } internal override SpacePlan Measure(Size availableSpace) diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index 8eb91f9..cea2719 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -19,12 +19,10 @@ namespace QuestPDF.Infrastructure internal bool? HasUnderline { get; set; } internal bool? WrapAnywhere { get; set; } - internal object PaintKey { get; private set; } - internal object FontMetricsKey { get; private set; } + // 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); - // REVIEW: Should this be a method call that news up a TextStyle, - // or can it be a static variable? - // (style mutations seem to create a clone anyway) internal static readonly TextStyle LibraryDefault = new TextStyle { Color = Colors.Black, @@ -49,10 +47,7 @@ namespace QuestPDF.Infrastructure return; HasGlobalStyleApplied = true; - ApplyParentStyle(globalStyle); - PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color); - FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic); } internal void ApplyParentStyle(TextStyle parentStyle) From bc853f48fc0734c48a736f6fe88f95ce7318d43b Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 8 Sep 2022 16:49:28 +0200 Subject: [PATCH 02/14] Implemented TextStyleManager to reduce memory usage --- QuestPDF/Drawing/DocumentGenerator.cs | 15 +- QuestPDF/Drawing/FontManager.cs | 26 +-- QuestPDF/Elements/Dynamic.cs | 2 +- QuestPDF/Elements/Text/Items/TextBlockSpan.cs | 2 +- QuestPDF/Fluent/TextExtensions.cs | 73 +++--- .../Fluent/TextSpanDescriptorExtensions.cs | 70 +++--- QuestPDF/Fluent/TextStyleExtensions.cs | 35 ++- QuestPDF/Infrastructure/TextStyle.cs | 64 +----- QuestPDF/Infrastructure/TextStyleManager.cs | 212 ++++++++++++++++++ 9 files changed, 316 insertions(+), 183 deletions(-) create mode 100644 QuestPDF/Infrastructure/TextStyleManager.cs diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index c24e285..84b0b3d 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -172,7 +172,7 @@ namespace QuestPDF.Drawing { if (textBlockItem is TextBlockSpan textSpan) { - textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle); + textSpan.Style = textSpan.Style.ApplyGlobalStyle(TextStyle.LibraryDefault); } else if (textBlockItem is TextBlockElement textElement) { @@ -184,18 +184,13 @@ namespace QuestPDF.Drawing } if (content is DynamicHost dynamicHost) - dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle); - - var targetTextStyle = documentDefaultTextStyle; + dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle); if (content is DefaultTextStyle defaultTextStyleElement) - { - defaultTextStyleElement.TextStyle.ApplyParentStyle(documentDefaultTextStyle); - targetTextStyle = defaultTextStyleElement.TextStyle; - } - + documentDefaultTextStyle = defaultTextStyleElement.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle); + foreach (var child in content.GetChildren()) - ApplyDefaultTextStyle(child, targetTextStyle); + ApplyDefaultTextStyle(child, documentDefaultTextStyle); } } } \ No newline at end of file diff --git a/QuestPDF/Drawing/FontManager.cs b/QuestPDF/Drawing/FontManager.cs index 280cca8..0f90b7f 100644 --- a/QuestPDF/Drawing/FontManager.cs +++ b/QuestPDF/Drawing/FontManager.cs @@ -14,13 +14,13 @@ namespace QuestPDF.Drawing { public static class FontManager { - private static ConcurrentDictionary StyleSets = new(); - private static ConcurrentDictionary FontMetrics = new(); - private static ConcurrentDictionary FontPaints = new(); - private static ConcurrentDictionary ColorPaints = new(); - private static ConcurrentDictionary ShaperFonts = new(); - private static ConcurrentDictionary Fonts = new(); - private static ConcurrentDictionary TextShapers = new(); + private static readonly ConcurrentDictionary StyleSets = new(); + private static readonly ConcurrentDictionary FontMetrics = new(); + private static readonly ConcurrentDictionary FontPaints = new(); + private static readonly ConcurrentDictionary ColorPaints = new(); + private static readonly ConcurrentDictionary ShaperFonts = new(); + private static readonly ConcurrentDictionary Fonts = new(); + private static readonly ConcurrentDictionary TextShapers = new(); static FontManager() { @@ -110,7 +110,7 @@ namespace QuestPDF.Drawing internal static SKPaint ToPaint(this TextStyle style) { - return FontPaints.GetOrAdd(style.PaintKey, key => Convert(style)); + return FontPaints.GetOrAdd(style, Convert); static SKPaint Convert(TextStyle style) { @@ -172,14 +172,14 @@ namespace QuestPDF.Drawing internal static SKFontMetrics ToFontMetrics(this TextStyle style) { - return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics); + return FontMetrics.GetOrAdd(style, key => key.NormalPosition().ToPaint().FontMetrics); } internal static Font ToShaperFont(this TextStyle style) { - return ShaperFonts.GetOrAdd(style.PaintKey, _ => + return ShaperFonts.GetOrAdd(style, key => { - var typeface = style.ToPaint().Typeface; + var typeface = key.ToPaint().Typeface; using var harfBuzzBlob = typeface.OpenStream(out var ttcIndex).ToHarfBuzzBlob(); @@ -200,12 +200,12 @@ namespace QuestPDF.Drawing internal static TextShaper ToTextShaper(this TextStyle style) { - return TextShapers.GetOrAdd(style.PaintKey, _ => new TextShaper(style)); + return TextShapers.GetOrAdd(style, key => new TextShaper(key)); } internal static SKFont ToFont(this TextStyle style) { - return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont()); + return Fonts.GetOrAdd(style, key => key.ToPaint().ToFont()); } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Dynamic.cs b/QuestPDF/Elements/Dynamic.cs index ce8ba74..482704a 100644 --- a/QuestPDF/Elements/Dynamic.cs +++ b/QuestPDF/Elements/Dynamic.cs @@ -11,7 +11,7 @@ namespace QuestPDF.Elements private DynamicComponentProxy Child { get; } private object InitialComponentState { get; set; } - internal TextStyle TextStyle { get; } = new(); + internal TextStyle TextStyle { get; set; } = TextStyle.Default; public DynamicHost(DynamicComponentProxy child) { diff --git a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs index 033931e..4dfad0f 100644 --- a/QuestPDF/Elements/Text/Items/TextBlockSpan.cs +++ b/QuestPDF/Elements/Text/Items/TextBlockSpan.cs @@ -14,7 +14,7 @@ namespace QuestPDF.Elements.Text.Items internal class TextBlockSpan : ITextBlockItem { public string Text { get; set; } - public TextStyle Style { get; set; } = new(); + public TextStyle Style { get; set; } = TextStyle.Default; public TextShapingResult? TextShapingResult { get; set; } private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new (); diff --git a/QuestPDF/Fluent/TextExtensions.cs b/QuestPDF/Fluent/TextExtensions.cs index 80416a0..2b2c796 100644 --- a/QuestPDF/Fluent/TextExtensions.cs +++ b/QuestPDF/Fluent/TextExtensions.cs @@ -12,11 +12,18 @@ namespace QuestPDF.Fluent { public class TextSpanDescriptor { - internal TextStyle TextStyle { get; } + internal TextStyle TextStyle = TextStyle.Default; + internal Action AssignTextStyle { get; } - internal TextSpanDescriptor(TextStyle textStyle) + internal TextSpanDescriptor(Action assignTextStyle) { - TextStyle = textStyle; + AssignTextStyle = assignTextStyle; + } + + internal void MutateTextStyle(Func handler) + { + TextStyle = handler(TextStyle); + AssignTextStyle(TextStyle); } } @@ -24,16 +31,16 @@ namespace QuestPDF.Fluent public class TextPageNumberDescriptor : TextSpanDescriptor { - internal PageNumberFormatter FormatFunction { get; private set; } = x => x?.ToString() ?? string.Empty; - - internal TextPageNumberDescriptor(TextStyle textStyle) : base(textStyle) + internal Action AssignFormatFunction { get; } + + internal TextPageNumberDescriptor(Action assignTextStyle, Action assignFormatFunction) : base(assignTextStyle) { - + AssignFormatFunction = assignFormatFunction; } public TextPageNumberDescriptor Format(PageNumberFormatter formatter) { - FormatFunction = formatter ?? FormatFunction; + AssignFormatFunction(formatter); return this; } } @@ -91,19 +98,15 @@ namespace QuestPDF.Fluent public TextSpanDescriptor Span(string? text) { - var style = DefaultStyle.Clone(); - var descriptor = new TextSpanDescriptor(style); - if (text == null) - return descriptor; + return new TextSpanDescriptor(_ => { }); var items = text .Replace("\r", string.Empty) .Split(new[] { '\n' }, StringSplitOptions.None) .Select(x => new TextBlockSpan { - Text = x, - Style = style + Text = x }) .ToList(); @@ -118,7 +121,7 @@ namespace QuestPDF.Fluent .ToList() .ForEach(TextBlocks.Add); - return descriptor; + return new TextSpanDescriptor(x => items.ForEach(y => y.Style = x)); } public TextSpanDescriptor Line(string? text) @@ -134,16 +137,10 @@ namespace QuestPDF.Fluent private TextPageNumberDescriptor PageNumber(Func pageNumber) { - var style = DefaultStyle.Clone(); - var descriptor = new TextPageNumberDescriptor(style); + var textBlockItem = new TextBlockPageNumber(); + AddItemToLastTextBlock(textBlockItem); - AddItemToLastTextBlock(new TextBlockPageNumber - { - Source = context => descriptor.FormatFunction(pageNumber(context)), - Style = style - }); - - return descriptor; + return new TextPageNumberDescriptor(x => textBlockItem.Style = x, x => textBlockItem.Source = context => x(pageNumber(context))); } public TextPageNumberDescriptor CurrentPageNumber() @@ -187,20 +184,17 @@ namespace QuestPDF.Fluent if (IsNullOrEmpty(sectionName)) throw new ArgumentException("Section name cannot be null or empty", nameof(sectionName)); - var style = DefaultStyle.Clone(); - var descriptor = new TextSpanDescriptor(style); - if (IsNullOrEmpty(text)) - return descriptor; - - AddItemToLastTextBlock(new TextBlockSectionLink + return new TextSpanDescriptor(_ => { }); + + var textBlockItem = new TextBlockSectionLink { - Style = style, Text = text, SectionName = sectionName - }); + }; - return descriptor; + AddItemToLastTextBlock(textBlockItem); + return new TextSpanDescriptor(x => textBlockItem.Style = x); } [Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")] @@ -214,20 +208,17 @@ namespace QuestPDF.Fluent if (IsNullOrEmpty(url)) throw new ArgumentException("Url cannot be null or empty", nameof(url)); - var style = DefaultStyle.Clone(); - var descriptor = new TextSpanDescriptor(style); - if (IsNullOrEmpty(text)) - return descriptor; + return new TextSpanDescriptor(_ => { }); - AddItemToLastTextBlock(new TextBlockHyperlink + var textBlockItem = new TextBlockHyperlink { - Style = style, Text = text, Url = url - }); + }; - return descriptor; + AddItemToLastTextBlock(textBlockItem); + return new TextSpanDescriptor(x => textBlockItem.Style = x); } [Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")] diff --git a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs index 41af136..5bc12e8 100644 --- a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs +++ b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs @@ -11,120 +11,124 @@ namespace QuestPDF.Fluent if (style == null) return descriptor; - descriptor.TextStyle.OverrideStyle(style); + descriptor.MutateTextStyle(x => x.OverrideStyle(style)); return descriptor; } public static T FontColor(this T descriptor, string value) where T : TextSpanDescriptor { - descriptor.TextStyle.Color = value; + descriptor.MutateTextStyle(x => x.FontColor(value)); return descriptor; } public static T BackgroundColor(this T descriptor, string value) where T : TextSpanDescriptor { - descriptor.TextStyle.BackgroundColor = value; + descriptor.MutateTextStyle(x => x.BackgroundColor(value)); return descriptor; } public static T FontFamily(this T descriptor, string value) where T : TextSpanDescriptor { - descriptor.TextStyle.FontFamily = value; + descriptor.MutateTextStyle(x => x.FontFamily(value)); return descriptor; } public static T FontSize(this T descriptor, float value) where T : TextSpanDescriptor { - descriptor.TextStyle.Size = value; + descriptor.MutateTextStyle(x => x.FontSize(value)); return descriptor; } public static T LineHeight(this T descriptor, float value) where T : TextSpanDescriptor { - descriptor.TextStyle.LineHeight = value; + descriptor.MutateTextStyle(x => x.LineHeight(value)); return descriptor; } public static T Italic(this T descriptor, bool value = true) where T : TextSpanDescriptor { - descriptor.TextStyle.IsItalic = value; + descriptor.MutateTextStyle(x => x.Italic(value)); return descriptor; } public static T Strikethrough(this T descriptor, bool value = true) where T : TextSpanDescriptor { - descriptor.TextStyle.HasStrikethrough = value; + descriptor.MutateTextStyle(x => x.Strikethrough(value)); return descriptor; } public static T Underline(this T descriptor, bool value = true) where T : TextSpanDescriptor { - descriptor.TextStyle.HasUnderline = value; + descriptor.MutateTextStyle(x => x.Underline(value)); return descriptor; } public static T WrapAnywhere(this T descriptor, bool value = true) where T : TextSpanDescriptor { - descriptor.TextStyle.WrapAnywhere = value; + descriptor.MutateTextStyle(x => x.WrapAnywhere(value)); return descriptor; } #region Weight - public static T Weight(this T descriptor, FontWeight weight) where T : TextSpanDescriptor - { - descriptor.TextStyle.FontWeight = weight; - return descriptor; - } - public static T Thin(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.Thin); + descriptor.MutateTextStyle(x => x.Thin()); + return descriptor; } public static T ExtraLight(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.ExtraLight); + descriptor.MutateTextStyle(x => x.ExtraLight()); + return descriptor; } public static T Light(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.Light); + descriptor.MutateTextStyle(x => x.Light()); + return descriptor; } public static T NormalWeight(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.Normal); + descriptor.MutateTextStyle(x => x.NormalWeight()); + return descriptor; } public static T Medium(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.Medium); + descriptor.MutateTextStyle(x => x.Medium()); + return descriptor; } public static T SemiBold(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.SemiBold); + descriptor.MutateTextStyle(x => x.SemiBold()); + return descriptor; } public static T Bold(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.Bold); + descriptor.MutateTextStyle(x => x.Bold()); + return descriptor; } public static T ExtraBold(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.ExtraBold); + descriptor.MutateTextStyle(x => x.ExtraBold()); + return descriptor; } public static T Black(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.Black); + descriptor.MutateTextStyle(x => x.Black()); + return descriptor; } public static T ExtraBlack(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Weight(FontWeight.ExtraBlack); + descriptor.MutateTextStyle(x => x.ExtraBlack()); + return descriptor; } #endregion @@ -132,24 +136,22 @@ namespace QuestPDF.Fluent #region Position public static T NormalPosition(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Position(FontPosition.Normal); + descriptor.MutateTextStyle(x => x.NormalPosition()); + return descriptor; } public static T Subscript(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Position(FontPosition.Subscript); + descriptor.MutateTextStyle(x => x.Subscript()); + return descriptor; } public static T Superscript(this T descriptor) where T : TextSpanDescriptor { - return descriptor.Position(FontPosition.Superscript); - } - - private static T Position(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor - { - descriptor.TextStyle.FontPosition = fontPosition; + descriptor.MutateTextStyle(x => x.Superscript()); return descriptor; } + #endregion } } \ No newline at end of file diff --git a/QuestPDF/Fluent/TextStyleExtensions.cs b/QuestPDF/Fluent/TextStyleExtensions.cs index 7823908..e573982 100644 --- a/QuestPDF/Fluent/TextStyleExtensions.cs +++ b/QuestPDF/Fluent/TextStyleExtensions.cs @@ -4,16 +4,10 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Fluent { + + public static class TextStyleExtensions { - private static TextStyle Mutate(this TextStyle style, Action handler) - { - style = style.Clone(); - - handler(style); - return style; - } - [Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")] public static TextStyle Color(this TextStyle style, string value) { @@ -22,12 +16,12 @@ namespace QuestPDF.Fluent public static TextStyle FontColor(this TextStyle style, string value) { - return style.Mutate(x => x.Color = value); + return style.Mutate(TextStyleProperty.Color, value); } public static TextStyle BackgroundColor(this TextStyle style, string value) { - return style.Mutate(x => x.BackgroundColor = value); + return style.Mutate(TextStyleProperty.BackgroundColor, value); } [Obsolete("This element has been renamed since version 2022.3. Please use the FontFamily method.")] @@ -38,7 +32,7 @@ namespace QuestPDF.Fluent public static TextStyle FontFamily(this TextStyle style, string value) { - return style.Mutate(x => x.FontFamily = value); + return style.Mutate(TextStyleProperty.FontFamily, value); } [Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")] @@ -49,39 +43,39 @@ namespace QuestPDF.Fluent public static TextStyle FontSize(this TextStyle style, float value) { - return style.Mutate(x => x.Size = value); + return style.Mutate(TextStyleProperty.Size, value); } public static TextStyle LineHeight(this TextStyle style, float value) { - return style.Mutate(x => x.LineHeight = value); + return style.Mutate(TextStyleProperty.LineHeight, value); } public static TextStyle Italic(this TextStyle style, bool value = true) { - return style.Mutate(x => x.IsItalic = value); + return style.Mutate(TextStyleProperty.IsItalic, value); } public static TextStyle Strikethrough(this TextStyle style, bool value = true) { - return style.Mutate(x => x.HasStrikethrough = value); + return style.Mutate(TextStyleProperty.HasStrikethrough, value); } public static TextStyle Underline(this TextStyle style, bool value = true) { - return style.Mutate(x => x.HasUnderline = value); + return style.Mutate(TextStyleProperty.HasUnderline, value); } public static TextStyle WrapAnywhere(this TextStyle style, bool value = true) { - return style.Mutate(x => x.WrapAnywhere = value); + return style.Mutate(TextStyleProperty.WrapAnywhere, value); } #region Weight public static TextStyle Weight(this TextStyle style, FontWeight weight) { - return style.Mutate(x => x.FontWeight = weight); + return style.Mutate(TextStyleProperty.FontWeight, weight); } public static TextStyle Thin(this TextStyle style) @@ -154,10 +148,7 @@ namespace QuestPDF.Fluent private static TextStyle Position(this TextStyle style, FontPosition fontPosition) { - if (style.FontPosition == fontPosition) - return style; - - return style.Mutate(t => t.FontPosition = fontPosition); + return style.Mutate(TextStyleProperty.FontPosition, fontPosition); } #endregion } diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index 8eb91f9..2a49d9a 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -3,10 +3,8 @@ using QuestPDF.Helpers; namespace QuestPDF.Infrastructure { - public class TextStyle + public record TextStyle { - internal bool HasGlobalStyleApplied { get; private set; } - internal string? Color { get; set; } internal string? BackgroundColor { get; set; } internal string? FontFamily { get; set; } @@ -19,13 +17,7 @@ namespace QuestPDF.Infrastructure internal bool? HasUnderline { get; set; } internal bool? WrapAnywhere { get; set; } - internal object PaintKey { get; private set; } - internal object FontMetricsKey { get; private set; } - - // REVIEW: Should this be a method call that news up a TextStyle, - // or can it be a static variable? - // (style mutations seem to create a clone anyway) - internal static readonly TextStyle LibraryDefault = new TextStyle + internal static TextStyle LibraryDefault { get; } = new() { Color = Colors.Black, BackgroundColor = Colors.Transparent, @@ -40,56 +32,6 @@ namespace QuestPDF.Infrastructure WrapAnywhere = false }; - // it is important to create new instances for the DefaultTextStyle element to work correctly - public static TextStyle Default => new TextStyle(); - - internal void ApplyGlobalStyle(TextStyle globalStyle) - { - if (HasGlobalStyleApplied) - return; - - HasGlobalStyleApplied = true; - - ApplyParentStyle(globalStyle); - PaintKey ??= (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color); - FontMetricsKey ??= (FontFamily, Size, FontWeight, IsItalic); - } - - internal void ApplyParentStyle(TextStyle parentStyle) - { - Color ??= parentStyle.Color; - BackgroundColor ??= parentStyle.BackgroundColor; - FontFamily ??= parentStyle.FontFamily; - Size ??= parentStyle.Size; - LineHeight ??= parentStyle.LineHeight; - FontWeight ??= parentStyle.FontWeight; - FontPosition ??= parentStyle.FontPosition; - IsItalic ??= parentStyle.IsItalic; - HasStrikethrough ??= parentStyle.HasStrikethrough; - HasUnderline ??= parentStyle.HasUnderline; - WrapAnywhere ??= parentStyle.WrapAnywhere; - } - - internal void OverrideStyle(TextStyle parentStyle) - { - Color = parentStyle.Color ?? Color; - BackgroundColor = parentStyle.BackgroundColor ?? BackgroundColor; - FontFamily = parentStyle.FontFamily ?? FontFamily; - Size = parentStyle.Size ?? Size; - LineHeight = parentStyle.LineHeight ?? LineHeight; - FontWeight = parentStyle.FontWeight ?? FontWeight; - FontPosition = parentStyle.FontPosition ?? FontPosition; - IsItalic = parentStyle.IsItalic ?? IsItalic; - HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough; - HasUnderline = parentStyle.HasUnderline ?? HasUnderline; - WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere; - } - - internal TextStyle Clone() - { - var clone = (TextStyle)MemberwiseClone(); - clone.HasGlobalStyleApplied = false; - return clone; - } + public static TextStyle Default { get; } = new(); } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/TextStyleManager.cs b/QuestPDF/Infrastructure/TextStyleManager.cs new file mode 100644 index 0000000..9e46a73 --- /dev/null +++ b/QuestPDF/Infrastructure/TextStyleManager.cs @@ -0,0 +1,212 @@ +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 (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; + } + } +} \ No newline at end of file From d4448437aca24737f56460a98d68f23e5eb91dfa Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 8 Sep 2022 17:29:52 +0200 Subject: [PATCH 03/14] Reduced creation of DefaultTextStyle objects --- QuestPDF/Fluent/TextExtensions.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/QuestPDF/Fluent/TextExtensions.cs b/QuestPDF/Fluent/TextExtensions.cs index 2b2c796..92ae7c6 100644 --- a/QuestPDF/Fluent/TextExtensions.cs +++ b/QuestPDF/Fluent/TextExtensions.cs @@ -48,7 +48,7 @@ namespace QuestPDF.Fluent public class TextDescriptor { private ICollection TextBlocks { get; } = new List(); - private TextStyle DefaultStyle { get; set; } = TextStyle.Default; + private TextStyle? DefaultStyle { get; set; } internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; private float Spacing { get; set; } = 0f; @@ -242,7 +242,9 @@ namespace QuestPDF.Fluent internal void Compose(IContainer container) { TextBlocks.ToList().ForEach(x => x.Alignment = Alignment); - container = container.DefaultTextStyle(DefaultStyle); + + if (DefaultStyle != null) + container = container.DefaultTextStyle(DefaultStyle); if (TextBlocks.Count == 1) { From 1ba01a1cf5446e023c90f4adbea6eecff5c7786f Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Fri, 9 Sep 2022 11:32:05 +0200 Subject: [PATCH 04/14] Fixed override value --- QuestPDF/Infrastructure/TextStyleManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/QuestPDF/Infrastructure/TextStyleManager.cs b/QuestPDF/Infrastructure/TextStyleManager.cs index 9e46a73..0d30c84 100644 --- a/QuestPDF/Infrastructure/TextStyleManager.cs +++ b/QuestPDF/Infrastructure/TextStyleManager.cs @@ -32,6 +32,9 @@ namespace QuestPDF.Infrastructure 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) From 5390fc3f1bc9d704bc16672302f59f9447d10b0c Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sat, 10 Sep 2022 23:27:27 +0200 Subject: [PATCH 05/14] Implemented font-fallback as required configuration --- QuestPDF.Examples/TextExamples.cs | 9 +- QuestPDF/Elements/Text/FontFallback.cs | 101 ++++++++++++++---- .../Fluent/TextSpanDescriptorExtensions.cs | 11 ++ QuestPDF/Fluent/TextStyleExtensions.cs | 10 ++ QuestPDF/Infrastructure/TextStyle.cs | 22 +++- 5 files changed, 129 insertions(+), 24 deletions(-) diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index 8acda8b..905bcb8 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -625,7 +625,7 @@ namespace QuestPDF.Examples { RenderingTest .Create() - .ProducePdf() + .ProduceImages() .ShowResults() .RenderDocument(container => { @@ -633,6 +633,9 @@ namespace QuestPDF.Examples { 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); @@ -640,11 +643,11 @@ namespace QuestPDF.Examples { 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(); diff --git a/QuestPDF/Elements/Text/FontFallback.cs b/QuestPDF/Elements/Text/FontFallback.cs index 387d794..79caeba 100644 --- a/QuestPDF/Elements/Text/FontFallback.cs +++ b/QuestPDF/Elements/Text/FontFallback.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using QuestPDF.Drawing; using QuestPDF.Drawing.Exceptions; using QuestPDF.Elements.Text.Items; @@ -16,45 +18,99 @@ namespace QuestPDF.Elements.Text 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 SplitWithFontFallback(this string text, TextStyle textStyle) { - var partStartIndex = 0; - var partTextStyle = 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 font = partTextStyle.ToFont(); - var typeface = font.Typeface; + var newFallbackOption = MatchFallbackOption(fallbackOptions, codepoint); - if (font.ContainsGlyph(codepoint)) + if (newFallbackOption == spanFallbackOption) continue; - - var fallbackTypeface = FontManager.MatchCharacter(typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codepoint); - - if (fallbackTypeface == null) - throw new DocumentDrawingException($"Could not find an appropriate font fallback for text: '{text}'"); yield return new TextRun { - Content = text.Substring(partStartIndex, i - partStartIndex), - Style = partTextStyle + Content = text.Substring(spanStartIndex, i - spanStartIndex), + Style = spanFallbackOption.Style }; - partStartIndex = i; - partTextStyle = textStyle.FontFamily(fallbackTypeface.FamilyName).Weight((FontWeight)fallbackTypeface.FontWeight); + spanStartIndex = i; + spanFallbackOption = newFallbackOption; } - if (partStartIndex > text.Length) + if (spanStartIndex > text.Length) yield break; yield return new TextRun { - Content = text.Substring(partStartIndex, text.Length - partStartIndex), - Style = partTextStyle + Content = text.Substring(spanStartIndex, text.Length - spanStartIndex), + Style = spanFallbackOption.Style }; + + static IEnumerable 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 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 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 ApplyFontFallback(this ICollection textBlockItems) @@ -63,6 +119,13 @@ namespace QuestPDF.Elements.Text { 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) diff --git a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs index 41af136..3c8e7dc 100644 --- a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs +++ b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs @@ -15,6 +15,17 @@ namespace QuestPDF.Fluent return descriptor; } + public static T Fallback(this T descriptor, TextStyle? value = null) where T : TextSpanDescriptor + { + descriptor.TextStyle.Fallback = value; + return descriptor; + } + + public static T Fallback(this T descriptor, Func handler) where T : TextSpanDescriptor + { + return descriptor.Fallback(handler(TextStyle.Default)); + } + public static T FontColor(this T descriptor, string value) where T : TextSpanDescriptor { descriptor.TextStyle.Color = value; diff --git a/QuestPDF/Fluent/TextStyleExtensions.cs b/QuestPDF/Fluent/TextStyleExtensions.cs index 7823908..6c7396b 100644 --- a/QuestPDF/Fluent/TextStyleExtensions.cs +++ b/QuestPDF/Fluent/TextStyleExtensions.cs @@ -14,6 +14,16 @@ namespace QuestPDF.Fluent 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 handler) + { + return style.Fallback(handler(TextStyle.Default)); + } + [Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")] public static TextStyle Color(this TextStyle style, string value) { diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index cea2719..1bb500e 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -1,4 +1,5 @@ using System; +using HarfBuzzSharp; using QuestPDF.Helpers; namespace QuestPDF.Infrastructure @@ -19,6 +20,8 @@ namespace QuestPDF.Infrastructure internal bool? HasUnderline { get; set; } internal bool? WrapAnywhere { get; set; } + internal TextStyle? Fallback { get; set; } + // TODO: without cache, this may be an expensive operation internal object PaintKey => (FontFamily, Size, FontWeight, FontPosition, IsItalic, Color); internal object FontMetricsKey => (FontFamily, Size, FontWeight, IsItalic); @@ -35,7 +38,8 @@ namespace QuestPDF.Infrastructure IsItalic = false, HasStrikethrough = false, HasUnderline = false, - WrapAnywhere = false + WrapAnywhere = false, + Fallback = null }; // it is important to create new instances for the DefaultTextStyle element to work correctly @@ -48,9 +52,18 @@ namespace QuestPDF.Infrastructure 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) + internal void ApplyParentStyle(TextStyle parentStyle, bool mapFallback = true) { Color ??= parentStyle.Color; BackgroundColor ??= parentStyle.BackgroundColor; @@ -63,6 +76,9 @@ namespace QuestPDF.Infrastructure HasStrikethrough ??= parentStyle.HasStrikethrough; HasUnderline ??= parentStyle.HasUnderline; WrapAnywhere ??= parentStyle.WrapAnywhere; + + if (mapFallback) + Fallback ??= parentStyle.Fallback?.Clone(); } internal void OverrideStyle(TextStyle parentStyle) @@ -78,12 +94,14 @@ namespace QuestPDF.Infrastructure 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; } } From fbebbd85eb6a34713849b6dd3a50ee36559072a5 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 15 Sep 2022 15:09:30 +0200 Subject: [PATCH 06/14] Fixed build, adapted fallback process for cached TextStyle --- QuestPDF/Drawing/DocumentGenerator.cs | 2 +- QuestPDF/Elements/Text/FontFallback.cs | 12 ++-- QuestPDF/Fluent/TextStyleExtensions.cs | 2 +- QuestPDF/Infrastructure/TextStyleManager.cs | 73 +++++++++++++++------ 4 files changed, 63 insertions(+), 26 deletions(-) diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index 84b0b3d..b64baac 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -172,7 +172,7 @@ namespace QuestPDF.Drawing { if (textBlockItem is TextBlockSpan textSpan) { - textSpan.Style = textSpan.Style.ApplyGlobalStyle(TextStyle.LibraryDefault); + textSpan.Style = textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle); } else if (textBlockItem is TextBlockElement textElement) { diff --git a/QuestPDF/Elements/Text/FontFallback.cs b/QuestPDF/Elements/Text/FontFallback.cs index 79caeba..e9acbfd 100644 --- a/QuestPDF/Elements/Text/FontFallback.cs +++ b/QuestPDF/Elements/Text/FontFallback.cs @@ -85,22 +85,26 @@ namespace QuestPDF.Elements.Text if (fallbackOption.Font.ContainsGlyph(codepoint)) return fallbackOption; } - + + throw CreateNotMatchingFontException(codepoint); + } + + static Exception CreateNotMatchingFontException(int codepoint) + { var character = char.ConvertFromUtf32(codepoint); var unicode = $"U-{codepoint:X4}"; - var proposedFonts = FindFontsContainingGlyph(codepoint); var proposedFontsFormatted = proposedFonts.Any() ? string.Join(", ", proposedFonts) : "no fonts available"; - throw new DocumentDrawingException( + return new DocumentDrawingException( $"Could not find an appropriate font fallback for glyph: {unicode} '{character}'. " + $"Font families available on current environment that contain this glyph: {proposedFontsFormatted}. " + $"Possible solutions: " + $"1) Use one of the listed fonts as the primary font in your document. " + $"2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. "); } - + static IEnumerable FindFontsContainingGlyph(int codepoint) { var fontManager = SKFontManager.Default; diff --git a/QuestPDF/Fluent/TextStyleExtensions.cs b/QuestPDF/Fluent/TextStyleExtensions.cs index 723cec5..6119f6d 100644 --- a/QuestPDF/Fluent/TextStyleExtensions.cs +++ b/QuestPDF/Fluent/TextStyleExtensions.cs @@ -156,7 +156,7 @@ namespace QuestPDF.Fluent public static TextStyle Fallback(this TextStyle style, TextStyle? value = null) { - return style.Mutate(x => x.Fallback = value); + return style.Mutate(TextStyleProperty.Fallback, value); } public static TextStyle Fallback(this TextStyle style, Func handler) diff --git a/QuestPDF/Infrastructure/TextStyleManager.cs b/QuestPDF/Infrastructure/TextStyleManager.cs index 0d30c84..031c56b 100644 --- a/QuestPDF/Infrastructure/TextStyleManager.cs +++ b/QuestPDF/Infrastructure/TextStyleManager.cs @@ -16,13 +16,15 @@ namespace QuestPDF.Infrastructure IsItalic, HasStrikethrough, HasUnderline, - WrapAnywhere + WrapAnywhere, + Fallback } 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(); + private static readonly ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new(); + private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleApplyGlobalCache = new(); + private static readonly ConcurrentDictionary<(TextStyle origin, TextStyle parent), TextStyle> TextStyleOverrideCache = new(); public static TextStyle Mutate(this TextStyle origin, TextStyleProperty property, object value) { @@ -30,7 +32,7 @@ namespace QuestPDF.Infrastructure 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) return origin; @@ -177,38 +179,69 @@ namespace QuestPDF.Infrastructure return origin with { WrapAnywhere = castedValue }; } + + if (property == TextStyleProperty.Fallback) + { + if (!overrideValue && origin.Fallback != null) + return origin; + + var castedValue = (TextStyle?)value; + + if (origin.Fallback == castedValue) + return origin; + + return origin with { Fallback = castedValue }; + } throw new ArgumentOutOfRangeException(nameof(property), property, "Expected to mutate the TextStyle object. Provided property type is not supported."); } internal static TextStyle ApplyGlobalStyle(this TextStyle style, TextStyle parent) { - var cacheKey = (style, parent, false); - return TextStyleApplyCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, key.overrideValue)); + var cacheKey = (style, parent); + return TextStyleApplyGlobalCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, overrideStyle: false).ApplyFontFallback()); + } + + private static TextStyle ApplyFontFallback(this TextStyle style) + { + var targetFallbackStyle = style + ?.Fallback + ?.ApplyStyle(style, overrideStyle: false, applyFallback: false) + ?.ApplyFontFallback(); + + return MutateStyle(style, TextStyleProperty.Fallback, targetFallbackStyle); } internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent) { - var cacheKey = (style, parent, true); - return TextStyleApplyCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, key.overrideValue)); + var cacheKey = (style, parent); + + return TextStyleOverrideCache.GetOrAdd(cacheKey, key => + { + var result = ApplyStyle(key.origin, key.parent); + return MutateStyle(result, TextStyleProperty.Fallback, key.parent.Fallback); + }); } - private static TextStyle ApplyStyle(TextStyle style, TextStyle parent, bool overrideValue) + private static TextStyle ApplyStyle(this TextStyle style, TextStyle parent, bool overrideStyle = true, bool applyFallback = true) { var result = style; - result = MutateStyle(result, TextStyleProperty.Color, parent.Color, 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); + result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideStyle); + result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideStyle); + result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle); + result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideStyle); + result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideStyle); + result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideStyle); + result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideStyle); + result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideStyle); + result = MutateStyle(result, TextStyleProperty.HasStrikethrough, parent.HasStrikethrough, overrideStyle); + result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideStyle); + result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideStyle); + if (applyFallback) + result = MutateStyle(result, TextStyleProperty.Fallback, parent.Fallback, overrideStyle); + return result; } } From 556f87ff25fd3f03607904c5f566db85a269702d Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 15 Sep 2022 21:20:25 +0200 Subject: [PATCH 07/14] Settings renaming --- QuestPDF/Drawing/TextShaper.cs | 2 +- QuestPDF/Settings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/QuestPDF/Drawing/TextShaper.cs b/QuestPDF/Drawing/TextShaper.cs index 23ac13f..297c34f 100644 --- a/QuestPDF/Drawing/TextShaper.cs +++ b/QuestPDF/Drawing/TextShaper.cs @@ -56,7 +56,7 @@ namespace QuestPDF.Drawing yOffset += glyphPositions[i].YAdvance * scaleY; } - if (Settings.CheckIfAllTextGlyphsAreAvailableInSpecifiedFont) + if (Settings.CheckIfAllTextGlyphsAreAvailable) CheckIfAllGlyphsAreAvailable(glyphs, text); return new TextShapingResult(glyphs); diff --git a/QuestPDF/Settings.cs b/QuestPDF/Settings.cs index 10776ee..59abc95 100644 --- a/QuestPDF/Settings.cs +++ b/QuestPDF/Settings.cs @@ -35,6 +35,6 @@ /// However, it provides hints that used fonts are not sufficient to produce correct results. /// /// By default, this flag is enabled only when the debugger IS attached. - public static bool CheckIfAllTextGlyphsAreAvailableInSpecifiedFont { get; set; } = System.Diagnostics.Debugger.IsAttached; + public static bool CheckIfAllTextGlyphsAreAvailable { get; set; } = System.Diagnostics.Debugger.IsAttached; } } \ No newline at end of file From 6b535752dfba7698d31bc4c31ef43995c9a55788 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 15 Sep 2022 22:12:06 +0200 Subject: [PATCH 08/14] Fixed formatting page numbers --- QuestPDF/Elements/Text/FontFallback.cs | 2 +- QuestPDF/Fluent/TextExtensions.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/QuestPDF/Elements/Text/FontFallback.cs b/QuestPDF/Elements/Text/FontFallback.cs index e9acbfd..2e19bdd 100644 --- a/QuestPDF/Elements/Text/FontFallback.cs +++ b/QuestPDF/Elements/Text/FontFallback.cs @@ -121,7 +121,7 @@ namespace QuestPDF.Elements.Text { foreach (var textBlockItem in textBlockItems) { - if (textBlockItem is TextBlockSpan textBlockSpan) + if (textBlockItem is TextBlockSpan textBlockSpan and not TextBlockPageNumber) { // perform font-fallback operation only when any fallback is available if (textBlockSpan.Style.Fallback == null) diff --git a/QuestPDF/Fluent/TextExtensions.cs b/QuestPDF/Fluent/TextExtensions.cs index 92ae7c6..fba270d 100644 --- a/QuestPDF/Fluent/TextExtensions.cs +++ b/QuestPDF/Fluent/TextExtensions.cs @@ -36,6 +36,7 @@ namespace QuestPDF.Fluent internal TextPageNumberDescriptor(Action assignTextStyle, Action assignFormatFunction) : base(assignTextStyle) { AssignFormatFunction = assignFormatFunction; + AssignFormatFunction(x => x?.ToString()); } public TextPageNumberDescriptor Format(PageNumberFormatter formatter) From db2df756247053334d6f6880f5e0198db8781b12 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Thu, 15 Sep 2022 22:19:28 +0200 Subject: [PATCH 09/14] Text example: fixed visuals --- QuestPDF.Examples/TextBenchmark.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuestPDF.Examples/TextBenchmark.cs b/QuestPDF.Examples/TextBenchmark.cs index a5d1fb9..053fbaf 100644 --- a/QuestPDF.Examples/TextBenchmark.cs +++ b/QuestPDF.Examples/TextBenchmark.cs @@ -111,7 +111,7 @@ namespace QuestPDF.Examples { page.Margin(50); - page.Content().Column(column => + page.Content().PaddingVertical(10).Column(column => { column.Item().Element(Title); column.Item().PageBreak(); From 34fed6d54771b9265c1649350e3de165adc65853 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Fri, 16 Sep 2022 16:08:02 +0200 Subject: [PATCH 10/14] Settings.CheckIfAllTextGlyphsAreAvailable improvements --- QuestPDF/Drawing/TextShaper.cs | 17 ----------------- QuestPDF/Elements/Text/FontFallback.cs | 3 +-- 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/QuestPDF/Drawing/TextShaper.cs b/QuestPDF/Drawing/TextShaper.cs index 297c34f..509482c 100644 --- a/QuestPDF/Drawing/TextShaper.cs +++ b/QuestPDF/Drawing/TextShaper.cs @@ -55,9 +55,6 @@ namespace QuestPDF.Drawing xOffset += glyphPositions[i].XAdvance * scaleX; yOffset += glyphPositions[i].YAdvance * scaleY; } - - if (Settings.CheckIfAllTextGlyphsAreAvailable) - CheckIfAllGlyphsAreAvailable(glyphs, text); return new TextShapingResult(glyphs); } @@ -78,20 +75,6 @@ namespace QuestPDF.Drawing else throw new NotSupportedException("TextEncoding of type GlyphId is not supported."); } - - void CheckIfAllGlyphsAreAvailable(ShapedGlyph[] glyphs, string originalText) - { - var containsMissingGlyphs = glyphs.Any(x => x.Codepoint == default); - - if (!containsMissingGlyphs) - return; - - throw new ArgumentException( - $"Detected missing font glyphs while rendering text. " + - $"This means that the document contains text with characters not present in the assigned font. " + - $"Such characters are replaced by placeholders, usually visible as empty rectangles. " + - $"Font family used: {TextStyle.FontFamily}. Issue detected in text: '{originalText}'"); - } } internal struct ShapedGlyph diff --git a/QuestPDF/Elements/Text/FontFallback.cs b/QuestPDF/Elements/Text/FontFallback.cs index 2e19bdd..5eabe9c 100644 --- a/QuestPDF/Elements/Text/FontFallback.cs +++ b/QuestPDF/Elements/Text/FontFallback.cs @@ -123,8 +123,7 @@ namespace QuestPDF.Elements.Text { if (textBlockItem is TextBlockSpan textBlockSpan and not TextBlockPageNumber) { - // perform font-fallback operation only when any fallback is available - if (textBlockSpan.Style.Fallback == null) + if (!Settings.CheckIfAllTextGlyphsAreAvailable && textBlockSpan.Style.Fallback == null) { yield return textBlockSpan; continue; From 2adff114006f0333eaf6598a17937e41ca19981c Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Fri, 16 Sep 2022 19:46:32 +0200 Subject: [PATCH 11/14] 2022.9.0-alpha1 --- QuestPDF/QuestPDF.csproj | 2 +- QuestPDF/Resources/ReleaseNotes.txt | 26 ++++++-------------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index 44a8cda..2acb10a 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -3,7 +3,7 @@ MarcinZiabek CodeFlint QuestPDF - 2022.8.2 + 2022.9.0-alpha1 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. $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt")) 9 diff --git a/QuestPDF/Resources/ReleaseNotes.txt b/QuestPDF/Resources/ReleaseNotes.txt index 8ed583b..44c7fc7 100644 --- a/QuestPDF/Resources/ReleaseNotes.txt +++ b/QuestPDF/Resources/ReleaseNotes.txt @@ -1,20 +1,6 @@ -2022.8.0: - -- Improved library performance, -- Breaking change: changed default font from Calibri to an open-source Lato, -- Default font files are included with the nuget package, making it safe to deploy on any environment, -- Default font files are significantly smaller, so output document files should be smaller too (up to 20x reduction in size), -- When requested font is not available on the runtime environment, library provides list of available fonts, -- Fixed a rare layout overflow exception with the Inlined element, -- Fixed a memory leak connected to the HarfBuzz library. - - -2022.8.1: -- Fixed: default text style does not always work -- Fixed: page breaking rendering does not work in very specific corner cases -- Stability improvements for text wrapping -- Updated stability of rendering elements in negative space -- Optimization for the Column element: do not measure child when available height is negative - -2022.8.2 -- Fixed: the Column element incorrectly renders zero-height elements. +2022.9.0 +- Implemented font-fallback algorithm, +- Introduced new Settings API, +- Significantly reduced memory allocation cost for TextStyle objects, +- Implemented optional checking if all font glyphs are available, +- Minor text-rendering optimizations. From 719a3385f62e2a6a76a15286b8ec93e2764aca6d Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Fri, 16 Sep 2022 19:52:10 +0200 Subject: [PATCH 12/14] Updated Previewer to 2022.9 --- QuestPDF.Previewer/QuestPDF.Previewer.csproj | 2 +- QuestPDF/Previewer/PreviewerService.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/QuestPDF.Previewer/QuestPDF.Previewer.csproj b/QuestPDF.Previewer/QuestPDF.Previewer.csproj index 64b7719..196a94f 100644 --- a/QuestPDF.Previewer/QuestPDF.Previewer.csproj +++ b/QuestPDF.Previewer/QuestPDF.Previewer.csproj @@ -4,7 +4,7 @@ MarcinZiabek CodeFlint QuestPDF.Previewer - 2022.8.0 + 2022.9.0 true questpdf-previewer 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. diff --git a/QuestPDF/Previewer/PreviewerService.cs b/QuestPDF/Previewer/PreviewerService.cs index f9d9c7b..2925a7c 100644 --- a/QuestPDF/Previewer/PreviewerService.cs +++ b/QuestPDF/Previewer/PreviewerService.cs @@ -19,7 +19,7 @@ namespace QuestPDF.Previewer public event Action? OnPreviewerStopped; private const int RequiredPreviewerVersionMajor = 2022; - private const int RequiredPreviewerVersionMinor = 8; + private const int RequiredPreviewerVersionMinor = 9; public PreviewerService(int port) { From f028f82e11902fb9802212af77d0cd73a86e5002 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sat, 17 Sep 2022 20:42:14 +0200 Subject: [PATCH 13/14] Removed unsued file --- QuestPDF/Drawing/TextMeasurement.cs | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 QuestPDF/Drawing/TextMeasurement.cs diff --git a/QuestPDF/Drawing/TextMeasurement.cs b/QuestPDF/Drawing/TextMeasurement.cs deleted file mode 100644 index 36d5dd6..0000000 --- a/QuestPDF/Drawing/TextMeasurement.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace QuestPDF.Drawing -{ - internal struct TextMeasurement - { - public int LineIndex { get; set; } - public float FragmentWidth { get; set; } - } -} \ No newline at end of file From b307304f46e7a222209cf44b093901eaae0880fe Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sun, 18 Sep 2022 20:15:41 +0200 Subject: [PATCH 14/14] Fixed typo --- QuestPDF/Settings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/QuestPDF/Settings.cs b/QuestPDF/Settings.cs index 59abc95..5314c96 100644 --- a/QuestPDF/Settings.cs +++ b/QuestPDF/Settings.cs @@ -3,7 +3,7 @@ public static class Settings { /// - /// This value represents the maximum lenght of the document that the library produces. + /// This value represents the maximum length of the document that the library produces. /// This is useful when layout constraints are too strong, e.g. one element does not fit in another. /// In such cases, the library would produce document of infinite length, consuming all available resources. /// To break the algorithm and save the environment, the library breaks the rendering process after reaching specified length of document.