LayoutTest.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. using System.Collections;
  2. using QuestPDF.Drawing;
  3. using QuestPDF.Drawing.Proxy;
  4. using QuestPDF.Elements;
  5. using QuestPDF.Fluent;
  6. using QuestPDF.Helpers;
  7. using QuestPDF.Infrastructure;
  8. using SkiaSharp;
  9. namespace QuestPDF.LayoutTests.TestEngine;
  10. internal class LayoutBuilderDescriptor
  11. {
  12. public void Compose(Action<IContainer> container)
  13. {
  14. }
  15. }
  16. internal class DocumentLayoutBuilder
  17. {
  18. public List<PageDrawingCommand> Commands { get; } = new();
  19. public PageLayoutDescriptor Page()
  20. {
  21. var page = new PageDrawingCommand();
  22. Commands.Add(page);
  23. return new PageLayoutDescriptor(page);
  24. }
  25. }
  26. internal class PageLayoutDescriptor
  27. {
  28. private PageDrawingCommand Command { get; }
  29. public PageLayoutDescriptor(PageDrawingCommand command)
  30. {
  31. Command = command;
  32. }
  33. public PageLayoutDescriptor TakenAreaSize(float width, float height)
  34. {
  35. Command.RequiredArea = new Size(width, height);
  36. return this;
  37. }
  38. public PageLayoutDescriptor Content(Action<PageLayoutBuilder> content)
  39. {
  40. var pageContent = new PageLayoutBuilder();
  41. content(pageContent);
  42. Command.Children = pageContent.Commands;
  43. return this;
  44. }
  45. }
  46. internal class PageLayoutBuilder
  47. {
  48. public List<ChildDrawingCommand> Commands { get;} = new();
  49. public ChildLayoutDescriptor Child(string childId)
  50. {
  51. var child = new ChildDrawingCommand { ChildId = childId };
  52. Commands.Add(child);
  53. return new ChildLayoutDescriptor(child);
  54. }
  55. }
  56. internal class ChildLayoutDescriptor
  57. {
  58. private ChildDrawingCommand Command { get; }
  59. public ChildLayoutDescriptor(ChildDrawingCommand command)
  60. {
  61. Command = command;
  62. }
  63. public ChildLayoutDescriptor Position(float x, float y)
  64. {
  65. Command.Position = new Position(x, y);
  66. return this;
  67. }
  68. public ChildLayoutDescriptor Size(float width, float height)
  69. {
  70. Command.Size = new Size(width, height);
  71. return this;
  72. }
  73. }
  74. internal class ExpectedLayoutChildPosition
  75. {
  76. public string ElementId { get; set; }
  77. public int PageNumber { get; set; }
  78. public int DepthIndex { get; set; }
  79. public Position Position { get; set; }
  80. public Size Size { get; set; }
  81. }
  82. internal static class ElementExtensions
  83. {
  84. public static MockDescriptor Mock(this IContainer element, string id)
  85. {
  86. var mock = new ElementMock
  87. {
  88. Id = id
  89. };
  90. element.Element(mock);
  91. return new MockDescriptor(mock);
  92. }
  93. }
  94. internal class MockDescriptor
  95. {
  96. private ElementMock Mock { get; }
  97. public MockDescriptor(ElementMock mock)
  98. {
  99. Mock = mock;
  100. }
  101. public MockDescriptor Size(float width, float height)
  102. {
  103. Mock.TotalWidth = width;
  104. Mock.TotalHeight = height;
  105. return this;
  106. }
  107. }
  108. internal class LayoutTest
  109. {
  110. private const string DocumentColor = Colors.Grey.Lighten1;
  111. private const string PageColor = Colors.Grey.Lighten3;
  112. private const string TargetColor = Colors.White;
  113. private const string GridColor = Colors.Grey.Lighten1;
  114. public Size PageSize { get; set; }
  115. public ICollection<PageDrawingCommand> ActualCommands { get; set; }
  116. public ICollection<PageDrawingCommand> ExpectedCommands { get; set; }
  117. public static LayoutTest HavingSpaceOfSize(float width, float height)
  118. {
  119. return new LayoutTest
  120. {
  121. PageSize = new Size(width, height)
  122. };
  123. }
  124. public LayoutTest WithContent(Action<IContainer> handler)
  125. {
  126. // compose content
  127. var container = new Container();
  128. container.Element(handler);
  129. ActualCommands = GenerateResult(PageSize, container);
  130. return this;
  131. }
  132. private static ICollection<PageDrawingCommand> GenerateResult(Size pageSize, Container container)
  133. {
  134. // inject dependencies
  135. var pageContext = new PageContext();
  136. pageContext.ResetPageNumber();
  137. var canvas = new PreviewerCanvas();
  138. container.InjectDependencies(pageContext, canvas);
  139. // distribute global state
  140. container.ApplyInheritedAndGlobalTexStyle(TextStyle.Default);
  141. container.ApplyContentDirection(ContentDirection.LeftToRight);
  142. container.ApplyDefaultImageConfiguration(DocumentSettings.Default.ImageRasterDpi, DocumentSettings.Default.ImageCompressionQuality, true);
  143. // render
  144. container.VisitChildren(x => (x as IStateResettable)?.ResetState());
  145. canvas.BeginDocument();
  146. var pageSizes = new List<Size>();
  147. while(true)
  148. {
  149. var spacePlan = container.Measure(pageSize);
  150. pageSizes.Add(spacePlan);
  151. if (spacePlan.Type == SpacePlanType.Wrap)
  152. {
  153. throw new Exception();
  154. }
  155. try
  156. {
  157. canvas.BeginPage(pageSize);
  158. container.Draw(pageSize);
  159. pageContext.IncrementPageNumber();
  160. }
  161. catch (Exception exception)
  162. {
  163. canvas.EndDocument();
  164. throw new Exception();
  165. }
  166. canvas.EndPage();
  167. if (spacePlan.Type == SpacePlanType.FullRender)
  168. break;
  169. }
  170. // extract results
  171. var mocks = container.ExtractElementsOfType<ElementMock>().Select(x => x.Value); // mock cannot contain another mock, flat structure
  172. return mocks
  173. .SelectMany(x => x.DrawingCommands)
  174. .GroupBy(x => x.PageNumber)
  175. .Select(x => new PageDrawingCommand
  176. {
  177. RequiredArea = pageSizes[x.Key - 1],
  178. Children = x
  179. .Select(y => new ChildDrawingCommand
  180. {
  181. ChildId = y.ElementId,
  182. Size = y.Size,
  183. Position = y.Position
  184. })
  185. .ToList()
  186. })
  187. .ToList();
  188. }
  189. public void ExpectWrap()
  190. {
  191. }
  192. public LayoutTest ExpectedDrawResult(Action<DocumentLayoutBuilder> handler)
  193. {
  194. var builder = new DocumentLayoutBuilder();
  195. handler(builder);
  196. ExpectedCommands = builder.Commands;
  197. return this;
  198. }
  199. public void CompareVisually()
  200. {
  201. VisualizeExpectedResult(PageSize, ActualCommands, ExpectedCommands);
  202. }
  203. public void Validate()
  204. {
  205. if (ActualCommands.Count != ExpectedCommands.Count)
  206. throw new Exception($"Generated {ActualCommands.Count} but expected {ExpectedCommands.Count} pages.");
  207. var numberOfPages = ActualCommands.Count;
  208. foreach (var i in Enumerable.Range(0, numberOfPages))
  209. {
  210. try
  211. {
  212. var actual = ActualCommands.ElementAt(i);
  213. var expected = ExpectedCommands.ElementAt(i);
  214. if (Math.Abs(actual.RequiredArea.Width - expected.RequiredArea.Width) > Size.Epsilon)
  215. throw new Exception($"Taken area width is equal to {actual.RequiredArea.Width} but expected {expected.RequiredArea.Width}");
  216. if (Math.Abs(actual.RequiredArea.Height - expected.RequiredArea.Height) > Size.Epsilon)
  217. throw new Exception($"Taken area height is equal to {actual.RequiredArea.Height} but expected {expected.RequiredArea.Height}");
  218. if (actual.Children.Count != expected.Children.Count)
  219. throw new Exception($"Visible {actual.Children.Count} but expected {expected.Children.Count}");
  220. foreach (var child in expected.Children)
  221. {
  222. var matchingActualElements = actual
  223. .Children
  224. .Where(x => x.ChildId == child.ChildId)
  225. .Where(x => Position.Equal(x.Position, child.Position))
  226. .Where(x => Size.Equal(x.Size, child.Size))
  227. .Count();
  228. if (matchingActualElements == 0)
  229. throw new Exception($"Cannot find actual drawing command for child {child.ChildId} on position {child.Position} and size {child.Size}");
  230. if (matchingActualElements > 1)
  231. throw new Exception($"Found multiple drawing commands for child {child.ChildId} on position {child.Position} and size {child.Size}");
  232. }
  233. // todo: add z-depth testing
  234. var actualOverlaps = GetOverlappingItems(actual.Children);
  235. var expectedOverlaps = GetOverlappingItems(expected.Children);
  236. foreach (var overlap in expectedOverlaps)
  237. {
  238. var matchingActualElements = actualOverlaps.Count(x => x.Item1 == overlap.Item1 && x.Item2 == overlap.Item2);
  239. if (matchingActualElements != 1)
  240. throw new Exception($"Element {overlap.Item1} should be visible underneath element {overlap.Item2}");
  241. }
  242. IEnumerable<(string, string)> GetOverlappingItems(ICollection<ChildDrawingCommand> items)
  243. {
  244. for (var i = 0; i < items.Count; i++)
  245. {
  246. for (var j = i; j < items.Count; j++)
  247. {
  248. var beforeChild = items.ElementAt(i);
  249. var afterChild = items.ElementAt(j);
  250. var beforeBoundingBox = BoundingBox.From(beforeChild.Position, beforeChild.Size);
  251. var afterBoundingBox = BoundingBox.From(afterChild.Position, afterChild.Size);
  252. var intersection = BoundingBoxExtensions.Intersection(beforeBoundingBox, afterBoundingBox);
  253. if (intersection == null)
  254. continue;
  255. yield return (beforeChild.ChildId, afterChild.ChildId);
  256. }
  257. }
  258. }
  259. }
  260. catch (Exception e)
  261. {
  262. throw new Exception($"Error on page {i + 1}: {e.Message}");
  263. }
  264. }
  265. }
  266. private static void VisualizeExpectedResult(Size pageSize, ICollection<PageDrawingCommand> left, ICollection<PageDrawingCommand> right)
  267. {
  268. var path = "test.pdf";
  269. if (File.Exists(path))
  270. File.Delete(path);
  271. // default colors
  272. var defaultColors = new string[]
  273. {
  274. Colors.Red.Medium,
  275. Colors.Green.Medium,
  276. Colors.Blue.Medium,
  277. Colors.Pink.Medium,
  278. Colors.Orange.Medium,
  279. Colors.Lime.Medium,
  280. Colors.Cyan.Medium,
  281. Colors.Indigo.Medium
  282. };
  283. // determine children colors
  284. var children = Enumerable
  285. .Concat(left, right)
  286. .SelectMany(x => x.Children)
  287. .Select(x => x.ChildId)
  288. .Distinct()
  289. .ToList();
  290. var colors = Enumerable
  291. .Range(0, children.Count)
  292. .ToDictionary(i => children[i], i => defaultColors[i]);
  293. // create new pdf document output
  294. var matrixHeight = Math.Max(left.Count, right.Count);
  295. const int pagePadding = 25;
  296. var imageInfo = new SKImageInfo((int)pageSize.Width * 2 + pagePadding * 4, (int)(pageSize.Height * matrixHeight + pagePadding * (matrixHeight + 2)));
  297. const int outputScale = 2;
  298. using var pdf = SKDocument.CreatePdf(path);
  299. using var canvas = pdf.BeginPage(imageInfo.Width * outputScale, imageInfo.Height * outputScale);
  300. canvas.Scale(outputScale, outputScale);
  301. // page background
  302. canvas.Clear(SKColor.Parse(DocumentColor));
  303. // chain titles
  304. // available area
  305. using var titlePaint = TextStyle.LibraryDefault.FontSize(16).Bold().ToPaint().Clone();
  306. titlePaint.TextAlign = SKTextAlign.Center;
  307. canvas.Save();
  308. canvas.Translate(pagePadding + pageSize.Width / 2f, pagePadding + titlePaint.TextSize / 2);
  309. canvas.DrawText("RESULT", 0, 0, titlePaint);
  310. canvas.Translate(pagePadding * 2 + pageSize.Width, 0);
  311. canvas.DrawText("EXPECTED", 0, 0, titlePaint);
  312. canvas.Restore();
  313. // side visualization
  314. canvas.Save();
  315. canvas.Translate(pagePadding, pagePadding * 2);
  316. DrawSide(left);
  317. canvas.Translate(pageSize.Width + pagePadding * 2, 0);
  318. DrawSide(right);
  319. canvas.Restore();
  320. // draw page numbers
  321. canvas.Save();
  322. canvas.Translate(pagePadding * 2 + pageSize.Width, pagePadding * 2 + titlePaint.TextSize);
  323. foreach (var i in Enumerable.Range(0, matrixHeight))
  324. {
  325. canvas.DrawText((i + 1).ToString(), 0, 0, titlePaint);
  326. canvas.Translate(0, pagePadding + pageSize.Height);
  327. }
  328. canvas.Restore();
  329. pdf.EndPage();
  330. pdf.Close();
  331. void DrawSide(ICollection<PageDrawingCommand> commands)
  332. {
  333. canvas.Save();
  334. foreach (var pageDrawingCommand in commands)
  335. {
  336. DrawPage(pageDrawingCommand);
  337. canvas.Translate(0, pageSize.Height + pagePadding);
  338. }
  339. canvas.Restore();
  340. }
  341. void DrawPageGrid(Size pageSize)
  342. {
  343. using var paint = new SKPaint
  344. {
  345. Color = SKColor.Parse(GridColor),
  346. StrokeWidth = 1
  347. };
  348. const float GridSize = 10f;
  349. foreach (var i in Enumerable.Range(1, (int)Math.Floor(pageSize.Width / GridSize)))
  350. {
  351. canvas.DrawLine(new SKPoint(i * GridSize, 0), new SKPoint(i * GridSize, pageSize.Height), paint);
  352. }
  353. foreach (var i in Enumerable.Range(1, (int)Math.Floor(pageSize.Height / GridSize)))
  354. {
  355. canvas.DrawLine(new SKPoint(0, i * GridSize), new SKPoint(pageSize.Width, i * GridSize), paint);
  356. }
  357. }
  358. void DrawPage(PageDrawingCommand command)
  359. {
  360. // available area
  361. using var availableAreaPaint = new SKPaint
  362. {
  363. Color = SKColor.Parse(PageColor)
  364. };
  365. canvas.DrawRect(0, 0, pageSize.Width, pageSize.Height, availableAreaPaint);
  366. // taken area
  367. using var takenAreaPaint = new SKPaint
  368. {
  369. Color = SKColor.Parse(TargetColor)
  370. };
  371. canvas.DrawRect(0, 0, command.RequiredArea.Width, command.RequiredArea.Height, takenAreaPaint);
  372. DrawPageGrid(pageSize);
  373. // draw children
  374. foreach (var child in command.Children)
  375. {
  376. canvas.Save();
  377. const float strokeWidth = 3f;
  378. var color = colors[child.ChildId];
  379. using var childBorderPaint = new SKPaint
  380. {
  381. Color = SKColor.Parse(color),
  382. IsStroke = true,
  383. StrokeWidth = strokeWidth
  384. };
  385. using var childAreaPaint = new SKPaint
  386. {
  387. Color = SKColor.Parse(color).WithAlpha(128)
  388. };
  389. canvas.Translate(child.Position.X, child.Position.Y);
  390. canvas.DrawRect(0, 0, child.Size.Width, child.Size.Height, childAreaPaint);
  391. canvas.DrawRect(strokeWidth / 2, strokeWidth / 2, child.Size.Width - strokeWidth, child.Size.Height - strokeWidth, childBorderPaint);
  392. canvas.Restore();
  393. }
  394. }
  395. // save
  396. GenerateExtensions.OpenFileUsingDefaultProgram(path);
  397. }
  398. }