diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs
index 86baf52b..58219348 100644
--- a/src/SixLabors.Fonts/GlyphMetrics.cs
+++ b/src/SixLabors.Fonts/GlyphMetrics.cs
@@ -401,7 +401,8 @@ void SetDecoration(TextDecorations decorations, float thickness, float position)
/// The .
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected internal static bool ShouldSkipGlyphRendering(CodePoint codePoint)
- => UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) && !UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint);
+ => CodePoint.IsNewLine(codePoint) ||
+ (UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) && !UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint));
///
/// Returns the size to render/measure the glyph based on the given size and resolution in px units.
diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs
index fdd1402d..24f26901 100644
--- a/src/SixLabors.Fonts/TextLayout.cs
+++ b/src/SixLabors.Fonts/TextLayout.cs
@@ -391,12 +391,23 @@ private static IEnumerable LayoutLineHorizontal(
TextLine.GlyphLayoutData data = textLine[i];
if (data.IsNewLine)
{
+ glyphs.Add(new GlyphLayout(
+ new Glyph(data.Metrics[0], data.PointSize),
+ boxLocation,
+ penLocation,
+ Vector2.Zero,
+ data.ScaledAdvance,
+ yLineAdvance,
+ GlyphLayoutMode.Horizontal,
+ true,
+ data.GraphemeIndex,
+ data.StringIndex));
+
penLocation.X = originX;
penLocation.Y += yLineAdvance;
-
boxLocation.X = originX;
boxLocation.Y += advanceY;
- continue;
+ return glyphs;
}
int j = 0;
@@ -524,12 +535,23 @@ private static IEnumerable LayoutLineVertical(
TextLine.GlyphLayoutData data = textLine[i];
if (data.IsNewLine)
{
+ glyphs.Add(new GlyphLayout(
+ new Glyph(data.Metrics[0], data.PointSize),
+ boxLocation,
+ penLocation,
+ Vector2.Zero,
+ xLineAdvance,
+ data.ScaledAdvance,
+ GlyphLayoutMode.Vertical,
+ true,
+ data.GraphemeIndex,
+ data.StringIndex));
+
boxLocation.X += advanceX;
boxLocation.Y = originY;
-
penLocation.X += xLineAdvance;
penLocation.Y = originY;
- continue;
+ return glyphs;
}
int j = 0;
@@ -671,12 +693,23 @@ private static IEnumerable LayoutLineVerticalMixed(
TextLine.GlyphLayoutData data = textLine[i];
if (data.IsNewLine)
{
+ glyphs.Add(new GlyphLayout(
+ new Glyph(data.Metrics[0], data.PointSize),
+ boxLocation,
+ penLocation,
+ Vector2.Zero,
+ xLineAdvance,
+ data.ScaledAdvance,
+ GlyphLayoutMode.Vertical,
+ true,
+ data.GraphemeIndex,
+ data.StringIndex));
+
boxLocation.X += advanceX;
boxLocation.Y = originY;
-
penLocation.X += xLineAdvance;
penLocation.Y = originY;
- continue;
+ return glyphs;
}
if (data.IsTransformed)
@@ -1170,10 +1203,30 @@ VerticalOrientationType.Rotate or
{
// Mandatory line break at index.
TextLine remaining = textLine.SplitAt(i);
- textLines.Add(textLine.Finalize(options));
- textLine = remaining;
- i = -1;
- lineAdvance = 0;
+
+ if (shouldWrap && textLine.ScaledLineAdvance - glyph.ScaledAdvance > wrappingLength)
+ {
+ // We've overshot the wrapping length so we need to split the line
+ // at the previous break and add both lines.
+ TextLine overflow = textLine.SplitAt(lastLineBreak, keepAll);
+ if (overflow != textLine)
+ {
+ textLines.Add(textLine.Finalize(options));
+ textLine = overflow;
+ }
+
+ textLines.Add(textLine.Finalize(options));
+ textLine = remaining;
+ i = -1;
+ lineAdvance = 0;
+ }
+ else
+ {
+ textLines.Add(textLine.Finalize(options));
+ textLine = remaining;
+ i = -1;
+ lineAdvance = 0;
+ }
}
else if (shouldWrap)
{
@@ -1201,7 +1254,7 @@ VerticalOrientationType.Rotate or
{
// If the current break is a space, and the line minus the space
// is less than the wrapping length, we can break using the current break.
- float previousAdvance = lineAdvance - (float)glyph.ScaledAdvance;
+ float previousAdvance = lineAdvance - glyph.ScaledAdvance;
TextLine.GlyphLayoutData lastGlyph = textLine[i - 1];
if (CodePoint.IsWhiteSpace(lastGlyph.CodePoint))
{
@@ -1463,8 +1516,9 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll)
private void TrimTrailingWhitespace()
{
- int index = this.data.Count;
- while (index > 0)
+ int count = this.data.Count;
+ int index = count;
+ while (index > 1)
{
// Trim trailing breaking whitespace.
CodePoint point = this.data[index - 1].CodePoint;
@@ -1476,9 +1530,9 @@ private void TrimTrailingWhitespace()
index--;
}
- if (index < this.data.Count && index != 0)
+ if (index < count)
{
- this.data.RemoveRange(index, this.data.Count - index);
+ this.data.RemoveRange(index, count - index);
}
}
diff --git a/tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine__WrappingLength_800_.png b/tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine__WrappingLength_800_.png
new file mode 100644
index 00000000..ed2adad0
--- /dev/null
+++ b/tests/Images/ReferenceOutput/LineWrappingWithExplicitNewLine__WrappingLength_800_.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2b4b11a402255b4dca480bece6991e6ec4514e08c29fdad0b29aca09f9a274e8
+size 16832
diff --git a/tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine__WrappingLength_800_.png b/tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine__WrappingLength_800_.png
new file mode 100644
index 00000000..d167a331
--- /dev/null
+++ b/tests/Images/ReferenceOutput/LineWrappingWithImplicitNewLine__WrappingLength_800_.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:7385a22475af5810017247faa02cea7cf406b8b14440f82c6972d04020c154d6
+size 15927
diff --git a/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs__WrappingLength_1900_.png b/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs__WrappingLength_1900_.png
new file mode 100644
index 00000000..afea7655
--- /dev/null
+++ b/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs__WrappingLength_1900_.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:09759e0fda60ef494d81a8c8abefce73c2f896154e29c1d3e28c419b2597fc48
+size 23678
diff --git a/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-4.png b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-5.png
similarity index 100%
rename from tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-4.png
rename to tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-5.png
diff --git a/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-4.png b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-6.png
similarity index 100%
rename from tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-4.png
rename to tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-6.png
diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_27.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_27.cs
index 71dcbc94..bf52e238 100644
--- a/tests/SixLabors.Fonts.Tests/Issues/Issues_27.cs
+++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_27.cs
@@ -12,7 +12,7 @@ public void ThrowsMeasuringWhitespace()
Font font = new FontCollection().Add(TestFonts.WendyOneFile).CreateFont(12);
FontRectangle size = TextMeasurer.MeasureBounds(" ", new TextOptions(new Font(font, 30)));
- Assert.Equal(60, size.Width, 1F);
+ Assert.Equal(6, size.Width, 1F);
Assert.Equal(0, size.Height, 1F);
}
}
diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs
index 6e7f5d14..8b2db2c7 100644
--- a/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs
+++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs
@@ -9,8 +9,8 @@ namespace SixLabors.Fonts.Tests.Issues;
public class Issues_33
{
[Theory]
- [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
- [InlineData("\n\tHelloworld", 310, 10)]
+ [InlineData("\naaaabbbbccccddddeeee\n\t\t\t3 tabs\n\t\t\t\t\t5 tabs", 580, 120)]
+ [InlineData("\n\tHelloworld", 310, 60)]
[InlineData("\tHelloworld", 310, 10)]
[InlineData(" Helloworld", 340, 10)]
[InlineData("Hell owor ld\t", 340, 10)]
diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_35.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_35.cs
index 268ba043..1a8eb029 100644
--- a/tests/SixLabors.Fonts.Tests/Issues/Issues_35.cs
+++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_35.cs
@@ -12,20 +12,10 @@ public class Issues_35
public void RenderingTabAtStartOrLineTooShort()
{
Font font = CreateFont("\t x");
- FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
- FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
- FontRectangle tabWithXWidth = TextMeasurer.MeasureBounds("\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
-
- Assert.Equal(tabWidth.Width + xWidth.Width, tabWithXWidth.Width, 2F);
- }
-
- [Fact]
- public void Rendering2TabsAtStartOfLineTooShort()
- {
- Font font = CreateFont("\t x");
- FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
- FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
- FontRectangle tabWithXWidth = TextMeasurer.MeasureBounds("\t\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
+ TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor };
+ FontRectangle xWidth = TextMeasurer.MeasureBounds("x", options);
+ FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", options);
+ FontRectangle tabWithXWidth = TextMeasurer.MeasureBounds("\tx", options);
Assert.Equal(tabWidth.Width + xWidth.Width, tabWithXWidth.Width, 2F);
}
@@ -34,27 +24,17 @@ public void Rendering2TabsAtStartOfLineTooShort()
public void TwoTabsAreDoubleWidthOfOneTab()
{
Font font = CreateFont("\t x");
- FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
- FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
- FontRectangle twoTabWidth = TextMeasurer.MeasureBounds("\t\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
-
- Assert.Equal(twoTabWidth.Width, tabWidth.Width * 2, 2F);
- }
-
- [Fact]
- public void TwoTabsAreDoubleWidthOfOneTabMinusXWidth()
- {
- Font font = CreateFont("\t x");
- FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
- FontRectangle tabWidth = TextMeasurer.MeasureBounds("\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
- FontRectangle twoTabWidth = TextMeasurer.MeasureBounds("\t\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
+ TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor };
+ FontRectangle xWidth = TextMeasurer.MeasureBounds("x", options);
+ FontRectangle tabWithXWidth = TextMeasurer.MeasureBounds("\tx", options);
+ FontRectangle tabTabWithXWidth = TextMeasurer.MeasureBounds("\t\tx", options);
- Assert.Equal(twoTabWidth.Width - xWidth.Width, (tabWidth.Width - xWidth.Width) * 2, 2F);
+ Assert.Equal(tabTabWithXWidth.Width - xWidth.Width, 2 * (tabWithXWidth.Width - xWidth.Width), 2F);
}
public static Font CreateFont(string text)
{
- var fc = (IFontMetricsCollection)new FontCollection();
+ IFontMetricsCollection fc = new FontCollection();
Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12);
return new Font(d, 1F);
}
diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_36.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_36.cs
index ede609a9..9b849369 100644
--- a/tests/SixLabors.Fonts.Tests/Issues/Issues_36.cs
+++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_36.cs
@@ -14,15 +14,16 @@ public class Issues_36
[InlineData(3)]
[InlineData(4)]
[InlineData(5)]
- public void TextWidthForTabOnlyTextShouldBeSingleTabWidthMultipliedByTabCount(int tabCount)
+ public void TextWidthForTabOnlyTextShouldBeSingleTabWidth(int tabCount)
{
- Font font = CreateFont("\t x");
+ Font font = CreateFont("\t");
+ TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor };
- FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
+ FontRectangle tabWidth = TextMeasurer.MeasureBounds("\t", options);
string tabString = string.Empty.PadRight(tabCount, '\t');
- FontRectangle tabCountWidth = TextMeasurer.MeasureBounds(tabString, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
+ FontRectangle tabCountWidth = TextMeasurer.MeasureBounds(tabString, options);
- Assert.Equal(tabWidth.Width * tabCount, tabCountWidth.Width, 2F);
+ Assert.Equal(tabWidth.Width, tabCountWidth.Width, 2F);
}
[Theory]
@@ -35,10 +36,11 @@ public void TextWidthForTabOnlyTextShouldBeSingleTabWidthMultipliedByTabCountMin
{
Font font = CreateFont("\t x");
- FontRectangle xWidth = TextMeasurer.MeasureBounds("x", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
- FontRectangle tabWidth = TextMeasurer.MeasureBounds("\tx", new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
+ TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor };
+ FontRectangle xWidth = TextMeasurer.MeasureBounds("x", options);
+ FontRectangle tabWidth = TextMeasurer.MeasureBounds("\tx", options);
string tabString = "x".PadLeft(tabCount + 1, '\t');
- FontRectangle tabCountWidth = TextMeasurer.MeasureBounds(tabString, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor });
+ FontRectangle tabCountWidth = TextMeasurer.MeasureBounds(tabString, options);
float singleTabWidth = tabWidth.Width - xWidth.Width;
float finalTabWidth = tabCountWidth.Width - xWidth.Width;
@@ -47,7 +49,7 @@ public void TextWidthForTabOnlyTextShouldBeSingleTabWidthMultipliedByTabCountMin
public static Font CreateFont(string text)
{
- var fc = (IFontMetricsCollection)new FontCollection();
+ IFontMetricsCollection fc = new FontCollection();
Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12);
return new Font(d, 1F);
}
diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs
index fd9ec3cf..60d5ca06 100644
--- a/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs
+++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs
@@ -22,8 +22,10 @@ public void RenderingTextIncludesAllGlyphs()
.AppendLine(" NEWS_CATEGORY=EWF&NEWS_HASH=4b298ff9277ef9fdf515356be95ea3caf57cd36&OFFSET=0&SEARCH_VALUE=CA88105E1088&ID_NEWS")
.Append(" ");
+ TextLayoutTestUtilities.TestLayout(stringBuilder.ToString(), options);
+
int lineCount = TextMeasurer.CountLines(stringBuilder.ToString(), options);
- Assert.Equal(2, lineCount);
+ Assert.Equal(4, lineCount);
#endif
}
}
diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs
index 01cc6bee..6475667d 100644
--- a/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs
+++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs
@@ -8,7 +8,7 @@ namespace SixLabors.Fonts.Tests.Issues;
public class Issues_434
{
[Theory]
- [InlineData("- Lorem ipsullll\n\ndolor sit amet\n-consectetur elit", 4)]
+ [InlineData("- Lorem ipsullll\n\ndolor sit amet\n-consectetur elit", 5)]
public void ShouldInsertExtraLineBreaksA(string text, int expectedLineCount)
{
if (SystemFonts.TryGet("Arial", out FontFamily family))
@@ -20,20 +20,18 @@ public void ShouldInsertExtraLineBreaksA(string text, int expectedLineCount)
WrappingLength = 400,
};
- // Line count includes rendered lines only.
- // Line breaks cause offsetting of subsequent lines.
+ TextLayoutTestUtilities.TestLayout(text, options, properties: expectedLineCount);
+
int lineCount = TextMeasurer.CountLines(text, options);
Assert.Equal(expectedLineCount, lineCount);
IReadOnlyList layout = TextLayout.GenerateLayout(text, options);
- Assert.Equal(46, layout.Count);
-
- TextLayoutTestUtilities.TestLayout(text, options, properties: expectedLineCount);
+ Assert.Equal(47, layout.Count);
}
}
[Theory]
- [InlineData("- Lorem ipsullll\n\n\ndolor sit amet\n-consectetur elit", 4)]
+ [InlineData("- Lorem ipsullll\n\n\ndolor sit amet\n-consectetur elit", 6)]
public void ShouldInsertExtraLineBreaksB(string text, int expectedLineCount)
{
if (SystemFonts.TryGet("Arial", out FontFamily family))
@@ -45,15 +43,13 @@ public void ShouldInsertExtraLineBreaksB(string text, int expectedLineCount)
WrappingLength = 400,
};
- // Line count includes rendered lines only.
- // Line breaks cause offsetting of subsequent lines.
+ TextLayoutTestUtilities.TestLayout(text, options, properties: expectedLineCount);
+
int lineCount = TextMeasurer.CountLines(text, options);
Assert.Equal(expectedLineCount, lineCount);
IReadOnlyList layout = TextLayout.GenerateLayout(text, options);
- Assert.Equal(46, layout.Count);
-
- TextLayoutTestUtilities.TestLayout(text, options, properties: expectedLineCount);
+ Assert.Equal(48, layout.Count);
}
}
}
diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_441.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_441.cs
new file mode 100644
index 00000000..39567162
--- /dev/null
+++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_441.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Six Labors.
+// Licensed under the Six Labors Split License.
+
+using System.Numerics;
+
+namespace SixLabors.Fonts.Tests.Issues;
+
+public class Issues_441
+{
+ [Fact]
+ public void LineWrappingWithExplicitNewLine()
+ {
+ if (SystemFonts.TryGet("Arial", out FontFamily family))
+ {
+ Font font = family.CreateFont(80);
+
+ TextLayoutTestUtilities.TestLayout(
+ "What connects the following:\nx",
+ new TextOptions(font)
+ {
+ Origin = new Vector2(50, 20),
+ WrappingLength = 800
+ });
+ }
+ }
+
+ [Fact]
+ public void LineWrappingWithImplicitNewLine()
+ {
+ if (SystemFonts.TryGet("Arial", out FontFamily family))
+ {
+ Font font = family.CreateFont(80);
+
+ TextLayoutTestUtilities.TestLayout(
+ "What connects the following: x",
+ new TextOptions(font)
+ {
+ Origin = new Vector2(50, 20),
+ WrappingLength = 800
+ });
+ }
+ }
+}