From 2dfd9be66afcbd1a571c707acf4e41e2a857e02e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 5 Jun 2024 09:08:11 +0200 Subject: [PATCH] [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 --- .../Media/Fonts/EmbeddedFontCollection.cs | 52 ++- src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs | 71 +++ .../Fonts/Tables/BigEndianBinaryReader.cs | 422 +++++++++++++++++ .../Fonts/Tables/EncodingIDExtensions.cs | 31 ++ .../Media/Fonts/Tables/EncodingIDs.cs | 47 ++ .../Media/Fonts/Tables/FeatureListTable.cs | 125 ++++++ .../Media/Fonts/Tables/HorizontalHeadTable.cs | 153 +++++++ .../Fonts/Tables/InvalidFontTableException.cs | 29 ++ .../Media/Fonts/Tables/KnownNameIds.cs | 123 +++++ .../Fonts/Tables/MissingFontTableException.cs | 29 ++ .../Media/Fonts/Tables/Name/NameRecord.cs | 45 ++ .../Media/Fonts/Tables/Name/NameTable.cs | 185 ++++++++ .../Media/Fonts/Tables/OS2Table.cs | 423 ++++++++++++++++++ .../Media/Fonts/Tables/PlatformIDs.cs | 37 ++ .../Media/Fonts/Tables/StringLoader.cs | 38 ++ src/Avalonia.Base/Media/GlyphRun.cs | 2 +- src/Avalonia.Base/Media/IGlyphTypeface2.cs | 16 +- .../Media/TextFormatting/TextLineImpl.cs | 8 +- src/Avalonia.Controls/Documents/InlineRun.cs | 2 +- src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs | 127 +++++- .../Avalonia.Skia.UnitTests.csproj | 1 + .../Media/FontManagerTests.cs | 37 ++ tests/Avalonia.Skia.UnitTests/Win32Fact.cs | 14 + 23 files changed, 1977 insertions(+), 40 deletions(-) create mode 100644 src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/EncodingIDExtensions.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/EncodingIDs.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/InvalidFontTableException.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/KnownNameIds.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/MissingFontTableException.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs create mode 100644 src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs create mode 100644 tests/Avalonia.Skia.UnitTests/Win32Fact.cs diff --git a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs index 8a958621c3d..9a2e8cb1a4c 100644 --- a/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/EmbeddedFontCollection.cs @@ -45,27 +45,11 @@ public override void Initialize(IFontManagerImpl fontManager) if (fontManager.TryCreateGlyphTypeface(stream, FontSimulations.None, out var glyphTypeface)) { - if (!_glyphTypefaceCache.TryGetValue(glyphTypeface.FamilyName, out var glyphTypefaces)) - { - glyphTypefaces = new ConcurrentDictionary(); - - 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 @@ public override bool TryGetGlyphTypeface(string familyName, FontStyle style, Fon } public override IEnumerator 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(); + }); + + typefaces.TryAdd( + new FontCollectionKey(glyphTypeface.Style, glyphTypeface.Weight, glyphTypeface.Stretch), + glyphTypeface); + } + } } } diff --git a/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs b/src/Avalonia.Base/Media/Fonts/OpenTypeTag.cs new file mode 100644 index 00000000000..b0c725ca92e --- /dev/null +++ b/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); + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs b/src/Avalonia.Base/Media/Fonts/Tables/BigEndianBinaryReader.cs new file mode 100644 index 00000000000..bca46b7e8c9 --- /dev/null +++ b/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 +{ + /// + /// BinaryReader using big-endian encoding. + /// + [DebuggerDisplay("Start: {StartOfStream}, Position: {BaseStream.Position}")] + internal class BigEndianBinaryReader : IDisposable + { + /// + /// Buffer used for temporary storage before conversion into primitives + /// + private readonly byte[] _buffer = new byte[16]; + + private readonly bool _leaveOpen; + + /// + /// Initializes a new instance of the class. + /// Constructs a new binary reader with the given bit converter, reading + /// to the given stream, using the given encoding. + /// + /// Stream to read data from + /// if set to true [leave open]. + public BigEndianBinaryReader(Stream stream, bool leaveOpen) + { + BaseStream = stream; + StartOfStream = stream.Position; + _leaveOpen = leaveOpen; + } + + private long StartOfStream { get; } + + /// + /// Gets the underlying stream of the EndianBinaryReader. + /// + public Stream BaseStream { get; } + + /// + /// Seeks within the stream. + /// + /// Offset to seek to. + /// Origin of seek operation. If SeekOrigin.Begin, the offset will be set to the start of stream position. + 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); + } + + /// + /// Reads a single byte from the stream. + /// + /// The byte read + public byte ReadByte() + { + ReadInternal(_buffer, 1); + return _buffer[0]; + } + + /// + /// Reads a single signed byte from the stream. + /// + /// The byte read + public sbyte ReadSByte() + { + ReadInternal(_buffer, 1); + return unchecked((sbyte)_buffer[0]); + } + + public float ReadF2dot14() + { + const float f2Dot14ToFloat = 16384.0f; + return ReadInt16() / f2Dot14ToFloat; + } + + /// + /// Reads a 16-bit signed integer from the stream, using the bit converter + /// for this reader. 2 bytes are read. + /// + /// The 16-bit integer read + public short ReadInt16() + { + ReadInternal(_buffer, 2); + + return BinaryPrimitives.ReadInt16BigEndian(_buffer); + } + + public TEnum ReadInt16() + 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(); + + /// + /// Reads a fixed 32-bit value from the stream. + /// 4 bytes are read. + /// + /// The 32-bit value read. + public float ReadFixed() + { + ReadInternal(_buffer, 4); + return BinaryPrimitives.ReadInt32BigEndian(_buffer) / 65536F; + } + + /// + /// Reads a 32-bit signed integer from the stream, using the bit converter + /// for this reader. 4 bytes are read. + /// + /// The 32-bit integer read + public int ReadInt32() + { + ReadInternal(_buffer, 4); + + return BinaryPrimitives.ReadInt32BigEndian(_buffer); + } + + /// + /// Reads a 64-bit signed integer from the stream. + /// 8 bytes are read. + /// + /// The 64-bit integer read. + public long ReadInt64() + { + ReadInternal(_buffer, 8); + + return BinaryPrimitives.ReadInt64BigEndian(_buffer); + } + + /// + /// Reads a 16-bit unsigned integer from the stream. + /// 2 bytes are read. + /// + /// The 16-bit unsigned integer read. + public ushort ReadUInt16() + { + ReadInternal(_buffer, 2); + + return BinaryPrimitives.ReadUInt16BigEndian(_buffer); + } + + /// + /// Reads a 16-bit unsigned integer from the stream representing an offset position. + /// 2 bytes are read. + /// + /// The 16-bit unsigned integer read. + public ushort ReadOffset16() => ReadUInt16(); + + public TEnum ReadUInt16() + where TEnum : struct, Enum + { + TryConvert(ReadUInt16(), out TEnum value); + return value; + } + + /// + /// Reads array of 16-bit unsigned integers from the stream. + /// + /// The length. + /// + /// The 16-bit unsigned integer read. + /// + public ushort[] ReadUInt16Array(int length) + { + ushort[] data = new ushort[length]; + for (int i = 0; i < length; i++) + { + data[i] = ReadUInt16(); + } + + return data; + } + + /// + /// Reads array of 16-bit unsigned integers from the stream to the buffer. + /// + /// The buffer to read to. + public void ReadUInt16Array(Span buffer) + { + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = ReadUInt16(); + } + } + + /// + /// Reads array or 32-bit unsigned integers from the stream. + /// + /// The length. + /// + /// The 32-bit unsigned integer read. + /// + 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; + } + + /// + /// Reads array of 16-bit unsigned integers from the stream. + /// + /// The length. + /// + /// The 16-bit signed integer read. + /// + public short[] ReadInt16Array(int length) + { + short[] data = new short[length]; + for (int i = 0; i < length; i++) + { + data[i] = ReadInt16(); + } + + return data; + } + + /// + /// Reads an array of 16-bit signed integers from the stream to the buffer. + /// + /// The buffer to read to. + public void ReadInt16Array(Span buffer) + { + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = ReadInt16(); + } + } + + /// + /// Reads a 8-bit unsigned integer from the stream, using the bit converter + /// for this reader. 1 bytes are read. + /// + /// The 8-bit unsigned integer read. + public byte ReadUInt8() + { + ReadInternal(_buffer, 1); + return _buffer[0]; + } + + /// + /// Reads a 24-bit unsigned integer from the stream, using the bit converter + /// for this reader. 3 bytes are read. + /// + /// The 24-bit unsigned integer read. + public int ReadUInt24() + { + byte highByte = ReadByte(); + return (highByte << 16) | ReadUInt16(); + } + + /// + /// Reads a 32-bit unsigned integer from the stream, using the bit converter + /// for this reader. 4 bytes are read. + /// + /// The 32-bit unsigned integer read. + public uint ReadUInt32() + { + ReadInternal(_buffer, 4); + + return BinaryPrimitives.ReadUInt32BigEndian(_buffer); + } + + /// + /// Reads a 32-bit unsigned integer from the stream representing an offset position. + /// 4 bytes are read. + /// + /// The 32-bit unsigned integer read. + public uint ReadOffset32() => ReadUInt32(); + + /// + /// 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. + /// + /// The number of bytes to read. + /// The bytes read. + 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; + } + + /// + /// 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. + /// + /// The bytes to read. + /// The encoding. + /// + /// The string read from the stream. + /// + public string ReadString(int bytesToRead, Encoding encoding) + { + byte[] data = new byte[bytesToRead]; + ReadInternal(data, bytesToRead); + return encoding.GetString(data, 0, data.Length); + } + + /// + /// Reads the uint32 string. + /// + /// a 4 character long UTF8 encoded string. + public string ReadTag() + { + ReadInternal(_buffer, 4); + + return Encoding.UTF8.GetString(_buffer, 0, 4); + } + + /// + /// Reads an offset consuming the given nuber of bytes. + /// + /// The offset size in bytes. + /// The 32-bit signed integer representing the offset. + /// Size is not in range. + 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(), + }; + + /// + /// Reads the given number of bytes from the stream, throwing an exception + /// if they can't all be read. + /// + /// Buffer to read into. + /// Number of bytes to read. + 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 input, out TEnum value) + where T : struct, IConvertible, IFormattable, IComparable + where TEnum : struct, Enum + { + if (Unsafe.SizeOf() == Unsafe.SizeOf()) + { + value = Unsafe.As(ref input); + return true; + } + + value = default; + return false; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDExtensions.cs b/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDExtensions.cs new file mode 100644 index 00000000000..6c054648cd4 --- /dev/null +++ b/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 +{ + /// + /// Converts encoding ID to TextEncoding + /// + internal static class EncodingIDExtensions + { + /// + /// Converts encoding ID to TextEncoding + /// + /// The identifier. + /// the encoding for this encoding ID + public static Encoding AsEncoding(this EncodingIDs id) + { + switch (id) + { + case EncodingIDs.Unicode11: + case EncodingIDs.Unicode2: + return Encoding.BigEndianUnicode; + default: + return Encoding.UTF8; + } + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDs.cs b/src/Avalonia.Base/Media/Fonts/Tables/EncodingIDs.cs new file mode 100644 index 00000000000..78ab91cc775 --- /dev/null +++ b/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 +{ + /// + /// Encoding IDS + /// + internal enum EncodingIDs : ushort + { + /// + /// Unicode 1.0 semantics + /// + Unicode1 = 0, + + /// + /// Unicode 1.1 semantics + /// + Unicode11 = 1, + + /// + /// ISO/IEC 10646 semantics + /// + ISO10646 = 2, + + /// + /// Unicode 2.0 and onwards semantics, Unicode BMP only (cmap subtable formats 0, 4, 6). + /// + Unicode2 = 3, + + /// + /// Unicode 2.0 and onwards semantics, Unicode full repertoire (cmap subtable formats 0, 4, 6, 10, 12). + /// + Unicode2Plus = 4, + + /// + /// Unicode Variation Sequences (cmap subtable format 14). + /// + UnicodeVariationSequences = 5, + + /// + /// Unicode full repertoire (cmap subtable formats 0, 4, 6, 10, 12, 13) + /// + UnicodeFull = 6, + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/FeatureListTable.cs new file mode 100644 index 00000000000..0a916c7ed06 --- /dev/null +++ b/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 +{ + /// + /// 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. + /// + /// + /// + internal class FeatureListTable + { + private static OpenTypeTag GSubTag = OpenTypeTag.Parse("GSUB"); + private static OpenTypeTag GPosTag = OpenTypeTag.Parse("GPOS"); + + private FeatureListTable(IReadOnlyList features) + { + Features = features; + } + + public IReadOnlyList 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(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*/); + } + + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/HorizontalHeadTable.cs new file mode 100644 index 00000000000..10b831dd1e1 --- /dev/null +++ b/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); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/InvalidFontTableException.cs b/src/Avalonia.Base/Media/Fonts/Tables/InvalidFontTableException.cs new file mode 100644 index 00000000000..d8be26a848c --- /dev/null +++ b/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 +{ + /// + /// Exception font loading can throw if it encounters invalid data during font loading. + /// + /// + public class InvalidFontTableException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The table. + public InvalidFontTableException(string message, string table) + : base(message) + => Table = table; + + /// + /// Gets the table where the error originated. + /// + public string Table { get; } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/KnownNameIds.cs b/src/Avalonia.Base/Media/Fonts/Tables/KnownNameIds.cs new file mode 100644 index 00000000000..82e69266000 --- /dev/null +++ b/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 +{ + /// + /// Provides enumeration of common name ids + /// + /// + public enum KnownNameIds : ushort + { + /// + /// The copyright notice + /// + CopyrightNotice = 0, + + /// + /// 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). + /// + FontFamilyName = 1, + + /// + /// 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. + /// + FontSubfamilyName = 2, + + /// + /// The unique font identifier + /// + UniqueFontID = 3, + + /// + /// 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. + /// + FullFontName = 4, + + /// + /// Version string. Should begin with the syntax 'Version <number>.<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. + /// + Version = 5, + + /// + /// 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 '[', ']', '(', ')', '{', '}', '<', '>', '/', '%'. + /// 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. + /// + PostscriptName = 6, + + /// + /// 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. + /// + Trademark = 7, + + /// + /// The manufacturer + /// + Manufacturer = 8, + + /// + /// Designer; name of the designer of the typeface. + /// + Designer = 9, + + /// + /// Description; description of the typeface. Can contain revision information, usage recommendations, history, features, etc. + /// + Description = 10, + + /// + /// 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. + /// + VendorUrl = 11, + + /// + /// URL Designer; URL of typeface designer (with protocol, e.g., http://, ftp://). + /// + DesignerUrl = 12, + + /// + /// 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. + /// + LicenseDescription = 13, + + /// + /// License Info URL; URL where additional licensing information can be found. + /// + LicenseInfoUrl = 14, + + /// + /// 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".) + /// + TypographicFamilyName = 16, + + /// + /// 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".) + /// + TypographicSubfamilyName = 17, + + /// + /// 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. + /// + SampleText = 19, + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/MissingFontTableException.cs b/src/Avalonia.Base/Media/Fonts/Tables/MissingFontTableException.cs new file mode 100644 index 00000000000..890414fc594 --- /dev/null +++ b/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 +{ + /// + /// Exception font loading can throw if it finds a required table is missing during font loading. + /// + /// + public class MissingFontTableException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The table. + public MissingFontTableException(string message, string table) + : base(message) + => Table = table; + + /// + /// Gets the table where the error originated. + /// + public string Table { get; } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameRecord.cs new file mode 100644 index 00000000000..7a7ad71995d --- /dev/null +++ b/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(); + var encodingId = reader.ReadUInt16(); + var encoding = encodingId.AsEncoding(); + var languageID = reader.ReadUInt16(); + var nameID = reader.ReadUInt16(); + + var stringReader = StringLoader.Create(reader, encoding); + + return new NameRecord(platform, languageID, nameID, string.Empty) + { + StringReader = stringReader + }; + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Name/NameTable.cs new file mode 100644 index 00000000000..f94482222cb --- /dev/null +++ b/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 languages) + { + _names = names; + Languages = languages; + } + + public IReadOnlyList Languages { get; } + + /// + /// Gets the name of the font. + /// + /// + /// The name of the font. + /// + public string Id(CultureInfo culture) + => GetNameById(culture, KnownNameIds.UniqueFontID); + + /// + /// Gets the name of the font. + /// + /// + /// The name of the font. + /// + public string FontName(CultureInfo culture) + => GetNameById(culture, KnownNameIds.FullFontName); + + /// + /// Gets the name of the font. + /// + /// + /// The name of the font. + /// + public string FontFamilyName(CultureInfo culture) + => GetNameById(culture, KnownNameIds.FontFamilyName); + + /// + /// Gets the name of the font. + /// + /// + /// The name of the font. + /// + 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(); + 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(); + + //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(); + + 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); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs b/src/Avalonia.Base/Media/Fonts/Tables/OS2Table.cs new file mode 100644 index 00000000000..73d80edd7d9 --- /dev/null +++ b/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 + { + /// + /// Font contains italic or oblique characters, otherwise they are upright. + /// + ITALIC = 1, + + /// + /// Characters are underscored. + /// + UNDERSCORE = 1 << 1, + + /// + /// Characters have their foreground and background reversed. + /// + NEGATIVE = 1 << 2, + + /// + /// characters, otherwise they are solid. + /// + OUTLINED = 1 << 3, + + /// + /// Characters are overstruck. + /// + STRIKEOUT = 1 << 4, + + /// + /// Characters are emboldened. + /// + BOLD = 1 << 5, + + /// + /// Characters are in the standard weight/style for the font. + /// + REGULAR = 1 << 6, + + /// + /// 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. + /// + USE_TYPO_METRICS = 1 << 7, + + /// + /// 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.) + /// + WWS = 1 << 8, + + /// + /// Font contains oblique characters. + /// + OBLIQUE = 1 << 9, + + // 10–15 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(); + 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); + } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs b/src/Avalonia.Base/Media/Fonts/Tables/PlatformIDs.cs new file mode 100644 index 00000000000..c57c4e2726a --- /dev/null +++ b/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 +{ + /// + /// platforms ids + /// + internal enum PlatformIDs : ushort + { + /// + /// Unicode platform + /// + Unicode = 0, + + /// + /// Script manager code + /// + Macintosh = 1, + + /// + /// [deprecated] ISO encoding + /// + ISO = 2, + + /// + /// Window encoding + /// + Windows = 3, + + /// + /// Custom platform + /// + Custom = 4 // Custom None + } +} diff --git a/src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs b/src/Avalonia.Base/Media/Fonts/Tables/StringLoader.cs new file mode 100644 index 00000000000..a42c87b5bd4 --- /dev/null +++ b/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); + } +} diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index b881b358aab..350f3ec0286 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -688,7 +688,7 @@ private GlyphRunMetrics CreateGlyphRunMetrics() return new GlyphRunMetrics { - Baseline = -GlyphTypeface.Metrics.Ascent * Scale, + Baseline = (-GlyphTypeface.Metrics.Ascent + GlyphTypeface.Metrics.LineGap) * Scale, Width = width, WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace, Height = height, diff --git a/src/Avalonia.Base/Media/IGlyphTypeface2.cs b/src/Avalonia.Base/Media/IGlyphTypeface2.cs index d0152ccb3ca..d2e0e4d2dd6 100644 --- a/src/Avalonia.Base/Media/IGlyphTypeface2.cs +++ b/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 { - /// /// Returns the font file stream represented by the object. /// /// The stream. /// Returns true if the stream can be obtained, otherwise false. bool TryGetStream([NotNullWhen(true)] out Stream? stream); + + /// + /// Gets the localized family names. + /// + IReadOnlyDictionary FamilyNames { get; } + + /// + /// Gets supported font features. + /// + IReadOnlyList SupportedFeatures { get; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index f60440e6b1e..954a9b2debf 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1287,11 +1287,11 @@ private TextLineMetrics CreateLineMetrics() { 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)); diff --git a/src/Avalonia.Controls/Documents/InlineRun.cs b/src/Avalonia.Controls/Documents/InlineRun.cs index 1090b1334a2..e808823b906 100644 --- a/src/Avalonia.Controls/Documents/InlineRun.cs +++ b/src/Avalonia.Controls/Documents/InlineRun.cs @@ -30,7 +30,7 @@ public override double Baseline baseline = baselineOffsetValue; } - return -baseline; + return baseline; } } diff --git a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs b/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs index f734bfa3a63..f5631435072 100644 --- a/src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs +++ b/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? _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 @@ public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations) typeface.FontSlant.ToAvalonia(); Stretch = (FontStretch)typeface.FontStyle.Width; + + _nameTable = NameTable.Load(this); + + FamilyName = _nameTable.FontFamilyName(CultureInfo.InvariantCulture); + + var familyNames = new Dictionary(_nameTable.Languages.Count); + + foreach (var language in _nameTable.Languages) + { + familyNames.Add(language, _nameTable.FontFamilyName(language)); + } + + FamilyNames = familyNames; + } + + public IReadOnlyDictionary FamilyNames { get; } + + public IReadOnlyList SupportedFeatures + { + get + { + if (_supportedFeatures != null) + { + return _supportedFeatures; + } + + var gPosFeatures = FeatureListTable.LoadGPos(this); + var gSubFeatures = FeatureListTable.LoadGSub(this); + + var supportedFeatures = new List(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 @@ public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations) public int GlyphCount { get; } - public string FamilyName => _typeface.FamilyName; + public string FamilyName { get; } public FontWeight Weight { get; } diff --git a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj index a80948005a1..c40454dc629 100644 --- a/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj +++ b/tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj @@ -16,6 +16,7 @@ + diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index 85bec100beb..e5ce99f0796 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/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 @@ public void Should_Create_Synthetic_Typeface() } } } + + [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); + } + } + } } } diff --git a/tests/Avalonia.Skia.UnitTests/Win32Fact.cs b/tests/Avalonia.Skia.UnitTests/Win32Fact.cs new file mode 100644 index 00000000000..29f8a3cb742 --- /dev/null +++ b/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; + } + } +}