TextBlock.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using QuestPDF.Drawing;
  5. using QuestPDF.Drawing.Exceptions;
  6. using QuestPDF.Elements.Text.Items;
  7. using QuestPDF.Helpers;
  8. using QuestPDF.Infrastructure;
  9. using QuestPDF.Skia;
  10. using QuestPDF.Skia.Text;
  11. namespace QuestPDF.Elements.Text
  12. {
  13. internal sealed class TextBlock : Element, IStateResettable, IContentDirectionAware
  14. {
  15. public ContentDirection ContentDirection { get; set; }
  16. public TextHorizontalAlignment? Alignment { get; set; }
  17. public int? LineClamp { get; set; }
  18. public List<ITextBlockItem> Items { get; set; } = new();
  19. private SkParagraph Paragraph { get; set; }
  20. private bool RebuildParagraphForEveryPage { get; set; }
  21. private bool AreParagraphMetricsValid { get; set; }
  22. private SkSize[] LineMetrics { get; set; }
  23. private float WidthForLineMetricsCalculation { get; set; }
  24. private SkRect[] PlaceholderPositions { get; set; }
  25. private float MaximumWidth { get; set; }
  26. private int CurrentLineIndex { get; set; }
  27. private float CurrentTopOffset { get; set; }
  28. public string Text => string.Join(" ", Items.OfType<TextBlockSpan>().Select(x => x.Text));
  29. ~TextBlock()
  30. {
  31. Paragraph?.Dispose();
  32. }
  33. public void ResetState()
  34. {
  35. CurrentLineIndex = 0;
  36. CurrentTopOffset = 0;
  37. }
  38. internal override SpacePlan Measure(Size availableSpace)
  39. {
  40. if (Items.Count == 0)
  41. return SpacePlan.FullRender(Size.Zero);
  42. Initialize();
  43. CalculateParagraphMetrics(availableSpace);
  44. if (MaximumWidth == 0)
  45. return SpacePlan.FullRender(Size.Zero);
  46. if (CurrentLineIndex > LineMetrics.Length)
  47. return SpacePlan.FullRender(Size.Zero);
  48. var totalHeight = 0f;
  49. var totalLines = 0;
  50. for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
  51. {
  52. var lineMetric = LineMetrics[lineIndex];
  53. var newTotalHeight = totalHeight + lineMetric.Height;
  54. if (newTotalHeight > availableSpace.Height + Size.Epsilon)
  55. break;
  56. totalHeight = newTotalHeight;
  57. totalLines++;
  58. }
  59. if (totalLines == 0)
  60. return SpacePlan.Wrap();
  61. var requiredArea = new Size(
  62. Math.Min(MaximumWidth, availableSpace.Width),
  63. Math.Min(totalHeight, availableSpace.Height));
  64. if (CurrentLineIndex + totalLines < LineMetrics.Length)
  65. return SpacePlan.PartialRender(requiredArea);
  66. return SpacePlan.FullRender(requiredArea);
  67. }
  68. internal override void Draw(Size availableSpace)
  69. {
  70. if (Items.Count == 0)
  71. return;
  72. CalculateParagraphMetrics(availableSpace);
  73. if (MaximumWidth == 0)
  74. return;
  75. var (linesToDraw, takenHeight) = DetermineLinesToDraw();
  76. DrawParagraph();
  77. CurrentLineIndex += linesToDraw;
  78. CurrentTopOffset += takenHeight;
  79. if (CurrentLineIndex == LineMetrics.Length)
  80. ResetState();
  81. return;
  82. (int linesToDraw, float takenHeight) DetermineLinesToDraw()
  83. {
  84. var linesToDraw = 0;
  85. var takenHeight = 0f;
  86. for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
  87. {
  88. var lineMetric = LineMetrics[lineIndex];
  89. var newTotalHeight = takenHeight + lineMetric.Height;
  90. if (newTotalHeight > availableSpace.Height + Size.Epsilon)
  91. break;
  92. takenHeight = newTotalHeight;
  93. linesToDraw++;
  94. }
  95. return (linesToDraw, takenHeight);
  96. }
  97. void DrawParagraph()
  98. {
  99. var takesMultiplePages = linesToDraw != LineMetrics.Length;
  100. if (takesMultiplePages)
  101. {
  102. Canvas.Save();
  103. Canvas.ClipRectangle(new SkRect(0, 0, availableSpace.Width, takenHeight));
  104. Canvas.Translate(new Position(0, -CurrentTopOffset));
  105. }
  106. Canvas.DrawParagraph(Paragraph);
  107. DrawInjectedElements();
  108. DrawHyperlinks();
  109. DrawSectionLinks();
  110. if (takesMultiplePages)
  111. Canvas.Restore();
  112. }
  113. void DrawInjectedElements()
  114. {
  115. var elementItems = Items.OfType<TextBlockElement>().ToArray();
  116. for (var placeholderIndex = 0; placeholderIndex < PlaceholderPositions.Length; placeholderIndex++)
  117. {
  118. var placeholder = PlaceholderPositions[placeholderIndex];
  119. var associatedElement = elementItems[placeholderIndex];
  120. associatedElement.ConfigureElement(PageContext, Canvas);
  121. var offset = new Position(placeholder.Left, placeholder.Top);
  122. if (!IsPositionVisible(offset))
  123. continue;
  124. Canvas.Translate(offset);
  125. associatedElement.Element.Draw(new Size(placeholder.Width, placeholder.Height));
  126. Canvas.Translate(offset.Reverse());
  127. }
  128. }
  129. void DrawHyperlinks()
  130. {
  131. foreach (var hyperlink in Items.OfType<TextBlockHyperlink>())
  132. {
  133. var positions = Paragraph.GetTextRangePositions(hyperlink.ParagraphBeginIndex, hyperlink.ParagraphBeginIndex + hyperlink.Text.Length);
  134. foreach (var position in positions)
  135. {
  136. var offset = new Position(position.Left, position.Top);
  137. if (!IsPositionVisible(offset))
  138. continue;
  139. Canvas.Translate(offset);
  140. Canvas.DrawHyperlink(hyperlink.Url, new Size(position.Width, position.Height));
  141. Canvas.Translate(offset.Reverse());
  142. }
  143. }
  144. }
  145. void DrawSectionLinks()
  146. {
  147. foreach (var sectionLink in Items.OfType<TextBlockSectionLink>())
  148. {
  149. var positions = Paragraph.GetTextRangePositions(sectionLink.ParagraphBeginIndex, sectionLink.ParagraphBeginIndex + sectionLink.Text.Length);
  150. var targetName = PageContext.GetDocumentLocationName(sectionLink.SectionName);
  151. foreach (var position in positions)
  152. {
  153. var offset = new Position(position.Left, position.Top);
  154. if (!IsPositionVisible(offset))
  155. continue;
  156. Canvas.Translate(offset);
  157. Canvas.DrawSectionLink(targetName, new Size(position.Width, position.Height));
  158. Canvas.Translate(offset.Reverse());
  159. }
  160. }
  161. }
  162. bool IsPositionVisible(Position position)
  163. {
  164. return CurrentTopOffset <= position.Y || position.Y <= CurrentTopOffset + takenHeight;
  165. }
  166. }
  167. private void Initialize()
  168. {
  169. if (Paragraph != null && !RebuildParagraphForEveryPage)
  170. return;
  171. RebuildParagraphForEveryPage = Items.Any(x => x is TextBlockPageNumber);
  172. BuildParagraph();
  173. AreParagraphMetricsValid = false;
  174. }
  175. private void BuildParagraph()
  176. {
  177. var paragraphStyle = new ParagraphStyleConfiguration
  178. {
  179. Alignment = MapAlignment(Alignment ?? TextHorizontalAlignment.Start),
  180. Direction = MapDirection(ContentDirection),
  181. MaxLinesVisible = LineClamp ?? 1_000_000
  182. };
  183. var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
  184. try
  185. {
  186. Paragraph = CreateParagraph(builder);
  187. }
  188. finally
  189. {
  190. SkParagraphBuilderPoolManager.Return(builder);
  191. }
  192. static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
  193. {
  194. return alignment switch
  195. {
  196. TextHorizontalAlignment.Left => ParagraphStyleConfiguration.TextAlign.Left,
  197. TextHorizontalAlignment.Center => ParagraphStyleConfiguration.TextAlign.Center,
  198. TextHorizontalAlignment.Right => ParagraphStyleConfiguration.TextAlign.Right,
  199. TextHorizontalAlignment.Justify => ParagraphStyleConfiguration.TextAlign.Justify,
  200. TextHorizontalAlignment.Start => ParagraphStyleConfiguration.TextAlign.Start,
  201. TextHorizontalAlignment.End => ParagraphStyleConfiguration.TextAlign.End,
  202. _ => throw new Exception()
  203. };
  204. }
  205. static ParagraphStyleConfiguration.TextDirection MapDirection(ContentDirection direction)
  206. {
  207. return direction switch
  208. {
  209. ContentDirection.LeftToRight => ParagraphStyleConfiguration.TextDirection.Ltr,
  210. ContentDirection.RightToLeft => ParagraphStyleConfiguration.TextDirection.Rtl,
  211. _ => throw new Exception()
  212. };
  213. }
  214. static SkPlaceholderStyle.PlaceholderAlignment MapInjectedTextAlignment(TextInjectedElementAlignment alignment)
  215. {
  216. return alignment switch
  217. {
  218. TextInjectedElementAlignment.AboveBaseline => SkPlaceholderStyle.PlaceholderAlignment.AboveBaseline,
  219. TextInjectedElementAlignment.BelowBaseline => SkPlaceholderStyle.PlaceholderAlignment.BelowBaseline,
  220. TextInjectedElementAlignment.Top => SkPlaceholderStyle.PlaceholderAlignment.Top,
  221. TextInjectedElementAlignment.Bottom => SkPlaceholderStyle.PlaceholderAlignment.Bottom,
  222. TextInjectedElementAlignment.Middle => SkPlaceholderStyle.PlaceholderAlignment.Middle,
  223. _ => throw new Exception()
  224. };
  225. }
  226. SkParagraph CreateParagraph(SkParagraphBuilder builder)
  227. {
  228. var currentTextIndex = 0;
  229. foreach (var textBlockItem in Items)
  230. {
  231. if (textBlockItem is TextBlockSpan textBlockSpan)
  232. {
  233. if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
  234. textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
  235. else if (textBlockItem is TextBlockHyperlink textBlockHyperlink)
  236. textBlockHyperlink.ParagraphBeginIndex = currentTextIndex;
  237. else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
  238. textBlockPageNumber.UpdatePageNumberText(PageContext);
  239. var textStyle = textBlockSpan.Style.GetSkTextStyle();
  240. builder.AddText(textBlockSpan.Text, textStyle);
  241. currentTextIndex += textBlockSpan.Text.Length;
  242. }
  243. else if (textBlockItem is TextBlockElement textBlockElement)
  244. {
  245. textBlockElement.ConfigureElement(PageContext, Canvas);
  246. textBlockElement.UpdateElementSize();
  247. builder.AddPlaceholder(new SkPlaceholderStyle
  248. {
  249. Width = textBlockElement.ElementSize.Width,
  250. Height = textBlockElement.ElementSize.Height,
  251. Alignment = MapInjectedTextAlignment(textBlockElement.Alignment),
  252. Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
  253. BaselineOffset = 0
  254. });
  255. }
  256. }
  257. return builder.CreateParagraph();
  258. }
  259. }
  260. private void CalculateParagraphMetrics(Size availableSpace)
  261. {
  262. // SkParagraph seems to require a bigger space buffer to calculate metrics correctly
  263. const float epsilon = 1f;
  264. if (Math.Abs(WidthForLineMetricsCalculation - availableSpace.Width) > epsilon)
  265. AreParagraphMetricsValid = false;
  266. if (AreParagraphMetricsValid)
  267. return;
  268. WidthForLineMetricsCalculation = availableSpace.Width;
  269. Paragraph.PlanLayout(availableSpace.Width + epsilon);
  270. CheckUnresolvedGlyphs();
  271. LineMetrics = Paragraph.GetLineMetrics();
  272. PlaceholderPositions = Paragraph.GetPlaceholderPositions();
  273. MaximumWidth = LineMetrics.Any() ? LineMetrics.Max(x => x.Width) : 0;
  274. AreParagraphMetricsValid = true;
  275. }
  276. private void CheckUnresolvedGlyphs()
  277. {
  278. if (!Settings.CheckIfAllTextGlyphsAreAvailable)
  279. return;
  280. var unsupportedGlyphs = Paragraph.GetUnresolvedCodepoints();
  281. if (!unsupportedGlyphs.Any())
  282. return;
  283. var formattedGlyphs = unsupportedGlyphs
  284. .Select(codepoint =>
  285. {
  286. var character = char.ConvertFromUtf32(codepoint);
  287. return $"U-{codepoint:X4} '{character}'";
  288. });
  289. var glyphs = string.Join("\n", formattedGlyphs);
  290. throw new DocumentDrawingException(
  291. $"Could not find an appropriate font fallback for the following glyphs: \n" +
  292. $"${glyphs} \n\n" +
  293. $"Possible solutions: \n" +
  294. $"1) Install fonts that contain missing glyphs in your runtime environment. \n" +
  295. $"2) Configure the fallback TextStyle using the 'TextStyle.FontFamilyFallback' method. \n" +
  296. $"3) Register additional application specific fonts using the 'FontManager.RegisterFont' method. \n\n" +
  297. $"You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. \n" +
  298. $"However, this may result with text glyphs being incorrectly rendered without any warning.");
  299. }
  300. }
  301. }