Browse Source

Merge pull request #7344 from Gillibald/feature/fullFontTestBackend

Introduce font related platform implementations for unit tests
repro/minimal-repro-stackoverflow-onewaytosource-binding
Max Katz 4 years ago
committed by Dan Walmsley
parent
commit
01f44de076
  1. 1
      tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj
  2. 96
      tests/Avalonia.UnitTests/HarfBuzzFontManagerImpl.cs
  3. 158
      tests/Avalonia.UnitTests/HarfBuzzGlyphTypefaceImpl.cs
  4. 147
      tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs
  5. 6
      tests/Avalonia.UnitTests/TestServices.cs
  6. 11
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs
  7. 6
      tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs

1
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
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
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
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
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,

11
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
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)

Loading…
Cancel
Save