Skip to content

Commit 24871a8

Browse files
Merge pull request #440 from SixLabors/js/additional-linebreak-fixes
Fix calculation of wrapping advance
2 parents 6de8c5c + 371e95a commit 24871a8

17 files changed

+101
-67
lines changed

src/SixLabors.Fonts/TextLayout.cs

+27-21
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,8 @@ private static TextBox BreakLines(
894894
LayoutMode layoutMode)
895895
{
896896
bool shouldWrap = options.WrappingLength > 0;
897+
898+
// Wrapping length is always provided in pixels. Convert to inches for comparison.
897899
float wrappingLength = shouldWrap ? options.WrappingLength / options.Dpi : float.MaxValue;
898900
bool breakAll = options.WordBreaking == WordBreaking.BreakAll;
899901
bool keepAll = options.WordBreaking == WordBreaking.KeepAll;
@@ -904,7 +906,6 @@ private static TextBox BreakLines(
904906

905907
int graphemeIndex;
906908
int codePointIndex = 0;
907-
float lineAdvance = 0;
908909
List<TextLine> textLines = new();
909910
TextLine textLine = new();
910911
int stringIndex = 0;
@@ -1073,7 +1074,7 @@ VerticalOrientationType.Rotate or
10731074
}
10741075
}
10751076

1076-
// Now scale the advance.
1077+
// Now scale the advance. We use inches for comparison.
10771078
if (isHorizontalLayout || shouldRotate)
10781079
{
10791080
float scaleAX = pointSize / glyph.ScaleFactor.X;
@@ -1115,7 +1116,6 @@ VerticalOrientationType.Rotate or
11151116
descender -= delta;
11161117

11171118
// Add our metrics to the line.
1118-
lineAdvance += decomposedAdvance;
11191119
textLine.Add(
11201120
isDecomposed ? new GlyphMetrics[] { metric } : metrics,
11211121
pointSize,
@@ -1153,58 +1153,60 @@ VerticalOrientationType.Rotate or
11531153
int maxLineBreakIndex = lineBreaks.Count - 1;
11541154
LineBreak lastLineBreak = lineBreaks[lineBreakIndex];
11551155
LineBreak currentLineBreak = lineBreaks[lineBreakIndex];
1156+
float lineAdvance = 0;
11561157

1157-
lineAdvance = 0;
11581158
for (int i = 0; i < textLine.Count; i++)
11591159
{
11601160
int max = textLine.Count - 1;
11611161
TextLine.GlyphLayoutData glyph = textLine[i];
11621162
codePointIndex = glyph.CodePointIndex;
11631163
int graphemeCodePointIndex = glyph.GraphemeCodePointIndex;
1164-
float glyphAdvance = glyph.ScaledAdvance;
1165-
lineAdvance += glyphAdvance;
11661164

11671165
if (graphemeCodePointIndex == 0 && textLine.Count > 0)
11681166
{
1167+
lineAdvance += glyph.ScaledAdvance;
1168+
11691169
if (codePointIndex == currentLineBreak.PositionWrap && currentLineBreak.Required)
11701170
{
11711171
// Mandatory line break at index.
11721172
TextLine remaining = textLine.SplitAt(i);
11731173
textLines.Add(textLine.Finalize(options));
11741174
textLine = remaining;
1175-
i = 0;
1175+
i = -1;
11761176
lineAdvance = 0;
11771177
}
11781178
else if (shouldWrap)
11791179
{
1180-
float currentAdvance = lineAdvance + glyphAdvance;
1181-
if (currentAdvance >= wrappingLength)
1180+
if (lineAdvance >= wrappingLength)
11821181
{
11831182
if (breakAll)
11841183
{
1185-
// Insert a forced break at this index.
1184+
// Insert a forced break.
11861185
TextLine remaining = textLine.SplitAt(i);
1187-
textLines.Add(textLine.Finalize(options));
1188-
textLine = remaining;
1189-
i = 0;
1190-
lineAdvance = 0;
1186+
if (remaining != textLine)
1187+
{
1188+
textLines.Add(textLine.Finalize(options));
1189+
textLine = remaining;
1190+
i = -1;
1191+
lineAdvance = 0;
1192+
}
11911193
}
11921194
else if (codePointIndex == currentLineBreak.PositionWrap || i == max)
11931195
{
1194-
LineBreak lineBreak = currentAdvance == wrappingLength
1196+
LineBreak lineBreak = lineAdvance == wrappingLength
11951197
? currentLineBreak
11961198
: lastLineBreak;
11971199

11981200
if (i > 0)
11991201
{
12001202
// If the current break is a space, and the line minus the space
12011203
// is less than the wrapping length, we can break using the current break.
1202-
float positionAdvance = lineAdvance;
1204+
float previousAdvance = lineAdvance - (float)glyph.ScaledAdvance;
12031205
TextLine.GlyphLayoutData lastGlyph = textLine[i - 1];
12041206
if (CodePoint.IsWhiteSpace(lastGlyph.CodePoint))
12051207
{
1206-
positionAdvance -= lastGlyph.ScaledAdvance;
1207-
if (positionAdvance <= wrappingLength)
1208+
previousAdvance -= lastGlyph.ScaledAdvance;
1209+
if (previousAdvance <= wrappingLength)
12081210
{
12091211
lineBreak = currentLineBreak;
12101212
}
@@ -1220,7 +1222,7 @@ VerticalOrientationType.Rotate or
12201222
{
12211223
if (breakWord)
12221224
{
1223-
// If the line is too long, insert a forced line break.
1225+
// If the line is too long, insert a forced break.
12241226
if (textLine.ScaledLineAdvance > wrappingLength)
12251227
{
12261228
TextLine overflow = textLine.SplitAt(wrappingLength);
@@ -1233,7 +1235,7 @@ VerticalOrientationType.Rotate or
12331235

12341236
textLines.Add(textLine.Finalize(options));
12351237
textLine = remaining;
1236-
i = 0;
1238+
i = -1;
12371239
lineAdvance = 0;
12381240
}
12391241
}
@@ -1326,7 +1328,11 @@ public void Add(
13261328
{
13271329
// Reset metrics.
13281330
// We track the maximum metrics for each line to ensure glyphs can be aligned.
1329-
this.ScaledLineAdvance += scaledAdvance;
1331+
if (graphemeIndex == 0)
1332+
{
1333+
this.ScaledLineAdvance += scaledAdvance;
1334+
}
1335+
13301336
this.ScaledMaxLineHeight = MathF.Max(this.ScaledMaxLineHeight, scaledLineHeight);
13311337
this.ScaledMaxAscender = MathF.Max(this.ScaledMaxAscender, scaledAscender);
13321338
this.ScaledMaxDescender = MathF.Max(this.ScaledMaxDescender, scaledDescender);
Loading

tests/Images/ReferenceOutput/CountLinesWrappingLength_100-4.png

-3
This file was deleted.
Loading

tests/Images/ReferenceOutput/CountLinesWrappingLength_50-5.png

-3
This file was deleted.

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

+24
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,37 @@ public void ShouldNotInsertExtraLineBreaks()
2121
WrappingLength = 400,
2222
};
2323

24+
TextLayoutTestUtilities.TestLayout(text, options);
25+
2426
int lineCount = TextMeasurer.CountLines(text, options);
2527
Assert.Equal(4, lineCount);
2628

2729
IReadOnlyList<GlyphLayout> layout = TextLayout.GenerateLayout(text, options);
2830
Assert.Equal(46, layout.Count);
31+
}
32+
}
33+
34+
[Fact]
35+
public void ShouldNotInsertExtraLineBreaks_2()
36+
{
37+
if (SystemFonts.TryGet("Arial", out FontFamily family))
38+
{
39+
Font font = family.CreateFont(60);
40+
const string text = "- Lorem ipsullll dolor sit amet\n-consectetur elit";
41+
42+
TextOptions options = new(font)
43+
{
44+
Origin = new Vector2(50, 20),
45+
WrappingLength = 400,
46+
};
2947

3048
TextLayoutTestUtilities.TestLayout(text, options);
49+
50+
int lineCount = TextMeasurer.CountLines(text, options);
51+
Assert.Equal(4, lineCount);
52+
53+
IReadOnlyList<GlyphLayout> layout = TextLayout.GenerateLayout(text, options);
54+
Assert.Equal(46, layout.Count);
3155
}
3256
}
3357
}

tests/SixLabors.Fonts.Tests/TextLayoutTests.cs

+23-22
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Globalization;
55
using System.Numerics;
6+
using System.Text;
67
using SixLabors.Fonts.Tests.Fakes;
78
using SixLabors.Fonts.Unicode;
89
using SixLabors.ImageSharp.Drawing.Processing;
@@ -289,11 +290,11 @@ public void MeasureTextWordWrappingHorizontalTopBottom(string text, float height
289290
LayoutMode = LayoutMode.HorizontalTopBottom
290291
};
291292

292-
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
293+
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
293294

295+
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
294296
Assert.Equal(width, size.Width, 4F);
295297
Assert.Equal(height, size.Height, 4F);
296-
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
297298
}
298299
}
299300

@@ -315,11 +316,12 @@ public void MeasureTextWordWrappingHorizontalBottomTop(string text, float height
315316
LayoutMode = LayoutMode.HorizontalBottomTop
316317
};
317318

318-
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
319+
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
320+
319321

322+
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
320323
Assert.Equal(width, size.Width, 4F);
321324
Assert.Equal(height, size.Height, 4F);
322-
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
323325
}
324326
}
325327

@@ -338,11 +340,11 @@ public void MeasureTextWordWrappingVerticalLeftRight(string text, float height,
338340
LayoutMode = LayoutMode.VerticalLeftRight
339341
};
340342

341-
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
343+
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
342344

345+
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
343346
Assert.Equal(width, size.Width, 4F);
344347
Assert.Equal(height, size.Height, 4F);
345-
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
346348
}
347349
}
348350

@@ -361,11 +363,11 @@ public void MeasureTextWordWrappingVerticalRightLeft(string text, float height,
361363
LayoutMode = LayoutMode.VerticalRightLeft
362364
};
363365

364-
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
366+
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
365367

368+
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
366369
Assert.Equal(width, size.Width, 4F);
367370
Assert.Equal(height, size.Height, 4F);
368-
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
369371
}
370372
}
371373

@@ -384,11 +386,11 @@ public void MeasureTextWordWrappingVerticalMixedLeftRight(string text, float hei
384386
LayoutMode = LayoutMode.VerticalMixedLeftRight
385387
};
386388

387-
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
389+
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
388390

391+
FontRectangle size = TextMeasurer.MeasureBounds(text, options);
389392
Assert.Equal(width, size.Width, 4F);
390393
Assert.Equal(height, size.Height, 4F);
391-
TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width });
392394
}
393395
}
394396

@@ -416,14 +418,11 @@ public void MeasureTextWordBreakMatchesMDN(string text, LayoutMode layoutMode, W
416418
FallbackFontFamilies = new[] { jhengHei }
417419
};
418420

419-
FontRectangle size = TextMeasurer.MeasureAdvance(
420-
text,
421-
options);
421+
TextLayoutTestUtilities.TestLayout(text, options, properties: new { layoutMode, wordBreaking });
422422

423+
FontRectangle size = TextMeasurer.MeasureAdvance(text, options);
423424
Assert.Equal(width, size.Width, 4F);
424425
Assert.Equal(height, size.Height, 4F);
425-
426-
TextLayoutTestUtilities.TestLayout(text, options, properties: new { layoutMode, wordBreaking });
427426
}
428427
}
429428

@@ -455,10 +454,10 @@ public void MeasureTextWordBreak(string text, LayoutMode layoutMode, WordBreakin
455454
text,
456455
options);
457456

457+
TextLayoutTestUtilities.TestLayout(text, options, properties: new { layoutMode, wordBreaking });
458+
458459
Assert.Equal(width, size.Width, 4F);
459460
Assert.Equal(height, size.Height, 4F);
460-
461-
TextLayoutTestUtilities.TestLayout(text, options, properties: new { layoutMode, wordBreaking });
462461
}
463462
}
464463

@@ -550,8 +549,8 @@ public void CountLinesWithSpan()
550549

551550
[Theory]
552551
[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 25, 6)]
553-
[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 50, 5)]
554-
[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 100, 4)]
552+
[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 50, 4)]
553+
[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 100, 3)]
555554
[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 200, 3)]
556555
public void CountLinesWrappingLength(string text, int wrappingLength, int usedLines)
557556
{
@@ -561,9 +560,10 @@ public void CountLinesWrappingLength(string text, int wrappingLength, int usedLi
561560
WrappingLength = wrappingLength
562561
};
563562

563+
TextLayoutTestUtilities.TestLayout(text, options, properties: usedLines);
564+
564565
int count = TextMeasurer.CountLines(text, options);
565566
Assert.Equal(usedLines, count);
566-
TextLayoutTestUtilities.TestLayout(text, options, properties: usedLines);
567567
}
568568

569569
[Fact]
@@ -1217,10 +1217,11 @@ public void BreakWordEnsuresSingleCharacterPerLine()
12171217
};
12181218

12191219
const string text = "Hello World!";
1220-
int lineCount = TextMeasurer.CountLines(text, options);
1221-
Assert.Equal(text.Length - 1, lineCount);
12221220

12231221
TextLayoutTestUtilities.TestLayout(text, options);
1222+
1223+
int lineCount = TextMeasurer.CountLines(text, options);
1224+
Assert.Equal(text.Length - 1, lineCount);
12241225
}
12251226

12261227
private class CaptureGlyphBoundBuilder : IGlyphRenderer

0 commit comments

Comments
 (0)