| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383 |
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using QuestPDF.Drawing;
- 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
- {
- internal sealed class TextBlock : Element, IStateResettable, IContentDirectionAware
- {
- public ContentDirection ContentDirection { get; set; }
-
- 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));
- ~TextBlock()
- {
- Paragraph?.Dispose();
- }
-
- public void ResetState()
- {
- CurrentLineIndex = 0;
- CurrentTopOffset = 0;
- }
-
- internal override SpacePlan Measure(Size availableSpace)
- {
- if (Items.Count == 0)
- return SpacePlan.FullRender(Size.Zero);
-
- Initialize();
- CalculateParagraphMetrics(availableSpace);
- if (MaximumWidth == 0)
- return SpacePlan.FullRender(Size.Zero);
-
- if (CurrentLineIndex > LineMetrics.Length)
- return SpacePlan.FullRender(Size.Zero);
-
- var totalHeight = 0f;
- var totalLines = 0;
-
- 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 (totalLines == 0)
- return SpacePlan.Wrap();
- var requiredArea = new Size(
- Math.Min(MaximumWidth, availableSpace.Width),
- Math.Min(totalHeight, availableSpace.Height));
-
- if (CurrentLineIndex + totalLines < LineMetrics.Length)
- return SpacePlan.PartialRender(requiredArea);
- return SpacePlan.FullRender(requiredArea);
- }
- internal override void Draw(Size availableSpace)
- {
- if (Items.Count == 0)
- return;
-
- CalculateParagraphMetrics(availableSpace);
- if (MaximumWidth == 0)
- return;
-
- var (linesToDraw, takenHeight) = DetermineLinesToDraw();
- DrawParagraph();
-
- CurrentLineIndex += linesToDraw;
- CurrentTopOffset += takenHeight;
- if (CurrentLineIndex == LineMetrics.Length)
- ResetState();
-
- return;
- (int linesToDraw, float takenHeight) DetermineLinesToDraw()
- {
- var linesToDraw = 0;
- var takenHeight = 0f;
-
- for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
- {
- var lineMetric = LineMetrics[lineIndex];
-
- var newTotalHeight = takenHeight + lineMetric.Height;
- if (newTotalHeight > availableSpace.Height + Size.Epsilon)
- break;
-
- 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));
- }
-
- Canvas.DrawParagraph(Paragraph);
- DrawInjectedElements();
- DrawHyperlinks();
- DrawSectionLinks();
-
- if (takesMultiplePages)
- Canvas.Restore();
- }
- 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 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());
- }
- }
-
- 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());
- }
- }
- }
-
- void DrawSectionLinks()
- {
- foreach (var sectionLink in Items.OfType<TextBlockSectionLink>())
- {
- 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());
- }
- }
- }
- bool IsPositionVisible(Position position)
- {
- return CurrentTopOffset <= position.Y || position.Y <= CurrentTopOffset + takenHeight;
- }
- }
-
- private void Initialize()
- {
- if (Paragraph != null && !RebuildParagraphForEveryPage)
- return;
-
- RebuildParagraphForEveryPage = Items.Any(x => x is TextBlockPageNumber);
- BuildParagraph();
-
- AreParagraphMetricsValid = false;
- }
- private void BuildParagraph()
- {
- var paragraphStyle = new ParagraphStyleConfiguration
- {
- Alignment = MapAlignment(Alignment ?? TextHorizontalAlignment.Start),
- Direction = MapDirection(ContentDirection),
- MaxLinesVisible = LineClamp ?? 1_000_000
- };
-
- var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
- try
- {
- Paragraph = CreateParagraph(builder);
- }
- finally
- {
- SkParagraphBuilderPoolManager.Return(builder);
- }
- static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
- {
- 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()
- };
- }
- static ParagraphStyleConfiguration.TextDirection MapDirection(ContentDirection direction)
- {
- return direction switch
- {
- ContentDirection.LeftToRight => ParagraphStyleConfiguration.TextDirection.Ltr,
- ContentDirection.RightToLeft => ParagraphStyleConfiguration.TextDirection.Rtl,
- _ => throw new Exception()
- };
- }
-
- static SkPlaceholderStyle.PlaceholderAlignment MapInjectedTextAlignment(TextInjectedElementAlignment alignment)
- {
- return alignment switch
- {
- 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()
- };
- }
- SkParagraph CreateParagraph(SkParagraphBuilder builder)
- {
- var currentTextIndex = 0;
-
- foreach (var textBlockItem in Items)
- {
- if (textBlockItem is TextBlockSpan textBlockSpan)
- {
- 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);
-
- var textStyle = textBlockSpan.Style.GetSkTextStyle();
- builder.AddText(textBlockSpan.Text, textStyle);
- currentTextIndex += textBlockSpan.Text.Length;
- }
- else if (textBlockItem is TextBlockElement textBlockElement)
- {
- textBlockElement.ConfigureElement(PageContext, Canvas);
- textBlockElement.UpdateElementSize();
-
- builder.AddPlaceholder(new SkPlaceholderStyle
- {
- Width = textBlockElement.ElementSize.Width,
- Height = textBlockElement.ElementSize.Height,
- Alignment = MapInjectedTextAlignment(textBlockElement.Alignment),
- Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
- BaselineOffset = 0
- });
- }
- }
- 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.");
- }
- }
- }
|