Marcin Ziąbek пре 1 година
родитељ
комит
7c8a2c27c6

+ 7 - 4
Source/QuestPDF.Examples/ContentDirectionExamples.cs

@@ -316,16 +316,19 @@ namespace QuestPDF.Examples
                     column.Item().Element(ContentWithAlignment(null));
                     
                     column.Item().Text("Left alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(HorizontalAlignment.Left));
+                    column.Item().Element(ContentWithAlignment(TextHorizontalAlignment.Left));
                     
                     column.Item().Text("Center alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(HorizontalAlignment.Center));
+                    column.Item().Element(ContentWithAlignment(TextHorizontalAlignment.Center));
                     
                     column.Item().Text("Right alignment").FontSize(14).SemiBold();
-                    column.Item().Element(ContentWithAlignment(HorizontalAlignment.Right));
+                    column.Item().Element(ContentWithAlignment(TextHorizontalAlignment.Right));
+                    
+                    column.Item().Text("Justify alignment").FontSize(14).SemiBold();
+                    column.Item().Element(ContentWithAlignment(TextHorizontalAlignment.Right));
                 });
 
-                static Action<IContainer> ContentWithAlignment(HorizontalAlignment? alignment)
+                static Action<IContainer> ContentWithAlignment(TextHorizontalAlignment? alignment)
                 {
                     return container =>
                     {

+ 3 - 3
Source/QuestPDF.Examples/TextExamples.cs

@@ -138,7 +138,7 @@ namespace QuestPDF.Examples
                         .Padding(20)
                         .Column(column =>
                         {
-                            var letterSpacing = new[] { -0.1f, 0f, 0.2f };
+                            var letterSpacing = new[] { -1f, 0f, 2f };
                             var paragraph = Placeholders.Sentence().ToUpper();
 
                             foreach (var spacing in letterSpacing)
@@ -180,7 +180,7 @@ namespace QuestPDF.Examples
                         .Padding(50)
                         .Column(column =>
                         {
-                            var letterSpacing = new[] { -0.1f, 0f, 0.2f };
+                            var letterSpacing = new[] { -1f, 0f, 2f };
                             var paragraph = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا";
                             foreach (var spacing in letterSpacing)
                             {
@@ -222,7 +222,7 @@ namespace QuestPDF.Examples
                         .Padding(50)
                         .Column(column =>
                         {
-                            var letterSpacing = new[] { 0f, 0.5f };
+                            var letterSpacing = new[] { 0f, 5f };
                             
                             
                             

+ 4 - 11
Source/QuestPDF.UnitTests/TextStyleTests.cs

@@ -1,4 +1,5 @@
-using FluentAssertions;
+using System.Collections.Generic;
+using FluentAssertions;
 using NUnit.Framework;
 using QuestPDF.Fluent;
 using QuestPDF.Helpers;
@@ -37,20 +38,12 @@ namespace QuestPDF.UnitTests
             // assert
             var expectedStyle = TextStyle.LibraryDefault with
             {
+                Id = 20, // expect to break when adding new TextStyle properties
                 Size = 20, 
                 FontFamily = "Times New Roman",
                 FontWeight = FontWeight.Bold,
                 BackgroundColor = Colors.Red.Lighten2,
-                HasStrikethrough = true,
-                Fallback = TextStyle.LibraryDefault with
-                {
-                    Size = 20,
-                    FontFamily = "Microsoft YaHei",
-                    FontWeight = FontWeight.Bold,
-                    BackgroundColor = Colors.Red.Lighten2,
-                    HasUnderline = true,
-                    HasStrikethrough = true
-                }
+                HasStrikethrough = true
             };
 
             targetStyle.Should().BeEquivalentTo(expectedStyle);

+ 37 - 30
Source/QuestPDF/Elements/Text/TextBlock.cs

@@ -3,7 +3,6 @@ using System.Collections.Generic;
 using System.Linq;
 using QuestPDF.Drawing;
 using QuestPDF.Elements.Text.Items;
-using QuestPDF.Helpers;
 using QuestPDF.Infrastructure;
 using QuestPDF.Skia;
 using QuestPDF.Skia.Text;
@@ -14,7 +13,7 @@ namespace QuestPDF.Elements.Text
     {
         public ContentDirection ContentDirection { get; set; }
         
-        public HorizontalAlignment? Alignment { get; set; }
+        public TextHorizontalAlignment? Alignment { get; set; }
         public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();
 
         private bool RebuildParagraphForEveryPage { get; set; }
@@ -40,28 +39,17 @@ namespace QuestPDF.Elements.Text
             CurrentTopOffset = 0;
         }
         
-        void SetDefaultAlignment()
-        {
-            if (Alignment.HasValue)
-                return;
-
-            Alignment = ContentDirection == ContentDirection.LeftToRight
-                ? HorizontalAlignment.Left
-                : HorizontalAlignment.Right;
-        }
-
         internal override SpacePlan Measure(Size availableSpace)
         {
             if (Items.Count == 0)
                 return SpacePlan.FullRender(Size.Zero);
             
-            SetDefaultAlignment();
             Initialize();
 
             if (Math.Abs(WidthForLineMetricsCalculation - availableSpace.Width) > Size.Epsilon)
             {
                 WidthForLineMetricsCalculation = availableSpace.Width;
-                Paragraph.PlanLayout(availableSpace.Width);
+                Paragraph.PlanLayout(availableSpace.Width + 1);
                 LineMetrics = Paragraph.GetLineMetrics();
                 PlaceholderPositions = Paragraph.GetPlaceholderPositions();
                 MaximumWidth = LineMetrics.Max(x => x.Width);
@@ -233,13 +221,15 @@ namespace QuestPDF.Elements.Text
 
         private void BuildParagraph()
         {
-            using var paragraphStyle = new SkParagraphStyle(new ParagraphStyleConfiguration
+            var paragraphStyle = new ParagraphStyleConfiguration
             {
-                Alignment = ParagraphStyleConfiguration.TextAlign.Justify,
-                Direction = ParagraphStyleConfiguration.TextDirection.Ltr,
-                MaxLinesVisible = 1000000,
-                Ellipsis = "..."
-            });
+                Alignment = MapAlignment(Alignment ?? TextHorizontalAlignment.Start),
+                Direction = MapDirection(ContentDirection),
+                
+                // TODO: add API to support specifying the maximum number of lines to render
+                // if the text is longer than maximum number of lines, the ellipsis will be rendered 
+                MaxLinesVisible = 1_000_000
+            };
             
             using var paragraphBuilder = SkParagraphBuilder.Create(paragraphStyle, FontManager.FontCollection);
             var currentTextIndex = 0;
@@ -248,16 +238,6 @@ namespace QuestPDF.Elements.Text
             {
                 if (textBlockItem is TextBlockSpan textBlockSpan)
                 {
-                    var style = textBlockSpan.Style;
-                
-                    var textStyle = new SkTextStyle(new TextStyleConfiguration
-                    {
-                        FontFamilies = new string[8] { style.FontFamily, null, null, null, null, null, null, null },
-                        FontSize = style.Size.Value,
-                        FontWeight = (TextStyleConfiguration.FontWeights)style.FontWeight.Value,
-                        ForegroundColor = style.Color.ColorToCode()
-                    });
-
                     if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
                         textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
 
@@ -267,6 +247,7 @@ namespace QuestPDF.Elements.Text
                     else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
                         textBlockPageNumber.UpdatePageNumberText(PageContext);
                 
+                    var textStyle = textBlockSpan.Style.GetSkTextStyle();
                     paragraphBuilder.AddText(textBlockSpan.Text, textStyle);
                     currentTextIndex += textBlockSpan.Text.Length;
                 }
@@ -277,6 +258,8 @@ namespace QuestPDF.Elements.Text
                     {
                         Width = textBlockElement.ElementSize.Width,
                         Height = textBlockElement.ElementSize.Height,
+                        
+                        // TODO: add API support for exact positioning of text-injected elements
                         Alignment = SkPlaceholderStyle.PlaceholderAlignment.AboveBaseline,
                         Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
                         BaselineOffset = 0
@@ -285,6 +268,30 @@ namespace QuestPDF.Elements.Text
             }
 
             Paragraph = paragraphBuilder.CreateParagraph();
+            
+            static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
+            {
+                return alignment switch
+                {
+                    TextHorizontalAlignment.Left => ParagraphStyleConfiguration.TextAlign.Left,
+                    TextHorizontalAlignment.Center => ParagraphStyleConfiguration.TextAlign.Center,
+                    TextHorizontalAlignment.Right => ParagraphStyleConfiguration.TextAlign.Right,
+                    TextHorizontalAlignment.Justify => ParagraphStyleConfiguration.TextAlign.Justify,
+                    TextHorizontalAlignment.Start => ParagraphStyleConfiguration.TextAlign.Start,
+                    TextHorizontalAlignment.End => ParagraphStyleConfiguration.TextAlign.End,
+                    _ => throw new Exception()
+                };
+            }
+
+            static ParagraphStyleConfiguration.TextDirection MapDirection(ContentDirection direction)
+            {
+                return direction switch
+                {
+                    ContentDirection.LeftToRight => ParagraphStyleConfiguration.TextDirection.Ltr,
+                    ContentDirection.RightToLeft => ParagraphStyleConfiguration.TextDirection.Rtl,
+                    _ => throw new Exception()
+                };
+            }
         }
     }
 }

+ 52 - 10
Source/QuestPDF/Fluent/TextExtensions.cs

@@ -63,8 +63,7 @@ namespace QuestPDF.Fluent
     {
         private TextBlock TextBlock { get; } = new();
         private TextStyle? DefaultStyle { get; set; }
-        internal HorizontalAlignment? Alignment { get; set; }
-        private float Spacing { get; set; } = 0f;
+        internal TextHorizontalAlignment? Alignment { get; set; }
 
         /// <summary>
         /// Applies a consistent text style for the whole content within this <see cref="TextExtensions.Text">Text</see> element.
@@ -89,7 +88,7 @@ namespace QuestPDF.Fluent
         /// </summary>
         public void AlignLeft()
         {
-            Alignment = HorizontalAlignment.Left;
+            Alignment = TextHorizontalAlignment.Left;
         }
         
         /// <summary>
@@ -97,7 +96,7 @@ namespace QuestPDF.Fluent
         /// </summary>
         public void AlignCenter()
         {
-            Alignment = HorizontalAlignment.Center;
+            Alignment = TextHorizontalAlignment.Center;
         }
         
         /// <summary>
@@ -105,15 +104,47 @@ namespace QuestPDF.Fluent
         /// </summary>
         public void AlignRight()
         {
-            Alignment = HorizontalAlignment.Right;
+            Alignment = TextHorizontalAlignment.Right;
         }
-
+        
         /// <summary>
-        /// Adjusts the gap between successive paragraphs (separated by line breaks).
+        /// <para>
+        /// Justifies the text within its container.
+        /// </para>
+        ///
+        /// <para>
+        /// This method sets the horizontal alignment of the text to be justified, meaning it aligns along both the left and right margins.
+        /// This is achieved by adjusting the spacing between words and characters as necessary so that each line of text stretches from one end of the text column to the other.
+        /// This creates a clean, block-like appearance for the text.
+        /// </para>
         /// </summary>
+        public void Justify()
+        {
+            Alignment = TextHorizontalAlignment.Justify;
+        }
+        
+        /// <summary>
+        /// Aligns the text horizontally to the start of the container. 
+        /// This method sets the horizontal alignment of the text to the start (left for left-to-right languages, right for right-to-left languages).
+        /// </summary>
+        public void AlignStart()
+        {
+            Alignment = TextHorizontalAlignment.Start;
+        }
+        
+        /// <summary>
+        /// Aligns the text horizontally to the end of the container. 
+        /// This method sets the horizontal alignment of the text to the end (right for left-to-right languages, start for right-to-left languages).
+        /// </summary>
+        public void AlignEnd()
+        {
+            Alignment = TextHorizontalAlignment.End;
+        }
+        
+        [Obsolete("This method is not supported since the 2024.3 version. Please split your text into separate paragraphs, combine using the Column element that also provides the Spacing capability.")]
         public void ParagraphSpacing(float value, Unit unit = Unit.Point)
         {
-            Spacing = value.ToPoints(unit);
+            
         }
 
         [Obsolete("This element has been renamed since version 2022.3. Please use the overload that returns a TextSpanDescriptor object which allows to specify text style.")]
@@ -350,7 +381,7 @@ namespace QuestPDF.Fluent
             var descriptor = new TextDescriptor();
             
             if (element is Alignment alignment)
-                descriptor.Alignment = alignment.Horizontal;
+                descriptor.Alignment = MapAlignment(alignment.Horizontal);
             
             content?.Invoke(descriptor);
             descriptor.Compose(element);
@@ -377,12 +408,23 @@ namespace QuestPDF.Fluent
             var textDescriptor = new TextDescriptor();
             
             if (element is Alignment alignment)
-                textDescriptor.Alignment = alignment.Horizontal;
+                textDescriptor.Alignment = MapAlignment(alignment.Horizontal);
             
             var span = textDescriptor.Span(text);
             textDescriptor.Compose(element);
 
             return span;
         }
+        
+        private static TextHorizontalAlignment? MapAlignment(HorizontalAlignment? alignment)
+        {
+            return alignment switch
+            {
+                HorizontalAlignment.Left => TextHorizontalAlignment.Left,
+                HorizontalAlignment.Center => TextHorizontalAlignment.Center,
+                HorizontalAlignment.Right => TextHorizontalAlignment.Right,
+                _ => null
+            };
+        }
     }
 }

+ 10 - 3
Source/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs

@@ -16,14 +16,14 @@ namespace QuestPDF.Fluent
             return descriptor;
         }
         
-        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />
+        [Obsolete("This setting is not supported since the 2024.3 version. Please use the FontFamilyFallback method or rely on the new automated fallback mechanism.")]
         public static T Fallback<T>(this T descriptor, TextStyle? value = null) where T : TextSpanDescriptor
         {
             descriptor.MutateTextStyle(TextStyleExtensions.Fallback, value);
             return descriptor;
         }
         
-        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />
+        [Obsolete("This setting is not supported since the 2024.3 version. Please use the FontFamilyFallback method or rely on the new automated fallback mechanism.")]
         public static T Fallback<T>(this T descriptor, Func<TextStyle, TextStyle> handler) where T : TextSpanDescriptor
         {
             return descriptor.Fallback(handler(TextStyle.Default));
@@ -54,6 +54,13 @@ namespace QuestPDF.Fluent
             return descriptor;
         }
         
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />
+        public static T FontFamilyFallback<T>(this T descriptor, string value) where T : TextSpanDescriptor
+        {
+            descriptor.MutateTextStyle(TextStyleExtensions.FontFamilyFallback, value);
+            return descriptor;
+        }
+        
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontSize"]/*' />
         public static T FontSize<T>(this T descriptor, float value) where T : TextSpanDescriptor
         {
@@ -103,9 +110,9 @@ namespace QuestPDF.Fluent
         }
 
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.wrapAnywhere"]/*' />
+        [Obsolete("This setting is not supported since the 2024.3 version. This flag should be handled automatically by the layout engine.")]
         public static T WrapAnywhere<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
         {
-            descriptor.MutateTextStyle(TextStyleExtensions.WrapAnywhere, value);
             return descriptor;
         }
 

+ 10 - 3
Source/QuestPDF/Fluent/TextStyleExtensions.cs

@@ -40,6 +40,12 @@ namespace QuestPDF.Fluent
             return style.Mutate(TextStyleProperty.FontFamily, value);
         }
         
+        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />
+        public static TextStyle FontFamilyFallback(this TextStyle style, string value)
+        {
+            return style.Mutate(TextStyleProperty.FontFamilyFallback, value);
+        }
+        
         [Obsolete("This element has been renamed since version 2022.3. Please use the FontSize method.")]
         public static TextStyle Size(this TextStyle style, float value)
         {
@@ -89,9 +95,10 @@ namespace QuestPDF.Fluent
         }
         
         /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.wrapAnywhere"]/*' />
+        [Obsolete("This setting is not supported since the 2024.3 version. This flag should be handled automatically by the layout engine.")]
         public static TextStyle WrapAnywhere(this TextStyle style, bool value = true)
         {
-            return style.Mutate(TextStyleProperty.WrapAnywhere, value);
+            return style;
         }
 
         #region Weight
@@ -202,13 +209,13 @@ namespace QuestPDF.Fluent
 
         #region Fallback
         
-        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />
+        [Obsolete("This setting is not supported since the 2024.3 version. Please use the FontFamilyFallback method or rely on the new automated fallback mechanism.")]
         public static TextStyle Fallback(this TextStyle style, TextStyle? value = null)
         {
             return style;
         }
         
-        /// <include file='../Resources/Documentation.xml' path='documentation/doc[@for="text.fontFallback"]/*' />
+        [Obsolete("This setting is not supported since the 2024.3 version. Please use the FontFamilyFallback method or rely on the new automated fallback mechanism.")]
         public static TextStyle Fallback(this TextStyle style, Func<TextStyle, TextStyle> handler)
         {
             return style.Fallback(handler(TextStyle.Default));

+ 11 - 0
Source/QuestPDF/Infrastructure/TextHorizontalAlignment.cs

@@ -0,0 +1,11 @@
+namespace QuestPDF.Infrastructure;
+
+public enum TextHorizontalAlignment
+{
+    Left,
+    Center,
+    Right,
+    Justify,
+    Start,
+    End
+}

+ 1 - 6
Source/QuestPDF/Resources/Documentation.xml

@@ -371,14 +371,9 @@
 
     <doc for="text.fontFallback">
         <summary>
-            Specifies an alternative text style to be used when the primary font family does not support certain character glyphs.
+            Specifies an alternative font family to be used when the primary font family does not support certain character glyphs.
             <a href="https://www.questpdf.com/api-reference/text.html#font-fallback">Learn more</a>
         </summary>
-        
-        <remarks>
-            The library supports nested font fallbacks, ensuring a series of alternative fonts. 
-            Individual properties can be adjusted for each level of fallback.
-        </remarks>
     </doc>
 
     <!-- PREVIEWER -->

+ 1 - 0
Source/QuestPDF/Skia/Text/SkTextStyle.cs

@@ -21,6 +21,7 @@ internal struct TextStyleConfiguration
     public TextDecorationMode DecorationMode;
     public TextDecorationStyle DecorationStyle;
     
+    public float LineHeight;
     public float LetterSpacing;
     public float WordSpacing;
     public float BaselineOffset;

BIN
Source/QuestPDF/skia_questpdf.dylib