Browse Source

Optimized drawing border

MarcinZiabek 4 years ago
parent
commit
d39819bf8d

+ 2 - 2
QuestPDF.UnitTests/BackgroundTests.cs

@@ -21,7 +21,7 @@ namespace QuestPDF.UnitTests
                     Color = Colors.Red.Medium
                 })
                 .DrawElement(new Size(400, 300))
-                .ExpectCanvasDrawRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
+                .ExpectCanvasDrawFilledRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
                 .CheckDrawResult();
         }
         
@@ -35,7 +35,7 @@ namespace QuestPDF.UnitTests
                     Child = x.CreateChild()
                 })
                 .DrawElement(new Size(400, 300))
-                .ExpectCanvasDrawRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
+                .ExpectCanvasDrawFilledRectangle(new Position(0, 0), new Size(400, 300), Colors.Red.Medium)
                 .ExpectChildDraw(new Size(400, 300))
                 .CheckDrawResult();
         }

+ 30 - 6
QuestPDF.UnitTests/BorderTests.cs

@@ -1,9 +1,11 @@
-using NUnit.Framework;
+using System.Drawing;
+using NUnit.Framework;
 using QuestPDF.Drawing;
 using QuestPDF.Elements;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.UnitTests.TestEngine;
+using Size = QuestPDF.Infrastructure.Size;
 
 namespace QuestPDF.UnitTests
 {
@@ -32,7 +34,29 @@ namespace QuestPDF.UnitTests
         }
         
         [Test]
-        public void Draw_HorizontalRight_VerticalTop()
+        public void Draw_SameWidths_Optimized()
+        {
+            TestPlan
+                .For(x => new Border
+                {
+                    Top = 10,
+                    Right = 10,
+                    Bottom = 10,
+                    Left = 10,
+                    
+                    Color = Colors.Red.Medium,
+                    
+                    Child = x.CreateChild()
+                })
+                .DrawElement(new Size(400, 300))
+                .ExpectChildDraw(new Size(400, 300))
+                .ExpectCanvasDrawStrokedRectangle(new Size(400, 300), Colors.Red.Medium, 10)
+                .ExpectChildDraw(new Size(400, 300))
+                .CheckDrawResult();
+        }
+        
+        [Test]
+        public void Draw_VariousWidths()
         {
             TestPlan
                 .For(x => new Border
@@ -48,10 +72,10 @@ namespace QuestPDF.UnitTests
                 })
                 .DrawElement(new Size(400, 300))
                 .ExpectChildDraw(new Size(400, 300))
-                .ExpectCanvasDrawRectangle(new Position(-20, -5), new Size(430, 10), Colors.Red.Medium) // top
-                .ExpectCanvasDrawRectangle(new Position(-20, -5), new Size(40, 320), Colors.Red.Medium) // left
-                .ExpectCanvasDrawRectangle(new Position(-20, 285), new Size(430, 30), Colors.Red.Medium) // bottom
-                .ExpectCanvasDrawRectangle(new Position(390, -5), new Size(20, 320), Colors.Red.Medium) // right
+                .ExpectCanvasDrawFilledRectangle(new Position(-20, -5), new Size(430, 10), Colors.Red.Medium) // top
+                .ExpectCanvasDrawFilledRectangle(new Position(-20, -5), new Size(40, 320), Colors.Red.Medium) // left
+                .ExpectCanvasDrawFilledRectangle(new Position(-20, 285), new Size(430, 30), Colors.Red.Medium) // bottom
+                .ExpectCanvasDrawFilledRectangle(new Position(390, -5), new Size(20, 320), Colors.Red.Medium) // right
                 .ExpectChildDraw(new Size(400, 300))
                 .CheckDrawResult();
         }

+ 4 - 2
QuestPDF.UnitTests/TestEngine/MockCanvas.cs

@@ -11,13 +11,15 @@ namespace QuestPDF.UnitTests.TestEngine
         public Action<float, float> ScaleFunc { get; set; }
         public Action<SKImage, Position, Size> DrawImageFunc { get; set; }
         public Action<string, Position, TextStyle> DrawTextFunc { get; set; }
-        public Action<Position, Size, string> DrawRectFunc { get; set; }
+        public Action<Position, Size, string> DrawFilledRectangleFunc { get; set; }
+        public Action<Size, string, float> DrawStrokedRectangleFunc { get; set; }
 
         public void Translate(Position vector) => TranslateFunc(vector);
         public void Rotate(float angle) => RotateFunc(angle);
         public void Scale(float scaleX, float scaleY) => ScaleFunc(scaleX, scaleY);
 
-        public void DrawRectangle(Position vector, Size size, string color) => DrawRectFunc(vector, size, color);
+        public void DrawFilledRectangle(Position vector, Size size, string color) => DrawFilledRectangleFunc(vector, size, color);
+        public void DrawStrokedRectangle(Size size, string color, float width) => DrawStrokedRectangleFunc(size, color, width);
         public void DrawText(string text, Position position, TextStyle style) => DrawTextFunc(text, position, style);
         public void DrawImage(SKImage image, Position position, Size size) => DrawImageFunc(image, position, size);
         

+ 2 - 1
QuestPDF.UnitTests/TestEngine/OperationRecordingCanvas.cs

@@ -14,7 +14,8 @@ namespace QuestPDF.UnitTests.TestEngine
         public void Rotate(float angle) => Operations.Add(new CanvasRotateOperation(angle));
         public void Scale(float scaleX, float scaleY) => Operations.Add(new CanvasScaleOperation(scaleX, scaleY));
 
-        public void DrawRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawRectangleOperation(vector, size, color));
+        public void DrawFilledRectangle(Position vector, Size size, string color) => Operations.Add(new CanvasDrawFilledRectangleOperation(vector, size, color));
+        public void DrawStrokedRectangle(Size size, string color, float width) => Operations.Add(new CanvasDrawStrokedRectangleOperation(size, color, width));
         public void DrawText(string text, Position position, TextStyle style) => Operations.Add(new CanvasDrawTextOperation(text, position, style));
         public void DrawImage(SKImage image, Position position, Size size) => Operations.Add(new CanvasDrawImageOperation(position, size));
         

+ 2 - 2
QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawRectangleOperation.cs → QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawFilledRectangleOperation.cs

@@ -2,13 +2,13 @@
 
 namespace QuestPDF.UnitTests.TestEngine.Operations
 {
-    internal class CanvasDrawRectangleOperation : OperationBase
+    internal class CanvasDrawFilledRectangleOperation : OperationBase
     {
         public Position Position { get; } 
         public Size Size { get; }
         public string Color { get; }
 
-        public CanvasDrawRectangleOperation(Position position, Size size, string color)
+        public CanvasDrawFilledRectangleOperation(Position position, Size size, string color)
         {
             Position = position;
             Size = size;

+ 18 - 0
QuestPDF.UnitTests/TestEngine/Operations/CanvasDrawStrokedRectangleOperation.cs

@@ -0,0 +1,18 @@
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.UnitTests.TestEngine.Operations
+{
+    internal class CanvasDrawStrokedRectangleOperation : OperationBase
+    {
+        public Size Size { get; }
+        public string Color { get; }
+        public float Width { get; set; }
+        
+        public CanvasDrawStrokedRectangleOperation(Size size, string color, float width)
+        {
+            Size = size;
+            Color = color;
+            Width = width;
+        }
+    }
+}

+ 24 - 9
QuestPDF.UnitTests/TestEngine/TestPlan.cs

@@ -70,17 +70,27 @@ namespace QuestPDF.UnitTests.TestEngine
                     Assert.AreEqual(expected.ScaleX, scaleX, "Scale X");
                     Assert.AreEqual(expected.ScaleY, scaleY, "Scale Y");
                 },
-                DrawRectFunc = (position, size, color) =>
+                DrawFilledRectangleFunc = (position, size, color) =>
                 {
-                    var expected = GetExpected<CanvasDrawRectangleOperation>();
+                    var expected = GetExpected<CanvasDrawFilledRectangleOperation>();
                     
-                    Assert.AreEqual(expected.Position.X, position.X, "Draw rectangle: X");
-                    Assert.AreEqual(expected.Position.Y, position.Y, "Draw rectangle: Y");
+                    Assert.AreEqual(expected.Position.X, position.X, "Draw filled rectangle: X");
+                    Assert.AreEqual(expected.Position.Y, position.Y, "Draw filled rectangle: Y");
                     
-                    Assert.AreEqual(expected.Size.Width, size.Width, "Draw rectangle: width");
-                    Assert.AreEqual(expected.Size.Height, size.Height, "Draw rectangle: height");
+                    Assert.AreEqual(expected.Size.Width, size.Width, "Draw filled rectangle: width");
+                    Assert.AreEqual(expected.Size.Height, size.Height, "Draw filled rectangle: height");
                     
-                    Assert.AreEqual(expected.Color, color, "Draw rectangle: color");
+                    Assert.AreEqual(expected.Color, color, "Draw filled rectangle: color");
+                },
+                DrawStrokedRectangleFunc = (size, color, width) =>
+                {
+                    var expected = GetExpected<CanvasDrawStrokedRectangleOperation>();
+
+                    Assert.AreEqual(expected.Size.Width, size.Width, "Draw stroked rectangle: width");
+                    Assert.AreEqual(expected.Size.Height, size.Height, "Draw stroked rectangle: height");
+                    
+                    Assert.AreEqual(expected.Color, color, "Draw stroked rectangle: color");
+                    Assert.AreEqual(expected.Width, width, "Draw stroked rectangle: width");
                 },
                 DrawTextFunc = (text, position, style) => 
                 {
@@ -196,9 +206,14 @@ namespace QuestPDF.UnitTests.TestEngine
             return AddOperation(new CanvasRotateOperation(angle));
         }
         
-        public TestPlan ExpectCanvasDrawRectangle(Position position, Size size, string color)
+        public TestPlan ExpectCanvasDrawFilledRectangle(Position position, Size size, string color)
+        {
+            return AddOperation(new CanvasDrawFilledRectangleOperation(position, size, color));
+        }
+        
+        public TestPlan ExpectCanvasDrawStrokedRectangle(Size size, string color, float width)
         {
-            return AddOperation(new CanvasDrawRectangleOperation(position, size, color));
+            return AddOperation(new CanvasDrawStrokedRectangleOperation(size, color, width));
         }
         
         public TestPlan ExpectCanvasDrawText(string text, Position position, TextStyle style)

+ 6 - 1
QuestPDF/Drawing/FreeCanvas.cs

@@ -36,7 +36,12 @@ namespace QuestPDF.Drawing
             
         }
 
-        public void DrawRectangle(Position vector, Size size, string color)
+        public void DrawFilledRectangle(Position vector, Size size, string color)
+        {
+            
+        }
+        
+        public void DrawStrokedRectangle(Size size, string color, float width)
         {
             
         }

+ 41 - 0
QuestPDF/Drawing/SkiaCache.cs

@@ -0,0 +1,41 @@
+using System.Collections.Concurrent;
+using SkiaSharp;
+
+namespace QuestPDF.Drawing
+{
+    internal class SkiaCache
+    {
+        private static ConcurrentDictionary<string, SKPaint> RectangleFillPaints = new ConcurrentDictionary<string, SKPaint>();
+        private static ConcurrentDictionary<string, SKPaint> RectangleStrokePaints = new ConcurrentDictionary<string, SKPaint>();
+
+        public static SKPaint GetRectangleFillPaint(string color)
+        {
+            return RectangleFillPaints.GetOrAdd(color, _ => Convert());
+
+            SKPaint Convert()
+            {
+                return new SKPaint
+                {
+                    Color = SKColor.Parse(color),
+                    Style = SKPaintStyle.Fill
+                };
+            }
+        }
+        
+        public static SKPaint GetRectangleStrokePaint(string color, float width)
+        {
+            var key = $"{color}|{width}";
+            return RectangleStrokePaints.GetOrAdd(key, _ => Convert());
+
+            SKPaint Convert()
+            {
+                return new SKPaint
+                {
+                    Color = SKColor.Parse(color),
+                    Style = SKPaintStyle.Stroke,
+                    StrokeWidth = width
+                };
+            }
+        }
+    }
+}

+ 8 - 2
QuestPDF/Drawing/SkiaCanvasBase.cs

@@ -18,14 +18,20 @@ namespace QuestPDF.Drawing
             Canvas.Translate(vector.X, vector.Y);
         }
 
-        public void DrawRectangle(Position vector, Size size, string color)
+        public void DrawFilledRectangle(Position vector, Size size, string color)
         {
             if (size.Width < Size.Epsilon || size.Height < Size.Epsilon)
                 return;
 
-            var paint = color.ColorToPaint();
+            var paint = SkiaCache.GetRectangleFillPaint(color);
             Canvas.DrawRect(vector.X, vector.Y, size.Width, size.Height, paint);
         }
+        
+        public void DrawStrokedRectangle(Size size, string color, float width)
+        {
+            var paint = SkiaCache.GetRectangleStrokePaint(color, width);
+            Canvas.DrawRect(0, 0, size.Width, size.Height, paint);
+        }
 
         public void DrawText(string text, Position vector, TextStyle style)
         {

+ 1 - 1
QuestPDF/Elements/Background.cs

@@ -9,7 +9,7 @@ namespace QuestPDF.Elements
         
         internal override void Draw(Size availableSpace)
         {
-            Canvas.DrawRectangle(Position.Zero, availableSpace, Color);
+            Canvas.DrawFilledRectangle(Position.Zero, availableSpace, Color);
             base.Draw(availableSpace);
         }
     }

+ 38 - 18
QuestPDF/Elements/Border.cs

@@ -1,3 +1,4 @@
+using System;
 using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 
@@ -15,31 +16,50 @@ namespace QuestPDF.Elements
         internal override void Draw(Size availableSpace)
         {
             base.Draw(availableSpace);
-            
-            Canvas.DrawRectangle(
-                new Position(-Left/2, -Top/2), 
-                new Size(availableSpace.Width + Left/2 + Right/2, Top), 
+
+            if (HasEqualWidthOnAllSides())
+            {
+                Canvas.DrawStrokedRectangle(availableSpace, Color, Top);
+            }
+            else
+            {
+                DrawCustomRectangle(availableSpace);
+            }
+        }
+
+        private void DrawCustomRectangle(Size size)
+        {
+            Canvas.DrawFilledRectangle(
+                new Position(-Left / 2, -Top / 2),
+                new Size(size.Width + Left / 2 + Right / 2, Top),
                 Color);
-            
-            Canvas.DrawRectangle(
-                new Position(-Left/2, -Top/2), 
-                new Size(Left, availableSpace.Height + Top/2 + Bottom/2), 
+
+            Canvas.DrawFilledRectangle(
+                new Position(-Left / 2, -Top / 2),
+                new Size(Left, size.Height + Top / 2 + Bottom / 2),
                 Color);
-            
-            Canvas.DrawRectangle(
-                new Position(-Left/2, availableSpace.Height-Bottom/2), 
-                new Size(availableSpace.Width + Left/2 + Right/2, Bottom), 
+
+            Canvas.DrawFilledRectangle(
+                new Position(-Left / 2, size.Height - Bottom / 2),
+                new Size(size.Width + Left / 2 + Right / 2, Bottom),
                 Color);
-            
-            Canvas.DrawRectangle(
-                new Position(availableSpace.Width-Right/2, -Top/2), 
-                new Size(Right, availableSpace.Height + Top/2 + Bottom/2), 
+
+            Canvas.DrawFilledRectangle(
+                new Position(size.Width - Right / 2, -Top / 2),
+                new Size(Right, size.Height + Top / 2 + Bottom / 2),
                 Color);
         }
+        
+        private bool HasEqualWidthOnAllSides()
+        {
+            return IsClose(Top, Bottom) && 
+                   IsClose(Left, Right) &&
+                   IsClose(Top, Left);
+        }
 
-        public override string ToString()
+        private static bool IsClose(float x, float y)
         {
-            return $"Border: Top({Top}) Right({Right}) Bottom({Bottom}) Left({Left}) Color({Color})";
+            return Math.Abs(x - y) < Size.Epsilon;
         }
     }
 }

+ 2 - 2
QuestPDF/Elements/Text/Items/TextBlockSpan.cs

@@ -111,7 +111,7 @@ namespace QuestPDF.Elements.Text.Items
 
             var text = Text.Substring(request.StartIndex, request.EndIndex - request.StartIndex);
             
-            request.Canvas.DrawRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
+            request.Canvas.DrawFilledRectangle(new Position(0, request.TotalAscent), new Size(request.TextSize.Width, request.TextSize.Height), Style.BackgroundColor);
             request.Canvas.DrawText(text, Position.Zero, Style);
 
             // draw underline
@@ -124,7 +124,7 @@ namespace QuestPDF.Elements.Text.Items
 
             void DrawLine(float offset, float thickness)
             {
-                request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
+                request.Canvas.DrawFilledRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
             }
         }
     }

+ 2 - 1
QuestPDF/Infrastructure/ICanvas.cs

@@ -6,7 +6,8 @@ namespace QuestPDF.Infrastructure
     {
         void Translate(Position vector);
         
-        void DrawRectangle(Position vector, Size size, string color);
+        void DrawFilledRectangle(Position vector, Size size, string color);
+        void DrawStrokedRectangle(Size size, string color, float width);
         void DrawText(string text, Position position, TextStyle style);
         void DrawImage(SKImage image, Position position, Size size);