From ba825420809cb9534e0962ccd1e15201594aedbc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 18 Dec 2024 23:04:27 +1000 Subject: [PATCH 01/11] Refactor line breaking --- src/SixLabors.Fonts/TextLayout.cs | 98 +++++-------------- .../SixLabors.Fonts.Tests/Issues/Issues_33.cs | 4 +- .../Issues/Issues_431.cs | 4 +- .../Issues/Issues_434.cs | 31 ++++++ .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 33 +++++++ 5 files changed, 91 insertions(+), 79 deletions(-) create mode 100644 tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 976b00ff..9085bd58 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -910,6 +910,7 @@ private static TextBox BreakLines( } int lineBreakIndex = 0; + int maxLineBreakIndex = lineBreaks.Count - 1; LineBreak lastLineBreak = lineBreaks[lineBreakIndex]; LineBreak currentLineBreak = lineBreaks[lineBreakIndex]; int graphemeIndex; @@ -1089,7 +1090,6 @@ VerticalOrientationType.Rotate or { float scaleAX = pointSize / glyph.ScaleFactor.X; glyphAdvance *= scaleAX; - for (int i = 0; i < decomposedAdvances.Length; i++) { decomposedAdvances[i] *= scaleAX; @@ -1106,71 +1106,43 @@ VerticalOrientationType.Rotate or } // Should we start a new line? - bool requiredBreak = false; - if (graphemeCodePointIndex == 0) + if (graphemeCodePointIndex == 0 && textLine.Count > 0) { - // Mandatory wrap at index. - if (currentLineBreak.PositionWrap == codePointIndex && currentLineBreak.Required) + if (codePointIndex == currentLineBreak.PositionWrap && currentLineBreak.Required) { + // Mandatory line break at index. textLines.Add(textLine.Finalize()); glyphCount += textLine.Count; textLine = new(); lineAdvance = 0; - requiredBreak = true; } else if (shouldWrap && lineAdvance + glyphAdvance >= wrappingLength) { - // Forced wordbreak - if (breakAll && textLine.Count > 0) + if (breakAll) { + // Insert a forced break at this index. textLines.Add(textLine.Finalize()); glyphCount += textLine.Count; textLine = new(); lineAdvance = 0; } - else if (currentLineBreak.PositionMeasure == codePointIndex) - { - // Exact length match. Check for CJK - if (keepAll) - { - TextLine split = textLine.SplitAt(lastLineBreak, keepAll); - if (split != textLine) - { - textLines.Add(textLine.Finalize()); - textLine = split; - lineAdvance = split.ScaledLineAdvance; - } - } - else if (textLine.Count > 0) - { - textLines.Add(textLine.Finalize()); - glyphCount += textLine.Count; - textLine = new(); - lineAdvance = 0; - } - } - else if (currentLineBreak.PositionWrap == codePointIndex) + else if (codePointIndex == currentLineBreak.PositionWrap) { - // Exact length match. Check for CJK - TextLine split = textLine.SplitAt(currentLineBreak, keepAll); + // Split the line at the last line break. + // CJK characters will not be split if 'keepAll' is true. + TextLine split = textLine.SplitAt(lastLineBreak, keepAll); if (split != textLine) { textLines.Add(textLine.Finalize()); textLine = split; lineAdvance = split.ScaledLineAdvance; } - else if (textLine.Count > 0) - { - textLines.Add(textLine.Finalize()); - textLine = new(); - lineAdvance = 0; - } } - else if (lastLineBreak.PositionWrap < codePointIndex && !CodePoint.IsWhiteSpace(codePoint)) + else if (breakWord) { - // Split the current text line into two at the last wrapping point if the current glyph - // does not represent whitespace. Whitespace characters will be correctly trimmed at the - // next iteration. + // We have to do more work here and check each exceeding codepoint. + // If we can split the line at the last line break, use that, otherwise + // we have to insert a break at the current index. TextLine split = textLine.SplitAt(lastLineBreak, keepAll); if (split != textLine) { @@ -1178,54 +1150,26 @@ VerticalOrientationType.Rotate or textLine = split; lineAdvance = split.ScaledLineAdvance; } - else if (breakWord && textLine.Count > 0) + else { + // Insert a forced break at this index. textLines.Add(textLine.Finalize()); glyphCount += textLine.Count; textLine = new(); lineAdvance = 0; } } - else if (breakWord && textLine.Count > 0) - { - textLines.Add(textLine.Finalize()); - glyphCount += textLine.Count; - textLine = new(); - lineAdvance = 0; - } } } // Find the next line break. - if (currentLineBreak.PositionWrap == codePointIndex) + if (lineBreakIndex < maxLineBreakIndex && + (currentLineBreak.PositionWrap == codePointIndex)) { lastLineBreak = currentLineBreak; currentLineBreak = lineBreaks[++lineBreakIndex]; } - // Do not start a line following a break with breaking whitespace - // unless the break was required. - if (textLine.Count == 0 - && textLines.Count > 0 - && !requiredBreak - && CodePoint.IsWhiteSpace(codePoint) - && !CodePoint.IsNonBreakingSpace(codePoint) - && !CodePoint.IsTabulation(codePoint) - && !CodePoint.IsNewLine(codePoint)) - { - codePointIndex++; - graphemeCodePointIndex++; - continue; - } - - if (textLine.Count > 0 && CodePoint.IsNewLine(codePoint)) - { - // Do not add new lines unless at position zero. - codePointIndex++; - graphemeCodePointIndex++; - continue; - } - // For non-decomposed glyphs the length is always 1. for (int i = 0; i < decomposedAdvances.Length; i++) { @@ -1467,7 +1411,11 @@ private void TrimTrailingWhitespaceAndRecalculateMetrics() this.ScaledMaxLineHeight = lineHeight; } - public TextLine Finalize() => this.BidiReOrder(); + public TextLine Finalize() + { + this.TrimTrailingWhitespaceAndRecalculateMetrics(); + return this.BidiReOrder(); + } public void Justify(TextOptions options) { diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs index c5c40b5a..6e7f5d14 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs @@ -13,8 +13,8 @@ public class Issues_33 [InlineData("\n\tHelloworld", 310, 10)] [InlineData("\tHelloworld", 310, 10)] [InlineData(" Helloworld", 340, 10)] - [InlineData("Hell owor ld\t", 390, 10)] - [InlineData("Helloworld ", 360, 10)] + [InlineData("Hell owor ld\t", 340, 10)] + [InlineData("Helloworld ", 280, 10)] public void WhiteSpaceAtStartOfLineNotMeasured(string text, float width, float height) { Font font = CreateFont(text); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs index ec47bbd4..5b3bcc6f 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs @@ -22,10 +22,10 @@ public void ShouldNotInsertExtraLineBreaks() }; int lineCount = TextMeasurer.CountLines(text, options); - Assert.Equal(4, lineCount); + Assert.Equal(3, lineCount); IReadOnlyList layout = TextLayout.GenerateLayout(text, options); - Assert.Equal(46, layout.Count); + Assert.Equal(47, layout.Count); } } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs new file mode 100644 index 00000000..e0a0f4ea --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts.Tests.Issues; + +public class Issues_434 +{ + [Theory] + [InlineData("- Lorem ipsullll\n\ndolor sit amet\n-consectetur elit", 3)] + [InlineData("- Lorem ipsullll\n\n\ndolor sit amet\n-consectetur elit", 3)] + public void ShouldNotInsertExtraLineBreaks(string text, int expectedLineCount) + { + if (SystemFonts.TryGet("Arial", out FontFamily family)) + { + Font font = family.CreateFont(60); + TextOptions options = new(font) + { + Origin = new Vector2(50, 20), + WrappingLength = 400, + }; + + int lineCount = TextMeasurer.CountLines(text, options); + Assert.Equal(expectedLineCount, lineCount); + + IReadOnlyList layout = TextLayout.GenerateLayout(text, options); + Assert.Equal(47, layout.Count); + } + } +} diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index dff3522a..51a86826 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -373,6 +373,39 @@ public void MeasureTextWordWrappingVerticalMixedLeftRight(string text, float hei } #if OS_WINDOWS + [Theory] + [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Standard, 100, 870)] + //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 120, 399)] + //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakWord, 120, 400)] + //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 60, 699)] + //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Standard, 101, 870)] + //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 121, 399)] + //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakWord, 121, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 61, 699)] + public void MeasureTextWordBreakMatchesMDN(string text, LayoutMode layoutMode, WordBreaking wordBreaking, float height, float width) + { + // Testing using Windows only to ensure that actual glyphs are rendered + // against known physically tested values. + FontFamily arial = SystemFonts.Get("Arial"); + FontFamily jhengHei = SystemFonts.Get("Microsoft JhengHei"); + + Font font = arial.CreateFont(16); + FontRectangle size = TextMeasurer.MeasureAdvance( + text, + new TextOptions(font) + { + Dpi = 96, + WrappingLength = 238, + LayoutMode = layoutMode, + WordBreaking = wordBreaking, + FallbackFontFamilies = new[] { jhengHei } + }); + + Assert.Equal(width, size.Width, 4F); + Assert.Equal(height, size.Height, 4F); + } + + [Theory] [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Standard, 100, 870)] [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 120, 399)] From a52a7a54af5e96320b7bcc0bba60a735344497ea Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 19 Dec 2024 21:52:20 +1000 Subject: [PATCH 02/11] Move line breaking after bidi --- src/SixLabors.Fonts/TextLayout.cs | 296 ++++++++++-------- ...Length_wrappingLength_100-usedLines_7_.png | 3 + ...Length_wrappingLength_200-usedLines_6_.png | 3 + ...gLength_wrappingLength_25-usedLines_7_.png | 3 + ...gLength_wrappingLength_50-usedLines_7_.png | 3 + .../ImageComparison/ExactImageComparer.cs | 58 ++++ ...ImageDifferenceIsOverThresholdException.cs | 63 ++++ .../ImageDimensionsMismatchException.cs | 20 ++ .../Exceptions/ImagesSimilarityException.cs | 14 + .../ImageComparison/ImageComparer.cs | 177 +++++++++++ .../ImageComparison/ImageSimilarityReport.cs | 110 +++++++ .../ImageComparison/PixelDifference.cs | 47 +++ .../ImageComparison/TestImageExtensions.cs | 82 +++++ .../ImageComparison/TolerantImageComparer.cs | 118 +++++++ .../SixLabors.Fonts.Tests.csproj | 1 + .../SixLabors.Fonts.Tests/TestEnvironment.cs | 53 +++- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 30 +- 17 files changed, 930 insertions(+), 151 deletions(-) create mode 100644 tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_100-usedLines_7_.png create mode 100644 tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_200-usedLines_6_.png create mode 100644 tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_25-usedLines_7_.png create mode 100644 tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_50-usedLines_7_.png create mode 100644 tests/SixLabors.Fonts.Tests/ImageComparison/ExactImageComparer.cs create mode 100644 tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs create mode 100644 tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImageDimensionsMismatchException.cs create mode 100644 tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImagesSimilarityException.cs create mode 100644 tests/SixLabors.Fonts.Tests/ImageComparison/ImageComparer.cs create mode 100644 tests/SixLabors.Fonts.Tests/ImageComparison/ImageSimilarityReport.cs create mode 100644 tests/SixLabors.Fonts.Tests/ImageComparison/PixelDifference.cs create mode 100644 tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs create mode 100644 tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 9085bd58..0274e2e8 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -901,24 +901,11 @@ private static TextBox BreakLines( bool isVerticalLayout = layoutMode.IsVertical(); bool isVerticalMixedLayout = layoutMode.IsVerticalMixed(); - // Calculate the position of potential line breaks. - LineBreakEnumerator lineBreakEnumerator = new(text); - List lineBreaks = new(); - while (lineBreakEnumerator.MoveNext()) - { - lineBreaks.Add(lineBreakEnumerator.Current); - } - - int lineBreakIndex = 0; - int maxLineBreakIndex = lineBreaks.Count - 1; - LineBreak lastLineBreak = lineBreaks[lineBreakIndex]; - LineBreak currentLineBreak = lineBreaks[lineBreakIndex]; int graphemeIndex; int codePointIndex = 0; float lineAdvance = 0; List textLines = new(); TextLine textLine = new(); - int glyphCount = 0; int stringIndex = 0; // No glyph should contain more than 64 metrics. @@ -1105,71 +1092,6 @@ VerticalOrientationType.Rotate or } } - // Should we start a new line? - if (graphemeCodePointIndex == 0 && textLine.Count > 0) - { - if (codePointIndex == currentLineBreak.PositionWrap && currentLineBreak.Required) - { - // Mandatory line break at index. - textLines.Add(textLine.Finalize()); - glyphCount += textLine.Count; - textLine = new(); - lineAdvance = 0; - } - else if (shouldWrap && lineAdvance + glyphAdvance >= wrappingLength) - { - if (breakAll) - { - // Insert a forced break at this index. - textLines.Add(textLine.Finalize()); - glyphCount += textLine.Count; - textLine = new(); - lineAdvance = 0; - } - else if (codePointIndex == currentLineBreak.PositionWrap) - { - // Split the line at the last line break. - // CJK characters will not be split if 'keepAll' is true. - TextLine split = textLine.SplitAt(lastLineBreak, keepAll); - if (split != textLine) - { - textLines.Add(textLine.Finalize()); - textLine = split; - lineAdvance = split.ScaledLineAdvance; - } - } - else if (breakWord) - { - // We have to do more work here and check each exceeding codepoint. - // If we can split the line at the last line break, use that, otherwise - // we have to insert a break at the current index. - TextLine split = textLine.SplitAt(lastLineBreak, keepAll); - if (split != textLine) - { - textLines.Add(textLine.Finalize()); - textLine = split; - lineAdvance = split.ScaledLineAdvance; - } - else - { - // Insert a forced break at this index. - textLines.Add(textLine.Finalize()); - glyphCount += textLine.Count; - textLine = new(); - lineAdvance = 0; - } - } - } - } - - // Find the next line break. - if (lineBreakIndex < maxLineBreakIndex && - (currentLineBreak.PositionWrap == codePointIndex)) - { - lastLineBreak = currentLineBreak; - currentLineBreak = lineBreaks[++lineBreakIndex]; - } - // For non-decomposed glyphs the length is always 1. for (int i = 0; i < decomposedAdvances.Length; i++) { @@ -1203,6 +1125,7 @@ VerticalOrientationType.Rotate or bidiRuns[bidiMap[codePointIndex]], graphemeIndex, codePointIndex, + graphemeCodePointIndex, shouldRotate || shouldOffset, isDecomposed, stringIndex); @@ -1215,6 +1138,93 @@ VerticalOrientationType.Rotate or stringIndex += graphemeEnumerator.Current.Length; } + // Resolve the bidi order for the line. + // This reorders the glyphs in the line to match the visual order. + textLine.BidiReOrder(); + + // Now we need to loop through our reordered line and split it at any line breaks. + // + // First calculate the position of potential line breaks. + LineBreakEnumerator lineBreakEnumerator = new(text); + List lineBreaks = new(); + while (lineBreakEnumerator.MoveNext()) + { + lineBreaks.Add(lineBreakEnumerator.Current); + } + + // Then split the line at the line breaks. + int lineBreakIndex = 0; + int maxLineBreakIndex = lineBreaks.Count - 1; + LineBreak lastLineBreak = lineBreaks[lineBreakIndex]; + LineBreak currentLineBreak = lineBreaks[lineBreakIndex]; + + lineAdvance = 0; + for (int i = 0; i < textLine.Count; i++) + { + int max = textLine.Count - 1; + TextLine.GlyphLayoutData glyph = textLine[i]; + codePointIndex = glyph.CodePointIndex; + int graphemeCodePointIndex = glyph.GraphemeCodePointIndex; + float glyphAdvance = glyph.ScaledAdvance; + lineAdvance += glyphAdvance; + + if (graphemeCodePointIndex == 0 && textLine.Count > 0) + { + if (codePointIndex == currentLineBreak.PositionWrap && currentLineBreak.Required) + { + // Mandatory line break at index. + TextLine remaining = textLine.SplitAt(i); + textLines.Add(textLine.Finalize()); + textLine = remaining; + i = 0; + lineAdvance = 0; + } + else if (shouldWrap && lineAdvance + glyphAdvance >= wrappingLength) + { + if (breakAll) + { + // Insert a forced break at this index. + TextLine remaining = textLine.SplitAt(i); + textLines.Add(textLine.Finalize()); + textLine = remaining; + i = 0; + lineAdvance = 0; + } + else if (codePointIndex == currentLineBreak.PositionWrap || i == max) + { + // If we are at the position wrap we can break here. + // Split the line at the last line break. + // CJK characters will not be split if 'keepAll' is true. + TextLine remaining = textLine.SplitAt(lastLineBreak, keepAll); + if (remaining != textLine) + { + textLines.Add(textLine.Finalize()); + textLine = remaining; + i = 0; + lineAdvance = 0; + } + } + else if (breakWord) + { + // Insert a forced break at this index. + TextLine remaining = textLine.SplitAt(i); + textLines.Add(textLine.Finalize()); + textLine = remaining; + i = 0; + lineAdvance = 0; + } + } + } + + // Find the next line break. + if (lineBreakIndex < maxLineBreakIndex && + (currentLineBreak.PositionWrap == codePointIndex)) + { + lastLineBreak = currentLineBreak; + currentLineBreak = lineBreaks[++lineBreakIndex]; + } + } + // Add the final line. if (textLine.Count > 0) { @@ -1245,7 +1255,11 @@ public float ScaledMaxAdvance() internal sealed class TextLine { - private readonly List data = new(); + private readonly List data; + + public TextLine() => this.data = new(16); + + public TextLine(int capacity) => this.data = new(capacity); public int Count => this.data.Count; @@ -1268,7 +1282,8 @@ public void Add( float scaledDescender, BidiRun bidiRun, int graphemeIndex, - int offset, + int codePointIndex, + int graphemeCodePointIndex, bool isTransformed, bool isDecomposed, int stringIndex) @@ -1289,12 +1304,24 @@ public void Add( scaledDescender, bidiRun, graphemeIndex, - offset, + codePointIndex, + graphemeCodePointIndex, isTransformed, isDecomposed, stringIndex)); } + public TextLine SplitAt(int index) + { + TextLine result = new(); + result.data.AddRange(this.data.GetRange(index, this.data.Count - index)); + RecalculateLineMetrics(result); + + this.data.RemoveRange(index, this.data.Count - index); + RecalculateLineMetrics(this); + return result; + } + public TextLine SplitAt(LineBreak lineBreak, bool keepAll) { int index = this.data.Count; @@ -1302,8 +1329,7 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) while (index > 0) { glyphWrap = this.data[--index]; - - if (glyphWrap.Offset == lineBreak.PositionWrap) + if (glyphWrap.CodePointIndex == lineBreak.PositionWrap) { break; } @@ -1313,13 +1339,13 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) { // Now trim trailing whitespace from this line in the case of an exact // length line break (non CJK) - this.TrimTrailingWhitespaceAndRecalculateMetrics(); + RecalculateLineMetrics(this); return this; } // Word breaks should not be used for Chinese/Japanese/Korean (CJK) text // when word-breaking mode is keep-all. - if (keepAll && UnicodeUtility.IsCJKCodePoint((uint)glyphWrap.CodePoint.Value)) + if (!lineBreak.Required && keepAll && UnicodeUtility.IsCJKCodePoint((uint)glyphWrap.CodePoint.Value)) { // Loop through previous glyphs to see if there is // a non CJK codepoint we can break at. @@ -1337,48 +1363,35 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) { // Now trim trailing whitespace from this line in the case of an exact // length line break (non CJK) - this.TrimTrailingWhitespaceAndRecalculateMetrics(); + RecalculateLineMetrics(this); return this; } } // Create a new line ensuring we capture the initial metrics. - TextLine result = new(); - result.data.AddRange(this.data.GetRange(index, this.data.Count - index)); - - float advance = 0; - float ascender = 0; - float descender = 0; - float lineHeight = 0; - for (int i = 0; i < result.data.Count; i++) - { - GlyphLayoutData glyph = result.data[i]; - advance += glyph.ScaledAdvance; - ascender = MathF.Max(ascender, glyph.ScaledAscender); - descender = MathF.Max(descender, glyph.ScaledDescender); - lineHeight = MathF.Max(lineHeight, glyph.ScaledLineHeight); - } - - result.ScaledLineAdvance = advance; - result.ScaledMaxAscender = ascender; - result.ScaledMaxDescender = descender; - result.ScaledMaxLineHeight = lineHeight; + int count = this.data.Count - index; + TextLine result = new(count); + result.data.AddRange(this.data.GetRange(index, count)); + RecalculateLineMetrics(result); // Remove those items from this line. - this.data.RemoveRange(index, this.data.Count - index); + this.data.RemoveRange(index, count); // Now trim trailing whitespace from this line. - this.TrimTrailingWhitespaceAndRecalculateMetrics(); + RecalculateLineMetrics(this); + // this.TrimTrailingWhitespaceAndRecalculateMetrics(); return result; } - private void TrimTrailingWhitespaceAndRecalculateMetrics() + private TextLine TrimTrailingWhitespaceAndRecalculateMetrics() { int index = this.data.Count; while (index > 0) { - if (!CodePoint.IsWhiteSpace(this.data[index - 1].CodePoint)) + // Trim trailing breaking whitespace. + CodePoint point = this.data[index - 1].CodePoint; + if (!CodePoint.IsWhiteSpace(point) || CodePoint.IsNonBreakingSpace(point)) { break; } @@ -1391,31 +1404,12 @@ private void TrimTrailingWhitespaceAndRecalculateMetrics() this.data.RemoveRange(index, this.data.Count - index); } - // Lastly recalculate this line metrics. - float advance = 0; - float ascender = 0; - float descender = 0; - float lineHeight = 0; - for (int i = 0; i < this.data.Count; i++) - { - GlyphLayoutData glyph = this.data[i]; - advance += glyph.ScaledAdvance; - ascender = MathF.Max(ascender, glyph.ScaledAscender); - descender = MathF.Max(descender, glyph.ScaledDescender); - lineHeight = MathF.Max(lineHeight, glyph.ScaledLineHeight); - } - - this.ScaledLineAdvance = advance; - this.ScaledMaxAscender = ascender; - this.ScaledMaxDescender = descender; - this.ScaledMaxLineHeight = lineHeight; + RecalculateLineMetrics(this); + return this; } public TextLine Finalize() - { - this.TrimTrailingWhitespaceAndRecalculateMetrics(); - return this.BidiReOrder(); - } + => this.TrimTrailingWhitespaceAndRecalculateMetrics(); public void Justify(TextOptions options) { @@ -1489,7 +1483,7 @@ public void Justify(TextOptions options) } } - private TextLine BidiReOrder() + public void BidiReOrder() { // Build up the collection of ordered runs. BidiRun run = this.data[0].BidiRun; @@ -1539,7 +1533,7 @@ private TextLine BidiReOrder() if (max == 0 || (min == max && (max & 1) == 0)) { // Nothing to reverse. - return this; + return; } // Now apply the reversal and replace the original contents. @@ -1567,8 +1561,28 @@ private TextLine BidiReOrder() this.data.AddRange(current.AsSlice()); current = current.Next; } + } - return this; + private static void RecalculateLineMetrics(TextLine textLine) + { + // Lastly recalculate this line metrics. + float advance = 0; + float ascender = 0; + float descender = 0; + float lineHeight = 0; + for (int i = 0; i < textLine.Count; i++) + { + GlyphLayoutData glyph = textLine[i]; + advance += glyph.ScaledAdvance; + ascender = MathF.Max(ascender, glyph.ScaledAscender); + descender = MathF.Max(descender, glyph.ScaledDescender); + lineHeight = MathF.Max(lineHeight, glyph.ScaledLineHeight); + } + + textLine.ScaledLineAdvance = advance; + textLine.ScaledMaxAscender = ascender; + textLine.ScaledMaxDescender = descender; + textLine.ScaledMaxLineHeight = lineHeight; } /// @@ -1644,7 +1658,8 @@ public GlyphLayoutData( float scaledDescender, BidiRun bidiRun, int graphemeIndex, - int offset, + int codePointIndex, + int graphemeCodePointIndex, bool isTransformed, bool isDecomposed, int stringIndex) @@ -1657,7 +1672,8 @@ public GlyphLayoutData( this.ScaledDescender = scaledDescender; this.BidiRun = bidiRun; this.GraphemeIndex = graphemeIndex; - this.Offset = offset; + this.CodePointIndex = codePointIndex; + this.GraphemeCodePointIndex = graphemeCodePointIndex; this.IsTransformed = isTransformed; this.IsDecomposed = isDecomposed; this.StringIndex = stringIndex; @@ -1683,7 +1699,9 @@ public GlyphLayoutData( public int GraphemeIndex { get; } - public int Offset { get; } + public int GraphemeCodePointIndex { get; } + + public int CodePointIndex { get; } public bool IsTransformed { get; } @@ -1694,7 +1712,7 @@ public GlyphLayoutData( public readonly bool IsNewLine => CodePoint.IsNewLine(this.CodePoint); private readonly string DebuggerDisplay => FormattableString - .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {this.TextDirection} : {this.Offset}, level: {this.BidiRun.Level}"); + .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {this.TextDirection} : {this.CodePointIndex}, level: {this.BidiRun.Level}"); } private sealed class OrderedBidiRun diff --git a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_100-usedLines_7_.png b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_100-usedLines_7_.png new file mode 100644 index 00000000..d30b1f24 --- /dev/null +++ b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_100-usedLines_7_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c09ad5c85f708f5cd2135a52b13aa248a130abbc7fe8f0449b44d62f9d360384 +size 4505 diff --git a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_200-usedLines_6_.png b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_200-usedLines_6_.png new file mode 100644 index 00000000..60fccee3 --- /dev/null +++ b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_200-usedLines_6_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d54e2b3f85b01ee45f25e53c8f97e80ca9d7655ff6b8aa643664912812a3976 +size 4521 diff --git a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_25-usedLines_7_.png b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_25-usedLines_7_.png new file mode 100644 index 00000000..7407b0cf --- /dev/null +++ b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_25-usedLines_7_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1161af3a0ffc7d4835e58bcfa064713fa06971747414a144c3b979fdabf1bbdd +size 4855 diff --git a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_50-usedLines_7_.png b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_50-usedLines_7_.png new file mode 100644 index 00000000..4ec38250 --- /dev/null +++ b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_50-usedLines_7_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a11863fa11c93d158560fadddcb4768047c5c7f0069054cbf9392b5dc234759 +size 4853 diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/ExactImageComparer.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/ExactImageComparer.cs new file mode 100644 index 00000000..f321e849 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/ExactImageComparer.cs @@ -0,0 +1,58 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.Fonts.Tests.ImageComparison; + +public class ExactImageComparer : ImageComparer +{ + public static ExactImageComparer Instance { get; } = new ExactImageComparer(); + + public override ImageSimilarityReport CompareImagesOrFrames( + int index, + ImageFrame expected, + ImageFrame actual) + { + if (expected.Size != actual.Size) + { + throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); + } + + int width = actual.Width; + + // TODO: Comparing through Rgba64 may not be robust enough because of the existence of super high precision pixel types. + Rgba64[] aBuffer = new Rgba64[width]; + Rgba64[] bBuffer = new Rgba64[width]; + + List differences = new(); + Configuration configuration = expected.Configuration; + Buffer2D expectedBuffer = expected.PixelBuffer; + Buffer2D actualBuffer = actual.PixelBuffer; + + for (int y = 0; y < actual.Height; y++) + { + Span aSpan = expectedBuffer.DangerousGetRowSpan(y); + Span bSpan = actualBuffer.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToRgba64(configuration, aSpan, aBuffer); + PixelOperations.Instance.ToRgba64(configuration, bSpan, bBuffer); + + for (int x = 0; x < width; x++) + { + Rgba64 aPixel = aBuffer[x]; + Rgba64 bPixel = bBuffer[x]; + + if (aPixel != bPixel) + { + PixelDifference diff = new(new Point(x, y), aPixel, bPixel); + differences.Add(diff); + } + } + } + + return new ImageSimilarityReport(index, expected, actual, differences); + } +} diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs new file mode 100644 index 00000000..a3253a8c --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using System.Text; + +namespace SixLabors.Fonts.Tests.ImageComparison; + +public class ImageDifferenceIsOverThresholdException : ImagesSimilarityException +{ + public ImageSimilarityReport[] Reports { get; } + + public ImageDifferenceIsOverThresholdException(params ImageSimilarityReport[] reports) + : base("Image difference is over threshold!" + FormatReports(reports)) + => this.Reports = reports.ToArray(); + + private static string FormatReports(IEnumerable reports) + { + StringBuilder sb = new(); + + sb.Append(Environment.NewLine); + sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment OS : {0}", GetEnvironmentName()); + sb.Append(Environment.NewLine); + + sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment is CI : {0}", TestEnvironment.RunsOnCI); + sb.Append(Environment.NewLine); + + sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment OS Architecture : {0}", TestEnvironment.OSArchitecture); + sb.Append(Environment.NewLine); + + sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment Process Architecture : {0}", TestEnvironment.ProcessArchitecture); + sb.Append(Environment.NewLine); + + foreach (ImageSimilarityReport r in reports) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "Report ImageFrame {0}: ", r.Index) + .Append(r) + .Append(Environment.NewLine); + } + + return sb.ToString(); + } + + private static string GetEnvironmentName() + { + if (TestEnvironment.IsMacOS) + { + return "MacOS"; + } + + if (TestEnvironment.IsLinux) + { + return "Linux"; + } + + if (TestEnvironment.IsWindows) + { + return "Windows"; + } + + return "Unknown"; + } +} diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImageDimensionsMismatchException.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImageDimensionsMismatchException.cs new file mode 100644 index 00000000..9cdd5e0e --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImageDimensionsMismatchException.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp; + +namespace SixLabors.Fonts.Tests.ImageComparison; + +public class ImageDimensionsMismatchException : ImagesSimilarityException +{ + public ImageDimensionsMismatchException(Size expectedSize, Size actualSize) + : base($"The image dimensions {actualSize} do not match the expected {expectedSize}!") + { + this.ExpectedSize = expectedSize; + this.ActualSize = actualSize; + } + + public Size ExpectedSize { get; } + + public Size ActualSize { get; } +} diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImagesSimilarityException.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImagesSimilarityException.cs new file mode 100644 index 00000000..652ce3ef --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/Exceptions/ImagesSimilarityException.cs @@ -0,0 +1,14 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tests.ImageComparison; + +using System; + +public class ImagesSimilarityException : Exception +{ + public ImagesSimilarityException(string message) + : base(message) + { + } +} diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/ImageComparer.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/ImageComparer.cs new file mode 100644 index 00000000..ae3f6883 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/ImageComparer.cs @@ -0,0 +1,177 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.Fonts.Tests.ImageComparison; + +public abstract class ImageComparer +{ + public static ImageComparer Exact { get; } = Tolerant(0, 0); + + /// + /// Returns an instance of . + /// Individual Manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'. + /// + /// The maximal tolerated difference represented by a value between 0 and 1. + /// Gets the threshold of the individual pixels before they accumulate towards the overall difference. + /// A ImageComparer instance. + public static ImageComparer Tolerant( + float imageThreshold = TolerantImageComparer.DefaultImageThreshold, + int perPixelManhattanThreshold = 0) => + new TolerantImageComparer(imageThreshold, perPixelManhattanThreshold); + + /// + /// Returns Tolerant(imageThresholdInPercent/100) + /// + /// The maximal tolerated difference represented by a value between 0 and 100. + /// Gets the threshold of the individual pixels before they accumulate towards the overall difference. + /// A ImageComparer instance. + public static ImageComparer TolerantPercentage(float imageThresholdInPercent, int perPixelManhattanThreshold = 0) + => Tolerant(imageThresholdInPercent / 100F, perPixelManhattanThreshold); + + public abstract ImageSimilarityReport CompareImagesOrFrames( + int index, + ImageFrame expected, + ImageFrame actual) + where TPixelA : unmanaged, IPixel + where TPixelB : unmanaged, IPixel; +} + +public static class ImageComparerExtensions +{ + public static ImageSimilarityReport CompareImagesOrFrames( + this ImageComparer comparer, + Image expected, + Image actual) + where TPixelA : unmanaged, IPixel + where TPixelB : unmanaged, IPixel => comparer.CompareImagesOrFrames(0, expected.Frames.RootFrame, actual.Frames.RootFrame); + + public static IEnumerable> CompareImages( + this ImageComparer comparer, + Image expected, + Image actual, + Func predicate = null) + where TPixelA : unmanaged, IPixel + where TPixelB : unmanaged, IPixel + { + List> result = new(); + + int expectedFrameCount = actual.Frames.Count; + if (predicate != null) + { + expectedFrameCount = 0; + for (int i = 0; i < actual.Frames.Count; i++) + { + if (predicate(i, actual.Frames.Count)) + { + expectedFrameCount++; + } + } + } + + if (expected.Frames.Count != expectedFrameCount) + { + throw new ImagesSimilarityException("Frame count does not match!"); + } + + for (int i = 0; i < expected.Frames.Count; i++) + { + if (predicate != null && !predicate(i, expected.Frames.Count)) + { + continue; + } + + ImageSimilarityReport report = comparer.CompareImagesOrFrames(i, expected.Frames[i], actual.Frames[i]); + if (!report.IsEmpty) + { + result.Add(report); + } + } + + return result; + } + + public static void VerifySimilarity( + this ImageComparer comparer, + Image expected, + Image actual, + Func predicate = null) + where TPixelA : unmanaged, IPixel + where TPixelB : unmanaged, IPixel + { + if (expected.Size != actual.Size) + { + throw new ImageDimensionsMismatchException(expected.Size, actual.Size); + } + + int expectedFrameCount = actual.Frames.Count; + if (predicate != null) + { + expectedFrameCount = 0; + for (int i = 0; i < actual.Frames.Count; i++) + { + if (predicate(i, actual.Frames.Count)) + { + expectedFrameCount++; + } + } + } + + if (expected.Frames.Count != expectedFrameCount) + { + throw new ImagesSimilarityException("Image frame count does not match!"); + } + + IEnumerable reports = comparer.CompareImages(expected, actual, predicate); + if (reports.Any()) + { + throw new ImageDifferenceIsOverThresholdException(reports.ToArray()); + } + } + + public static void VerifySimilarityIgnoreRegion( + this ImageComparer comparer, + Image expected, + Image actual, + Rectangle ignoredRegion) + where TPixelA : unmanaged, IPixel + where TPixelB : unmanaged, IPixel + { + if (expected.Size != actual.Size) + { + throw new ImageDimensionsMismatchException(expected.Size, actual.Size); + } + + if (expected.Frames.Count != actual.Frames.Count) + { + throw new ImagesSimilarityException("Image frame count does not match!"); + } + + IEnumerable> reports = comparer.CompareImages(expected, actual); + if (reports.Any()) + { + List> cleanedReports = new(reports.Count()); + foreach (ImageSimilarityReport r in reports) + { + IEnumerable outsideChanges = r.Differences.Where( + x => + !(ignoredRegion.X <= x.Position.X + && x.Position.X <= ignoredRegion.Right + && ignoredRegion.Y <= x.Position.Y + && x.Position.Y <= ignoredRegion.Bottom)); + + if (outsideChanges.Any()) + { + cleanedReports.Add(new ImageSimilarityReport(r.Index, r.ExpectedImage, r.ActualImage, outsideChanges, null)); + } + } + + if (cleanedReports.Count > 0) + { + throw new ImageDifferenceIsOverThresholdException(cleanedReports.ToArray()); + } + } + } +} diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/ImageSimilarityReport.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/ImageSimilarityReport.cs new file mode 100644 index 00000000..9ad00120 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/ImageSimilarityReport.cs @@ -0,0 +1,110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Globalization; +using System.Text; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.Fonts.Tests.ImageComparison; + +public class ImageSimilarityReport +{ + protected ImageSimilarityReport( + int index, + object expectedImage, + object actualImage, + IEnumerable differences, + float? totalNormalizedDifference = null) + { + this.Index = index; + this.ExpectedImage = expectedImage; + this.ActualImage = actualImage; + this.TotalNormalizedDifference = totalNormalizedDifference; + this.Differences = differences.ToArray(); + } + + public int Index { get; } + + public object ExpectedImage { get; } + + public object ActualImage { get; } + + // TODO: This should not be a nullable value! + public float? TotalNormalizedDifference { get; } + + public string DifferencePercentageString + { + get + { + if (!this.TotalNormalizedDifference.HasValue) + { + return "?"; + } + else if (this.TotalNormalizedDifference == 0) + { + return "0%"; + } + else + { + return $"{this.TotalNormalizedDifference.Value * 100:0.0000}%"; + } + } + } + + public PixelDifference[] Differences { get; } + + public bool IsEmpty => this.Differences.Length == 0; + + public override string ToString() => this.IsEmpty ? "[SimilarImages]" : this.PrintDifference(); + + private string PrintDifference() + { + StringBuilder sb = new(); + if (this.TotalNormalizedDifference.HasValue) + { + sb.AppendLine() + .AppendLine(CultureInfo.InvariantCulture, $"Total difference: {this.DifferencePercentageString}"); + } + + int max = Math.Min(5, this.Differences.Length); + + for (int i = 0; i < max; i++) + { + sb.Append(this.Differences[i]); + if (i < max - 1) + { + sb.AppendFormat(CultureInfo.InvariantCulture, ";{0}", Environment.NewLine); + } + } + + if (this.Differences.Length >= 5) + { + sb.Append("..."); + } + + return sb.ToString(); + } +} + +public class ImageSimilarityReport : ImageSimilarityReport + where TPixelA : unmanaged, IPixel + where TPixelB : unmanaged, IPixel +{ + public ImageSimilarityReport( + int index, + ImageFrame expectedImage, + ImageFrame actualImage, + IEnumerable differences, + float? totalNormalizedDifference = null) + : base(index, expectedImage, actualImage, differences, totalNormalizedDifference) + { + } + + public static ImageSimilarityReport Empty => + new(0, null, null, Enumerable.Empty(), 0f); + + public new ImageFrame ExpectedImage => (ImageFrame)base.ExpectedImage; + + public new ImageFrame ActualImage => (ImageFrame)base.ActualImage; +} diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/PixelDifference.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/PixelDifference.cs new file mode 100644 index 00000000..309790e6 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/PixelDifference.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.Fonts.Tests.ImageComparison; + +public readonly struct PixelDifference +{ + public PixelDifference( + Point position, + int redDifference, + int greenDifference, + int blueDifference, + int alphaDifference) + { + this.Position = position; + this.RedDifference = redDifference; + this.GreenDifference = greenDifference; + this.BlueDifference = blueDifference; + this.AlphaDifference = alphaDifference; + } + + public PixelDifference(Point position, Rgba64 expected, Rgba64 actual) + : this( + position, + actual.R - expected.R, + actual.G - expected.G, + actual.B - expected.B, + actual.A - expected.A) + { + } + + public Point Position { get; } + + public int RedDifference { get; } + + public int GreenDifference { get; } + + public int BlueDifference { get; } + + public int AlphaDifference { get; } + + public override string ToString() => + $"[Δ({this.RedDifference},{this.GreenDifference},{this.BlueDifference},{this.AlphaDifference}) @ ({this.Position.X},{this.Position.Y})]"; +} diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs new file mode 100644 index 00000000..bfc9d16c --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Reflection; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Tests.ImageComparison; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.Fonts.Tests.TestUtilities; + +public static class TestImageExtensions +{ + public static string DebugSave( + this Image image, + string extension = null, + [CallerMemberName] string test = "", + object properties = null) + { + string outputDirectory = TestEnvironment.ActualOutputDirectoryFullPath; + if (!Directory.Exists(outputDirectory)) + { + Directory.CreateDirectory(outputDirectory); + } + + string path = Path.Combine(outputDirectory, $"{test}{FormatTestDetails(properties)}.{extension ?? "png"}"); + image.Save(path); + + return path; + } + + public static void CompareToReference( + this Image image, + float percentageTolerance = 0F, + string extension = null, + [CallerMemberName] string test = "", + object properties = null) + where TPixel : unmanaged, IPixel + { + string path = image.DebugSave(extension, test, properties: properties); + string referencePath = path.Replace(TestEnvironment.ActualOutputDirectoryFullPath, TestEnvironment.ReferenceOutputDirectoryFullPath); + + if (!File.Exists(referencePath)) + { + throw new FileNotFoundException($"The reference image file was not found: {referencePath}"); + } + + using Image expected = Image.Load(referencePath); + TolerantImageComparer comparer = new(percentageTolerance / 100F); + ImageSimilarityReport report = comparer.CompareImagesOrFrames(expected, image); + + if (!report.IsEmpty) + { + throw new ImageDifferenceIsOverThresholdException(report); + } + } + + private static string FormatTestDetails(object properties) + { + if (properties is null) + { + return "-"; + } + + if (properties is FormattableString fs) + { + return FormattableString.Invariant(fs); + } + else if (properties is string s) + { + return FormattableString.Invariant($"-{s}-"); + } + + IEnumerable runtimeProperties = properties.GetType().GetRuntimeProperties(); + + return FormattableString.Invariant($"_{string.Join( + "-", + runtimeProperties.ToDictionary(x => x.Name, x => x.GetValue(properties)) + .Select(x => FormattableString.Invariant($"{x.Key}_{x.Value}")))}_"); + } +} + diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs new file mode 100644 index 00000000..58ae66e5 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.Fonts.Tests.ImageComparison; + +public class TolerantImageComparer : ImageComparer +{ + // 1% of all pixels in a 100*100 pixel area are allowed to have a difference of 1 unit + // 257 = (1 / 255) * 65535. + public const float DefaultImageThreshold = 257F / (100 * 100 * 65535); + + /// + /// Individual Manhattan pixel difference is only added to total image difference when the individual difference is over 'perPixelManhattanThreshold'. + /// + /// The maximal tolerated difference represented by a value between 0.0 and 1.0 scaled to 0 and 65535. + /// Gets the threshold of the individual pixels before they accumulate towards the overall difference. + public TolerantImageComparer(float imageThreshold, int perPixelManhattanThreshold = 0) + { + Guard.MustBeGreaterThanOrEqualTo(imageThreshold, 0, nameof(imageThreshold)); + + this.ImageThreshold = imageThreshold; + this.PerPixelManhattanThreshold = perPixelManhattanThreshold; + } + + /// + /// + /// Gets the maximal tolerated difference represented by a value between 0.0 and 1.0 scaled to 0 and 65535. + /// Examples of percentage differences on a single pixel: + /// 1. PixelA = (65535,65535,65535,0) PixelB =(0,0,0,65535) leads to 100% difference on a single pixel + /// 2. PixelA = (65535,65535,65535,0) PixelB =(65535,65535,65535,65535) leads to 25% difference on a single pixel + /// 3. PixelA = (65535,65535,65535,0) PixelB =(32767,32767,32767,32767) leads to 50% difference on a single pixel + /// + /// + /// The total differences is the sum of all pixel differences normalized by image dimensions! + /// The individual distances are calculated using the Manhattan function: + /// + /// https://en.wikipedia.org/wiki/Taxicab_geometry + /// + /// ImageThresholdInPercent = 1/255 = 257/65535 means that we allow one unit difference per channel on a 1x1 image + /// ImageThresholdInPercent = 1/(100*100*255) = 257/(100*100*65535) means that we allow only one unit difference per channel on a 100x100 image + /// + /// + public float ImageThreshold { get; } + + /// + /// Gets the threshold of the individual pixels before they accumulate towards the overall difference. + /// For an individual pixel pair the value is the Manhattan distance of pixels: + /// + /// https://en.wikipedia.org/wiki/Taxicab_geometry + /// + /// + public int PerPixelManhattanThreshold { get; } + + public override ImageSimilarityReport CompareImagesOrFrames(int index, ImageFrame expected, ImageFrame actual) + { + if (expected.Size != actual.Size) + { + throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); + } + + int width = actual.Width; + + // TODO: Comparing through Rgba64 may not robust enough because of the existence of super high precision pixel types. + Rgba64[] aBuffer = new Rgba64[width]; + Rgba64[] bBuffer = new Rgba64[width]; + + float totalDifference = 0F; + + List differences = new(); + Configuration configuration = expected.Configuration; + Buffer2D expectedBuffer = expected.PixelBuffer; + Buffer2D actualBuffer = actual.PixelBuffer; + + for (int y = 0; y < actual.Height; y++) + { + Span aSpan = expectedBuffer.DangerousGetRowSpan(y); + Span bSpan = actualBuffer.DangerousGetRowSpan(y); + + PixelOperations.Instance.ToRgba64(configuration, aSpan, aBuffer); + PixelOperations.Instance.ToRgba64(configuration, bSpan, bBuffer); + + for (int x = 0; x < width; x++) + { + int d = GetManhattanDistanceInRgbaSpace(ref aBuffer[x], ref bBuffer[x]); + + if (d > this.PerPixelManhattanThreshold) + { + PixelDifference diff = new(new Point(x, y), aBuffer[x], bBuffer[x]); + differences.Add(diff); + + totalDifference += d; + } + } + } + + float normalizedDifference = totalDifference / (actual.Width * (float)actual.Height); + normalizedDifference /= 4F * 65535F; + + if (normalizedDifference > this.ImageThreshold) + { + return new ImageSimilarityReport(index, expected, actual, differences, normalizedDifference); + } + + return ImageSimilarityReport.Empty; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetManhattanDistanceInRgbaSpace(ref Rgba64 a, ref Rgba64 b) + => Diff(a.R, b.R) + Diff(a.G, b.G) + Diff(a.B, b.B) + Diff(a.A, b.A); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Diff(ushort a, ushort b) => Math.Abs(a - b); +} diff --git a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj index 2014f031..3114ff53 100644 --- a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj +++ b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj @@ -35,6 +35,7 @@ + diff --git a/tests/SixLabors.Fonts.Tests/TestEnvironment.cs b/tests/SixLabors.Fonts.Tests/TestEnvironment.cs index 84a931a0..a4531530 100644 --- a/tests/SixLabors.Fonts.Tests/TestEnvironment.cs +++ b/tests/SixLabors.Fonts.Tests/TestEnvironment.cs @@ -2,13 +2,20 @@ // Licensed under the Six Labors Split License. using System.Reflection; +using System.Runtime.InteropServices; namespace SixLabors.Fonts.Tests; internal static class TestEnvironment { + private static readonly FileInfo TestAssemblyFile = new(typeof(TestEnvironment).GetTypeInfo().Assembly.Location); + private const string SixLaborsSolutionFileName = "SixLabors.Fonts.sln"; + private const string ActualOutputDirectoryRelativePath = @"tests\Images\ActualOutput"; + + private const string ReferenceOutputDirectoryRelativePath = @"tests\Images\ReferenceOutput"; + private const string UnicodeTestDataRelativePath = @"tests\UnicodeTestData\"; private static readonly Lazy SolutionDirectoryFullPathLazy = new(GetSolutionDirectoryFullPathImpl); @@ -20,15 +27,43 @@ internal static class TestEnvironment /// internal static string UnicodeTestDataFullPath => GetFullPath(UnicodeTestDataRelativePath); - private static string GetSolutionDirectoryFullPathImpl() - { - string assemblyLocation = Path.GetDirectoryName(new Uri(typeof(TestEnvironment).GetTypeInfo().Assembly.CodeBase).LocalPath); + /// + /// Gets the correct full path to the Actual Output directory. (To be written to by the test cases.) + /// + internal static string ActualOutputDirectoryFullPath => GetFullPath(ActualOutputDirectoryRelativePath); + + /// + /// Gets the correct full path to the Expected Output directory. (To compare the test results to.) + /// + internal static string ReferenceOutputDirectoryFullPath => GetFullPath(ReferenceOutputDirectoryRelativePath); + + internal static bool IsLinux => RuntimeInformation.IsOSPlatform(OSPlatform.Linux); - var assemblyFile = new FileInfo(assemblyLocation); + internal static bool IsMacOS => RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - DirectoryInfo directory = assemblyFile.Directory; + internal static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + + internal static bool Is64BitProcess => Environment.Is64BitProcess; + + internal static Architecture OSArchitecture => RuntimeInformation.OSArchitecture; + + internal static Architecture ProcessArchitecture => RuntimeInformation.ProcessArchitecture; + + + /// + /// Gets a value indicating whether test execution runs on CI. + /// +#if ENV_CI + internal static bool RunsOnCI => true; +#else + internal static bool RunsOnCI => false; +#endif + + private static string GetSolutionDirectoryFullPathImpl() + { + DirectoryInfo directory = TestAssemblyFile.Directory; - while (!directory.EnumerateFiles(SixLaborsSolutionFileName).Any()) + while (directory?.EnumerateFiles(SixLaborsSolutionFileName).Any() == false) { try { @@ -36,14 +71,14 @@ private static string GetSolutionDirectoryFullPathImpl() } catch (Exception ex) { - throw new Exception( - $"Unable to find SixLabors solution directory from {assemblyLocation} because of {ex.GetType().Name}!", + throw new DirectoryNotFoundException( + $"Unable to find solution directory from {TestAssemblyFile} because of {ex.GetType().Name}!", ex); } if (directory == null) { - throw new Exception($"Unable to find SixLabors solution directory from {assemblyLocation}!"); + throw new DirectoryNotFoundException($"Unable to find solution directory from {TestAssemblyFile}!"); } } diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 51a86826..497d7836 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -4,7 +4,12 @@ using System.Globalization; using System.Numerics; using SixLabors.Fonts.Tests.Fakes; +using SixLabors.Fonts.Tests.TestUtilities; using SixLabors.Fonts.Unicode; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace SixLabors.Fonts.Tests; @@ -531,10 +536,24 @@ public void CountLinesWithSpan() [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 200, 6)] public void CountLinesWrappingLength(string text, int wrappingLength, int usedLines) { - Font font = CreateFont(text); - int count = TextMeasurer.CountLines(text, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor, WrappingLength = wrappingLength }); + Font font = CreateRenderingFont(); + RichTextOptions options = new(font) + { + // Dpi = font.FontMetrics.ScaleFactor, + WrappingLength = wrappingLength + }; - Assert.Equal(usedLines, count); + int count = TextMeasurer.CountLines(text, options); + + // Assert.Equal(usedLines, count); + FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); + int width = (int)Math.Ceiling(advance.Width); + int height = (int)Math.Ceiling(advance.Height); + + using Image img = new(Math.Max(wrappingLength + 1, width), height, Color.White); + img.Mutate(x => x.DrawLine(Color.Red, 1, new(wrappingLength, 0), new(wrappingLength, height))); + img.Mutate(ctx => ctx.DrawText(options, text, Color.Black)); + img.DebugSave(properties: new { wrappingLength, usedLines }); } [Fact] @@ -1351,6 +1370,11 @@ public FontRectangle BenchmarkTest() private static readonly Font Arial = SystemFonts.CreateFont("Arial", 12); #endif + public static Font CreateRenderingFont() + { + return new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(12); + } + public static Font CreateFont(string text) { var fc = (IFontMetricsCollection)new FontCollection(); From 7236a639c60b405360c7c080ce8ac15d1bd1f950 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 7 Jan 2025 19:42:55 +1000 Subject: [PATCH 03/11] Fix issues and add visual references --- src/SixLabors.Fonts/TextLayout.cs | 166 ++++++---- ...Length_wrappingLength_100-usedLines_7_.png | 3 - ...Length_wrappingLength_200-usedLines_6_.png | 3 - ...gLength_wrappingLength_25-usedLines_7_.png | 3 - ...gLength_wrappingLength_50-usedLines_7_.png | 3 - .../CountLinesWrappingLength_100-4.png | 3 + .../CountLinesWrappingLength_200-3.png | 3 + .../CountLinesWrappingLength_25-6.png | 3 + .../CountLinesWrappingLength_50-5.png | 3 + ...zontalBottomTop-wordBreaking_BreakAll_.png | 3 + ...ontalBottomTop-wordBreaking_BreakWord_.png | 3 + ...izontalBottomTop-wordBreaking_KeepAll_.png | 3 + ...zontalBottomTop-wordBreaking_Standard_.png | 3 + ...zontalTopBottom-wordBreaking_BreakAll_.png | 3 + ...ontalTopBottom-wordBreaking_BreakWord_.png | 3 + ...izontalTopBottom-wordBreaking_KeepAll_.png | 3 + ...zontalTopBottom-wordBreaking_Standard_.png | 3 + ...zontalBottomTop-wordBreaking_BreakAll_.png | 3 + ...ontalBottomTop-wordBreaking_BreakWord_.png | 3 + ...izontalBottomTop-wordBreaking_KeepAll_.png | 3 + ...zontalBottomTop-wordBreaking_Standard_.png | 3 + ...zontalTopBottom-wordBreaking_BreakAll_.png | 3 + ...ontalTopBottom-wordBreaking_BreakWord_.png | 3 + ...izontalTopBottom-wordBreaking_KeepAll_.png | 3 + ...zontalTopBottom-wordBreaking_Standard_.png | 3 + ...BottomTop_350-_height_10-width_87.125_.png | 3 + ...omTop_350-_height_11.438-width_279.13_.png | 3 + ...omTop_350-_height_62.625-width_318.86_.png | 3 + ...TopBottom_350-_height_10-width_87.125_.png | 3 + ...ottom_350-_height_11.438-width_279.13_.png | 3 + ...ottom_350-_height_62.625-width_318.86_.png | 3 + ...LeftRight_350-_height_171.25-width_10_.png | 3 + ...Right_350-_height_267.25-width_23.875_.png | 3 + ...ight_350-_height_318.563-width_62.813_.png | 3 + ...ight_350-_height_279.125-width_11.438_.png | 3 + ...ight_350-_height_318.563-width_62.813_.png | 3 + ...LeftRight_350-_height_87.125-width_10_.png | 3 + ...RightLeft_350-_height_171.25-width_10_.png | 3 + ...tLeft_350-_height_267.25-width_23.875_.png | 3 + ...Left_350-_height_318.563-width_62.813_.png | 3 + .../ShouldInsertExtraLineBreaksA_400-4.png | 3 + .../ShouldInsertExtraLineBreaksB_400-4.png | 3 + ...MatchBrowserBreak__WrappingLength_372_.png | 3 + ...rtExtraLineBreaks__WrappingLength_400_.png | 3 + ...ight-TextJustification_InterCharacter_.png | 3 + ...Left-TextJustification_InterCharacter_.png | 3 + ...ight-TextJustification_InterCharacter_.png | 3 + ...Left-TextJustification_InterCharacter_.png | 3 + ...ftToRight-TextJustification_InterWord_.png | 3 + ...ghtToLeft-TextJustification_InterWord_.png | 3 + ...ftToRight-TextJustification_InterWord_.png | 3 + ...ghtToLeft-TextJustification_InterWord_.png | 3 + .../ImageComparison/TestImageExtensions.cs | 33 +- .../ImageComparison/TolerantImageComparer.cs | 2 +- .../Issues/Issues_367.cs | 2 + .../Issues/Issues_431.cs | 6 +- .../Issues/Issues_434.cs | 36 ++- .../SixLabors.Fonts.Tests.csproj | 21 +- .../TextLayoutTestUtilities.cs | 116 +++++++ .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 300 +++++++++--------- 60 files changed, 605 insertions(+), 230 deletions(-) delete mode 100644 tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_100-usedLines_7_.png delete mode 100644 tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_200-usedLines_6_.png delete mode 100644 tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_25-usedLines_7_.png delete mode 100644 tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_50-usedLines_7_.png create mode 100644 tests/Images/ReferenceOutput/CountLinesWrappingLength_100-4.png create mode 100644 tests/Images/ReferenceOutput/CountLinesWrappingLength_200-3.png create mode 100644 tests/Images/ReferenceOutput/CountLinesWrappingLength_25-6.png create mode 100644 tests/Images/ReferenceOutput/CountLinesWrappingLength_50-5.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_10-width_87.125_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_11.438-width_279.13_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_62.625-width_318.86_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_10-width_87.125_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_11.438-width_279.13_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_62.625-width_318.86_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_171.25-width_10_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_267.25-width_23.875_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_318.563-width_62.813_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_279.125-width_11.438_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_318.563-width_62.813_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_87.125-width_10_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_171.25-width_10_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_267.25-width_23.875_.png create mode 100644 tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_318.563-width_62.813_.png create mode 100644 tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-4.png create mode 100644 tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-4.png create mode 100644 tests/Images/ReferenceOutput/ShouldMatchBrowserBreak__WrappingLength_372_.png create mode 100644 tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks__WrappingLength_400_.png create mode 100644 tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_LeftToRight-TextJustification_InterCharacter_.png create mode 100644 tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_RightToLeft-TextJustification_InterCharacter_.png create mode 100644 tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_LeftToRight-TextJustification_InterCharacter_.png create mode 100644 tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_RightToLeft-TextJustification_InterCharacter_.png create mode 100644 tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_LeftToRight-TextJustification_InterWord_.png create mode 100644 tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_RightToLeft-TextJustification_InterWord_.png create mode 100644 tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_LeftToRight-TextJustification_InterWord_.png create mode 100644 tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_RightToLeft-TextJustification_InterWord_.png create mode 100644 tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 0274e2e8..46137fcb 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -695,10 +695,11 @@ private static IEnumerable LayoutLineVerticalMixed( // Adjust the horizontal offset further by considering the descender differences: // - Subtract the current glyph's descender (data.ScaledDescender) to align it properly. - float descenderDelta = (Math.Abs(textLine.ScaledMaxDescender) - Math.Abs(data.ScaledDescender)) * .5F; + float descenderAbs = Math.Abs(data.ScaledDescender); + float descenderDelta = (Math.Abs(textLine.ScaledMaxDescender) - descenderAbs) * .5F; // Final horizontal center offset combines the baseline and descender adjustments. - float centerOffsetX = (baselineDelta - data.ScaledDescender) + descenderDelta; + float centerOffsetX = baselineDelta + descenderAbs + descenderDelta; glyphs.Add(new GlyphLayout( new Glyph(metric, data.PointSize), @@ -1138,12 +1139,7 @@ VerticalOrientationType.Rotate or stringIndex += graphemeEnumerator.Current.Length; } - // Resolve the bidi order for the line. - // This reorders the glyphs in the line to match the visual order. - textLine.BidiReOrder(); - - // Now we need to loop through our reordered line and split it at any line breaks. - // + // Now we need to loop through our line and split it at any line breaks. // First calculate the position of potential line breaks. LineBreakEnumerator lineBreakEnumerator = new(text); List lineBreaks = new(); @@ -1174,44 +1170,69 @@ VerticalOrientationType.Rotate or { // Mandatory line break at index. TextLine remaining = textLine.SplitAt(i); - textLines.Add(textLine.Finalize()); + textLines.Add(textLine.Finalize(options)); textLine = remaining; i = 0; lineAdvance = 0; } - else if (shouldWrap && lineAdvance + glyphAdvance >= wrappingLength) + else if (shouldWrap) { - if (breakAll) - { - // Insert a forced break at this index. - TextLine remaining = textLine.SplitAt(i); - textLines.Add(textLine.Finalize()); - textLine = remaining; - i = 0; - lineAdvance = 0; - } - else if (codePointIndex == currentLineBreak.PositionWrap || i == max) + float currentAdvance = lineAdvance + glyphAdvance; + if (currentAdvance >= wrappingLength) { - // If we are at the position wrap we can break here. - // Split the line at the last line break. - // CJK characters will not be split if 'keepAll' is true. - TextLine remaining = textLine.SplitAt(lastLineBreak, keepAll); - if (remaining != textLine) + if (breakAll) { - textLines.Add(textLine.Finalize()); + // Insert a forced break at this index. + TextLine remaining = textLine.SplitAt(i); + textLines.Add(textLine.Finalize(options)); textLine = remaining; i = 0; lineAdvance = 0; } - } - else if (breakWord) - { - // Insert a forced break at this index. - TextLine remaining = textLine.SplitAt(i); - textLines.Add(textLine.Finalize()); - textLine = remaining; - i = 0; - lineAdvance = 0; + else if (codePointIndex == currentLineBreak.PositionWrap || i == max) + { + LineBreak lineBreak = currentAdvance == wrappingLength + ? currentLineBreak + : lastLineBreak; + + if (i > 0) + { + // 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 positionAdvance = lineAdvance; + TextLine.GlyphLayoutData lastGlyph = textLine[i - 1]; + if (CodePoint.IsWhiteSpace(lastGlyph.CodePoint)) + { + positionAdvance -= lastGlyph.ScaledAdvance; + if (positionAdvance <= wrappingLength) + { + lineBreak = currentLineBreak; + } + } + } + + // If we are at the position wrap we can break here. + // Split the line at the appropriate break. + // CJK characters will not be split if 'keepAll' is true. + TextLine remaining = textLine.SplitAt(lineBreak, keepAll); + + if (remaining != textLine) + { + if (breakWord) + { + // If the line is too long, insert a forced line break. + if (textLine.ScaledLineAdvance > wrappingLength) + { + remaining.InsertAt(0, textLine.SplitAt(wrappingLength)); + } + } + + textLines.Add(textLine.Finalize(options)); + textLine = remaining; + i = 0; + lineAdvance = 0; + } + } } } } @@ -1228,27 +1249,23 @@ VerticalOrientationType.Rotate or // Add the final line. if (textLine.Count > 0) { - textLines.Add(textLine.Finalize()); + textLines.Add(textLine.Finalize(options)); } - return new TextBox(options, textLines); + return new TextBox(textLines); } internal sealed class TextBox { - public TextBox(TextOptions options, IReadOnlyList textLines) - { - this.TextLines = textLines; - for (int i = 0; i < this.TextLines.Count - 1; i++) - { - this.TextLines[i].Justify(options); - } - } + private float? scaledMaxAdvance; + + public TextBox(IReadOnlyList textLines) + => this.TextLines = textLines; public IReadOnlyList TextLines { get; } public float ScaledMaxAdvance() - => this.TextLines.Max(x => x.ScaledLineAdvance); + => this.scaledMaxAdvance ??= this.TextLines.Max(x => x.ScaledLineAdvance); public TextDirection TextDirection() => this.TextLines[0][0].TextDirection; } @@ -1311,8 +1328,20 @@ public void Add( stringIndex)); } + public TextLine InsertAt(int index, TextLine textLine) + { + this.data.InsertRange(index, textLine.data); + RecalculateLineMetrics(this); + return this; + } + public TextLine SplitAt(int index) { + if (index == 0 || index >= this.Count) + { + return this; + } + TextLine result = new(); result.data.AddRange(this.data.GetRange(index, this.data.Count - index)); RecalculateLineMetrics(result); @@ -1322,6 +1351,28 @@ public TextLine SplitAt(int index) return result; } + public TextLine SplitAt(float length) + { + TextLine result = new(); + float advance = 0; + for (int i = 0; i < this.data.Count; i++) + { + GlyphLayoutData glyph = this.data[i]; + advance += glyph.ScaledAdvance; + if (advance >= length) + { + result.data.AddRange(this.data.GetRange(i, this.data.Count - i)); + RecalculateLineMetrics(result); + + this.data.RemoveRange(i, this.data.Count - i); + RecalculateLineMetrics(this); + return result; + } + } + + return this; + } + public TextLine SplitAt(LineBreak lineBreak, bool keepAll) { int index = this.data.Count; @@ -1337,9 +1388,6 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) if (index == 0) { - // Now trim trailing whitespace from this line in the case of an exact - // length line break (non CJK) - RecalculateLineMetrics(this); return this; } @@ -1361,9 +1409,6 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) if (index == 0) { - // Now trim trailing whitespace from this line in the case of an exact - // length line break (non CJK) - RecalculateLineMetrics(this); return this; } } @@ -1376,15 +1421,11 @@ public TextLine SplitAt(LineBreak lineBreak, bool keepAll) // Remove those items from this line. this.data.RemoveRange(index, count); - - // Now trim trailing whitespace from this line. RecalculateLineMetrics(this); - // this.TrimTrailingWhitespaceAndRecalculateMetrics(); - return result; } - private TextLine TrimTrailingWhitespaceAndRecalculateMetrics() + private void TrimTrailingWhitespace() { int index = this.data.Count; while (index > 0) @@ -1403,14 +1444,19 @@ private TextLine TrimTrailingWhitespaceAndRecalculateMetrics() { this.data.RemoveRange(index, this.data.Count - index); } + } + + public TextLine Finalize(TextOptions options) + { + this.TrimTrailingWhitespace(); + this.BidiReOrder(); + RecalculateLineMetrics(this); + this.Justify(options); RecalculateLineMetrics(this); return this; } - public TextLine Finalize() - => this.TrimTrailingWhitespaceAndRecalculateMetrics(); - public void Justify(TextOptions options) { if (options.WrappingLength == -1F || options.TextJustification == TextJustification.None) diff --git a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_100-usedLines_7_.png b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_100-usedLines_7_.png deleted file mode 100644 index d30b1f24..00000000 --- a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_100-usedLines_7_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c09ad5c85f708f5cd2135a52b13aa248a130abbc7fe8f0449b44d62f9d360384 -size 4505 diff --git a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_200-usedLines_6_.png b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_200-usedLines_6_.png deleted file mode 100644 index 60fccee3..00000000 --- a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_200-usedLines_6_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5d54e2b3f85b01ee45f25e53c8f97e80ca9d7655ff6b8aa643664912812a3976 -size 4521 diff --git a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_25-usedLines_7_.png b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_25-usedLines_7_.png deleted file mode 100644 index 7407b0cf..00000000 --- a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_25-usedLines_7_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1161af3a0ffc7d4835e58bcfa064713fa06971747414a144c3b979fdabf1bbdd -size 4855 diff --git a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_50-usedLines_7_.png b/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_50-usedLines_7_.png deleted file mode 100644 index 4ec38250..00000000 --- a/tests/Images/ActualOutput/CountLinesWrappingLength_wrappingLength_50-usedLines_7_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a11863fa11c93d158560fadddcb4768047c5c7f0069054cbf9392b5dc234759 -size 4853 diff --git a/tests/Images/ReferenceOutput/CountLinesWrappingLength_100-4.png b/tests/Images/ReferenceOutput/CountLinesWrappingLength_100-4.png new file mode 100644 index 00000000..aef12a47 --- /dev/null +++ b/tests/Images/ReferenceOutput/CountLinesWrappingLength_100-4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:071789cee1ac03354e5727bda21321e8bb875ae417adfe5e745877023d62c6d7 +size 2963 diff --git a/tests/Images/ReferenceOutput/CountLinesWrappingLength_200-3.png b/tests/Images/ReferenceOutput/CountLinesWrappingLength_200-3.png new file mode 100644 index 00000000..7e9c8708 --- /dev/null +++ b/tests/Images/ReferenceOutput/CountLinesWrappingLength_200-3.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e2c7bb91aeeffc4d573d4509f55e58d5051221027003a584411f0b97141da16 +size 2966 diff --git a/tests/Images/ReferenceOutput/CountLinesWrappingLength_25-6.png b/tests/Images/ReferenceOutput/CountLinesWrappingLength_25-6.png new file mode 100644 index 00000000..10880c45 --- /dev/null +++ b/tests/Images/ReferenceOutput/CountLinesWrappingLength_25-6.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2959540711f38c08a319251355e838daabc3ac7721a89bb90261730732f6abab +size 3033 diff --git a/tests/Images/ReferenceOutput/CountLinesWrappingLength_50-5.png b/tests/Images/ReferenceOutput/CountLinesWrappingLength_50-5.png new file mode 100644 index 00000000..4ee0e7d2 --- /dev/null +++ b/tests/Images/ReferenceOutput/CountLinesWrappingLength_50-5.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:907dcca95723f6b21aa3791e047476e31b2d8b13f88f3c5e6604696ce5b469f5 +size 3022 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png new file mode 100644 index 00000000..67114c86 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e13708fcaf702a91540b1f68803cc2cff14de1a97cef771ca0bcc69e414a706e +size 13446 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png new file mode 100644 index 00000000..d0834260 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5a5ec93c6ece40f6c3577970cd0fa1f97d74d019ed52cf3ce9812b4d805624b0 +size 13613 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png new file mode 100644 index 00000000..bee92951 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fcaefcb524334225b07b555b296d3b9030a98b775f726b5c1f61997aed0ce587 +size 14041 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png new file mode 100644 index 00000000..7fcec502 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2951c4befa6d1ceb5afbdd4b36dc5df51752ad1d3ad1a01be70b717cb5811be +size 15221 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png new file mode 100644 index 00000000..2c0ae06c --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1896cd5f5d0521261c4b9e366ae56baa89f48cfd21b7d1a5d4a4e4c50b3f8542 +size 13485 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png new file mode 100644 index 00000000..bc6d223a --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d714d870453515f516af0fe8267795df3b36059a9c8c91b0946889985cd089e +size 13705 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png new file mode 100644 index 00000000..1a34cb35 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e6d7e2da8ffab3af8ec50463e38b6c5d96059919c4439271f5ee620b76c74e2 +size 14356 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png new file mode 100644 index 00000000..19231d99 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreakMatchesMDN_238-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe506ff8a945fea5f04fcd34a9a63ffcb89d610021acabe711be83ad19e6a96c +size 15607 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png new file mode 100644 index 00000000..44196676 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakAll_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66b5c1046ab68824efdd3af54c7753a18d7bb12b7a2960c956395b0d20bf19e2 +size 17444 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png new file mode 100644 index 00000000..9cd73e78 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_BreakWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9820e4f9f1719bf48b098991ee175f7c648f5edaa856b579402e75ea597a125 +size 19470 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png new file mode 100644 index 00000000..a5aa2a33 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_KeepAll_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7ffff224f6a16286cffa7e02e9d5069b389d9ce7274ed917489652f7e0163e7 +size 12638 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png new file mode 100644 index 00000000..53d98e24 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalBottomTop-wordBreaking_Standard_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f986a3bc5f09bd7c20108d9ed667e86de0b28ef813cc3426452b9d47b1ff31ba +size 21147 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png new file mode 100644 index 00000000..71ac3151 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakAll_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a99e819c0a8d7380e7afbdd289ebe0a2ee6f8c62b51ab7d10b06cfddf0729c55 +size 17437 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png new file mode 100644 index 00000000..932a7435 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_BreakWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1cb4294d6f5f72a6af860d8aab9f11b5224f7206add5d7bbcdc22d959a14a78c +size 19409 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png new file mode 100644 index 00000000..a6c638c8 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_KeepAll_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09cac7e7096c0fc3f92d3620580465f10e758edb0a23882e19af7cbf49238180 +size 20415 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png new file mode 100644 index 00000000..f41cf435 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordBreak_500-_layoutMode_HorizontalTopBottom-wordBreaking_Standard_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35095f9cb0df878caa355d5fc22474ecd25e43a60bcb67b68293dcd43eafc0c3 +size 21227 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_10-width_87.125_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_10-width_87.125_.png new file mode 100644 index 00000000..7413ec0e --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_10-width_87.125_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a504887655b5b61de10d5b09496330232136cc6854d28319c3c99375fb5424e8 +size 905 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_11.438-width_279.13_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_11.438-width_279.13_.png new file mode 100644 index 00000000..fcdea96c --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_11.438-width_279.13_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e5a90ec1b1cf8d69e0173d4c8a08a3bbbb2a428eee7250dc358e6f5abf6542d +size 948 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_62.625-width_318.86_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_62.625-width_318.86_.png new file mode 100644 index 00000000..02bc02c6 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalBottomTop_350-_height_62.625-width_318.86_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcf9ccad091fde6016070afcaa24e9040d6a81672f04350e56aa446802675bff +size 9272 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_10-width_87.125_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_10-width_87.125_.png new file mode 100644 index 00000000..7413ec0e --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_10-width_87.125_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a504887655b5b61de10d5b09496330232136cc6854d28319c3c99375fb5424e8 +size 905 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_11.438-width_279.13_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_11.438-width_279.13_.png new file mode 100644 index 00000000..fcdea96c --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_11.438-width_279.13_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e5a90ec1b1cf8d69e0173d4c8a08a3bbbb2a428eee7250dc358e6f5abf6542d +size 948 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_62.625-width_318.86_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_62.625-width_318.86_.png new file mode 100644 index 00000000..ed9c7eee --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingHorizontalTopBottom_350-_height_62.625-width_318.86_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afd34328b9e945944d90f8e56c33cff7d796a5ab22a55a4b1be5eeb629fb2f5a +size 9268 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_171.25-width_10_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_171.25-width_10_.png new file mode 100644 index 00000000..538e432c --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_171.25-width_10_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b81778b219f78d986e93643c3821c8c2d5c2eb2b488170db3ffa3cf42e6e0e0a +size 853 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_267.25-width_23.875_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_267.25-width_23.875_.png new file mode 100644 index 00000000..f48a36ba --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_267.25-width_23.875_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40e27a17bd7e5fe1d5177cb34e456a91cda7d555cfdd53c4e8a75722cbe41d09 +size 1083 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_318.563-width_62.813_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_318.563-width_62.813_.png new file mode 100644 index 00000000..df8cb7f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalLeftRight_350-_height_318.563-width_62.813_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57b1755082a3e82879b28ff20f2e4e2cd017aa8b0b236678a8dbf5cc6ddab17d +size 10317 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_279.125-width_11.438_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_279.125-width_11.438_.png new file mode 100644 index 00000000..d06deb08 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_279.125-width_11.438_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:739c2d5459270188f0c003ee017184c4127a686e04d939d731aa186d0daa68f7 +size 911 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_318.563-width_62.813_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_318.563-width_62.813_.png new file mode 100644 index 00000000..df8cb7f9 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_318.563-width_62.813_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57b1755082a3e82879b28ff20f2e4e2cd017aa8b0b236678a8dbf5cc6ddab17d +size 10317 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_87.125-width_10_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_87.125-width_10_.png new file mode 100644 index 00000000..7c5836e3 --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalMixedLeftRight_350-_height_87.125-width_10_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cab59d7637dd6820a15b63fc68fc541482916d555ef38337fe5f583136bce5d5 +size 873 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_171.25-width_10_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_171.25-width_10_.png new file mode 100644 index 00000000..538e432c --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_171.25-width_10_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b81778b219f78d986e93643c3821c8c2d5c2eb2b488170db3ffa3cf42e6e0e0a +size 853 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_267.25-width_23.875_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_267.25-width_23.875_.png new file mode 100644 index 00000000..d920a37d --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_267.25-width_23.875_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:902799aae2e8229dcacbf04554eb5b64d30c7c47fe9a9794be8ac34616a414db +size 1091 diff --git a/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_318.563-width_62.813_.png b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_318.563-width_62.813_.png new file mode 100644 index 00000000..0aa163cb --- /dev/null +++ b/tests/Images/ReferenceOutput/MeasureTextWordWrappingVerticalRightLeft_350-_height_318.563-width_62.813_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4b0bc75fa8b2ff464b38401741a58ce0fd7095767883d8fcd227e336a8f9c08 +size 10241 diff --git a/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-4.png b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-4.png new file mode 100644 index 00000000..1d754d10 --- /dev/null +++ b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksA_400-4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:576b23370c4d5ba88e835169c80babeda2ed03c32c767193724ee14ab3f18c48 +size 15102 diff --git a/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-4.png b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-4.png new file mode 100644 index 00000000..46544ba7 --- /dev/null +++ b/tests/Images/ReferenceOutput/ShouldInsertExtraLineBreaksB_400-4.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:435603a3488351bfd79647eb11e598dcb14e90cde9e849832e2a1f2f5cf53e14 +size 15691 diff --git a/tests/Images/ReferenceOutput/ShouldMatchBrowserBreak__WrappingLength_372_.png b/tests/Images/ReferenceOutput/ShouldMatchBrowserBreak__WrappingLength_372_.png new file mode 100644 index 00000000..0873537d --- /dev/null +++ b/tests/Images/ReferenceOutput/ShouldMatchBrowserBreak__WrappingLength_372_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82acb3f6de8b9682037417ddffb5f9815b0a74c727ee52f817c9b624ce3a7735 +size 5935 diff --git a/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks__WrappingLength_400_.png b/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks__WrappingLength_400_.png new file mode 100644 index 00000000..4796c6bd --- /dev/null +++ b/tests/Images/ReferenceOutput/ShouldNotInsertExtraLineBreaks__WrappingLength_400_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18b14563c798925aaeee959bbeb12bd392af681ab620fb15a0264f7f2904f2a6 +size 14829 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_LeftToRight-TextJustification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_LeftToRight-TextJustification_InterCharacter_.png new file mode 100644 index 00000000..1be8b4c3 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_LeftToRight-TextJustification_InterCharacter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a34f20be58409060a7a0c071de1ad19be52861b47d42af70e814a11605fc6d43 +size 8774 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_RightToLeft-TextJustification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_RightToLeft-TextJustification_InterCharacter_.png new file mode 100644 index 00000000..1503764f --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_RightToLeft-TextJustification_InterCharacter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9d261076440457a717da33a8681738e06a52182ef5430ea1c874c320a53c0e9 +size 8792 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_LeftToRight-TextJustification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_LeftToRight-TextJustification_InterCharacter_.png new file mode 100644 index 00000000..7f0f8d4d --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_LeftToRight-TextJustification_InterCharacter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41c2eb051160a94e3b63f789916daba0093af365b54bcd6fbd3c65a0114b295d +size 7507 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_RightToLeft-TextJustification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_RightToLeft-TextJustification_InterCharacter_.png new file mode 100644 index 00000000..7221c2e5 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_RightToLeft-TextJustification_InterCharacter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b8e48085ea9ffbe30b4be7de45aad642645a61edef7a4dd0b19a612c37e66bc7 +size 7432 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_LeftToRight-TextJustification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_LeftToRight-TextJustification_InterWord_.png new file mode 100644 index 00000000..dd5d86ea --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_LeftToRight-TextJustification_InterWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d6b060baad2258565758b6eebe3cd23061490a861dcb2b7b3b9276714dc5174 +size 8842 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_RightToLeft-TextJustification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_RightToLeft-TextJustification_InterWord_.png new file mode 100644 index 00000000..141b2191 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_RightToLeft-TextJustification_InterWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f675cd2d88ed4e69b6710d002e73f9b43530c60a748f1112f39d040840b0f498 +size 8696 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_LeftToRight-TextJustification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_LeftToRight-TextJustification_InterWord_.png new file mode 100644 index 00000000..aaacf0d3 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_LeftToRight-TextJustification_InterWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a8a54b0c1707f57d99afdcd3cfb5518636e96f40ebab213e83cc8ef9a85edb7 +size 6725 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_RightToLeft-TextJustification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_RightToLeft-TextJustification_InterWord_.png new file mode 100644 index 00000000..3917ae77 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_RightToLeft-TextJustification_InterWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73d1573b88cc105218d1a557c7b096a50ed7aeca0a76d3719d2243ef7f764020 +size 6721 diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs index bfc9d16c..921d2cbe 100644 --- a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Runtime.CompilerServices; +using System.Text; using SixLabors.Fonts.Tests.ImageComparison; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; @@ -15,7 +16,7 @@ public static string DebugSave( this Image image, string extension = null, [CallerMemberName] string test = "", - object properties = null) + params object[] properties) { string outputDirectory = TestEnvironment.ActualOutputDirectoryFullPath; if (!Directory.Exists(outputDirectory)) @@ -34,7 +35,7 @@ public static void CompareToReference( float percentageTolerance = 0F, string extension = null, [CallerMemberName] string test = "", - object properties = null) + params object[] properties) where TPixel : unmanaged, IPixel { string path = image.DebugSave(extension, test, properties: properties); @@ -55,7 +56,18 @@ public static void CompareToReference( } } - private static string FormatTestDetails(object properties) + private static string FormatTestDetails(params object[] properties) + { + if (properties?.Any() != true) + { + return "-"; + } + + StringBuilder sb = new(); + return $"_{string.Join("-", properties.Select(FormatTestDetails))}"; + } + + public static string FormatTestDetails(object properties) { if (properties is null) { @@ -70,13 +82,24 @@ private static string FormatTestDetails(object properties) { return FormattableString.Invariant($"-{s}-"); } + else if (properties is Dictionary dictionary) + { + return FormattableString.Invariant($"_{string.Join( + "-", + dictionary.Select(x => FormattableString.Invariant($"{x.Key}_{x.Value}")))}_"); + } - IEnumerable runtimeProperties = properties.GetType().GetRuntimeProperties(); + Type type = properties.GetType(); + TypeInfo info = type.GetTypeInfo(); + if (info.IsPrimitive || info.IsEnum || type == typeof(decimal)) + { + return FormattableString.Invariant($"{properties}"); + } + IEnumerable runtimeProperties = type.GetRuntimeProperties(); return FormattableString.Invariant($"_{string.Join( "-", runtimeProperties.ToDictionary(x => x.Name, x => x.GetValue(properties)) .Select(x => FormattableString.Invariant($"{x.Key}_{x.Value}")))}_"); } } - diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs index 58ae66e5..2cf4ff9f 100644 --- a/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/TolerantImageComparer.cs @@ -58,7 +58,7 @@ public TolerantImageComparer(float imageThreshold, int perPixelManhattanThreshol public override ImageSimilarityReport CompareImagesOrFrames(int index, ImageFrame expected, ImageFrame actual) { - if (expected.Size != actual.Size) + if (expected.Size() != actual.Size()) { throw new InvalidOperationException("Calling ImageComparer is invalid when dimensions mismatch!"); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_367.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_367.cs index 58b44e6f..aaa87cd2 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_367.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_367.cs @@ -25,6 +25,8 @@ public void ShouldMatchBrowserBreak() Assert.Equal(3, lineCount); FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); + TextLayoutTestUtilities.TestLayout(text, options); + Assert.Equal(354.968658F, advance.Width, Comparer); Assert.Equal(48, advance.Height, Comparer); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs index 5b3bcc6f..f19d8f9e 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs @@ -22,10 +22,12 @@ public void ShouldNotInsertExtraLineBreaks() }; int lineCount = TextMeasurer.CountLines(text, options); - Assert.Equal(3, lineCount); + Assert.Equal(4, lineCount); IReadOnlyList layout = TextLayout.GenerateLayout(text, options); - Assert.Equal(47, layout.Count); + Assert.Equal(46, layout.Count); + + TextLayoutTestUtilities.TestLayout(text, options); } } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs index e0a0f4ea..01cc6bee 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs @@ -8,9 +8,8 @@ namespace SixLabors.Fonts.Tests.Issues; public class Issues_434 { [Theory] - [InlineData("- Lorem ipsullll\n\ndolor sit amet\n-consectetur elit", 3)] - [InlineData("- Lorem ipsullll\n\n\ndolor sit amet\n-consectetur elit", 3)] - public void ShouldNotInsertExtraLineBreaks(string text, int expectedLineCount) + [InlineData("- Lorem ipsullll\n\ndolor sit amet\n-consectetur elit", 4)] + public void ShouldInsertExtraLineBreaksA(string text, int expectedLineCount) { if (SystemFonts.TryGet("Arial", out FontFamily family)) { @@ -21,11 +20,40 @@ public void ShouldNotInsertExtraLineBreaks(string text, int expectedLineCount) WrappingLength = 400, }; + // Line count includes rendered lines only. + // Line breaks cause offsetting of subsequent lines. int lineCount = TextMeasurer.CountLines(text, options); Assert.Equal(expectedLineCount, lineCount); IReadOnlyList layout = TextLayout.GenerateLayout(text, options); - Assert.Equal(47, layout.Count); + Assert.Equal(46, layout.Count); + + TextLayoutTestUtilities.TestLayout(text, options, properties: expectedLineCount); + } + } + + [Theory] + [InlineData("- Lorem ipsullll\n\n\ndolor sit amet\n-consectetur elit", 4)] + public void ShouldInsertExtraLineBreaksB(string text, int expectedLineCount) + { + if (SystemFonts.TryGet("Arial", out FontFamily family)) + { + Font font = family.CreateFont(60); + TextOptions options = new(font) + { + Origin = new Vector2(50, 20), + WrappingLength = 400, + }; + + // Line count includes rendered lines only. + // Line breaks cause offsetting of subsequent lines. + 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); } } } diff --git a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj index 3114ff53..f94c0a62 100644 --- a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj +++ b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj @@ -10,7 +10,7 @@ CA1304 - + @@ -23,7 +23,20 @@ - + + + + $(DefineConstants);SUPPORTS_DRAWING + true + + + + + + @@ -35,8 +48,8 @@ - - + + diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs new file mode 100644 index 00000000..266f4220 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +#if SUPPORTS_DRAWING +using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Tests.TestUtilities; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +#endif + +namespace SixLabors.Fonts.Tests; + +internal static class TextLayoutTestUtilities +{ + public static void TestLayout( + string text, + TextOptions options, + float percentageTolerance = 0F, + [CallerMemberName] string test = "", + params object[] properties) + { +#if SUPPORTS_DRAWING + FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); + int width = (int)(Math.Ceiling(advance.Width) + Math.Ceiling(options.Origin.X)); + int height = (int)(Math.Ceiling(advance.Height) + Math.Ceiling(options.Origin.Y)); + + bool isVertical = !options.LayoutMode.IsHorizontal(); + int wrappingLength = isVertical + ? (int)(Math.Ceiling(options.WrappingLength) + Math.Ceiling(options.Origin.Y)) + : (int)(Math.Ceiling(options.WrappingLength) + Math.Ceiling(options.Origin.X)); + + int imageWidth = isVertical ? width : Math.Max(width, wrappingLength + 1); + int imageHeight = isVertical ? Math.Max(height, wrappingLength + 1) : height; + + using Image img = new(imageWidth, imageHeight, Color.White); + + img.Mutate(ctx => ctx.DrawText(FromTextOptions(options), text, Color.Black)); + + if (wrappingLength > 0) + { + if (!options.LayoutMode.IsHorizontal()) + { + img.Mutate(x => x.DrawLine(Color.Red, 1, new(0, wrappingLength), new(width, wrappingLength))); + } + else + { + img.Mutate(x => x.DrawLine(Color.Red, 1, new(wrappingLength, 0), new(wrappingLength, height))); + } + + if (properties.Any()) + { + List extended = properties.ToList(); + extended.Insert(0, options.WrappingLength); + img.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: extended.ToArray()); + } + else + { + img.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: new { options.WrappingLength }); + } + } + else + { + img.CompareToReference(percentageTolerance: percentageTolerance, test: test, properties: properties); + } + +#endif + } + +#if SUPPORTS_DRAWING + private static RichTextOptions FromTextOptions(TextOptions options) + { + RichTextOptions result = new(options.Font) + { + FallbackFontFamilies = new List(options.FallbackFontFamilies), + TabWidth = options.TabWidth, + HintingMode = options.HintingMode, + Dpi = options.Dpi, + LineSpacing = options.LineSpacing, + Origin = options.Origin, + WrappingLength = options.WrappingLength, + WordBreaking = options.WordBreaking, + TextDirection = options.TextDirection, + TextAlignment = options.TextAlignment, + TextJustification = options.TextJustification, + HorizontalAlignment = options.HorizontalAlignment, + VerticalAlignment = options.VerticalAlignment, + LayoutMode = options.LayoutMode, + KerningMode = options.KerningMode, + ColorFontSupport = options.ColorFontSupport, + FeatureTags = new List(options.FeatureTags), + }; + + if (options.TextRuns.Count > 0) + { + List runs = new(options.TextRuns.Count); + foreach (TextRun run in options.TextRuns) + { + runs.Add(new RichTextRun() + { + Font = run.Font, + Start = run.Start, + End = run.End, + TextAttributes = run.TextAttributes, + TextDecorations = run.TextDecorations + }); + } + } + + return result; + } +#endif +} diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 497d7836..340aef0a 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -4,12 +4,8 @@ using System.Globalization; using System.Numerics; using SixLabors.Fonts.Tests.Fakes; -using SixLabors.Fonts.Tests.TestUtilities; using SixLabors.Fonts.Unicode; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; -using SixLabors.ImageSharp.PixelFormats; -using SixLabors.ImageSharp.Processing; namespace SixLabors.Fonts.Tests; @@ -276,172 +272,195 @@ public void TryMeasureCharacterBounds() } [Theory] - [InlineData("hello world", 10, 310)] - [InlineData( - "hello world hello world hello world", - 70, // 30 actual line height * 2 + 10 actual height - 310)] + [InlineData("hello world", 10, 87.125F)] + [InlineData("hello world hello world hello world", 11.438F, 279.13F)] [InlineData(// issue https://github.com/SixLabors/ImageSharp.Drawing/issues/115 "这是一段长度超出设定的换行宽度的文本,但是没有在设定的宽度处换行。这段文本用于演示问题。希望可以修复。如果有需要可以联系我。", - 160, // 30 actual line height * 2 + 10 actual height - 310)] + 62.625, + 318.86F)] public void MeasureTextWordWrappingHorizontalTopBottom(string text, float height, float width) { - Font font = CreateFont(text); - FontRectangle size = TextMeasurer.MeasureBounds(text, new TextOptions(font) + if (SystemFonts.TryGet("SimSun", out FontFamily family)) { - Dpi = font.FontMetrics.ScaleFactor, - WrappingLength = 350, - LayoutMode = LayoutMode.HorizontalTopBottom - }); + Font font = family.CreateFont(16); + TextOptions options = new(font) + { + WrappingLength = 350, + LayoutMode = LayoutMode.HorizontalTopBottom + }; - Assert.Equal(width, size.Width, 4F); - Assert.Equal(height, size.Height, 4F); + FontRectangle size = TextMeasurer.MeasureBounds(text, options); + + Assert.Equal(width, size.Width, 4F); + Assert.Equal(height, size.Height, 4F); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); + } } [Theory] - [InlineData("hello world", 10, 310)] - [InlineData( - "hello world hello world hello world", - 70, // 30 actual line height * 2 + 10 actual height - 310)] + [InlineData("hello world", 10, 87.125F)] + [InlineData("hello world hello world hello world", 11.438F, 279.13F)] [InlineData(// issue https://github.com/SixLabors/ImageSharp.Drawing/issues/115 "这是一段长度超出设定的换行宽度的文本,但是没有在设定的宽度处换行。这段文本用于演示问题。希望可以修复。如果有需要可以联系我。", - 160, // 30 actual line height * 2 + 10 actual height - 310)] + 62.625, + 318.86F)] public void MeasureTextWordWrappingHorizontalBottomTop(string text, float height, float width) { - Font font = CreateFont(text); - FontRectangle size = TextMeasurer.MeasureBounds(text, new TextOptions(font) + if (SystemFonts.TryGet("SimSun", out FontFamily family)) { - Dpi = font.FontMetrics.ScaleFactor, - WrappingLength = 350, - LayoutMode = LayoutMode.HorizontalBottomTop - }); + Font font = family.CreateFont(16); + TextOptions options = new(font) + { + WrappingLength = 350, + LayoutMode = LayoutMode.HorizontalBottomTop + }; - Assert.Equal(width, size.Width, 4F); - Assert.Equal(height, size.Height, 4F); + FontRectangle size = TextMeasurer.MeasureBounds(text, options); + + Assert.Equal(width, size.Width, 4F); + Assert.Equal(height, size.Height, 4F); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); + } } [Theory] - [InlineData("hello world", 310, 10)] - [InlineData("hello world hello world hello world", 310, 70)] - [InlineData("这是一段长度超出设定的换行宽度的文本,但是没有在设定的宽度处换行。这段文本用于演示问题。希望可以修复。如果有需要可以联系我。", 310, 160)] + [InlineData("hello world", 171.25F, 10)] + [InlineData("hello world hello world hello world", 267.25F, 23.875F)] + [InlineData("这是一段长度超出设定的换行宽度的文本,但是没有在设定的宽度处换行。这段文本用于演示问题。希望可以修复。如果有需要可以联系我。", 318.563F, 62.813F)] public void MeasureTextWordWrappingVerticalLeftRight(string text, float height, float width) { - Font font = CreateFont(text); - FontRectangle size = TextMeasurer.MeasureBounds(text, new TextOptions(font) + if (SystemFonts.TryGet("SimSun", out FontFamily family)) { - Dpi = font.FontMetrics.ScaleFactor, - WrappingLength = 350, - LayoutMode = LayoutMode.VerticalLeftRight - }); + Font font = family.CreateFont(16); + TextOptions options = new(font) + { + WrappingLength = 350, + LayoutMode = LayoutMode.VerticalLeftRight + }; - Assert.Equal(width, size.Width, 4F); - Assert.Equal(height, size.Height, 4F); + FontRectangle size = TextMeasurer.MeasureBounds(text, options); + + Assert.Equal(width, size.Width, 4F); + Assert.Equal(height, size.Height, 4F); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); + } } [Theory] - [InlineData("hello world", 310, 10)] - [InlineData("hello world hello world hello world", 310, 70)] - [InlineData("这是一段长度超出设定的换行宽度的文本,但是没有在设定的宽度处换行。这段文本用于演示问题。希望可以修复。如果有需要可以联系我。", 310, 160)] + [InlineData("hello world", 171.25F, 10)] + [InlineData("hello world hello world hello world", 267.25F, 23.875F)] + [InlineData("这是一段长度超出设定的换行宽度的文本,但是没有在设定的宽度处换行。这段文本用于演示问题。希望可以修复。如果有需要可以联系我。", 318.563F, 62.813F)] public void MeasureTextWordWrappingVerticalRightLeft(string text, float height, float width) { - Font font = CreateFont(text); - FontRectangle size = TextMeasurer.MeasureBounds(text, new TextOptions(font) + if (SystemFonts.TryGet("SimSun", out FontFamily family)) { - Dpi = font.FontMetrics.ScaleFactor, - WrappingLength = 350, - LayoutMode = LayoutMode.VerticalRightLeft - }); + Font font = family.CreateFont(16); + TextOptions options = new(font) + { + WrappingLength = 350, + LayoutMode = LayoutMode.VerticalRightLeft + }; - Assert.Equal(width, size.Width, 4F); - Assert.Equal(height, size.Height, 4F); + FontRectangle size = TextMeasurer.MeasureBounds(text, options); + + Assert.Equal(width, size.Width, 4F); + Assert.Equal(height, size.Height, 4F); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); + } } [Theory] - [InlineData("hello world", 310, 10)] - [InlineData("hello world hello world hello world", 310, 70)] - [InlineData("这是一段长度超出设定的换行宽度的文本,但是没有在设定的宽度处换行。这段文本用于演示问题。希望可以修复。如果有需要可以联系我。", 310, 160)] + [InlineData("hello world", 87.125F, 10)] + [InlineData("hello world hello world hello world", 279.125F, 11.438F)] + [InlineData("这是一段长度超出设定的换行宽度的文本,但是没有在设定的宽度处换行。这段文本用于演示问题。希望可以修复。如果有需要可以联系我。", 318.563F, 62.813F)] public void MeasureTextWordWrappingVerticalMixedLeftRight(string text, float height, float width) { - Font font = CreateFont(text); - FontRectangle size = TextMeasurer.MeasureBounds(text, new TextOptions(font) + if (SystemFonts.TryGet("SimSun", out FontFamily family)) { - Dpi = font.FontMetrics.ScaleFactor, - WrappingLength = 350, - LayoutMode = LayoutMode.VerticalMixedLeftRight - }); + Font font = family.CreateFont(16); + TextOptions options = new(font) + { + WrappingLength = 350, + LayoutMode = LayoutMode.VerticalMixedLeftRight + }; - Assert.Equal(width, size.Width, 4F); - Assert.Equal(height, size.Height, 4F); + FontRectangle size = TextMeasurer.MeasureBounds(text, options); + + Assert.Equal(width, size.Width, 4F); + Assert.Equal(height, size.Height, 4F); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { height, width }); + } } -#if OS_WINDOWS [Theory] - [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Standard, 100, 870)] - //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 120, 399)] - //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakWord, 120, 400)] - //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 60, 699)] - //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Standard, 101, 870)] - //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 121, 399)] - //[InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakWord, 121, 400)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 61, 699)] + [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Standard, 100, 696.51F)] + [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 129.29F, 237.53F)] + [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakWord, 128, 237.53F)] + [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 65.29F, 699)] + [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Standard, 96F, 696.51F)] + [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 129.29F, 237.53F)] + [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakWord, 128, 237.53F)] + [InlineData("Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 61, 699)] public void MeasureTextWordBreakMatchesMDN(string text, LayoutMode layoutMode, WordBreaking wordBreaking, float height, float width) { - // Testing using Windows only to ensure that actual glyphs are rendered - // against known physically tested values. - FontFamily arial = SystemFonts.Get("Arial"); - FontFamily jhengHei = SystemFonts.Get("Microsoft JhengHei"); - - Font font = arial.CreateFont(16); - FontRectangle size = TextMeasurer.MeasureAdvance( - text, - new TextOptions(font) + // See https://developer.mozilla.org/en-US/docs/Web/CSS/word-break + if (SystemFonts.TryGet("Arial", out FontFamily arial) && + SystemFonts.TryGet("Microsoft JhengHei", out FontFamily jhengHei)) + { + Font font = arial.CreateFont(16); + TextOptions options = new(font) { - Dpi = 96, WrappingLength = 238, LayoutMode = layoutMode, WordBreaking = wordBreaking, FallbackFontFamilies = new[] { jhengHei } - }); + }; - Assert.Equal(width, size.Width, 4F); - Assert.Equal(height, size.Height, 4F); - } + FontRectangle size = TextMeasurer.MeasureAdvance( + text, + options); + Assert.Equal(width, size.Width, 4F); + Assert.Equal(height, size.Height, 4F); + + TextLayoutTestUtilities.TestLayout(text, options, properties: new { layoutMode, wordBreaking }); + } + } [Theory] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Standard, 100, 870)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 120, 399)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakWord, 120, 400)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 60, 699)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Standard, 101, 870)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 121, 399)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakWord, 121, 400)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.Standard, 100, 870.635F)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakAll, 100, 500)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.BreakWord, 120, 490.35F)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalTopBottom, WordBreaking.KeepAll, 81.89F, 870.635F)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.Standard, 101, 870.635F)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakAll, 100, 500)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.BreakWord, 121, 490.35F)] [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", LayoutMode.HorizontalBottomTop, WordBreaking.KeepAll, 61, 699)] public void MeasureTextWordBreak(string text, LayoutMode layoutMode, WordBreaking wordBreaking, float height, float width) { - // Testing using Windows only to ensure that actual glyphs are rendered - // against known physically tested values. - FontFamily arial = SystemFonts.Get("Arial"); - FontFamily jhengHei = SystemFonts.Get("Microsoft JhengHei"); - - Font font = arial.CreateFont(20); - FontRectangle size = TextMeasurer.MeasureAdvance( - text, - new TextOptions(font) + // See https://developer.mozilla.org/en-US/docs/Web/CSS/word-break + if (SystemFonts.TryGet("Arial", out FontFamily arial) && + SystemFonts.TryGet("Microsoft JhengHei", out FontFamily jhengHei)) + { + Font font = arial.CreateFont(20); + TextOptions options = new(font) { - WrappingLength = 400, + WrappingLength = 500, LayoutMode = layoutMode, WordBreaking = wordBreaking, FallbackFontFamilies = new[] { jhengHei } - }); + }; - Assert.Equal(width, size.Width, 4F); - Assert.Equal(height, size.Height, 4F); + FontRectangle size = TextMeasurer.MeasureAdvance( + text, + options); + + Assert.Equal(width, size.Width, 4F); + Assert.Equal(height, size.Height, 4F); + + TextLayoutTestUtilities.TestLayout(text, options, properties: new { layoutMode, wordBreaking }); + } } -#endif [Theory] [InlineData("ab", 477, 1081, false)] // no kerning rules defined for lowercase ab so widths should stay the same @@ -530,30 +549,21 @@ public void CountLinesWithSpan() } [Theory] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 25, 7)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 50, 7)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 100, 7)] - [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 200, 6)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 25, 6)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 50, 5)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 100, 4)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 200, 3)] public void CountLinesWrappingLength(string text, int wrappingLength, int usedLines) { Font font = CreateRenderingFont(); RichTextOptions options = new(font) { - // Dpi = font.FontMetrics.ScaleFactor, WrappingLength = wrappingLength }; int count = TextMeasurer.CountLines(text, options); - - // Assert.Equal(usedLines, count); - FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); - int width = (int)Math.Ceiling(advance.Width); - int height = (int)Math.Ceiling(advance.Height); - - using Image img = new(Math.Max(wrappingLength + 1, width), height, Color.White); - img.Mutate(x => x.DrawLine(Color.Red, 1, new(wrappingLength, 0), new(wrappingLength, height))); - img.Mutate(ctx => ctx.DrawText(options, text, Color.Black)); - img.DebugSave(properties: new { wrappingLength, usedLines }); + Assert.Equal(usedLines, count); + TextLayoutTestUtilities.TestLayout(text, options, properties: usedLines); } [Fact] @@ -660,8 +670,8 @@ public void TextJustification_InterCharacter_Horizontal(TextDirection direction) { const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; const float wrappingLength = 400; - const float pointSize = 20; - Font font = CreateFont(text, pointSize); + const float pointSize = 12; + Font font = CreateRenderingFont(pointSize); TextOptions options = new(font) { TextDirection = direction, @@ -672,9 +682,11 @@ public void TextJustification_InterCharacter_Horizontal(TextDirection direction) // Collect the first line so we can compare it to the target wrapping length. IReadOnlyList justifiedGlyphs = TextLayout.GenerateLayout(text.AsSpan(), options); IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); - TextMeasurer.TryGetCharacterAdvances(justifiedLine, options.Dpi, out ReadOnlySpan justifiedCharacterBounds); + TextMeasurer.TryGetCharacterAdvances(justifiedLine, options.Dpi, out ReadOnlySpan advances); - Assert.Equal(wrappingLength, justifiedCharacterBounds.ToArray().Sum(x => x.Bounds.Width), 4F); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, options.TextJustification }); + + Assert.Equal(wrappingLength, advances.ToArray().Sum(x => x.Bounds.Width), 4F); // Now compare character widths. options.TextJustification = TextJustification.None; @@ -688,11 +700,11 @@ public void TextJustification_InterCharacter_Horizontal(TextDirection direction) { if (i == characterBounds.Length - 1) { - Assert.Equal(justifiedCharacterBounds[i].Bounds.Width, characterBounds[i].Bounds.Width); + Assert.Equal(advances[i].Bounds.Width, characterBounds[i].Bounds.Width); } else { - Assert.True(justifiedCharacterBounds[i].Bounds.Width > characterBounds[i].Bounds.Width); + Assert.True(advances[i].Bounds.Width > characterBounds[i].Bounds.Width); } } } @@ -704,8 +716,8 @@ public void TextJustification_InterWord_Horizontal(TextDirection direction) { const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; const float wrappingLength = 400; - const float pointSize = 20; - Font font = CreateFont(text, pointSize); + const float pointSize = 12; + Font font = CreateRenderingFont(pointSize); TextOptions options = new(font) { TextDirection = direction, @@ -718,6 +730,8 @@ public void TextJustification_InterWord_Horizontal(TextDirection direction) IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); TextMeasurer.TryGetCharacterAdvances(justifiedLine, options.Dpi, out ReadOnlySpan justifiedCharacterBounds); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, options.TextJustification }); + Assert.Equal(wrappingLength, justifiedCharacterBounds.ToArray().Sum(x => x.Bounds.Width), 4F); // Now compare character widths. @@ -748,8 +762,8 @@ public void TextJustification_InterCharacter_Vertical(TextDirection direction) { const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; const float wrappingLength = 400; - const float pointSize = 20; - Font font = CreateFont(text, pointSize); + const float pointSize = 12; + Font font = CreateRenderingFont(pointSize); TextOptions options = new(font) { LayoutMode = LayoutMode.VerticalLeftRight, @@ -763,6 +777,8 @@ public void TextJustification_InterCharacter_Vertical(TextDirection direction) IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); TextMeasurer.TryGetCharacterAdvances(justifiedLine, options.Dpi, out ReadOnlySpan justifiedCharacterBounds); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, options.TextJustification }); + Assert.Equal(wrappingLength, justifiedCharacterBounds.ToArray().Sum(x => x.Bounds.Height), 4F); // Now compare character widths. @@ -793,8 +809,8 @@ public void TextJustification_InterWord_Vertical(TextDirection direction) { const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; const float wrappingLength = 400; - const float pointSize = 20; - Font font = CreateFont(text, pointSize); + const float pointSize = 12; + Font font = CreateRenderingFont(pointSize); TextOptions options = new(font) { LayoutMode = LayoutMode.VerticalLeftRight, @@ -808,6 +824,8 @@ public void TextJustification_InterWord_Vertical(TextDirection direction) IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); TextMeasurer.TryGetCharacterAdvances(justifiedLine, options.Dpi, out ReadOnlySpan justifiedCharacterBounds); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, options.TextJustification }); + Assert.Equal(wrappingLength, justifiedCharacterBounds.ToArray().Sum(x => x.Bounds.Height), 4F); // Now compare character widths. @@ -1370,10 +1388,8 @@ public FontRectangle BenchmarkTest() private static readonly Font Arial = SystemFonts.CreateFont("Arial", 12); #endif - public static Font CreateRenderingFont() - { - return new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(12); - } + public static Font CreateRenderingFont(float pointSize = 12) + => new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(pointSize); public static Font CreateFont(string text) { From 33fcad7e4b9b51f5556f92d19da564c83c09a91b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 7 Jan 2025 19:45:33 +1000 Subject: [PATCH 04/11] Update .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d213743d..68bc8687 100644 --- a/.gitignore +++ b/.gitignore @@ -254,6 +254,8 @@ paket-files/ *.sln.iml /samples/DrawWithImageSharp/Output +# Tests +**/Images/ActualOutput /tests/CodeCoverage/OpenCover.* SixLabors.Shapes.Coverage.xml /SixLabors.Fonts.Coverage.xml From 8b6de4e0c455d6246330c4a6221a15180d1819ce Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 8 Jan 2025 13:37:38 +1000 Subject: [PATCH 05/11] Add bin logging --- .github/workflows/build-and-test.yml | 4 +++- ci-build.ps1 | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index f9d086d7..eda8224b 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -155,7 +155,9 @@ jobs: if: failure() with: name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip - path: tests/Images/ActualOutput/ + path: | + tests/Images/ActualOutput/ + msbuild.log - name: Codecov Update uses: codecov/codecov-action@v4 diff --git a/ci-build.ps1 b/ci-build.ps1 index d45af6ff..7cecb4f4 100644 --- a/ci-build.ps1 +++ b/ci-build.ps1 @@ -8,4 +8,4 @@ dotnet clean -c Release $repositoryUrl = "https://github.com/$env:GITHUB_REPOSITORY" # Building for a specific framework. -dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl +dotnet build -c Release -f $targetFramework /p:RepositoryUrl=$repositoryUrl -bl From 4b9ebb92bbb2b0c9b762742ae4c7210f8d8a441b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 8 Jan 2025 13:40:25 +1000 Subject: [PATCH 06/11] Use correct log file name --- .github/workflows/build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index eda8224b..27f189f0 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -157,7 +157,7 @@ jobs: name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip path: | tests/Images/ActualOutput/ - msbuild.log + **/msbuild.binlog - name: Codecov Update uses: codecov/codecov-action@v4 From dfedca528bf28b84af3fcd5b2d754c3f7906af22 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 8 Jan 2025 14:01:29 +1000 Subject: [PATCH 07/11] Temporarily disable the COM analyzer to work around build issue --- src/SixLabors.Fonts/SixLabors.Fonts.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SixLabors.Fonts/SixLabors.Fonts.csproj b/src/SixLabors.Fonts/SixLabors.Fonts.csproj index 0cf7745d..b90f8cfd 100644 --- a/src/SixLabors.Fonts/SixLabors.Fonts.csproj +++ b/src/SixLabors.Fonts/SixLabors.Fonts.csproj @@ -19,6 +19,9 @@ enable Nullable + + + $(NoWarn);IL2050; From 237eeab380a308529be8bdf67140d751857746ef Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 8 Jan 2025 14:43:57 +1000 Subject: [PATCH 08/11] Use langversion 10 --- .../ImageComparison/TestImageExtensions.cs | 9 ++------- tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs | 12 ++++++++---- .../SixLabors.Fonts.Tests.csproj | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs index 921d2cbe..6fce63c6 100644 --- a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs @@ -84,9 +84,7 @@ public static string FormatTestDetails(object properties) } else if (properties is Dictionary dictionary) { - return FormattableString.Invariant($"_{string.Join( - "-", - dictionary.Select(x => FormattableString.Invariant($"{x.Key}_{x.Value}")))}_"); + return FormattableString.Invariant($"_{string.Join("-", dictionary.Select(x => FormattableString.Invariant($"{x.Key}_{x.Value}")))}_"); } Type type = properties.GetType(); @@ -97,9 +95,6 @@ public static string FormatTestDetails(object properties) } IEnumerable runtimeProperties = type.GetRuntimeProperties(); - return FormattableString.Invariant($"_{string.Join( - "-", - runtimeProperties.ToDictionary(x => x.Name, x => x.GetValue(properties)) - .Select(x => FormattableString.Invariant($"{x.Key}_{x.Value}")))}_"); + return FormattableString.Invariant($"_{string.Join("-", runtimeProperties.ToDictionary(x => x.Name, x => x.GetValue(properties)).Select(x => FormattableString.Invariant($"{x.Key}_{x.Value}")))}_"); } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs index 12f61e8e..fd9ec3cf 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Text; + namespace SixLabors.Fonts.Tests.Issues; public class Issues_400 { @@ -14,11 +16,13 @@ public void RenderingTextIncludesAllGlyphs() WrappingLength = 1900 }; - const string content = """ - NEWS_CATEGORY=EWF&NEWS_HASH=4b298ff9277ef9fdf515356be95ea3caf57cd36&OFFSET=0&SEARCH_VALUE=CA88105E1088&ID_NEWS - """; + StringBuilder stringBuilder = new(); + stringBuilder + .AppendLine() + .AppendLine(" NEWS_CATEGORY=EWF&NEWS_HASH=4b298ff9277ef9fdf515356be95ea3caf57cd36&OFFSET=0&SEARCH_VALUE=CA88105E1088&ID_NEWS") + .Append(" "); - int lineCount = TextMeasurer.CountLines(content, options); + int lineCount = TextMeasurer.CountLines(stringBuilder.ToString(), options); Assert.Equal(2, lineCount); #endif } diff --git a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj index f94c0a62..686eaa70 100644 --- a/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj +++ b/tests/SixLabors.Fonts.Tests/SixLabors.Fonts.Tests.csproj @@ -3,7 +3,7 @@ True AnyCPU;x64;x86 - 11 + 10 From a214311eb44781126100f01d80ea3fc4b06a3c91 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 8 Jan 2025 16:38:55 +1000 Subject: [PATCH 09/11] Up default tolerance --- .../ImageComparison/TestImageExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs index 6fce63c6..80183068 100644 --- a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs @@ -32,7 +32,7 @@ public static string DebugSave( public static void CompareToReference( this Image image, - float percentageTolerance = 0F, + float percentageTolerance = 0.05F, string extension = null, [CallerMemberName] string test = "", params object[] properties) From 079ebc67323fd2775da5f5b19c47f29fb7e726cc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 8 Jan 2025 16:47:47 +1000 Subject: [PATCH 10/11] Use correct constructor --- .../ImageComparison/TestImageExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs index 80183068..5fe7d3c9 100644 --- a/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs +++ b/tests/SixLabors.Fonts.Tests/ImageComparison/TestImageExtensions.cs @@ -47,7 +47,7 @@ public static void CompareToReference( } using Image expected = Image.Load(referencePath); - TolerantImageComparer comparer = new(percentageTolerance / 100F); + ImageComparer comparer = ImageComparer.TolerantPercentage(percentageTolerance); ImageSimilarityReport report = comparer.CompareImagesOrFrames(expected, image); if (!report.IsEmpty) From 62920eea86ebda19e5a6251fe8bd5f31c11568bd Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Wed, 8 Jan 2025 18:06:57 +1000 Subject: [PATCH 11/11] Update TextLayoutTestUtilities.cs --- tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs index 266f4220..51c68030 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs @@ -19,7 +19,7 @@ internal static class TextLayoutTestUtilities public static void TestLayout( string text, TextOptions options, - float percentageTolerance = 0F, + float percentageTolerance = 0.05F, [CallerMemberName] string test = "", params object[] properties) {