TextBlock.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  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 string LineClampEllipsis { get; set; }
  19. public float ParagraphSpacing { get; set; }
  20. public float ParagraphFirstLineIndentation { get; set; }
  21. public List<ITextBlockItem> Items { get; set; } = new();
  22. private SkParagraph Paragraph { get; set; }
  23. private bool RebuildParagraphForEveryPage { get; set; }
  24. private bool AreParagraphMetricsValid { get; set; }
  25. private bool AreParagraphItemsTransformedWithSpacingAndIndentation { get; set; }
  26. private SkSize[] LineMetrics { get; set; }
  27. private float WidthForLineMetricsCalculation { get; set; }
  28. private SkRect[] PlaceholderPositions { get; set; }
  29. private float MaximumWidth { get; set; }
  30. private bool IsRendered { get; set; }
  31. private int CurrentLineIndex { get; set; }
  32. private float CurrentTopOffset { get; set; }
  33. public string Text => string.Join(" ", Items.OfType<TextBlockSpan>().Select(x => x.Text));
  34. ~TextBlock()
  35. {
  36. Paragraph?.Dispose();
  37. }
  38. public void ResetState(bool hardReset)
  39. {
  40. IsRendered = false;
  41. CurrentLineIndex = 0;
  42. CurrentTopOffset = 0;
  43. }
  44. internal override SpacePlan Measure(Size availableSpace)
  45. {
  46. if (Items.Count == 0)
  47. return SpacePlan.FullRender(Size.Zero);
  48. if (IsRendered)
  49. return SpacePlan.FullRender(Size.Zero);
  50. Initialize();
  51. CalculateParagraphMetrics(availableSpace);
  52. if (MaximumWidth == 0)
  53. return SpacePlan.FullRender(Size.Zero);
  54. var totalHeight = 0f;
  55. var totalLines = 0;
  56. for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
  57. {
  58. var lineMetric = LineMetrics[lineIndex];
  59. var newTotalHeight = totalHeight + lineMetric.Height;
  60. if (newTotalHeight > availableSpace.Height + Size.Epsilon)
  61. break;
  62. totalHeight = newTotalHeight;
  63. totalLines++;
  64. }
  65. if (totalLines == 0)
  66. return SpacePlan.Wrap();
  67. var requiredArea = new Size(
  68. Math.Min(MaximumWidth, availableSpace.Width),
  69. Math.Min(totalHeight, availableSpace.Height));
  70. if (CurrentLineIndex + totalLines < LineMetrics.Length)
  71. return SpacePlan.PartialRender(requiredArea);
  72. return SpacePlan.FullRender(requiredArea);
  73. }
  74. internal override void Draw(Size availableSpace)
  75. {
  76. if (Items.Count == 0)
  77. return;
  78. CalculateParagraphMetrics(availableSpace);
  79. if (MaximumWidth == 0)
  80. return;
  81. var (linesToDraw, takenHeight) = DetermineLinesToDraw();
  82. DrawParagraph();
  83. CurrentLineIndex += linesToDraw;
  84. CurrentTopOffset += takenHeight;
  85. if (CurrentLineIndex == LineMetrics.Length)
  86. IsRendered = true;
  87. return;
  88. (int linesToDraw, float takenHeight) DetermineLinesToDraw()
  89. {
  90. var linesToDraw = 0;
  91. var takenHeight = 0f;
  92. for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
  93. {
  94. var lineMetric = LineMetrics[lineIndex];
  95. var newTotalHeight = takenHeight + lineMetric.Height;
  96. if (newTotalHeight > availableSpace.Height + Size.Epsilon)
  97. break;
  98. takenHeight = newTotalHeight;
  99. linesToDraw++;
  100. }
  101. return (linesToDraw, takenHeight);
  102. }
  103. void DrawParagraph()
  104. {
  105. var takesMultiplePages = linesToDraw != LineMetrics.Length;
  106. if (takesMultiplePages)
  107. {
  108. Canvas.Save();
  109. Canvas.ClipRectangle(new SkRect(0, 0, availableSpace.Width, takenHeight));
  110. Canvas.Translate(new Position(0, -CurrentTopOffset));
  111. }
  112. Canvas.DrawParagraph(Paragraph);
  113. DrawInjectedElements();
  114. DrawHyperlinks();
  115. DrawSectionLinks();
  116. if (takesMultiplePages)
  117. Canvas.Restore();
  118. }
  119. void DrawInjectedElements()
  120. {
  121. foreach (var textBlockElement in Items.OfType<TextBlockElement>())
  122. {
  123. var placeholder = PlaceholderPositions[textBlockElement.ParagraphBlockIndex];
  124. textBlockElement.ConfigureElement(PageContext, Canvas);
  125. var offset = new Position(placeholder.Left, placeholder.Top);
  126. if (!IsPositionVisible(offset))
  127. continue;
  128. Canvas.Translate(offset);
  129. textBlockElement.Element.Draw(new Size(placeholder.Width, placeholder.Height));
  130. Canvas.Translate(offset.Reverse());
  131. }
  132. }
  133. void DrawHyperlinks()
  134. {
  135. foreach (var hyperlink in Items.OfType<TextBlockHyperlink>())
  136. {
  137. var positions = Paragraph.GetTextRangePositions(hyperlink.ParagraphBeginIndex, hyperlink.ParagraphBeginIndex + hyperlink.Text.Length);
  138. foreach (var position in positions)
  139. {
  140. var offset = new Position(position.Left, position.Top);
  141. if (!IsPositionVisible(offset))
  142. continue;
  143. Canvas.Translate(offset);
  144. Canvas.DrawHyperlink(hyperlink.Url, new Size(position.Width, position.Height));
  145. Canvas.Translate(offset.Reverse());
  146. }
  147. }
  148. }
  149. void DrawSectionLinks()
  150. {
  151. foreach (var sectionLink in Items.OfType<TextBlockSectionLink>())
  152. {
  153. var positions = Paragraph.GetTextRangePositions(sectionLink.ParagraphBeginIndex, sectionLink.ParagraphBeginIndex + sectionLink.Text.Length);
  154. var targetName = PageContext.GetDocumentLocationName(sectionLink.SectionName);
  155. foreach (var position in positions)
  156. {
  157. var offset = new Position(position.Left, position.Top);
  158. if (!IsPositionVisible(offset))
  159. continue;
  160. Canvas.Translate(offset);
  161. Canvas.DrawSectionLink(targetName, new Size(position.Width, position.Height));
  162. Canvas.Translate(offset.Reverse());
  163. }
  164. }
  165. }
  166. bool IsPositionVisible(Position position)
  167. {
  168. return CurrentTopOffset <= position.Y || position.Y <= CurrentTopOffset + takenHeight;
  169. }
  170. }
  171. private void Initialize()
  172. {
  173. if (Paragraph != null && !RebuildParagraphForEveryPage)
  174. return;
  175. if (!AreParagraphItemsTransformedWithSpacingAndIndentation)
  176. {
  177. Items = ApplyParagraphSpacingToTextBlockItems().ToList();
  178. AreParagraphItemsTransformedWithSpacingAndIndentation = true;
  179. }
  180. RebuildParagraphForEveryPage = Items.Any(x => x is TextBlockPageNumber);
  181. BuildParagraph();
  182. AreParagraphMetricsValid = false;
  183. }
  184. private void BuildParagraph()
  185. {
  186. using var clampLinesEllipsis = new SkText(LineClampEllipsis);
  187. var paragraphStyle = new ParagraphStyleConfiguration
  188. {
  189. Alignment = MapAlignment(Alignment ?? TextHorizontalAlignment.Start),
  190. Direction = MapDirection(ContentDirection),
  191. MaxLinesVisible = LineClamp ?? 1_000_000,
  192. LineClampEllipsis = clampLinesEllipsis.Instance
  193. };
  194. var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
  195. try
  196. {
  197. Paragraph = CreateParagraph(builder);
  198. }
  199. finally
  200. {
  201. SkParagraphBuilderPoolManager.Return(builder);
  202. }
  203. static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
  204. {
  205. return alignment switch
  206. {
  207. TextHorizontalAlignment.Left => ParagraphStyleConfiguration.TextAlign.Left,
  208. TextHorizontalAlignment.Center => ParagraphStyleConfiguration.TextAlign.Center,
  209. TextHorizontalAlignment.Right => ParagraphStyleConfiguration.TextAlign.Right,
  210. TextHorizontalAlignment.Justify => ParagraphStyleConfiguration.TextAlign.Justify,
  211. TextHorizontalAlignment.Start => ParagraphStyleConfiguration.TextAlign.Start,
  212. TextHorizontalAlignment.End => ParagraphStyleConfiguration.TextAlign.End,
  213. _ => throw new Exception()
  214. };
  215. }
  216. static ParagraphStyleConfiguration.TextDirection MapDirection(ContentDirection direction)
  217. {
  218. return direction switch
  219. {
  220. ContentDirection.LeftToRight => ParagraphStyleConfiguration.TextDirection.Ltr,
  221. ContentDirection.RightToLeft => ParagraphStyleConfiguration.TextDirection.Rtl,
  222. _ => throw new Exception()
  223. };
  224. }
  225. static SkPlaceholderStyle.PlaceholderAlignment MapInjectedTextAlignment(TextInjectedElementAlignment alignment)
  226. {
  227. return alignment switch
  228. {
  229. TextInjectedElementAlignment.AboveBaseline => SkPlaceholderStyle.PlaceholderAlignment.AboveBaseline,
  230. TextInjectedElementAlignment.BelowBaseline => SkPlaceholderStyle.PlaceholderAlignment.BelowBaseline,
  231. TextInjectedElementAlignment.Top => SkPlaceholderStyle.PlaceholderAlignment.Top,
  232. TextInjectedElementAlignment.Bottom => SkPlaceholderStyle.PlaceholderAlignment.Bottom,
  233. TextInjectedElementAlignment.Middle => SkPlaceholderStyle.PlaceholderAlignment.Middle,
  234. _ => throw new Exception()
  235. };
  236. }
  237. SkParagraph CreateParagraph(SkParagraphBuilder builder)
  238. {
  239. var currentTextIndex = 0;
  240. var currentBlockIndex = 0;
  241. foreach (var textBlockItem in Items)
  242. {
  243. if (textBlockItem is TextBlockSpan textBlockSpan)
  244. {
  245. if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
  246. textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
  247. else if (textBlockItem is TextBlockHyperlink textBlockHyperlink)
  248. textBlockHyperlink.ParagraphBeginIndex = currentTextIndex;
  249. else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
  250. textBlockPageNumber.UpdatePageNumberText(PageContext);
  251. var textStyle = textBlockSpan.Style.GetSkTextStyle();
  252. var text = textBlockSpan.Text?.Replace("\r", "") ?? "";
  253. builder.AddText(text, textStyle);
  254. currentTextIndex += text.Length;
  255. }
  256. else if (textBlockItem is TextBlockElement textBlockElement)
  257. {
  258. textBlockElement.ConfigureElement(PageContext, Canvas);
  259. textBlockElement.UpdateElementSize();
  260. textBlockElement.ParagraphBlockIndex = currentBlockIndex;
  261. builder.AddPlaceholder(new SkPlaceholderStyle
  262. {
  263. Width = textBlockElement.ElementSize.Width,
  264. Height = textBlockElement.ElementSize.Height,
  265. Alignment = MapInjectedTextAlignment(textBlockElement.Alignment),
  266. Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
  267. BaselineOffset = 0
  268. });
  269. currentTextIndex++;
  270. currentBlockIndex++;
  271. }
  272. else if (textBlockItem is TextBlockParagraphSpacing spacing)
  273. {
  274. builder.AddPlaceholder(new SkPlaceholderStyle
  275. {
  276. Width = spacing.Width,
  277. Height = spacing.Height,
  278. Alignment = SkPlaceholderStyle.PlaceholderAlignment.Middle,
  279. Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
  280. BaselineOffset = 0
  281. });
  282. currentTextIndex++;
  283. currentBlockIndex++;
  284. }
  285. }
  286. return builder.CreateParagraph();
  287. }
  288. }
  289. private IEnumerable<ITextBlockItem> ApplyParagraphSpacingToTextBlockItems()
  290. {
  291. if (ParagraphSpacing < Size.Epsilon && ParagraphFirstLineIndentation < Size.Epsilon)
  292. return Items;
  293. var result = new List<ITextBlockItem>();
  294. AddParagraphFirstLineIndentation();
  295. foreach (var textBlockItem in Items)
  296. {
  297. if (textBlockItem is not TextBlockSpan textBlockSpan)
  298. {
  299. result.Add(textBlockItem);
  300. continue;
  301. }
  302. if (textBlockItem is TextBlockPageNumber)
  303. {
  304. result.Add(textBlockItem);
  305. continue;
  306. }
  307. var textFragments = textBlockSpan.Text.Split('\n');
  308. foreach (var textFragment in textFragments)
  309. {
  310. AddClonedTextBlockSpanWithTextFragment(textBlockSpan, textFragment);
  311. if (textFragment == textFragments.Last())
  312. continue;
  313. AddParagraphSpacing();
  314. AddParagraphFirstLineIndentation();
  315. }
  316. }
  317. return result;
  318. void AddClonedTextBlockSpanWithTextFragment(TextBlockSpan originalSpan, string textFragment)
  319. {
  320. TextBlockSpan newItem;
  321. if (originalSpan is TextBlockSectionLink textBlockSectionLink)
  322. newItem = new TextBlockSectionLink { SectionName = textBlockSectionLink.SectionName };
  323. else if (originalSpan is TextBlockHyperlink textBlockHyperlink)
  324. newItem = new TextBlockHyperlink { Url = textBlockHyperlink.Url };
  325. else if (originalSpan is TextBlockPageNumber textBlockPageNumber)
  326. newItem = textBlockPageNumber;
  327. else
  328. newItem = new TextBlockSpan();
  329. newItem.Text = textFragment;
  330. newItem.Style = originalSpan.Style;
  331. result.Add(newItem);
  332. }
  333. void AddParagraphSpacing()
  334. {
  335. if (ParagraphSpacing <= Size.Epsilon)
  336. return;
  337. // space ensure proper line spacing
  338. result.Add(new TextBlockSpan() { Text = "\n ", Style = TextStyle.ParagraphSpacing });
  339. result.Add(new TextBlockParagraphSpacing(0, ParagraphSpacing));
  340. result.Add(new TextBlockSpan() { Text = " \n", Style = TextStyle.ParagraphSpacing });
  341. }
  342. void AddParagraphFirstLineIndentation()
  343. {
  344. if (ParagraphFirstLineIndentation <= Size.Epsilon)
  345. return;
  346. result.Add(new TextBlockSpan() { Text = "\n", Style = TextStyle.ParagraphSpacing });
  347. result.Add(new TextBlockParagraphSpacing(ParagraphFirstLineIndentation, 0));
  348. }
  349. }
  350. private void CalculateParagraphMetrics(Size availableSpace)
  351. {
  352. // SkParagraph seems to require a bigger space buffer to calculate metrics correctly
  353. const float epsilon = 1f;
  354. if (Math.Abs(WidthForLineMetricsCalculation - availableSpace.Width) > epsilon)
  355. AreParagraphMetricsValid = false;
  356. if (AreParagraphMetricsValid)
  357. return;
  358. WidthForLineMetricsCalculation = availableSpace.Width;
  359. Paragraph.PlanLayout(availableSpace.Width + epsilon);
  360. CheckUnresolvedGlyphs();
  361. LineMetrics = Paragraph.GetLineMetrics();
  362. PlaceholderPositions = Paragraph.GetPlaceholderPositions();
  363. MaximumWidth = LineMetrics.Any() ? LineMetrics.Max(x => x.Width) : 0;
  364. AreParagraphMetricsValid = true;
  365. }
  366. private void CheckUnresolvedGlyphs()
  367. {
  368. if (!Settings.CheckIfAllTextGlyphsAreAvailable)
  369. return;
  370. var unsupportedGlyphs = Paragraph.GetUnresolvedCodepoints();
  371. if (!unsupportedGlyphs.Any())
  372. return;
  373. var formattedGlyphs = unsupportedGlyphs
  374. .Select(codepoint =>
  375. {
  376. var character = char.ConvertFromUtf32(codepoint);
  377. return $"U-{codepoint:X4} '{character}'";
  378. });
  379. var glyphs = string.Join("\n", formattedGlyphs);
  380. throw new DocumentDrawingException(
  381. $"Could not find an appropriate font fallback for the following glyphs: \n" +
  382. $"${glyphs} \n\n" +
  383. $"Possible solutions: \n" +
  384. $"1) Install fonts that contain missing glyphs in your runtime environment. \n" +
  385. $"2) Configure the fallback TextStyle using the 'TextStyle.FontFamilyFallback' method. \n" +
  386. $"3) Register additional application specific fonts using the 'FontManager.RegisterFont' method. \n\n" +
  387. $"You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. \n" +
  388. $"However, this may result with text glyphs being incorrectly rendered without any warning.");
  389. }
  390. }
  391. }