From 91405e38da8260a7f74bc900f2786ea0024a0041 Mon Sep 17 00:00:00 2001 From: Gerdus Date: Sun, 24 Mar 2024 13:38:59 +0200 Subject: [PATCH] Fix Character Bounds --- src/SixLabors.Fonts/GlyphLayout.cs | 29 ++--- tests/SixLabors.Fonts.Tests/GlyphTests.cs | 2 +- .../SixLabors.Fonts.Tests/TextLayoutTests.cs | 101 ++++++++++++++++++ 3 files changed, 118 insertions(+), 14 deletions(-) diff --git a/src/SixLabors.Fonts/GlyphLayout.cs b/src/SixLabors.Fonts/GlyphLayout.cs index 2b0c3ba8..a4bd7dbf 100644 --- a/src/SixLabors.Fonts/GlyphLayout.cs +++ b/src/SixLabors.Fonts/GlyphLayout.cs @@ -100,8 +100,15 @@ internal GlyphLayout( internal FontRectangle BoundingBox(float dpi) { - Vector2 origin = (this.PenLocation + this.Offset) * dpi; - FontRectangle box = this.Glyph.BoundingBox(this.LayoutMode, this.BoxLocation, dpi); + // Same logic as in TrueTypeGlyphMetrics.RenderTo + Vector2 location = this.PenLocation; + Vector2 offset = this.Offset; + + location *= dpi; + offset *= dpi; + Vector2 renderLocation = location + offset; + + FontRectangle box = this.Glyph.BoundingBox(this.LayoutMode, renderLocation, dpi); if (this.IsWhiteSpace()) { @@ -110,8 +117,8 @@ internal FontRectangle BoundingBox(float dpi) if (this.LayoutMode == GlyphLayoutMode.Vertical) { return new FontRectangle( - box.X + origin.X, - box.Y + origin.Y, + box.X, + box.Y, box.Width, this.AdvanceY * dpi); } @@ -119,24 +126,20 @@ internal FontRectangle BoundingBox(float dpi) if (this.LayoutMode == GlyphLayoutMode.VerticalRotated) { return new FontRectangle( - box.X + origin.X, - box.Y + origin.Y, + box.X, + box.Y, 0, this.AdvanceY * dpi); } return new FontRectangle( - box.X + origin.X, - box.Y + origin.Y, + box.X, + box.Y, this.AdvanceX * dpi, box.Height); } - return new FontRectangle( - box.X + origin.X, - box.Y + origin.Y, - box.Width, - box.Height); + return box; } /// diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index 70c67be5..6671bf24 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -164,7 +164,7 @@ public void EmojiWidthIsComputedCorrectlyWithSubstitutionOnZwj() FontRectangle size = TextMeasurer.MeasureSize(text, new TextOptions(font)); FontRectangle size2 = TextMeasurer.MeasureSize(text2, new TextOptions(font)); - Assert.Equal(52f, size.Width); + Assert.Equal(51f, size.Width); Assert.Equal(51f, size2.Width); } diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index b3f6b1dc..5000c4fd 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -1062,6 +1062,107 @@ public void DoesMeasureCharacterLayoutIncludeStringIndex() Assert.Equal(graphemeCount - 1, lastBound.GraphemeIndex); } + [Fact] + public void DoesMeasureCharacterBoundsExtendForAdvanceMultipliers() + { + FontFamily family = new FontCollection().Add(TestFonts.OpenSansFile); + family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics); + + TextOptions options = new(family.CreateFont(metrics.UnitsPerEm)) + { + TabWidth = 8 + }; + + const string text = "H\tH"; + + IReadOnlyList glyphsToRender = CaptureGlyphBoundBuilder.GenerateGlyphsBoxes(text, options); + TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds); + + IReadOnlyList glyphLayouts = TextLayout.GenerateLayout(text, options); + + Assert.Equal(glyphsToRender.Count, bounds.Length); + Assert.Equal(glyphsToRender.Count, glyphsToRender.Count); + + for (int glyphIndex = 0; glyphIndex < glyphsToRender.Count; glyphIndex++) + { + FontRectangle renderGlyph = glyphsToRender[glyphIndex]; + FontRectangle measureGlyph = bounds[glyphIndex].Bounds; + GlyphLayout glyphLayout = glyphLayouts[glyphIndex]; + + if (glyphLayout.IsWhiteSpace()) + { + Assert.Equal(renderGlyph.X, measureGlyph.X); + Assert.Equal(renderGlyph.Y, measureGlyph.Y); + Assert.Equal(glyphLayout.AdvanceX * options.Dpi, measureGlyph.Width); + Assert.Equal(renderGlyph.Height, measureGlyph.Height); + } + else + { + Assert.Equal(renderGlyph, measureGlyph); + } + } + } + + [Fact] + public void IsMeasureCharacterBoundsSameAsRenderBounds() + { + FontFamily family = new FontCollection().Add(TestFonts.OpenSansFile); + family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics); + + TextOptions options = new(family.CreateFont(metrics.UnitsPerEm)) + { + }; + + const string text = "Hello WorLLd"; + + IReadOnlyList glyphsToRender = CaptureGlyphBoundBuilder.GenerateGlyphsBoxes(text, options); + TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds); + + Assert.Equal(glyphsToRender.Count, bounds.Length); + + for (int glyphIndex = 0; glyphIndex < glyphsToRender.Count; glyphIndex++) + { + FontRectangle renderGlyph = glyphsToRender[glyphIndex]; + FontRectangle measureGlyph = bounds[glyphIndex].Bounds; + + Assert.Equal(renderGlyph.X, measureGlyph.X); + Assert.Equal(renderGlyph.Y, measureGlyph.Y); + Assert.Equal(renderGlyph.Width, measureGlyph.Width); + Assert.Equal(renderGlyph.Height, measureGlyph.Height); + + Assert.Equal(renderGlyph, measureGlyph); + } + } + + private class CaptureGlyphBoundBuilder : IGlyphRenderer + { + public static List GenerateGlyphsBoxes(string text, TextOptions options) + { + CaptureGlyphBoundBuilder glyphBuilder = new(); + TextRenderer renderer = new(glyphBuilder); + renderer.RenderText(text, options); + return glyphBuilder.GlyphBounds; + } + public readonly List GlyphBounds = new(); + public CaptureGlyphBoundBuilder() { } + bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) + { + this.GlyphBounds.Add(bounds); + return true; + } + public void BeginFigure() { } + public void MoveTo(Vector2 point) { } + public void QuadraticBezierTo(Vector2 secondControlPoint, Vector2 point) { } + public void CubicBezierTo(Vector2 secondControlPoint, Vector2 thirdControlPoint, Vector2 point) { } + public void LineTo(Vector2 point) { } + public void EndFigure() { } + public void EndGlyph() { } + public void EndText() { } + void IGlyphRenderer.BeginText(in FontRectangle bounds) { } + public TextDecorations EnabledDecorations() => TextDecorations.None; + public void SetDecoration(TextDecorations textDecorations, Vector2 start, Vector2 end, float thickness) { } + } + private static readonly Font OpenSansTTF = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(10); private static readonly Font OpenSansWoff = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(10);