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