|
|
@@ -1,9 +1,13 @@
|
|
|
-using System.Collections.Generic;
|
|
|
+using System;
|
|
|
+using System.Collections.Generic;
|
|
|
using System.Linq;
|
|
|
using QuestPDF.Drawing;
|
|
|
-using QuestPDF.Elements.Text.Calculation;
|
|
|
+using QuestPDF.Drawing.Exceptions;
|
|
|
using QuestPDF.Elements.Text.Items;
|
|
|
+using QuestPDF.Helpers;
|
|
|
using QuestPDF.Infrastructure;
|
|
|
+using QuestPDF.Skia;
|
|
|
+using QuestPDF.Skia.Text;
|
|
|
|
|
|
namespace QuestPDF.Elements.Text
|
|
|
{
|
|
|
@@ -11,232 +15,369 @@ namespace QuestPDF.Elements.Text
|
|
|
{
|
|
|
public ContentDirection ContentDirection { get; set; }
|
|
|
|
|
|
- public HorizontalAlignment? Alignment { get; set; }
|
|
|
- public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
|
|
|
+ public TextHorizontalAlignment? Alignment { get; set; }
|
|
|
+ public int? LineClamp { get; set; }
|
|
|
+ public List<ITextBlockItem> Items { get; set; } = new();
|
|
|
|
|
|
+ private SkParagraph Paragraph { get; set; }
|
|
|
+
|
|
|
+ private bool RebuildParagraphForEveryPage { get; set; }
|
|
|
+ private bool AreParagraphMetricsValid { get; set; }
|
|
|
+
|
|
|
+ private SkSize[] LineMetrics { get; set; }
|
|
|
+ private float WidthForLineMetricsCalculation { get; set; }
|
|
|
+ private SkRect[] PlaceholderPositions { get; set; }
|
|
|
+ private float MaximumWidth { get; set; }
|
|
|
+
|
|
|
+ private int CurrentLineIndex { get; set; }
|
|
|
+ private float CurrentTopOffset { get; set; }
|
|
|
+
|
|
|
public string Text => string.Join(" ", Items.OfType<TextBlockSpan>().Select(x => x.Text));
|
|
|
|
|
|
- private Queue<ITextBlockItem> RenderingQueue { get; set; }
|
|
|
- private int CurrentElementIndex { get; set; }
|
|
|
-
|
|
|
- private bool FontFallbackApplied { get; set; } = false;
|
|
|
-
|
|
|
- public void ResetState()
|
|
|
+ ~TextBlock()
|
|
|
{
|
|
|
- ApplyFontFallback();
|
|
|
- ApplyPageContextToSectionLinks();
|
|
|
- InitializeQueue();
|
|
|
- CurrentElementIndex = 0;
|
|
|
-
|
|
|
- void InitializeQueue()
|
|
|
- {
|
|
|
- // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
|
|
|
- if (RenderingQueue == null)
|
|
|
- {
|
|
|
- RenderingQueue = new Queue<ITextBlockItem>(Items);
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- RenderingQueue.Clear();
|
|
|
-
|
|
|
- foreach (var item in Items)
|
|
|
- RenderingQueue.Enqueue(item);
|
|
|
- }
|
|
|
-
|
|
|
- void ApplyFontFallback()
|
|
|
- {
|
|
|
- if (FontFallbackApplied)
|
|
|
- return;
|
|
|
-
|
|
|
- Items = Items.ApplyFontFallback().ToList();
|
|
|
- FontFallbackApplied = true;
|
|
|
- }
|
|
|
-
|
|
|
- void ApplyPageContextToSectionLinks()
|
|
|
- {
|
|
|
- foreach (var sectionLink in Items.OfType<TextBlockSectionLink>())
|
|
|
- sectionLink.PageContext = PageContext;
|
|
|
- }
|
|
|
+ Paragraph?.Dispose();
|
|
|
}
|
|
|
|
|
|
- void SetDefaultAlignment()
|
|
|
+ public void ResetState()
|
|
|
{
|
|
|
- if (Alignment.HasValue)
|
|
|
- return;
|
|
|
-
|
|
|
- Alignment = ContentDirection == ContentDirection.LeftToRight
|
|
|
- ? HorizontalAlignment.Left
|
|
|
- : HorizontalAlignment.Right;
|
|
|
+ CurrentLineIndex = 0;
|
|
|
+ CurrentTopOffset = 0;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
internal override SpacePlan Measure(Size availableSpace)
|
|
|
{
|
|
|
- SetDefaultAlignment();
|
|
|
-
|
|
|
- if (!RenderingQueue.Any())
|
|
|
+ if (Items.Count == 0)
|
|
|
return SpacePlan.FullRender(Size.Zero);
|
|
|
|
|
|
- var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
|
|
|
+ Initialize();
|
|
|
+ CalculateParagraphMetrics(availableSpace);
|
|
|
|
|
|
- if (!lines.Any())
|
|
|
- return SpacePlan.Wrap();
|
|
|
+ if (MaximumWidth == 0)
|
|
|
+ return SpacePlan.FullRender(Size.Zero);
|
|
|
+
|
|
|
+ if (CurrentLineIndex > LineMetrics.Length)
|
|
|
+ return SpacePlan.FullRender(Size.Zero);
|
|
|
+
|
|
|
+ var totalHeight = 0f;
|
|
|
+ var totalLines = 0;
|
|
|
|
|
|
- var width = lines.Max(x => x.Width);
|
|
|
- var height = lines.Sum(x => x.LineHeight);
|
|
|
+ for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
|
|
|
+ {
|
|
|
+ var lineMetric = LineMetrics[lineIndex];
|
|
|
+ var newTotalHeight = totalHeight + lineMetric.Height;
|
|
|
+
|
|
|
+ if (newTotalHeight > availableSpace.Height + Size.Epsilon)
|
|
|
+ break;
|
|
|
+
|
|
|
+ totalHeight = newTotalHeight;
|
|
|
+ totalLines++;
|
|
|
+ }
|
|
|
|
|
|
- if (width > availableSpace.Width + Size.Epsilon || height > availableSpace.Height + Size.Epsilon)
|
|
|
+ if (totalLines == 0)
|
|
|
return SpacePlan.Wrap();
|
|
|
|
|
|
- var fullyRenderedItemsCount = lines
|
|
|
- .SelectMany(x => x.Elements)
|
|
|
- .GroupBy(x => x.Item)
|
|
|
- .Count(x => x.Any(y => y.Measurement.IsLast));
|
|
|
+ var requiredArea = new Size(
|
|
|
+ Math.Min(MaximumWidth, availableSpace.Width),
|
|
|
+ Math.Min(totalHeight, availableSpace.Height));
|
|
|
|
|
|
- if (fullyRenderedItemsCount == RenderingQueue.Count)
|
|
|
- return SpacePlan.FullRender(width, height);
|
|
|
-
|
|
|
- return SpacePlan.PartialRender(width, height);
|
|
|
+ if (CurrentLineIndex + totalLines < LineMetrics.Length)
|
|
|
+ return SpacePlan.PartialRender(requiredArea);
|
|
|
+
|
|
|
+ return SpacePlan.FullRender(requiredArea);
|
|
|
}
|
|
|
|
|
|
internal override void Draw(Size availableSpace)
|
|
|
{
|
|
|
- SetDefaultAlignment();
|
|
|
-
|
|
|
- var lines = DivideTextItemsIntoLines(availableSpace.Width, availableSpace.Height).ToList();
|
|
|
+ if (Items.Count == 0)
|
|
|
+ return;
|
|
|
|
|
|
- if (!lines.Any())
|
|
|
+ CalculateParagraphMetrics(availableSpace);
|
|
|
+
|
|
|
+ if (MaximumWidth == 0)
|
|
|
return;
|
|
|
|
|
|
- var topOffset = 0f;
|
|
|
+ var (linesToDraw, takenHeight) = DetermineLinesToDraw();
|
|
|
+ DrawParagraph();
|
|
|
+
|
|
|
+ CurrentLineIndex += linesToDraw;
|
|
|
+ CurrentTopOffset += takenHeight;
|
|
|
|
|
|
- foreach (var line in lines)
|
|
|
- {
|
|
|
- var leftOffset = GetAlignmentOffset(line.Width);
|
|
|
+ if (CurrentLineIndex == LineMetrics.Length)
|
|
|
+ ResetState();
|
|
|
+
|
|
|
+ return;
|
|
|
|
|
|
- foreach (var item in line.Elements.Where(x => x.Measurement.Width > 0.0))
|
|
|
+ (int linesToDraw, float takenHeight) DetermineLinesToDraw()
|
|
|
+ {
|
|
|
+ var linesToDraw = 0;
|
|
|
+ var takenHeight = 0f;
|
|
|
+
|
|
|
+ for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
|
|
|
{
|
|
|
- var textDrawingRequest = new TextDrawingRequest
|
|
|
- {
|
|
|
- Canvas = Canvas,
|
|
|
- PageContext = PageContext,
|
|
|
-
|
|
|
- StartIndex = item.Measurement.StartIndex,
|
|
|
- EndIndex = item.Measurement.EndIndex,
|
|
|
-
|
|
|
- TextSize = new Size(item.Measurement.Width, line.LineHeight),
|
|
|
- TotalAscent = line.Ascent
|
|
|
- };
|
|
|
+ var lineMetric = LineMetrics[lineIndex];
|
|
|
|
|
|
- var canvasOffset = ContentDirection == ContentDirection.LeftToRight
|
|
|
- ? new Position(leftOffset, topOffset - line.Ascent)
|
|
|
- : new Position(availableSpace.Width - leftOffset - item.Measurement.Width, topOffset - line.Ascent);
|
|
|
-
|
|
|
- Canvas.Translate(canvasOffset);
|
|
|
- item.Item.Draw(textDrawingRequest);
|
|
|
- Canvas.Translate(canvasOffset.Reverse());
|
|
|
+ var newTotalHeight = takenHeight + lineMetric.Height;
|
|
|
+
|
|
|
+ if (newTotalHeight > availableSpace.Height + Size.Epsilon)
|
|
|
+ break;
|
|
|
|
|
|
- leftOffset += item.Measurement.Width;
|
|
|
+ takenHeight = newTotalHeight;
|
|
|
+ linesToDraw++;
|
|
|
+ }
|
|
|
+
|
|
|
+ return (linesToDraw, takenHeight);
|
|
|
+ }
|
|
|
+
|
|
|
+ void DrawParagraph()
|
|
|
+ {
|
|
|
+ var takesMultiplePages = linesToDraw != LineMetrics.Length;
|
|
|
+
|
|
|
+ if (takesMultiplePages)
|
|
|
+ {
|
|
|
+ Canvas.Save();
|
|
|
+ Canvas.ClipRectangle(new SkRect(0, 0, availableSpace.Width, takenHeight));
|
|
|
+ Canvas.Translate(new Position(0, -CurrentTopOffset));
|
|
|
}
|
|
|
|
|
|
- topOffset += line.LineHeight;
|
|
|
+ Canvas.DrawParagraph(Paragraph);
|
|
|
+ DrawInjectedElements();
|
|
|
+ DrawHyperlinks();
|
|
|
+ DrawSectionLinks();
|
|
|
+
|
|
|
+ if (takesMultiplePages)
|
|
|
+ Canvas.Restore();
|
|
|
}
|
|
|
|
|
|
- lines
|
|
|
- .SelectMany(x => x.Elements)
|
|
|
- .GroupBy(x => x.Item)
|
|
|
- .Where(x => x.Any(y => y.Measurement.IsLast))
|
|
|
- .Select(x => x.Key)
|
|
|
- .ToList()
|
|
|
- .ForEach(x => RenderingQueue.Dequeue());
|
|
|
+ void DrawInjectedElements()
|
|
|
+ {
|
|
|
+ var elementItems = Items.OfType<TextBlockElement>().ToArray();
|
|
|
+
|
|
|
+ for (var placeholderIndex = 0; placeholderIndex < PlaceholderPositions.Length; placeholderIndex++)
|
|
|
+ {
|
|
|
+ var placeholder = PlaceholderPositions[placeholderIndex];
|
|
|
+ var associatedElement = elementItems[placeholderIndex];
|
|
|
+
|
|
|
+ associatedElement.ConfigureElement(PageContext, Canvas);
|
|
|
|
|
|
- var lastElementMeasurement = lines.Last().Elements.Last().Measurement;
|
|
|
- CurrentElementIndex = lastElementMeasurement.IsLast ? 0 : lastElementMeasurement.NextIndex;
|
|
|
+ var offset = new Position(placeholder.Left, placeholder.Top);
|
|
|
+
|
|
|
+ if (!IsPositionVisible(offset))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ Canvas.Translate(offset);
|
|
|
+ associatedElement.Element.Draw(new Size(placeholder.Width, placeholder.Height));
|
|
|
+ Canvas.Translate(offset.Reverse());
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (!RenderingQueue.Any())
|
|
|
- ResetState();
|
|
|
+ void DrawHyperlinks()
|
|
|
+ {
|
|
|
+ foreach (var hyperlink in Items.OfType<TextBlockHyperlink>())
|
|
|
+ {
|
|
|
+ var positions = Paragraph.GetTextRangePositions(hyperlink.ParagraphBeginIndex, hyperlink.ParagraphBeginIndex + hyperlink.Text.Length);
|
|
|
+
|
|
|
+ foreach (var position in positions)
|
|
|
+ {
|
|
|
+ var offset = new Position(position.Left, position.Top);
|
|
|
+
|
|
|
+ if (!IsPositionVisible(offset))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ Canvas.Translate(offset);
|
|
|
+ Canvas.DrawHyperlink(hyperlink.Url, new Size(position.Width, position.Height));
|
|
|
+ Canvas.Translate(offset.Reverse());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- float GetAlignmentOffset(float lineWidth)
|
|
|
+ void DrawSectionLinks()
|
|
|
{
|
|
|
- var emptySpace = availableSpace.Width - lineWidth;
|
|
|
-
|
|
|
- return Alignment switch
|
|
|
+ foreach (var sectionLink in Items.OfType<TextBlockSectionLink>())
|
|
|
{
|
|
|
- HorizontalAlignment.Left => ContentDirection == ContentDirection.LeftToRight ? 0 : emptySpace,
|
|
|
- HorizontalAlignment.Center => emptySpace / 2,
|
|
|
- HorizontalAlignment.Right => ContentDirection == ContentDirection.LeftToRight ? emptySpace : 0,
|
|
|
- _ => 0
|
|
|
- };
|
|
|
+ var positions = Paragraph.GetTextRangePositions(sectionLink.ParagraphBeginIndex, sectionLink.ParagraphBeginIndex + sectionLink.Text.Length);
|
|
|
+ var targetName = PageContext.GetDocumentLocationName(sectionLink.SectionName);
|
|
|
+
|
|
|
+ foreach (var position in positions)
|
|
|
+ {
|
|
|
+ var offset = new Position(position.Left, position.Top);
|
|
|
+
|
|
|
+ if (!IsPositionVisible(offset))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ Canvas.Translate(offset);
|
|
|
+ Canvas.DrawSectionLink(targetName, new Size(position.Width, position.Height));
|
|
|
+ Canvas.Translate(offset.Reverse());
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- public IEnumerable<TextLine> DivideTextItemsIntoLines(float availableWidth, float availableHeight)
|
|
|
+ bool IsPositionVisible(Position position)
|
|
|
+ {
|
|
|
+ return CurrentTopOffset <= position.Y || position.Y <= CurrentTopOffset + takenHeight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Initialize()
|
|
|
{
|
|
|
- var queue = new Queue<ITextBlockItem>(RenderingQueue);
|
|
|
- var currentItemIndex = CurrentElementIndex;
|
|
|
- var currentHeight = 0f;
|
|
|
+ if (Paragraph != null && !RebuildParagraphForEveryPage)
|
|
|
+ return;
|
|
|
+
|
|
|
+ RebuildParagraphForEveryPage = Items.Any(x => x is TextBlockPageNumber);
|
|
|
+ BuildParagraph();
|
|
|
+
|
|
|
+ AreParagraphMetricsValid = false;
|
|
|
+ }
|
|
|
|
|
|
- while (queue.Any())
|
|
|
+ private void BuildParagraph()
|
|
|
+ {
|
|
|
+ var paragraphStyle = new ParagraphStyleConfiguration
|
|
|
{
|
|
|
- var line = GetNextLine();
|
|
|
-
|
|
|
- if (!line.Elements.Any())
|
|
|
- yield break;
|
|
|
-
|
|
|
- if (currentHeight + line.LineHeight > availableHeight + Size.Epsilon)
|
|
|
- yield break;
|
|
|
+ Alignment = MapAlignment(Alignment ?? TextHorizontalAlignment.Start),
|
|
|
+ Direction = MapDirection(ContentDirection),
|
|
|
+ MaxLinesVisible = LineClamp ?? 1_000_000
|
|
|
+ };
|
|
|
+
|
|
|
+ var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
|
|
|
|
|
|
- currentHeight += line.LineHeight;
|
|
|
- yield return line;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ Paragraph = CreateParagraph(builder);
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ SkParagraphBuilderPoolManager.Return(builder);
|
|
|
}
|
|
|
|
|
|
- TextLine GetNextLine()
|
|
|
+ static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
|
|
|
{
|
|
|
- var currentWidth = 0f;
|
|
|
+ return alignment switch
|
|
|
+ {
|
|
|
+ TextHorizontalAlignment.Left => ParagraphStyleConfiguration.TextAlign.Left,
|
|
|
+ TextHorizontalAlignment.Center => ParagraphStyleConfiguration.TextAlign.Center,
|
|
|
+ TextHorizontalAlignment.Right => ParagraphStyleConfiguration.TextAlign.Right,
|
|
|
+ TextHorizontalAlignment.Justify => ParagraphStyleConfiguration.TextAlign.Justify,
|
|
|
+ TextHorizontalAlignment.Start => ParagraphStyleConfiguration.TextAlign.Start,
|
|
|
+ TextHorizontalAlignment.End => ParagraphStyleConfiguration.TextAlign.End,
|
|
|
+ _ => throw new Exception()
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- var currentLineElements = new List<TextLineElement>();
|
|
|
+ static ParagraphStyleConfiguration.TextDirection MapDirection(ContentDirection direction)
|
|
|
+ {
|
|
|
+ return direction switch
|
|
|
+ {
|
|
|
+ ContentDirection.LeftToRight => ParagraphStyleConfiguration.TextDirection.Ltr,
|
|
|
+ ContentDirection.RightToLeft => ParagraphStyleConfiguration.TextDirection.Rtl,
|
|
|
+ _ => throw new Exception()
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- while (true)
|
|
|
+ static SkPlaceholderStyle.PlaceholderAlignment MapInjectedTextAlignment(TextInjectedElementAlignment alignment)
|
|
|
+ {
|
|
|
+ return alignment switch
|
|
|
{
|
|
|
- if (!queue.Any())
|
|
|
- break;
|
|
|
+ TextInjectedElementAlignment.AboveBaseline => SkPlaceholderStyle.PlaceholderAlignment.AboveBaseline,
|
|
|
+ TextInjectedElementAlignment.BelowBaseline => SkPlaceholderStyle.PlaceholderAlignment.BelowBaseline,
|
|
|
+ TextInjectedElementAlignment.Top => SkPlaceholderStyle.PlaceholderAlignment.Top,
|
|
|
+ TextInjectedElementAlignment.Bottom => SkPlaceholderStyle.PlaceholderAlignment.Bottom,
|
|
|
+ TextInjectedElementAlignment.Middle => SkPlaceholderStyle.PlaceholderAlignment.Middle,
|
|
|
+ _ => throw new Exception()
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- var currentElement = queue.Peek();
|
|
|
-
|
|
|
- var measurementRequest = new TextMeasurementRequest
|
|
|
+ SkParagraph CreateParagraph(SkParagraphBuilder builder)
|
|
|
+ {
|
|
|
+ var currentTextIndex = 0;
|
|
|
+
|
|
|
+ foreach (var textBlockItem in Items)
|
|
|
+ {
|
|
|
+ if (textBlockItem is TextBlockSpan textBlockSpan)
|
|
|
{
|
|
|
- Canvas = Canvas,
|
|
|
- PageContext = PageContext,
|
|
|
-
|
|
|
- StartIndex = currentItemIndex,
|
|
|
- AvailableWidth = availableWidth - currentWidth,
|
|
|
-
|
|
|
- IsFirstElementInBlock = currentElement == Items.First(),
|
|
|
- IsFirstElementInLine = !currentLineElements.Any()
|
|
|
- };
|
|
|
-
|
|
|
- var measurementResponse = currentElement.Measure(measurementRequest);
|
|
|
+ if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
|
|
|
+ textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
|
|
|
+
|
|
|
+ else if (textBlockItem is TextBlockHyperlink textBlockHyperlink)
|
|
|
+ textBlockHyperlink.ParagraphBeginIndex = currentTextIndex;
|
|
|
+
|
|
|
+ else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
|
|
|
+ textBlockPageNumber.UpdatePageNumberText(PageContext);
|
|
|
|
|
|
- if (measurementResponse == null)
|
|
|
- break;
|
|
|
-
|
|
|
- currentLineElements.Add(new TextLineElement
|
|
|
+ var textStyle = textBlockSpan.Style.GetSkTextStyle();
|
|
|
+ builder.AddText(textBlockSpan.Text, textStyle);
|
|
|
+ currentTextIndex += textBlockSpan.Text.Length;
|
|
|
+ }
|
|
|
+ else if (textBlockItem is TextBlockElement textBlockElement)
|
|
|
{
|
|
|
- Item = currentElement,
|
|
|
- Measurement = measurementResponse
|
|
|
- });
|
|
|
-
|
|
|
- currentWidth += measurementResponse.Width;
|
|
|
- currentItemIndex = measurementResponse.NextIndex;
|
|
|
+ textBlockElement.ConfigureElement(PageContext, Canvas);
|
|
|
+ textBlockElement.UpdateElementSize();
|
|
|
|
|
|
- if (!measurementResponse.IsLast)
|
|
|
- break;
|
|
|
-
|
|
|
- currentItemIndex = 0;
|
|
|
- queue.Dequeue();
|
|
|
+ builder.AddPlaceholder(new SkPlaceholderStyle
|
|
|
+ {
|
|
|
+ Width = textBlockElement.ElementSize.Width,
|
|
|
+ Height = textBlockElement.ElementSize.Height,
|
|
|
+ Alignment = MapInjectedTextAlignment(textBlockElement.Alignment),
|
|
|
+ Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
|
|
|
+ BaselineOffset = 0
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- return TextLine.From(currentLineElements);
|
|
|
+ return builder.CreateParagraph();
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private void CalculateParagraphMetrics(Size availableSpace)
|
|
|
+ {
|
|
|
+ // SkParagraph seems to require a bigger space buffer to calculate metrics correctly
|
|
|
+ const float epsilon = 1f;
|
|
|
+
|
|
|
+ if (Math.Abs(WidthForLineMetricsCalculation - availableSpace.Width) > epsilon)
|
|
|
+ AreParagraphMetricsValid = false;
|
|
|
+
|
|
|
+ if (AreParagraphMetricsValid)
|
|
|
+ return;
|
|
|
+
|
|
|
+ WidthForLineMetricsCalculation = availableSpace.Width;
|
|
|
+
|
|
|
+ Paragraph.PlanLayout(availableSpace.Width + epsilon);
|
|
|
+ CheckUnresolvedGlyphs();
|
|
|
+
|
|
|
+ LineMetrics = Paragraph.GetLineMetrics();
|
|
|
+ PlaceholderPositions = Paragraph.GetPlaceholderPositions();
|
|
|
+ MaximumWidth = LineMetrics.Any() ? LineMetrics.Max(x => x.Width) : 0;
|
|
|
+
|
|
|
+ AreParagraphMetricsValid = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void CheckUnresolvedGlyphs()
|
|
|
+ {
|
|
|
+ if (!Settings.CheckIfAllTextGlyphsAreAvailable)
|
|
|
+ return;
|
|
|
+
|
|
|
+ var unsupportedGlyphs = Paragraph.GetUnresolvedCodepoints();
|
|
|
+
|
|
|
+ if (!unsupportedGlyphs.Any())
|
|
|
+ return;
|
|
|
+
|
|
|
+ var formattedGlyphs = unsupportedGlyphs
|
|
|
+ .Select(codepoint =>
|
|
|
+ {
|
|
|
+ var character = char.ConvertFromUtf32(codepoint);
|
|
|
+ return $"U-{codepoint:X4} '{character}'";
|
|
|
+ });
|
|
|
+
|
|
|
+ var glyphs = string.Join("\n", formattedGlyphs);
|
|
|
+
|
|
|
+ throw new DocumentDrawingException(
|
|
|
+ $"Could not find an appropriate font fallback for the following glyphs: \n" +
|
|
|
+ $"${glyphs} \n\n" +
|
|
|
+ $"Possible solutions: \n" +
|
|
|
+ $"1) Install fonts that contain missing glyphs in your runtime environment. \n" +
|
|
|
+ $"2) Configure the fallback TextStyle using the 'TextStyle.FontFamilyFallback' method. \n" +
|
|
|
+ $"3) Register additional application specific fonts using the 'FontManager.RegisterFont' method. \n\n" +
|
|
|
+ $"You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. \n" +
|
|
|
+ $"However, this may result with text glyphs being incorrectly rendered without any warning.");
|
|
|
+ }
|
|
|
}
|
|
|
}
|