diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index 2e422357..45acf4fe 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -77,6 +77,12 @@ public static FontRectangle MeasureSize(ReadOnlySpan text, TextOptions opt /// The text. /// The text shaping options. /// The rendered glyph bounds of the text if it was to be rendered. + /// + /// This measures the tight ink bounds enclosing all rendered glyphs. The returned rectangle + /// may be smaller or larger than the logical advance and may have a non-zero origin. + /// Use for the logical layout box or + /// for the union of both. + /// public static FontRectangle MeasureBounds(string text, TextOptions options) => MeasureBounds(text.AsSpan(), options); @@ -89,6 +95,8 @@ public static FontRectangle MeasureBounds(string text, TextOptions options) /// The union of the logical advance rectangle and the rendered glyph bounds if the text was to be rendered. /// /// + /// The returned rectangle is in absolute coordinates and is large enough to contain both the logical advance + /// rectangle and the rendered glyph bounds. /// Use this method when both typographic advance and rendered glyph overshoot must fit within the same rectangle. /// public static FontRectangle MeasureRenderableBounds(string text, TextOptions options) @@ -100,6 +108,12 @@ public static FontRectangle MeasureRenderableBounds(string text, TextOptions opt /// The text. /// The text shaping options. /// The rendered glyph bounds of the text if it was to be rendered. + /// + /// This measures the tight ink bounds enclosing all rendered glyphs. The returned rectangle + /// may be smaller or larger than the logical advance and may have a non-zero origin. + /// Use for the logical layout box or + /// for the union of both. + /// public static FontRectangle MeasureBounds(ReadOnlySpan text, TextOptions options) => GetBounds(TextLayout.GenerateLayout(text, options), options.Dpi); @@ -112,6 +126,8 @@ public static FontRectangle MeasureBounds(ReadOnlySpan text, TextOptions o /// The union of the logical advance rectangle and the rendered glyph bounds if the text was to be rendered. /// /// + /// The returned rectangle is in absolute coordinates and is large enough to contain both the logical advance + /// rectangle and the rendered glyph bounds. /// Use this method when both typographic advance and rendered glyph overshoot must fit within the same rectangle. /// public static FontRectangle MeasureRenderableBounds(ReadOnlySpan text, TextOptions options) @@ -122,8 +138,9 @@ public static FontRectangle MeasureRenderableBounds(ReadOnlySpan text, Tex } FontRectangle advance = MeasureAdvance(text, options); + FontRectangle absoluteAdvance = new(options.Origin.X, options.Origin.Y, advance.Width, advance.Height); FontRectangle bounds = MeasureBounds(text, options); - return FontRectangle.Union(advance, bounds); + return FontRectangle.Union(absoluteAdvance, bounds); } /// @@ -133,6 +150,11 @@ public static FontRectangle MeasureRenderableBounds(ReadOnlySpan text, Tex /// The text shaping options. /// The list of per-entry logical advances of the text if it was to be rendered. /// Whether any of the entries had non-empty advances. + /// + /// Each entry reflects the typographic advance width and height for one character. + /// Use for per-character ink bounds or + /// for the union of both. + /// public static bool TryMeasureCharacterAdvances(string text, TextOptions options, out ReadOnlySpan advances) => TryMeasureCharacterAdvances(text.AsSpan(), options, out advances); @@ -143,6 +165,11 @@ public static bool TryMeasureCharacterAdvances(string text, TextOptions options, /// The text shaping options. /// The list of per-entry logical advances of the text if it was to be rendered. /// Whether any of the entries had non-empty advances. + /// + /// Each entry reflects the typographic advance width and height for one character. + /// Use for per-character ink bounds or + /// for the union of both. + /// public static bool TryMeasureCharacterAdvances(ReadOnlySpan text, TextOptions options, out ReadOnlySpan advances) => TryGetCharacterAdvances(TextLayout.GenerateLayout(text, options), options.Dpi, out advances); @@ -173,6 +200,11 @@ public static bool TryMeasureCharacterSizes(ReadOnlySpan text, TextOptions /// The text shaping options. /// The list of per-entry rendered glyph bounds of the text if it was to be rendered. /// Whether any of the entries had non-empty bounds. + /// + /// Each entry reflects the tight ink bounds of one rendered glyph. + /// Use for per-character logical advances or + /// for the union of both. + /// public static bool TryMeasureCharacterBounds(string text, TextOptions options, out ReadOnlySpan bounds) => TryMeasureCharacterBounds(text.AsSpan(), options, out bounds); @@ -183,6 +215,10 @@ public static bool TryMeasureCharacterBounds(string text, TextOptions options, o /// The text shaping options. /// The list of per-entry renderable bounds of the text if it was to be rendered. /// Whether any of the entries had non-empty bounds. + /// + /// Each returned rectangle is in absolute coordinates and is large enough to contain both the logical advance + /// rectangle and the rendered glyph bounds for the corresponding laid-out entry. + /// public static bool TryMeasureCharacterRenderableBounds(string text, TextOptions options, out ReadOnlySpan bounds) => TryMeasureCharacterRenderableBounds(text.AsSpan(), options, out bounds); @@ -193,6 +229,11 @@ public static bool TryMeasureCharacterRenderableBounds(string text, TextOptions /// The text shaping options. /// The list of per-entry rendered glyph bounds of the text if it was to be rendered. /// Whether any of the entries had non-empty bounds. + /// + /// Each entry reflects the tight ink bounds of one rendered glyph. + /// Use for per-character logical advances or + /// for the union of both. + /// public static bool TryMeasureCharacterBounds(ReadOnlySpan text, TextOptions options, out ReadOnlySpan bounds) => TryGetCharacterBounds(TextLayout.GenerateLayout(text, options), options.Dpi, out bounds); @@ -203,6 +244,10 @@ public static bool TryMeasureCharacterBounds(ReadOnlySpan text, TextOption /// The text shaping options. /// The list of per-entry renderable bounds of the text if it was to be rendered. /// Whether any of the entries had non-empty bounds. + /// + /// Each returned rectangle is in absolute coordinates and is large enough to contain both the logical advance + /// rectangle and the rendered glyph bounds for the corresponding laid-out entry. + /// public static bool TryMeasureCharacterRenderableBounds(ReadOnlySpan text, TextOptions options, out ReadOnlySpan bounds) => TryGetCharacterRenderableBounds(TextLayout.GenerateLayout(text, options), options.Dpi, out bounds); @@ -510,8 +555,9 @@ internal static bool TryGetCharacterRenderableBounds(IReadOnlyList for (int i = 0; i < glyphLayouts.Count; i++) { GlyphLayout g = glyphLayouts[i]; - FontRectangle advance = new(0, 0, g.AdvanceX * dpi, g.AdvanceY * dpi); - FontRectangle bounds = FontRectangle.Union(advance, g.BoundingBox(dpi)); + FontRectangle glyphBounds = g.BoundingBox(dpi); + FontRectangle advance = new(g.BoxLocation.X * dpi, g.BoxLocation.Y * dpi, g.AdvanceX * dpi, g.AdvanceY * dpi); + FontRectangle bounds = FontRectangle.Union(advance, glyphBounds); hasSize |= bounds.Width > 0 || bounds.Height > 0; characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, in bounds, g.GraphemeIndex, g.StringIndex); } diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs index ac6e44cd..d76fdfa0 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs @@ -31,9 +31,12 @@ public static void TestLayout( params object[] properties) { #if SUPPORTS_DRAWING - FontRectangle renderBounds = TextMeasurer.MeasureRenderableBounds(text, options); - int width = (int)(Math.Ceiling(renderBounds.Width) + Math.Ceiling(options.Origin.X)); - int height = (int)(Math.Ceiling(renderBounds.Height) + Math.Ceiling(options.Origin.Y)); + FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); + FontRectangle bounds = TextMeasurer.MeasureBounds(text, options); + FontRectangle renderBounds = FontRectangle.Union(advance, bounds); + + int width = Math.Max(1, (int)Math.Ceiling(options.Origin.X + renderBounds.Right - Math.Min(0, renderBounds.Left))); + int height = Math.Max(1, (int)Math.Ceiling(options.Origin.Y + renderBounds.Bottom - Math.Min(0, renderBounds.Top))); bool isVertical = !options.LayoutMode.IsHorizontal(); int wrappingLength = isVertical