Skip to content

Commit

Permalink
Merge pull request #285 from SixLabors/js/justified-text
Browse files Browse the repository at this point in the history
Add Text Justification Options
  • Loading branch information
JimBobSquarePants authored Jul 2, 2022
2 parents cab42e5 + dd419f4 commit 691f770
Show file tree
Hide file tree
Showing 7 changed files with 401 additions and 33 deletions.
28 changes: 28 additions & 0 deletions src/SixLabors.Fonts/TextJustification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

namespace SixLabors.Fonts
{
/// <summary>
/// Text justification modes.
/// </summary>
public enum TextJustification
{
/// <summary>
/// No justification
/// </summary>
None = 0,

/// <summary>
/// The text is justified by adding space between words (effectively varying word-spacing),
/// which is most appropriate for languages that separate words using spaces, like English or Korean.
/// </summary>
InterWord,

/// <summary>
/// The text is justified by adding space between characters (effectively varying letter-spacing),
/// which is most appropriate for languages like Japanese.
/// </summary>
InterCharacter
}
}
111 changes: 96 additions & 15 deletions src/SixLabors.Fonts/TextLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -729,17 +729,17 @@ private static TextBox BreakLines(
continue;
}

// Calculate the advance for the current codepoint.
CodePoint codePoint = codePointEnumerator.Current;
GlyphMetrics glyph = metrics[0];
float glyphAdvance = isHorizontal ? glyph.AdvanceWidth : glyph.AdvanceHeight;
if (CodePoint.IsVariationSelector(codePoint))
{
codePointIndex++;
graphemeCodePointIndex++;
continue;
}

// Calculate the advance for the current codepoint.
GlyphMetrics glyph = metrics[0];
float glyphAdvance = isHorizontal ? glyph.AdvanceWidth : glyph.AdvanceHeight;
if (CodePoint.IsTabulation(codePoint))
{
glyphAdvance *= options.TabWidth;
Expand Down Expand Up @@ -797,7 +797,7 @@ private static TextBox BreakLines(
// Mandatory wrap at index.
if (currentLineBreak.PositionWrap == codePointIndex && currentLineBreak.Required)
{
textLines.Add(textLine.BidiReOrder());
textLines.Add(textLine.Finalize());
glyphCount += textLine.Count;
textLine = new();
lineAdvance = 0;
Expand All @@ -808,7 +808,7 @@ private static TextBox BreakLines(
// Forced wordbreak
if (breakAll)
{
textLines.Add(textLine.BidiReOrder());
textLines.Add(textLine.Finalize());
glyphCount += textLine.Count;
textLine = new();
lineAdvance = 0;
Expand All @@ -821,14 +821,14 @@ private static TextBox BreakLines(
TextLine split = textLine.SplitAt(lastLineBreak, keepAll);
if (split != textLine)
{
textLines.Add(textLine.BidiReOrder());
textLines.Add(textLine.Finalize());
textLine = split;
lineAdvance = split.ScaledLineAdvance;
}
}
else
{
textLines.Add(textLine.BidiReOrder());
textLines.Add(textLine.Finalize());
glyphCount += textLine.Count;
textLine = new();
lineAdvance = 0;
Expand All @@ -840,7 +840,7 @@ private static TextBox BreakLines(
TextLine split = textLine.SplitAt(currentLineBreak, keepAll);
if (split != textLine)
{
textLines.Add(textLine.BidiReOrder());
textLines.Add(textLine.Finalize());
textLine = split;
lineAdvance = split.ScaledLineAdvance;
}
Expand All @@ -851,7 +851,7 @@ private static TextBox BreakLines(
TextLine split = textLine.SplitAt(lastLineBreak, keepAll);
if (split != textLine)
{
textLines.Add(textLine.BidiReOrder());
textLines.Add(textLine.Finalize());
textLine = split;
lineAdvance = split.ScaledLineAdvance;
}
Expand Down Expand Up @@ -931,15 +931,22 @@ private static TextBox BreakLines(
// Add the final line.
if (textLine.Count > 0)
{
textLines.Add(textLine.BidiReOrder());
textLines.Add(textLine.Finalize());
}

return new TextBox(textLines);
return new TextBox(options, textLines);
}

internal sealed class TextBox
{
public TextBox(IReadOnlyList<TextLine> textLines) => this.TextLines = textLines;
public TextBox(TextOptions options, IReadOnlyList<TextLine> textLines)
{
this.TextLines = textLines;
for (int i = 0; i < this.TextLines.Count - 1; i++)
{
this.TextLines[i].Justify(options);
}
}

public IReadOnlyList<TextLine> TextLines { get; }

Expand Down Expand Up @@ -1090,7 +1097,81 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll)
return result;
}

public TextLine BidiReOrder()
public TextLine Finalize() => this.BidiReOrder();

public void Justify(TextOptions options)
{
if (options.WrappingLength == -1F || options.TextJustification == TextJustification.None)
{
return;
}

if (this.ScaledLineAdvance == 0)
{
return;
}

float delta = (options.WrappingLength / options.Dpi) - this.ScaledLineAdvance;
if (delta <= 0)
{
return;
}

// Increase the advance for all non zero-width glyphs but the last.
if (options.TextJustification == TextJustification.InterCharacter)
{
int nonZeroCount = 0;
for (int i = 0; i < this.data.Count - 1; i++)
{
GlyphLayoutData glyph = this.data[i];
if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint))
{
nonZeroCount++;
}
}

float padding = delta / nonZeroCount;
for (int i = 0; i < this.data.Count - 1; i++)
{
GlyphLayoutData glyph = this.data[i];
if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint))
{
glyph.ScaledAdvance += padding;
this.data[i] = glyph;
}
}

return;
}

// Increase the advance for all spaces but the last.
if (options.TextJustification == TextJustification.InterWord)
{
// Count all the whitespace characters.
int whiteSpaceCount = 0;
for (int i = 0; i < this.data.Count - 1; i++)
{
GlyphLayoutData glyph = this.data[i];
if (CodePoint.IsWhiteSpace(glyph.CodePoint))
{
whiteSpaceCount++;
}
}

float padding = delta / whiteSpaceCount;
for (int i = 0; i < this.data.Count - 1; i++)
{
GlyphLayoutData glyph = this.data[i];
if (CodePoint.IsWhiteSpace(glyph.CodePoint))
{
glyph.ScaledAdvance += padding;
this.data[i] = glyph;
}
}
}
}

private TextLine BidiReOrder()
{
// Build up the collection of ordered runs.
BidiRun run = this.data[0].BidiRun;
Expand Down Expand Up @@ -1234,7 +1315,7 @@ private static OrderedBidiRun LinearReOrder(OrderedBidiRun? line)
}

[DebuggerDisplay("{DebuggerDisplay,nq}")]
internal readonly struct GlyphLayoutData
internal struct GlyphLayoutData
{
public GlyphLayoutData(
IReadOnlyList<GlyphMetrics> metrics,
Expand Down Expand Up @@ -1266,7 +1347,7 @@ public GlyphLayoutData(

public float PointSize { get; }

public float ScaledAdvance { get; }
public float ScaledAdvance { get; set; }

public float ScaledLineHeight { get; }

Expand Down
59 changes: 51 additions & 8 deletions src/SixLabors.Fonts/TextMeasurer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace SixLabors.Fonts
public static class TextMeasurer
{
/// <summary>
/// Measures the text.
/// Measures the size of the text in pixel units.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="options">The text shaping options.</param>
Expand All @@ -23,7 +23,7 @@ public static FontRectangle Measure(string text, TextOptions options)
=> Measure(text.AsSpan(), options);

/// <summary>
/// Measures the text.
/// Measures the size of the text in pixel units.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="options">The text shaping options.</param>
Expand All @@ -32,7 +32,7 @@ public static FontRectangle Measure(ReadOnlySpan<char> text, TextOptions options
=> GetSize(TextLayout.GenerateLayout(text, options), options.Dpi);

/// <summary>
/// Measures the text.
/// Measures the text bounds in pixel units.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="options">The text shaping options.</param>
Expand All @@ -41,7 +41,7 @@ public static FontRectangle MeasureBounds(string text, TextOptions options)
=> MeasureBounds(text.AsSpan(), options);

/// <summary>
/// Measures the text.
/// Measures the text bounds in pixel units.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="options">The text shaping options.</param>
Expand All @@ -50,7 +50,27 @@ public static FontRectangle MeasureBounds(ReadOnlySpan<char> text, TextOptions o
=> GetBounds(TextLayout.GenerateLayout(text, options), options.Dpi);

/// <summary>
/// Measures the character bounds of the text. For each control character the list contains a <c>null</c> element.
/// Measures the size of each character of the text in pixel units.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="options">The text shaping options.</param>
/// <param name="characterBounds">The list of character dimensions of the text if it was to be rendered.</param>
/// <returns>Whether any of the characters had non-empty dimensions.</returns>
public static bool TryMeasureCharacterDimensions(string text, TextOptions options, out GlyphBounds[] characterBounds)
=> TryMeasureCharacterDimensions(text.AsSpan(), options, out characterBounds);

/// <summary>
/// Measures the size of each character of the text in pixel units.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="options">The text shaping options.</param>
/// <param name="characterBounds">The list of character dimensions of the text if it was to be rendered.</param>
/// <returns>Whether any of the characters had non-empty dimensions.</returns>
public static bool TryMeasureCharacterDimensions(ReadOnlySpan<char> text, TextOptions options, out GlyphBounds[] characterBounds)
=> TryGetCharacterDimensions(TextLayout.GenerateLayout(text, options), options.Dpi, out characterBounds);

/// <summary>
/// Measures the character bounds of the text in pixel units.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="options">The text shaping options.</param>
Expand All @@ -60,7 +80,7 @@ public static bool TryMeasureCharacterBounds(string text, TextOptions options, o
=> TryMeasureCharacterBounds(text.AsSpan(), options, out characterBounds);

/// <summary>
/// Measures the character bounds of the text. For each control character the list contains a <c>null</c> element.
/// Measures the character bounds of the text in pixel units.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="options">The text shaping options.</param>
Expand Down Expand Up @@ -178,6 +198,28 @@ internal static FontRectangle GetBounds(IReadOnlyList<GlyphLayout> glyphLayouts,
return FontRectangle.FromLTRB(left, top, right, bottom);
}

internal static bool TryGetCharacterDimensions(IReadOnlyList<GlyphLayout> glyphLayouts, float dpi, out GlyphBounds[] characterBounds)
{
bool hasSize = false;
if (glyphLayouts.Count == 0)
{
characterBounds = Array.Empty<GlyphBounds>();
return hasSize;
}

var characterBoundsList = new GlyphBounds[glyphLayouts.Count];
for (int i = 0; i < glyphLayouts.Count; i++)
{
GlyphLayout glyph = glyphLayouts[i];
FontRectangle bounds = new(0, 0, glyph.Width * dpi, glyph.Height * dpi);
hasSize |= bounds.Width > 0 || bounds.Height > 0;
characterBoundsList[i] = new GlyphBounds(glyph.Glyph.GlyphMetrics.CodePoint, bounds);
}

characterBounds = characterBoundsList;
return hasSize;
}

internal static bool TryGetCharacterBounds(IReadOnlyList<GlyphLayout> glyphLayouts, float dpi, out GlyphBounds[] characterBounds)
{
bool hasSize = false;
Expand All @@ -191,8 +233,9 @@ internal static bool TryGetCharacterBounds(IReadOnlyList<GlyphLayout> glyphLayou
for (int i = 0; i < glyphLayouts.Count; i++)
{
GlyphLayout g = glyphLayouts[i];
hasSize |= !g.IsStartOfLine;
characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, g.BoundingBox(dpi));
FontRectangle bounds = g.BoundingBox(dpi);
hasSize |= bounds.Width > 0 || bounds.Height > 0;
characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, bounds);
}

characterBounds = characterBoundsList;
Expand Down
6 changes: 6 additions & 0 deletions src/SixLabors.Fonts/TextOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public TextOptions(TextOptions options)
this.WordBreaking = options.WordBreaking;
this.TextDirection = options.TextDirection;
this.TextAlignment = options.TextAlignment;
this.TextJustification = options.TextJustification;
this.HorizontalAlignment = options.HorizontalAlignment;
this.VerticalAlignment = options.VerticalAlignment;
this.LayoutMode = options.LayoutMode;
Expand Down Expand Up @@ -152,6 +153,11 @@ public float LineSpacing
/// </summary>
public TextAlignment TextAlignment { get; set; }

/// <summary>
/// Gets or sets the justification of the text within the box.
/// </summary>
public TextJustification TextJustification { get; set; }

/// <summary>
/// Gets or sets the horizontal alignment of the text box.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion src/SixLabors.Fonts/Unicode/UnicodeTrie.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ internal sealed class UnicodeTrie

public UnicodeTrie(ReadOnlySpan<byte> rawData)
{
var header = MemoryMarshal.Read<UnicodeTrieHeader>(rawData);
UnicodeTrieHeader header = MemoryMarshal.Read<UnicodeTrieHeader>(rawData);

if (!BitConverter.IsLittleEndian)
{
Expand Down
Loading

0 comments on commit 691f770

Please sign in to comment.