Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text justification #188

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions QuestPDF.Examples/TextExamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -403,5 +403,61 @@ public void BreakingLongWord()
});
});
}

[Test]
public void JustifiedText()
{
RenderingTest
.Create()
.ProducePdf()
.ShowResults()
.RenderDocument(container =>
{
container.Page(page =>
{
page.Margin(50);
page.PageColor(Colors.White);

page.Size(PageSizes.A4);

page.Content().Row(r =>
{
void TextSample(bool justify, string text)
{
r.RelativeItem().Border(1).Padding(10).Column(c =>
{
c.Item().AlignMiddle().Text(text).Bold().FontSize(20);

c.Item().Text(text =>
{
if (justify)
text.AlignJustify();

text.ParagraphSpacing(10);

text.Line(Placeholders.LoremIpsum()).BackgroundColor(Colors.Green.Lighten2);

text.Span("This text is a normal text, ");
text.Span("this is a bold text, ").Bold();
text.Span("this is a red and underlined text,").FontColor(Colors.Red.Medium).Underline();
text.Span(" and this is slightly bigger text.").FontSize(25);

text.Span("The new text element also supports injecting custom content between words: ");
text.Element().PaddingBottom(-4).Height(16).Width(32).Image(Placeholders.Image);
text.Span(". ");
text.Span("Also supports ");
text.Hyperlink("hyper links", "https://www.questpdf.com").FontColor(Colors.Blue.Medium).Underline();
text.Span("!");
text.Span("12312342423423423423423423123");
});
});
}

TextSample(justify: false, "Normal");
TextSample(justify: true, "Justified");
});
});
});
}
}
}
5 changes: 3 additions & 2 deletions QuestPDF/Elements/Text/Items/TextBlockSpan.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Infrastructure;
Expand Down Expand Up @@ -75,7 +76,7 @@ internal class TextBlockSpan : ITextBlockItem

// measure final text
var width = paint.MeasureText(text);

return new TextMeasurementResult
{
Width = width,
Expand Down Expand Up @@ -143,6 +144,6 @@ void DrawLine(float offset, float thickness)
{
request.Canvas.DrawRectangle(new Position(0, offset - thickness / 2f), new Size(request.TextSize.Width, thickness), Style.Color);
}
}
}
}
}
38 changes: 25 additions & 13 deletions QuestPDF/Elements/Text/TextBlock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
using QuestPDF.Drawing;
using QuestPDF.Elements.Text.Calculation;
using QuestPDF.Elements.Text.Items;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;

namespace QuestPDF.Elements.Text
{
internal class TextBlock : Element, IStateResettable
{
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
public TextAlignment Alignment { get; set; } = TextAlignment.Left;
public List<ITextBlockItem> Items { get; set; } = new List<ITextBlockItem>();

public string Text => string.Join(" ", Items.Where(x => x is TextBlockSpan).Cast<TextBlockSpan>().Select(x => x.Text));
Expand Down Expand Up @@ -60,18 +61,29 @@ internal override void Draw(Size availableSpace)

var heightOffset = 0f;
var widthOffset = 0f;


var lastLine = lines.Last();
foreach (var line in lines)
{
widthOffset = 0f;

var alignmentOffset = GetAlignmentOffset(line.Width);

Canvas.Translate(new Position(alignmentOffset, 0));
Canvas.Translate(new Position(0, -line.Ascent));


var spacingAfterWord = 0f;
//Only if textblock is justified and its not the last line.
if (Alignment == TextAlignment.Justify && line != lastLine)
{
var totalWhitespace = availableSpace.Width - line.Width;
spacingAfterWord = totalWhitespace / (line.Elements.Count - 1);
}

var lastItem = line.Elements.Last();
foreach (var item in line.Elements)
{
var additionalWidth = lastItem == item ? 0 : spacingAfterWord;
var textDrawingRequest = new TextDrawingRequest
{
Canvas = Canvas,
Expand All @@ -80,20 +92,20 @@ internal override void Draw(Size availableSpace)
StartIndex = item.Measurement.StartIndex,
EndIndex = item.Measurement.EndIndex,

TextSize = new Size(item.Measurement.Width, line.LineHeight),
TextSize = new Size(item.Measurement.Width + additionalWidth, line.LineHeight),
TotalAscent = line.Ascent
};

item.Item.Draw(textDrawingRequest);

Canvas.Translate(new Position(item.Measurement.Width, 0));
widthOffset += item.Measurement.Width;
Canvas.Translate(new Position(item.Measurement.Width + spacingAfterWord, 0));
widthOffset += item.Measurement.Width + spacingAfterWord;
}

Canvas.Translate(new Position(-alignmentOffset, 0));
Canvas.Translate(new Position(-line.Width, line.Ascent));
Canvas.Translate(new Position(-widthOffset, line.Ascent));
Canvas.Translate(new Position(0, line.LineHeight));

heightOffset += line.LineHeight;
}

Expand All @@ -115,15 +127,15 @@ internal override void Draw(Size availableSpace)

float GetAlignmentOffset(float lineWidth)
{
if (Alignment == HorizontalAlignment.Left)
if (Alignment == TextAlignment.Left || Alignment == TextAlignment.Justify)
return 0;

var emptySpace = availableSpace.Width - lineWidth;

if (Alignment == HorizontalAlignment.Right)
if (Alignment == TextAlignment.Right)
return emptySpace;

if (Alignment == HorizontalAlignment.Center)
if (Alignment == TextAlignment.Center)
return emptySpace / 2;

throw new ArgumentException();
Expand Down
90 changes: 67 additions & 23 deletions QuestPDF/Fluent/TextExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using QuestPDF.Elements;
using QuestPDF.Elements.Text;
using QuestPDF.Elements.Text.Items;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
using static System.String;

Expand Down Expand Up @@ -42,7 +43,7 @@ public class TextDescriptor
{
private ICollection<TextBlock> TextBlocks { get; } = new List<TextBlock>();
private TextStyle DefaultStyle { get; set; } = TextStyle.Default;
internal HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
internal TextAlignment Alignment { get; set; } = TextAlignment.Left;
private float Spacing { get; set; } = 0f;

public void DefaultTextStyle(TextStyle style)
Expand All @@ -57,17 +58,22 @@ public void DefaultTextStyle(Func<TextStyle, TextStyle> style)

public void AlignLeft()
{
Alignment = HorizontalAlignment.Left;
Alignment = TextAlignment.Left;
}

public void AlignCenter()
{
Alignment = HorizontalAlignment.Center;
Alignment = TextAlignment.Center;
}

public void AlignRight()
{
Alignment = HorizontalAlignment.Right;
Alignment = TextAlignment.Right;
}

public void AlignJustify()
{
Alignment = TextAlignment.Justify;
}

public void ParagraphSpacing(float value, Unit unit = Unit.Point)
Expand All @@ -76,13 +82,28 @@ public void ParagraphSpacing(float value, Unit unit = Unit.Point)
}

private void AddItemToLastTextBlock(ITextBlockItem item)
{
GetLastTextBlock().Items.Add(item);
}

private void AddItemToLastTextBlock<T>(string? text, TextStyle style, Func<T> func) where T : TextBlockSpan
{
AddItemsToLastTextBlock(GenerateItems(text, style, func));
}

private void AddItemsToLastTextBlock(IEnumerable<ITextBlockItem> item)
{
GetLastTextBlock().Items.AddRange(item);
}

private TextBlock GetLastTextBlock()
{
if (!TextBlocks.Any())
TextBlocks.Add(new TextBlock());
TextBlocks.Last().Items.Add(item);

return TextBlocks.Last();
}

[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.")]
public void Span(string? text, TextStyle style)
{
Expand All @@ -96,24 +117,19 @@ public TextSpanDescriptor Span(string? text)

if (text == null)
return descriptor;

var items = text
.Replace("\r", string.Empty)
.Split(new[] { '\n' }, StringSplitOptions.None)
.Select(x => new TextBlockSpan
{
Text = x,
Style = style
})
.ToList();
.ToArray();

AddItemToLastTextBlock(items.First());
AddItemToLastTextBlock(items.First(), style, () => new TextBlockSpan());

items
.Skip(1)
.Select(x => new TextBlock
{
Items = new List<ITextBlockItem> { x }
{
Items = GenerateItems(x, style, () => new TextBlockSpan()).ToList()
})
.ToList()
.ForEach(TextBlocks.Add);
Expand Down Expand Up @@ -195,8 +211,8 @@ public TextSpanDescriptor SectionLink(string? text, string sectionName)

AddItemToLastTextBlock(new TextBlockSectionlLink
{
Style = style,
Text = text,
Style = style,
SectionName = sectionName
});

Expand All @@ -222,9 +238,9 @@ public TextSpanDescriptor Hyperlink(string? text, string url)

AddItemToLastTextBlock(new TextBlockHyperlink
{
Style = style,
Text = text,
Url = url
Style = style,
Url = url,
});

return descriptor;
Expand Down Expand Up @@ -260,16 +276,44 @@ internal void Compose(IContainer container)
column.Item().Element(textBlock);
});
}

internal IEnumerable<ITextBlockItem> GenerateItems<T>(string? segment, TextStyle style, Func<T> func) where T : TextBlockSpan
{
T CreateItem(string text)
{
var item = func();
item.Text = text;
item.Style = style;
return item;
}

if (IsNullOrEmpty(segment))
{
yield return CreateItem(string.Empty);
yield break;
}

if (Alignment == TextAlignment.Justify)
{
foreach (var split in segment.SplitAndKeep(new[] { ' ' }))
{
yield return CreateItem(split);
}
yield break;
}

yield return CreateItem(segment);
}
}

public static class TextExtensions
{
public static void Text(this IContainer element, Action<TextDescriptor> content)
{
var descriptor = new TextDescriptor();
if (element is Alignment alignment)
descriptor.Alignment = alignment.Horizontal;

if (element is Alignment alignment && descriptor.Alignment != TextAlignment.Justify)
descriptor.Alignment = alignment.Horizontal.ToTextAlignment();

content?.Invoke(descriptor);
descriptor.Compose(element);
Expand Down
2 changes: 1 addition & 1 deletion QuestPDF/Fluent/TextSpanDescriptorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public static T Italic<T>(this T descriptor, bool value = true) where T : TextSp
descriptor.TextStyle.IsItalic = value;
return descriptor;
}

public static T Strikethrough<T>(this T descriptor, bool value = true) where T : TextSpanDescriptor
{
descriptor.TextStyle.HasStrikethrough = value;
Expand Down
2 changes: 1 addition & 1 deletion QuestPDF/Fluent/TextStyleExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public static TextStyle Italic(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.IsItalic = value);
}

public static TextStyle Strikethrough(this TextStyle style, bool value = true)
{
return style.Mutate(x => x.HasStrikethrough = value);
Expand Down
Loading