Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5390fc3f1b | |||
| 0468dd5f02 |
@@ -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,43 @@ namespace QuestPDF.Examples
|
||||
.FontSize(20);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FontFallback()
|
||||
{
|
||||
RenderingTest
|
||||
.Create()
|
||||
.ProduceImages()
|
||||
.ShowResults()
|
||||
.RenderDocument(container =>
|
||||
{
|
||||
container.Page(page =>
|
||||
{
|
||||
page.Margin(50);
|
||||
page.PageColor(Colors.White);
|
||||
page.DefaultTextStyle(x => x
|
||||
.Fallback(y => y.FontFamily("Segoe UI Emoji")
|
||||
.Fallback(y => y.FontFamily("Microsoft YaHei"))));
|
||||
|
||||
page.Size(PageSizes.A4);
|
||||
|
||||
page.Content().Text(t =>
|
||||
{
|
||||
t.Line("This is normal text.");
|
||||
t.EmptyLine();
|
||||
|
||||
t.Line("Following line should use font fallback:");
|
||||
t.Line("中文文本");
|
||||
t.EmptyLine();
|
||||
|
||||
t.Line("The following line contains a mix of known and unknown characters.");
|
||||
t.Line("Mixed line: This 中文 is 文文 a mixed 本 本 line 本 中文文本!");
|
||||
t.EmptyLine();
|
||||
|
||||
t.Line("Emojis work out of the box because of font fallback: 😊😅🥳👍❤😍👌");
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,7 +172,7 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
if (textBlockItem is TextBlockSpan textSpan)
|
||||
{
|
||||
textSpan.Style = textSpan.Style.ApplyGlobalStyle(TextStyle.LibraryDefault);
|
||||
textSpan.Style.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
}
|
||||
else if (textBlockItem is TextBlockElement textElement)
|
||||
{
|
||||
@@ -184,13 +184,18 @@ namespace QuestPDF.Drawing
|
||||
}
|
||||
|
||||
if (content is DynamicHost dynamicHost)
|
||||
dynamicHost.TextStyle = dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
dynamicHost.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
var targetTextStyle = documentDefaultTextStyle;
|
||||
|
||||
if (content is DefaultTextStyle defaultTextStyleElement)
|
||||
documentDefaultTextStyle = defaultTextStyleElement.TextStyle.ApplyGlobalStyle(documentDefaultTextStyle);
|
||||
|
||||
{
|
||||
defaultTextStyleElement.TextStyle.ApplyParentStyle(documentDefaultTextStyle);
|
||||
targetTextStyle = defaultTextStyleElement.TextStyle;
|
||||
}
|
||||
|
||||
foreach (var child in content.GetChildren())
|
||||
ApplyDefaultTextStyle(child, documentDefaultTextStyle);
|
||||
ApplyDefaultTextStyle(child, targetTextStyle);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,11 @@ namespace QuestPDF.Drawing.Exceptions
|
||||
{
|
||||
public class DocumentDrawingException : Exception
|
||||
{
|
||||
internal DocumentDrawingException(string message) : base(message)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
internal DocumentDrawingException(string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
|
||||
|
||||
@@ -14,13 +14,13 @@ namespace QuestPDF.Drawing
|
||||
{
|
||||
public static class FontManager
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, SKFontMetrics> FontMetrics = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, SKPaint> FontPaints = new();
|
||||
private static readonly ConcurrentDictionary<string, SKPaint> ColorPaints = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, Font> ShaperFonts = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, SKFont> Fonts = new();
|
||||
private static readonly ConcurrentDictionary<TextStyle, TextShaper> TextShapers = new();
|
||||
private static ConcurrentDictionary<string, FontStyleSet> StyleSets = new();
|
||||
private static ConcurrentDictionary<object, SKFontMetrics> FontMetrics = new();
|
||||
private static ConcurrentDictionary<object, SKPaint> FontPaints = new();
|
||||
private static ConcurrentDictionary<string, SKPaint> ColorPaints = new();
|
||||
private static ConcurrentDictionary<object, Font> ShaperFonts = new();
|
||||
private static ConcurrentDictionary<object, SKFont> Fonts = new();
|
||||
private static ConcurrentDictionary<object, TextShaper> TextShapers = new();
|
||||
|
||||
static FontManager()
|
||||
{
|
||||
@@ -110,7 +110,7 @@ namespace QuestPDF.Drawing
|
||||
|
||||
internal static SKPaint ToPaint(this TextStyle style)
|
||||
{
|
||||
return FontPaints.GetOrAdd(style, Convert);
|
||||
return FontPaints.GetOrAdd(style.PaintKey, key => Convert(style));
|
||||
|
||||
static SKPaint Convert(TextStyle style)
|
||||
{
|
||||
@@ -172,14 +172,14 @@ namespace QuestPDF.Drawing
|
||||
|
||||
internal static SKFontMetrics ToFontMetrics(this TextStyle style)
|
||||
{
|
||||
return FontMetrics.GetOrAdd(style, key => key.NormalPosition().ToPaint().FontMetrics);
|
||||
return FontMetrics.GetOrAdd(style.FontMetricsKey, key => style.NormalPosition().ToPaint().FontMetrics);
|
||||
}
|
||||
|
||||
internal static Font ToShaperFont(this TextStyle style)
|
||||
{
|
||||
return ShaperFonts.GetOrAdd(style, key =>
|
||||
return ShaperFonts.GetOrAdd(style.PaintKey, _ =>
|
||||
{
|
||||
var typeface = key.ToPaint().Typeface;
|
||||
var typeface = style.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, key => new TextShaper(key));
|
||||
return TextShapers.GetOrAdd(style.PaintKey, _ => new TextShaper(style));
|
||||
}
|
||||
|
||||
internal static SKFont ToFont(this TextStyle style)
|
||||
{
|
||||
return Fonts.GetOrAdd(style, key => key.ToPaint().ToFont());
|
||||
return Fonts.GetOrAdd(style.PaintKey, _ => style.ToPaint().ToFont());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace QuestPDF.Elements
|
||||
private DynamicComponentProxy Child { get; }
|
||||
private object InitialComponentState { get; set; }
|
||||
|
||||
internal TextStyle TextStyle { get; set; } = TextStyle.Default;
|
||||
internal TextStyle TextStyle { get; } = new();
|
||||
|
||||
public DynamicHost(DynamicComponentProxy child)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using QuestPDF.Drawing;
|
||||
using QuestPDF.Drawing.Exceptions;
|
||||
using QuestPDF.Elements.Text.Items;
|
||||
using QuestPDF.Fluent;
|
||||
using QuestPDF.Infrastructure;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace QuestPDF.Elements.Text
|
||||
{
|
||||
internal static class FontFallback
|
||||
{
|
||||
public struct TextRun
|
||||
{
|
||||
public string Content { get; set; }
|
||||
public TextStyle Style { get; set; }
|
||||
}
|
||||
|
||||
public class FallbackOption
|
||||
{
|
||||
public TextStyle Style { get; set; }
|
||||
public SKFont Font { get; set; }
|
||||
public SKTypeface Typeface { get; set; }
|
||||
}
|
||||
|
||||
private static SKFontManager FontManager => SKFontManager.Default;
|
||||
|
||||
public static IEnumerable<TextRun> SplitWithFontFallback(this string text, TextStyle textStyle)
|
||||
{
|
||||
var fallbackOptions = GetFallbackOptions(textStyle).ToArray();
|
||||
|
||||
var spanStartIndex = 0;
|
||||
var spanFallbackOption = fallbackOptions[0];
|
||||
|
||||
for (var i = 0; i < text.Length; i += char.IsSurrogatePair(text, i) ? 2 : 1)
|
||||
{
|
||||
var codepoint = char.ConvertToUtf32(text, i);
|
||||
var newFallbackOption = MatchFallbackOption(fallbackOptions, codepoint);
|
||||
|
||||
if (newFallbackOption == spanFallbackOption)
|
||||
continue;
|
||||
|
||||
yield return new TextRun
|
||||
{
|
||||
Content = text.Substring(spanStartIndex, i - spanStartIndex),
|
||||
Style = spanFallbackOption.Style
|
||||
};
|
||||
|
||||
spanStartIndex = i;
|
||||
spanFallbackOption = newFallbackOption;
|
||||
}
|
||||
|
||||
if (spanStartIndex > text.Length)
|
||||
yield break;
|
||||
|
||||
yield return new TextRun
|
||||
{
|
||||
Content = text.Substring(spanStartIndex, text.Length - spanStartIndex),
|
||||
Style = spanFallbackOption.Style
|
||||
};
|
||||
|
||||
static IEnumerable<FallbackOption> GetFallbackOptions(TextStyle? textStyle)
|
||||
{
|
||||
while (textStyle != null)
|
||||
{
|
||||
var font = textStyle.ToFont();
|
||||
|
||||
yield return new FallbackOption
|
||||
{
|
||||
Style = textStyle,
|
||||
Font = font,
|
||||
Typeface = font.Typeface
|
||||
};
|
||||
|
||||
textStyle = textStyle.Fallback;
|
||||
}
|
||||
}
|
||||
|
||||
static FallbackOption MatchFallbackOption(ICollection<FallbackOption> fallbackOptions, int codepoint)
|
||||
{
|
||||
foreach (var fallbackOption in fallbackOptions)
|
||||
{
|
||||
if (fallbackOption.Font.ContainsGlyph(codepoint))
|
||||
return fallbackOption;
|
||||
}
|
||||
|
||||
var character = char.ConvertFromUtf32(codepoint);
|
||||
var unicode = $"U-{codepoint:X4}";
|
||||
|
||||
|
||||
var proposedFonts = FindFontsContainingGlyph(codepoint);
|
||||
var proposedFontsFormatted = proposedFonts.Any() ? string.Join(", ", proposedFonts) : "no fonts available";
|
||||
|
||||
throw new DocumentDrawingException(
|
||||
$"Could not find an appropriate font fallback for glyph: {unicode} '{character}'. " +
|
||||
$"Font families available on current environment that contain this glyph: {proposedFontsFormatted}. " +
|
||||
$"Possible solutions: " +
|
||||
$"1) Use one of the listed fonts as the primary font in your document. " +
|
||||
$"2) Configure the fallback TextStyle using the 'TextStyle.Fallback' method with one of the listed fonts. ");
|
||||
}
|
||||
|
||||
static IEnumerable<string> FindFontsContainingGlyph(int codepoint)
|
||||
{
|
||||
var fontManager = SKFontManager.Default;
|
||||
|
||||
return fontManager
|
||||
.GetFontFamilies()
|
||||
.Select(fontManager.MatchFamily)
|
||||
.Where(x => x.ContainsGlyph(codepoint))
|
||||
.Select(x => x.FamilyName);
|
||||
}
|
||||
}
|
||||
|
||||
public static IEnumerable<ITextBlockItem> ApplyFontFallback(this ICollection<ITextBlockItem> textBlockItems)
|
||||
{
|
||||
foreach (var textBlockItem in textBlockItems)
|
||||
{
|
||||
if (textBlockItem is TextBlockSpan textBlockSpan)
|
||||
{
|
||||
// perform font-fallback operation only when any fallback is available
|
||||
if (textBlockSpan.Style.Fallback == null)
|
||||
{
|
||||
yield return textBlockSpan;
|
||||
continue;
|
||||
}
|
||||
|
||||
var textRuns = textBlockSpan.Text.SplitWithFontFallback(textBlockSpan.Style);
|
||||
|
||||
foreach (var textRun in textRuns)
|
||||
{
|
||||
yield return new TextBlockSpan
|
||||
{
|
||||
Text = textRun.Content,
|
||||
Style = textRun.Style
|
||||
};
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return textBlockItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ namespace QuestPDF.Elements.Text.Items
|
||||
internal class TextBlockSpan : ITextBlockItem
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public TextStyle Style { get; set; } = TextStyle.Default;
|
||||
public TextShapingResult? TextShapingResult { get; set; }
|
||||
public TextStyle Style { get; set; } = new();
|
||||
private TextShapingResult? TextShapingResult { get; set; }
|
||||
|
||||
private Dictionary<(int startIndex, float availableWidth), TextMeasurementResult?> MeasureCache = new ();
|
||||
protected virtual bool EnableTextCache => true;
|
||||
|
||||
@@ -18,8 +18,11 @@ namespace QuestPDF.Elements.Text
|
||||
private Queue<ITextBlockItem> RenderingQueue { get; set; }
|
||||
private int CurrentElementIndex { get; set; }
|
||||
|
||||
private bool FontFallbackApplied { get; set; } = false;
|
||||
|
||||
public void ResetState()
|
||||
{
|
||||
ApplyFontFallback();
|
||||
InitializeQueue();
|
||||
CurrentElementIndex = 0;
|
||||
|
||||
@@ -37,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)
|
||||
|
||||
@@ -12,18 +12,11 @@ namespace QuestPDF.Fluent
|
||||
{
|
||||
public class TextSpanDescriptor
|
||||
{
|
||||
internal TextStyle TextStyle = TextStyle.Default;
|
||||
internal Action<TextStyle> AssignTextStyle { get; }
|
||||
internal TextStyle TextStyle { get; }
|
||||
|
||||
internal TextSpanDescriptor(Action<TextStyle> assignTextStyle)
|
||||
internal TextSpanDescriptor(TextStyle textStyle)
|
||||
{
|
||||
AssignTextStyle = assignTextStyle;
|
||||
}
|
||||
|
||||
internal void MutateTextStyle(Func<TextStyle, TextStyle> handler)
|
||||
{
|
||||
TextStyle = handler(TextStyle);
|
||||
AssignTextStyle(TextStyle);
|
||||
TextStyle = textStyle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,16 +24,16 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public class TextPageNumberDescriptor : TextSpanDescriptor
|
||||
{
|
||||
internal Action<PageNumberFormatter> AssignFormatFunction { get; }
|
||||
|
||||
internal TextPageNumberDescriptor(Action<TextStyle> assignTextStyle, Action<PageNumberFormatter> assignFormatFunction) : base(assignTextStyle)
|
||||
internal PageNumberFormatter FormatFunction { get; private set; } = x => x?.ToString() ?? string.Empty;
|
||||
|
||||
internal TextPageNumberDescriptor(TextStyle textStyle) : base(textStyle)
|
||||
{
|
||||
AssignFormatFunction = assignFormatFunction;
|
||||
|
||||
}
|
||||
|
||||
public TextPageNumberDescriptor Format(PageNumberFormatter formatter)
|
||||
{
|
||||
AssignFormatFunction(formatter);
|
||||
FormatFunction = formatter ?? FormatFunction;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +41,7 @@ namespace QuestPDF.Fluent
|
||||
public class TextDescriptor
|
||||
{
|
||||
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
|
||||
private TextStyle? DefaultStyle { get; set; }
|
||||
private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
|
||||
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
|
||||
private float Spacing { get; set; } = 0f;
|
||||
|
||||
@@ -98,15 +91,19 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public TextSpanDescriptor Span(string? text)
|
||||
{
|
||||
var style = DefaultStyle.Clone();
|
||||
var descriptor = new TextSpanDescriptor(style);
|
||||
|
||||
if (text == null)
|
||||
return new TextSpanDescriptor(_ => { });
|
||||
return descriptor;
|
||||
|
||||
var items = text
|
||||
.Replace("\r", string.Empty)
|
||||
.Split(new[] { '\n' }, StringSplitOptions.None)
|
||||
.Select(x => new TextBlockSpan
|
||||
{
|
||||
Text = x
|
||||
Text = x,
|
||||
Style = style
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@@ -121,7 +118,7 @@ namespace QuestPDF.Fluent
|
||||
.ToList()
|
||||
.ForEach(TextBlocks.Add);
|
||||
|
||||
return new TextSpanDescriptor(x => items.ForEach(y => y.Style = x));
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public TextSpanDescriptor Line(string? text)
|
||||
@@ -137,10 +134,16 @@ namespace QuestPDF.Fluent
|
||||
|
||||
private TextPageNumberDescriptor PageNumber(Func<IPageContext, int?> pageNumber)
|
||||
{
|
||||
var textBlockItem = new TextBlockPageNumber();
|
||||
AddItemToLastTextBlock(textBlockItem);
|
||||
var style = DefaultStyle.Clone();
|
||||
var descriptor = new TextPageNumberDescriptor(style);
|
||||
|
||||
return new TextPageNumberDescriptor(x => textBlockItem.Style = x, x => textBlockItem.Source = context => x(pageNumber(context)));
|
||||
AddItemToLastTextBlock(new TextBlockPageNumber
|
||||
{
|
||||
Source = context => descriptor.FormatFunction(pageNumber(context)),
|
||||
Style = style
|
||||
});
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public TextPageNumberDescriptor CurrentPageNumber()
|
||||
@@ -184,17 +187,20 @@ 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 new TextSpanDescriptor(_ => { });
|
||||
|
||||
var textBlockItem = new TextBlockSectionLink
|
||||
return descriptor;
|
||||
|
||||
AddItemToLastTextBlock(new TextBlockSectionLink
|
||||
{
|
||||
Style = style,
|
||||
Text = text,
|
||||
SectionName = sectionName
|
||||
};
|
||||
});
|
||||
|
||||
AddItemToLastTextBlock(textBlockItem);
|
||||
return new TextSpanDescriptor(x => textBlockItem.Style = x);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the SectionLink method.")]
|
||||
@@ -208,17 +214,20 @@ 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 new TextSpanDescriptor(_ => { });
|
||||
return descriptor;
|
||||
|
||||
var textBlockItem = new TextBlockHyperlink
|
||||
AddItemToLastTextBlock(new TextBlockHyperlink
|
||||
{
|
||||
Style = style,
|
||||
Text = text,
|
||||
Url = url
|
||||
};
|
||||
});
|
||||
|
||||
AddItemToLastTextBlock(textBlockItem);
|
||||
return new TextSpanDescriptor(x => textBlockItem.Style = x);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the Hyperlink method.")]
|
||||
@@ -242,9 +251,7 @@ namespace QuestPDF.Fluent
|
||||
internal void Compose(IContainer container)
|
||||
{
|
||||
TextBlocks.ToList().ForEach(x => x.Alignment = Alignment);
|
||||
|
||||
if (DefaultStyle != null)
|
||||
container = container.DefaultTextStyle(DefaultStyle);
|
||||
container = container.DefaultTextStyle(DefaultStyle);
|
||||
|
||||
if (TextBlocks.Count == 1)
|
||||
{
|
||||
|
||||
@@ -11,124 +11,131 @@ namespace QuestPDF.Fluent
|
||||
if (style == null)
|
||||
return descriptor;
|
||||
|
||||
descriptor.MutateTextStyle(x => x.OverrideStyle(style));
|
||||
descriptor.TextStyle.OverrideStyle(style);
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Fallback<T>(this T descriptor, TextStyle? value = null) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.Fallback = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Fallback<T>(this T descriptor, Func<TextStyle, TextStyle> handler) where T : TextSpanDescriptor
|
||||
{
|
||||
return descriptor.Fallback(handler(TextStyle.Default));
|
||||
}
|
||||
|
||||
public static T FontColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.FontColor(value));
|
||||
descriptor.TextStyle.Color = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T BackgroundColor<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.BackgroundColor(value));
|
||||
descriptor.TextStyle.BackgroundColor = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T FontFamily<T>(this T descriptor, string value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.FontFamily(value));
|
||||
descriptor.TextStyle.FontFamily = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T FontSize<T>(this T descriptor, float value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.FontSize(value));
|
||||
descriptor.TextStyle.Size = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T LineHeight<T>(this T descriptor, float value) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.LineHeight(value));
|
||||
descriptor.TextStyle.LineHeight = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Italic<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Italic(value));
|
||||
descriptor.TextStyle.IsItalic = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Strikethrough<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Strikethrough(value));
|
||||
descriptor.TextStyle.HasStrikethrough = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Underline<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Underline(value));
|
||||
descriptor.TextStyle.HasUnderline = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T WrapAnywhere<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.WrapAnywhere(value));
|
||||
descriptor.TextStyle.WrapAnywhere = value;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
#region Weight
|
||||
|
||||
public static T Weight<T>(this T descriptor, FontWeight weight) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.FontWeight = weight;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public static T Thin<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Thin());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.Thin);
|
||||
}
|
||||
|
||||
public static T ExtraLight<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.ExtraLight());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.ExtraLight);
|
||||
}
|
||||
|
||||
public static T Light<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Light());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.Light);
|
||||
}
|
||||
|
||||
public static T NormalWeight<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.NormalWeight());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.Normal);
|
||||
}
|
||||
|
||||
public static T Medium<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Medium());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.Medium);
|
||||
}
|
||||
|
||||
public static T SemiBold<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.SemiBold());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.SemiBold);
|
||||
}
|
||||
|
||||
public static T Bold<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Bold());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.Bold);
|
||||
}
|
||||
|
||||
public static T ExtraBold<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.ExtraBold());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.ExtraBold);
|
||||
}
|
||||
|
||||
public static T Black<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Black());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.Black);
|
||||
}
|
||||
|
||||
public static T ExtraBlack<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.ExtraBlack());
|
||||
return descriptor;
|
||||
return descriptor.Weight(FontWeight.ExtraBlack);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -136,22 +143,24 @@ namespace QuestPDF.Fluent
|
||||
#region Position
|
||||
public static T NormalPosition<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.NormalPosition());
|
||||
return descriptor;
|
||||
return descriptor.Position(FontPosition.Normal);
|
||||
}
|
||||
|
||||
public static T Subscript<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Subscript());
|
||||
return descriptor;
|
||||
return descriptor.Position(FontPosition.Subscript);
|
||||
}
|
||||
|
||||
public static T Superscript<T>(this T descriptor) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.MutateTextStyle(x => x.Superscript());
|
||||
return descriptor.Position(FontPosition.Superscript);
|
||||
}
|
||||
|
||||
private static T Position<T>(this T descriptor, FontPosition fontPosition) where T : TextSpanDescriptor
|
||||
{
|
||||
descriptor.TextStyle.FontPosition = fontPosition;
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,26 @@ using QuestPDF.Infrastructure;
|
||||
|
||||
namespace QuestPDF.Fluent
|
||||
{
|
||||
|
||||
|
||||
public static class TextStyleExtensions
|
||||
{
|
||||
private static TextStyle Mutate(this TextStyle style, Action<TextStyle> handler)
|
||||
{
|
||||
style = style.Clone();
|
||||
|
||||
handler(style);
|
||||
return style;
|
||||
}
|
||||
|
||||
public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
|
||||
{
|
||||
return style.Mutate(x => x.Fallback = value);
|
||||
}
|
||||
|
||||
public static TextStyle Fallback(this TextStyle style, Func<TextStyle, TextStyle> handler)
|
||||
{
|
||||
return style.Fallback(handler(TextStyle.Default));
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontColor method.")]
|
||||
public static TextStyle Color(this TextStyle style, string value)
|
||||
{
|
||||
@@ -16,12 +32,12 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public static TextStyle FontColor(this TextStyle style, string value)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.Color, value);
|
||||
return style.Mutate(x => x.Color = value);
|
||||
}
|
||||
|
||||
public static TextStyle BackgroundColor(this TextStyle style, string value)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.BackgroundColor, value);
|
||||
return style.Mutate(x => x.BackgroundColor = value);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontFamily method.")]
|
||||
@@ -32,7 +48,7 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public static TextStyle FontFamily(this TextStyle style, string value)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.FontFamily, value);
|
||||
return style.Mutate(x => x.FontFamily = value);
|
||||
}
|
||||
|
||||
[Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")]
|
||||
@@ -43,39 +59,39 @@ namespace QuestPDF.Fluent
|
||||
|
||||
public static TextStyle FontSize(this TextStyle style, float value)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.Size, value);
|
||||
return style.Mutate(x => x.Size = value);
|
||||
}
|
||||
|
||||
public static TextStyle LineHeight(this TextStyle style, float value)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.LineHeight, value);
|
||||
return style.Mutate(x => x.LineHeight = value);
|
||||
}
|
||||
|
||||
public static TextStyle Italic(this TextStyle style, bool value = true)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.IsItalic, value);
|
||||
return style.Mutate(x => x.IsItalic = value);
|
||||
}
|
||||
|
||||
public static TextStyle Strikethrough(this TextStyle style, bool value = true)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.HasStrikethrough, value);
|
||||
return style.Mutate(x => x.HasStrikethrough = value);
|
||||
}
|
||||
|
||||
public static TextStyle Underline(this TextStyle style, bool value = true)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.HasUnderline, value);
|
||||
return style.Mutate(x => x.HasUnderline = value);
|
||||
}
|
||||
|
||||
public static TextStyle WrapAnywhere(this TextStyle style, bool value = true)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.WrapAnywhere, value);
|
||||
return style.Mutate(x => x.WrapAnywhere = value);
|
||||
}
|
||||
|
||||
#region Weight
|
||||
|
||||
public static TextStyle Weight(this TextStyle style, FontWeight weight)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.FontWeight, weight);
|
||||
return style.Mutate(x => x.FontWeight = weight);
|
||||
}
|
||||
|
||||
public static TextStyle Thin(this TextStyle style)
|
||||
@@ -148,7 +164,10 @@ namespace QuestPDF.Fluent
|
||||
|
||||
private static TextStyle Position(this TextStyle style, FontPosition fontPosition)
|
||||
{
|
||||
return style.Mutate(TextStyleProperty.FontPosition, fontPosition);
|
||||
if (style.FontPosition == fontPosition)
|
||||
return style;
|
||||
|
||||
return style.Mutate(t => t.FontPosition = fontPosition);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
using System;
|
||||
using HarfBuzzSharp;
|
||||
using QuestPDF.Helpers;
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
public record TextStyle
|
||||
public class TextStyle
|
||||
{
|
||||
internal bool HasGlobalStyleApplied { get; private set; }
|
||||
|
||||
internal string? Color { get; set; }
|
||||
internal string? BackgroundColor { get; set; }
|
||||
internal string? FontFamily { get; set; }
|
||||
@@ -17,7 +20,13 @@ namespace QuestPDF.Infrastructure
|
||||
internal bool? HasUnderline { get; set; }
|
||||
internal bool? WrapAnywhere { get; set; }
|
||||
|
||||
internal static TextStyle LibraryDefault { get; } = new()
|
||||
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);
|
||||
|
||||
internal static readonly TextStyle LibraryDefault = new TextStyle
|
||||
{
|
||||
Color = Colors.Black,
|
||||
BackgroundColor = Colors.Transparent,
|
||||
@@ -29,9 +38,71 @@ namespace QuestPDF.Infrastructure
|
||||
IsItalic = false,
|
||||
HasStrikethrough = false,
|
||||
HasUnderline = false,
|
||||
WrapAnywhere = false
|
||||
WrapAnywhere = false,
|
||||
Fallback = null
|
||||
};
|
||||
|
||||
public static TextStyle Default { get; } = new();
|
||||
// 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);
|
||||
|
||||
if (Fallback != null)
|
||||
ApplyFallbackStyle(this);
|
||||
}
|
||||
|
||||
internal void ApplyFallbackStyle(TextStyle parentStyle)
|
||||
{
|
||||
ApplyParentStyle(parentStyle, false);
|
||||
Fallback?.ApplyFallbackStyle(this);
|
||||
}
|
||||
|
||||
internal void ApplyParentStyle(TextStyle parentStyle, bool mapFallback = true)
|
||||
{
|
||||
Color ??= parentStyle.Color;
|
||||
BackgroundColor ??= parentStyle.BackgroundColor;
|
||||
FontFamily ??= parentStyle.FontFamily;
|
||||
Size ??= parentStyle.Size;
|
||||
LineHeight ??= parentStyle.LineHeight;
|
||||
FontWeight ??= parentStyle.FontWeight;
|
||||
FontPosition ??= parentStyle.FontPosition;
|
||||
IsItalic ??= parentStyle.IsItalic;
|
||||
HasStrikethrough ??= parentStyle.HasStrikethrough;
|
||||
HasUnderline ??= parentStyle.HasUnderline;
|
||||
WrapAnywhere ??= parentStyle.WrapAnywhere;
|
||||
|
||||
if (mapFallback)
|
||||
Fallback ??= parentStyle.Fallback?.Clone();
|
||||
}
|
||||
|
||||
internal void OverrideStyle(TextStyle parentStyle)
|
||||
{
|
||||
Color = parentStyle.Color ?? Color;
|
||||
BackgroundColor = parentStyle.BackgroundColor ?? BackgroundColor;
|
||||
FontFamily = parentStyle.FontFamily ?? FontFamily;
|
||||
Size = parentStyle.Size ?? Size;
|
||||
LineHeight = parentStyle.LineHeight ?? LineHeight;
|
||||
FontWeight = parentStyle.FontWeight ?? FontWeight;
|
||||
FontPosition = parentStyle.FontPosition ?? FontPosition;
|
||||
IsItalic = parentStyle.IsItalic ?? IsItalic;
|
||||
HasStrikethrough = parentStyle.HasStrikethrough ?? HasStrikethrough;
|
||||
HasUnderline = parentStyle.HasUnderline ?? HasUnderline;
|
||||
WrapAnywhere = parentStyle.WrapAnywhere ?? WrapAnywhere;
|
||||
Fallback = parentStyle.Fallback?.Clone() ?? Fallback;
|
||||
}
|
||||
|
||||
internal TextStyle Clone()
|
||||
{
|
||||
var clone = (TextStyle)MemberwiseClone();
|
||||
clone.HasGlobalStyleApplied = false;
|
||||
clone.Fallback = Fallback?.Clone();
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,215 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using QuestPDF.Fluent;
|
||||
|
||||
namespace QuestPDF.Infrastructure
|
||||
{
|
||||
internal enum TextStyleProperty
|
||||
{
|
||||
Color,
|
||||
BackgroundColor,
|
||||
FontFamily,
|
||||
Size,
|
||||
LineHeight,
|
||||
FontWeight,
|
||||
FontPosition,
|
||||
IsItalic,
|
||||
HasStrikethrough,
|
||||
HasUnderline,
|
||||
WrapAnywhere
|
||||
}
|
||||
|
||||
internal static class TextStyleManager
|
||||
{
|
||||
public static ConcurrentDictionary<(TextStyle origin, TextStyleProperty property, object value), TextStyle> TextStyleMutateCache = new();
|
||||
public static ConcurrentDictionary<(TextStyle origin, TextStyle parent, bool overrideValue), TextStyle> TextStyleApplyCache = new();
|
||||
|
||||
public static TextStyle Mutate(this TextStyle origin, TextStyleProperty property, object value)
|
||||
{
|
||||
var cacheKey = (origin, property, value);
|
||||
return TextStyleMutateCache.GetOrAdd(cacheKey, x => MutateStyle(x.origin, x.property, x.value));
|
||||
}
|
||||
|
||||
private static TextStyle MutateStyle(TextStyle origin, TextStyleProperty property, object value, bool overrideValue = true)
|
||||
{
|
||||
if (overrideValue && value == null)
|
||||
return origin;
|
||||
|
||||
if (property == TextStyleProperty.Color)
|
||||
{
|
||||
if (!overrideValue && origin.Color != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (string?)value;
|
||||
|
||||
if (origin.Color == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { Color = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.BackgroundColor)
|
||||
{
|
||||
if (!overrideValue && origin.BackgroundColor != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (string?)value;
|
||||
|
||||
if (origin.BackgroundColor == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { BackgroundColor = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.FontFamily)
|
||||
{
|
||||
if (!overrideValue && origin.FontFamily != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (string?)value;
|
||||
|
||||
if (origin.FontFamily == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { FontFamily = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.Size)
|
||||
{
|
||||
if (!overrideValue && origin.Size != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (float?)value;
|
||||
|
||||
if (origin.Size == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { Size = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.LineHeight)
|
||||
{
|
||||
if (!overrideValue && origin.LineHeight != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (float?)value;
|
||||
|
||||
if (origin.LineHeight == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { LineHeight = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.FontWeight)
|
||||
{
|
||||
if (!overrideValue && origin.FontWeight != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (FontWeight?)value;
|
||||
|
||||
if (origin.FontWeight == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { FontWeight = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.FontPosition)
|
||||
{
|
||||
if (!overrideValue && origin.FontPosition != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (FontPosition?)value;
|
||||
|
||||
if (origin.FontPosition == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { FontPosition = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.IsItalic)
|
||||
{
|
||||
if (!overrideValue && origin.IsItalic != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (bool?)value;
|
||||
|
||||
if (origin.IsItalic == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { IsItalic = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.HasStrikethrough)
|
||||
{
|
||||
if (!overrideValue && origin.HasStrikethrough != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (bool?)value;
|
||||
|
||||
if (origin.HasStrikethrough == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { HasStrikethrough = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.HasUnderline)
|
||||
{
|
||||
if (!overrideValue && origin.HasUnderline != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (bool?)value;
|
||||
|
||||
if (origin.HasUnderline == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { HasUnderline = castedValue };
|
||||
}
|
||||
|
||||
if (property == TextStyleProperty.WrapAnywhere)
|
||||
{
|
||||
if (!overrideValue && origin.WrapAnywhere != null)
|
||||
return origin;
|
||||
|
||||
var castedValue = (bool?)value;
|
||||
|
||||
if (origin.WrapAnywhere == castedValue)
|
||||
return origin;
|
||||
|
||||
return origin with { WrapAnywhere = castedValue };
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(property), property, "Expected to mutate the TextStyle object. Provided property type is not supported.");
|
||||
}
|
||||
|
||||
internal static TextStyle ApplyGlobalStyle(this TextStyle style, TextStyle parent)
|
||||
{
|
||||
var cacheKey = (style, parent, false);
|
||||
return TextStyleApplyCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, key.overrideValue));
|
||||
}
|
||||
|
||||
internal static TextStyle OverrideStyle(this TextStyle style, TextStyle parent)
|
||||
{
|
||||
var cacheKey = (style, parent, true);
|
||||
return TextStyleApplyCache.GetOrAdd(cacheKey, key => ApplyStyle(key.origin, key.parent, key.overrideValue));
|
||||
}
|
||||
|
||||
private static TextStyle ApplyStyle(TextStyle style, TextStyle parent, bool overrideValue)
|
||||
{
|
||||
var result = style;
|
||||
|
||||
result = MutateStyle(result, TextStyleProperty.Color, parent.Color, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.BackgroundColor, parent.BackgroundColor, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.HasStrikethrough, parent.HasStrikethrough, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.HasUnderline, parent.HasUnderline, overrideValue);
|
||||
result = MutateStyle(result, TextStyleProperty.WrapAnywhere, parent.WrapAnywhere, overrideValue);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user