public void Should_Enumerate(string text, int expectedLength) { var enumerator = new GraphemeEnumerator(text.AsMemory()); Assert.True(enumerator.MoveNext()); Assert.Equal(expectedLength, enumerator.Current.Text.Length); }
public void Should_Get_CharacterHit_From_Distance_RTL_With_TextStyles() { using (Start()) { var text = "أَبْجَدِيَّة عَرَبِيَّة"; var i = 0; var graphemeEnumerator = new GraphemeEnumerator(text.AsMemory()); while (graphemeEnumerator.MoveNext()) { var grapheme = graphemeEnumerator.Current; var textStyleOverrides = new[] { new ValueSpan <TextRunProperties>(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) }; i += grapheme.Text.Length; var layout = new TextLayout( text, Typeface.Default, 12, Brushes.Black, textStyleOverrides: textStyleOverrides); var textLine = layout.TextLines[0]; var shapedRuns = textLine.TextRuns.Cast <ShapedTextCharacters>().ToList(); var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList(); var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList(); var currentX = 0.0; var cluster = text.Length; for (int j = 0; j < clusters.Count - 1; j++) { var glyphAdvance = glyphAdvances[j]; var characterHit = textLine.GetCharacterHitFromDistance(currentX); Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength); var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); Assert.Equal(currentX, distance, 5); currentX += glyphAdvance; cluster = clusters[j]; } } } }
/// <summary> /// Tries to get a shapeable length that is supported by the specified typeface. /// </summary> /// <param name="text">The text.</param> /// <param name="typeface">The typeface that is used to find matching characters.</param> /// <param name="defaultTypeface"></param> /// <param name="length">The shapeable length.</param> /// <param name="script"></param> /// <returns></returns> protected static bool TryGetShapeableLength( ReadOnlySlice <char> text, Typeface typeface, Typeface?defaultTypeface, out int length, out Script script) { length = 0; script = Script.Unknown; if (text.Length == 0) { return(false); } var font = typeface.GlyphTypeface; var defaultFont = defaultTypeface?.GlyphTypeface; var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext()) { var currentGrapheme = enumerator.Current; var currentScript = currentGrapheme.FirstCodepoint.Script; if (currentScript != Script.Common && defaultFont != null && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } //Stop at the first missing glyph if (!currentGrapheme.FirstCodepoint.IsBreakChar && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } if (currentScript != script) { if (script is Script.Unknown || currentScript != Script.Common && script is Script.Common or Script.Inherited) { script = currentScript; } else { if (currentScript != Script.Inherited && currentScript != Script.Common) { break; } } } length += currentGrapheme.Text.Length; }
/// <summary> /// Creates a shapeable text run with unique properties. /// </summary> /// <param name="text">The text to create text runs from.</param> /// <param name="defaultProperties">The default text run properties.</param> /// <returns>A list of shapeable text runs.</returns> private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice <char> text, TextRunProperties defaultProperties) { var defaultTypeface = defaultProperties.Typeface; var currentTypeface = defaultTypeface; if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count)) { return(new ShapeableTextCharacters(text.Take(count), new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, defaultProperties.TextDecorations, defaultProperties.ForegroundBrush))); } var codepoint = Codepoint.ReadAt(text, count, out _); //ToDo: Fix FontFamily fallback var matchFound = FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); if (matchFound && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { //Fallback found return(new ShapeableTextCharacters(text.Take(count), new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, defaultProperties.TextDecorations, defaultProperties.ForegroundBrush))); } // no fallback found currentTypeface = defaultTypeface; var glyphTypeface = currentTypeface.GlyphTypeface; var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext()) { var grapheme = enumerator.Current; if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } count += grapheme.Text.Length; } return(new ShapeableTextCharacters(text.Take(count), new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, defaultProperties.TextDecorations, defaultProperties.ForegroundBrush))); }
/// <summary> /// Creates a text style run with unique properties. /// </summary> /// <param name="text">The text to create text runs from.</param> /// <param name="defaultStyle"></param> /// <returns>A list of text runs.</returns> protected TextStyleRun CreateShapableTextStyleRun(ReadOnlySlice <char> text, TextStyle defaultStyle) { var defaultTypeface = defaultStyle.TextFormat.Typeface; var currentTypeface = defaultTypeface; if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count)) { return(new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface, defaultStyle.TextFormat.FontRenderingEmSize, defaultStyle.Foreground, defaultStyle.TextDecorations))); } var codepoint = Codepoint.ReadAt(text, count, out _); //ToDo: Fix FontFamily fallback currentTypeface = FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style); if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { //Fallback found return(new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface, defaultStyle.TextFormat.FontRenderingEmSize, defaultStyle.Foreground, defaultStyle.TextDecorations))); } // no fallback found currentTypeface = defaultTypeface; var glyphTypeface = currentTypeface.GlyphTypeface; var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext()) { var grapheme = enumerator.Current; if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } count += grapheme.Text.Length; } return(new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface, defaultStyle.TextFormat.FontRenderingEmSize, defaultStyle.Foreground, defaultStyle.TextDecorations))); }
private bool Run(GraphemeBreakData t) { var text = Encoding.UTF32.GetString(MemoryMarshal.Cast <int, byte>(t.Codepoints).ToArray()); var grapheme = Encoding.UTF32.GetString(MemoryMarshal.Cast <int, byte>(t.Grapheme).ToArray()).AsSpan(); var enumerator = new GraphemeEnumerator(text.AsMemory()); enumerator.MoveNext(); var actual = enumerator.Current.Text.Span; var pass = true; if (actual.Length != grapheme.Length) { pass = false; } if (pass) { for (int i = 0; i < grapheme.Length; i++) { var a = grapheme[i]; var b = actual[i]; if (a != b) { pass = false; break; } } } if (!pass) { _outputHelper.WriteLine($"Failed line {t.LineNumber}"); _outputHelper.WriteLine($" Text: {text}"); _outputHelper.WriteLine($" Codepoints: {string.Join(" ", t.Codepoints)}"); _outputHelper.WriteLine($" Grapheme: {string.Join(" ", t.Grapheme)}"); _outputHelper.WriteLine($" Line: {t.Line}"); return(false); } return(true); }
public void Should_Enumerate_Other() { const string text = "ABCDEFGHIJ"; var enumerator = new GraphemeEnumerator(text.AsMemory()); var count = 0; while (enumerator.MoveNext()) { Assert.Equal(1, enumerator.Current.Text.Length); count++; } Assert.Equal(10, count); }
private static int CoerceLength(ReadOnlySlice <char> text, int length) { var finalLength = 0; var graphemeEnumerator = new GraphemeEnumerator(text); while (graphemeEnumerator.MoveNext()) { var grapheme = graphemeEnumerator.Current; finalLength += grapheme.Text.Length; if (finalLength >= length) { return(finalLength); } } return(length); }
/// <summary> /// Tries to get run properties. /// </summary> /// <param name="defaultTypeface"></param> /// <param name="text"></param> /// <param name="typeface">The typeface that is used to find matching characters.</param> /// <param name="count"></param> /// <returns></returns> protected bool TryGetRunProperties(ReadOnlySlice <char> text, Typeface typeface, Typeface defaultTypeface, out int count) { if (text.Length == 0) { count = 0; return(false); } var isFallback = typeface != defaultTypeface; count = 0; var script = Script.Common; //var direction = BiDiClass.LeftToRight; var font = typeface.GlyphTypeface; var defaultFont = defaultTypeface.GlyphTypeface; var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext()) { var grapheme = enumerator.Current; var currentScript = grapheme.FirstCodepoint.Script; //var currentDirection = grapheme.FirstCodepoint.BiDiClass; //// ToDo: Implement BiDi algorithm //if (currentScript.HorizontalDirection != direction) //{ // if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint)) // { // break; // } //} if (currentScript != script) { if (currentScript != Script.Inherited && currentScript != Script.Common) { if (script == Script.Inherited || script == Script.Common) { script = currentScript; } else { break; } } } if (isFallback) { if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } } if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _)) { if (!grapheme.FirstCodepoint.IsWhiteSpace) { break; } } count += grapheme.Text.Length; } return(count > 0); }
public void Should_Not_Alter_Lines_After_TextStyleSpan_Was_Applied() { using (Start()) { const string text = "אחד !\ntwo !\nשְׁלוֹשָׁה !"; var red = new SolidColorBrush(Colors.Red).ToImmutable(); var black = Brushes.Black.ToImmutable(); var expected = new TextLayout( text, Typeface.Default, 12.0f, black, textWrapping: TextWrapping.Wrap); var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast <ShapedTextCharacters>() .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); var outer = new GraphemeEnumerator(text.AsMemory()); var inner = new GraphemeEnumerator(text.AsMemory()); var i = 0; var j = 0; while (true) { while (inner.MoveNext()) { j += inner.Current.Text.Length; if (j + i > text.Length) { break; } var spans = new[] { new ValueSpan <TextRunProperties>(i, j, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: red)) }; var actual = new TextLayout( text, Typeface.Default, 12.0f, black, textWrapping: TextWrapping.Wrap, textStyleOverrides: spans); var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast <ShapedTextCharacters>() .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count); for (var k = 0; k < expectedGlyphs.Count; k++) { Assert.Equal(expectedGlyphs[k], actualGlyphs[k]); } } if (!outer.MoveNext()) { break; } inner = new GraphemeEnumerator(text.AsMemory()); i += outer.Current.Text.Length; } } }
/// <summary> /// Creates a shapeable text run with unique properties. /// </summary> /// <param name="text">The text to create text runs from.</param> /// <param name="defaultProperties">The default text run properties.</param> /// <param name="biDiLevel">The bidi level of the run.</param> /// <param name="previousProperties"></param> /// <returns>A list of shapeable text runs.</returns> private static ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice <char> text, TextRunProperties defaultProperties, sbyte biDiLevel, ref TextRunProperties?previousProperties) { var defaultTypeface = defaultProperties.Typeface; var currentTypeface = defaultTypeface; var previousTypeface = previousProperties?.Typeface; if (TryGetShapeableLength(text, currentTypeface, null, out var count, out var script)) { if (script == Script.Common && previousTypeface is not null) { if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out var fallbackCount, out _)) { return(new ShapeableTextCharacters(text.Take(fallbackCount), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel)); } } return(new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), biDiLevel)); } if (previousTypeface is not null) { if (TryGetShapeableLength(text, previousTypeface.Value, defaultTypeface, out count, out _)) { return(new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel)); } } var codepoint = Codepoint.ReplacementCodepoint; var codepointEnumerator = new CodepointEnumerator(text.Skip(count)); while (codepointEnumerator.MoveNext()) { if (codepointEnumerator.Current.IsWhiteSpace) { continue; } codepoint = codepointEnumerator.Current; break; } //ToDo: Fix FontFamily fallback var matchFound = FontManager.Current.TryMatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.Stretch, defaultTypeface.FontFamily, defaultProperties.CultureInfo, out currentTypeface); if (matchFound && TryGetShapeableLength(text, currentTypeface, defaultTypeface, out count, out _)) { //Fallback found return(new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface), biDiLevel)); } // no fallback found currentTypeface = defaultTypeface; var glyphTypeface = currentTypeface.GlyphTypeface; var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext()) { var grapheme = enumerator.Current; if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) { break; } count += grapheme.Text.Length; } return(new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel)); }
/// <summary> /// Tries to get run properties. /// </summary> /// <param name="defaultTypeface"></param> /// <param name="text"></param> /// <param name="typeface">The typeface that is used to find matching characters.</param> /// <param name="count"></param> /// <returns></returns> protected bool TryGetRunProperties(ReadOnlySlice <char> text, Typeface typeface, Typeface defaultTypeface, out int count) { if (text.Length == 0) { count = 0; return(false); } var isFallback = typeface != defaultTypeface; count = 0; var script = Script.Unknown; var direction = BiDiClass.LeftToRight; var font = typeface.GlyphTypeface; var defaultFont = defaultTypeface.GlyphTypeface; var enumerator = new GraphemeEnumerator(text); while (enumerator.MoveNext()) { var currentGrapheme = enumerator.Current; var currentScript = currentGrapheme.FirstCodepoint.Script; var currentDirection = currentGrapheme.FirstCodepoint.BiDiClass; //// ToDo: Implement BiDi algorithm //if (currentScript.HorizontalDirection != direction) //{ // if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint)) // { // break; // } //} if (currentScript != script) { if (script is Script.Unknown) { script = currentScript; } else { if (currentScript != Script.Inherited && currentScript != Script.Common) { break; } } } //Only handle non whitespace here if (!currentGrapheme.FirstCodepoint.IsWhiteSpace) { //Stop at the first glyph that is present in the default typeface. if (isFallback && defaultFont.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } //Stop at the first missing glyph if (!font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } } if (!currentGrapheme.FirstCodepoint.IsWhiteSpace && !font.TryGetGlyph(currentGrapheme.FirstCodepoint, out _)) { break; } if (direction == BiDiClass.RightToLeft && currentDirection == BiDiClass.CommonSeparator) { break; } count += currentGrapheme.Text.Length; direction = currentDirection; } return(count > 0); }