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