Browse Source

[Text] Multiple text processing fixes (#15837)

* Add font table loading

* Add localized family names

* Adjust license reference

* Add support for localized family names to the FontManager

* Add supported font features list

* Add unit test

* Fix font metrics

* Fix TextLineImpl baseline calculation of drawable runs

* Invert InlineRun baseline

* Adjust drawable run ascent offset calculation
Benedikt Stebner 1 year ago
parent
commit
2dfd9be66a

+ 35 - 17
src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs

@@ -45,27 +45,11 @@ namespace Avalonia.Media.Fonts
 
                 if (fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
                 {
-                    if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces))
-                    {
-                        glyphTypefaces = new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
-
-                        if (_glyphTypefaceCache.TryAdd(glyphTypeface.FamilyName, glyphTypefaces))
-                        {
-                            _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
-                        }
-                    }
-
-                    var key = new FontCollectionKey(
-                           glyphTypeface.Style,
-                           glyphTypeface.Weight,
-                           glyphTypeface.Stretch);
-
-                    glyphTypefaces.TryAdd(key, glyphTypeface);
+                    AddGlyphTypeface(glyphTypeface);
                 }
             }
         }
 
-
         public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
             FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
         {
@@ -142,5 +126,39 @@ namespace Avalonia.Media.Fonts
         }
 
         public override IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator();
+
+        private void AddGlyphTypeface(IGlyphTypeface glyphTypeface)
+        {
+            if (glyphTypeface is IGlyphTypeface2 glyphTypeface2)
+            {
+                foreach (var kvp in glyphTypeface2.FamilyNames)
+                {
+                    var familyName = kvp.Value;
+
+                    AddGlyphTypefaceByFamilyName(familyName, glyphTypeface);
+                }
+            }
+            else
+            {
+                AddGlyphTypefaceByFamilyName(glyphTypeface.FamilyName, glyphTypeface);
+            }
+
+            return;
+
+            void AddGlyphTypefaceByFamilyName(string familyName, IGlyphTypeface glyphTypeface)
+            {
+                var typefaces = _glyphTypefaceCache.GetOrAdd(familyName,
+                    x =>
+                    {
+                        _fontFamilies.Add(new FontFamily(_key, glyphTypeface.FamilyName));
+
+                        return new ConcurrentDictionary<FontCollectionKey, IGlyphTypeface?>();
+                    });
+
+                typefaces.TryAdd(
+                    new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch),
+                    glyphTypeface);
+            }
+        }
     }
 }

+ 71 - 0
src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs

@@ -0,0 +1,71 @@
+using System;
+
+namespace Avalonia.Media.Fonts
+{
+    internal readonly record struct OpenTypeTag
+    {
+        public static readonly OpenTypeTag None = new OpenTypeTag(0, 0, 0, 0);
+        public static readonly OpenTypeTag Max = new OpenTypeTag(byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
+        public static readonly OpenTypeTag MaxSigned = new OpenTypeTag((byte)sbyte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue);
+
+        private readonly uint _value;
+
+        public OpenTypeTag(uint value)
+        {
+            _value = value;
+        }
+
+        public OpenTypeTag(char c1, char c2, char c3, char c4)
+        {
+            _value = (uint)(((byte)c1 << 24) | ((byte)c2 << 16) | ((byte)c3 << 8) | (byte)c4);
+        }
+
+        private OpenTypeTag(byte c1, byte c2, byte c3, byte c4)
+        {
+            _value = (uint)((c1 << 24) | (c2 << 16) | (c3 << 8) | c4);
+        }
+
+        public static OpenTypeTag Parse(string tag)
+        {
+            if (string.IsNullOrEmpty(tag))
+                return None;
+
+            var realTag = new char[4];
+
+            var len = Math.Min(4, tag.Length);
+            var i = 0;
+            for (; i < len; i++)
+                realTag[i] = tag[i];
+            for (; i < 4; i++)
+                realTag[i] = ' ';
+
+            return new OpenTypeTag(realTag[0], realTag[1], realTag[2], realTag[3]);
+        }
+
+        public override string ToString()
+        {
+            if (_value == None)
+            {
+                return nameof(None);
+            }
+            if (_value == Max)
+            {
+                return nameof(Max);
+            }
+            if (_value == MaxSigned)
+            {
+                return nameof(MaxSigned);
+            }
+
+            return string.Concat(
+                (char)(byte)(_value >> 24),
+                (char)(byte)(_value >> 16),
+                (char)(byte)(_value >> 8),
+                (char)(byte)_value);
+        }
+
+        public static implicit operator uint(OpenTypeTag tag) => tag._value;
+
+        public static implicit operator OpenTypeTag(uint tag) => new OpenTypeTag(tag);
+    }
+}

+ 422 - 0
src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs

@@ -0,0 +1,422 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+using System;
+using System.Buffers.Binary;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// BinaryReader using big-endian encoding.
+    /// </summary>
+    [DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")]
+    internal class BigEndianBinaryReader : IDisposable
+    {
+        /// <summary>
+        /// Buffer used for temporary storage before conversion into primitives
+        /// </summary>
+        private readonly byte[] _buffer = new byte[16];
+
+        private readonly bool _leaveOpen;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="BigEndianBinaryReader" /> class.
+        /// Constructs a new binary reader with the given bit converter, reading
+        /// to the given stream, using the given encoding.
+        /// </summary>
+        /// <param name="stream">Stream to read data from</param>
+        /// <param name="leaveOpen">if set to <c>true</c> [leave open].</param>
+        public BigEndianBinaryReader(Stream stream, bool leaveOpen)
+        {
+            BaseStream = stream;
+            StartOfStream = stream.Position;
+            _leaveOpen = leaveOpen;
+        }
+
+        private long StartOfStream { get; }
+
+        /// <summary>
+        /// Gets the underlying stream of the EndianBinaryReader.
+        /// </summary>
+        public Stream BaseStream { get; }
+
+        /// <summary>
+        /// Seeks within the stream.
+        /// </summary>
+        /// <param name="offset">Offset to seek to.</param>
+        /// <param name="origin">Origin of seek operation. If SeekOrigin.Begin, the offset will be set to the start of stream position.</param>
+        public void Seek(long offset, SeekOrigin origin)
+        {
+            // If SeekOrigin.Begin, the offset will be set to the start of stream position.
+            if (origin == SeekOrigin.Begin)
+            {
+                offset += StartOfStream;
+            }
+
+            BaseStream.Seek(offset, origin);
+        }
+
+        /// <summary>
+        /// Reads a single byte from the stream.
+        /// </summary>
+        /// <returns>The byte read</returns>
+        public byte ReadByte()
+        {
+            ReadInternal(_buffer, 1);
+            return _buffer[0];
+        }
+
+        /// <summary>
+        /// Reads a single signed byte from the stream.
+        /// </summary>
+        /// <returns>The byte read</returns>
+        public sbyte ReadSByte()
+        {
+            ReadInternal(_buffer, 1);
+            return unchecked((sbyte)_buffer[0]);
+        }
+
+        public float ReadF2dot14()
+        {
+            const float f2Dot14ToFloat = 16384.0f;
+            return ReadInt16() / f2Dot14ToFloat;
+        }
+
+        /// <summary>
+        /// Reads a 16-bit signed integer from the stream, using the bit converter
+        /// for this reader. 2 bytes are read.
+        /// </summary>
+        /// <returns>The 16-bit integer read</returns>
+        public short ReadInt16()
+        {
+            ReadInternal(_buffer, 2);
+
+            return BinaryPrimitives.ReadInt16BigEndian(_buffer);
+        }
+
+        public TEnum ReadInt16<TEnum>()
+            where TEnum : struct, Enum
+        {
+            TryConvert(ReadUInt16(), out TEnum value);
+            return value;
+        }
+
+        public short ReadFWORD() => ReadInt16();
+
+        public short[] ReadFWORDArray(int length) => ReadInt16Array(length);
+
+        public ushort ReadUFWORD() => ReadUInt16();
+
+        /// <summary>
+        /// Reads a fixed 32-bit value from the stream.
+        /// 4 bytes are read.
+        /// </summary>
+        /// <returns>The 32-bit value read.</returns>
+        public float ReadFixed()
+        {
+            ReadInternal(_buffer, 4);
+            return BinaryPrimitives.ReadInt32BigEndian(_buffer) / 65536F;
+        }
+
+        /// <summary>
+        /// Reads a 32-bit signed integer from the stream, using the bit converter
+        /// for this reader. 4 bytes are read.
+        /// </summary>
+        /// <returns>The 32-bit integer read</returns>
+        public int ReadInt32()
+        {
+            ReadInternal(_buffer, 4);
+
+            return BinaryPrimitives.ReadInt32BigEndian(_buffer);
+        }
+
+        /// <summary>
+        /// Reads a 64-bit signed integer from the stream.
+        /// 8 bytes are read.
+        /// </summary>
+        /// <returns>The 64-bit integer read.</returns>
+        public long ReadInt64()
+        {
+            ReadInternal(_buffer, 8);
+
+            return BinaryPrimitives.ReadInt64BigEndian(_buffer);
+        }
+
+        /// <summary>
+        /// Reads a 16-bit unsigned integer from the stream.
+        /// 2 bytes are read.
+        /// </summary>
+        /// <returns>The 16-bit unsigned integer read.</returns>
+        public ushort ReadUInt16()
+        {
+            ReadInternal(_buffer, 2);
+
+            return BinaryPrimitives.ReadUInt16BigEndian(_buffer);
+        }
+
+        /// <summary>
+        /// Reads a 16-bit unsigned integer from the stream representing an offset position.
+        /// 2 bytes are read.
+        /// </summary>
+        /// <returns>The 16-bit unsigned integer read.</returns>
+        public ushort ReadOffset16() => ReadUInt16();
+
+        public TEnum ReadUInt16<TEnum>()
+            where TEnum : struct, Enum
+        {
+            TryConvert(ReadUInt16(), out TEnum value);
+            return value;
+        }
+
+        /// <summary>
+        /// Reads array of 16-bit unsigned integers from the stream.
+        /// </summary>
+        /// <param name="length">The length.</param>
+        /// <returns>
+        /// The 16-bit unsigned integer read.
+        /// </returns>
+        public ushort[] ReadUInt16Array(int length)
+        {
+            ushort[] data = new ushort[length];
+            for (int i = 0; i < length; i++)
+            {
+                data[i] = ReadUInt16();
+            }
+
+            return data;
+        }
+
+        /// <summary>
+        /// Reads array of 16-bit unsigned integers from the stream to the buffer.
+        /// </summary>
+        /// <param name="buffer">The buffer to read to.</param>
+        public void ReadUInt16Array(Span<ushort> buffer)
+        {
+            for (int i = 0; i < buffer.Length; i++)
+            {
+                buffer[i] = ReadUInt16();
+            }
+        }
+
+        /// <summary>
+        /// Reads array or 32-bit unsigned integers from the stream.
+        /// </summary>
+        /// <param name="length">The length.</param>
+        /// <returns>
+        /// The 32-bit unsigned integer read.
+        /// </returns>
+        public uint[] ReadUInt32Array(int length)
+        {
+            uint[] data = new uint[length];
+            for (int i = 0; i < length; i++)
+            {
+                data[i] = ReadUInt32();
+            }
+
+            return data;
+        }
+
+        public byte[] ReadUInt8Array(int length)
+        {
+            byte[] data = new byte[length];
+
+            ReadInternal(data, length);
+
+            return data;
+        }
+
+        /// <summary>
+        /// Reads array of 16-bit unsigned integers from the stream.
+        /// </summary>
+        /// <param name="length">The length.</param>
+        /// <returns>
+        /// The 16-bit signed integer read.
+        /// </returns>
+        public short[] ReadInt16Array(int length)
+        {
+            short[] data = new short[length];
+            for (int i = 0; i < length; i++)
+            {
+                data[i] = ReadInt16();
+            }
+
+            return data;
+        }
+
+        /// <summary>
+        /// Reads an array of 16-bit signed integers from the stream to the buffer.
+        /// </summary>
+        /// <param name="buffer">The buffer to read to.</param>
+        public void ReadInt16Array(Span<short> buffer)
+        {
+            for (int i = 0; i < buffer.Length; i++)
+            {
+                buffer[i] = ReadInt16();
+            }
+        }
+
+        /// <summary>
+        /// Reads a 8-bit unsigned integer from the stream, using the bit converter
+        /// for this reader. 1 bytes are read.
+        /// </summary>
+        /// <returns>The 8-bit unsigned integer read.</returns>
+        public byte ReadUInt8()
+        {
+            ReadInternal(_buffer, 1);
+            return _buffer[0];
+        }
+
+        /// <summary>
+        /// Reads a 24-bit unsigned integer from the stream, using the bit converter
+        /// for this reader. 3 bytes are read.
+        /// </summary>
+        /// <returns>The 24-bit unsigned integer read.</returns>
+        public int ReadUInt24()
+        {
+            byte highByte = ReadByte();
+            return (highByte << 16) | ReadUInt16();
+        }
+
+        /// <summary>
+        /// Reads a 32-bit unsigned integer from the stream, using the bit converter
+        /// for this reader. 4 bytes are read.
+        /// </summary>
+        /// <returns>The 32-bit unsigned integer read.</returns>
+        public uint ReadUInt32()
+        {
+            ReadInternal(_buffer, 4);
+
+            return BinaryPrimitives.ReadUInt32BigEndian(_buffer);
+        }
+
+        /// <summary>
+        /// Reads a 32-bit unsigned integer from the stream representing an offset position.
+        /// 4 bytes are read.
+        /// </summary>
+        /// <returns>The 32-bit unsigned integer read.</returns>
+        public uint ReadOffset32() => ReadUInt32();
+
+        /// <summary>
+        /// Reads the specified number of bytes, returning them in a new byte array.
+        /// If not enough bytes are available before the end of the stream, this
+        /// method will return what is available.
+        /// </summary>
+        /// <param name="count">The number of bytes to read.</param>
+        /// <returns>The bytes read.</returns>
+        public byte[] ReadBytes(int count)
+        {
+            byte[] ret = new byte[count];
+            int index = 0;
+            while (index < count)
+            {
+                int read = BaseStream.Read(ret, index, count - index);
+
+                // Stream has finished half way through. That's fine, return what we've got.
+                if (read == 0)
+                {
+                    byte[] copy = new byte[index];
+                    Buffer.BlockCopy(ret, 0, copy, 0, index);
+                    return copy;
+                }
+
+                index += read;
+            }
+
+            return ret;
+        }
+
+        /// <summary>
+        /// Reads a string of a specific length, which specifies the number of bytes
+        /// to read from the stream. These bytes are then converted into a string with
+        /// the encoding for this reader.
+        /// </summary>
+        /// <param name="bytesToRead">The bytes to read.</param>
+        /// <param name="encoding">The encoding.</param>
+        /// <returns>
+        /// The string read from the stream.
+        /// </returns>
+        public string ReadString(int bytesToRead, Encoding encoding)
+        {
+            byte[] data = new byte[bytesToRead];
+            ReadInternal(data, bytesToRead);
+            return encoding.GetString(data, 0, data.Length);
+        }
+
+        /// <summary>
+        /// Reads the uint32 string.
+        /// </summary>
+        /// <returns>a 4 character long UTF8 encoded string.</returns>
+        public string ReadTag()
+        {
+            ReadInternal(_buffer, 4);
+
+            return Encoding.UTF8.GetString(_buffer, 0, 4);
+        }
+
+        /// <summary>
+        /// Reads an offset consuming the given nuber of bytes.
+        /// </summary>
+        /// <param name="size">The offset size in bytes.</param>
+        /// <returns>The 32-bit signed integer representing the offset.</returns>
+        /// <exception cref="InvalidOperationException">Size is not in range.</exception>
+        public int ReadOffset(int size)
+            => size switch
+            {
+                1 => ReadByte(),
+                2 => (ReadByte() << 8) | (ReadByte() << 0),
+                3 => (ReadByte() << 16) | (ReadByte() << 8) | (ReadByte() << 0),
+                4 => (ReadByte() << 24) | (ReadByte() << 16) | (ReadByte() << 8) | (ReadByte() << 0),
+                _ => throw new InvalidOperationException(),
+            };
+
+        /// <summary>
+        /// Reads the given number of bytes from the stream, throwing an exception
+        /// if they can't all be read.
+        /// </summary>
+        /// <param name="data">Buffer to read into.</param>
+        /// <param name="size">Number of bytes to read.</param>
+        private void ReadInternal(byte[] data, int size)
+        {
+            int index = 0;
+
+            while (index < size)
+            {
+                int read = BaseStream.Read(data, index, size - index);
+                if (read == 0)
+                {
+                    throw new EndOfStreamException($"End of stream reached with {size - index} byte{(size - index == 1 ? "s" : string.Empty)} left to read.");
+                }
+
+                index += read;
+            }
+        }
+
+        public void Dispose()
+        {
+            if (!_leaveOpen)
+            {
+                BaseStream?.Dispose();
+            }
+        }
+
+        [MethodImpl(MethodImplOptions.AggressiveInlining)]
+        private static bool TryConvert<T, TEnum>(T input, out TEnum value)
+            where T : struct, IConvertible, IFormattable, IComparable
+            where TEnum : struct, Enum
+        {
+            if (Unsafe.SizeOf<T>() == Unsafe.SizeOf<TEnum>())
+            {
+                value = Unsafe.As<T, TEnum>(ref input);
+                return true;
+            }
+
+            value = default;
+            return false;
+        }
+    }
+}

+ 31 - 0
src/Avalonia.Base/Media/Fonts/Tables/EncodingIDExtensions.cs

@@ -0,0 +1,31 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+using System.Text;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// Converts encoding ID to TextEncoding
+    /// </summary>
+    internal static class EncodingIDExtensions
+    {
+        /// <summary>
+        /// Converts encoding ID to TextEncoding
+        /// </summary>
+        /// <param name="id">The identifier.</param>
+        /// <returns>the encoding for this encoding ID</returns>
+        public static Encoding AsEncoding(this EncodingIDs id)
+        {
+            switch (id)
+            {
+                case EncodingIDs.Unicode11:
+                case EncodingIDs.Unicode2:
+                    return Encoding.BigEndianUnicode;
+                default:
+                    return Encoding.UTF8;
+            }
+        }
+    }
+}

+ 47 - 0
src/Avalonia.Base/Media/Fonts/Tables/EncodingIDs.cs

@@ -0,0 +1,47 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// Encoding IDS
+    /// </summary>
+    internal enum EncodingIDs : ushort
+    {
+        /// <summary>
+        /// Unicode 1.0 semantics
+        /// </summary>
+        Unicode1 = 0,
+
+        /// <summary>
+        /// Unicode 1.1 semantics
+        /// </summary>
+        Unicode11 = 1,
+
+        /// <summary>
+        /// ISO/IEC 10646 semantics
+        /// </summary>
+        ISO10646 = 2,
+
+        /// <summary>
+        /// Unicode 2.0 and onwards semantics, Unicode BMP only (cmap subtable formats 0, 4, 6).
+        /// </summary>
+        Unicode2 = 3,
+
+        /// <summary>
+        /// Unicode 2.0 and onwards semantics, Unicode full repertoire (cmap subtable formats 0, 4, 6, 10, 12).
+        /// </summary>
+        Unicode2Plus = 4,
+
+        /// <summary>
+        /// Unicode Variation Sequences (cmap subtable format 14).
+        /// </summary>
+        UnicodeVariationSequences = 5,
+
+        /// <summary>
+        /// Unicode full repertoire (cmap subtable formats 0, 4, 6, 10, 12, 13)
+        /// </summary>
+        UnicodeFull = 6,
+    }
+}

+ 125 - 0
src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs

@@ -0,0 +1,125 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+using System.Collections.Generic;
+using System.IO;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// Features provide information about how to use the glyphs in a font to render a script or language.
+    /// For example, an Arabic font might have a feature for substituting initial glyph forms, and a Kanji font
+    /// might have a feature for positioning glyphs vertically. All OpenType Layout features define data for
+    /// glyph substitution, glyph positioning, or both.
+    /// <see href="https://docs.microsoft.com/en-us/typography/opentype/spec/featurelist"/>
+    /// <see href="https://docs.microsoft.com/en-us/typography/opentype/spec/chapter2#feature-list-table"/>
+    /// </summary>
+    internal class FeatureListTable
+    {
+        private static OpenTypeTag GSubTag = OpenTypeTag.Parse("GSUB");
+        private static OpenTypeTag GPosTag = OpenTypeTag.Parse("GPOS");
+
+        private FeatureListTable(IReadOnlyList<OpenTypeTag> features)
+        {
+            Features = features;
+        }
+
+        public IReadOnlyList<OpenTypeTag> Features { get; }
+
+        public static FeatureListTable? LoadGSub(IGlyphTypeface glyphTypeface)
+        {
+            if (!glyphTypeface.TryGetTable(GSubTag, out var gPosTable))
+            {
+                return null;
+            }
+
+            using var stream = new MemoryStream(gPosTable);
+            using var reader = new BigEndianBinaryReader(stream, false);
+
+            return Load(reader);
+
+        }
+        public static FeatureListTable? LoadGPos(IGlyphTypeface glyphTypeface)
+        {
+            if (!glyphTypeface.TryGetTable(GPosTag, out var gSubTable))
+            {
+                return null;
+            }
+
+            using var stream = new MemoryStream(gSubTable);
+            using var reader = new BigEndianBinaryReader(stream, false);
+
+            return Load(reader);
+
+        }
+
+        private static FeatureListTable Load(BigEndianBinaryReader reader)
+        {
+            // GPOS/GSUB Header, Version 1.0
+            // +----------+-------------------+-----------------------------------------------------------+
+            // | Type     | Name              | Description                                               |
+            // +==========+===================+===========================================================+
+            // | uint16   | majorVersion      | Major version of the GPOS table, = 1                      |
+            // +----------+-------------------+-----------------------------------------------------------+
+            // | uint16   | minorVersion      | Minor version of the GPOS table, = 0                      |
+            // +----------+-------------------+-----------------------------------------------------------+
+            // | Offset16 | scriptListOffset  | Offset to ScriptList table, from beginning of GPOS table  |
+            // +----------+-------------------+-----------------------------------------------------------+
+            // | Offset16 | featureListOffset | Offset to FeatureList table, from beginning of GPOS table |
+            // +----------+-------------------+-----------------------------------------------------------+
+            // | Offset16 | lookupListOffset  | Offset to LookupList table, from beginning of GPOS table  |
+            // +----------+-------------------+-----------------------------------------------------------+
+
+            reader.ReadUInt16();
+            reader.ReadUInt16();
+
+            reader.ReadOffset16();
+            var featureListOffset = reader.ReadOffset16();
+
+            return Load(reader, featureListOffset);
+        }
+
+        private static FeatureListTable Load(BigEndianBinaryReader reader, long offset)
+        {
+            // FeatureList
+            // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
+            // | Type          | Name                         | Description                                                                                                     |
+            // +===============+==============================+=================================================================================================================+
+            // | uint16        | featureCount                 | Number of FeatureRecords in this table                                                                          |
+            // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
+            // | FeatureRecord | featureRecords[featureCount] | Array of FeatureRecords — zero-based (first feature has FeatureIndex = 0), listed alphabetically by feature tag |
+            // +---------------+------------------------------+-----------------------------------------------------------------------------------------------------------------+
+            reader.Seek(offset, SeekOrigin.Begin);
+
+            var featureCount = reader.ReadUInt16();
+
+            var features = new List<OpenTypeTag>(featureCount);
+
+            for (var i = 0; i < featureCount; i++)
+            {
+                // FeatureRecord
+                // +----------+---------------+--------------------------------------------------------+
+                // | Type     | Name          | Description                                            |
+                // +==========+===============+========================================================+
+                // | Tag      | featureTag    | 4-byte feature identification tag                      |
+                // +----------+---------------+--------------------------------------------------------+
+                // | Offset16 | featureOffset | Offset to Feature table, from beginning of FeatureList |
+                // +----------+---------------+--------------------------------------------------------+
+                var featureTag = reader.ReadUInt32();
+
+                reader.ReadOffset16();
+
+                var tag = new OpenTypeTag(featureTag);
+
+                if (!features.Contains(tag))
+                {
+                    features.Add(tag);
+                }
+            }
+
+            return new FeatureListTable(features /*featureTables*/);
+        }
+
+    }
+}

+ 153 - 0
src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs

@@ -0,0 +1,153 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+using System.IO;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    internal class HorizontalHeadTable
+    {
+        internal const string TableName = "hhea";
+        internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName);
+
+        public HorizontalHeadTable(
+            short ascender,
+            short descender,
+            short lineGap,
+            ushort advanceWidthMax,
+            short minLeftSideBearing,
+            short minRightSideBearing,
+            short xMaxExtent,
+            short caretSlopeRise,
+            short caretSlopeRun,
+            short caretOffset,
+            ushort numberOfHMetrics)
+        {
+            Ascender = ascender;
+            Descender = descender;
+            LineGap = lineGap;
+            AdvanceWidthMax = advanceWidthMax;
+            MinLeftSideBearing = minLeftSideBearing;
+            MinRightSideBearing = minRightSideBearing;
+            XMaxExtent = xMaxExtent;
+            CaretSlopeRise = caretSlopeRise;
+            CaretSlopeRun = caretSlopeRun;
+            CaretOffset = caretOffset;
+            NumberOfHMetrics = numberOfHMetrics;
+        }
+
+        public ushort AdvanceWidthMax { get; }
+
+        public short Ascender { get; }
+
+        public short CaretOffset { get; }
+
+        public short CaretSlopeRise { get; }
+
+        public short CaretSlopeRun { get; }
+
+        public short Descender { get; }
+
+        public short LineGap { get; }
+
+        public short MinLeftSideBearing { get; }
+
+        public short MinRightSideBearing { get; }
+
+        public ushort NumberOfHMetrics { get; }
+
+        public short XMaxExtent { get; }
+
+        public static HorizontalHeadTable Load(IGlyphTypeface glyphTypeface)
+        {
+            if (!glyphTypeface.TryGetTable(Tag, out var table))
+            {
+                throw new MissingFontTableException("Could not load table", "name");
+            }
+
+            using var stream = new MemoryStream(table);
+            using var binaryReader = new BigEndianBinaryReader(stream, false);
+
+            // Move to start of table.
+            return Load(binaryReader);
+        }
+
+        public static HorizontalHeadTable Load(BigEndianBinaryReader reader)
+        {
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | Type   | Name                | Description                                                                     |
+            // +========+=====================+=================================================================================+
+            // | Fixed  | version             | 0x00010000 (1.0)                                                                |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | FWord  | ascent              | Distance from baseline of highest ascender                                      |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | FWord  | descent             | Distance from baseline of lowest descender                                      |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | FWord  | lineGap             | typographic line gap                                                            |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | uFWord | advanceWidthMax     | must be consistent with horizontal metrics                                      |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | FWord  | minLeftSideBearing  | must be consistent with horizontal metrics                                      |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | FWord  | minRightSideBearing | must be consistent with horizontal metrics                                      |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | FWord  | xMaxExtent          | max(lsb + (xMax-xMin))                                                          |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | int16  | caretSlopeRise      | used to calculate the slope of the caret (rise/run) set to 1 for vertical caret |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | int16  | caretSlopeRun       | 0 for vertical                                                                  |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | FWord  | caretOffset         | set value to 0 for non-slanted fonts                                            |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | int16  | reserved            | set value to 0                                                                  |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | int16  | reserved            | set value to 0                                                                  |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | int16  | reserved            | set value to 0                                                                  |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | int16  | reserved            | set value to 0                                                                  |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | int16  | metricDataFormat    | 0 for current format                                                            |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            // | uint16 | numOfLongHorMetrics | number of advance widths in metrics table                                       |
+            // +--------+---------------------+---------------------------------------------------------------------------------+
+            ushort majorVersion = reader.ReadUInt16();
+            ushort minorVersion = reader.ReadUInt16();
+            short ascender = reader.ReadFWORD();
+            short descender = reader.ReadFWORD();
+            short lineGap = reader.ReadFWORD();
+            ushort advanceWidthMax = reader.ReadUFWORD();
+            short minLeftSideBearing = reader.ReadFWORD();
+            short minRightSideBearing = reader.ReadFWORD();
+            short xMaxExtent = reader.ReadFWORD();
+            short caretSlopeRise = reader.ReadInt16();
+            short caretSlopeRun = reader.ReadInt16();
+            short caretOffset = reader.ReadInt16();
+            reader.ReadInt16(); // reserved
+            reader.ReadInt16(); // reserved
+            reader.ReadInt16(); // reserved
+            reader.ReadInt16(); // reserved
+            short metricDataFormat = reader.ReadInt16(); // 0
+            if (metricDataFormat != 0)
+            {
+                throw new InvalidFontTableException($"Expected metricDataFormat = 0 found {metricDataFormat}", TableName);
+            }
+
+            ushort numberOfHMetrics = reader.ReadUInt16();
+
+            return new HorizontalHeadTable(
+                ascender,
+                descender,
+                lineGap,
+                advanceWidthMax,
+                minLeftSideBearing,
+                minRightSideBearing,
+                xMaxExtent,
+                caretSlopeRise,
+                caretSlopeRun,
+                caretOffset,
+                numberOfHMetrics);
+        }
+    }
+}

+ 29 - 0
src/Avalonia.Base/Media/Fonts/Tables/InvalidFontTableException.cs

@@ -0,0 +1,29 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+using System;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// Exception font loading can throw if it encounters invalid data during font loading.
+    /// </summary>
+    /// <seealso cref="Exception" />
+    public class InvalidFontTableException : Exception
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="InvalidFontTableException"/> class.
+        /// </summary>
+        /// <param name="message">The message that describes the error.</param>
+        /// <param name="table">The table.</param>
+        public InvalidFontTableException(string message, string table)
+            : base(message)
+            => Table = table;
+
+        /// <summary>
+        /// Gets the table where the error originated.
+        /// </summary>
+        public string Table { get; }
+    }
+}

+ 123 - 0
src/Avalonia.Base/Media/Fonts/Tables/KnownNameIds.cs

@@ -0,0 +1,123 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// Provides enumeration of common name ids
+    /// <see href="https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids"/>
+    /// </summary>
+    public enum KnownNameIds : ushort
+    {
+        /// <summary>
+        /// The copyright notice
+        /// </summary>
+        CopyrightNotice = 0,
+
+        /// <summary>
+        /// The font family name; Up to four fonts can share the Font Family name, forming a font style linking
+        /// group (regular, italic, bold, bold italic — as defined by OS/2.fsSelection bit settings).
+        /// </summary>
+        FontFamilyName = 1,
+
+        /// <summary>
+        /// The font subfamily name; The Font Subfamily name distinguishes the font in a group with the same Font Family name (name ID 1).
+        /// This is assumed to address style (italic, oblique) and weight (light, bold, black, etc.). A font with no particular differences
+        /// in weight or style (e.g. medium weight, not italic and fsSelection bit 6 set) should have the string "Regular" stored in this position.
+        /// </summary>
+        FontSubfamilyName = 2,
+
+        /// <summary>
+        /// The unique font identifier
+        /// </summary>
+        UniqueFontID = 3,
+
+        /// <summary>
+        /// The full font name; a combination of strings 1 and 2, or a similar human-readable variant. If string 2 is "Regular", it is sometimes omitted from name ID 4.
+        /// </summary>
+        FullFontName = 4,
+
+        /// <summary>
+        /// Version string. Should begin with the syntax 'Version &lt;number&gt;.&lt;number>' (upper case, lower case, or mixed, with a space between "Version" and the number).
+        /// The string must contain a version number of the following form: one or more digits (0-9) of value less than 65,535, followed by a period, followed by one or more
+        /// digits of value less than 65,535. Any character other than a digit will terminate the minor number. A character such as ";" is helpful to separate different pieces of version information.
+        /// The first such match in the string can be used by installation software to compare font versions.
+        /// Note that some installers may require the string to start with "Version ", followed by a version number as above.
+        /// </summary>
+        Version = 5,
+
+        /// <summary>
+        /// Postscript name for the font; Name ID 6 specifies a string which is used to invoke a PostScript language font that corresponds to this OpenType font.
+        /// When translated to ASCII, the name string must be no longer than 63 characters and restricted to the printable ASCII subset, codes 33 to 126,
+        /// except for the 10 characters '[', ']', '(', ')', '{', '}', '&lt;', '&gt;', '/', '%'.
+        /// In a CFF OpenType font, there is no requirement that this name be the same as the font name in the CFF’s Name INDEX.
+        /// Thus, the same CFF may be shared among multiple font components in a Font Collection. See the 'name' table section of
+        /// Recommendations for OpenType fonts "" for additional information.
+        /// </summary>
+        PostscriptName = 6,
+
+        /// <summary>
+        /// Trademark; this is used to save any trademark notice/information for this font. Such information should
+        /// be based on legal advice. This is distinctly separate from the copyright.
+        /// </summary>
+        Trademark = 7,
+
+        /// <summary>
+        /// The manufacturer
+        /// </summary>
+        Manufacturer = 8,
+
+        /// <summary>
+        /// Designer; name of the designer of the typeface.
+        /// </summary>
+        Designer = 9,
+
+        /// <summary>
+        /// Description; description of the typeface. Can contain revision information, usage recommendations, history, features, etc.
+        /// </summary>
+        Description = 10,
+
+        /// <summary>
+        /// URL Vendor; URL of font vendor (with protocol, e.g., http://, ftp://). If a unique serial number is embedded in
+        /// the URL, it can be used to register the font.
+        /// </summary>
+        VendorUrl = 11,
+
+        /// <summary>
+        /// URL Designer; URL of typeface designer (with protocol, e.g., http://, ftp://).
+        /// </summary>
+        DesignerUrl = 12,
+
+        /// <summary>
+        /// License Description; description of how the font may be legally used, or different example scenarios for licensed use.
+        /// This field should be written in plain language, not legalese.
+        /// </summary>
+        LicenseDescription = 13,
+
+        /// <summary>
+        /// License Info URL; URL where additional licensing information can be found.
+        /// </summary>
+        LicenseInfoUrl = 14,
+
+        /// <summary>
+        /// Typographic Family name: The typographic family grouping doesn't impose any constraints on the number of faces within it,
+        /// in contrast with the 4-style family grouping (ID 1), which is present both for historical reasons and to express style linking groups.
+        /// If name ID 16 is absent, then name ID 1 is considered to be the typographic family name.
+        /// (In earlier versions of the specification, name ID 16 was known as "Preferred Family".)
+        /// </summary>
+        TypographicFamilyName = 16,
+
+        /// <summary>
+        /// Typographic Subfamily name: This allows font designers to specify a subfamily name within the typographic family grouping.
+        /// This string must be unique within a particular typographic family. If it is absent, then name ID 2 is considered to be the
+        /// typographic subfamily name. (In earlier versions of the specification, name ID 17 was known as "Preferred Subfamily".)
+        /// </summary>
+        TypographicSubfamilyName = 17,
+
+        /// <summary>
+        /// Sample text; This can be the font name, or any other text that the designer thinks is the best sample to display the font in.
+        /// </summary>
+        SampleText = 19,
+    }
+}

+ 29 - 0
src/Avalonia.Base/Media/Fonts/Tables/MissingFontTableException.cs

@@ -0,0 +1,29 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+using System;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// Exception font loading can throw if it finds a required table is missing during font loading.
+    /// </summary>
+    /// <seealso cref="Exception" />
+    public class MissingFontTableException : Exception
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="MissingFontTableException"/> class.
+        /// </summary>
+        /// <param name="message">The message that describes the error.</param>
+        /// <param name="table">The table.</param>
+        public MissingFontTableException(string message, string table)
+            : base(message)
+            => Table = table;
+
+        /// <summary>
+        /// Gets the table where the error originated.
+        /// </summary>
+        public string Table { get; }
+    }
+}

+ 45 - 0
src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs

@@ -0,0 +1,45 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+namespace Avalonia.Media.Fonts.Tables.Name
+{
+    internal class NameRecord
+    {
+        private readonly string value;
+
+        public NameRecord(PlatformIDs platform, ushort languageId, KnownNameIds nameId, string value)
+        {
+            Platform = platform;
+            LanguageID = languageId;
+            NameID = nameId;
+            this.value = value;
+        }
+
+        public PlatformIDs Platform { get; }
+
+        public ushort LanguageID { get; }
+
+        public KnownNameIds NameID { get; }
+
+        internal StringLoader? StringReader { get; private set; }
+
+        public string Value => StringReader?.Value ?? value;
+
+        public static NameRecord Read(BigEndianBinaryReader reader)
+        {
+            var platform = reader.ReadUInt16<PlatformIDs>();
+            var encodingId = reader.ReadUInt16<EncodingIDs>();
+            var encoding = encodingId.AsEncoding();
+            var languageID = reader.ReadUInt16();
+            var nameID = reader.ReadUInt16<KnownNameIds>();
+
+            var stringReader = StringLoader.Create(reader, encoding);
+
+            return new NameRecord(platform, languageID, nameID, string.Empty)
+            {
+                StringReader = stringReader
+            };
+        }
+    }
+}

+ 185 - 0
src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs

@@ -0,0 +1,185 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+using System.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+
+namespace Avalonia.Media.Fonts.Tables.Name
+{
+    internal class NameTable
+    {
+        internal const string TableName = "name";
+        internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName);
+
+        private readonly NameRecord[] _names;
+
+        internal NameTable(NameRecord[] names, IReadOnlyList<CultureInfo> languages)
+        {
+            _names = names;
+            Languages = languages;
+        }
+
+        public IReadOnlyList<CultureInfo> Languages { get; }
+
+        /// <summary>
+        /// Gets the name of the font.
+        /// </summary>
+        /// <value>
+        /// The name of the font.
+        /// </value>
+        public string Id(CultureInfo culture)
+            => GetNameById(culture, KnownNameIds.UniqueFontID);
+
+        /// <summary>
+        /// Gets the name of the font.
+        /// </summary>
+        /// <value>
+        /// The name of the font.
+        /// </value>
+        public string FontName(CultureInfo culture)
+            => GetNameById(culture, KnownNameIds.FullFontName);
+
+        /// <summary>
+        /// Gets the name of the font.
+        /// </summary>
+        /// <value>
+        /// The name of the font.
+        /// </value>
+        public string FontFamilyName(CultureInfo culture)
+            => GetNameById(culture, KnownNameIds.FontFamilyName);
+
+        /// <summary>
+        /// Gets the name of the font.
+        /// </summary>
+        /// <value>
+        /// The name of the font.
+        /// </value>
+        public string FontSubFamilyName(CultureInfo culture)
+            => GetNameById(culture, KnownNameIds.FontSubfamilyName);
+
+        public string GetNameById(CultureInfo culture, KnownNameIds nameId)
+        {
+            var languageId = culture.LCID;
+            NameRecord? usaVersion = null;
+            NameRecord? firstWindows = null;
+            NameRecord? first = null;
+            foreach (var name in _names)
+            {
+                if (name.NameID == nameId)
+                {
+                    // Get just the first one, just in case.
+                    first ??= name;
+                    if (name.Platform == PlatformIDs.Windows)
+                    {
+                        // If us not found return the first windows one.
+                        firstWindows ??= name;
+                        if (name.LanguageID == 0x0409)
+                        {
+                            // Grab the us version as its on next best match.
+                            usaVersion ??= name;
+                        }
+
+                        if (name.LanguageID == languageId)
+                        {
+                            // Return the most exact first.
+                            return name.Value;
+                        }
+                    }
+                }
+            }
+
+            return usaVersion?.Value ??
+                   firstWindows?.Value ??
+                   first?.Value ??
+                   string.Empty;
+        }
+
+        public string GetNameById(CultureInfo culture, ushort nameId)
+            => GetNameById(culture, (KnownNameIds)nameId);
+
+        public static NameTable Load(IGlyphTypeface glyphTypeface)
+        {
+            if (!glyphTypeface.TryGetTable(Tag, out var table))
+            {
+                throw new MissingFontTableException("Could not load table", "name");
+            }
+
+            using var stream = new MemoryStream(table);
+            using var binaryReader = new BigEndianBinaryReader(stream, false);
+
+            // Move to start of table.
+            return Load(binaryReader);
+        }
+
+        public static NameTable Load(BigEndianBinaryReader reader)
+        {
+            var strings = new List<StringLoader>();
+            var format = reader.ReadUInt16();
+            var nameCount = reader.ReadUInt16();
+            var stringOffset = reader.ReadUInt16();
+
+            var names = new NameRecord[nameCount];
+
+            for (var i = 0; i < nameCount; i++)
+            {
+                names[i] = NameRecord.Read(reader);
+
+                var sr = names[i].StringReader;
+
+                if (sr is not null)
+                {
+                    strings.Add(sr);
+                }
+            }
+
+            //var languageNames = Array.Empty<StringLoader>();
+
+            //if (format == 1)
+            //{
+            //    // Format 1 adds language data.
+            //    var langCount = reader.ReadUInt16();
+            //    languageNames = new StringLoader[langCount];
+
+            //    for (var i = 0; i < langCount; i++)
+            //    {
+            //        languageNames[i] = StringLoader.Create(reader);
+
+            //        strings.Add(languageNames[i]);
+            //    }
+            //}
+
+            foreach (var readable in strings)
+            {
+                var readableStartOffset = stringOffset + readable.Offset;
+
+                reader.Seek(readableStartOffset, SeekOrigin.Begin);
+
+                readable.LoadValue(reader);
+            }
+
+            var cultures = new List<CultureInfo>();
+
+            foreach (var nameRecord in names)
+            {
+                if (nameRecord.NameID != KnownNameIds.FontFamilyName || nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0)
+                {
+                    continue;
+                }
+
+                var culture = new CultureInfo(nameRecord.LanguageID);
+
+                if (!cultures.Contains(culture))
+                {
+                    cultures.Add(culture);
+                }
+            }
+
+            //var languages = languageNames.Select(x => x.Value).ToArray();
+
+            return new NameTable(names, cultures);
+        }
+    }
+}

+ 423 - 0
src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs

@@ -0,0 +1,423 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+using System;
+using System.IO;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    internal sealed class OS2Table
+    {
+        internal const string TableName = "OS/2";
+        internal static OpenTypeTag Tag = OpenTypeTag.Parse(TableName);
+
+        private readonly ushort styleType;
+        private readonly byte[] panose;
+        private readonly short capHeight;
+        private readonly short familyClass;
+        private readonly short heightX;
+        private readonly string tag;
+        private readonly ushort codePageRange1;
+        private readonly ushort codePageRange2;
+        private readonly uint unicodeRange1;
+        private readonly uint unicodeRange2;
+        private readonly uint unicodeRange3;
+        private readonly uint unicodeRange4;
+        private readonly ushort breakChar;
+        private readonly ushort defaultChar;
+        private readonly ushort firstCharIndex;
+        private readonly ushort lastCharIndex;
+        private readonly ushort lowerOpticalPointSize;
+        private readonly ushort maxContext;
+        private readonly ushort upperOpticalPointSize;
+        private readonly ushort weightClass;
+        private readonly ushort widthClass;
+        private readonly short averageCharWidth;
+
+        public OS2Table(
+            short averageCharWidth,
+            ushort weightClass,
+            ushort widthClass,
+            ushort styleType,
+            short subscriptXSize,
+            short subscriptYSize,
+            short subscriptXOffset,
+            short subscriptYOffset,
+            short superscriptXSize,
+            short superscriptYSize,
+            short superscriptXOffset,
+            short superscriptYOffset,
+            short strikeoutSize,
+            short strikeoutPosition,
+            short familyClass,
+            byte[] panose,
+            uint unicodeRange1,
+            uint unicodeRange2,
+            uint unicodeRange3,
+            uint unicodeRange4,
+            string tag,
+            FontStyleSelection fontStyle,
+            ushort firstCharIndex,
+            ushort lastCharIndex,
+            short typoAscender,
+            short typoDescender,
+            short typoLineGap,
+            ushort winAscent,
+            ushort winDescent)
+        {
+            this.averageCharWidth = averageCharWidth;
+            this.weightClass = weightClass;
+            this.widthClass = widthClass;
+            this.styleType = styleType;
+            SubscriptXSize = subscriptXSize;
+            SubscriptYSize = subscriptYSize;
+            SubscriptXOffset = subscriptXOffset;
+            SubscriptYOffset = subscriptYOffset;
+            SuperscriptXSize = superscriptXSize;
+            SuperscriptYSize = superscriptYSize;
+            SuperscriptXOffset = superscriptXOffset;
+            SuperscriptYOffset = superscriptYOffset;
+            StrikeoutSize = strikeoutSize;
+            StrikeoutPosition = strikeoutPosition;
+            this.familyClass = familyClass;
+            this.panose = panose;
+            this.unicodeRange1 = unicodeRange1;
+            this.unicodeRange2 = unicodeRange2;
+            this.unicodeRange3 = unicodeRange3;
+            this.unicodeRange4 = unicodeRange4;
+            this.tag = tag;
+            FontStyle = fontStyle;
+            this.firstCharIndex = firstCharIndex;
+            this.lastCharIndex = lastCharIndex;
+            TypoAscender = typoAscender;
+            TypoDescender = typoDescender;
+            TypoLineGap = typoLineGap;
+            WinAscent = winAscent;
+            WinDescent = winDescent;
+        }
+
+        public OS2Table(
+            OS2Table version0Table,
+            ushort codePageRange1,
+            ushort codePageRange2,
+            short heightX,
+            short capHeight,
+            ushort defaultChar,
+            ushort breakChar,
+            ushort maxContext)
+            : this(
+                version0Table.averageCharWidth,
+                version0Table.weightClass,
+                version0Table.widthClass,
+                version0Table.styleType,
+                version0Table.SubscriptXSize,
+                version0Table.SubscriptYSize,
+                version0Table.SubscriptXOffset,
+                version0Table.SubscriptYOffset,
+                version0Table.SuperscriptXSize,
+                version0Table.SuperscriptYSize,
+                version0Table.SuperscriptXOffset,
+                version0Table.SuperscriptYOffset,
+                version0Table.StrikeoutSize,
+                version0Table.StrikeoutPosition,
+                version0Table.familyClass,
+                version0Table.panose,
+                version0Table.unicodeRange1,
+                version0Table.unicodeRange2,
+                version0Table.unicodeRange3,
+                version0Table.unicodeRange4,
+                version0Table.tag,
+                version0Table.FontStyle,
+                version0Table.firstCharIndex,
+                version0Table.lastCharIndex,
+                version0Table.TypoAscender,
+                version0Table.TypoDescender,
+                version0Table.TypoLineGap,
+                version0Table.WinAscent,
+                version0Table.WinDescent)
+        {
+            this.codePageRange1 = codePageRange1;
+            this.codePageRange2 = codePageRange2;
+            this.heightX = heightX;
+            this.capHeight = capHeight;
+            this.defaultChar = defaultChar;
+            this.breakChar = breakChar;
+            this.maxContext = maxContext;
+        }
+
+        public OS2Table(OS2Table versionLessThan5Table, ushort lowerOpticalPointSize, ushort upperOpticalPointSize)
+            : this(
+                versionLessThan5Table,
+                versionLessThan5Table.codePageRange1,
+                versionLessThan5Table.codePageRange2,
+                versionLessThan5Table.heightX,
+                versionLessThan5Table.capHeight,
+                versionLessThan5Table.defaultChar,
+                versionLessThan5Table.breakChar,
+                versionLessThan5Table.maxContext)
+        {
+            this.lowerOpticalPointSize = lowerOpticalPointSize;
+            this.upperOpticalPointSize = upperOpticalPointSize;
+        }
+
+        [Flags]
+        internal enum FontStyleSelection : ushort
+        {
+            /// <summary>
+            /// Font contains italic or oblique characters, otherwise they are upright.
+            /// </summary>
+            ITALIC = 1,
+
+            /// <summary>
+            /// Characters are underscored.
+            /// </summary>
+            UNDERSCORE = 1 << 1,
+
+            /// <summary>
+            /// Characters have their foreground and background reversed.
+            /// </summary>
+            NEGATIVE = 1 << 2,
+
+            /// <summary>
+            /// characters, otherwise they are solid.
+            /// </summary>
+            OUTLINED = 1 << 3,
+
+            /// <summary>
+            /// Characters are overstruck.
+            /// </summary>
+            STRIKEOUT = 1 << 4,
+
+            /// <summary>
+            /// Characters are emboldened.
+            /// </summary>
+            BOLD = 1 << 5,
+
+            /// <summary>
+            /// Characters are in the standard weight/style for the font.
+            /// </summary>
+            REGULAR = 1 << 6,
+
+            /// <summary>
+            /// If set, it is strongly recommended to use OS/2.typoAscender - OS/2.typoDescender+ OS/2.typoLineGap as a value for default line spacing for this font.
+            /// </summary>
+            USE_TYPO_METRICS = 1 << 7,
+
+            /// <summary>
+            /// The font has ‘name’ table strings consistent with a weight/width/slope family without requiring use of ‘name’ IDs 21 and 22. (Please see more detailed description below.)
+            /// </summary>
+            WWS = 1 << 8,
+
+            /// <summary>
+            /// Font contains oblique characters.
+            /// </summary>
+            OBLIQUE = 1 << 9,
+
+            // 10–15        <reserved>  Reserved; set to 0.
+        }
+
+        public FontStyleSelection FontStyle { get; }
+
+        public short TypoAscender { get; }
+
+        public short TypoDescender { get; }
+
+        public short TypoLineGap { get; }
+
+        public ushort WinAscent { get; }
+
+        public ushort WinDescent { get; }
+
+        public short StrikeoutPosition { get; }
+
+        public short StrikeoutSize { get; }
+
+        public short SubscriptXOffset { get; }
+
+        public short SubscriptXSize { get; }
+
+        public short SubscriptYOffset { get; }
+
+        public short SubscriptYSize { get; }
+
+        public short SuperscriptXOffset { get; }
+
+        public short SuperscriptXSize { get; }
+
+        public short SuperscriptYOffset { get; }
+
+        public short SuperscriptYSize { get; }
+
+        public static OS2Table? Load(IGlyphTypeface glyphTypeface)
+        {
+            if (!glyphTypeface.TryGetTable(Tag, out var table))
+            {
+                return null;
+            }
+
+            using var stream = new MemoryStream(table);
+            using var binaryReader = new BigEndianBinaryReader(stream, false);
+
+            // Move to start of table.
+            return Load(binaryReader);
+        }
+
+        public static OS2Table Load(BigEndianBinaryReader reader)
+        {
+            // Version 1.0
+            // Type   | Name                   | Comments
+            // -------|------------------------|-----------------------
+            // uint16 |version                 | 0x0005
+            // int16  |xAvgCharWidth           |
+            // uint16 |usWeightClass           |
+            // uint16 |usWidthClass            |
+            // uint16 |fsType                  |
+            // int16  |ySubscriptXSize         |
+            // int16  |ySubscriptYSize         |
+            // int16  |ySubscriptXOffset       |
+            // int16  |ySubscriptYOffset       |
+            // int16  |ySuperscriptXSize       |
+            // int16  |ySuperscriptYSize       |
+            // int16  |ySuperscriptXOffset     |
+            // int16  |ySuperscriptYOffset     |
+            // int16  |yStrikeoutSize          |
+            // int16  |yStrikeoutPosition      |
+            // int16  |sFamilyClass            |
+            // uint8  |panose[10]              |
+            // uint32 |ulUnicodeRange1         | Bits 0–31
+            // uint32 |ulUnicodeRange2         | Bits 32–63
+            // uint32 |ulUnicodeRange3         | Bits 64–95
+            // uint32 |ulUnicodeRange4         | Bits 96–127
+            // Tag    |achVendID               |
+            // uint16 |fsSelection             |
+            // uint16 |usFirstCharIndex        |
+            // uint16 |usLastCharIndex         |
+            // int16  |sTypoAscender           |
+            // int16  |sTypoDescender          |
+            // int16  |sTypoLineGap            |
+            // uint16 |usWinAscent             |
+            // uint16 |usWinDescent            |
+            // uint32 |ulCodePageRange1        | Bits 0–31
+            // uint32 |ulCodePageRange2        | Bits 32–63
+            // int16  |sxHeight                |
+            // int16  |sCapHeight              |
+            // uint16 |usDefaultChar           |
+            // uint16 |usBreakChar             |
+            // uint16 |usMaxContext            |
+            // uint16 |usLowerOpticalPointSize |
+            // uint16 |usUpperOpticalPointSize |
+            ushort version = reader.ReadUInt16(); // assert 0x0005
+            short averageCharWidth = reader.ReadInt16();
+            ushort weightClass = reader.ReadUInt16();
+            ushort widthClass = reader.ReadUInt16();
+            ushort styleType = reader.ReadUInt16();
+            short subscriptXSize = reader.ReadInt16();
+            short subscriptYSize = reader.ReadInt16();
+            short subscriptXOffset = reader.ReadInt16();
+            short subscriptYOffset = reader.ReadInt16();
+
+            short superscriptXSize = reader.ReadInt16();
+            short superscriptYSize = reader.ReadInt16();
+            short superscriptXOffset = reader.ReadInt16();
+            short superscriptYOffset = reader.ReadInt16();
+
+            short strikeoutSize = reader.ReadInt16();
+            short strikeoutPosition = reader.ReadInt16();
+            short familyClass = reader.ReadInt16();
+            byte[] panose = reader.ReadUInt8Array(10);
+            uint unicodeRange1 = reader.ReadUInt32(); // Bits 0–31
+            uint unicodeRange2 = reader.ReadUInt32(); // Bits 32–63
+            uint unicodeRange3 = reader.ReadUInt32(); // Bits 64–95
+            uint unicodeRange4 = reader.ReadUInt32(); // Bits 96–127
+            string tag = reader.ReadTag();
+            FontStyleSelection fontStyle = reader.ReadUInt16<FontStyleSelection>();
+            ushort firstCharIndex = reader.ReadUInt16();
+            ushort lastCharIndex = reader.ReadUInt16();
+            short typoAscender = reader.ReadInt16();
+            short typoDescender = reader.ReadInt16();
+            short typoLineGap = reader.ReadInt16();
+            ushort winAscent = reader.ReadUInt16();
+            ushort winDescent = reader.ReadUInt16();
+
+            var version0Table = new OS2Table(
+                    averageCharWidth,
+                    weightClass,
+                    widthClass,
+                    styleType,
+                    subscriptXSize,
+                    subscriptYSize,
+                    subscriptXOffset,
+                    subscriptYOffset,
+                    superscriptXSize,
+                    superscriptYSize,
+                    superscriptXOffset,
+                    superscriptYOffset,
+                    strikeoutSize,
+                    strikeoutPosition,
+                    familyClass,
+                    panose,
+                    unicodeRange1,
+                    unicodeRange2,
+                    unicodeRange3,
+                    unicodeRange4,
+                    tag,
+                    fontStyle,
+                    firstCharIndex,
+                    lastCharIndex,
+                    typoAscender,
+                    typoDescender,
+                    typoLineGap,
+                    winAscent,
+                    winDescent);
+
+            if (version == 0)
+            {
+                return version0Table;
+            }
+
+            short heightX = 0;
+            short capHeight = 0;
+
+            ushort defaultChar = 0;
+            ushort breakChar = 0;
+            ushort maxContext = 0;
+
+            ushort codePageRange1 = reader.ReadUInt16(); // Bits 0–31
+            ushort codePageRange2 = reader.ReadUInt16(); // Bits 32–63
+
+            // fields exist only in > v1 https://docs.microsoft.com/en-us/typography/opentype/spec/os2
+            if (version > 1)
+            {
+                heightX = reader.ReadInt16();
+                capHeight = reader.ReadInt16();
+                defaultChar = reader.ReadUInt16();
+                breakChar = reader.ReadUInt16();
+                maxContext = reader.ReadUInt16();
+            }
+
+            var versionLessThan5Table = new OS2Table(
+                    version0Table,
+                    codePageRange1,
+                    codePageRange2,
+                    heightX,
+                    capHeight,
+                    defaultChar,
+                    breakChar,
+                    maxContext);
+
+            if (version < 5)
+            {
+                return versionLessThan5Table;
+            }
+
+            ushort lowerOpticalPointSize = reader.ReadUInt16();
+            ushort upperOpticalPointSize = reader.ReadUInt16();
+
+            return new OS2Table(
+                versionLessThan5Table,
+                lowerOpticalPointSize,
+                upperOpticalPointSize);
+        }
+    }
+}

+ 37 - 0
src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs

@@ -0,0 +1,37 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    /// <summary>
+    /// platforms ids
+    /// </summary>
+    internal enum PlatformIDs : ushort
+    {
+        /// <summary>
+        /// Unicode platform
+        /// </summary>
+        Unicode = 0,
+
+        /// <summary>
+        /// Script manager code
+        /// </summary>
+        Macintosh = 1,
+
+        /// <summary>
+        /// [deprecated] ISO encoding
+        /// </summary>
+        ISO = 2,
+
+        /// <summary>
+        /// Window encoding
+        /// </summary>
+        Windows = 3,
+
+        /// <summary>
+        /// Custom platform
+        /// </summary>
+        Custom = 4 // Custom  None
+    }
+}

+ 38 - 0
src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs

@@ -0,0 +1,38 @@
+// Copyright (c) Six Labors.
+// Licensed under the Apache License, Version 2.0.
+// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
+
+using System.Diagnostics;
+using System.Text;
+
+namespace Avalonia.Media.Fonts.Tables
+{
+    [DebuggerDisplay("Offset: {Offset}, Length: {Length}, Value: {Value}")]
+    internal class StringLoader
+    {
+        public StringLoader(ushort length, ushort offset, Encoding encoding)
+        {
+            Length = length;
+            Offset = offset;
+            Encoding = encoding;
+            Value = string.Empty;
+        }
+
+        public ushort Length { get; }
+
+        public ushort Offset { get; }
+
+        public string Value { get; private set; }
+
+        public Encoding Encoding { get; }
+
+        public static StringLoader Create(BigEndianBinaryReader reader)
+            => Create(reader, Encoding.BigEndianUnicode);
+
+        public static StringLoader Create(BigEndianBinaryReader reader, Encoding encoding)
+            => new StringLoader(reader.ReadUInt16(), reader.ReadUInt16(), encoding);
+
+        public void LoadValue(BigEndianBinaryReader reader)
+            => Value = reader.ReadString(Length, Encoding).Replace("\0", string.Empty);
+    }
+}

+ 1 - 1
src/Avalonia.Base/Media/GlyphRun.cs

@@ -688,7 +688,7 @@ namespace Avalonia.Media
 
             return new GlyphRunMetrics
             {
-                Baseline = -GlyphTypeface.Metrics.Ascent * Scale,
+                Baseline = (-GlyphTypeface.Metrics.Ascent + GlyphTypeface.Metrics.LineGap) * Scale,
                 Width = width,
                 WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace,
                 Height = height,

+ 14 - 2
src/Avalonia.Base/Media/IGlyphTypeface2.cs

@@ -1,16 +1,28 @@
-using System.Diagnostics.CodeAnalysis;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using System.IO;
+using Avalonia.Media.Fonts;
 
 namespace Avalonia.Media
 {
     internal interface IGlyphTypeface2 : IGlyphTypeface
     {
-
         /// <summary>
         /// Returns the font file stream represented by the <see cref="IGlyphTypeface"/> object.
         /// </summary>
         /// <param name="stream">The stream.</param>
         /// <returns>Returns <c>true</c> if the stream can be obtained, otherwise <c>false</c>.</returns>
         bool TryGetStream([NotNullWhen(true)] out Stream? stream);
+
+        /// <summary>
+        /// Gets the localized family names.
+        /// </summary>
+        IReadOnlyDictionary<CultureInfo, string> FamilyNames { get; }
+
+        /// <summary>
+        /// Gets supported font features.
+        /// </summary>
+        IReadOnlyList<OpenTypeTag> SupportedFeatures { get; }
     }
 }

+ 4 - 4
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@@ -1287,11 +1287,11 @@ namespace Avalonia.Media.TextFormatting
                             {
                                 height = drawableTextRun.Size.Height;
                             }
+                          
+                            //Adjust current ascent so drawables and text align at the bottom edge of the line.
+                            var offset = Math.Max(0, drawableTextRun.Baseline + ascent - descent);
 
-                            if (ascent > -drawableTextRun.Baseline)
-                            {
-                                ascent = -drawableTextRun.Baseline;
-                            }
+                            ascent -= offset;
 
                             bounds = bounds.Union(new Rect(new Point(bounds.Right, 0), drawableTextRun.Size));
 

+ 1 - 1
src/Avalonia.Controls/Documents/InlineRun.cs

@@ -30,7 +30,7 @@ namespace Avalonia.Controls.Documents
                     baseline = baselineOffsetValue;
                 }
 
-                return -baseline;
+                return baseline;
             }
         }
 

+ 112 - 15
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@@ -1,49 +1,85 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
 using System.IO;
 using System.Runtime.InteropServices;
 using Avalonia.Media;
+using Avalonia.Media.Fonts;
+using Avalonia.Media.Fonts.Tables;
+using Avalonia.Media.Fonts.Tables.Name;
 using HarfBuzzSharp;
 using SkiaSharp;
 
 namespace Avalonia.Skia
 {
-    internal class GlyphTypefaceImpl : IGlyphTypeface, IGlyphTypeface2
+    internal class GlyphTypefaceImpl : IGlyphTypeface2
     {
         private bool _isDisposed;
         private readonly SKTypeface _typeface;
+        private readonly NameTable _nameTable;
+        private readonly OS2Table? _os2Table;
+        private readonly HorizontalHeadTable _hhTable;
+        private IReadOnlyList<OpenTypeTag>? _supportedFeatures;
 
         public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
         {
             _typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
 
-            Face = new Face(GetTable)
-            {
-                UnitsPerEm = typeface.UnitsPerEm
-            };
+            Face = new Face(GetTable) { UnitsPerEm = typeface.UnitsPerEm };
 
             Font = new Font(Face);
 
             Font.SetFunctionsOpenType();
 
-            Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalAscender, out var ascent);
-            Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalDescender, out var descent);
-            Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.HorizontalLineGap, out var lineGap);
-            Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughOffset);
-            Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughSize);
             Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlineOffset);
             Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineSize);
 
+            _os2Table = OS2Table.Load(this);
+            _hhTable = HorizontalHeadTable.Load(this);
+
+            int ascent;
+            int descent;
+            int lineGap;
+
+            if (_os2Table != null && (_os2Table.FontStyle & OS2Table.FontStyleSelection.USE_TYPO_METRICS) != 0)
+            {
+                ascent = -_os2Table.TypoAscender;
+                descent = -_os2Table.TypoDescender;
+                lineGap = _os2Table.TypoLineGap;
+            }
+            else
+            {
+                ascent = -_hhTable.Ascender;
+                descent = -_hhTable.Descender;
+                lineGap = _hhTable.LineGap;
+            }
+
+            if (_os2Table != null && (ascent == 0 || descent == 0))
+            {
+                if (_os2Table.TypoAscender != 0 || _os2Table.TypoDescender != 0)
+                {
+                    ascent = -_os2Table.TypoAscender;
+                    descent = -_os2Table.TypoDescender;
+                    lineGap = _os2Table.TypoLineGap;
+                }
+                else
+                {
+                    ascent = -_os2Table.WinAscent;
+                    descent = _os2Table.WinDescent;
+                }
+            }
+
             Metrics = new FontMetrics
             {
                 DesignEmHeight = (short)Face.UnitsPerEm,
-                Ascent = -ascent,
-                Descent = -descent,
+                Ascent = ascent,
+                Descent = descent,
                 LineGap = lineGap,
                 UnderlinePosition = -underlineOffset,
                 UnderlineThickness = underlineSize,
-                StrikethroughPosition = -strikethroughOffset,
-                StrikethroughThickness = strikethroughSize,
+                StrikethroughPosition = -_os2Table?.StrikeoutPosition ?? 0,
+                StrikethroughThickness = _os2Table?.StrikeoutSize ?? 0,
                 IsFixedPitch = typeface.IsFixedPitch
             };
 
@@ -58,6 +94,67 @@ namespace Avalonia.Skia
                 typeface.FontSlant.ToAvalonia();
 
             Stretch = (FontStretch)typeface.FontStyle.Width;
+
+            _nameTable = NameTable.Load(this);
+
+            FamilyName = _nameTable.FontFamilyName(CultureInfo.InvariantCulture);
+
+            var familyNames = new Dictionary<CultureInfo, string>(_nameTable.Languages.Count);
+
+            foreach (var language in _nameTable.Languages)
+            {
+                familyNames.Add(language, _nameTable.FontFamilyName(language));
+            }
+
+            FamilyNames = familyNames;
+        }
+
+        public IReadOnlyDictionary<CultureInfo, string> FamilyNames { get; }
+
+        public IReadOnlyList<OpenTypeTag> SupportedFeatures
+        {
+            get
+            {
+                if (_supportedFeatures != null)
+                {
+                    return _supportedFeatures;
+                }
+
+                var gPosFeatures = FeatureListTable.LoadGPos(this);
+                var gSubFeatures = FeatureListTable.LoadGSub(this);
+
+                var supportedFeatures = new List<OpenTypeTag>(gPosFeatures?.Features.Count ?? 0 + gSubFeatures?.Features.Count ?? 0);
+
+                if (gPosFeatures != null)
+                {
+                    foreach (var gPosFeature in gPosFeatures.Features)
+                    {
+                        if (supportedFeatures.Contains(gPosFeature))
+                        {
+                            continue;
+                        }
+
+                        supportedFeatures.Add(gPosFeature);
+                    }
+                }
+
+                if (gSubFeatures != null)
+                {
+                    foreach (var gSubFeature in gSubFeatures.Features)
+                    {
+                        if (supportedFeatures.Contains(gSubFeature))
+                        {
+                            continue;
+                        }
+
+                        supportedFeatures.Add(gSubFeature);
+                    }
+                }
+
+                _supportedFeatures = supportedFeatures;
+
+                return supportedFeatures;
+            }
         }
 
         public Face Face { get; }
@@ -72,7 +169,7 @@ namespace Avalonia.Skia
 
         public int GlyphCount { get; }
 
-        public string FamilyName => _typeface.FamilyName;
+        public string FamilyName { get; }
 
         public FontWeight Weight { get; }
 

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

@@ -16,6 +16,7 @@
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
     <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Fonts.Inter\Avalonia.Fonts.Inter.csproj" />
     <ProjectReference Include="..\..\src\Skia\Avalonia.Skia\Avalonia.Skia.csproj" />
     <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
   </ItemGroup>

+ 37 - 0
tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Linq;
+using Avalonia.Fonts.Inter;
 using Avalonia.Headless;
 using Avalonia.Media;
 using Avalonia.Media.Fonts;
@@ -298,5 +299,41 @@ namespace Avalonia.Skia.UnitTests.Media
                 }
             }
         }
+
+        [Win32Fact("Requires Windows Fonts")]
+        public void Should_Get_GlyphTypeface_By_Localized_FamilyName()
+        {
+            using (UnitTestApplication.Start(
+                       TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                using (AvaloniaLocator.EnterScope())
+                {
+                    Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("微軟正黑體"), out var glyphTypeface));
+
+                    Assert.Equal("Microsoft JhengHei",glyphTypeface.FamilyName);
+                }
+            }
+        }
+
+        [Fact]
+        public void Should_Get_FontFeatures()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl())))
+            {
+                using (AvaloniaLocator.EnterScope())
+                {
+                     FontManager.Current.AddFontCollection(new InterFontCollection());
+
+                    Assert.True(FontManager.Current.TryGetGlyphTypeface(new Typeface("fonts:Inter#Inter"),
+                        out var glyphTypeface));
+
+                    Assert.Equal("Inter", glyphTypeface.FamilyName);
+
+                    var features = ((IGlyphTypeface2)glyphTypeface).SupportedFeatures;
+
+                    Assert.NotEmpty(features);
+                }
+            }
+        }
     }
 }

+ 14 - 0
tests/Avalonia.Skia.UnitTests/Win32Fact.cs

@@ -0,0 +1,14 @@
+using System.Runtime.InteropServices;
+using Xunit;
+
+namespace Avalonia.Skia.UnitTests
+{
+    internal class Win32Fact : FactAttribute
+    {
+        public Win32Fact(string message)
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+                Skip = message;
+        }
+    }
+}