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) {