From b859d2463e7a10d971e3fef2fa6dcbfd00cc6405 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 25 Sep 2024 16:31:35 +0200 Subject: [PATCH 1/2] Ignore null terminator and replace them with a zero space before shaping --- .../Media/TextFormatting/TextCharacters.cs | 11 +++++++++ src/Skia/Avalonia.Skia/TextShaperImpl.cs | 14 +---------- .../Media/TextFormatting/TextLineTests.cs | 23 +++++++++++++++++++ 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 7d4fac337d6..3ad196f281f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -9,6 +9,8 @@ namespace Avalonia.Media.TextFormatting /// public class TextCharacters : TextRun { + private static char[] ZeroWidthSpace = ['\u200b']; + /// /// Constructs a run for text content from a string. /// @@ -82,6 +84,15 @@ private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; + //Read first codepoint + var firstCodepoint = Codepoint.ReadAt(textSpan, 0, out _); + + //Detect null terminator + if (firstCodepoint.Value == 0) + { + return new UnshapedTextRun(ZeroWidthSpace.AsMemory(), defaultProperties, biDiLevel); + } + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count)) { return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 768e8f3a2f3..cedb2f63cf1 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -14,8 +14,6 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - private const uint ZeroWidthSpace = '\u200b'; - private static readonly ConcurrentDictionary s_cachedLanguage = new(); public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions options) @@ -69,17 +67,7 @@ public ShapedBuffer ShapeText(ReadOnlyMemory text, TextShaperOptions optio var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)(sourceInfo.Cluster); - - if (glyphIndex == 0) - { - var codepoint = Codepoint.ReadAt(textSpan, glyphCluster, out _); - - if (codepoint.GeneralCategory == GeneralCategory.Control) - { - glyphIndex = options.Typeface.GetGlyph(ZeroWidthSpace); - } - } + var glyphCluster = (int)sourceInfo.Cluster; var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index f933ffb17be..ddb10a31daf 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -1374,6 +1374,29 @@ public void Should_GetPreviousCharacterHit_Non_Trailing() } } + [Fact] + public void Should_Ignore_Null_Terminator() + { + var text = "\x0"; + + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var textSource = new SingleBufferTextSource(text, defaultProperties, true); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left, + true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0)); + + Assert.NotNull(textLine); + + Assert.Equal(0, textLine.Width); + } + } + private class FixedRunsTextSource : ITextSource { private readonly IReadOnlyList _textRuns; From 483ce3949496b1360435fa534d0381896fe0054a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 26 Sep 2024 07:21:39 +0200 Subject: [PATCH 2/2] Replace multiple null terminators at random positions --- .../Media/TextFormatting/TextCharacters.cs | 23 ++++++++++++++----- .../Media/TextFormatting/TextLineTests.cs | 12 ++++++---- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 3ad196f281f..d001f8f3701 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting /// public class TextCharacters : TextRun { - private static char[] ZeroWidthSpace = ['\u200b']; + private static char ZeroWidthSpace = '\u200b'; /// /// Constructs a run for text content from a string. @@ -84,16 +84,21 @@ private static UnshapedTextRun CreateShapeableRun(ReadOnlyMemory text, var previousGlyphTypeface = previousProperties?.CachedGlyphTypeface; var textSpan = text.Span; - //Read first codepoint - var firstCodepoint = Codepoint.ReadAt(textSpan, 0, out _); + var count = 0; + var codepoints = new CodepointEnumerator(textSpan); + + while(codepoints.MoveNext(out var firstCodepoint) && firstCodepoint.Value == 0) + { + count++; + } //Detect null terminator - if (firstCodepoint.Value == 0) + if (count > 0) { - return new UnshapedTextRun(ZeroWidthSpace.AsMemory(), defaultProperties, biDiLevel); + return new UnshapedTextRun(new string(ZeroWidthSpace, count).AsMemory(), defaultProperties, biDiLevel); } - if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out var count)) + if (TryGetShapeableLength(textSpan, defaultGlyphTypeface, null, out count)) { return new UnshapedTextRun(text.Slice(0, count), defaultProperties.WithTypeface(defaultTypeface), biDiLevel); @@ -188,6 +193,12 @@ internal static bool TryGetShapeableLength( var currentCodepoint = currentGrapheme.FirstCodepoint; var currentScript = currentCodepoint.Script; + if(currentCodepoint.Value == 0) + { + //Do not include null terminators + break; + } + if (!currentCodepoint.IsWhiteSpace && defaultGlyphTypeface != null && defaultGlyphTypeface.TryGetGlyph(currentCodepoint, out _)) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index ddb10a31daf..6f8b2c407fd 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -1374,11 +1374,13 @@ public void Should_GetPreviousCharacterHit_Non_Trailing() } } - [Fact] - public void Should_Ignore_Null_Terminator() + [Theory] + [InlineData("\0", 0.0)] + [InlineData("\0\0\0", 0.0)] + [InlineData("\0A\0\0", 7.201171875)] + [InlineData("\0AA\0AA\0", 28.8046875)] + public void Should_Ignore_Null_Terminator(string text, double width) { - var text = "\x0"; - using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); @@ -1393,7 +1395,7 @@ public void Should_Ignore_Null_Terminator() Assert.NotNull(textLine); - Assert.Equal(0, textLine.Width); + Assert.Equal(width, textLine.Width); } }