Skip to content

Commit a82862f

Browse files
Merge pull request #442 from SixLabors/js/additional-linebreak-fixes
Fix newline handling and whitespace trimming
2 parents 24871a8 + d6c5c87 commit a82862f

14 files changed

+158
-71
lines changed

src/SixLabors.Fonts/GlyphMetrics.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,8 @@ void SetDecoration(TextDecorations decorations, float thickness, float position)
401401
/// <returns>The <see cref="bool"/>.</returns>
402402
[MethodImpl(MethodImplOptions.AggressiveInlining)]
403403
protected internal static bool ShouldSkipGlyphRendering(CodePoint codePoint)
404-
=> UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) && !UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint);
404+
=> CodePoint.IsNewLine(codePoint) ||
405+
(UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) && !UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint));
405406

406407
/// <summary>
407408
/// Returns the size to render/measure the glyph based on the given size and resolution in px units.

src/SixLabors.Fonts/TextLayout.cs

+69-15
Original file line numberDiff line numberDiff line change
@@ -391,12 +391,23 @@ private static IEnumerable<GlyphLayout> LayoutLineHorizontal(
391391
TextLine.GlyphLayoutData data = textLine[i];
392392
if (data.IsNewLine)
393393
{
394+
glyphs.Add(new GlyphLayout(
395+
new Glyph(data.Metrics[0], data.PointSize),
396+
boxLocation,
397+
penLocation,
398+
Vector2.Zero,
399+
data.ScaledAdvance,
400+
yLineAdvance,
401+
GlyphLayoutMode.Horizontal,
402+
true,
403+
data.GraphemeIndex,
404+
data.StringIndex));
405+
394406
penLocation.X = originX;
395407
penLocation.Y += yLineAdvance;
396-
397408
boxLocation.X = originX;
398409
boxLocation.Y += advanceY;
399-
continue;
410+
return glyphs;
400411
}
401412

402413
int j = 0;
@@ -524,12 +535,23 @@ private static IEnumerable<GlyphLayout> LayoutLineVertical(
524535
TextLine.GlyphLayoutData data = textLine[i];
525536
if (data.IsNewLine)
526537
{
538+
glyphs.Add(new GlyphLayout(
539+
new Glyph(data.Metrics[0], data.PointSize),
540+
boxLocation,
541+
penLocation,
542+
Vector2.Zero,
543+
xLineAdvance,
544+
data.ScaledAdvance,
545+
GlyphLayoutMode.Vertical,
546+
true,
547+
data.GraphemeIndex,
548+
data.StringIndex));
549+
527550
boxLocation.X += advanceX;
528551
boxLocation.Y = originY;
529-
530552
penLocation.X += xLineAdvance;
531553
penLocation.Y = originY;
532-
continue;
554+
return glyphs;
533555
}
534556

535557
int j = 0;
@@ -671,12 +693,23 @@ private static IEnumerable<GlyphLayout> LayoutLineVerticalMixed(
671693
TextLine.GlyphLayoutData data = textLine[i];
672694
if (data.IsNewLine)
673695
{
696+
glyphs.Add(new GlyphLayout(
697+
new Glyph(data.Metrics[0], data.PointSize),
698+
boxLocation,
699+
penLocation,
700+
Vector2.Zero,
701+
xLineAdvance,
702+
data.ScaledAdvance,
703+
GlyphLayoutMode.Vertical,
704+
true,
705+
data.GraphemeIndex,
706+
data.StringIndex));
707+
674708
boxLocation.X += advanceX;
675709
boxLocation.Y = originY;
676-
677710
penLocation.X += xLineAdvance;
678711
penLocation.Y = originY;
679-
continue;
712+
return glyphs;
680713
}
681714

682715
if (data.IsTransformed)
@@ -1170,10 +1203,30 @@ VerticalOrientationType.Rotate or
11701203
{
11711204
// Mandatory line break at index.
11721205
TextLine remaining = textLine.SplitAt(i);
1173-
textLines.Add(textLine.Finalize(options));
1174-
textLine = remaining;
1175-
i = -1;
1176-
lineAdvance = 0;
1206+
1207+
if (shouldWrap && textLine.ScaledLineAdvance - glyph.ScaledAdvance > wrappingLength)
1208+
{
1209+
// We've overshot the wrapping length so we need to split the line
1210+
// at the previous break and add both lines.
1211+
TextLine overflow = textLine.SplitAt(lastLineBreak, keepAll);
1212+
if (overflow != textLine)
1213+
{
1214+
textLines.Add(textLine.Finalize(options));
1215+
textLine = overflow;
1216+
}
1217+
1218+
textLines.Add(textLine.Finalize(options));
1219+
textLine = remaining;
1220+
i = -1;
1221+
lineAdvance = 0;
1222+
}
1223+
else
1224+
{
1225+
textLines.Add(textLine.Finalize(options));
1226+
textLine = remaining;
1227+
i = -1;
1228+
lineAdvance = 0;
1229+
}
11771230
}
11781231
else if (shouldWrap)
11791232
{
@@ -1201,7 +1254,7 @@ VerticalOrientationType.Rotate or
12011254
{
12021255
// If the current break is a space, and the line minus the space
12031256
// is less than the wrapping length, we can break using the current break.
1204-
float previousAdvance = lineAdvance - (float)glyph.ScaledAdvance;
1257+
float previousAdvance = lineAdvance - glyph.ScaledAdvance;
12051258
TextLine.GlyphLayoutData lastGlyph = textLine[i - 1];
12061259
if (CodePoint.IsWhiteSpace(lastGlyph.CodePoint))
12071260
{
@@ -1463,8 +1516,9 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll)
14631516

14641517
private void TrimTrailingWhitespace()
14651518
{
1466-
int index = this.data.Count;
1467-
while (index > 0)
1519+
int count = this.data.Count;
1520+
int index = count;
1521+
while (index > 1)
14681522
{
14691523
// Trim trailing breaking whitespace.
14701524
CodePoint point = this.data[index - 1].CodePoint;
@@ -1476,9 +1530,9 @@ private void TrimTrailingWhitespace()
14761530
index--;
14771531
}
14781532

1479-
if (index < this.data.Count && index != 0)
1533+
if (index < count)
14801534
{
1481-
this.data.RemoveRange(index, this.data.Count - index);
1535+
this.data.RemoveRange(index, count - index);
14821536
}
14831537
}
14841538

Loading
Loading
Loading

tests/SixLabors.Fonts.Tests/Issues/Issues_27.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public void ThrowsMeasuringWhitespace()
1212
Font font = new FontCollection().Add(TestFonts.WendyOneFile).CreateFont(12);
1313
FontRectangle size = TextMeasurer.MeasureBounds(" ", new TextOptions(new Font(font, 30)));
1414

15-
Assert.Equal(60, size.Width, 1F);
15+
Assert.Equal(6, size.Width, 1F);
1616
Assert.Equal(0, size.Height, 1F);
1717
}
1818
}

tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ namespace SixLabors.Fonts.Tests.Issues;
99
public class Issues_33
1010
{
1111
[Theory]
12-
[InlineData("\naaaabbbbccccddddeeee\n\t\t\t3 tabs\n\t\t\t\t\t5 tabs", 580, 70)] // newlines aren't directly measured but it is used for offsetting
13-
[InlineData("\n\tHelloworld", 310, 10)]
12+
[InlineData("\naaaabbbbccccddddeeee\n\t\t\t3 tabs\n\t\t\t\t\t5 tabs", 580, 120)]
13+
[InlineData("\n\tHelloworld", 310, 60)]
1414
[InlineData("\tHelloworld", 310, 10)]
1515
[InlineData(" Helloworld", 340, 10)]
1616
[InlineData("Hell owor ld\t", 340, 10)]

tests/SixLabors.Fonts.Tests/Issues/Issues_35.cs

+10-30
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,10 @@ public class Issues_35
1212
public void RenderingTabAtStartOrLineTooShort()
1313
{
1414
Font font = CreateFont("\t x");
15-
FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
16-
FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
17-
FontRectangle tabWithXWidth = TextMeasurer.MeasureBounds("\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
18-
19-
Assert.Equal(tabWidth.Width + xWidth.Width, tabWithXWidth.Width, 2F);
20-
}
21-
22-
[Fact]
23-
public void Rendering2TabsAtStartOfLineTooShort()
24-
{
25-
Font font = CreateFont("\t x");
26-
FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
27-
FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
28-
FontRectangle tabWithXWidth = TextMeasurer.MeasureBounds("\t\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
15+
TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor };
16+
FontRectangle xWidth = TextMeasurer.MeasureBounds("x", options);
17+
FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", options);
18+
FontRectangle tabWithXWidth = TextMeasurer.MeasureBounds("\tx", options);
2919

3020
Assert.Equal(tabWidth.Width + xWidth.Width, tabWithXWidth.Width, 2F);
3121
}
@@ -34,27 +24,17 @@ public void Rendering2TabsAtStartOfLineTooShort()
3424
public void TwoTabsAreDoubleWidthOfOneTab()
3525
{
3626
Font font = CreateFont("\t x");
37-
FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
38-
FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
39-
FontRectangle twoTabWidth = TextMeasurer.MeasureBounds("\t\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
40-
41-
Assert.Equal(twoTabWidth.Width, tabWidth.Width * 2, 2F);
42-
}
43-
44-
[Fact]
45-
public void TwoTabsAreDoubleWidthOfOneTabMinusXWidth()
46-
{
47-
Font font = CreateFont("\t x");
48-
FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
49-
FontRectangle tabWidth = TextMeasurer.MeasureBounds("\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
50-
FontRectangle twoTabWidth = TextMeasurer.MeasureBounds("\t\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
27+
TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor };
28+
FontRectangle xWidth = TextMeasurer.MeasureBounds("x", options);
29+
FontRectangle tabWithXWidth = TextMeasurer.MeasureBounds("\tx", options);
30+
FontRectangle tabTabWithXWidth = TextMeasurer.MeasureBounds("\t\tx", options);
5131

52-
Assert.Equal(twoTabWidth.Width - xWidth.Width, (tabWidth.Width - xWidth.Width) * 2, 2F);
32+
Assert.Equal(tabTabWithXWidth.Width - xWidth.Width, 2 * (tabWithXWidth.Width - xWidth.Width), 2F);
5333
}
5434

5535
public static Font CreateFont(string text)
5636
{
57-
var fc = (IFontMetricsCollection)new FontCollection();
37+
IFontMetricsCollection fc = new FontCollection();
5838
Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12);
5939
return new Font(d, 1F);
6040
}

tests/SixLabors.Fonts.Tests/Issues/Issues_36.cs

+11-9
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,16 @@ public class Issues_36
1414
[InlineData(3)]
1515
[InlineData(4)]
1616
[InlineData(5)]
17-
public void TextWidthForTabOnlyTextShouldBeSingleTabWidthMultipliedByTabCount(int tabCount)
17+
public void TextWidthForTabOnlyTextShouldBeSingleTabWidth(int tabCount)
1818
{
19-
Font font = CreateFont("\t x");
19+
Font font = CreateFont("\t");
20+
TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor };
2021

21-
FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
22+
FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", options);
2223
string tabString = string.Empty.PadRight(tabCount, '\t');
23-
FontRectangle tabCountWidth = TextMeasurer.MeasureBounds(tabString, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
24+
FontRectangle tabCountWidth = TextMeasurer.MeasureBounds(tabString, options);
2425

25-
Assert.Equal(tabWidth.Width * tabCount, tabCountWidth.Width, 2F);
26+
Assert.Equal(tabWidth.Width, tabCountWidth.Width, 2F);
2627
}
2728

2829
[Theory]
@@ -35,10 +36,11 @@ public void TextWidthForTabOnlyTextShouldBeSingleTabWidthMultipliedByTabCountMin
3536
{
3637
Font font = CreateFont("\t x");
3738

38-
FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
39-
FontRectangle tabWidth = TextMeasurer.MeasureBounds("\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
39+
TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor };
40+
FontRectangle xWidth = TextMeasurer.MeasureBounds("x", options);
41+
FontRectangle tabWidth = TextMeasurer.MeasureBounds("\tx", options);
4042
string tabString = "x".PadLeft(tabCount + 1, '\t');
41-
FontRectangle tabCountWidth = TextMeasurer.MeasureBounds(tabString, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
43+
FontRectangle tabCountWidth = TextMeasurer.MeasureBounds(tabString, options);
4244

4345
float singleTabWidth = tabWidth.Width - xWidth.Width;
4446
float finalTabWidth = tabCountWidth.Width - xWidth.Width;
@@ -47,7 +49,7 @@ public void TextWidthForTabOnlyTextShouldBeSingleTabWidthMultipliedByTabCountMin
4749

4850
public static Font CreateFont(string text)
4951
{
50-
var fc = (IFontMetricsCollection)new FontCollection();
52+
IFontMetricsCollection fc = new FontCollection();
5153
Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12);
5254
return new Font(d, 1F);
5355
}

tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ public void RenderingTextIncludesAllGlyphs()
2222
.AppendLine(" NEWS_CATEGORY=EWF&NEWS_HASH=4b298ff9277ef9fdf515356be95ea3caf57cd36&OFFSET=0&SEARCH_VALUE=CA88105E1088&ID_NEWS")
2323
.Append(" ");
2424

25+
TextLayoutTestUtilities.TestLayout(stringBuilder.ToString(), options);
26+
2527
int lineCount = TextMeasurer.CountLines(stringBuilder.ToString(), options);
26-
Assert.Equal(2, lineCount);
28+
Assert.Equal(4, lineCount);
2729
#endif
2830
}
2931
}

tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs

+8-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ namespace SixLabors.Fonts.Tests.Issues;
88
public class Issues_434
99
{
1010
[Theory]
11-
[InlineData("- Lorem ipsullll\n\ndolor sit amet\n-consectetur elit", 4)]
11+
[InlineData("- Lorem ipsullll\n\ndolor sit amet\n-consectetur elit", 5)]
1212
public void ShouldInsertExtraLineBreaksA(string text, int expectedLineCount)
1313
{
1414
if (SystemFonts.TryGet("Arial", out FontFamily family))
@@ -20,20 +20,18 @@ public void ShouldInsertExtraLineBreaksA(string text, int expectedLineCount)
2020
WrappingLength = 400,
2121
};
2222

23-
// Line count includes rendered lines only.
24-
// Line breaks cause offsetting of subsequent lines.
23+
TextLayoutTestUtilities.TestLayout(text, options, properties: expectedLineCount);
24+
2525
int lineCount = TextMeasurer.CountLines(text, options);
2626
Assert.Equal(expectedLineCount, lineCount);
2727

2828
IReadOnlyList<GlyphLayout> layout = TextLayout.GenerateLayout(text, options);
29-
Assert.Equal(46, layout.Count);
30-
31-
TextLayoutTestUtilities.TestLayout(text, options, properties: expectedLineCount);
29+
Assert.Equal(47, layout.Count);
3230
}
3331
}
3432

3533
[Theory]
36-
[InlineData("- Lorem ipsullll\n\n\ndolor sit amet\n-consectetur elit", 4)]
34+
[InlineData("- Lorem ipsullll\n\n\ndolor sit amet\n-consectetur elit", 6)]
3735
public void ShouldInsertExtraLineBreaksB(string text, int expectedLineCount)
3836
{
3937
if (SystemFonts.TryGet("Arial", out FontFamily family))
@@ -45,15 +43,13 @@ public void ShouldInsertExtraLineBreaksB(string text, int expectedLineCount)
4543
WrappingLength = 400,
4644
};
4745

48-
// Line count includes rendered lines only.
49-
// Line breaks cause offsetting of subsequent lines.
46+
TextLayoutTestUtilities.TestLayout(text, options, properties: expectedLineCount);
47+
5048
int lineCount = TextMeasurer.CountLines(text, options);
5149
Assert.Equal(expectedLineCount, lineCount);
5250

5351
IReadOnlyList<GlyphLayout> layout = TextLayout.GenerateLayout(text, options);
54-
Assert.Equal(46, layout.Count);
55-
56-
TextLayoutTestUtilities.TestLayout(text, options, properties: expectedLineCount);
52+
Assert.Equal(48, layout.Count);
5753
}
5854
}
5955
}

0 commit comments

Comments
 (0)