diff --git a/src/SixLabors.Fonts/GlyphBounds.cs b/src/SixLabors.Fonts/GlyphBounds.cs
index 69d8a0d0..87730e98 100644
--- a/src/SixLabors.Fonts/GlyphBounds.cs
+++ b/src/SixLabors.Fonts/GlyphBounds.cs
@@ -15,10 +15,14 @@ public readonly struct GlyphBounds
///
/// The Unicode codepoint for the glyph.
/// The glyph bounds.
- public GlyphBounds(CodePoint codePoint, in FontRectangle bounds)
+ /// The index of the grapheme in original text.
+ /// The index of the codepoint in original text..
+ public GlyphBounds(CodePoint codePoint, in FontRectangle bounds, int graphemeIndex, int stringIndex)
{
this.Codepoint = codePoint;
this.Bounds = bounds;
+ this.GraphemeIndex = graphemeIndex;
+ this.StringIndex = stringIndex;
}
///
@@ -31,6 +35,16 @@ public GlyphBounds(CodePoint codePoint, in FontRectangle bounds)
///
public FontRectangle Bounds { get; }
+ ///
+ /// Gets grapheme index of glyph in original text.
+ ///
+ public int GraphemeIndex { get; }
+
+ ///
+ /// Gets string index of glyph in original text.
+ ///
+ public int StringIndex { get; }
+
///
public override string ToString()
=> $"Codepoint: {this.Codepoint}, Bounds: {this.Bounds}.";
diff --git a/src/SixLabors.Fonts/GlyphLayout.cs b/src/SixLabors.Fonts/GlyphLayout.cs
index 050c8e82..2b0c3ba8 100644
--- a/src/SixLabors.Fonts/GlyphLayout.cs
+++ b/src/SixLabors.Fonts/GlyphLayout.cs
@@ -19,7 +19,9 @@ internal GlyphLayout(
float advanceWidth,
float advanceHeight,
GlyphLayoutMode layoutMode,
- bool isStartOfLine)
+ bool isStartOfLine,
+ int graphemeIndex,
+ int stringIndex)
{
this.Glyph = glyph;
this.CodePoint = glyph.GlyphMetrics.CodePoint;
@@ -30,6 +32,8 @@ internal GlyphLayout(
this.AdvanceY = advanceHeight;
this.LayoutMode = layoutMode;
this.IsStartOfLine = isStartOfLine;
+ this.GraphemeIndex = graphemeIndex;
+ this.StringIndex = stringIndex;
}
///
@@ -78,6 +82,16 @@ internal GlyphLayout(
///
public bool IsStartOfLine { get; }
+ ///
+ /// Gets grapheme index of glyph in original text.
+ ///
+ public int GraphemeIndex { get; }
+
+ ///
+ /// Gets string index of glyph in original text.
+ ///
+ public int StringIndex { get; }
+
///
/// Gets a value indicating whether the glyph represents a whitespace character.
///
diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs
index 84d4af37..a9b1b02d 100644
--- a/src/SixLabors.Fonts/TextLayout.cs
+++ b/src/SixLabors.Fonts/TextLayout.cs
@@ -420,7 +420,9 @@ private static IEnumerable LayoutLineHorizontal(
data.ScaledAdvance,
advanceY,
GlyphLayoutMode.Horizontal,
- i == 0 && j == 0));
+ i == 0 && j == 0,
+ data.GraphemeIndex,
+ data.StringIndex));
j++;
}
@@ -556,7 +558,9 @@ private static IEnumerable LayoutLineVertical(
advanceX,
data.ScaledAdvance,
GlyphLayoutMode.Vertical,
- i == 0 && j == 0));
+ i == 0 && j == 0,
+ data.GraphemeIndex,
+ data.StringIndex));
j++;
}
@@ -689,7 +693,9 @@ private static IEnumerable LayoutLineVerticalMixed(
advanceX,
data.ScaledAdvance,
GlyphLayoutMode.VerticalRotated,
- i == 0 && j == 0));
+ i == 0 && j == 0,
+ data.GraphemeIndex,
+ data.StringIndex));
j++;
}
@@ -712,7 +718,9 @@ private static IEnumerable LayoutLineVerticalMixed(
advanceX,
data.ScaledAdvance,
GlyphLayoutMode.Vertical,
- i == 0 && j == 0));
+ i == 0 && j == 0,
+ data.GraphemeIndex,
+ data.StringIndex));
j++;
}
@@ -895,6 +903,7 @@ private static TextBox BreakLines(
List textLines = new();
TextLine textLine = new();
int glyphCount = 0;
+ int stringIndex = 0;
// No glyph should contain more than 64 metrics.
// We do a sanity check below just in case.
@@ -1205,12 +1214,15 @@ private static TextBox BreakLines(
graphemeIndex,
codePointIndex,
isRotated,
- isDecomposed);
+ isDecomposed,
+ stringIndex);
}
codePointIndex++;
graphemeCodePointIndex++;
}
+
+ stringIndex += graphemeEnumerator.Current.Length;
}
// Add the final line.
@@ -1268,7 +1280,8 @@ public void Add(
int graphemeIndex,
int offset,
bool isRotated,
- bool isDecomposed)
+ bool isDecomposed,
+ int stringIndex)
{
// Reset metrics.
// We track the maximum metrics for each line to ensure glyphs can be aligned.
@@ -1288,7 +1301,8 @@ public void Add(
graphemeIndex,
offset,
isRotated,
- isDecomposed));
+ isDecomposed,
+ stringIndex));
}
public TextLine SplitAt(LineBreak lineBreak, bool keepAll)
@@ -1627,7 +1641,8 @@ public GlyphLayoutData(
int graphemeIndex,
int offset,
bool isRotated,
- bool isDecomposed)
+ bool isDecomposed,
+ int stringIndex)
{
this.Metrics = metrics;
this.PointSize = pointSize;
@@ -1640,6 +1655,7 @@ public GlyphLayoutData(
this.Offset = offset;
this.IsRotated = isRotated;
this.IsDecomposed = isDecomposed;
+ this.StringIndex = stringIndex;
}
public readonly CodePoint CodePoint => this.Metrics[0].CodePoint;
@@ -1668,6 +1684,8 @@ public GlyphLayoutData(
public bool IsDecomposed { get; }
+ public int StringIndex { get; }
+
public readonly bool IsNewLine => CodePoint.IsNewLine(this.CodePoint);
private readonly string DebuggerDisplay => FormattableString
diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs
index 746a232d..12162095 100644
--- a/src/SixLabors.Fonts/TextMeasurer.cs
+++ b/src/SixLabors.Fonts/TextMeasurer.cs
@@ -265,7 +265,7 @@ internal static bool TryGetCharacterAdvances(IReadOnlyList glyphLay
GlyphLayout glyph = glyphLayouts[i];
FontRectangle bounds = new(0, 0, glyph.AdvanceX * dpi, glyph.AdvanceY * dpi);
hasSize |= bounds.Width > 0 || bounds.Height > 0;
- characterBoundsList[i] = new GlyphBounds(glyph.Glyph.GlyphMetrics.CodePoint, in bounds);
+ characterBoundsList[i] = new GlyphBounds(glyph.Glyph.GlyphMetrics.CodePoint, in bounds, glyph.GraphemeIndex, glyph.StringIndex);
}
characterBounds = characterBoundsList;
@@ -290,7 +290,7 @@ internal static bool TryGetCharacterSizes(IReadOnlyList glyphLayout
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);
+ characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, in bounds, g.GraphemeIndex, g.StringIndex);
}
characterBounds = characterBoundsList;
@@ -312,7 +312,7 @@ internal static bool TryGetCharacterBounds(IReadOnlyList glyphLayou
GlyphLayout g = glyphLayouts[i];
FontRectangle bounds = g.BoundingBox(dpi);
hasSize |= bounds.Width > 0 || bounds.Height > 0;
- characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, in bounds);
+ characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, in bounds, g.GraphemeIndex, g.StringIndex);
}
characterBounds = characterBoundsList;
diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs
index 5d0c89a8..b3f6b1dc 100644
--- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs
+++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs
@@ -3,7 +3,6 @@
using System.Globalization;
using System.Numerics;
-using System.Text;
using SixLabors.Fonts.Tests.Fakes;
using SixLabors.Fonts.Unicode;
@@ -241,10 +240,10 @@ public void TryMeasureCharacterBounds()
const string text = "a b\nc";
GlyphBounds[] expectedGlyphMetrics =
{
- new(new CodePoint('a'), new FontRectangle(10, 0, 10, 10)),
- new(new CodePoint(' '), new FontRectangle(40, 0, 30, 10)),
- new(new CodePoint('b'), new FontRectangle(70, 0, 10, 10)),
- new(new CodePoint('c'), new FontRectangle(10, 30, 10, 10)),
+ new(new CodePoint('a'), new FontRectangle(10, 0, 10, 10), 0, 0),
+ new(new CodePoint(' '), new FontRectangle(40, 0, 30, 10), 1, 1),
+ new(new CodePoint('b'), new FontRectangle(70, 0, 10, 10), 2, 2),
+ new(new CodePoint('c'), new FontRectangle(10, 30, 10, 10), 3, 3),
};
Font font = CreateFont(text);
@@ -996,6 +995,73 @@ public void CanMeasureMultilineCharacterLayouts()
}
}
+ [Fact]
+ public void DoesMeasureCharacterLayoutIncludeStringIndex()
+ {
+ FontFamily family = new FontCollection().Add(TestFonts.OpenSansFile);
+ family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics);
+
+ TextOptions options = new(family.CreateFont(metrics.UnitsPerEm))
+ {
+ LineSpacing = 1.5F
+ };
+
+ const string text = "The quick๐ฉ๐ฝโ๐ brown fox jumps over \r\n the lazy dog";
+
+ Assert.True(TextMeasurer.TryMeasureCharacterAdvances(text, options, out ReadOnlySpan advances));
+ Assert.True(TextMeasurer.TryMeasureCharacterSizes(text, options, out ReadOnlySpan sizes));
+ Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds));
+
+ Assert.Equal(advances.Length, sizes.Length);
+ Assert.Equal(advances.Length, bounds.Length);
+
+ int stringIndex = -1;
+
+ for (int i = 0; i < advances.Length; i++)
+ {
+ GlyphBounds advance = advances[i];
+ GlyphBounds size = sizes[i];
+ GlyphBounds bound = bounds[i];
+
+ Assert.Equal(bound.StringIndex, advance.StringIndex);
+ Assert.Equal(bound.StringIndex, size.StringIndex);
+
+ Assert.Equal(bound.GraphemeIndex, advance.GraphemeIndex);
+ Assert.Equal(bound.GraphemeIndex, size.GraphemeIndex);
+
+ if (bound.Codepoint == new CodePoint("k"[0]))
+ {
+ stringIndex = text.IndexOf("k", StringComparison.InvariantCulture);
+ Assert.Equal(stringIndex, bound.StringIndex);
+ Assert.Equal(stringIndex, bound.GraphemeIndex);
+ }
+
+ // after emoji
+ if (bound.Codepoint == new CodePoint("b"[0]))
+ {
+ stringIndex = text.IndexOf("b", StringComparison.InvariantCulture);
+ Assert.NotEqual(bound.StringIndex, bound.GraphemeIndex);
+ Assert.Equal(stringIndex, bound.StringIndex);
+ Assert.Equal(11, bound.GraphemeIndex);
+ }
+ }
+
+ SpanGraphemeEnumerator graphemeEnumerator = new(text);
+ int graphemeCount = 0;
+ while (graphemeEnumerator.MoveNext())
+ {
+ graphemeCount += 1;
+ }
+
+ GlyphBounds firstBound = bounds[0];
+ Assert.Equal(0, firstBound.StringIndex);
+ Assert.Equal(0, firstBound.GraphemeIndex);
+
+ GlyphBounds lastBound = bounds[^1];
+ Assert.Equal(text.Length - 1, lastBound.StringIndex);
+ Assert.Equal(graphemeCount - 1, lastBound.GraphemeIndex);
+ }
+
private static readonly Font OpenSansTTF = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(10);
private static readonly Font OpenSansWoff = new FontCollection().Add(TestFonts.OpenSansFile).CreateFont(10);