Pārlūkot izejas kodu

Introduce HarfBuzz platform implementations for unit tests

Benedikt Stebner 3 gadi atpakaļ
vecāks
revīzija
1855914717

+ 1 - 0
tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj

@@ -29,4 +29,5 @@
   <Import Project="..\..\build\Moq.props" />
   <Import Project="..\..\build\Rx.props" />
   <Import Project="..\..\build\SharedVersion.props" />
+  <Import Project="..\..\build\HarfBuzzSharp.props" />
 </Project>

+ 96 - 0
tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs

@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Platform;
+
+namespace Avalonia.UnitTests
+{
+    public class HarfBuzzFontManagerImpl : IFontManagerImpl
+    {
+        private readonly Typeface[] _customTypefaces;
+        private readonly string _defaultFamilyName;
+
+        private static readonly Typeface _defaultTypeface =
+            new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono");
+        private  static readonly Typeface _italicTypeface =
+            new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Sans");
+        private  static readonly Typeface _emojiTypeface =
+            new Typeface("resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Twitter Color Emoji");
+
+        public HarfBuzzFontManagerImpl(string defaultFamilyName = "resm:Avalonia.UnitTests.Assets?assembly=Avalonia.UnitTests#Noto Mono")
+        {
+            _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
+            _defaultFamilyName = defaultFamilyName;
+        }
+
+        public string GetDefaultFontFamilyName()
+        {
+            return _defaultFamilyName;
+        }
+
+        public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
+        {
+            return _customTypefaces.Select(x => x.FontFamily!.Name);
+        }
+
+        public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fontWeight, FontFamily fontFamily,
+            CultureInfo culture, out Typeface fontKey)
+        {
+            foreach (var customTypeface in _customTypefaces)
+            {
+                var glyphTypeface = customTypeface.GlyphTypeface;
+
+                if (!glyphTypeface.TryGetGlyph((uint)codepoint, out _))
+                {
+                    continue;
+                }
+                
+                fontKey = customTypeface;
+                    
+                return true;
+            }
+
+            fontKey = default;
+
+            return false;
+        }
+
+        public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
+        {
+            var fontFamily = typeface.FontFamily;
+
+            if (fontFamily == null)
+            {
+                return null;
+            }
+
+            if (fontFamily.IsDefault)
+            {
+                fontFamily = _defaultTypeface.FontFamily;
+            }
+            
+            if (fontFamily!.Key == null)
+            {
+                return null;
+            }
+            
+            var fontAssets = FontFamilyLoader.LoadFontAssets(fontFamily.Key);
+
+            var asset = fontAssets.First();
+            
+            var assetLoader = AvaloniaLocator.Current.GetService<IAssetLoader>();
+
+            if (assetLoader == null)
+            {
+                throw new NotSupportedException("IAssetLoader is not registered.");
+            }
+            
+            var stream = assetLoader.Open(asset);
+            
+            return new HarfBuzzGlyphTypefaceImpl(stream);
+        }
+    }
+}

+ 158 - 0
tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs

@@ -0,0 +1,158 @@
+using System;
+using System.IO;
+using Avalonia.Platform;
+using HarfBuzzSharp;
+
+namespace Avalonia.UnitTests
+{
+   public class HarfBuzzGlyphTypefaceImpl : IGlyphTypefaceImpl
+    {
+        private bool _isDisposed;
+        private Blob _blob;
+
+        public HarfBuzzGlyphTypefaceImpl(Stream data, bool isFakeBold = false, bool isFakeItalic = false)
+        {
+            _blob = Blob.FromStream(data);
+            
+            Face = new Face(_blob, 0);
+
+            Font = new Font(Face);
+
+            Font.SetFunctionsOpenType();
+
+            Font.GetScale(out var scale, out _);
+            
+            DesignEmHeight = (short)scale;
+
+            var metrics = Font.OpenTypeMetrics;
+
+            const double defaultFontRenderingEmSize = 12.0;
+
+            Ascent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalAscender) / defaultFontRenderingEmSize * DesignEmHeight);
+
+            Descent = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalDescender) / defaultFontRenderingEmSize * DesignEmHeight);
+
+            LineGap = (int)(metrics.GetXVariation(OpenTypeMetricsTag.HorizontalLineGap) / defaultFontRenderingEmSize * DesignEmHeight);
+
+            UnderlinePosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineOffset) / defaultFontRenderingEmSize * DesignEmHeight);
+
+            UnderlineThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.UnderlineSize) / defaultFontRenderingEmSize * DesignEmHeight);
+
+            StrikethroughPosition = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutOffset) / defaultFontRenderingEmSize * DesignEmHeight);
+
+            StrikethroughThickness = (int)(metrics.GetXVariation(OpenTypeMetricsTag.StrikeoutSize) / defaultFontRenderingEmSize * DesignEmHeight);
+
+            IsFixedPitch = GetGlyphAdvance(GetGlyph('a')) == GetGlyphAdvance(GetGlyph('b'));
+
+            IsFakeBold = isFakeBold;
+
+            IsFakeItalic = isFakeItalic;
+        }
+
+        public Face Face { get; }
+
+        public Font Font { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public short DesignEmHeight { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int Ascent { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int Descent { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int LineGap { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int UnderlinePosition { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int UnderlineThickness { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int StrikethroughPosition { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int StrikethroughThickness { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public bool IsFixedPitch { get; }
+        
+        public bool IsFakeBold { get; }
+        
+        public bool IsFakeItalic { get; }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public ushort GetGlyph(uint codepoint)
+        {
+            if (Font.TryGetGlyph(codepoint, out var glyph))
+            {
+                return (ushort)glyph;
+            }
+
+            return 0;
+        }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints)
+        {
+            var glyphs = new ushort[codepoints.Length];
+
+            for (var i = 0; i < codepoints.Length; i++)
+            {
+                if (Font.TryGetGlyph(codepoints[i], out var glyph))
+                {
+                    glyphs[i] = (ushort)glyph;
+                }
+            }
+
+            return glyphs;
+        }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int GetGlyphAdvance(ushort glyph)
+        {
+            return Font.GetHorizontalGlyphAdvance(glyph);
+        }
+
+        /// <inheritdoc cref="IGlyphTypefaceImpl"/>
+        public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs)
+        {
+            var glyphIndices = new uint[glyphs.Length];
+
+            for (var i = 0; i < glyphs.Length; i++)
+            {
+                glyphIndices[i] = glyphs[i];
+            }
+
+            return Font.GetHorizontalGlyphAdvances(glyphIndices);
+        }
+
+        private void Dispose(bool disposing)
+        {
+            if (_isDisposed)
+            {
+                return;
+            }
+
+            _isDisposed = true;
+
+            if (!disposing)
+            {
+                return;
+            }
+
+            Font?.Dispose();
+            Face?.Dispose();
+            _blob?.Dispose();
+        }
+
+        public void Dispose()
+        {
+            Dispose(true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}

+ 147 - 0
tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs

@@ -0,0 +1,147 @@
+using System;
+using System.Globalization;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting.Unicode;
+using Avalonia.Platform;
+using Avalonia.Utilities;
+using HarfBuzzSharp;
+using Buffer = HarfBuzzSharp.Buffer;
+
+namespace Avalonia.UnitTests
+{
+    public class HarfBuzzTextShaperImpl : ITextShaperImpl
+    {
+        public GlyphRun ShapeText(ReadOnlySlice<char> text, Typeface typeface, double fontRenderingEmSize,
+            CultureInfo culture)
+        {
+            using (var buffer = new Buffer())
+            {
+                FillBuffer(buffer, text);
+
+                buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture);
+
+                buffer.GuessSegmentProperties();
+
+                var glyphTypeface = typeface.GlyphTypeface;
+
+                var font = ((HarfBuzzGlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font;
+
+                font.Shape(buffer);
+
+                font.GetScale(out var scaleX, out _);
+
+                var textScale = fontRenderingEmSize / scaleX;
+
+                var bufferLength = buffer.Length;
+
+                var glyphInfos = buffer.GetGlyphInfoSpan();
+
+                var glyphPositions = buffer.GetGlyphPositionSpan();
+
+                var glyphIndices = new ushort[bufferLength];
+
+                var clusters = new ushort[bufferLength];
+
+                double[] glyphAdvances = null;
+
+                Vector[] glyphOffsets = null;
+
+                for (var i = 0; i < bufferLength; i++)
+                {
+                    glyphIndices[i] = (ushort)glyphInfos[i].Codepoint;
+
+                    clusters[i] = (ushort)glyphInfos[i].Cluster;
+
+                    if (!glyphTypeface.IsFixedPitch)
+                    {
+                        SetAdvance(glyphPositions, i, textScale, ref glyphAdvances);
+                    }
+
+                    SetOffset(glyphPositions, i, textScale, ref glyphOffsets);
+                }
+
+                return new GlyphRun(glyphTypeface, fontRenderingEmSize,
+                    new ReadOnlySlice<ushort>(glyphIndices),
+                    new ReadOnlySlice<double>(glyphAdvances),
+                    new ReadOnlySlice<Vector>(glyphOffsets),
+                    text,
+                    new ReadOnlySlice<ushort>(clusters),
+                    buffer.Direction == Direction.LeftToRight ? 0 : 1);
+            }
+        }
+
+        private static void FillBuffer(Buffer buffer, ReadOnlySlice<char> text)
+        {
+            buffer.ContentType = ContentType.Unicode;
+
+            var i = 0;
+
+            while (i < text.Length)
+            {
+                var codepoint = Codepoint.ReadAt(text, i, out var count);
+
+                var cluster = (uint)(text.Start + i);
+
+                if (codepoint.IsBreakChar)
+                {
+                    if (i + 1 < text.Length)
+                    {
+                        var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _);
+
+                        if (nextCodepoint == '\n' && codepoint == '\r')
+                        {
+                            count++;
+
+                            buffer.Add('\u200C', cluster);
+
+                            buffer.Add('\u200D', cluster);
+                        }
+                        else
+                        {
+                            buffer.Add('\u200C', cluster);
+                        }
+                    }
+                    else
+                    {
+                        buffer.Add('\u200C', cluster);
+                    }
+                }
+                else
+                {
+                    buffer.Add(codepoint, cluster);
+                }
+
+                i += count;
+            }
+        }
+
+        private static void SetOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
+            ref Vector[] offsetBuffer)
+        {
+            var position = glyphPositions[index];
+
+            if (position.XOffset == 0 && position.YOffset == 0)
+            {
+                return;
+            }
+
+            offsetBuffer ??= new Vector[glyphPositions.Length];
+
+            var offsetX = position.XOffset * textScale;
+
+            var offsetY = position.YOffset * textScale;
+
+            offsetBuffer[index] = new Vector(offsetX, offsetY);
+        }
+
+        private static void SetAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale,
+            ref double[] advanceBuffer)
+        {
+            advanceBuffer ??= new double[glyphPositions.Length];
+
+            // Depends on direction of layout
+            // advanceBuffer[index] = buffer.GlyphPositions[index].YAdvance * textScale;
+            advanceBuffer[index] = glyphPositions[index].XAdvance * textScale;
+        }
+    }
+}

+ 6 - 0
tests/Avalonia.UnitTests/TestServices.cs

@@ -58,6 +58,12 @@ namespace Avalonia.UnitTests
         public static readonly TestServices RealStyler = new TestServices(
             styler: new Styler());
 
+        public static readonly TestServices TextServices = new TestServices(
+            assetLoader: new AssetLoader(),
+            renderInterface: new MockPlatformRenderInterface(),
+            fontManagerImpl: new HarfBuzzFontManagerImpl(),
+            textShaperImpl: new HarfBuzzTextShaperImpl());
+        
         public TestServices(
             IAssetLoader assetLoader = null,
             IFocusManager focusManager = null,

+ 10 - 1
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@@ -48,7 +48,16 @@ namespace Avalonia.Visuals.UnitTests.Media
         [Fact]
         public void Should_Use_FontManagerOptions_FontFallback()
         {
-            var options = new FontManagerOptions { FontFallbacks = new[] { new FontFallback { FontFamily = new FontFamily("MyFont"), UnicodeRange = UnicodeRange.Default} } };
+            var options = new FontManagerOptions
+            {
+                FontFallbacks = new[]
+                {
+                    new FontFallback
+                    {
+                        FontFamily = new FontFamily("MyFont"), UnicodeRange = UnicodeRange.Default
+                    }
+                }
+            };
 
             using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
                 .With(fontManagerImpl: new MockFontManagerImpl())))

+ 6 - 0
tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs

@@ -22,6 +22,7 @@ namespace Avalonia.Visuals.UnitTests.Media
         [Theory]
         public void Should_Get_Distance_From_CharacterHit(double[] advances, ushort[] clusters, int start, int trailingLength, double expectedDistance)
         {
+            using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
             using (var glyphRun = CreateGlyphRun(advances, clusters))
             {
                 var characterHit = new CharacterHit(start, trailingLength);
@@ -40,6 +41,7 @@ namespace Avalonia.Visuals.UnitTests.Media
         public void Should_Get_CharacterHit_FromDistance(double[] advances, ushort[] clusters, double distance, int start,
             int trailingLengthExpected, bool isInsideExpected)
         {
+            using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
             using (var glyphRun = CreateGlyphRun(advances, clusters))
             {
                 var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside);
@@ -63,6 +65,7 @@ namespace Avalonia.Visuals.UnitTests.Media
         public void Should_Find_Nearest_CharacterHit(double[] advances, ushort[] clusters, int bidiLevel,
             int index, int expectedIndex, int expectedLength, double expectedWidth)
         {
+            using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
             using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
             {
                 var textBounds = glyphRun.FindNearestCharacterHit(index, out var width);
@@ -87,6 +90,7 @@ namespace Avalonia.Visuals.UnitTests.Media
             int nextIndex, int nextLength,
             int bidiLevel)
         {
+            using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
             using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
             {
                 var characterHit = glyphRun.GetNextCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
@@ -109,6 +113,7 @@ namespace Avalonia.Visuals.UnitTests.Media
             int previousIndex, int previousLength,
             int bidiLevel)
         {
+            using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
             using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
             {
                 var characterHit = glyphRun.GetPreviousCaretCharacterHit(new CharacterHit(currentIndex, currentLength));
@@ -128,6 +133,7 @@ namespace Avalonia.Visuals.UnitTests.Media
         [Theory]
         public void Should_Find_Glyph_Index(double[] advances, ushort[] clusters, int bidiLevel)
         {
+            using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
             using (var glyphRun = CreateGlyphRun(advances, clusters, bidiLevel))
             {
                 if (glyphRun.IsLeftToRight)