From 59307932857a2f3934047937f2143f7a3beb4533 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 8 Feb 2024 20:07:46 +1000 Subject: [PATCH 1/5] Correctly read woff2 bounds --- src/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs | 3 +- .../Fonts/PermanentMarker-Regular.ttf | Bin 0 -> 73620 bytes .../Fonts/PermanentMarker-Regular.woff2 | Bin 0 -> 29564 bytes .../Issues/Issues_375.cs | 30 ++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 tests/SixLabors.Fonts.Tests/Fonts/PermanentMarker-Regular.ttf create mode 100644 tests/SixLabors.Fonts.Tests/Fonts/PermanentMarker-Regular.woff2 create mode 100644 tests/SixLabors.Fonts.Tests/Issues/Issues_375.cs diff --git a/src/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs b/src/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs index fe2923cd..05e76d74 100644 --- a/src/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs +++ b/src/SixLabors.Fonts/Tables/Woff/Woff2Utils.cs @@ -237,7 +237,8 @@ public static GlyphLoader[] LoadAllGlyphs(BigEndianBinaryReader reader, EmptyGly } // Read the bounding box stream. - int bitmapCount = (numGlyphs + 7) / 8; + reader.BaseStream.Position = bboxStreamOffset; + int bitmapCount = ((numGlyphs + 31) >> 5) << 2; byte[] boundsBitmap = ExpandBitmap(reader.ReadBytes(bitmapCount)); for (ushort i = 0; i < numGlyphs; i++) { diff --git a/tests/SixLabors.Fonts.Tests/Fonts/PermanentMarker-Regular.ttf b/tests/SixLabors.Fonts.Tests/Fonts/PermanentMarker-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..6541e9d87e656f7b341c853abb4ced9a6397ef39 GIT binary patch literal 73620 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
    S}D~Xz1ggD{L7{ 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 Jo`3ha{{z9RE`|UA literal 0 HcmV?d00001 diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_375.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_375.cs new file mode 100644 index 00000000..29c7e019 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_375.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tests.Issues; +public class Issues_375 +{ + [Fact] + public void DiacriticsAreMeasuredCorrectly() + { + Font font = new FontCollection().Add(TestFonts.PermanentMarkerRegularFile).CreateFont(142); + + TextOptions options = new(font); + + FontRectangle bounds = TextMeasurer.MeasureBounds("È", options); + FontRectangle size = TextMeasurer.MeasureSize("È", options); + FontRectangle advance = TextMeasurer.MeasureAdvance("È", options); + + Font fontWoff = new FontCollection().Add(TestFonts.PermanentMarkerRegularWoff2File).CreateFont(142); + + TextOptions optionsWoff = new(fontWoff); + + FontRectangle boundsWoff = TextMeasurer.MeasureBounds("È", optionsWoff); + FontRectangle sizeWoff = TextMeasurer.MeasureSize("È", optionsWoff); + FontRectangle advanceWoff = TextMeasurer.MeasureAdvance("È", optionsWoff); + + Assert.Equal(bounds, boundsWoff); + Assert.Equal(size, sizeWoff); + Assert.Equal(advance, advanceWoff); + } +} From 203dac4c62d2565c2e02a2cdd787bfc15dc319d3 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 8 Feb 2024 20:08:22 +1000 Subject: [PATCH 2/5] Fix bounds and size measurement --- samples/DrawWithImageSharp/BoundingBoxes.cs | 40 +- .../DrawWithImageSharp/CustomGlyphBuilder.cs | 8 +- samples/DrawWithImageSharp/Program.cs | 108 ++--- src/SixLabors.Fonts/GlyphLayout.cs | 4 +- src/SixLabors.Fonts/GlyphMetrics.cs | 34 +- .../GlyphSubstitutionCollection.cs | 30 ++ .../Tables/Cff/CffGlyphMetrics.cs | 2 +- .../Tables/TrueType/TrueTypeGlyphMetrics.cs | 2 +- src/SixLabors.Fonts/TextLayout.cs | 20 +- src/SixLabors.Fonts/TextMeasurer.cs | 17 +- src/SixLabors.Fonts/Unicode/UnicodeUtility.cs | 33 ++ tests/SixLabors.Fonts.Tests/GlyphTests.cs | 10 +- .../Issues/Issues_180.cs | 4 +- .../Issues/Issues_269.cs | 2 +- .../SixLabors.Fonts.Tests/Issues/Issues_32.cs | 6 +- tests/SixLabors.Fonts.Tests/TestFonts.cs | 4 + .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 380 +++++++++--------- 17 files changed, 386 insertions(+), 318 deletions(-) diff --git a/samples/DrawWithImageSharp/BoundingBoxes.cs b/samples/DrawWithImageSharp/BoundingBoxes.cs index 4040e424..97791c9b 100644 --- a/samples/DrawWithImageSharp/BoundingBoxes.cs +++ b/samples/DrawWithImageSharp/BoundingBoxes.cs @@ -8,41 +8,49 @@ using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using IOPath = System.IO.Path; namespace DrawWithImageSharp; public static class BoundingBoxes { - public static void Generate(string text, Font font) + public static void Generate(string text, TextOptions options) { - using var img = new Image(1000, 1000); - img.Mutate(x => x.Fill(Color.White)); + FontRectangle bounds = TextMeasurer.MeasureBounds(text, options); + FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); + using Image image = new((int)Math.Ceiling(options.Origin.X + (Math.Max(advance.Width, bounds.Width) + 1)), (int)Math.Ceiling(options.Origin.Y + (Math.Max(advance.Height, bounds.Height) + 1))); + image.Mutate(x => x.Fill(Color.White)); - TextOptions options = new(font); - FontRectangle box = TextMeasurer.MeasureBounds(text, options); - (IPathCollection paths, IPathCollection boxes) = GenerateGlyphsWithBox(text, options); - - Rgba32 f = Color.Fuchsia; - f.A = 128; + Vector2 origin = options.Origin; - img.Mutate(x => x.Fill(Color.Black, paths) - .Draw(f, 1, boxes) - .Draw(Color.Lime, 1, new RectangularPolygon(box.Location, box.Size))); + FontRectangle size = TextMeasurer.MeasureSize(text, options); - img.Save("Output/Boxed.png"); + (IPathCollection paths, IPathCollection boxes) = GenerateGlyphsWithBox(text, options); + image.Mutate( + x => x.Fill(Color.Black, paths) + .Draw(Color.Yellow, 1, boxes) + .Draw(Color.Purple, 1, new RectangularPolygon(bounds.X, bounds.Y, bounds.Width, bounds.Height)) + .Draw(Color.Green, 1, new RectangularPolygon(size.X + bounds.X, size.Y + bounds.Y, size.Width, size.Height)) + .Draw(Color.Red, 1, new RectangularPolygon(advance.X + origin.X, advance.Y + origin.Y, advance.Width, advance.Height))); + + string path = IOPath.GetInvalidFileNameChars().Aggregate(text, (x, c) => x.Replace($"{c}", "-")); + string fullPath = IOPath.GetFullPath(IOPath.Combine($"Output/Boxed/{options.Font.Name}", IOPath.Combine(path))); + Directory.CreateDirectory(IOPath.GetDirectoryName(fullPath)); + + image.Save($"{fullPath}.png"); } /// - /// Generates the shapes corresponding the glyphs described by the font and with the setting ing withing the FontSpan + /// Generates the shapes corresponding the glyphs described by the font and settings. /// /// The text to generate glyphs for /// The style and settings to use while rendering the glyphs /// The paths, boxes, and text box. private static (IPathCollection Paths, IPathCollection Boxes) GenerateGlyphsWithBox(string text, TextOptions options) { - var glyphBuilder = new CustomGlyphBuilder(Vector2.Zero); + CustomGlyphBuilder glyphBuilder = new(); - var renderer = new TextRenderer(glyphBuilder); + TextRenderer renderer = new(glyphBuilder); renderer.RenderText(text, options); diff --git a/samples/DrawWithImageSharp/CustomGlyphBuilder.cs b/samples/DrawWithImageSharp/CustomGlyphBuilder.cs index 82a2e886..049e2ca9 100644 --- a/samples/DrawWithImageSharp/CustomGlyphBuilder.cs +++ b/samples/DrawWithImageSharp/CustomGlyphBuilder.cs @@ -27,17 +27,17 @@ public CustomGlyphBuilder(Vector2 origin) /// /// Gets the paths that have been rendered by this. /// - public IPathCollection Boxes => new PathCollection(this.glyphBounds.Select(x => new RectangularPolygon(x.Location, x.Size))); + public IPathCollection Boxes => new PathCollection(this.glyphBounds.Select(x => new RectangularPolygon(x.X, x.Y, x.Width, x.Height))); /// /// Gets the paths that have been rendered by this builder. /// public IPath TextBox { get; private set; } - protected override void BeginText(in FontRectangle rect) + protected override void BeginText(in FontRectangle bounds) { - this.TextBox = new RectangularPolygon(rect.Location, rect.Size); - base.BeginText(rect); + this.TextBox = new RectangularPolygon(bounds.X, bounds.Y, bounds.Width, bounds.Height); + base.BeginText(bounds); } protected override void BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) diff --git a/samples/DrawWithImageSharp/Program.cs b/samples/DrawWithImageSharp/Program.cs index 86fbb2eb..6421cd32 100644 --- a/samples/DrawWithImageSharp/Program.cs +++ b/samples/DrawWithImageSharp/Program.cs @@ -28,11 +28,21 @@ public static void Main(string[] args) FontFamily wendyOne = fonts.Add(IOPath.Combine("Fonts", "WendyOne-Regular.ttf")); FontFamily whitneyBook = fonts.Add(IOPath.Combine("Fonts", "whitney-book.ttf")); FontFamily colorEmoji = fonts.Add(IOPath.Combine("Fonts", "Twemoji Mozilla.ttf")); - FontFamily font2 = fonts.Add(IOPath.Combine("Fonts", "OpenSans-Regular.ttf")); + FontFamily openSans = fonts.Add(IOPath.Combine("Fonts", "OpenSans-Regular.ttf")); FontFamily sunflower = fonts.Add(IOPath.Combine("Fonts", "Sunflower-Medium.ttf")); FontFamily bugzilla = fonts.Add(IOPath.Combine("Fonts", "me_quran_volt_newmet.ttf")); FontFamily notoKR = fonts.Add(IOPath.Combine("Fonts", "NotoSansKR-Regular.otf")); + FontFamily marker = fonts.Add(IOPath.Combine("Fonts", "PermanentMarker-Regular.ttf")); + + FontFamily sEmji = fonts.Add(IOPath.Combine("Fonts", "seguiemj-win11.ttf")); + BoundingBoxes.Generate("\U0001F469\U0001F3FB\u200D\U0001F91D\u200D\U0001F469\U0001F3FC", new TextOptions(sEmji.CreateFont(72)) { LineSpacing = 1.4f }); + BoundingBoxes.Generate("\U0001F46D\U0001F3FB", new TextOptions(sEmji.CreateFont(72)) { LineSpacing = 1.4f }); + BoundingBoxes.Generate("È", new TextOptions(marker.CreateFont(142)) { LineSpacing = 1.4f }); + BoundingBoxes.Generate("H", new TextOptions(whitneyBook.CreateFont(25))); + + openSans.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics); + BoundingBoxes.Generate("A\nA\nA\nA", new TextOptions(openSans.CreateFont(metrics.UnitsPerEm)) { LineSpacing = 1.5f }); RenderText(notoKR, "\uD734", pointSize: 72); RenderText(notoKR, "Sphinx of black quartz, judge my vow!", pointSize: 72); @@ -47,16 +57,28 @@ public static void Main(string[] args) #if OS_WINDOWS + FontFamily arial = SystemFonts.Get("Arial"); + FontFamily jhengHei = SystemFonts.Get("Microsoft JhengHei"); FontFamily emojiFont = SystemFonts.Get("Segoe UI Emoji"); FontFamily uiFont = SystemFonts.Get("Segoe UI"); FontFamily arabicFont = SystemFonts.Get("Dubai"); - FontFamily tahoma = SystemFonts.Get("Tahoma"); - RenderText(SystemFonts.Get("Arial"), "abcdefghijklmnopqrstuvwxyz", pointSize: 30); - RenderText(SystemFonts.Get("Arial"), "abcdefghijklmnopqrstuvwxyz\r\nabcdefghijklmnopqrstuvwxyz", pointSize: 30); - RenderText(SystemFonts.Get("Arial"), "abcdef ghijk lmnopq rstuvwxyz", pointSize: 30); + BoundingBoxes.Generate( + "This is a long and Honorificabilitudinitatibus califragilisticexpialidocious Taumatawhakatangihangakoauauotamateaturipukakapikimaungahoronukupokaiwhenuakitanatahu グレートブリテンおよび北アイルランド連合王国という言葉は本当に長い言葉", + new TextOptions(arial.CreateFont(20)) + { + WrappingLength = 400, + LayoutMode = LayoutMode.HorizontalBottomTop, + WordBreaking = WordBreaking.Standard, + FallbackFontFamilies = new[] { jhengHei } + }); + return; + RenderText(arial, "abcdefghijklmnopqrstuvwxyz", pointSize: 30); + RenderText(arial, "abcdefghijklmnopqrstuvwxyz\r\nabcdefghijklmnopqrstuvwxyz", pointSize: 30); + RenderText(arial, "abcdef ghijk lmnopq rstuvwxyz", pointSize: 30); + // return; var textRuns = new List { @@ -80,7 +102,7 @@ public static void Main(string[] args) }; RenderText(bugzilla, arabic, pointSize: 72, textRuns: textRuns); - RenderText(font2, "\uFB01", pointSize: 11.25F); + RenderText(openSans, "\uFB01", pointSize: 11.25F); RenderText(fontWoff2, "\uFB01", pointSize: 11.25F); RenderText(tahoma, "p", pointSize: 11.25F); RenderText(tahoma, "Lorem ipsum dolor sit amet", pointSize: 11.25F); @@ -88,11 +110,11 @@ public static void Main(string[] args) RenderText(uiFont, "Soft\u00ADHyphen", pointSize: 72); - RenderText(uiFont, "first\n\n\n\nl", pointSize: 20, fallbackFonts: new[] { font2 }); + RenderText(uiFont, "first\n\n\n\nl", pointSize: 20, fallbackFonts: new[] { openSans }); - RenderText(uiFont, "first\n\n\n\nlast", pointSize: 20, fallbackFonts: new[] { font2 }); + RenderText(uiFont, "first\n\n\n\nlast", pointSize: 20, fallbackFonts: new[] { openSans }); RenderText(uiFont, "Testing", pointSize: 20); - RenderText(emojiFont, "👩🏽‍🚒a", pointSize: 72, fallbackFonts: new[] { font2 }); + RenderText(emojiFont, "👩🏽‍🚒a", pointSize: 72, fallbackFonts: new[] { openSans }); RenderText(arabicFont, "English اَلْعَرَبِيَّةُ English", pointSize: 20); RenderText(arabicFont, "English English", pointSize: 20); RenderText(arabicFont, "اَلْعَرَبِيَّةُ اَلْعَرَبِيَّةُ", pointSize: 20); @@ -102,19 +124,19 @@ public static void Main(string[] args) RenderText(arabicFont, "English اَلْعَرَبِيَّةُ", pointSize: 20); RenderTextProcessorWithAlignment(emojiFont, "😀A😀", pointSize: 20, fallbackFonts: new[] { colorEmoji }); - RenderTextProcessorWithAlignment(uiFont, "this\nis\na\ntest", pointSize: 20, fallbackFonts: new[] { font2 }); - RenderTextProcessorWithAlignment(uiFont, "first\n\n\n\nlast", pointSize: 20, fallbackFonts: new[] { font2 }); + RenderTextProcessorWithAlignment(uiFont, "this\nis\na\ntest", pointSize: 20, fallbackFonts: new[] { openSans }); + RenderTextProcessorWithAlignment(uiFont, "first\n\n\n\nlast", pointSize: 20, fallbackFonts: new[] { openSans }); - RenderText(emojiFont, "😀", pointSize: 72, fallbackFonts: new[] { font2 }); - RenderText(font2, string.Empty, pointSize: 72, fallbackFonts: new[] { emojiFont }); - RenderText(font2, "😀 Hello World! 😀", pointSize: 72, fallbackFonts: new[] { emojiFont }); + RenderText(emojiFont, "😀", pointSize: 72, fallbackFonts: new[] { openSans }); + RenderText(openSans, string.Empty, pointSize: 72, fallbackFonts: new[] { emojiFont }); + RenderText(openSans, "😀 Hello World! 😀", pointSize: 72, fallbackFonts: new[] { emojiFont }); #endif // fallback font tests - RenderTextProcessor(colorEmoji, "a😀d", pointSize: 72, fallbackFonts: new[] { font2 }); - RenderText(colorEmoji, "a😀d", pointSize: 72, fallbackFonts: new[] { font2 }); + RenderTextProcessor(colorEmoji, "a😀d", pointSize: 72, fallbackFonts: new[] { openSans }); + RenderText(colorEmoji, "a😀d", pointSize: 72, fallbackFonts: new[] { openSans }); - RenderText(colorEmoji, "😀", pointSize: 72, fallbackFonts: new[] { font2 }); + RenderText(colorEmoji, "😀", pointSize: 72, fallbackFonts: new[] { openSans }); //// general RenderText(font, "abc", 72); @@ -122,50 +144,46 @@ public static void Main(string[] args) RenderText(fontWoff, "abe", 72); RenderText(fontWoff, "ABf", 72); RenderText(fontWoff2, "woff2", 72); - RenderText(font2, "ov", 72); - RenderText(font2, "a\ta", 72); - RenderText(font2, "aa\ta", 72); - RenderText(font2, "aaa\ta", 72); - RenderText(font2, "aaaa\ta", 72); - RenderText(font2, "aaaaa\ta", 72); - RenderText(font2, "aaaaaa\ta", 72); - RenderText(font2, "Hello\nWorld", 72); + RenderText(openSans, "ov", 72); + RenderText(openSans, "a\ta", 72); + RenderText(openSans, "aa\ta", 72); + RenderText(openSans, "aaa\ta", 72); + RenderText(openSans, "aaaa\ta", 72); + RenderText(openSans, "aaaaa\ta", 72); + RenderText(openSans, "aaaaaa\ta", 72); + RenderText(openSans, "Hello\nWorld", 72); RenderText(carter, "Hello\0World", 72); RenderText(wendyOne, "Hello\0World", 72); RenderText(whitneyBook, "Hello\0World", 72); RenderText(sunflower, "í", 30); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 4 }, "\t\tx"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 4 }, "\t\t\tx"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 4 }, "\t\t\t\tx"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 4 }, "\t\t\t\t\tx"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 4 }, "\t\tx"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 4 }, "\t\t\tx"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 4 }, "\t\t\t\tx"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 4 }, "\t\t\t\t\tx"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 0 }, "Zero\tTab"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 0 }, "Zero\tTab"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 0 }, "Zero\tTab"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 1 }, "One\tTab"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 6 }, "\tTab Then Words"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 1 }, "Tab Then Words"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 1 }, "Words Then Tab\t"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 1 }, " Spaces Then Words"); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 1 }, "Words Then Spaces "); - RenderText(new RichTextOptions(new Font(font2, 72)) { TabWidth = 1 }, "\naaaabbbbccccddddeeee\n\t\t\t3 tabs\n\t\t\t\t\t5 tabs"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 0 }, "Zero\tTab"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 1 }, "One\tTab"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 6 }, "\tTab Then Words"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 1 }, "Tab Then Words"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 1 }, "Words Then Tab\t"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 1 }, " Spaces Then Words"); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 1 }, "Words Then Spaces "); + RenderText(new RichTextOptions(new Font(openSans, 72)) { TabWidth = 1 }, "\naaaabbbbccccddddeeee\n\t\t\t3 tabs\n\t\t\t\t\t5 tabs"); #if OS_WINDOWS RenderText(new Font(SystemFonts.Get("Arial"), 20f, FontStyle.Regular), "á é í ó ú ç ã õ", 200, 50); RenderText(new Font(SystemFonts.Get("Arial"), 10f, FontStyle.Regular), "PGEP0JK867", 200, 50); RenderText(new RichTextOptions(SystemFonts.CreateFont("consolas", 72)) { TabWidth = 4 }, "xxxxxxxxxxxxxxxx\n\txxxx\txxxx\n\t\txxxxxxxx\n\t\t\txxxx"); - BoundingBoxes.Generate("a b c y q G H T", SystemFonts.CreateFont("arial", 40f)); + BoundingBoxes.Generate("a b c y q G H T", new TextOptions(SystemFonts.CreateFont("arial", 40f))); TextAlignmentSample.Generate(SystemFonts.CreateFont("arial", 50f)); TextAlignmentWrapped.Generate(SystemFonts.CreateFont("arial", 50f)); FontFamily simsum = SystemFonts.Get("SimSun"); RenderText(simsum, "这是一段长度超出设定的换行宽度的文本,但是没有在设定的宽度处换行。这段文本用于演示问题。希望可以修复。如果有需要可以联系我。", 16); - - FontFamily jhengHei = SystemFonts.Get("Microsoft JhengHei"); RenderText(jhengHei, " ,;:!¥()?{}-=+\|~!@#%&", 16); - - FontFamily arial = SystemFonts.Get("Arial"); RenderText(arial, "ìíîï", 72); #endif var sb = new StringBuilder(); @@ -211,7 +229,7 @@ public static void RenderText(Font font, string text, int width, int height) public static void RenderText(RichTextOptions options, string text) { - FontRectangle size = TextMeasurer.MeasureSize(text, options); + FontRectangle size = TextMeasurer.MeasureAdvance(text, options); if (size == FontRectangle.Empty) { return; @@ -253,7 +271,7 @@ public static void RenderTextProcessor( textOptions.FallbackFontFamilies = fallbackFonts.ToArray(); } - FontRectangle textSize = TextMeasurer.MeasureSize(text, textOptions); + FontRectangle textSize = TextMeasurer.MeasureAdvance(text, textOptions); textOptions.Origin = new PointF(5, 5); using var img = new Image((int)Math.Ceiling(textSize.Width) + 20, (int)Math.Ceiling(textSize.Height) + 20); diff --git a/src/SixLabors.Fonts/GlyphLayout.cs b/src/SixLabors.Fonts/GlyphLayout.cs index 579f0259..050c8e82 100644 --- a/src/SixLabors.Fonts/GlyphLayout.cs +++ b/src/SixLabors.Fonts/GlyphLayout.cs @@ -82,12 +82,12 @@ internal GlyphLayout( /// Gets a value indicating whether the glyph represents a whitespace character. /// /// The . - public bool IsWhiteSpace() => GlyphMetrics.ShouldRenderWhiteSpaceOnly(this.CodePoint); + public bool IsWhiteSpace() => UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint); internal FontRectangle BoundingBox(float dpi) { Vector2 origin = (this.PenLocation + this.Offset) * dpi; - FontRectangle box = this.Glyph.BoundingBox(this.LayoutMode, Vector2.Zero, dpi); + FontRectangle box = this.Glyph.BoundingBox(this.LayoutMode, this.BoxLocation, dpi); if (this.IsWhiteSpace()) { diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index 83d1b879..059f275a 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -401,39 +401,7 @@ void SetDecoration(TextDecorations decorations, float thickness, float position) /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] protected internal static bool ShouldSkipGlyphRendering(CodePoint codePoint) - => UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) && !ShouldRenderWhiteSpaceOnly(codePoint); - - /// - /// Gets a value indicating whether the specified code point should be rendered as a white space only. - /// - /// The code point. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static bool ShouldRenderWhiteSpaceOnly(CodePoint codePoint) - { - if (CodePoint.IsWhiteSpace(codePoint)) - { - return true; - } - - // Note: While U+115F, U+1160, U+3164 and U+FFA0 are Default_Ignorable, - // we do NOT want to hide them, as the way Uniscribe has implemented them - // is with regular spacing glyphs, and that's the way fonts are made to work. - // As such, we make exceptions for those four. - // Also ignoring U+1BCA0..1BCA3. https://github.com/harfbuzz/harfbuzz/issues/503 - uint value = (uint)codePoint.Value; - if (value is 0x115F or 0x1160 or 0x3164 or 0xFFA0) - { - return true; - } - - if (UnicodeUtility.IsInRangeInclusive(value, 0x1BCA0, 0x1BCA3)) - { - return true; - } - - return false; - } + => UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) && !UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint); /// /// Returns the size to render/measure the glyph based on the given size and resolution in px units. diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index ed09d17a..a9b597ff 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -247,15 +247,30 @@ public void Replace(int index, ReadOnlySpan removalIndices, ushort glyphId, { // Remove the glyphs at each index. int codePointCount = 0; + CodePoint codePoint = default; for (int i = removalIndices.Length - 1; i >= 0; i--) { int match = removalIndices[i]; codePointCount += this.glyphs[match].Data.CodePointCount; + CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; + if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) + { + if (!CodePoint.IsZeroWidthJoiner(currentCodePoint)) + { + codePoint = currentCodePoint; + } + } + this.glyphs.RemoveAt(match); } // Assign our new id at the index. GlyphShapingData current = this.glyphs[index].Data; + if (codePoint != default) + { + current.CodePoint = codePoint; + } + current.CodePointCount += codePointCount; current.GlyphId = glyphId; current.LigatureId = ligatureId; @@ -276,15 +291,30 @@ public void Replace(int index, int count, ushort glyphId) { // Remove the glyphs at each index. int codePointCount = 0; + CodePoint codePoint = default; for (int i = count; i > 0; i--) { int match = index + i; codePointCount += this.glyphs[match].Data.CodePointCount; + CodePoint currentCodePoint = this.glyphs[match].Data.CodePoint; + if (!UnicodeUtility.IsDefaultIgnorableCodePoint((uint)codePoint.Value) || UnicodeUtility.ShouldRenderWhiteSpaceOnly(codePoint)) + { + if (!CodePoint.IsZeroWidthJoiner(currentCodePoint)) + { + codePoint = currentCodePoint; + } + } + this.glyphs.RemoveAt(match); } // Assign our new id at the index. GlyphShapingData current = this.glyphs[index].Data; + if (codePoint != default) + { + current.CodePoint = codePoint; + } + current.CodePointCount += codePointCount; current.GlyphId = glyphId; current.LigatureId = 0; diff --git a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs index 5d1f5171..331b756e 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs @@ -123,7 +123,7 @@ internal override void RenderTo(IGlyphRenderer renderer, Vector2 location, Vecto if (renderer.BeginGlyph(in box, in parameters)) { - if (!ShouldRenderWhiteSpaceOnly(this.CodePoint)) + if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint)) { if (this.GlyphColor.HasValue && renderer is IColorGlyphRenderer colorSurface) { diff --git a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs index e41a4d60..f4c9658d 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs @@ -129,7 +129,7 @@ internal override void RenderTo(IGlyphRenderer renderer, Vector2 location, Vecto if (renderer.BeginGlyph(in box, in parameters)) { - if (!ShouldRenderWhiteSpaceOnly(this.CodePoint)) + if (!UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint)) { if (this.GlyphColor.HasValue && renderer is IColorGlyphRenderer colorSurface) { diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 7cf6552f..332da962 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -409,6 +409,7 @@ private static IEnumerable LayoutLineHorizontal( continue; } + int j = 0; foreach (GlyphMetrics metric in data.Metrics) { glyphs.Add(new GlyphLayout( @@ -419,7 +420,9 @@ private static IEnumerable LayoutLineHorizontal( data.ScaledAdvance, advanceY, GlyphLayoutMode.Horizontal, - i == 0)); + i == 0 && j == 0)); + + j++; } boxLocation.X += data.ScaledAdvance; @@ -537,6 +540,7 @@ private static IEnumerable LayoutLineVertical( continue; } + int j = 0; foreach (GlyphMetrics metric in data.Metrics) { // Align the glyph horizontally and vertically centering horizontally around the baseline. @@ -552,7 +556,9 @@ private static IEnumerable LayoutLineVertical( advanceX, data.ScaledAdvance, GlyphLayoutMode.Vertical, - i == 0)); + i == 0 && j == 0)); + + j++; } penLocation.Y += data.ScaledAdvance; @@ -671,6 +677,7 @@ private static IEnumerable LayoutLineVerticalMixed( if (data.IsRotated) { + int j = 0; foreach (GlyphMetrics metric in data.Metrics) { Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; @@ -682,11 +689,14 @@ private static IEnumerable LayoutLineVerticalMixed( advanceX, data.ScaledAdvance, GlyphLayoutMode.VerticalRotated, - i == 0)); + i == 0 && j == 0)); + + j++; } } else { + int j = 0; foreach (GlyphMetrics metric in data.Metrics) { // Align the glyph horizontally and vertically centering horizontally around the baseline. @@ -702,7 +712,9 @@ private static IEnumerable LayoutLineVerticalMixed( advanceX, data.ScaledAdvance, GlyphLayoutMode.Vertical, - i == 0)); + i == 0 && j == 0)); + + j++; } } diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index c256fa41..746a232d 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -208,14 +208,8 @@ internal static FontRectangle GetAdvance(IReadOnlyList glyphLayouts internal static FontRectangle GetSize(IReadOnlyList glyphLayouts, float dpi) { - if (glyphLayouts.Count == 0) - { - return FontRectangle.Empty; - } - - Vector2 topLeft = glyphLayouts[0].BoxLocation * dpi; FontRectangle bounds = GetBounds(glyphLayouts, dpi); - return new FontRectangle(0, 0, MathF.Ceiling(bounds.Right - topLeft.X), MathF.Ceiling(bounds.Bottom - topLeft.Y)); + return new FontRectangle(0, 0, MathF.Ceiling(bounds.Width), MathF.Ceiling(bounds.Height)); } internal static FontRectangle GetBounds(IReadOnlyList glyphLayouts, float dpi) @@ -265,7 +259,7 @@ internal static bool TryGetCharacterAdvances(IReadOnlyList glyphLay return hasSize; } - var characterBoundsList = new GlyphBounds[glyphLayouts.Count]; + GlyphBounds[] characterBoundsList = new GlyphBounds[glyphLayouts.Count]; for (int i = 0; i < glyphLayouts.Count; i++) { GlyphLayout glyph = glyphLayouts[i]; @@ -287,14 +281,13 @@ internal static bool TryGetCharacterSizes(IReadOnlyList glyphLayout return hasSize; } - var characterBoundsList = new GlyphBounds[glyphLayouts.Count]; + GlyphBounds[] characterBoundsList = new GlyphBounds[glyphLayouts.Count]; for (int i = 0; i < glyphLayouts.Count; i++) { GlyphLayout g = glyphLayouts[i]; FontRectangle bounds = g.BoundingBox(dpi); - Vector2 location = g.BoxLocation * dpi; - bounds = new(0, 0, bounds.Right - location.X, bounds.Bottom - location.Y); + bounds = new(0, 0, bounds.Width, bounds.Height); hasSize |= bounds.Width > 0 || bounds.Height > 0; characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, in bounds); @@ -313,7 +306,7 @@ internal static bool TryGetCharacterBounds(IReadOnlyList glyphLayou return hasSize; } - var characterBoundsList = new GlyphBounds[glyphLayouts.Count]; + GlyphBounds[] characterBoundsList = new GlyphBounds[glyphLayouts.Count]; for (int i = 0; i < glyphLayouts.Count; i++) { GlyphLayout g = glyphLayouts[i]; diff --git a/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs b/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs index 03efd736..2c2447d7 100644 --- a/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs +++ b/src/SixLabors.Fonts/Unicode/UnicodeUtility.cs @@ -232,6 +232,7 @@ public static bool IsCJKCodePoint(uint value) /// /// Returns if is a Default Ignorable Code Point. /// + /// The codepoint value. /// /// /// @@ -403,6 +404,38 @@ public static bool IsDefaultIgnorableCodePoint(uint value) return false; } + /// + /// Gets a value indicating whether the specified code point should be rendered as a white space only. + /// + /// The code point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ShouldRenderWhiteSpaceOnly(CodePoint codePoint) + { + if (CodePoint.IsWhiteSpace(codePoint)) + { + return true; + } + + // Note: While U+115F, U+1160, U+3164 and U+FFA0 are Default_Ignorable, + // we do NOT want to hide them, as the way Uniscribe has implemented them + // is with regular spacing glyphs, and that's the way fonts are made to work. + // As such, we make exceptions for those four. + // Also ignoring U+1BCA0..1BCA3. https://github.com/harfbuzz/harfbuzz/issues/503 + uint value = (uint)codePoint.Value; + if (value is 0x115F or 0x1160 or 0x3164 or 0xFFA0) + { + return true; + } + + if (IsInRangeInclusive(value, 0x1BCA0, 0x1BCA3)) + { + return true; + } + + return false; + } + /// /// Returns the Unicode plane (0 through 16, inclusive) which contains this code point. /// diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index 753c3442..70c67be5 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -145,7 +145,7 @@ public void RenderColrGlyphWithVariationSelector() { Font font = new FontCollection().Add(TestFonts.TwemojiMozillaData()).CreateFont(12); - string text = "\u263A\uFE0F"; // Fully-qualified sequence for emoji 'smiling face' + const string text = "\u263A\uFE0F"; // Fully-qualified sequence for emoji 'smiling face' IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(font)); // Check that no glyphs were generated by the variation selector @@ -158,14 +158,14 @@ public void EmojiWidthIsComputedCorrectlyWithSubstitutionOnZwj() { Font font = new FontCollection().Add(TestFonts.SegoeuiEmojiData()).CreateFont(72); - string text = "\U0001F469\U0001F3FB\u200D\U0001F91D\u200D\U0001F469\U0001F3FC"; // women holding hands: light skin tone, medium-light skin tone - string text2 = "\U0001F46D\U0001F3FB"; // women holding hands: light skin tone + const string text = "\U0001F469\U0001F3FB\u200D\U0001F91D\u200D\U0001F469\U0001F3FC"; // women holding hands: light skin tone, medium-light skin tone + const string text2 = "\U0001F46D\U0001F3FB"; // women holding hands: light skin tone FontRectangle size = TextMeasurer.MeasureSize(text, new TextOptions(font)); FontRectangle size2 = TextMeasurer.MeasureSize(text2, new TextOptions(font)); - Assert.Equal(75F, size.Width); - Assert.Equal(75F, size2.Width); + Assert.Equal(52f, size.Width); + Assert.Equal(51f, size2.Width); } [Theory] diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs index d2055010..2ca56e94 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs @@ -13,7 +13,7 @@ public void CorrectlySetsHeightMetrics() FontRectangle size = TextMeasurer.MeasureSize("H", new TextOptions(font)); - Assert.Equal(16, size.Width, 1F); - Assert.Equal(21, size.Height, 1F); + Assert.Equal(14, size.Width, 1F); + Assert.Equal(17, size.Height, 1F); } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_269.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_269.cs index 9b364c0e..024fb867 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_269.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_269.cs @@ -13,6 +13,6 @@ public void CorrectlySetsMetricsForFontsNotAdheringToSpec() FontRectangle size = TextMeasurer.MeasureSize("H", new TextOptions(font)); Assert.Equal(32, size.Width, 1F); - Assert.Equal(27, size.Height, 1F); + Assert.Equal(25, size.Height, 1F); } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_32.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_32.cs index 679f04c4..d0e7f966 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_32.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_32.cs @@ -11,7 +11,7 @@ public class Issues_32 [Fact] public void TabWidth0CausesBadTabRendering() { - string text = "Hello\tworld"; + const string text = "Hello\tworld"; Font font = CreateFont(text); FontRectangle size = TextMeasurer.MeasureSize(text, new TextOptions(font) { @@ -21,12 +21,12 @@ public void TabWidth0CausesBadTabRendering() // tab width of 0 should make tabs not render at all Assert.Equal(10, size.Height, 4F); - Assert.Equal(320, size.Width, 4F); + Assert.Equal(311, size.Width, 4F); } public static Font CreateFont(string text) { - var fc = (IFontMetricsCollection)new FontCollection(); + IFontMetricsCollection fc = new FontCollection(); Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12); return new Font(d, 1F); } diff --git a/tests/SixLabors.Fonts.Tests/TestFonts.cs b/tests/SixLabors.Fonts.Tests/TestFonts.cs index 2808aa0e..b2fcc709 100644 --- a/tests/SixLabors.Fonts.Tests/TestFonts.cs +++ b/tests/SixLabors.Fonts.Tests/TestFonts.cs @@ -249,6 +249,10 @@ public static class TestFonts public static string PlantinStdRegularFile => GetFullPath("PlantinStdRegular.otf"); + public static string PermanentMarkerRegularFile => GetFullPath("PermanentMarker-Regular.ttf"); + + public static string PermanentMarkerRegularWoff2File => GetFullPath("PermanentMarker-Regular.woff2"); + public static Stream TwemojiMozillaData() => OpenStream(TwemojiMozillaFile); public static Stream SegoeuiEmojiData() => OpenStream(SegoeuiEmojiFile); diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index ee445d64..0040d5b9 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -3,6 +3,7 @@ using System.Globalization; using System.Numerics; +using System.Text; using SixLabors.Fonts.Tests.Fakes; using SixLabors.Fonts.Unicode; @@ -388,7 +389,7 @@ public void MeasureTextWordBreak(string text, LayoutMode layoutMode, WordBreakin FontFamily jhengHei = SystemFonts.Get("Microsoft JhengHei"); Font font = arial.CreateFont(20); - FontRectangle size = TextMeasurer.MeasureSize( + FontRectangle size = TextMeasurer.MeasureAdvance( text, new TextOptions(font) { @@ -780,100 +781,100 @@ public void TextJustification_InterWord_Vertical(TextDirection direction) public static TheoryData OpenSans_Data = new() { - { '!', new(0, 0, 2, 10) }, - { '"', new(0, 0, 4, 5) }, - { '#', new(0, 0, 7, 9) }, - { '$', new(0, 0, 6, 10) }, - { '%', new(0, 0, 8, 9) }, - { '&', new(0, 0, 8, 9) }, - { '\'', new(0, 0, 2, 5) }, - { '(', new(0, 0, 3, 11) }, - { ')', new(0, 0, 3, 11) }, - { '*', new(0, 0, 6, 6) }, - { '+', new(0, 0, 6, 8) }, - { ',', new(0, 0, 2, 11) }, - { '-', new(0, 0, 3, 7) }, - { '.', new(0, 0, 2, 10) }, - { '/', new(0, 0, 4, 9) }, - { '0', new(0, 0, 6, 9) }, - { '1', new(0, 0, 4, 9) }, - { '2', new(0, 0, 6, 9) }, - { '3', new(0, 0, 6, 9) }, - { '4', new(0, 0, 6, 9) }, - { '5', new(0, 0, 6, 9) }, - { '6', new(0, 0, 6, 9) }, - { '7', new(0, 0, 6, 9) }, - { '8', new(0, 0, 6, 9) }, - { '9', new(0, 0, 6, 9) }, - { ':', new(0, 0, 2, 10) }, - { ';', new(0, 0, 2, 11) }, - { '<', new(0, 0, 6, 8) }, - { '=', new(0, 0, 6, 7) }, - { '>', new(0, 0, 6, 8) }, - { '?', new(0, 0, 5, 10) }, - { '@', new(0, 0, 9, 10) }, - { 'A', new(0, 0, 7, 9) }, - { 'B', new(0, 0, 6, 9) }, - { 'C', new(0, 0, 6, 9) }, - { 'D', new(0, 0, 7, 9) }, - { 'E', new(0, 0, 5, 9) }, - { 'F', new(0, 0, 5, 9) }, - { 'G', new(0, 0, 7, 9) }, - { 'H', new(0, 0, 7, 9) }, - { 'I', new(0, 0, 2, 9) }, - { 'J', new(0, 0, 2, 11) }, - { 'K', new(0, 0, 7, 9) }, - { 'L', new(0, 0, 5, 9) }, - { 'M', new(0, 0, 9, 9) }, - { 'N', new(0, 0, 7, 9) }, - { 'O', new(0, 0, 8, 9) }, - { 'P', new(0, 0, 6, 9) }, - { 'Q', new(0, 0, 8, 11) }, - { 'R', new(0, 0, 7, 9) }, - { 'S', new(0, 0, 6, 9) }, - { 'T', new(0, 0, 6, 9) }, - { 'U', new(0, 0, 7, 9) }, - { 'V', new(0, 0, 6, 9) }, - { 'W', new(0, 0, 10, 9) }, - { 'X', new(0, 0, 6, 9) }, - { 'Y', new(0, 0, 6, 9) }, - { 'Z', new(0, 0, 6, 9) }, - { '[', new(0, 0, 4, 11) }, - { '\\', new(0, 0, 4, 9) }, - { ']', new(0, 0, 3, 11) }, - { '^', new(0, 0, 6, 7) }, - { '_', new(0, 0, 5, 11) }, - { '`', new(0, 0, 3, 3) }, - { 'a', new(0, 0, 5, 9) }, - { 'b', new(0, 0, 6, 9) }, - { 'c', new(0, 0, 5, 9) }, - { 'd', new(0, 0, 6, 9) }, - { 'e', new(0, 0, 6, 9) }, - { 'f', new(0, 0, 4, 9) }, - { 'g', new(0, 0, 6, 12) }, - { 'h', new(0, 0, 6, 9) }, - { 'i', new(0, 0, 2, 9) }, - { 'j', new(0, 0, 2, 12) }, - { 'k', new(0, 0, 6, 9) }, - { 'l', new(0, 0, 2, 9) }, - { 'm', new(0, 0, 9, 9) }, - { 'n', new(0, 0, 6, 9) }, - { 'o', new(0, 0, 6, 9) }, - { 'p', new(0, 0, 6, 12) }, - { 'q', new(0, 0, 6, 12) }, - { 'r', new(0, 0, 4, 9) }, - { 's', new(0, 0, 5, 9) }, - { 't', new(0, 0, 4, 9) }, - { 'u', new(0, 0, 6, 9) }, - { 'v', new(0, 0, 5, 9) }, - { 'w', new(0, 0, 8, 9) }, - { 'x', new(0, 0, 6, 9) }, - { 'y', new(0, 0, 6, 12) }, - { 'z', new(0, 0, 5, 9) }, - { '{', new(0, 0, 4, 11) }, - { '|', new(0, 0, 4, 12) }, - { '}', new(0, 0, 4, 11) }, - { '~', new(0, 0, 6, 6) }, + { '!', new(0, 0, 2, 8) }, + { '"', new(0, 0, 3, 3) }, + { '#', new(0, 0, 6, 8) }, + { '$', new(0, 0, 5, 9) }, + { '%', new(0, 0, 8, 8) }, + { '&', new(0, 0, 7, 8) }, + { '\'', new(0, 0, 1, 3) }, + { '(', new(0, 0, 3, 9) }, + { ')', new(0, 0, 3, 9) }, + { '*', new(0, 0, 5, 5) }, + { '+', new(0, 0, 5, 5) }, + { ',', new(0, 0, 2, 3) }, + { '-', new(0, 0, 3, 1) }, + { '.', new(0, 0, 2, 2) }, + { '/', new(0, 0, 4, 8) }, + { '0', new(0, 0, 5, 8) }, + { '1', new(0, 0, 3, 8) }, + { '2', new(0, 0, 5, 8) }, + { '3', new(0, 0, 5, 8) }, + { '4', new(0, 0, 6, 8) }, + { '5', new(0, 0, 5, 8) }, + { '6', new(0, 0, 5, 8) }, + { '7', new(0, 0, 5, 8) }, + { '8', new(0, 0, 5, 8) }, + { '9', new(0, 0, 5, 8) }, + { ':', new(0, 0, 2, 6) }, + { ';', new(0, 0, 2, 7) }, + { '<', new(0, 0, 5, 5) }, + { '=', new(0, 0, 5, 3) }, + { '>', new(0, 0, 5, 5) }, + { '?', new(0, 0, 4, 8) }, + { '@', new(0, 0, 8, 9) }, + { 'A', new(0, 0, 7, 8) }, + { 'B', new(0, 0, 5, 8) }, + { 'C', new(0, 0, 6, 8) }, + { 'D', new(0, 0, 6, 8) }, + { 'E', new(0, 0, 4, 8) }, + { 'F', new(0, 0, 4, 8) }, + { 'G', new(0, 0, 6, 8) }, + { 'H', new(0, 0, 6, 8) }, + { 'I', new(0, 0, 1, 8) }, + { 'J', new(0, 0, 3, 10) }, + { 'K', new(0, 0, 6, 8) }, + { 'L', new(0, 0, 4, 8) }, + { 'M', new(0, 0, 8, 8) }, + { 'N', new(0, 0, 6, 8) }, + { 'O', new(0, 0, 7, 8) }, + { 'P', new(0, 0, 5, 8) }, + { 'Q', new(0, 0, 7, 9) }, + { 'R', new(0, 0, 6, 8) }, + { 'S', new(0, 0, 5, 8) }, + { 'T', new(0, 0, 6, 8) }, + { 'U', new(0, 0, 6, 8) }, + { 'V', new(0, 0, 6, 8) }, + { 'W', new(0, 0, 9, 8) }, + { 'X', new(0, 0, 6, 8) }, + { 'Y', new(0, 0, 6, 8) }, + { 'Z', new(0, 0, 5, 8) }, + { '[', new(0, 0, 3, 9) }, + { '\\', new(0, 0, 4, 8) }, + { ']', new(0, 0, 3, 9) }, + { '^', new(0, 0, 5, 5) }, + { '_', new(0, 0, 5, 1) }, + { '`', new(0, 0, 2, 2) }, + { 'a', new(0, 0, 5, 6) }, + { 'b', new(0, 0, 5, 8) }, + { 'c', new(0, 0, 4, 6) }, + { 'd', new(0, 0, 5, 8) }, + { 'e', new(0, 0, 5, 6) }, + { 'f', new(0, 0, 4, 8) }, + { 'g', new(0, 0, 6, 8) }, + { 'h', new(0, 0, 5, 8) }, + { 'i', new(0, 0, 1, 8) }, + { 'j', new(0, 0, 3, 10) }, + { 'k', new(0, 0, 5, 8) }, + { 'l', new(0, 0, 1, 8) }, + { 'm', new(0, 0, 8, 6) }, + { 'n', new(0, 0, 5, 6) }, + { 'o', new(0, 0, 5, 6) }, + { 'p', new(0, 0, 5, 8) }, + { 'q', new(0, 0, 5, 8) }, + { 'r', new(0, 0, 4, 6) }, + { 's', new(0, 0, 4, 6) }, + { 't', new(0, 0, 4, 7) }, + { 'u', new(0, 0, 5, 6) }, + { 'v', new(0, 0, 5, 6) }, + { 'w', new(0, 0, 8, 6) }, + { 'x', new(0, 0, 5, 6) }, + { 'y', new(0, 0, 5, 8) }, + { 'z', new(0, 0, 4, 6) }, + { '{', new(0, 0, 4, 9) }, + { '|', new(0, 0, 1, 11) }, + { '}', new(0, 0, 4, 9) }, + { '~', new(0, 0, 5, 2) }, }; [Theory] @@ -1001,100 +1002,100 @@ public void CanMeasureMultilineCharacterLayouts() public static TheoryData SegoeUi_Data = new() { - { '!', new(0, 0, 2, 10) }, - { '"', new(0, 0, 4, 5) }, - { '#', new(0, 0, 6, 9) }, - { '$', new(0, 0, 5, 11) }, - { '%', new(0, 0, 8, 10) }, - { '&', new(0, 0, 8, 10) }, - { '\'', new(0, 0, 2, 5) }, - { '(', new(0, 0, 3, 11) }, - { ')', new(0, 0, 3, 11) }, - { '*', new(0, 0, 4, 6) }, - { '+', new(0, 0, 6, 9) }, - { ',', new(0, 0, 2, 11) }, - { '-', new(0, 0, 4, 7) }, - { '.', new(0, 0, 2, 10) }, - { '/', new(0, 0, 4, 11) }, - { '0', new(0, 0, 5, 10) }, - { '1', new(0, 0, 4, 10) }, - { '2', new(0, 0, 5, 10) }, - { '3', new(0, 0, 5, 10) }, - { '4', new(0, 0, 6, 10) }, - { '5', new(0, 0, 5, 10) }, - { '6', new(0, 0, 5, 10) }, - { '7', new(0, 0, 5, 10) }, - { '8', new(0, 0, 5, 10) }, - { '9', new(0, 0, 5, 10) }, - { ':', new(0, 0, 2, 10) }, - { ';', new(0, 0, 2, 11) }, - { '<', new(0, 0, 6, 9) }, - { '=', new(0, 0, 6, 8) }, - { '>', new(0, 0, 6, 9) }, - { '?', new(0, 0, 4, 10) }, - { '@', new(0, 0, 9, 11) }, - { 'A', new(0, 0, 7, 10) }, - { 'B', new(0, 0, 6, 10) }, - { 'C', new(0, 0, 6, 10) }, - { 'D', new(0, 0, 7, 10) }, - { 'E', new(0, 0, 5, 10) }, - { 'F', new(0, 0, 5, 10) }, - { 'G', new(0, 0, 7, 10) }, - { 'H', new(0, 0, 7, 10) }, - { 'I', new(0, 0, 2, 10) }, - { 'J', new(0, 0, 3, 10) }, - { 'K', new(0, 0, 6, 10) }, - { 'L', new(0, 0, 5, 10) }, - { 'M', new(0, 0, 9, 10) }, - { 'N', new(0, 0, 7, 10) }, - { 'O', new(0, 0, 8, 10) }, - { 'P', new(0, 0, 6, 10) }, - { 'Q', new(0, 0, 8, 11) }, - { 'R', new(0, 0, 6, 10) }, - { 'S', new(0, 0, 5, 10) }, - { 'T', new(0, 0, 6, 10) }, - { 'U', new(0, 0, 7, 10) }, - { 'V', new(0, 0, 7, 10) }, - { 'W', new(0, 0, 10, 10) }, - { 'X', new(0, 0, 6, 10) }, - { 'Y', new(0, 0, 6, 10) }, - { 'Z', new(0, 0, 6, 10) }, - { '[', new(0, 0, 3, 11) }, - { '\\', new(0, 0, 4, 11) }, - { ']', new(0, 0, 3, 11) }, - { '^', new(0, 0, 6, 7) }, - { '_', new(0, 0, 5, 11) }, - { '`', new(0, 0, 3, 4) }, - { 'a', new(0, 0, 5, 10) }, - { 'b', new(0, 0, 6, 10) }, - { 'c', new(0, 0, 5, 10) }, - { 'd', new(0, 0, 6, 10) }, - { 'e', new(0, 0, 5, 10) }, - { 'f', new(0, 0, 4, 10) }, - { 'g', new(0, 0, 6, 12) }, - { 'h', new(0, 0, 5, 10) }, - { 'i', new(0, 0, 2, 10) }, - { 'j', new(0, 0, 2, 12) }, - { 'k', new(0, 0, 5, 10) }, - { 'l', new(0, 0, 2, 10) }, - { 'm', new(0, 0, 8, 10) }, - { 'n', new(0, 0, 5, 10) }, - { 'o', new(0, 0, 6, 10) }, - { 'p', new(0, 0, 6, 12) }, - { 'q', new(0, 0, 6, 12) }, - { 'r', new(0, 0, 4, 10) }, - { 's', new(0, 0, 4, 10) }, - { 't', new(0, 0, 4, 10) }, - { 'u', new(0, 0, 5, 10) }, - { 'v', new(0, 0, 5, 10) }, - { 'w', new(0, 0, 8, 10) }, - { 'x', new(0, 0, 5, 10) }, - { 'y', new(0, 0, 5, 12) }, - { 'z', new(0, 0, 5, 10) }, - { '{', new(0, 0, 3, 11) }, - { '|', new(0, 0, 2, 12) }, - { '}', new(0, 0, 3, 11) }, - { '~', new(0, 0, 6, 7) } + { '!', new(0, 0, 2, 8) }, + { '"', new(0, 0, 3, 3) }, + { '#', new(0, 0, 6, 7) }, + { '$', new(0, 0, 4, 9) }, + { '%', new(0, 0, 8, 8) }, + { '&', new(0, 0, 8, 8) }, + { '\'', new(0, 0, 1, 3) }, + { '(', new(0, 0, 3, 9) }, + { ')', new(0, 0, 3, 9) }, + { '*', new(0, 0, 4, 4) }, + { '+', new(0, 0, 5, 5) }, + { ',', new(0, 0, 2, 3) }, + { '-', new(0, 0, 3, 1) }, + { '.', new(0, 0, 2, 2) }, + { '/', new(0, 0, 5, 9) }, + { '0', new(0, 0, 5, 8) }, + { '1', new(0, 0, 3, 8) }, + { '2', new(0, 0, 5, 8) }, + { '3', new(0, 0, 5, 8) }, + { '4', new(0, 0, 5, 8) }, + { '5', new(0, 0, 4, 8) }, + { '6', new(0, 0, 5, 8) }, + { '7', new(0, 0, 5, 8) }, + { '8', new(0, 0, 5, 8) }, + { '9', new(0, 0, 5, 8) }, + { ':', new(0, 0, 2, 6) }, + { ';', new(0, 0, 2, 7) }, + { '<', new(0, 0, 5, 5) }, + { '=', new(0, 0, 5, 3) }, + { '>', new(0, 0, 5, 5) }, + { '?', new(0, 0, 4, 8) }, + { '@', new(0, 0, 8, 9) }, + { 'A', new(0, 0, 7, 8) }, + { 'B', new(0, 0, 5, 8) }, + { 'C', new(0, 0, 6, 8) }, + { 'D', new(0, 0, 6, 8) }, + { 'E', new(0, 0, 4, 8) }, + { 'F', new(0, 0, 4, 8) }, + { 'G', new(0, 0, 6, 8) }, + { 'H', new(0, 0, 6, 8) }, + { 'I', new(0, 0, 1, 8) }, + { 'J', new(0, 0, 3, 8) }, + { 'K', new(0, 0, 5, 8) }, + { 'L', new(0, 0, 4, 8) }, + { 'M', new(0, 0, 8, 8) }, + { 'N', new(0, 0, 6, 8) }, + { 'O', new(0, 0, 7, 8) }, + { 'P', new(0, 0, 5, 8) }, + { 'Q', new(0, 0, 8, 9) }, + { 'R', new(0, 0, 6, 8) }, + { 'S', new(0, 0, 5, 8) }, + { 'T', new(0, 0, 5, 8) }, + { 'U', new(0, 0, 6, 8) }, + { 'V', new(0, 0, 7, 8) }, + { 'W', new(0, 0, 10, 8) }, + { 'X', new(0, 0, 6, 8) }, + { 'Y', new(0, 0, 6, 8) }, + { 'Z', new(0, 0, 6, 8) }, + { '[', new(0, 0, 2, 9) }, + { '\\', new(0, 0, 5, 9) }, + { ']', new(0, 0, 2, 9) }, + { '^', new(0, 0, 5, 5) }, + { '_', new(0, 0, 5, 1) }, + { '`', new(0, 0, 2, 2) }, + { 'a', new(0, 0, 4, 6) }, + { 'b', new(0, 0, 5, 8) }, + { 'c', new(0, 0, 4, 6) }, + { 'd', new(0, 0, 5, 8) }, + { 'e', new(0, 0, 5, 6) }, + { 'f', new(0, 0, 4, 8) }, + { 'g', new(0, 0, 5, 8) }, + { 'h', new(0, 0, 5, 8) }, + { 'i', new(0, 0, 2, 8) }, + { 'j', new(0, 0, 3, 10) }, + { 'k', new(0, 0, 5, 8) }, + { 'l', new(0, 0, 1, 8) }, + { 'm', new(0, 0, 8, 6) }, + { 'n', new(0, 0, 5, 6) }, + { 'o', new(0, 0, 5, 6) }, + { 'p', new(0, 0, 5, 8) }, + { 'q', new(0, 0, 5, 8) }, + { 'r', new(0, 0, 3, 6) }, + { 's', new(0, 0, 4, 6) }, + { 't', new(0, 0, 3, 7) }, + { 'u', new(0, 0, 5, 6) }, + { 'v', new(0, 0, 5, 5) }, + { 'w', new(0, 0, 7, 5) }, + { 'x', new(0, 0, 5, 5) }, + { 'y', new(0, 0, 5, 8) }, + { 'z', new(0, 0, 5, 5) }, + { '{', new(0, 0, 3, 9) }, + { '|', new(0, 0, 1, 10) }, + { '}', new(0, 0, 3, 9) }, + { '~', new(0, 0, 5, 2) } }; [Theory] @@ -1108,6 +1109,7 @@ public void TrueTypeHinting_CanHintSmallSegoeUi(char c, FontRectangle expected) }; FontRectangle actual = TextMeasurer.MeasureSize(c.ToString(), options); + Assert.Equal(expected, actual); } From bb13b0160a273b17f3904ca0f72bd86b96d0eccc Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 8 Feb 2024 20:30:33 +1000 Subject: [PATCH 3/5] Fix broken test --- tests/SixLabors.Fonts.Tests/TextLayoutTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 0040d5b9..3e42de66 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -952,7 +952,8 @@ public void CanMeasureCharacterLayouts() // Since this is a single line starting at 0,0 the following should be predictable. Assert.Equal(advance.Bounds.X, size.Bounds.X); - Assert.Equal(size.Bounds.Bottom, bound.Bounds.Bottom); + Assert.Equal(size.Bounds.Width, bound.Bounds.Width); + Assert.Equal(size.Bounds.Height, bound.Bounds.Height); } } From 476c5f34b139c4aec8fb91890322dd430baeb375 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 8 Feb 2024 22:31:26 +1000 Subject: [PATCH 4/5] Remove negative offset hack --- src/SixLabors.Fonts/TextLayout.cs | 29 ------------------- .../Issues/Issues_337.cs | 10 +++---- 2 files changed, 5 insertions(+), 34 deletions(-) diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index 332da962..db5fc0a6 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -1178,35 +1178,6 @@ private static TextBox BreakLines( : metric.FontMetrics.VerticalMetrics; float ascender = metricsHeader.Ascender * scaleY; - // Adjust ascender for glyphs with a negative tsb. e.g. emoji to prevent cutoff. - if (!CodePoint.IsWhiteSpace(codePoint)) - { - if (!isDecomposed) - { - short tsbOffset = 0; - - // We need to check all the metrics. - for (int mi = 0; mi < metrics.Count; mi++) - { - tsbOffset = Math.Min(tsbOffset, metrics[mi].TopSideBearing); - } - - if (tsbOffset < 0) - { - ascender -= tsbOffset * scaleY; - } - } - else - { - // Decomposed glyphs contain a single metric. - short tsbOffset = metric.TopSideBearing; - if (tsbOffset < 0) - { - ascender -= tsbOffset * scaleY; - } - } - } - // Match how line height is calculated for browsers. // https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height float descender = Math.Abs(metricsHeader.Descender * scaleY); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs index f38e1367..cf6e611d 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_337.cs @@ -22,11 +22,11 @@ public void CanShapeCompositeGlyphs() var expected = new FontRectangle[] { - new FontRectangle(0, -101.99994F, 1024, 1024), - new FontRectangle(1024, -101.99994F, 1024, 1024), - new FontRectangle(2048, -101.99994F, 1024, 1024), - new FontRectangle(3072, -101.99994F, 1024, 1024), - new FontRectangle(4096, -101.99994F, 1024, 1024) + new FontRectangle(0, -254.99994F, 1024, 1024), + new FontRectangle(1024, -254.99994F, 1024, 1024), + new FontRectangle(2048, -254.99994F, 1024, 1024), + new FontRectangle(3072, -254.99994F, 1024, 1024), + new FontRectangle(4096, -254.99994F, 1024, 1024) }; for (int i = 0; i < expected.Length; i++) From 7c23ae8520cd58670c1836c8c08621ae30d80796 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Fri, 9 Feb 2024 16:12:20 +1000 Subject: [PATCH 5/5] Fix 367 --- src/SixLabors.Fonts/TextLayout.cs | 6 ++++ .../Fonts/courier-prime.woff2 | Bin 0 -> 18764 bytes .../Issues/Issues_367.cs | 29 ++++++++++++++++++ tests/SixLabors.Fonts.Tests/TestFonts.cs | 2 ++ .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 2 +- 5 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/SixLabors.Fonts.Tests/Fonts/courier-prime.woff2 create mode 100644 tests/SixLabors.Fonts.Tests/Issues/Issues_367.cs diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index db5fc0a6..4b2b0c1e 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -1104,6 +1104,12 @@ private static TextBox BreakLines( 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)) { diff --git a/tests/SixLabors.Fonts.Tests/Fonts/courier-prime.woff2 b/tests/SixLabors.Fonts.Tests/Fonts/courier-prime.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..f7ee5b6afc2093a9f5805c7f5948d958d9a9653b GIT binary patch literal 18764 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 HHUIzs9AZpT literal 0 HcmV?d00001 diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_367.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_367.cs new file mode 100644 index 00000000..e37a33d3 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_367.cs @@ -0,0 +1,29 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Tests.Issues; +public class Issues_367 +{ + [Fact] + public void ShouldMatchBrowserBreak() + { + Font font = new FontCollection().Add(TestFonts.CourierPrimeFile).CreateFont(12); + + TextOptions options = new(font) + { + Dpi = 96f // 1in = 96px + }; + + const float wrappingLengthInInches = 3.875f; + options.WrappingLength = wrappingLengthInInches * options.Dpi; + + const string text = "Leer, but lonesome has fussin' change a faith. Themself seen and four trample."; + int lineCount = TextMeasurer.CountLines(text, options); + + Assert.Equal(3, lineCount); + + FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); + Assert.Equal(365, advance.Width); + Assert.Equal(48, advance.Height); + } +} diff --git a/tests/SixLabors.Fonts.Tests/TestFonts.cs b/tests/SixLabors.Fonts.Tests/TestFonts.cs index b2fcc709..18e4d016 100644 --- a/tests/SixLabors.Fonts.Tests/TestFonts.cs +++ b/tests/SixLabors.Fonts.Tests/TestFonts.cs @@ -253,6 +253,8 @@ public static class TestFonts public static string PermanentMarkerRegularWoff2File => GetFullPath("PermanentMarker-Regular.woff2"); + public static string CourierPrimeFile => GetFullPath("courier-prime.woff2"); + public static Stream TwemojiMozillaData() => OpenStream(TwemojiMozillaFile); public static Stream SegoeuiEmojiData() => OpenStream(SegoeuiEmojiFile); diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 3e42de66..5d0c89a8 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -493,7 +493,7 @@ 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, 6)] + [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 100, 7)] [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 200, 6)] public void CountLinesWrappingLength(string text, int wrappingLength, int usedLines) {