Browse Source

Initial text rendering improvements

Marcin Ziąbek 4 years ago
parent
commit
edc595d199

+ 37 - 13
QuestPDF.Examples/ElementExamples.cs

@@ -502,25 +502,49 @@ namespace QuestPDF.Examples
         {
             RenderingTest
                 .Create()
-                .PageSize(400, 250)
+                .PageSize(300, 175)
                 .FileName()
                 .Render(container =>
                 {
                     container
-                        .Padding(25)
-                        .Stack(stack =>
+                        .Background(Colors.White)
+                        .Padding(10)
+                        .Decoration(decoration =>
                         {
-                            var scales = new[] { 0.75f, 1f, 1.25f, 1.5f };
+                            var headerFontStyle = TextStyle
+                                .Default
+                                .Size(20)
+                                .Color(Colors.Blue.Darken2)
+                                .SemiBold();
+    
+                            decoration
+                                .Header()
+                                .PaddingBottom(10)
+                                .Text("Example: scale component", headerFontStyle);
+    
+                            decoration
+                                .Content()
+                                .Stack(stack =>
+                                {
+                                    var scales = new[] { 0.8f, 0.9f, 1.1f, 1.2f };
 
-                            foreach (var scale in scales)
-                            {
-                                stack
-                                    .Item()
-                                    .Border(1)
-                                    .Scale(scale)
-                                    .Padding(10)
-                                    .Text($"Content with {scale} scale.", TextStyle.Default.Size(20));
-                            }
+                                    foreach (var scale in scales)
+                                    {
+                                        var fontColor = scale <= 1f
+                                            ? Colors.Red.Lighten4
+                                            : Colors.Green.Lighten4;
+
+                                        var fontStyle = TextStyle.Default.Size(16);
+                
+                                        stack
+                                            .Item()
+                                            .Border(1)
+                                            .Background(fontColor)
+                                            .Scale(scale)
+                                            .Padding(5)
+                                            .Text($"Content with {scale} scale.", fontStyle);
+                                    }
+                                });
                         });
                 });
         }

+ 32 - 4
QuestPDF.Examples/Engine/RenderingTest.cs

@@ -9,10 +9,17 @@ using QuestPDF.Infrastructure;
 
 namespace QuestPDF.Examples.Engine
 {
+    public enum RenderingTestResult
+    {
+        Pdf,
+        Images
+    }
+    
     public class RenderingTest
     {
         private string FileNamePrefix = "test";
         private Size Size { get; set; }
+        private RenderingTestResult ResultType { get; set; } = RenderingTestResult.Images;
         
         private RenderingTest()
         {
@@ -35,18 +42,39 @@ namespace QuestPDF.Examples.Engine
             Size = new Size(width, height);
             return this;
         }
+
+        public RenderingTest ProducePdf()
+        {
+            ResultType = RenderingTestResult.Pdf;
+            return this;
+        }
+        
+        public RenderingTest ProduceImages()
+        {
+            ResultType = RenderingTestResult.Images;
+            return this;
+        }
         
         public void Render(Action<IContainer> content)
         {
             var container = new Container();
             content(container);
             
-            Func<int, string> fileNameSchema = i => $"{FileNamePrefix}-${i}.png";
-
             var document = new SimpleDocument(container, Size);
-            document.GenerateImages(fileNameSchema);
 
-            Process.Start("explorer", fileNameSchema(0));
+            if (ResultType == RenderingTestResult.Images)
+            {
+                Func<int, string> fileNameSchema = i => $"{FileNamePrefix}-${i}.png";
+                document.GenerateImages(fileNameSchema);
+                Process.Start("explorer", fileNameSchema(0));
+            }
+
+            if (ResultType == RenderingTestResult.Pdf)
+            {
+                var fileName = $"{FileNamePrefix}.pdf";
+                document.GeneratePdf(fileName);
+                Process.Start("explorer", fileName);
+            }
         }
     }
 }

+ 33 - 0
QuestPDF.Examples/TextExamples.cs

@@ -0,0 +1,33 @@
+using NUnit.Framework;
+using QuestPDF.Examples.Engine;
+using QuestPDF.Fluent;
+using QuestPDF.Helpers;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Examples
+{
+    public class TextExamples
+    {
+        [Test]
+        public void TextElements()
+        {
+            RenderingTest
+                .Create()
+                .PageSize(600, 400)
+                .FileName()
+                .ProducePdf()
+                .Render(container =>
+                {
+                    container.Padding(20).Text(text =>
+                    {
+                        text.Span("Let's start with something bold...", TextStyle.Default.SemiBold().Size(18));
+                        text.Span("And BIG...", TextStyle.Default.Size(28).Color(Colors.DeepOrange.Darken2).BackgroundColor(Colors.Yellow.Lighten3).Underlined());
+                        text.Span(Placeholders.LoremIpsum(), TextStyle.Default.Size(16));
+                        //text.Element().ExternalLink("https://www.questpdf.com/").Width(200).Height(50).Text("Visit questpdf.com", TextStyle.Default.Underlined().Color(Colors.Blue.Darken2));
+                        text.Span(Placeholders.LoremIpsum(), TextStyle.Default.Size(16).Stroked());
+                        text.Span("And now it's time for some colors 12345 678 90293 03490 83290.", TextStyle.Default.Size(20).Color(Colors.Green.Medium));
+                    });
+                });
+        }
+    }
+}

+ 15 - 0
QuestPDF/Drawing/SpacePlan/TextRender.cs

@@ -0,0 +1,15 @@
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Drawing.SpacePlan
+{
+    internal class TextRender : FullRender
+    {
+        public float Ascent { get; set; }
+        public float Descent { get; set; }
+        
+        public TextRender(float width, float height) : base(width, height)
+        {
+            
+        }
+    }
+}

+ 3 - 4
QuestPDF/Elements/Placeholder.cs

@@ -18,16 +18,15 @@ namespace QuestPDF.Elements
         {
             container
                 .Background(Colors.Grey.Lighten2)
+                .Padding(5)
                 .AlignMiddle()
                 .AlignCenter()
-                .Padding(5)
-                .MaxHeight(32)
                 .Element(x =>
                 {
                     if (string.IsNullOrWhiteSpace(Text))
-                        x.Image(ImageData, ImageScaling.FitArea);
+                        x.MaxHeight(32).Image(ImageData, ImageScaling.FitArea);
                     else
-                        x.Text(Text, TextStyle.Default.Size(14).SemiBold());
+                        x.Text(Text, TextStyle.Default.Size(14));
                 });
         }
     }

+ 155 - 0
QuestPDF/Elements/TextBlock.cs

@@ -0,0 +1,155 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements
+{
+    internal class TextBlock : Element, IStateResettable
+    {
+        public List<Element> Children { get; set; } = new List<Element>();
+        public Queue<Element> ChildrenQueue { get; set; } = new Queue<Element>();
+        
+        public void ResetState()
+        {
+            ChildrenQueue = new Queue<Element>(Children);
+        }
+        
+        internal override void HandleVisitor(Action<Element?> visit)
+        {
+            Children.ForEach(x => x?.HandleVisitor(visit));
+            base.HandleVisitor(visit);
+        }
+        
+        internal override ISpacePlan Measure(Size availableSpace)
+        {
+            return new FullRender(availableSpace);
+            
+            if (!ChildrenQueue.Any())
+                return new FullRender(Size.Zero);
+
+            if (Children.Count < 50)
+                return new FullRender(Size.Zero);
+            
+            var items = SelectItemsForCurrentLine(availableSpace);
+
+            if (items == null)
+                return new Wrap();
+
+            var totalWidth = items.Sum(x => x.Measurement.Width);
+            var totalHeight = items.Max(x => x.Measurement.Height);
+            
+            return new PartialRender(totalWidth, totalHeight);
+            
+            return new FullRender(Size.Zero);
+            return CreateParent(availableSpace).Measure(availableSpace);
+        }
+
+        internal override void Draw(Size availableSpace)
+        {
+            while (true)
+            {
+                if (!ChildrenQueue.Any())
+                    return;
+                
+                var items = SelectItemsForCurrentLine(availableSpace);
+            
+                if (items == null)
+                    return;
+
+                var totalWidth = items.Sum(x => x.Measurement.Width);
+                var totalHeight = items.Max(x => x.Measurement.Height);
+                
+                var spaceBetween = (availableSpace.Width - totalWidth) / (items.Count - 1);
+
+                var offset = items
+                    .Select(x => x.Measurement)
+                    .Cast<TextRender>()
+                    .Where(x => x != null)
+                    .Select(x => x.Ascent)
+                    .Select(Math.Abs)
+                    .Max();
+                
+                Canvas.Translate(new Position(0, offset));
+                
+                foreach (var item in items)
+                {
+                    item.Element.Draw(availableSpace);
+                    Canvas.Translate(new Position(item.Measurement.Width + spaceBetween, 0));
+                }
+            
+                Canvas.Translate(new Position(-availableSpace.Width - spaceBetween, totalHeight - offset));
+            
+                items.ForEach(x => ChildrenQueue.Dequeue());
+            }
+        }
+
+        Container CreateParent(Size availableSpace)
+        {
+            var children = Children
+                .Select(x => new
+                {
+                    Element = x,
+                    Measurement = x.Measure(Size.Max) as Size
+                })
+                .Select(x => new GridElement()
+                {
+                    Child = x.Element,
+                    Columns = (int)x.Measurement.Width + 1
+                })
+                .ToList();
+            
+            var grid = new Grid()
+            {
+                Alignment = HorizontalAlignment.Left,
+                ColumnsCount = (int)availableSpace.Width,
+                Children = children
+            };
+
+            var container = new Container();
+            grid.Compose(container);
+            container.HandleVisitor(x => x.Initialize(PageContext, Canvas));
+
+            return container;
+        }
+
+        private List<MeasuredElement>? SelectItemsForCurrentLine(Size availableSpace)
+        {
+            var totalWidth = 0f;
+
+            var items = ChildrenQueue
+                .Select(x => new MeasuredElement
+                {
+                    Element = x,
+                    Measurement = x.Measure(Size.Max) as FullRender
+                })
+                .TakeWhile(x =>
+                {
+                    if (x.Measurement == null)
+                        return false;
+                    
+                    if (totalWidth + x.Measurement.Width > availableSpace.Width)
+                        return false;
+
+                    totalWidth += x.Measurement.Width;
+                    return true;
+                })
+                .ToList();
+
+            if (items.Any(x => x.Measurement == null))
+                return null;
+                
+            if (items.Max(x => x.Measurement.Height) > availableSpace.Height)
+                return null;
+
+            return items;
+        }
+
+        private class MeasuredElement
+        {
+            public Element Element { get; set; }
+            public FullRender? Measurement { get; set; }
+        }
+    }
+}

+ 58 - 0
QuestPDF/Elements/TextItem.cs

@@ -0,0 +1,58 @@
+using System;
+using QuestPDF.Drawing;
+using QuestPDF.Drawing.SpacePlan;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Elements
+{
+    internal class TextItem : Element
+    {
+        public string Value { get; set; }
+        public TextStyle Style { get; set; } = new TextStyle();
+        
+        internal override ISpacePlan Measure(Size availableSpace)
+        {
+            var paint = Style.ToPaint();
+            var metrics = paint.FontMetrics;
+
+            var width = paint.MeasureText(Value);
+            var height = Math.Abs(metrics.Descent) + Math.Abs(metrics.Ascent);
+
+            if (availableSpace.Width < width || availableSpace.Height < height)
+                return new Wrap();
+            
+            return new TextRender(width, height)
+            {
+                Descent = metrics.Descent,
+                Ascent = metrics.Ascent
+            };
+        }
+
+        internal override void Draw(Size availableSpace)
+        {
+            var paint = Style.ToPaint();
+            var metrics = paint.FontMetrics;
+            
+            var size = Measure(availableSpace) as Size;
+            
+            if (size == null)
+                return;
+
+            Canvas.DrawRectangle(new Position(0, metrics.Ascent), new Size(size.Width, size.Height), Style.BackgroundColor);
+            Canvas.DrawText(Value, Position.Zero, Style);
+
+            // draw underline
+            if (Style.IsUnderlined && metrics.UnderlinePosition.HasValue)
+                DrawLine(metrics.UnderlinePosition.Value, metrics.UnderlineThickness.Value);
+            
+            // draw stroke
+            if (Style.IsStroked && metrics.StrikeoutPosition.HasValue)
+                DrawLine(metrics.StrikeoutPosition.Value, metrics.StrikeoutThickness.Value);
+
+            void DrawLine(float offset, float thickness)
+            {
+                Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(size.Width, thickness), Style.Color);
+            }
+        }
+    }
+}

+ 0 - 18
QuestPDF/Fluent/ElementExtensions.cs

@@ -92,25 +92,7 @@ namespace QuestPDF.Fluent
                 MinHeight = minHeight
             });
         }
-        
-        public static void Text(this IContainer element, object text, TextStyle? style = null)
-        {
-            text ??= string.Empty;
-            style ??= TextStyle.Default;
 
-            if (element is Alignment alignment)
-            {
-                style = style.Clone();
-                style.Alignment = alignment.Horizontal;
-            }
-            
-            element.Element(new Text
-            {
-                Value = text.ToString(),
-                Style = style
-            });
-        }
-        
         public static void PageBreak(this IContainer element)
         {
             element.Element(new PageBreak());

+ 57 - 0
QuestPDF/Fluent/TextExtensions.cs

@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using QuestPDF.Elements;
+using QuestPDF.Infrastructure;
+
+namespace QuestPDF.Fluent
+{
+    public class TextDescriptor
+    {
+        internal ICollection<Element> Elements = new List<Element>();
+        
+        internal TextDescriptor()
+        {
+            
+        }
+        
+        public void Span(string text, TextStyle? style = null)
+        {
+            text.Split(' ')
+                .Select(x => $"{x} ")
+                .Select(x => new TextItem
+                {
+                    Value = x,
+                    Style = style ?? TextStyle.Default
+                })
+                .ToList()
+                .ForEach(Elements.Add);
+        }
+
+        public IContainer Element()
+        {
+            var container = new Container();
+            Elements.Add(container);
+            return container;
+        }
+    }
+    
+    public static class TextExtensions
+    {
+        public static void Text(this IContainer element, Action<TextDescriptor> content)
+        {
+            var descriptor = new TextDescriptor();
+            content?.Invoke(descriptor);
+
+            element.Element(new TextBlock()
+            {
+                Children = descriptor.Elements.ToList()
+            });
+        }
+        
+        public static void Text(this IContainer element, object text, TextStyle? style = null)
+        {
+            element.Text(x => x.Span(text.ToString(), style));
+        }
+    }
+}

+ 16 - 1
QuestPDF/Fluent/TextStyleExtensions.cs

@@ -18,6 +18,11 @@ namespace QuestPDF.Fluent
             return style.Mutate(x => x.Color = value);
         }
         
+        public static TextStyle BackgroundColor(this TextStyle style, string value)
+        {
+            return style.Mutate(x => x.BackgroundColor = value);
+        }
+        
         public static TextStyle FontType(this TextStyle style, string value)
         {
             return style.Mutate(x => x.FontType = value);
@@ -37,7 +42,17 @@ namespace QuestPDF.Fluent
         {
             return style.Mutate(x => x.IsItalic = value);
         }
-
+        
+        public static TextStyle Stroked(this TextStyle style, bool value = true)
+        {
+            return style.Mutate(x => x.IsStroked = value);
+        }
+        
+        public static TextStyle Underlined(this TextStyle style, bool value = true)
+        {
+            return style.Mutate(x => x.IsUnderlined = value);
+        }
+        
         #region Alignmnet
         
         public static TextStyle Alignment(this TextStyle style, HorizontalAlignment value)

+ 4 - 1
QuestPDF/Infrastructure/TextStyle.cs

@@ -5,18 +5,21 @@ namespace QuestPDF.Infrastructure
     public class TextStyle
     {
         internal string Color { get; set; } = Colors.Black;
+        internal string BackgroundColor { get; set; } = Colors.Transparent;
         internal string FontType { get; set; } = "Calibri";
         internal float Size { get; set; } = 12;
         internal float LineHeight { get; set; } = 1.2f;
         internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
         internal FontWeight FontWeight { get; set; } = FontWeight.Normal;
         internal bool IsItalic { get; set; } = false;
+        internal bool IsStroked { get; set; } = false;
+        internal bool IsUnderlined { get; set; } = false;
 
         public static TextStyle Default => new TextStyle();
         
         public override string ToString()
         {
-            return $"{Color}|{FontType}|{Size}|{LineHeight}|{Alignment}|{FontWeight}|{IsItalic}";
+            return $"{Color}|{BackgroundColor}|{FontType}|{Size}|{LineHeight}|{Alignment}|{FontWeight}|{IsItalic}|{IsStroked}|{IsUnderlined}";
         }
 
         internal TextStyle Clone() => (TextStyle)MemberwiseClone();