Browse Source
* 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 calculationpull/15929/head
committed by
GitHub
23 changed files with 1977 additions and 40 deletions
@ -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); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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, |
|||
} |
|||
} |
|||
@ -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*/); |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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 <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.
|
|||
/// </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 '[', ']', '(', ')', '{', '}', '<', '>', '/', '%'.
|
|||
/// 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, |
|||
} |
|||
} |
|||
@ -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; } |
|||
} |
|||
} |
|||
@ -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 |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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
|
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Apache License, Version 2.0.
|
|||
// Ported from: https://github.com/SixLabors/Fonts/blob/034a440aece357341fcc6b02db58ffbe153e54ef/src/SixLabors.Fonts
|
|||
|
|||
using System.Diagnostics; |
|||
using System.Text; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables |
|||
{ |
|||
[DebuggerDisplay("Offset: {Offset}, Length: {Length}, Value: {Value}")] |
|||
internal class StringLoader |
|||
{ |
|||
public StringLoader(ushort length, ushort offset, Encoding encoding) |
|||
{ |
|||
Length = length; |
|||
Offset = offset; |
|||
Encoding = encoding; |
|||
Value = string.Empty; |
|||
} |
|||
|
|||
public ushort Length { get; } |
|||
|
|||
public ushort Offset { get; } |
|||
|
|||
public string Value { get; private set; } |
|||
|
|||
public Encoding Encoding { get; } |
|||
|
|||
public static StringLoader Create(BigEndianBinaryReader reader) |
|||
=> Create(reader, Encoding.BigEndianUnicode); |
|||
|
|||
public static StringLoader Create(BigEndianBinaryReader reader, Encoding encoding) |
|||
=> new StringLoader(reader.ReadUInt16(), reader.ReadUInt16(), encoding); |
|||
|
|||
public void LoadValue(BigEndianBinaryReader reader) |
|||
=> Value = reader.ReadString(Length, Encoding).Replace("\0", string.Empty); |
|||
} |
|||
} |
|||
@ -1,16 +1,28 @@ |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Globalization; |
|||
using System.IO; |
|||
using Avalonia.Media.Fonts; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
internal interface IGlyphTypeface2 : IGlyphTypeface |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Returns the font file stream represented by the <see cref="IGlyphTypeface"/> object.
|
|||
/// </summary>
|
|||
/// <param name="stream">The stream.</param>
|
|||
/// <returns>Returns <c>true</c> if the stream can be obtained, otherwise <c>false</c>.</returns>
|
|||
bool TryGetStream([NotNullWhen(true)] out Stream? stream); |
|||
|
|||
/// <summary>
|
|||
/// Gets the localized family names.
|
|||
/// </summary>
|
|||
IReadOnlyDictionary<CultureInfo, string> FamilyNames { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets supported font features.
|
|||
/// </summary>
|
|||
IReadOnlyList<OpenTypeTag> SupportedFeatures { get; } |
|||
} |
|||
} |
|||
|
|||
@ -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…
Reference in new issue