committed by
GitHub
16 changed files with 540 additions and 43 deletions
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue