diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index a8bca186..526c5d69 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -1179,7 +1179,28 @@ VerticalOrientationType.Rotate or List lineBreaks = new(); while (lineBreakEnumerator.MoveNext()) { - lineBreaks.Add(lineBreakEnumerator.Current); + // URLs are now so common in regular plain text that they need to be taken into account when + // assigning general-purpose line breaking properties. + // + // To handle this we disallow breaks after solidus (U+002F) entirely. + // Testing seems to indicate Chrome and other browsers do this as well. + // + // We do this outside of the line breaker so that the expected results from the Unicode + // tests are not affected. + // https://www.unicode.org/reports/tr14/#SY + LineBreak current = lineBreakEnumerator.Current; + int i = current.PositionMeasure; + if (i < textLine.Count) + { + CodePoint c = textLine[i].CodePoint; + CodePoint p = textLine[Math.Max(0, i - 1)].CodePoint; + if (c.Value == 0x002F || p.Value == 0x002F) + { + continue; + } + } + + lineBreaks.Add(current); } int processed = 0; diff --git a/src/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs b/src/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs index 98beaaf9..1d81a2be 100644 --- a/src/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs +++ b/src/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs @@ -347,11 +347,52 @@ private bool GetPairTableBreak(LineBreakClass lastClass) } // LB25 - if (this.lb25ex && (this.nextClass == LineBreakClass.PR || this.nextClass == LineBreakClass.NU)) + if (this.lb25ex) { - shouldBreak = true; - this.lb25ex = false; - break; + switch (lastClass) + { + case LineBreakClass.PO: + case LineBreakClass.PR: + if (this.currentClass == LineBreakClass.NU) + { + this.lb25ex = false; + return false; + } + + LineBreakClass ahead = this.PeekNextCharClass(); + if (ahead == LineBreakClass.NU && this.nextClass is LineBreakClass.OP or LineBreakClass.HY) + { + this.lb25ex = false; + return false; + } + + break; + case LineBreakClass.HY: + case LineBreakClass.OP: + if (this.currentClass == LineBreakClass.NU) + { + this.lb25ex = false; + return false; + } + + break; + + case LineBreakClass.NU: + if (this.currentClass is LineBreakClass.PO or LineBreakClass.PR or LineBreakClass.NU) + { + this.lb25ex = false; + return false; + } + + break; + } + + if (this.nextClass is LineBreakClass.PR or LineBreakClass.NU) + { + shouldBreak = true; + this.lb25ex = false; + break; + } } // LB24 diff --git a/tests/Images/ReferenceOutput/Issue_448__WrappingLength_150_.png b/tests/Images/ReferenceOutput/Issue_448__WrappingLength_150_.png new file mode 100644 index 00000000..d103d39c --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_448__WrappingLength_150_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5310d037cd18198ddf0d70c97754d45f5917236859b28b13a306e0ccbeb20046 +size 2578 diff --git a/tests/Images/ReferenceOutput/Issue_450__WrappingLength_960_.png b/tests/Images/ReferenceOutput/Issue_450__WrappingLength_960_.png new file mode 100644 index 00000000..3fb21b44 --- /dev/null +++ b/tests/Images/ReferenceOutput/Issue_450__WrappingLength_960_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c5b081acda3b2754a29ffd499c2571e285b1efe01bf5e6b793c8ec8c00848e1 +size 19688 diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_448.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_448.cs new file mode 100644 index 00000000..5ef9695a --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_448.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tests.Issues; +public class Issues_448 +{ + [Fact] + public void Issue_448() + { + if (SystemFonts.TryGet("Arial", out FontFamily family)) + { + Font font = family.CreateFont(20); + + TextLayoutTestUtilities.TestLayout( + "aaaaa bbbbb/ccccc ddddd", + new TextOptions(font) + { + WrappingLength = 150 + }); + } + } +} diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_450.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_450.cs new file mode 100644 index 00000000..2d15c0e6 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_450.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Tests.Issues; +public class Issues_450 +{ + [Fact] + public void Issue_450() + { + if (SystemFonts.TryGet("Arial", out FontFamily family)) + { + Font font = family.CreateFont(92); + + TextLayoutTestUtilities.TestLayout( + "Super, Smash Bros (1999)", + new TextOptions(font) + { + Origin = new Vector2(50, 20), + WrappingLength = 960, + }); + } + } +} diff --git a/tests/SixLabors.Fonts.Tests/Unicode/LineBreakEnumeratorTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/LineBreakEnumeratorTests.cs index 637c107e..c013797a 100644 --- a/tests/SixLabors.Fonts.Tests/Unicode/LineBreakEnumeratorTests.cs +++ b/tests/SixLabors.Fonts.Tests/Unicode/LineBreakEnumeratorTests.cs @@ -46,6 +46,43 @@ public void BasicLatinTest() Assert.False(lineBreaker.MoveNext()); } + [Fact] + public void NumericTests() + { + const string text1 = "Super Smash Bros (1999)"; + const string text2 = "Super, Smash Bros (1999)"; + List breaks1 = new(); + + foreach (LineBreak lineBreak in new LineBreakEnumerator(text1.AsSpan())) + { + breaks1.Add(lineBreak); + } + + Assert.Equal(5, breaks1[0].PositionMeasure); + Assert.Equal(6, breaks1[0].PositionWrap); + Assert.Equal(11, breaks1[1].PositionMeasure); + Assert.Equal(12, breaks1[1].PositionWrap); + Assert.Equal(16, breaks1[2].PositionMeasure); + Assert.Equal(17, breaks1[2].PositionWrap); + Assert.Equal(23, breaks1[3].PositionMeasure); + Assert.Equal(23, breaks1[3].PositionWrap); + + List breaks2 = new(); + foreach (LineBreak lineBreak in new LineBreakEnumerator(text2.AsSpan())) + { + breaks2.Add(lineBreak); + } + + Assert.Equal(6, breaks2[0].PositionMeasure); + Assert.Equal(7, breaks2[0].PositionWrap); + Assert.Equal(12, breaks2[1].PositionMeasure); + Assert.Equal(13, breaks2[1].PositionWrap); + Assert.Equal(17, breaks2[2].PositionMeasure); + Assert.Equal(18, breaks2[2].PositionWrap); + Assert.Equal(24, breaks2[3].PositionMeasure); + Assert.Equal(24, breaks2[3].PositionWrap); + } + [Fact] public void ForwardTextWithOuterWhitespace() {