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
pull/15929/head
Benedikt Stebner 2 years ago
committed by GitHub
parent
commit
2dfd9be66a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 52
      src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs
  2. 71
      src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs
  3. 422
      src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs
  4. 31
      src/Avalonia.Base/Media/Fonts/Tables/EncodingIDExtensions.cs
  5. 47
      src/Avalonia.Base/Media/Fonts/Tables/EncodingIDs.cs
  6. 125
      src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs
  7. 153
      src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs
  8. 29
      src/Avalonia.Base/Media/Fonts/Tables/InvalidFontTableException.cs
  9. 123
      src/Avalonia.Base/Media/Fonts/Tables/KnownNameIds.cs
  10. 29
      src/Avalonia.Base/Media/Fonts/Tables/MissingFontTableException.cs
  11. 45
      src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs
  12. 185
      src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs
  13. 423
      src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs
  14. 37
      src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs
  15. 38
      src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs
  16. 2
      src/Avalonia.Base/Media/GlyphRun.cs
  17. 16
      src/Avalonia.Base/Media/IGlyphTypeface2.cs
  18. 8
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  19. 2
      src/Avalonia.Controls/Documents/InlineRun.cs
  20. 127
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  21. 1
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  22. 37
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs
  23. 14
      tests/Avalonia.Skia.UnitTests/Win32Fact.cs

52
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 (fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface))
{ {
if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) AddGlyphTypeface(glyphTypeface);
{
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);
} }
} }
} }
public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight, public override bool TryGetGlyphTypeface(string familyName, FontStyle style, FontWeight weight,
FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface) FontStretch stretch, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{ {
@ -142,5 +126,39 @@ namespace Avalonia.Media.Fonts
} }
public override IEnumerator<FontFamily> GetEnumerator() => _fontFamilies.GetEnumerator(); 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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);
}
}

2
src/Avalonia.Base/Media/GlyphRun.cs

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

16
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 System.IO;
using Avalonia.Media.Fonts;
namespace Avalonia.Media namespace Avalonia.Media
{ {
internal interface IGlyphTypeface2 : IGlyphTypeface internal interface IGlyphTypeface2 : IGlyphTypeface
{ {
/// <summary> /// <summary>
/// Returns the font file stream represented by the <see cref="IGlyphTypeface"/> object. /// Returns the font file stream represented by the <see cref="IGlyphTypeface"/> object.
/// </summary> /// </summary>
/// <param name="stream">The stream.</param> /// <param name="stream">The stream.</param>
/// <returns>Returns <c>true</c> if the stream can be obtained, otherwise <c>false</c>.</returns> /// <returns>Returns <c>true</c> if the stream can be obtained, otherwise <c>false</c>.</returns>
bool TryGetStream([NotNullWhen(true)] out Stream? stream); 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; }
} }
} }

8
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

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

2
src/Avalonia.Controls/Documents/InlineRun.cs

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

127
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@ -1,49 +1,85 @@
using System; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts;
using Avalonia.Media.Fonts.Tables;
using Avalonia.Media.Fonts.Tables.Name;
using HarfBuzzSharp; using HarfBuzzSharp;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
{ {
internal class GlyphTypefaceImpl : IGlyphTypeface, IGlyphTypeface2 internal class GlyphTypefaceImpl : IGlyphTypeface2
{ {
private bool _isDisposed; private bool _isDisposed;
private readonly SKTypeface _typeface; 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) public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations)
{ {
_typeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); _typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
Face = new Face(GetTable) Face = new Face(GetTable) { UnitsPerEm = typeface.UnitsPerEm };
{
UnitsPerEm = typeface.UnitsPerEm
};
Font = new Font(Face); Font = new Font(Face);
Font.SetFunctionsOpenType(); 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.UnderlineOffset, out var underlineOffset);
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineSize); 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 Metrics = new FontMetrics
{ {
DesignEmHeight = (short)Face.UnitsPerEm, DesignEmHeight = (short)Face.UnitsPerEm,
Ascent = -ascent, Ascent = ascent,
Descent = -descent, Descent = descent,
LineGap = lineGap, LineGap = lineGap,
UnderlinePosition = -underlineOffset, UnderlinePosition = -underlineOffset,
UnderlineThickness = underlineSize, UnderlineThickness = underlineSize,
StrikethroughPosition = -strikethroughOffset, StrikethroughPosition = -_os2Table?.StrikeoutPosition ?? 0,
StrikethroughThickness = strikethroughSize, StrikethroughThickness = _os2Table?.StrikeoutSize ?? 0,
IsFixedPitch = typeface.IsFixedPitch IsFixedPitch = typeface.IsFixedPitch
}; };
@ -58,6 +94,67 @@ namespace Avalonia.Skia
typeface.FontSlant.ToAvalonia(); typeface.FontSlant.ToAvalonia();
Stretch = (FontStretch)typeface.FontStyle.Width; 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; } public Face Face { get; }
@ -72,7 +169,7 @@ namespace Avalonia.Skia
public int GlyphCount { get; } public int GlyphCount { get; }
public string FamilyName => _typeface.FamilyName; public string FamilyName { get; }
public FontWeight Weight { get; } public FontWeight Weight { get; }

1
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

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

37
tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Linq; using System.Linq;
using Avalonia.Fonts.Inter;
using Avalonia.Headless; using Avalonia.Headless;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Fonts; 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
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;
}
}
}
Loading…
Cancel
Save