LayoutTestOutputVisualization.cs 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. using QuestPDF.Drawing;
  2. using QuestPDF.Fluent;
  3. using QuestPDF.Helpers;
  4. using QuestPDF.Infrastructure;
  5. using SkiaSharp;
  6. namespace QuestPDF.LayoutTests.TestEngine;
  7. internal static class LayoutTestResultVisualization
  8. {
  9. // visual settings
  10. private const string DocumentBackgroundColor = Colors.Grey.Lighten1;
  11. private const string PageBackgroundColor = Colors.Grey.Lighten3;
  12. private const string RequiredAreaBackgroundColor = Colors.White;
  13. private const string GridLineColor = Colors.Grey.Lighten1;
  14. private static readonly string[] DefaultElementColors =
  15. {
  16. Colors.Green.Darken2,
  17. Colors.Blue.Darken2,
  18. Colors.Orange.Darken2,
  19. Colors.Lime.Darken2,
  20. Colors.Cyan.Darken2,
  21. Colors.Indigo.Darken2,
  22. Colors.Green.Lighten1,
  23. Colors.Blue.Lighten1,
  24. Colors.Orange.Lighten1,
  25. Colors.Lime.Lighten1,
  26. Colors.Cyan.Lighten1,
  27. Colors.Indigo.Lighten1
  28. };
  29. private const int Padding = 32;
  30. private const int OutputImageScale = 2;
  31. private const float GridSize = 10;
  32. private const float MockBorderThickness = 2;
  33. private const byte MockBackgroundOpacity = 128;
  34. // implementations
  35. public static void Visualize(LayoutTestResult result, Stream stream)
  36. {
  37. // determine output dimenstions
  38. var numberOfPages = Math.Max(result.ActualLayout.Pages.Count, result.ExpectedLayout.Pages.Count);
  39. var canvasWidth = result.PageSize.Width * 2 + Padding * 4;
  40. var canvasHeight = result.PageSize.Height * numberOfPages + Padding * (numberOfPages + 2);
  41. // create PDF
  42. using var pdf = SKDocument.CreatePdf(stream);
  43. using var canvas = pdf.BeginPage(canvasWidth * OutputImageScale, canvasHeight * OutputImageScale);
  44. canvas.Scale(OutputImageScale, OutputImageScale);
  45. canvas.Clear(SKColor.Parse(DocumentBackgroundColor));
  46. // draw content
  47. var mockColors = AssignColorsToMocks();
  48. DrawDocument();
  49. // finish generation
  50. pdf.EndPage();
  51. pdf.Close();
  52. IDictionary<string, string> AssignColorsToMocks()
  53. {
  54. var mocks = Enumerable
  55. .Concat(result.ActualLayout.Pages, result.ExpectedLayout.Pages)
  56. .SelectMany(x => x.Mocks)
  57. .Select(x => x.MockId)
  58. .Distinct()
  59. .ToList();
  60. return Enumerable
  61. .Range(0, mocks.Count)
  62. .ToDictionary(i => mocks[i], i => DefaultElementColors[i]);
  63. }
  64. void DrawDocument()
  65. {
  66. canvas.Translate(Padding, Padding);
  67. // draw title
  68. using var textPaint = TextStyle.LibraryDefault.FontSize(16).Bold().ToPaint().Clone();
  69. textPaint.TextAlign = SKTextAlign.Center;
  70. var actualHeaderPosition = new SKPoint(result.PageSize.Width / 2, textPaint.TextSize / 2);
  71. canvas.DrawText("ACTUAL", actualHeaderPosition, textPaint);
  72. var expectedHeaderPosition = new SKPoint(Padding * 2 + result.PageSize.Width * 1.5f, textPaint.TextSize / 2);
  73. canvas.DrawText("EXPECTED", expectedHeaderPosition, textPaint);
  74. // draw pages
  75. canvas.Save();
  76. canvas.Translate(0, Padding);
  77. foreach (var pageIndex in Enumerable.Range(0, numberOfPages))
  78. {
  79. var actualPage = result.ActualLayout.Pages.ElementAtOrDefault(pageIndex);
  80. var expectedPage = result.ExpectedLayout.Pages.ElementAtOrDefault(pageIndex);
  81. DrawPage(actualPage);
  82. DrawLayoutDifferences(actualPage, expectedPage);
  83. canvas.Translate(result.PageSize.Width + Padding, 0);
  84. canvas.DrawText((pageIndex + 1).ToString(), 0, textPaint.TextSize, textPaint);
  85. canvas.Translate(Padding, 0);
  86. DrawPage(expectedPage);
  87. DrawLayoutDifferences(expectedPage, actualPage);
  88. canvas.Translate(-result.PageSize.Width - Padding * 2, result.PageSize.Height + Padding);
  89. }
  90. canvas.Restore();
  91. }
  92. void DrawPage(LayoutTestResult.PageLayout? pageLayout)
  93. {
  94. // draw page
  95. using var availableAreaPaint = new SKPaint
  96. {
  97. Color = SKColor.Parse(PageBackgroundColor)
  98. };
  99. using var requiredAreaPaint = new SKPaint
  100. {
  101. Color = SKColor.Parse(RequiredAreaBackgroundColor)
  102. };
  103. canvas.DrawRect(0, 0, result.PageSize.Width, result.PageSize.Height, availableAreaPaint);
  104. canvas.DrawRect(0, 0, pageLayout?.RequiredArea.Width ?? 0, pageLayout?.RequiredArea.Height ?? 0, requiredAreaPaint);
  105. DrawGridLines();
  106. if (pageLayout == null)
  107. return;
  108. // draw mocks
  109. foreach (var mock in pageLayout.Mocks)
  110. {
  111. canvas.Save();
  112. var color = mockColors[mock.MockId];
  113. using var mockBorderPaint = new SKPaint
  114. {
  115. Color = SKColor.Parse(color),
  116. IsStroke = true,
  117. StrokeWidth = MockBorderThickness
  118. };
  119. using var mockAreaPaint = new SKPaint
  120. {
  121. Color = SKColor.Parse(color).WithAlpha(MockBackgroundOpacity)
  122. };
  123. canvas.Translate(mock.Position.X, mock.Position.Y);
  124. canvas.DrawRect(0, 0, mock.Size.Width, mock.Size.Height, mockAreaPaint);
  125. canvas.DrawRect(MockBorderThickness / 2, MockBorderThickness / 2, mock.Size.Width - MockBorderThickness, mock.Size.Height - MockBorderThickness, mockBorderPaint);
  126. canvas.Restore();
  127. }
  128. }
  129. void DrawGridLines()
  130. {
  131. using var paint = new SKPaint
  132. {
  133. Color = SKColor.Parse(GridLineColor),
  134. StrokeWidth = 1
  135. };
  136. var verticalLineCount = (int)Math.Floor(result.PageSize.Width / GridSize);
  137. var horizontalLineCount = (int)Math.Floor(result.PageSize.Height / GridSize);
  138. foreach (var i in Enumerable.Range(1, verticalLineCount))
  139. canvas.DrawLine(new SKPoint(i * GridSize, 0), new SKPoint(i * GridSize, result.PageSize.Height), paint);
  140. foreach (var i in Enumerable.Range(1, horizontalLineCount))
  141. canvas.DrawLine(new SKPoint(0, i * GridSize), new SKPoint(result.PageSize.Width, i * GridSize), paint);
  142. }
  143. void DrawLayoutDifferences(LayoutTestResult.PageLayout? target, LayoutTestResult.PageLayout? compareWith)
  144. {
  145. using var targetPath = BuildPathFromLayout(target);
  146. using var compareWithPath = BuildPathFromLayout(compareWith);
  147. using var differencePath = targetPath.Op(compareWithPath, SKPathOp.Difference);
  148. AnnotateInvalidAreaHelper.Annotate(canvas, differencePath);
  149. SKPath BuildPathFromLayout(LayoutTestResult.PageLayout? layout)
  150. {
  151. var resultPath = new SKPath();
  152. if (layout == null)
  153. return resultPath;
  154. foreach (var mock in layout.Mocks)
  155. {
  156. var position = new SKRect(
  157. mock.Position.X,
  158. mock.Position.Y,
  159. mock.Position.X + mock.Size.Width,
  160. mock.Position.Y + mock.Size.Height);
  161. resultPath.AddRect(position);
  162. }
  163. return resultPath;
  164. }
  165. }
  166. }
  167. }