diff --git a/QuestPDF.Examples/TextExamples.cs b/QuestPDF.Examples/TextExamples.cs index 9ef70bf..ecb5d95 100644 --- a/QuestPDF.Examples/TextExamples.cs +++ b/QuestPDF.Examples/TextExamples.cs @@ -122,7 +122,127 @@ namespace QuestPDF.Examples }); }); } - + + [Test] + public void LetterSpacing() + { + RenderingTest + .Create() + .PageSize(500, 700) + .ProduceImages() + .ShowResults() + .Render(container => + { + container + .Padding(20) + .Column(column => + { + var letterSpacing = new[] { -1f, 0f, 2f }; + var paragraph = Placeholders.Sentence(); + + foreach (var spacing in letterSpacing) + { + column + .Item() + .Border(1) + .Padding(10) + .Column(nestedColumn => + { + nestedColumn.Item() + .Text(paragraph) + .FontSize(16) + .LetterSpacing(spacing); + + nestedColumn.Item() + .Text($"Letter spacing of {spacing} pt") + .FontSize(10) + .Italic() + .FontColor(Colors.Blue.Medium); + }); + + } + }); + }); + } + + [Test] + public void LetterSpacing_Arabic() + { + RenderingTest + .Create() + .PageSize(500, 700) + .ProduceImages() + .ShowResults() + .Render(container => + { + container + .Padding(50) + .Column(column => + { + var letterSpacing = new[] { -1f, 0f, 2f }; + var paragraph = "ينا الألم. في بعض الأحيان ونظراً للالتزامات التي يفرضها علينا"; + foreach (var spacing in letterSpacing) + { + column + .Item() + .Border(1) + .Padding(10) + .Column(nestedColumn => + { + nestedColumn.Item() + .Text(paragraph) + .FontSize(16) + .FontFamily(Fonts.Calibri) + .LetterSpacing(spacing); + + nestedColumn.Item() + .Text($"Letter spacing of {spacing} pt") + .FontSize(10) + .Italic() + .FontColor(Colors.Blue.Medium); + }); + } + }); + }); + } + + + [Test] + public void LetterSpacing_Unicode() + { + RenderingTest + .Create() + .PageSize(500, 700) + .ProduceImages() + .ShowResults() + .Render(container => + { + container + .Padding(50) + .Column(column => + { + var letterSpacing = new[] { 0f, 20f }; + var paragraph = "Ţ̴̡̧̤̮̺̤̗͎̱̹͙͎͖͂̿̓́̉̊̀̍͜h̵̞̘͇̾̎̏̅į̵̹̖͔͉̰̎̉̄̐̏͑͂̅̃̃͘͝s̷͓͉̭̭̯̬̥̻̰̩̦̑̀̀͌́̒̍̒̌̇͛̀͛́̎ ̷̡̡̟͕̳̺̝̼͇͔̬̟̖͍̈́̽͜͝͝i̶͔͚̟̊̐͛́͛̄̌ṡ̸̡̤̪͙͍̥͙̟̼̝̰̥͈̿̓̄̿̓͠ ̶̢̦̙͍̯̖̱̰̯͕͔͎̯̝̎͑t̸͖̲̱̼̎͐̎̉̾̎̾̌̅̔̏͘ȩ̶̝̫̙͓̙̣̔̀̌̔̋̂̑̈́̏̀̈͘̕͜͝s̸̫̝̮̻̼͐̅̄̎̎̑͝ț̷̨̢̨̻͈̮̞̆͗̓͊̃̌͂̑̉̕̕͜͝͝"; + + foreach (var spacing in letterSpacing) + { + column.Item() + .Text($"Letter spacing of {spacing} pt") + .FontSize(10) + .Italic() + .FontColor(Colors.Blue.Medium); + + column.Item() + .PaddingVertical(50) + .Text(paragraph) + .FontSize(16) + .FontFamily(Fonts.Calibri) + .LetterSpacing(spacing); + } + }); + }); + } + [Test] public void SuperscriptSubscript_Simple() { diff --git a/QuestPDF/Drawing/TextShaper.cs b/QuestPDF/Drawing/TextShaper.cs index a5e6f9e..2df1a3b 100644 --- a/QuestPDF/Drawing/TextShaper.cs +++ b/QuestPDF/Drawing/TextShaper.cs @@ -47,17 +47,24 @@ namespace QuestPDF.Drawing var xOffset = 0f; var yOffset = 0f; + uint lastCluster = glyphInfos.LastOrDefault().Cluster; + var glyphs = new ShapedGlyph[length]; - for (var i = 0; i < length; i++) { + //We need to advance xOffset by the current letter spacing after each unicode cluster. + if (lastCluster != glyphInfos[i].Cluster) + { + lastCluster = glyphInfos[i].Cluster; + xOffset += TextStyle.LetterSpacing ?? 0; + } + glyphs[i] = new ShapedGlyph { Codepoint = (ushort)glyphInfos[i].Codepoint, Position = new SKPoint(xOffset + glyphPositions[i].XOffset * scaleX, yOffset - glyphPositions[i].YOffset * scaleY), Width = glyphPositions[i].XAdvance * scaleX - }; - + }; xOffset += glyphPositions[i].XAdvance * scaleX; yOffset += glyphPositions[i].YAdvance * scaleY; } diff --git a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs index 3d03891..30dcdaa 100644 --- a/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs +++ b/QuestPDF/Fluent/TextSpanDescriptorExtensions.cs @@ -55,7 +55,13 @@ namespace QuestPDF.Fluent descriptor.MutateTextStyle(x => x.LineHeight(value)); return descriptor; } - + + public static T LetterSpacing(this T descriptor, float value) where T : TextSpanDescriptor + { + descriptor.MutateTextStyle(x => x.LetterSpacing(value)); + return descriptor; + } + public static T Italic(this T descriptor, bool value = true) where T : TextSpanDescriptor { descriptor.MutateTextStyle(x => x.Italic(value)); diff --git a/QuestPDF/Fluent/TextStyleExtensions.cs b/QuestPDF/Fluent/TextStyleExtensions.cs index e8a9ab4..2fe2d6a 100644 --- a/QuestPDF/Fluent/TextStyleExtensions.cs +++ b/QuestPDF/Fluent/TextStyleExtensions.cs @@ -48,7 +48,12 @@ namespace QuestPDF.Fluent { return style.Mutate(TextStyleProperty.LineHeight, value); } - + + public static TextStyle LetterSpacing(this TextStyle style, float value) + { + return style.Mutate(TextStyleProperty.LetterSpacing, value); + } + public static TextStyle Italic(this TextStyle style, bool value = true) { return style.Mutate(TextStyleProperty.IsItalic, value); diff --git a/QuestPDF/Infrastructure/TextStyle.cs b/QuestPDF/Infrastructure/TextStyle.cs index f8536e0..4e365f8 100644 --- a/QuestPDF/Infrastructure/TextStyle.cs +++ b/QuestPDF/Infrastructure/TextStyle.cs @@ -10,6 +10,7 @@ namespace QuestPDF.Infrastructure internal string? FontFamily { get; set; } internal float? Size { get; set; } internal float? LineHeight { get; set; } + internal float? LetterSpacing { get; set; } internal FontWeight? FontWeight { get; set; } internal FontPosition? FontPosition { get; set; } internal bool? IsItalic { get; set; } @@ -27,6 +28,7 @@ namespace QuestPDF.Infrastructure FontFamily = Fonts.Lato, Size = 12, LineHeight = 1.2f, + LetterSpacing = 0, FontWeight = Infrastructure.FontWeight.Normal, FontPosition = Infrastructure.FontPosition.Normal, IsItalic = false, diff --git a/QuestPDF/Infrastructure/TextStyleManager.cs b/QuestPDF/Infrastructure/TextStyleManager.cs index eebfccc..444dee9 100644 --- a/QuestPDF/Infrastructure/TextStyleManager.cs +++ b/QuestPDF/Infrastructure/TextStyleManager.cs @@ -11,6 +11,7 @@ namespace QuestPDF.Infrastructure FontFamily, Size, LineHeight, + LetterSpacing, FontWeight, FontPosition, IsItalic, @@ -102,6 +103,19 @@ namespace QuestPDF.Infrastructure return origin with { LineHeight = castedValue }; } + + if(property == TextStyleProperty.LetterSpacing) + { + if (!overrideValue && origin.LetterSpacing != null) + return origin; + + var castedValue = (float?)value; + + if (origin.LetterSpacing == castedValue) + return origin; + + return origin with { LetterSpacing = castedValue }; + } if (property == TextStyleProperty.FontWeight) { @@ -246,6 +260,7 @@ namespace QuestPDF.Infrastructure result = MutateStyle(result, TextStyleProperty.FontFamily, parent.FontFamily, overrideStyle); result = MutateStyle(result, TextStyleProperty.Size, parent.Size, overrideStyle); result = MutateStyle(result, TextStyleProperty.LineHeight, parent.LineHeight, overrideStyle); + result = MutateStyle(result, TextStyleProperty.LetterSpacing, parent.LetterSpacing, overrideStyle); result = MutateStyle(result, TextStyleProperty.FontWeight, parent.FontWeight, overrideStyle); result = MutateStyle(result, TextStyleProperty.FontPosition, parent.FontPosition, overrideStyle); result = MutateStyle(result, TextStyleProperty.IsItalic, parent.IsItalic, overrideStyle);