108 changed files with 4152 additions and 2074 deletions
@ -0,0 +1,34 @@ |
|||
namespace Avalonia.Media.Fonts.Tables.Cmap |
|||
{ |
|||
// Encoding IDs. The meaning depends on the platform; common values are listed here.
|
|||
internal enum CmapEncoding : ushort |
|||
{ |
|||
// Unicode platform encodings
|
|||
Unicode_1_0 = 0, |
|||
Unicode_1_1 = 1, |
|||
Unicode_ISO_10646 = 2, |
|||
Unicode_2_0_BMP = 3, |
|||
Unicode_2_0_full = 4, |
|||
|
|||
// Macintosh encodings (selected)
|
|||
Macintosh_Roman = 0, |
|||
Macintosh_Japanese = 1, |
|||
Macintosh_ChineseTraditional = 2, |
|||
Macintosh_Korean = 3, |
|||
Macintosh_Arabic = 4, |
|||
Macintosh_Hebrew = 5, |
|||
Macintosh_Greek = 6, |
|||
Macintosh_Russian = 7, |
|||
Macintosh_RSymbol = 8, |
|||
|
|||
// Microsoft encodings
|
|||
Microsoft_Symbol = 0, |
|||
Microsoft_UnicodeBMP = 1, // UCS-2 / UTF-16 (BMP)
|
|||
Microsoft_ShiftJIS = 2, |
|||
Microsoft_PRChina = 3, |
|||
Microsoft_Big5 = 4, |
|||
Microsoft_Wansung = 5, |
|||
Microsoft_Johab = 6, |
|||
Microsoft_UCS4 = 10 // UTF-32 (format 12)
|
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
namespace Avalonia.Media.Fonts.Tables.Cmap |
|||
{ |
|||
// cmap format types
|
|||
internal enum CmapFormat : ushort |
|||
{ |
|||
Format0 = 0, // Byte encoding table
|
|||
Format2 = 2, // High-byte mapping through table (multi-byte charsets)
|
|||
Format4 = 4, // Segment mapping to delta values (most common)
|
|||
Format6 = 6, // Trimmed table mapping
|
|||
Format8 = 8, // Mixed 16/32-bit coverage
|
|||
Format10 = 10, // Trimmed array mapping (32-bit)
|
|||
Format12 = 12, // Segmented coverage (32-bit)
|
|||
Format13 = 13, // Many-to-one mappings
|
|||
Format14 = 14, // Unicode Variation Sequences
|
|||
} |
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Cmap |
|||
{ |
|||
internal sealed class CmapFormat12Table : IReadOnlyDictionary<int, ushort> |
|||
{ |
|||
private readonly ReadOnlyMemory<byte> _table; |
|||
private readonly int _groupCount; |
|||
private readonly ReadOnlyMemory<byte> _groups; |
|||
|
|||
private int? _count; |
|||
|
|||
public CmapFormat12Table(ReadOnlyMemory<byte> table) |
|||
{ |
|||
var reader = new BigEndianBinaryReader(table.Span); |
|||
|
|||
ushort format = reader.ReadUInt16(); |
|||
Debug.Assert(format == 12, "Format must be 12."); |
|||
|
|||
ushort reserved = reader.ReadUInt16(); |
|||
Debug.Assert(reserved == 0, "Reserved field must be 0."); |
|||
|
|||
uint length = reader.ReadUInt32(); |
|||
|
|||
_table = table.Slice(0, (int)length); |
|||
|
|||
uint language = reader.ReadUInt32(); |
|||
|
|||
_groupCount = (int)reader.ReadUInt32(); |
|||
|
|||
int groupsOffset = reader.Position; |
|||
int groupsLength = _groupCount * 12; |
|||
|
|||
Debug.Assert(length >= groupsOffset + groupsLength, "Length must cover all groups."); |
|||
|
|||
_groups = _table.Slice(groupsOffset, groupsLength); |
|||
} |
|||
|
|||
private static uint ReadUInt32BE(ReadOnlyMemory<byte> mem, int groupIndex, int fieldOffset) |
|||
{ |
|||
var span = mem.Span; |
|||
int byteIndex = groupIndex * 12 + fieldOffset; |
|||
return BinaryPrimitives.ReadUInt32BigEndian(span.Slice(byteIndex, 4)); |
|||
} |
|||
|
|||
// Binary search to find the group containing the code point
|
|||
private int FindGroupIndex(int codePoint) |
|||
{ |
|||
int lo = 0; |
|||
int hi = _groupCount - 1; |
|||
|
|||
while (lo <= hi) |
|||
{ |
|||
int mid = (lo + hi) >> 1; |
|||
uint start = ReadUInt32BE(_groups, mid, 0); |
|||
uint end = ReadUInt32BE(_groups, mid, 4); |
|||
|
|||
if (codePoint < start) |
|||
{ |
|||
hi = mid - 1; |
|||
} |
|||
else if (codePoint > end) |
|||
{ |
|||
lo = mid + 1; |
|||
} |
|||
else |
|||
{ |
|||
return mid; |
|||
} |
|||
} |
|||
|
|||
// Not found
|
|||
return -1; |
|||
} |
|||
|
|||
public ushort this[int codePoint] |
|||
{ |
|||
get |
|||
{ |
|||
int groupIndex = FindGroupIndex(codePoint); |
|||
|
|||
if (groupIndex < 0) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
uint start = ReadUInt32BE(_groups, groupIndex, 0); |
|||
uint startGlyph = ReadUInt32BE(_groups, groupIndex, 8); |
|||
|
|||
// Calculate glyph index
|
|||
return (ushort)(startGlyph + (codePoint - start)); |
|||
} |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
if (_count.HasValue) |
|||
{ |
|||
return _count.Value; |
|||
} |
|||
|
|||
long total = 0; |
|||
|
|||
for (int g = 0; g < _groupCount; g++) |
|||
{ |
|||
uint start = ReadUInt32BE(_groups, g, 0); |
|||
uint end = ReadUInt32BE(_groups, g, 4); |
|||
total += (end - start + 1); |
|||
} |
|||
|
|||
_count = (int)total; |
|||
|
|||
return _count.Value; |
|||
} |
|||
} |
|||
|
|||
public IEnumerable<int> Keys |
|||
{ |
|||
get |
|||
{ |
|||
for (int g = 0; g < _groupCount; g++) |
|||
{ |
|||
uint start = ReadUInt32BE(_groups, g, 0); |
|||
uint end = ReadUInt32BE(_groups, g, 4); |
|||
|
|||
for (uint cp = start; cp <= end; cp++) |
|||
{ |
|||
yield return (int)cp; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IEnumerable<ushort> Values |
|||
{ |
|||
get |
|||
{ |
|||
for (int g = 0; g < _groupCount; g++) |
|||
{ |
|||
uint start = ReadUInt32BE(_groups, g, 0); |
|||
uint end = ReadUInt32BE(_groups, g, 4); |
|||
uint startGlyph = ReadUInt32BE(_groups, g, 8); |
|||
|
|||
for (uint cp = start; cp <= end; cp++) |
|||
{ |
|||
yield return (ushort)(startGlyph + (cp - start)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool ContainsKey(int key) => this[key] != 0; |
|||
|
|||
public bool TryGetValue(int key, out ushort value) |
|||
{ |
|||
value = this[key]; |
|||
return value != 0; |
|||
} |
|||
|
|||
public IEnumerator<KeyValuePair<int, ushort>> GetEnumerator() |
|||
{ |
|||
for (int g = 0; g < _groupCount; g++) |
|||
{ |
|||
uint start = ReadUInt32BE(_groups, g, 0); |
|||
uint end = ReadUInt32BE(_groups, g, 4); |
|||
uint startGlyph = ReadUInt32BE(_groups, g, 8); |
|||
|
|||
for (uint cp = start; cp <= end; cp++) |
|||
{ |
|||
yield return new KeyValuePair<int, ushort>((int)cp, (ushort)(startGlyph + (cp - start))); |
|||
} |
|||
} |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
} |
|||
} |
|||
@ -0,0 +1,316 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Cmap |
|||
{ |
|||
internal sealed class CmapFormat4Table : IReadOnlyDictionary<int, ushort> |
|||
{ |
|||
private readonly ReadOnlyMemory<byte> _table; |
|||
|
|||
private readonly int _segCount; |
|||
private readonly ReadOnlyMemory<byte> _endCodes; |
|||
private readonly ReadOnlyMemory<byte> _startCodes; |
|||
private readonly ReadOnlyMemory<byte> _idDeltas; |
|||
private readonly ReadOnlyMemory<byte> _idRangeOffsets; |
|||
private readonly ReadOnlyMemory<byte> _glyphIdArray; |
|||
|
|||
private int? _count; |
|||
|
|||
public CmapFormat4Table(ReadOnlyMemory<byte> table) |
|||
{ |
|||
var reader = new BigEndianBinaryReader(table.Span); |
|||
|
|||
ushort format = reader.ReadUInt16(); // must be 4
|
|||
|
|||
Debug.Assert(format == 4, "Format must be 4."); |
|||
|
|||
ushort length = reader.ReadUInt16(); // length in bytes of this subtable
|
|||
|
|||
_table = table.Slice(0, length); |
|||
|
|||
ushort language = reader.ReadUInt16(); // language code, 0 for non-language-specific
|
|||
|
|||
ushort segCountX2 = reader.ReadUInt16(); // 2 * segCount
|
|||
_segCount = segCountX2 / 2; |
|||
|
|||
ushort searchRange = reader.ReadUInt16(); // searchRange = 2 * (2^floor(log2(segCount)))
|
|||
ushort entrySelector = reader.ReadUInt16(); // entrySelector = log2(searchRange/2)
|
|||
ushort rangeShift = reader.ReadUInt16(); // rangeShift = segCountX2 - searchRange
|
|||
|
|||
// Spec sanity checks
|
|||
Debug.Assert(searchRange == (ushort)(2 * (1 << (int)Math.Floor(Math.Log(_segCount, 2)))), |
|||
"searchRange must equal 2 * (2^floor(log2(segCount)))."); |
|||
Debug.Assert(entrySelector == (ushort)Math.Floor(Math.Log(_segCount, 2)), |
|||
"entrySelector must equal log2(searchRange/2)."); |
|||
Debug.Assert(rangeShift == (ushort)(segCountX2 - searchRange), |
|||
"rangeShift must equal segCountX2 - searchRange."); |
|||
|
|||
// Compute offsets
|
|||
int endCodeOffset = reader.Position; |
|||
int startCodeOffset = endCodeOffset + _segCount * 2 + 2; // + reservedPad
|
|||
int idDeltaOffset = startCodeOffset + _segCount * 2; // after startCodes
|
|||
int idRangeOffsetOffset = idDeltaOffset + _segCount * 2; // after idDeltas
|
|||
int glyphIdArrayOffset = idRangeOffsetOffset + _segCount * 2; // after idRangeOffsets
|
|||
|
|||
// Ensure declared length is consistent
|
|||
Debug.Assert(length >= glyphIdArrayOffset, |
|||
"Subtable length must be at least large enough to contain glyphIdArray."); |
|||
|
|||
// Slice directly
|
|||
_endCodes = _table.Slice(endCodeOffset, _segCount * 2); |
|||
|
|||
_startCodes = _table.Slice(startCodeOffset, _segCount * 2); |
|||
|
|||
_idDeltas = _table.Slice(idDeltaOffset, _segCount * 2); |
|||
|
|||
_idRangeOffsets = _table.Slice(idRangeOffsetOffset, _segCount * 2); |
|||
|
|||
int glyphCount = (length - glyphIdArrayOffset) / 2; |
|||
|
|||
Debug.Assert(glyphCount >= 0, "GlyphIdArray length must not be negative."); |
|||
|
|||
_glyphIdArray = _table.Slice(glyphIdArrayOffset, glyphCount * 2); |
|||
} |
|||
|
|||
// Reads a big-endian UInt16 from the specified word index in the given memory
|
|||
private static ushort ReadUInt16BE(ReadOnlyMemory<byte> mem, int wordIndex) |
|||
{ |
|||
var span = mem.Span; |
|||
int byteIndex = wordIndex * 2; |
|||
|
|||
// Ensure we don't go out of bounds
|
|||
return BinaryPrimitives.ReadUInt16BigEndian(span.Slice(byteIndex, 2)); |
|||
} |
|||
|
|||
public int Count |
|||
{ |
|||
get |
|||
{ |
|||
if (_count.HasValue) |
|||
{ |
|||
return _count.Value; |
|||
} |
|||
|
|||
int count = 0; |
|||
|
|||
for (int seg = 0; seg < _segCount; seg++) |
|||
{ |
|||
// Get start and end of segment
|
|||
int start = ReadUInt16BE(_startCodes, seg); |
|||
int end = ReadUInt16BE(_endCodes, seg); |
|||
|
|||
for (int cp = start; cp <= end; cp++) |
|||
{ |
|||
// Only count if maps to non-zero glyph
|
|||
if (this[cp] != 0) |
|||
{ |
|||
count++; |
|||
} |
|||
} |
|||
} |
|||
|
|||
_count = count; |
|||
|
|||
return count; |
|||
} |
|||
} |
|||
|
|||
public ushort this[int codePoint] |
|||
{ |
|||
get |
|||
{ |
|||
// Find the segment containing the codePoint
|
|||
int segmentIndex = FindSegmentIndex(codePoint); |
|||
|
|||
if (segmentIndex < 0) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets, segmentIndex); |
|||
ushort idDelta = ReadUInt16BE(_idDeltas, segmentIndex); |
|||
|
|||
// If idRangeOffset is 0, glyphId = (codePoint + idDelta) % 65536
|
|||
if (idRangeOffset == 0) |
|||
{ |
|||
return (ushort)((codePoint + idDelta) & 0xFFFF); |
|||
} |
|||
else |
|||
{ |
|||
int start = ReadUInt16BE(_startCodes, segmentIndex); |
|||
int ro = idRangeOffset / 2; // words
|
|||
// The index into the glyphIdArray
|
|||
int idx = (codePoint - start) + ro - (_segCount - segmentIndex); |
|||
|
|||
// Ensure index is within bounds of glyphIdArray
|
|||
int glyphArrayWords = _glyphIdArray.Length / 2; |
|||
|
|||
if ((uint)idx < (uint)glyphArrayWords) |
|||
{ |
|||
ushort glyphId = ReadUInt16BE(_glyphIdArray, idx); |
|||
|
|||
// If glyphId is not 0, apply idDelta
|
|||
if (glyphId != 0) |
|||
{ |
|||
glyphId = (ushort)((glyphId + idDelta) & 0xFFFF); |
|||
} |
|||
|
|||
return glyphId; |
|||
} |
|||
} |
|||
|
|||
// Not found or maps to missing glyph
|
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
public bool ContainsKey(int key) => this[key] != 0; |
|||
|
|||
public bool TryGetValue(int key, out ushort value) |
|||
{ |
|||
value = this[key]; |
|||
|
|||
return value != 0; |
|||
} |
|||
|
|||
public IEnumerable<int> Keys |
|||
{ |
|||
get |
|||
{ |
|||
for (int seg = 0; seg < _segCount; seg++) |
|||
{ |
|||
int start = ReadUInt16BE(_startCodes, seg); |
|||
int end = ReadUInt16BE(_endCodes, seg); |
|||
|
|||
for (int cp = start; cp <= end; cp++) |
|||
{ |
|||
ushort gid = ResolveGlyph(seg, cp); |
|||
|
|||
// Only yield code points that map to non-zero glyphs
|
|||
if (gid != 0) |
|||
{ |
|||
yield return cp; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IEnumerable<ushort> Values |
|||
{ |
|||
get |
|||
{ |
|||
for (int seg = 0; seg < _segCount; seg++) |
|||
{ |
|||
int start = ReadUInt16BE(_startCodes, seg); |
|||
int end = ReadUInt16BE(_endCodes, seg); |
|||
|
|||
for (int cp = start; cp <= end; cp++) |
|||
{ |
|||
ushort gid = ResolveGlyph(seg, cp); |
|||
|
|||
// Only yield non-zero glyphs
|
|||
if (gid != 0) |
|||
{ |
|||
yield return gid; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public IEnumerator<KeyValuePair<int, ushort>> GetEnumerator() |
|||
{ |
|||
for (int seg = 0; seg < _segCount; seg++) |
|||
{ |
|||
int start = ReadUInt16BE(_startCodes, seg); |
|||
int end = ReadUInt16BE(_endCodes, seg); |
|||
|
|||
for (int cp = start; cp <= end; cp++) |
|||
{ |
|||
ushort gid = ResolveGlyph(seg, cp); |
|||
|
|||
// Only yield mappings to non-zero glyphs
|
|||
if (gid != 0) |
|||
{ |
|||
yield return new KeyValuePair<int, ushort>(cp, gid); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|||
|
|||
// Resolves the glyph ID for a given code point within a specific segment
|
|||
private ushort ResolveGlyph(int segmentIndex, int codePoint) |
|||
{ |
|||
ushort idRangeOffset = ReadUInt16BE(_idRangeOffsets, segmentIndex); |
|||
ushort idDelta = ReadUInt16BE(_idDeltas, segmentIndex); |
|||
|
|||
if (idRangeOffset == 0) |
|||
{ |
|||
return (ushort)((codePoint + idDelta) & 0xFFFF); |
|||
} |
|||
else |
|||
{ |
|||
int start = ReadUInt16BE(_startCodes, segmentIndex); |
|||
int ro = idRangeOffset / 2; // words
|
|||
int idx = (codePoint - start) + ro - (_segCount - segmentIndex); |
|||
int glyphArrayWords = _glyphIdArray.Length / 2; |
|||
|
|||
if ((uint)idx < (uint)glyphArrayWords) |
|||
{ |
|||
ushort glyphId = ReadUInt16BE(_glyphIdArray, idx); |
|||
|
|||
if (glyphId != 0) |
|||
{ |
|||
glyphId = (ushort)((glyphId + idDelta) & 0xFFFF); |
|||
} |
|||
|
|||
return glyphId; |
|||
} |
|||
} |
|||
|
|||
// Not found or maps to missing glyph
|
|||
return 0; |
|||
} |
|||
|
|||
private int FindSegmentIndex(int codePoint) |
|||
{ |
|||
int lo = 0; |
|||
int hi = _segCount - 1; |
|||
|
|||
// Binary search over endCodes (sorted ascending)
|
|||
while (lo <= hi) |
|||
{ |
|||
int mid = (lo + hi) >> 1; |
|||
int end = ReadUInt16BE(_endCodes, mid); |
|||
|
|||
if (codePoint > end) |
|||
{ |
|||
lo = mid + 1; |
|||
} |
|||
else |
|||
{ |
|||
hi = mid - 1; |
|||
} |
|||
} |
|||
|
|||
// lo is now the first segment whose endCode >= codePoint
|
|||
if (lo < _segCount) |
|||
{ |
|||
int start = ReadUInt16BE(_startCodes, lo); |
|||
|
|||
if (codePoint >= start) |
|||
{ |
|||
return lo; |
|||
} |
|||
} |
|||
|
|||
return -1; // not found
|
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,42 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Cmap |
|||
{ |
|||
// Representation of a subtable entry in the 'cmap' table directory
|
|||
internal readonly record struct CmapSubtableEntry |
|||
{ |
|||
public CmapSubtableEntry(PlatformID platform, CmapEncoding encoding, int offset, CmapFormat format) : this() |
|||
{ |
|||
Platform = platform; |
|||
Encoding = encoding; |
|||
Offset = offset; |
|||
Format = format; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the platform identifier for the current environment.
|
|||
/// </summary>
|
|||
public PlatformID Platform { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the character map (CMap) encoding associated with this instance.
|
|||
/// </summary>
|
|||
///
|
|||
public CmapEncoding Encoding { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the offset of the sub table.
|
|||
/// </summary>
|
|||
public int Offset { get; init; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the format of the character-to-glyph mapping (cmap) table.
|
|||
/// </summary>
|
|||
public CmapFormat Format { get; init; } |
|||
|
|||
public ReadOnlyMemory<byte> GetSubtableMemory(ReadOnlyMemory<byte> table) |
|||
{ |
|||
return table.Slice(Offset); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,166 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Cmap |
|||
{ |
|||
/// <summary>
|
|||
/// Represents the 'cmap' table in an OpenType font, which maps character codes to glyph indices.
|
|||
/// </summary>
|
|||
/// <remarks>The 'cmap' table is a critical component of an OpenType font, enabling the mapping of
|
|||
/// character codes (e.g., Unicode) to glyph indices used for rendering text. This class provides functionality to
|
|||
/// load and parse the 'cmap' table from a font's platform-specific typeface.</remarks>
|
|||
internal sealed class CmapTable |
|||
{ |
|||
internal const string TableName = "cmap"; |
|||
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); |
|||
|
|||
public static IReadOnlyDictionary<int, ushort> Load(IGlyphTypeface glyphTypeface) |
|||
{ |
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) |
|||
{ |
|||
throw new InvalidOperationException("No cmap table found."); |
|||
} |
|||
|
|||
var reader = new BigEndianBinaryReader(table.Span); |
|||
|
|||
reader.ReadUInt16(); // version
|
|||
|
|||
var numTables = reader.ReadUInt16(); |
|||
|
|||
var entries = new CmapSubtableEntry[numTables]; |
|||
|
|||
for (var i = 0; i < numTables; i++) |
|||
{ |
|||
var platformID = (PlatformID)reader.ReadUInt16(); |
|||
var encodingID = (CmapEncoding)reader.ReadUInt16(); |
|||
var offset = (int)reader.ReadUInt32(); |
|||
|
|||
var position = reader.Position; |
|||
|
|||
reader.Seek(offset); |
|||
|
|||
var format = (CmapFormat)reader.ReadUInt16(); |
|||
|
|||
reader.Seek(position); |
|||
|
|||
var entry = new CmapSubtableEntry(platformID, encodingID, offset, format); |
|||
|
|||
entries[i] = entry; |
|||
} |
|||
|
|||
// Try to find the best Format 12 subtable entry
|
|||
if (TryFindFormat12Entry(entries, out var format12Entry)) |
|||
{ |
|||
// Prefer Format 12 if available
|
|||
return new CmapFormat12Table(format12Entry.GetSubtableMemory(table)); |
|||
} |
|||
|
|||
// Fallback to Format 4
|
|||
if (TryFindFormat4Entry(entries, out var format4Entry)) |
|||
{ |
|||
return new CmapFormat4Table(format4Entry.GetSubtableMemory(table)); |
|||
} |
|||
|
|||
throw new InvalidOperationException("No suitable cmap subtable found."); |
|||
|
|||
// Tries to find the best Format 12 subtable entry based on platform and encoding preferences
|
|||
static bool TryFindFormat12Entry(CmapSubtableEntry[] entries, out CmapSubtableEntry result) |
|||
{ |
|||
result = default; |
|||
var foundPlatformScore = int.MaxValue; |
|||
var foundEncodingScore = int.MaxValue; |
|||
|
|||
foreach (var entry in entries) |
|||
{ |
|||
if (entry.Format != CmapFormat.Format12) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var platformScore = entry.Platform switch |
|||
{ |
|||
PlatformID.Unicode => 0, |
|||
PlatformID.Windows => 1, |
|||
_ => 2 |
|||
}; |
|||
|
|||
var encodingScore = 2; // Default: lowest preference
|
|||
|
|||
switch (entry.Platform) |
|||
{ |
|||
case PlatformID.Unicode when entry.Encoding == CmapEncoding.Unicode_2_0_full: |
|||
encodingScore = 0; // non-BMP preferred
|
|||
break; |
|||
case PlatformID.Unicode when entry.Encoding == CmapEncoding.Unicode_2_0_BMP: |
|||
encodingScore = 1; // BMP
|
|||
break; |
|||
case PlatformID.Windows when entry.Encoding == CmapEncoding.Microsoft_UCS4 && platformScore != 0: |
|||
encodingScore = 0; // non-BMP preferred
|
|||
break; |
|||
case PlatformID.Windows when entry.Encoding == CmapEncoding.Microsoft_UnicodeBMP && platformScore != 0: |
|||
encodingScore = 1; // BMP
|
|||
break; |
|||
} |
|||
|
|||
if (encodingScore < foundEncodingScore || encodingScore == foundEncodingScore && platformScore < foundPlatformScore) |
|||
{ |
|||
result = entry; |
|||
foundEncodingScore = encodingScore; |
|||
foundPlatformScore = platformScore; |
|||
} |
|||
else |
|||
{ |
|||
if (platformScore < foundPlatformScore) |
|||
{ |
|||
result = entry; |
|||
foundEncodingScore = encodingScore; |
|||
foundPlatformScore = platformScore; |
|||
} |
|||
} |
|||
|
|||
if (foundPlatformScore == 0 && foundEncodingScore == 0) |
|||
{ |
|||
break; // Best possible match found
|
|||
} |
|||
} |
|||
|
|||
return result.Format != CmapFormat.Format0; |
|||
} |
|||
|
|||
// Tries to find the best Format 4 subtable entry based on platform preferences
|
|||
static bool TryFindFormat4Entry(CmapSubtableEntry[] entries, out CmapSubtableEntry result) |
|||
{ |
|||
result = default; |
|||
var foundPlatformScore = int.MaxValue; |
|||
|
|||
foreach (var entry in entries) |
|||
{ |
|||
if (entry.Format != CmapFormat.Format4) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var platformScore = entry.Platform switch |
|||
{ |
|||
PlatformID.Unicode => 0, |
|||
PlatformID.Windows => 1, |
|||
_ => 2 |
|||
}; |
|||
|
|||
if (platformScore < foundPlatformScore) |
|||
{ |
|||
result = entry; |
|||
foundPlatformScore = platformScore; |
|||
} |
|||
|
|||
if (foundPlatformScore == 0) |
|||
{ |
|||
break; // Best possible match found
|
|||
} |
|||
} |
|||
|
|||
return result.Format != CmapFormat.Format0; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,118 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables |
|||
{ |
|||
internal sealed class HeadTable |
|||
{ |
|||
internal const string TableName = "head"; |
|||
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); |
|||
|
|||
public float Version { get; } |
|||
public float FontRevision { get; } |
|||
public uint CheckSumAdjustment { get; } |
|||
public uint MagicNumber { get; } |
|||
public ushort Flags { get; } |
|||
public ushort UnitsPerEm { get; } |
|||
public long Created { get; } |
|||
public long Modified { get; } |
|||
public short XMin { get; } |
|||
public short YMin { get; } |
|||
public short XMax { get; } |
|||
public short YMax { get; } |
|||
public ushort MacStyle { get; } |
|||
public ushort LowestRecPPEM { get; } |
|||
public short FontDirectionHint { get; } |
|||
public short IndexToLocFormat { get; } |
|||
public short GlyphDataFormat { get; } |
|||
|
|||
private HeadTable( |
|||
float version, |
|||
float fontRevision, |
|||
uint checkSumAdjustment, |
|||
uint magicNumber, |
|||
ushort flags, |
|||
ushort unitsPerEm, |
|||
long created, |
|||
long modified, |
|||
short xMin, |
|||
short yMin, |
|||
short xMax, |
|||
short yMax, |
|||
ushort macStyle, |
|||
ushort lowestRecPPEM, |
|||
short fontDirectionHint, |
|||
short indexToLocFormat, |
|||
short glyphDataFormat) |
|||
{ |
|||
Version = version; |
|||
FontRevision = fontRevision; |
|||
CheckSumAdjustment = checkSumAdjustment; |
|||
MagicNumber = magicNumber; |
|||
Flags = flags; |
|||
UnitsPerEm = unitsPerEm; |
|||
Created = created; |
|||
Modified = modified; |
|||
XMin = xMin; |
|||
YMin = yMin; |
|||
XMax = xMax; |
|||
YMax = yMax; |
|||
MacStyle = macStyle; |
|||
LowestRecPPEM = lowestRecPPEM; |
|||
FontDirectionHint = fontDirectionHint; |
|||
IndexToLocFormat = indexToLocFormat; |
|||
GlyphDataFormat = glyphDataFormat; |
|||
} |
|||
|
|||
public static HeadTable Load(IGlyphTypeface glyphTypeface) |
|||
{ |
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) |
|||
{ |
|||
throw new InvalidOperationException("Could not load the 'head' table."); |
|||
} |
|||
|
|||
var reader = new BigEndianBinaryReader(table.Span); |
|||
|
|||
return Load(reader); |
|||
} |
|||
|
|||
private static HeadTable Load(BigEndianBinaryReader reader) |
|||
{ |
|||
float version = reader.ReadFixed(); |
|||
float fontRevision = reader.ReadFixed(); |
|||
uint checkSumAdjustment = reader.ReadUInt32(); |
|||
uint magicNumber = reader.ReadUInt32(); |
|||
ushort flags = reader.ReadUInt16(); |
|||
ushort unitsPerEm = reader.ReadUInt16(); |
|||
long created = reader.ReadInt64(); |
|||
long modified = reader.ReadInt64(); |
|||
short xMin = reader.ReadInt16(); |
|||
short yMin = reader.ReadInt16(); |
|||
short xMax = reader.ReadInt16(); |
|||
short yMax = reader.ReadInt16(); |
|||
ushort macStyle = reader.ReadUInt16(); |
|||
ushort lowestRecPPEM = reader.ReadUInt16(); |
|||
short fontDirectionHint = reader.ReadInt16(); |
|||
short indexToLocFormat = reader.ReadInt16(); |
|||
short glyphDataFormat = reader.ReadInt16(); |
|||
|
|||
return new HeadTable( |
|||
version, |
|||
fontRevision, |
|||
checkSumAdjustment, |
|||
magicNumber, |
|||
flags, |
|||
unitsPerEm, |
|||
created, |
|||
modified, |
|||
xMin, |
|||
yMin, |
|||
xMax, |
|||
yMax, |
|||
macStyle, |
|||
lowestRecPPEM, |
|||
fontDirectionHint, |
|||
indexToLocFormat, |
|||
glyphDataFormat); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
namespace Avalonia.Media.Fonts.Tables |
|||
{ |
|||
internal readonly struct MaxpTable |
|||
{ |
|||
internal const string TableName = "maxp"; |
|||
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); |
|||
|
|||
public ushort NumGlyphs { get; } |
|||
|
|||
private MaxpTable(ushort numGlyphs) |
|||
{ |
|||
NumGlyphs = numGlyphs; |
|||
} |
|||
|
|||
public static MaxpTable? Load(IGlyphTypeface fontFace) |
|||
{ |
|||
if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var binaryReader = new BigEndianBinaryReader(table.Span); |
|||
|
|||
return Load(binaryReader); |
|||
} |
|||
|
|||
private static MaxpTable Load(BigEndianBinaryReader reader) |
|||
{ |
|||
// Skip version (4 bytes)
|
|||
reader.ReadUInt32(); |
|||
|
|||
var numGlyphs = reader.ReadUInt16(); |
|||
|
|||
return new MaxpTable(numGlyphs); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
namespace Avalonia.Media.Fonts.Tables.Metrics |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a single horizontal metric record from the 'hmtx' table.
|
|||
/// </summary>
|
|||
internal readonly record struct HorizontalGlyphMetric |
|||
{ |
|||
/// <summary>
|
|||
/// The advance width of the glyph.
|
|||
/// </summary>
|
|||
public ushort AdvanceWidth { get; } |
|||
|
|||
/// <summary>
|
|||
/// The left side bearing of the glyph.
|
|||
/// </summary>
|
|||
public short LeftSideBearing { get; } |
|||
|
|||
public HorizontalGlyphMetric(ushort advanceWidth, short leftSideBearing) |
|||
{ |
|||
AdvanceWidth = advanceWidth; |
|||
LeftSideBearing = leftSideBearing; |
|||
} |
|||
|
|||
public override string ToString() => $"Advance={AdvanceWidth}, LSB={LeftSideBearing}"; |
|||
} |
|||
} |
|||
@ -0,0 +1,113 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Metrics |
|||
{ |
|||
internal class HorizontalMetricsTable |
|||
{ |
|||
public const string TagName = "hmtx"; |
|||
public static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TagName); |
|||
|
|||
private readonly ReadOnlyMemory<byte> _data; |
|||
private readonly ushort _numOfHMetrics; |
|||
private readonly uint _numGlyphs; |
|||
|
|||
private HorizontalMetricsTable(ReadOnlyMemory<byte> data, ushort numOfHMetrics, uint numGlyphs) |
|||
{ |
|||
_data = data; |
|||
_numOfHMetrics = numOfHMetrics; |
|||
_numGlyphs = numGlyphs; |
|||
} |
|||
|
|||
internal static HorizontalMetricsTable? Load(IGlyphTypeface glyphTypeface, ushort numberOfHMetrics, uint glyphCount) |
|||
{ |
|||
if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) |
|||
{ |
|||
return new HorizontalMetricsTable(table, numberOfHMetrics, glyphCount); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the horizontal glyph metrics for the specified glyph index.
|
|||
/// </summary>
|
|||
/// <remarks>This method retrieves the horizontal metrics for a single glyph by its index. The
|
|||
/// returned metrics include information such as advance width, left side bearing, and other glyph-specific
|
|||
/// data.</remarks>
|
|||
/// <param name="glyphIndex">The index of the glyph for which to retrieve metrics. Must be a valid glyph index within the font.</param>
|
|||
/// <returns>A <see cref="HorizontalGlyphMetric"/> object containing the horizontal metrics for the specified glyph.</returns>
|
|||
public HorizontalGlyphMetric GetMetrics(ushort glyphIndex) |
|||
{ |
|||
// Validate glyph index
|
|||
if (glyphIndex >= _numGlyphs) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(glyphIndex), $"Glyph index {glyphIndex} is out of range."); |
|||
} |
|||
|
|||
var reader = new BigEndianBinaryReader(_data.Span); |
|||
|
|||
if (glyphIndex < _numOfHMetrics) |
|||
{ |
|||
// Each record is 4 bytes
|
|||
reader.Seek(glyphIndex * 4); |
|||
|
|||
ushort advanceWidth = reader.ReadUInt16(); |
|||
short leftSideBearing = reader.ReadInt16(); |
|||
|
|||
return new HorizontalGlyphMetric(advanceWidth, leftSideBearing); |
|||
} |
|||
else |
|||
{ |
|||
// Last advance width
|
|||
reader.Seek((_numOfHMetrics - 1) * 4); |
|||
|
|||
ushort lastAdvanceWidth = reader.ReadUInt16(); |
|||
|
|||
// Offset into trailing LSB array
|
|||
int lsbIndex = glyphIndex - _numOfHMetrics; |
|||
int lsbOffset = _numOfHMetrics * 4 + lsbIndex * 2; |
|||
|
|||
reader.Seek(lsbOffset); |
|||
|
|||
short leftSideBearing = reader.ReadInt16(); |
|||
|
|||
return new HorizontalGlyphMetric(lastAdvanceWidth, leftSideBearing); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the advance width for a single glyph.
|
|||
/// </summary>
|
|||
/// <param name="glyphIndex">Glyph index to query.</param>
|
|||
/// <returns>Advance width for the glyph.</returns>
|
|||
public ushort GetAdvance(ushort glyphIndex) |
|||
{ |
|||
// Validate glyph index
|
|||
if (glyphIndex >= _numGlyphs) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(glyphIndex)); |
|||
} |
|||
|
|||
var reader = new BigEndianBinaryReader(_data.Span); |
|||
|
|||
if (glyphIndex < _numOfHMetrics) |
|||
{ |
|||
// Each record is 4 bytes
|
|||
reader.Seek(glyphIndex * 4); |
|||
|
|||
ushort advanceWidth = reader.ReadUInt16(); |
|||
|
|||
return advanceWidth; |
|||
} |
|||
else |
|||
{ |
|||
// Last advance width
|
|||
reader.Seek((_numOfHMetrics - 1) * 4); |
|||
|
|||
ushort lastAdvanceWidth = reader.ReadUInt16(); |
|||
|
|||
return lastAdvanceWidth; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
namespace Avalonia.Media.Fonts.Tables.Metrics |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a single vertical metric record from the 'vmtx' table.
|
|||
/// </summary>
|
|||
internal readonly record struct VerticalGlyphMetric |
|||
{ |
|||
public VerticalGlyphMetric(ushort advanceHeight, short topSideBearing) |
|||
{ |
|||
AdvanceHeight = advanceHeight; |
|||
TopSideBearing = topSideBearing; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The advance height of the glyph.
|
|||
/// </summary>
|
|||
public ushort AdvanceHeight { get; } |
|||
|
|||
/// <summary>
|
|||
/// The top side bearing of the glyph.
|
|||
/// </summary>
|
|||
public short TopSideBearing { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,110 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Media.Fonts.Tables.Metrics |
|||
{ |
|||
internal class VerticalMetricsTable |
|||
{ |
|||
public const string TagName = "vmtx"; |
|||
public static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TagName); |
|||
|
|||
private readonly ReadOnlyMemory<byte> _data; |
|||
private readonly ushort _numOfVMetrics; |
|||
private readonly uint _numGlyphs; |
|||
|
|||
private VerticalMetricsTable(ReadOnlyMemory<byte> data, ushort numOfVMetrics, uint numGlyphs) |
|||
{ |
|||
_data = data; |
|||
_numOfVMetrics = numOfVMetrics; |
|||
_numGlyphs = numGlyphs; |
|||
} |
|||
|
|||
public static VerticalMetricsTable? Load(IGlyphTypeface glyphTypeface, ushort numberOfVMetrics, uint glyphCount) |
|||
{ |
|||
if (glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) |
|||
{ |
|||
return new VerticalMetricsTable(table, numberOfVMetrics, glyphCount); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the vertical glyph metrics for the specified glyph index.
|
|||
/// </summary>
|
|||
/// <param name="glyphIndex">The index of the glyph for which to retrieve metrics.</param>
|
|||
/// <returns>A <see cref="VerticalGlyphMetric"/> containing the vertical metrics for the specified glyph.</returns>
|
|||
public VerticalGlyphMetric GetMetrics(ushort glyphIndex) |
|||
{ |
|||
// Validate glyph index
|
|||
if (glyphIndex >= _numGlyphs) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(glyphIndex), $"Glyph index {glyphIndex} is out of range."); |
|||
} |
|||
|
|||
var reader = new BigEndianBinaryReader(_data.Span); |
|||
|
|||
if (glyphIndex < _numOfVMetrics) |
|||
{ |
|||
// Each record is 4 bytes
|
|||
reader.Seek(glyphIndex * 4); |
|||
|
|||
ushort advanceHeight = reader.ReadUInt16(); |
|||
short topSideBearing = reader.ReadInt16(); |
|||
|
|||
return new VerticalGlyphMetric(advanceHeight, topSideBearing); |
|||
} |
|||
else |
|||
{ |
|||
// Last advance height
|
|||
reader.Seek((_numOfVMetrics - 1) * 4); |
|||
|
|||
ushort lastAdvanceHeight = reader.ReadUInt16(); |
|||
|
|||
// Offset into trailing TSB array
|
|||
int tsbIndex = glyphIndex - _numOfVMetrics; |
|||
int tsbOffset = _numOfVMetrics * 4 + tsbIndex * 2; |
|||
|
|||
reader.Seek(tsbOffset); |
|||
|
|||
short tsb = reader.ReadInt16(); |
|||
|
|||
return new VerticalGlyphMetric(lastAdvanceHeight, tsb); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Retrieves the advance height for a single glyph.
|
|||
/// </summary>
|
|||
/// <param name="glyphIndex">Glyph index to query.</param>
|
|||
/// <returns>Advance height for the glyph.</returns>
|
|||
public ushort GetAdvance(ushort glyphIndex) |
|||
{ |
|||
// Validate glyph index
|
|||
if (glyphIndex >= _numGlyphs) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(glyphIndex)); |
|||
} |
|||
|
|||
var reader = new BigEndianBinaryReader(_data.Span); |
|||
|
|||
if (glyphIndex < _numOfVMetrics) |
|||
{ |
|||
// Each record is 4 bytes
|
|||
reader.Seek(glyphIndex * 4); |
|||
|
|||
ushort advanceHeight = reader.ReadUInt16(); |
|||
|
|||
return advanceHeight; |
|||
} |
|||
else |
|||
{ |
|||
// Last advance height
|
|||
reader.Seek((_numOfVMetrics - 1) * 4); |
|||
|
|||
ushort lastAdvanceHeight = reader.ReadUInt16(); |
|||
|
|||
return lastAdvanceHeight; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
namespace Avalonia.Media.Fonts.Tables |
|||
{ |
|||
internal readonly struct PostTable |
|||
{ |
|||
internal const string TableName = "post"; |
|||
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); |
|||
|
|||
public float Version { get; } |
|||
public float ItalicAngle { get; } |
|||
public short UnderlinePosition { get; } |
|||
public short UnderlineThickness { get; } |
|||
public bool IsFixedPitch { get; } |
|||
|
|||
private PostTable(float version, float italicAngle, short underlinePosition, short underlineThickness, uint isFixedPitch) |
|||
{ |
|||
Version = version; |
|||
ItalicAngle = italicAngle; |
|||
UnderlinePosition = underlinePosition; |
|||
UnderlineThickness = underlineThickness; |
|||
IsFixedPitch = isFixedPitch != 0; |
|||
} |
|||
|
|||
public static PostTable Load(IGlyphTypeface glyphTypeface) |
|||
{ |
|||
if (!glyphTypeface.PlatformTypeface.TryGetTable(Tag, out var table)) |
|||
{ |
|||
return default; |
|||
} |
|||
|
|||
var binaryReader = new BigEndianBinaryReader(table.Span); |
|||
|
|||
return Load(binaryReader); |
|||
} |
|||
|
|||
private static PostTable Load(BigEndianBinaryReader reader) |
|||
{ |
|||
float version = reader.ReadFixed(); |
|||
float italicAngle = reader.ReadFixed(); |
|||
short underlinePosition = reader.ReadFWORD(); |
|||
short underlineThickness = reader.ReadFWORD(); |
|||
uint isFixedPitch = reader.ReadUInt32(); |
|||
|
|||
return new PostTable(version, italicAngle, underlinePosition, underlineThickness, isFixedPitch); |
|||
} |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
// 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); |
|||
} |
|||
} |
|||
@ -0,0 +1,128 @@ |
|||
namespace Avalonia.Media.Fonts.Tables |
|||
{ |
|||
internal class VerticalHeaderTable |
|||
{ |
|||
internal const string TableName = "vhea"; |
|||
internal static OpenTypeTag Tag { get; } = OpenTypeTag.Parse(TableName); |
|||
|
|||
public VerticalHeaderTable( |
|||
short ascender, |
|||
short descender, |
|||
short lineGap, |
|||
ushort advanceHeightMax, |
|||
short minTopSideBearing, |
|||
short minBottomSideBearing, |
|||
short yMaxExtent, |
|||
short caretSlopeRise, |
|||
short caretSlopeRun, |
|||
short caretOffset, |
|||
ushort numberOfVMetrics) |
|||
{ |
|||
Ascender = ascender; |
|||
Descender = descender; |
|||
LineGap = lineGap; |
|||
AdvanceHeightMax = advanceHeightMax; |
|||
MinTopSideBearing = minTopSideBearing; |
|||
MinBottomSideBearing = minBottomSideBearing; |
|||
YMaxExtent = yMaxExtent; |
|||
CaretSlopeRise = caretSlopeRise; |
|||
CaretSlopeRun = caretSlopeRun; |
|||
CaretOffset = caretOffset; |
|||
NumberOfVMetrics = numberOfVMetrics; |
|||
} |
|||
|
|||
public ushort AdvanceHeightMax { 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 MinTopSideBearing { get; } |
|||
|
|||
public short MinBottomSideBearing { get; } |
|||
|
|||
public ushort NumberOfVMetrics { get; } |
|||
|
|||
public short YMaxExtent { get; } |
|||
|
|||
public static VerticalHeaderTable? Load(IGlyphTypeface fontFace) |
|||
{ |
|||
if (!fontFace.PlatformTypeface.TryGetTable(Tag, out var table)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
var binaryReader = new BigEndianBinaryReader(table.Span); |
|||
|
|||
// Move to start of table.
|
|||
return Load(binaryReader); |
|||
} |
|||
|
|||
private static VerticalHeaderTable Load(BigEndianBinaryReader reader) |
|||
{ |
|||
// See OpenType spec for vhea:
|
|||
// | Fixed | version | 0x00010000 (1.0) |
|
|||
// | FWord | ascender | Distance from baseline of highest ascender (vertical) |
|
|||
// | FWord | descender | Distance from baseline of lowest descender (vertical) |
|
|||
// | FWord | lineGap | typographic line gap (vertical) |
|
|||
// | uFWord | advanceHeightMax | must be consistent with vertical metrics |
|
|||
// | FWord | minTopSideBearing | must be consistent with vertical metrics |
|
|||
// | FWord | minBottomSideBearing| must be consistent with vertical metrics |
|
|||
// | FWord | yMaxExtent | max(tsb + (yMax-yMin)) |
|
|||
// | 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 | numOfLongVerMetrics | number of advance heights in vertical metrics table |
|
|||
|
|||
ushort majorVersion = reader.ReadUInt16(); |
|||
ushort minorVersion = reader.ReadUInt16(); |
|||
short ascender = reader.ReadFWORD(); |
|||
short descender = reader.ReadFWORD(); |
|||
short lineGap = reader.ReadFWORD(); |
|||
ushort advanceHeightMax = reader.ReadUFWORD(); |
|||
short minTopSideBearing = reader.ReadFWORD(); |
|||
short minBottomSideBearing = reader.ReadFWORD(); |
|||
short yMaxExtent = 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 numberOfVMetrics = reader.ReadUInt16(); |
|||
|
|||
return new VerticalHeaderTable( |
|||
ascender, |
|||
descender, |
|||
lineGap, |
|||
advanceHeightMax, |
|||
minTopSideBearing, |
|||
minBottomSideBearing, |
|||
yMaxExtent, |
|||
caretSlopeRise, |
|||
caretSlopeRun, |
|||
caretOffset, |
|||
numberOfVMetrics); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,407 @@ |
|||
using System; |
|||
using System.Buffers; |
|||
using System.Buffers.Binary; |
|||
using System.Collections.Generic; |
|||
using System.IO; |
|||
using System.Runtime.InteropServices; |
|||
using System.Threading; |
|||
|
|||
namespace Avalonia.Media.Fonts |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a memory manager for unmanaged font data, providing functionality to access and manage font memory
|
|||
/// and OpenType table data.
|
|||
/// </summary>
|
|||
/// <remarks>This class encapsulates unmanaged memory containing font data and provides methods to
|
|||
/// retrieve specific OpenType table data. It ensures thread-safe access to the memory and supports pinning for
|
|||
/// interoperability scenarios. Instances of this class must be properly disposed to release unmanaged
|
|||
/// resources.</remarks>
|
|||
internal sealed unsafe class UnmanagedFontMemory : MemoryManager<byte>, IFontMemory |
|||
{ |
|||
private IntPtr _ptr; |
|||
private int _length; |
|||
private bool _disposed; |
|||
private int _pinCount; |
|||
|
|||
// Reader/writer lock to protect lifetime and cache access.
|
|||
private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.NoRecursion); |
|||
|
|||
/// <summary>
|
|||
/// Represents a cache of font table data, where each entry maps an OpenType tag to its corresponding byte data.
|
|||
/// </summary>
|
|||
/// <remarks>This dictionary is used to store preloaded font table data for efficient access. The
|
|||
/// keys are OpenType tags, which identify specific font tables, and the values are the corresponding byte data
|
|||
/// stored as read-only memory. This ensures that the data cannot be modified after being loaded into the
|
|||
/// cache.</remarks>
|
|||
private readonly Dictionary<OpenTypeTag, ReadOnlyMemory<byte>> _tableCache = []; |
|||
|
|||
private UnmanagedFontMemory(IntPtr ptr, int length) |
|||
{ |
|||
_ptr = ptr; |
|||
_length = length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Attempts to retrieve the memory region corresponding to the specified OpenType table tag.
|
|||
/// </summary>
|
|||
/// <remarks>This method searches for the specified OpenType table in the font data and retrieves
|
|||
/// its memory region if found. The method performs bounds checks to ensure the requested table is valid and
|
|||
/// safely accessible. If the table is not found or the font data is invalid, the method returns <see
|
|||
/// langword="false"/>.</remarks>
|
|||
/// <param name="tag">The <see cref="OpenTypeTag"/> identifying the table to retrieve. Must not be <see cref="OpenTypeTag.None"/>.</param>
|
|||
/// <param name="table">When this method returns, contains the memory region of the requested table if the operation succeeds;
|
|||
/// otherwise, contains the default value.</param>
|
|||
/// <returns><see langword="true"/> if the table memory was successfully retrieved; otherwise, <see langword="false"/>.</returns>
|
|||
/// <exception cref="ObjectDisposedException">Thrown if the font memory has been disposed.</exception>
|
|||
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table) |
|||
{ |
|||
table = default; |
|||
|
|||
// Validate tag
|
|||
if (tag == OpenTypeTag.None) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
_lock.EnterUpgradeableReadLock(); |
|||
|
|||
try |
|||
{ |
|||
if (_disposed) |
|||
{ |
|||
throw new ObjectDisposedException(nameof(UnmanagedFontMemory)); |
|||
} |
|||
|
|||
if (_ptr == IntPtr.Zero || _length < 12) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// Create a span over the unmanaged memory (read-only view)
|
|||
var fontData = Memory.Span; |
|||
|
|||
// Minimal SFNT header: 4 (sfnt) + 2 (numTables) + 6 (rest) = 12
|
|||
if (fontData.Length < 12) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// Check cache first
|
|||
if (_tableCache.TryGetValue(tag, out var cached)) |
|||
{ |
|||
table = cached; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
// Parse table directory
|
|||
var numTables = BinaryPrimitives.ReadUInt16BigEndian(fontData.Slice(4, 2)); |
|||
var recordsStart = 12; |
|||
var requiredDirectoryBytes = checked(recordsStart + numTables * 16); |
|||
|
|||
if (fontData.Length < requiredDirectoryBytes) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
for (int i = 0; i < numTables; i++) |
|||
{ |
|||
var entryOffset = recordsStart + i * 16; |
|||
var entrySlice = fontData.Slice(entryOffset, 16); |
|||
var entryTag = (OpenTypeTag)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(0, 4)); |
|||
|
|||
if (entryTag != tag) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var offset = (int)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(8, 4)); |
|||
var length = (int)BinaryPrimitives.ReadUInt32BigEndian(entrySlice.Slice(12, 4)); |
|||
|
|||
// Bounds checks - ensure values fit within the span
|
|||
if (offset > fontData.Length || length > fontData.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (offset + length > fontData.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if (offset < 0 || length < 0 || offset + length > fontData.Length) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
table = Memory.Slice(offset, length); |
|||
|
|||
// Acquire write lock to update cache
|
|||
_lock.EnterWriteLock(); |
|||
|
|||
try |
|||
{ |
|||
// Cache the result for faster subsequent lookups
|
|||
_tableCache[tag] = table; |
|||
|
|||
return true; |
|||
} |
|||
finally |
|||
{ |
|||
// Release write lock
|
|||
_lock.ExitWriteLock(); |
|||
} |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
finally |
|||
{ |
|||
// Release upgradeable read lock
|
|||
_lock.ExitUpgradeableReadLock(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Loads font data from the specified stream into unmanaged memory.
|
|||
/// </summary>
|
|||
public static UnmanagedFontMemory LoadFromStream(Stream stream) |
|||
{ |
|||
if (stream is null) |
|||
{ |
|||
throw new ArgumentNullException(nameof(stream)); |
|||
} |
|||
|
|||
if (!stream.CanRead) |
|||
{ |
|||
throw new ArgumentException("Stream is not readable", nameof(stream)); |
|||
} |
|||
|
|||
if (stream.CanSeek) |
|||
{ |
|||
var length = checked((int)stream.Length); |
|||
var ptr = Marshal.AllocHGlobal(length); |
|||
var buffer = ArrayPool<byte>.Shared.Rent(8192); |
|||
|
|||
try |
|||
{ |
|||
var remaining = length; |
|||
var offset = 0; |
|||
|
|||
while (remaining > 0) |
|||
{ |
|||
var toRead = Math.Min(buffer.Length, remaining); |
|||
var read = stream.Read(buffer, 0, toRead); |
|||
|
|||
if (read == 0) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
Marshal.Copy(buffer, 0, ptr + offset, read); |
|||
|
|||
offset += read; |
|||
|
|||
remaining -= read; |
|||
} |
|||
|
|||
return new UnmanagedFontMemory(ptr, offset); |
|||
} |
|||
catch |
|||
{ |
|||
Marshal.FreeHGlobal(ptr); |
|||
throw; |
|||
} |
|||
finally |
|||
{ |
|||
ArrayPool<byte>.Shared.Return(buffer); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
using var ms = new MemoryStream(); |
|||
|
|||
stream.CopyTo(ms); |
|||
|
|||
var len = checked((int)ms.Length); |
|||
|
|||
var buffer = ms.GetBuffer(); |
|||
|
|||
// GetBuffer may return a larger array than the actual data length.
|
|||
return CreateFromBytes(new ReadOnlySpan<byte>(buffer, 0, len)); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates an instance of <see cref="UnmanagedFontMemory"/> from the specified byte data.
|
|||
/// </summary>
|
|||
/// <remarks>The method allocates unmanaged memory to store the provided byte data. The caller is
|
|||
/// responsible for ensuring that the returned <see cref="UnmanagedFontMemory"/> instance is properly disposed
|
|||
/// to release the allocated memory.</remarks>
|
|||
/// <param name="data">A read-only span of bytes representing the font data. The span must not be empty.</param>
|
|||
/// <returns>An instance of <see cref="UnmanagedFontMemory"/> that encapsulates the unmanaged memory containing the font
|
|||
/// data.</returns>
|
|||
private static UnmanagedFontMemory CreateFromBytes(ReadOnlySpan<byte> data) |
|||
{ |
|||
var len = data.Length; |
|||
var ptr = Marshal.AllocHGlobal(len); |
|||
|
|||
try |
|||
{ |
|||
if (len > 0) |
|||
{ |
|||
unsafe |
|||
{ |
|||
fixed (byte* src = &MemoryMarshal.GetReference(data)) |
|||
{ |
|||
Buffer.MemoryCopy(src, (void*)ptr, len, len); |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new UnmanagedFontMemory(ptr, len); |
|||
} |
|||
catch |
|||
{ |
|||
Marshal.FreeHGlobal(ptr); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
// Implement MemoryManager<byte> members on the owner
|
|||
public override Span<byte> GetSpan() |
|||
{ |
|||
_lock.EnterReadLock(); |
|||
|
|||
try |
|||
{ |
|||
if (_disposed) |
|||
{ |
|||
throw new ObjectDisposedException(nameof(UnmanagedFontMemory)); |
|||
} |
|||
|
|||
if (_ptr == IntPtr.Zero || _length <= 0) |
|||
{ |
|||
return Span<byte>.Empty; |
|||
} |
|||
|
|||
unsafe |
|||
{ |
|||
return new Span<byte>((void*)_ptr.ToPointer(), _length); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_lock.ExitReadLock(); |
|||
} |
|||
} |
|||
|
|||
public override MemoryHandle Pin(int elementIndex = 0) |
|||
{ |
|||
if (elementIndex < 0) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(elementIndex)); |
|||
} |
|||
|
|||
// Increment pin count first to prevent dispose racing with pin.
|
|||
Interlocked.Increment(ref _pinCount); |
|||
|
|||
// Validate state under lock
|
|||
_lock.EnterReadLock(); |
|||
|
|||
try |
|||
{ |
|||
if (_disposed) |
|||
{ |
|||
throw new ObjectDisposedException(nameof(UnmanagedFontMemory)); |
|||
} |
|||
|
|||
if (_ptr == IntPtr.Zero || _length == 0) |
|||
{ |
|||
return new MemoryHandle(); |
|||
} |
|||
|
|||
if (elementIndex > _length) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(elementIndex)); |
|||
} |
|||
|
|||
unsafe |
|||
{ |
|||
var p = (byte*)_ptr.ToPointer() + elementIndex; |
|||
return new MemoryHandle(p); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
_lock.ExitReadLock(); |
|||
} |
|||
} |
|||
|
|||
public override void Unpin() |
|||
{ |
|||
// Decrement pin count
|
|||
Interlocked.Decrement(ref _pinCount); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Dispose(true); |
|||
|
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (disposing) |
|||
{ |
|||
// Explicit dispose: use lock to synchronize with other threads and dispose managed resources.
|
|||
_lock.EnterWriteLock(); |
|||
|
|||
try |
|||
{ |
|||
if (_disposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (Volatile.Read(ref _pinCount) > 0) |
|||
{ |
|||
throw new InvalidOperationException("Cannot dispose while memory is pinned."); |
|||
} |
|||
|
|||
if (_ptr != IntPtr.Zero) |
|||
{ |
|||
Marshal.FreeHGlobal(_ptr); |
|||
_ptr = IntPtr.Zero; |
|||
} |
|||
|
|||
_length = 0; |
|||
|
|||
_disposed = true; |
|||
} |
|||
finally |
|||
{ |
|||
_lock.ExitWriteLock(); |
|||
// Dispose the lock (managed resource) only on explicit dispose.
|
|||
_lock.Dispose(); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// Finalizer: do not touch managed objects. Free only unmanaged memory.
|
|||
var ptr = Interlocked.Exchange(ref _ptr, IntPtr.Zero); |
|||
|
|||
if (ptr != IntPtr.Zero) |
|||
{ |
|||
Marshal.FreeHGlobal(ptr); |
|||
} |
|||
|
|||
Interlocked.Exchange(ref _length, 0); |
|||
|
|||
// Mark as disposed to prevent further attempts to use the memory.
|
|||
_disposed = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,395 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Globalization; |
|||
using Avalonia.Media.Fonts; |
|||
using Avalonia.Media.Fonts.Tables; |
|||
using Avalonia.Media.Fonts.Tables.Cmap; |
|||
using Avalonia.Media.Fonts.Tables.Metrics; |
|||
using Avalonia.Media.Fonts.Tables.Name; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Media |
|||
{ |
|||
/// <summary>
|
|||
/// Represents a glyph typeface, providing access to font metrics, glyph mappings, and other font-related
|
|||
/// properties.
|
|||
/// </summary>
|
|||
/// <remarks>The <see cref="GlyphTypeface"/> class is used to encapsulate font data, including metrics,
|
|||
/// character-to-glyph mappings, and supported OpenType features. It supports platform-specific typefaces and
|
|||
/// applies optional font simulations such as bold or oblique. This class is typically used in text rendering and
|
|||
/// shaping scenarios.</remarks>
|
|||
internal class GlyphTypeface : IGlyphTypeface |
|||
{ |
|||
private bool _isDisposed; |
|||
|
|||
private readonly NameTable? _nameTable; |
|||
private readonly OS2Table? _os2Table; |
|||
private readonly IReadOnlyDictionary<int, ushort> _cmapTable; |
|||
private readonly HorizontalHeaderTable? _hhTable; |
|||
private readonly VerticalHeaderTable? _vhTable; |
|||
private readonly HorizontalMetricsTable? _hmTable; |
|||
private readonly VerticalMetricsTable? _vmTable; |
|||
|
|||
private IReadOnlyList<OpenTypeTag>? _supportedFeatures; |
|||
private ITextShaperTypeface? _textShaperTypeface; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="GlyphTypeface"/> class with the specified platform typeface and
|
|||
/// font simulations.
|
|||
/// </summary>
|
|||
/// <remarks>This constructor initializes the glyph typeface by loading various font tables,
|
|||
/// including OS/2, CMAP, and metrics tables, to calculate font metrics and other properties. It also determines
|
|||
/// font characteristics such as weight, style, stretch, and family names based on the provided typeface and
|
|||
/// font simulations.</remarks>
|
|||
/// <param name="typeface">The platform-specific typeface to be used for this <see cref="GlyphTypeface"/> instance. This parameter
|
|||
/// cannot be <c>null</c>.</param>
|
|||
/// <param name="fontSimulations">The font simulations to apply, such as bold or oblique. The default is <see cref="FontSimulations.None"/>.</param>
|
|||
/// <exception cref="InvalidOperationException">Thrown if required font tables (e.g., 'maxp') cannot be loaded.</exception>
|
|||
public GlyphTypeface(IPlatformTypeface typeface, FontSimulations fontSimulations = FontSimulations.None) |
|||
{ |
|||
PlatformTypeface = typeface; |
|||
|
|||
_os2Table = OS2Table.Load(this); |
|||
_cmapTable = CmapTable.Load(this); |
|||
|
|||
var maxpTable = MaxpTable.Load(this) ?? throw new InvalidOperationException("Could not load the 'maxp' table."); |
|||
|
|||
GlyphCount = maxpTable.NumGlyphs; |
|||
|
|||
_hhTable = HorizontalHeaderTable.Load(this); |
|||
|
|||
if (_hhTable is not null) |
|||
{ |
|||
_hmTable = HorizontalMetricsTable.Load(this, _hhTable.NumberOfHMetrics, GlyphCount); |
|||
} |
|||
|
|||
_vhTable = VerticalHeaderTable.Load(this); |
|||
|
|||
if (_vhTable is not null) |
|||
{ |
|||
_vmTable = VerticalMetricsTable.Load(this, _vhTable.NumberOfVMetrics, GlyphCount); |
|||
} |
|||
|
|||
var ascent = 0; |
|||
var descent = 0; |
|||
var lineGap = 0; |
|||
|
|||
if (_os2Table != null && (_os2Table.Selection & OS2Table.FontSelectionFlags.USE_TYPO_METRICS) != 0) |
|||
{ |
|||
ascent = -_os2Table.TypoAscender; |
|||
descent = -_os2Table.TypoDescender; |
|||
lineGap = _os2Table.TypoLineGap; |
|||
} |
|||
else |
|||
{ |
|||
if (_hhTable != null) |
|||
{ |
|||
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; |
|||
} |
|||
} |
|||
|
|||
var headTable = HeadTable.Load(this); |
|||
var postTable = PostTable.Load(this); |
|||
|
|||
var isFixedPitch = postTable.IsFixedPitch; |
|||
var underlineOffset = postTable.UnderlinePosition; |
|||
var underlineSize = postTable.UnderlineThickness; |
|||
|
|||
Metrics = new FontMetrics |
|||
{ |
|||
DesignEmHeight = (short)headTable.UnitsPerEm, |
|||
Ascent = ascent, |
|||
Descent = descent, |
|||
LineGap = lineGap, |
|||
UnderlinePosition = -underlineOffset, |
|||
UnderlineThickness = underlineSize, |
|||
StrikethroughPosition = -_os2Table?.StrikeoutPosition ?? 0, |
|||
StrikethroughThickness = _os2Table?.StrikeoutSize ?? 0, |
|||
IsFixedPitch = isFixedPitch |
|||
}; |
|||
|
|||
FontSimulations = fontSimulations; |
|||
|
|||
var fontWeight = _os2Table != null ? (FontWeight)_os2Table.WeightClass : FontWeight.Normal; |
|||
|
|||
Weight = (fontSimulations & FontSimulations.Bold) != 0 ? FontWeight.Bold : fontWeight; |
|||
|
|||
var style = _os2Table != null ? GetFontStyle(_os2Table, headTable, postTable) : FontStyle.Normal; |
|||
|
|||
Style = (fontSimulations & FontSimulations.Oblique) != 0 ? FontStyle.Italic : style; |
|||
|
|||
var stretch = _os2Table != null ? (FontStretch)_os2Table.WidthClass : FontStretch.Normal; |
|||
|
|||
Stretch = stretch; |
|||
|
|||
_nameTable = NameTable.Load(this); |
|||
|
|||
FamilyName = _nameTable?.FontFamilyName((ushort)CultureInfo.InvariantCulture.LCID) ?? "unknown"; |
|||
|
|||
TypographicFamilyName = _nameTable?.GetNameById((ushort)CultureInfo.InvariantCulture.LCID, KnownNameIds.TypographicFamilyName) ?? FamilyName; |
|||
|
|||
if (_nameTable != null) |
|||
{ |
|||
var familyNames = new Dictionary<CultureInfo, string>(1); |
|||
var faceNames = new Dictionary<CultureInfo, string>(1); |
|||
|
|||
foreach (var nameRecord in _nameTable) |
|||
{ |
|||
if (nameRecord.NameID == KnownNameIds.FontFamilyName) |
|||
{ |
|||
if (nameRecord.Platform != Fonts.Tables.PlatformID.Windows || nameRecord.LanguageID == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var culture = GetCulture(nameRecord.LanguageID); |
|||
|
|||
if (!familyNames.ContainsKey(culture)) |
|||
{ |
|||
familyNames[culture] = nameRecord.Value; |
|||
} |
|||
|
|||
} |
|||
|
|||
if (nameRecord.NameID == KnownNameIds.FontSubfamilyName) |
|||
{ |
|||
if (nameRecord.Platform != Fonts.Tables.PlatformID.Windows || nameRecord.LanguageID == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
var culture = GetCulture(nameRecord.LanguageID); |
|||
|
|||
if (!faceNames.ContainsKey(culture)) |
|||
{ |
|||
faceNames[culture] = nameRecord.Value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
FamilyNames = familyNames; |
|||
FaceNames = faceNames; |
|||
} |
|||
else |
|||
{ |
|||
FamilyNames = new Dictionary<CultureInfo, string> { { CultureInfo.InvariantCulture, FamilyName } }; |
|||
FaceNames = new Dictionary<CultureInfo, string> { { CultureInfo.InvariantCulture, Weight.ToString() } }; |
|||
} |
|||
|
|||
static CultureInfo GetCulture(int lcid) |
|||
{ |
|||
if (lcid == ushort.MaxValue) |
|||
{ |
|||
return CultureInfo.InvariantCulture; |
|||
} |
|||
|
|||
try |
|||
{ |
|||
return CultureInfo.GetCultureInfo(lcid) ?? CultureInfo.InvariantCulture; |
|||
} |
|||
catch (CultureNotFoundException) |
|||
{ |
|||
return CultureInfo.InvariantCulture; |
|||
} |
|||
} |
|||
} |
|||
|
|||
public string TypographicFamilyName { get; } |
|||
|
|||
public IReadOnlyDictionary<CultureInfo, string> FamilyNames { get; } |
|||
|
|||
public IReadOnlyDictionary<CultureInfo, string> FaceNames { get; } |
|||
|
|||
public IReadOnlyList<OpenTypeTag> SupportedFeatures |
|||
{ |
|||
get |
|||
{ |
|||
if (_supportedFeatures != null) |
|||
{ |
|||
return _supportedFeatures; |
|||
} |
|||
|
|||
var gPosFeatures = FeatureListTable.LoadGPos(this); |
|||
var gSubFeatures = FeatureListTable.LoadGSub(this); |
|||
|
|||
var supportedFeatures = new List<OpenTypeTag>(gPosFeatures?.Features.Count ?? 0 + gSubFeatures?.Features.Count ?? 0); |
|||
|
|||
if (gPosFeatures != null) |
|||
{ |
|||
foreach (var gPosFeature in gPosFeatures.Features) |
|||
{ |
|||
if (supportedFeatures.Contains(gPosFeature)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
supportedFeatures.Add(gPosFeature); |
|||
} |
|||
} |
|||
|
|||
if (gSubFeatures != null) |
|||
{ |
|||
foreach (var gSubFeature in gSubFeatures.Features) |
|||
{ |
|||
if (supportedFeatures.Contains(gSubFeature)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
supportedFeatures.Add(gSubFeature); |
|||
} |
|||
} |
|||
|
|||
_supportedFeatures = supportedFeatures; |
|||
|
|||
return supportedFeatures; |
|||
} |
|||
} |
|||
|
|||
public FontSimulations FontSimulations { get; } |
|||
|
|||
public int ReplacementCodepoint { get; } |
|||
|
|||
public FontMetrics Metrics { get; } |
|||
|
|||
public uint GlyphCount { get; } |
|||
|
|||
public string FamilyName { get; } |
|||
|
|||
public FontWeight Weight { get; } |
|||
|
|||
public FontStyle Style { get; } |
|||
|
|||
public FontStretch Stretch { get; } |
|||
|
|||
public IReadOnlyDictionary<int, ushort> CharacterToGlyphMap => _cmapTable; |
|||
|
|||
public IPlatformTypeface PlatformTypeface { get; } |
|||
|
|||
public ITextShaperTypeface TextShaperTypeface |
|||
{ |
|||
get |
|||
{ |
|||
if (_textShaperTypeface != null) |
|||
{ |
|||
return _textShaperTypeface; |
|||
} |
|||
|
|||
var textShaper = AvaloniaLocator.Current.GetRequiredService<ITextShaperImpl>(); |
|||
|
|||
_textShaperTypeface = textShaper.CreateTypeface(this); |
|||
|
|||
return _textShaperTypeface; |
|||
} |
|||
} |
|||
|
|||
private static FontStyle GetFontStyle(OS2Table oS2Table, HeadTable headTable, PostTable postTable) |
|||
{ |
|||
var isItalic = (oS2Table.Selection & OS2Table.FontSelectionFlags.ITALIC) != 0 || (headTable.MacStyle & 0x02) != 0; |
|||
|
|||
var isOblique = (oS2Table.Selection & OS2Table.FontSelectionFlags.OBLIQUE) != 0; |
|||
|
|||
var italicAngle = postTable.ItalicAngle; |
|||
|
|||
if (isOblique) |
|||
{ |
|||
return FontStyle.Oblique; |
|||
} |
|||
|
|||
if (Math.Abs(italicAngle) > 0.01f && !isItalic) |
|||
{ |
|||
return FontStyle.Oblique; |
|||
} |
|||
|
|||
if (isItalic) |
|||
{ |
|||
return FontStyle.Italic; |
|||
} |
|||
|
|||
return FontStyle.Normal; |
|||
} |
|||
|
|||
private void Dispose(bool disposing) |
|||
{ |
|||
if (_isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_isDisposed = true; |
|||
|
|||
if (!disposing) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
PlatformTypeface.Dispose(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Dispose(true); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
public ushort GetGlyphAdvance(ushort glyphId) |
|||
{ |
|||
if (_hmTable is null) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
return _hmTable.GetAdvance(glyphId); |
|||
} |
|||
|
|||
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) |
|||
{ |
|||
metrics = default; |
|||
|
|||
HorizontalGlyphMetric hMetric = default; |
|||
VerticalGlyphMetric vMetric = default; |
|||
|
|||
if (_hmTable != null) |
|||
{ |
|||
hMetric = _hmTable.GetMetrics(glyph); |
|||
} |
|||
|
|||
if (_vmTable != null) |
|||
{ |
|||
vMetric = _vmTable.GetMetrics(glyph); |
|||
} |
|||
|
|||
if (hMetric.Equals(default) && vMetric.Equals(default)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
metrics = new GlyphMetrics |
|||
{ |
|||
XBearing = hMetric.LeftSideBearing, |
|||
YBearing = vMetric.TopSideBearing, |
|||
Width = hMetric.AdvanceWidth, |
|||
Height = vMetric.AdvanceHeight |
|||
}; |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
using System.Collections.Generic; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
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 typographic family name.
|
|||
/// </summary>
|
|||
string TypographicFamilyName { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the localized family names.
|
|||
/// <para>Keys are culture identifiers.</para>
|
|||
/// </summary>
|
|||
IReadOnlyDictionary<ushort, string> FamilyNames { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets supported font features.
|
|||
/// </summary>
|
|||
IReadOnlyList<OpenTypeTag> SupportedFeatures { get; } |
|||
|
|||
/// <summary>
|
|||
/// Gets the localized face names.
|
|||
/// <para>Keys are culture identifiers.</para>
|
|||
/// </summary>
|
|||
IReadOnlyDictionary<ushort, string> FaceNames { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
<PropertyGroup> |
|||
<TargetFrameworks>$(AvsCurrentTargetFramework);$(AvsLegacyTargetFrameworks);netstandard2.0</TargetFrameworks> |
|||
<IncludeLinuxSkia>true</IncludeLinuxSkia> |
|||
<IncludeWasmSkia>true</IncludeWasmSkia> |
|||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
|||
<!-- No obsolete code usage --> |
|||
<WarningsAsErrors>$(WarningsAsErrors);CS0618</WarningsAsErrors> |
|||
</PropertyGroup> |
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
<Import Project="..\..\..\build\HarfBuzzSharp.props" /> |
|||
<Import Project="..\..\..\build\DevAnalyzers.props" /> |
|||
<Import Project="..\..\..\build\TrimmingEnable.props" /> |
|||
<Import Project="..\..\..\build\NullableEnable.props" /> |
|||
|
|||
<ItemGroup Label="InternalsVisibleTo"> |
|||
<InternalsVisibleTo Include="Avalonia.Benchmarks, PublicKey=$(AvaloniaPublicKey)" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
@ -0,0 +1,27 @@ |
|||
using Avalonia.Harfbuzz; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
|
|||
/// <summary>
|
|||
/// Configures the application to use HarfBuzz for text shaping.
|
|||
/// </summary>
|
|||
/// <remarks>This method adds a HarfBuzz-based text shaper implementation to the application, enabling
|
|||
/// advanced text shaping capabilities.</remarks>
|
|||
public static class HarfBuzzApplicationExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Configures the application to use HarfBuzz for text shaping.
|
|||
/// </summary>
|
|||
/// <remarks>This method integrates HarfBuzz, a text shaping engine, into the application,
|
|||
/// enabling advanced text layout and rendering capabilities.</remarks>
|
|||
/// <param name="builder">The <see cref="AppBuilder"/> instance to configure.</param>
|
|||
/// <returns>The configured <see cref="AppBuilder"/> instance.</returns>
|
|||
public static AppBuilder UseHarfBuzz(this AppBuilder builder) |
|||
{ |
|||
return builder.With<ITextShaperImpl>(new HarfBuzzTextShaper()); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,67 @@ |
|||
using System; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Media; |
|||
using HarfBuzzSharp; |
|||
|
|||
namespace Avalonia.Harfbuzz |
|||
{ |
|||
internal class HarfBuzzTypeface : ITextShaperTypeface |
|||
{ |
|||
public HarfBuzzTypeface(IGlyphTypeface glyphTypeface) |
|||
{ |
|||
GlyphTypeface = glyphTypeface; |
|||
|
|||
HBFace = new Face(GetTable) { UnitsPerEm = glyphTypeface.Metrics.DesignEmHeight }; |
|||
|
|||
HBFont = new Font(HBFace); |
|||
|
|||
HBFont.SetFunctionsOpenType(); |
|||
} |
|||
|
|||
public IGlyphTypeface GlyphTypeface { get; } |
|||
public Face HBFace { get; } |
|||
public Font HBFont { get; } |
|||
|
|||
private Blob? GetTable(Face face, Tag tag) |
|||
{ |
|||
if (!GlyphTypeface.PlatformTypeface.TryGetTable((uint)tag, out var table)) |
|||
{ |
|||
return null; |
|||
} |
|||
|
|||
// If table is backed by managed array, pin it and avoid copy.
|
|||
if (MemoryMarshal.TryGetArray(table, out var seg)) |
|||
{ |
|||
var handle = GCHandle.Alloc(seg.Array!, GCHandleType.Pinned); |
|||
var basePtr = handle.AddrOfPinnedObject(); |
|||
var ptr = IntPtr.Add(basePtr, seg.Offset); |
|||
|
|||
var release = new ReleaseDelegate(() => handle.Free()); |
|||
|
|||
return new Blob(ptr, seg.Count, MemoryMode.ReadOnly, release); |
|||
} |
|||
|
|||
// Fallback: allocate native memory and copy
|
|||
var nativePtr = Marshal.AllocHGlobal(table.Length); |
|||
|
|||
unsafe |
|||
{ |
|||
fixed (byte* src = table.Span) |
|||
{ |
|||
System.Buffer.MemoryCopy(src, (void*)nativePtr, table.Length, table.Length); |
|||
} |
|||
} |
|||
|
|||
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeHGlobal(nativePtr)); |
|||
|
|||
return new Blob(nativePtr, table.Length, MemoryMode.ReadOnly, releaseDelegate); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
HBFont.Dispose(); |
|||
HBFace.Dispose(); |
|||
} |
|||
|
|||
} |
|||
} |
|||
@ -1,393 +0,0 @@ |
|||
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 : IGlyphTypeface2 |
|||
{ |
|||
private bool _isDisposed; |
|||
private readonly NameTable? _nameTable; |
|||
private readonly OS2Table? _os2Table; |
|||
private readonly HorizontalHeadTable? _hhTable; |
|||
private IReadOnlyList<OpenTypeTag>? _supportedFeatures; |
|||
|
|||
public GlyphTypefaceImpl(SKTypeface typeface, FontSimulations fontSimulations) |
|||
{ |
|||
SKTypeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); |
|||
|
|||
Face = new Face(GetTable) { UnitsPerEm = typeface.UnitsPerEm }; |
|||
|
|||
Font = new Font(Face); |
|||
|
|||
Font.SetFunctionsOpenType(); |
|||
|
|||
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlineOffset); |
|||
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineSize); |
|||
|
|||
_os2Table = OS2Table.Load(this); |
|||
_hhTable = HorizontalHeadTable.Load(this); |
|||
|
|||
var ascent = 0; |
|||
var descent = 0; |
|||
var lineGap = 0; |
|||
|
|||
if (_os2Table != null && (_os2Table.FontStyle & OS2Table.FontStyleSelection.USE_TYPO_METRICS) != 0) |
|||
{ |
|||
ascent = -_os2Table.TypoAscender; |
|||
descent = -_os2Table.TypoDescender; |
|||
lineGap = _os2Table.TypoLineGap; |
|||
} |
|||
else |
|||
{ |
|||
if (_hhTable != null) |
|||
{ |
|||
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, |
|||
LineGap = lineGap, |
|||
UnderlinePosition = -underlineOffset, |
|||
UnderlineThickness = underlineSize, |
|||
StrikethroughPosition = -_os2Table?.StrikeoutPosition ?? 0, |
|||
StrikethroughThickness = _os2Table?.StrikeoutSize ?? 0, |
|||
IsFixedPitch = typeface.IsFixedPitch |
|||
}; |
|||
|
|||
GlyphCount = typeface.GlyphCount; |
|||
|
|||
FontSimulations = fontSimulations; |
|||
|
|||
var fontWeight = _os2Table != null ? (FontWeight)_os2Table.WeightClass : FontWeight.Normal; |
|||
|
|||
Weight = (fontSimulations & FontSimulations.Bold) != 0 ? FontWeight.Bold : fontWeight; |
|||
|
|||
var style = _os2Table != null ? GetFontStyle(_os2Table.FontStyle) : FontStyle.Normal; |
|||
|
|||
if (typeface.FontStyle.Slant == SKFontStyleSlant.Oblique) |
|||
{ |
|||
style = FontStyle.Oblique; |
|||
} |
|||
|
|||
Style = (fontSimulations & FontSimulations.Oblique) != 0 ? FontStyle.Italic : style; |
|||
|
|||
var stretch = _os2Table != null ? (FontStretch)_os2Table.WidthClass : FontStretch.Normal; |
|||
|
|||
Stretch = stretch; |
|||
|
|||
_nameTable = NameTable.Load(this); |
|||
|
|||
//Rely on Skia if no name table is present
|
|||
FamilyName = _nameTable?.FontFamilyName((ushort)CultureInfo.InvariantCulture.LCID) ?? typeface.FamilyName; |
|||
|
|||
TypographicFamilyName = _nameTable?.GetNameById((ushort)CultureInfo.InvariantCulture.LCID, KnownNameIds.TypographicFamilyName) ?? FamilyName; |
|||
|
|||
if(_nameTable != null) |
|||
{ |
|||
var familyNames = new Dictionary<ushort, string>(1); |
|||
var faceNames = new Dictionary<ushort, string>(1); |
|||
|
|||
foreach (var nameRecord in _nameTable) |
|||
{ |
|||
if(nameRecord.NameID == KnownNameIds.FontFamilyName) |
|||
{ |
|||
if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (!familyNames.ContainsKey(nameRecord.LanguageID)) |
|||
{ |
|||
familyNames[nameRecord.LanguageID] = nameRecord.Value; |
|||
} |
|||
} |
|||
|
|||
if(nameRecord.NameID == KnownNameIds.FontSubfamilyName) |
|||
{ |
|||
if (nameRecord.Platform != PlatformIDs.Windows || nameRecord.LanguageID == 0) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
if (!faceNames.ContainsKey(nameRecord.LanguageID)) |
|||
{ |
|||
faceNames[nameRecord.LanguageID] = nameRecord.Value; |
|||
} |
|||
} |
|||
} |
|||
|
|||
FamilyNames = familyNames; |
|||
FaceNames = faceNames; |
|||
} |
|||
else |
|||
{ |
|||
FamilyNames = new Dictionary<ushort, string> { { (ushort)CultureInfo.InvariantCulture.LCID, FamilyName } }; |
|||
FaceNames = new Dictionary<ushort, string> { { (ushort)CultureInfo.InvariantCulture.LCID, Weight.ToString() } }; |
|||
} |
|||
} |
|||
|
|||
public string TypographicFamilyName { get; } |
|||
|
|||
public IReadOnlyDictionary<ushort, string> FamilyNames { get; } |
|||
|
|||
public IReadOnlyDictionary<ushort, string> FaceNames { get; } |
|||
|
|||
public IReadOnlyList<OpenTypeTag> SupportedFeatures |
|||
{ |
|||
get |
|||
{ |
|||
if (_supportedFeatures != null) |
|||
{ |
|||
return _supportedFeatures; |
|||
} |
|||
|
|||
var gPosFeatures = FeatureListTable.LoadGPos(this); |
|||
var gSubFeatures = FeatureListTable.LoadGSub(this); |
|||
|
|||
var supportedFeatures = new List<OpenTypeTag>(gPosFeatures?.Features.Count ?? 0 + gSubFeatures?.Features.Count ?? 0); |
|||
|
|||
if (gPosFeatures != null) |
|||
{ |
|||
foreach (var gPosFeature in gPosFeatures.Features) |
|||
{ |
|||
if (supportedFeatures.Contains(gPosFeature)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
supportedFeatures.Add(gPosFeature); |
|||
} |
|||
} |
|||
|
|||
if (gSubFeatures != null) |
|||
{ |
|||
foreach (var gSubFeature in gSubFeatures.Features) |
|||
{ |
|||
if (supportedFeatures.Contains(gSubFeature)) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
supportedFeatures.Add(gSubFeature); |
|||
} |
|||
} |
|||
|
|||
_supportedFeatures = supportedFeatures; |
|||
|
|||
return supportedFeatures; |
|||
} |
|||
} |
|||
|
|||
public SKTypeface SKTypeface { get; } |
|||
|
|||
public Face Face { get; } |
|||
|
|||
public Font Font { get; } |
|||
|
|||
public FontSimulations FontSimulations { get; } |
|||
|
|||
public int ReplacementCodepoint { get; } |
|||
|
|||
public FontMetrics Metrics { get; } |
|||
|
|||
public int GlyphCount { get; } |
|||
|
|||
public string FamilyName { get; } |
|||
|
|||
public FontWeight Weight { get; } |
|||
|
|||
public FontStyle Style { get; } |
|||
|
|||
public FontStretch Stretch { get; } |
|||
|
|||
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) |
|||
{ |
|||
metrics = default; |
|||
|
|||
if (!Font.TryGetGlyphExtents(glyph, out var extents)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
metrics = new GlyphMetrics |
|||
{ |
|||
XBearing = extents.XBearing, |
|||
YBearing = extents.YBearing, |
|||
Width = extents.Width, |
|||
Height = extents.Height |
|||
}; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <inheritdoc cref="IGlyphTypeface"/>
|
|||
public ushort GetGlyph(uint codepoint) |
|||
{ |
|||
if (Font.TryGetGlyph(codepoint, out var glyph)) |
|||
{ |
|||
return (ushort)glyph; |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
public bool TryGetGlyph(uint codepoint, out ushort glyph) |
|||
{ |
|||
glyph = GetGlyph(codepoint); |
|||
|
|||
return glyph != 0; |
|||
} |
|||
|
|||
/// <inheritdoc cref="IGlyphTypeface"/>
|
|||
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) |
|||
{ |
|||
var glyphs = new ushort[codepoints.Length]; |
|||
|
|||
for (var i = 0; i < codepoints.Length; i++) |
|||
{ |
|||
if (Font.TryGetGlyph(codepoints[i], out var glyph)) |
|||
{ |
|||
glyphs[i] = (ushort)glyph; |
|||
} |
|||
} |
|||
|
|||
return glyphs; |
|||
} |
|||
|
|||
/// <inheritdoc cref="IGlyphTypeface"/>
|
|||
public int GetGlyphAdvance(ushort glyph) |
|||
{ |
|||
return Font.GetHorizontalGlyphAdvance(glyph); |
|||
} |
|||
|
|||
/// <inheritdoc cref="IGlyphTypeface"/>
|
|||
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) |
|||
{ |
|||
var glyphIndices = new uint[glyphs.Length]; |
|||
|
|||
for (var i = 0; i < glyphs.Length; i++) |
|||
{ |
|||
glyphIndices[i] = glyphs[i]; |
|||
} |
|||
|
|||
return Font.GetHorizontalGlyphAdvances(glyphIndices); |
|||
} |
|||
|
|||
private static FontStyle GetFontStyle(OS2Table.FontStyleSelection styleSelection) |
|||
{ |
|||
if ((styleSelection & OS2Table.FontStyleSelection.ITALIC) != 0) |
|||
{ |
|||
return FontStyle.Italic; |
|||
} |
|||
|
|||
if ((styleSelection & OS2Table.FontStyleSelection.OBLIQUE) != 0) |
|||
{ |
|||
return FontStyle.Oblique; |
|||
} |
|||
|
|||
return FontStyle.Normal; |
|||
} |
|||
|
|||
private Blob? GetTable(Face face, Tag tag) |
|||
{ |
|||
var size = SKTypeface.GetTableSize(tag); |
|||
|
|||
var data = Marshal.AllocCoTaskMem(size); |
|||
|
|||
var releaseDelegate = new ReleaseDelegate(() => Marshal.FreeCoTaskMem(data)); |
|||
|
|||
return SKTypeface.TryGetTableData(tag, 0, size, data) ? |
|||
new Blob(data, size, MemoryMode.ReadOnly, releaseDelegate) : null; |
|||
} |
|||
|
|||
public SKFont CreateSKFont(float size) |
|||
=> new(SKTypeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f) |
|||
{ |
|||
LinearMetrics = true, |
|||
Embolden = (FontSimulations & FontSimulations.Bold) != 0 |
|||
}; |
|||
|
|||
private void Dispose(bool disposing) |
|||
{ |
|||
if (_isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_isDisposed = true; |
|||
|
|||
if (!disposing) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Font.Dispose(); |
|||
Face.Dispose(); |
|||
SKTypeface.Dispose(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Dispose(true); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
public bool TryGetTable(uint tag, out byte[] table) |
|||
{ |
|||
return SKTypeface.TryGetTableData(tag, out table); |
|||
} |
|||
|
|||
public bool TryGetStream([NotNullWhen(true)] out Stream? stream) |
|||
{ |
|||
try |
|||
{ |
|||
var asset = SKTypeface.OpenStream(); |
|||
var size = asset.Length; |
|||
var buffer = new byte[size]; |
|||
|
|||
asset.Read(buffer, size); |
|||
|
|||
stream = new MemoryStream(buffer); |
|||
|
|||
return true; |
|||
} |
|||
catch |
|||
{ |
|||
stream = null; |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts; |
|||
using SkiaSharp; |
|||
|
|||
namespace Avalonia.Skia |
|||
{ |
|||
internal class SkiaTypeface : IPlatformTypeface |
|||
{ |
|||
public SkiaTypeface(SKTypeface typeface, FontSimulations fontSimulations) |
|||
{ |
|||
SKTypeface = typeface ?? throw new ArgumentNullException(nameof(typeface)); |
|||
FontSimulations = fontSimulations; |
|||
Weight = (FontWeight)typeface.FontWeight; |
|||
Style = typeface.FontStyle.Slant.ToAvalonia(); |
|||
Stretch = (FontStretch)typeface.FontWidth; |
|||
} |
|||
|
|||
public SKTypeface SKTypeface { get; } |
|||
|
|||
public FontSimulations FontSimulations { get; } |
|||
|
|||
public FontWeight Weight { get; } |
|||
|
|||
public FontStyle Style { get; } |
|||
|
|||
public FontStretch Stretch { get; } |
|||
|
|||
public SKFont CreateSKFont(float size) |
|||
{ |
|||
return new(SKTypeface, size, skewX: (FontSimulations & FontSimulations.Oblique) != 0 ? -0.3f : 0.0f) |
|||
{ |
|||
LinearMetrics = true, |
|||
Embolden = (FontSimulations & FontSimulations.Bold) != 0 |
|||
}; |
|||
} |
|||
|
|||
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table) |
|||
{ |
|||
table = default; |
|||
|
|||
if (SKTypeface.TryGetTableData(tag, out var data)) |
|||
{ |
|||
table = data; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public bool TryGetStream([NotNullWhen(true)] out Stream? stream) |
|||
{ |
|||
try |
|||
{ |
|||
var asset = SKTypeface.OpenStream(); |
|||
var size = asset.Length; |
|||
var buffer = new byte[size]; |
|||
|
|||
asset.Read(buffer, size); |
|||
|
|||
stream = new MemoryStream(buffer); |
|||
|
|||
return true; |
|||
} |
|||
catch |
|||
{ |
|||
stream = null; |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
SKTypeface.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,111 @@ |
|||
using System; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts; |
|||
using HarfBuzzSharp; |
|||
using SharpDX.DirectWrite; |
|||
|
|||
namespace Avalonia.Direct2D1.Media |
|||
{ |
|||
internal class DWriteTypeface : IPlatformTypeface |
|||
{ |
|||
private bool _isDisposed; |
|||
|
|||
public DWriteTypeface(SharpDX.DirectWrite.Font font) |
|||
{ |
|||
DWFont = font; |
|||
|
|||
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>(); |
|||
|
|||
Weight = (Avalonia.Media.FontWeight)DWFont.Weight; |
|||
|
|||
Style = (Avalonia.Media.FontStyle)DWFont.Style; |
|||
|
|||
Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch; |
|||
} |
|||
|
|||
private static uint SwapBytes(uint x) |
|||
{ |
|||
x = (x >> 16) | (x << 16); |
|||
|
|||
return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8); |
|||
} |
|||
|
|||
public SharpDX.DirectWrite.Font DWFont { get; } |
|||
|
|||
public FontFace1 FontFace { get; } |
|||
|
|||
public Face Face { get; } |
|||
|
|||
public HarfBuzzSharp.Font Font { get; } |
|||
|
|||
public Avalonia.Media.FontWeight Weight { get; } |
|||
|
|||
public Avalonia.Media.FontStyle Style { get; } |
|||
|
|||
public Avalonia.Media.FontStretch Stretch { get; } |
|||
|
|||
private void Dispose(bool disposing) |
|||
{ |
|||
if (_isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_isDisposed = true; |
|||
|
|||
if (!disposing) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Font?.Dispose(); |
|||
Face?.Dispose(); |
|||
FontFace?.Dispose(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Dispose(true); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table) |
|||
{ |
|||
table = default; |
|||
|
|||
var dwTag = (int)SwapBytes((uint)tag); |
|||
|
|||
if (FontFace.TryGetFontTable(dwTag, out var tableData, out _)) |
|||
{ |
|||
table = tableData.ToArray(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
|
|||
public bool TryGetStream([NotNullWhen(true)] out Stream stream) |
|||
{ |
|||
stream = default; |
|||
|
|||
var files = FontFace.GetFiles(); |
|||
|
|||
if (files.Length > 0) |
|||
{ |
|||
var file = files[0]; |
|||
|
|||
var referenceKey = file.GetReferenceKey(); |
|||
|
|||
stream = referenceKey.ToDataStream(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,216 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Media; |
|||
using HarfBuzzSharp; |
|||
using SharpDX.DirectWrite; |
|||
using FontMetrics = Avalonia.Media.FontMetrics; |
|||
using FontSimulations = Avalonia.Media.FontSimulations; |
|||
using GlyphMetrics = Avalonia.Media.GlyphMetrics; |
|||
|
|||
namespace Avalonia.Direct2D1.Media |
|||
{ |
|||
internal class GlyphTypefaceImpl : IGlyphTypeface |
|||
{ |
|||
private bool _isDisposed; |
|||
|
|||
public GlyphTypefaceImpl(SharpDX.DirectWrite.Font font) |
|||
{ |
|||
DWFont = font; |
|||
|
|||
FontFace = new FontFace(DWFont).QueryInterface<FontFace1>(); |
|||
|
|||
Face = new Face(GetTable); |
|||
|
|||
Font = new HarfBuzzSharp.Font(Face); |
|||
|
|||
Font.SetFunctionsOpenType(); |
|||
|
|||
Font.GetScale(out var xScale, out _); |
|||
|
|||
if (!Font.TryGetHorizontalFontExtents(out var fontExtents)) |
|||
{ |
|||
Font.TryGetVerticalFontExtents(out fontExtents); |
|||
} |
|||
|
|||
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineOffset, out var underlinePosition); |
|||
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.UnderlineSize, out var underlineThickness); |
|||
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutOffset, out var strikethroughPosition); |
|||
Font.OpenTypeMetrics.TryGetPosition(OpenTypeMetricsTag.StrikeoutSize, out var strikethroughThickness); |
|||
|
|||
Metrics = new FontMetrics |
|||
{ |
|||
DesignEmHeight = (short)xScale, |
|||
Ascent = -fontExtents.Ascender, |
|||
Descent = -fontExtents.Descender, |
|||
LineGap = fontExtents.LineGap, |
|||
UnderlinePosition = underlinePosition, |
|||
UnderlineThickness = underlineThickness, |
|||
StrikethroughPosition = strikethroughPosition, |
|||
StrikethroughThickness = strikethroughThickness, |
|||
IsFixedPitch = FontFace.IsMonospacedFont |
|||
}; |
|||
|
|||
FamilyName = DWFont.FontFamily.FamilyNames.GetString(0); |
|||
|
|||
Weight = (Avalonia.Media.FontWeight)DWFont.Weight; |
|||
|
|||
Style = (Avalonia.Media.FontStyle)DWFont.Style; |
|||
|
|||
Stretch = (Avalonia.Media.FontStretch)DWFont.Stretch; |
|||
} |
|||
|
|||
private Blob GetTable(Face face, Tag tag) |
|||
{ |
|||
var dwTag = (int)SwapBytes(tag); |
|||
|
|||
if (FontFace.TryGetFontTable(dwTag, out var tableData, out _)) |
|||
{ |
|||
return new Blob(tableData.Pointer, tableData.Size, MemoryMode.ReadOnly, () => { }); |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
private static uint SwapBytes(uint x) |
|||
{ |
|||
x = (x >> 16) | (x << 16); |
|||
|
|||
return ((x & 0xFF00FF00) >> 8) | ((x & 0x00FF00FF) << 8); |
|||
} |
|||
|
|||
public SharpDX.DirectWrite.Font DWFont { get; } |
|||
|
|||
public FontFace1 FontFace { get; } |
|||
|
|||
public Face Face { get; } |
|||
|
|||
public HarfBuzzSharp.Font Font { get; } |
|||
|
|||
public FontMetrics Metrics { get; } |
|||
|
|||
public int GlyphCount { get; set; } |
|||
|
|||
public FontSimulations FontSimulations => FontSimulations.None; |
|||
|
|||
public string FamilyName { get; } |
|||
|
|||
public Avalonia.Media.FontWeight Weight { get; } |
|||
|
|||
public Avalonia.Media.FontStyle Style { get; } |
|||
|
|||
public Avalonia.Media.FontStretch Stretch { get; } |
|||
|
|||
/// <inheritdoc cref="IGlyphTypeface"/>
|
|||
public ushort GetGlyph(uint codepoint) |
|||
{ |
|||
if (Font.TryGetGlyph(codepoint, out var glyph)) |
|||
{ |
|||
return (ushort)glyph; |
|||
} |
|||
|
|||
return 0; |
|||
} |
|||
|
|||
public bool TryGetGlyph(uint codepoint, out ushort glyph) |
|||
{ |
|||
glyph = GetGlyph(codepoint); |
|||
|
|||
return glyph != 0; |
|||
} |
|||
|
|||
/// <inheritdoc cref="IGlyphTypeface"/>
|
|||
public ushort[] GetGlyphs(ReadOnlySpan<uint> codepoints) |
|||
{ |
|||
var glyphs = new ushort[codepoints.Length]; |
|||
|
|||
for (var i = 0; i < codepoints.Length; i++) |
|||
{ |
|||
if (Font.TryGetGlyph(codepoints[i], out var glyph)) |
|||
{ |
|||
glyphs[i] = (ushort)glyph; |
|||
} |
|||
} |
|||
|
|||
return glyphs; |
|||
} |
|||
|
|||
/// <inheritdoc cref="IGlyphTypeface"/>
|
|||
public int GetGlyphAdvance(ushort glyph) |
|||
{ |
|||
return Font.GetHorizontalGlyphAdvance(glyph); |
|||
} |
|||
|
|||
/// <inheritdoc cref="IGlyphTypeface"/>
|
|||
public int[] GetGlyphAdvances(ReadOnlySpan<ushort> glyphs) |
|||
{ |
|||
var glyphIndices = new uint[glyphs.Length]; |
|||
|
|||
for (var i = 0; i < glyphs.Length; i++) |
|||
{ |
|||
glyphIndices[i] = glyphs[i]; |
|||
} |
|||
|
|||
return Font.GetHorizontalGlyphAdvances(glyphIndices); |
|||
} |
|||
|
|||
public bool TryGetGlyphMetrics(ushort glyph, out GlyphMetrics metrics) |
|||
{ |
|||
metrics = default; |
|||
|
|||
if (!Font.TryGetGlyphExtents(glyph, out var extents)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
metrics = new GlyphMetrics |
|||
{ |
|||
XBearing = extents.XBearing, |
|||
YBearing = extents.YBearing, |
|||
Width = extents.Width, |
|||
Height = extents.Height |
|||
}; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private void Dispose(bool disposing) |
|||
{ |
|||
if (_isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
_isDisposed = true; |
|||
|
|||
if (!disposing) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
Font?.Dispose(); |
|||
Face?.Dispose(); |
|||
FontFace?.Dispose(); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
Dispose(true); |
|||
GC.SuppressFinalize(this); |
|||
} |
|||
|
|||
public bool TryGetTable(uint tag, out byte[] table) |
|||
{ |
|||
table = null; |
|||
var blob = Face.ReferenceTable(tag); |
|||
|
|||
if (blob.Length > 0) |
|||
{ |
|||
table = blob.AsSpan().ToArray(); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -1,204 +0,0 @@ |
|||
using System; |
|||
using System.Buffers; |
|||
using System.Collections.Concurrent; |
|||
using System.Globalization; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Media.TextFormatting; |
|||
using Avalonia.Media.TextFormatting.Unicode; |
|||
using Avalonia.Platform; |
|||
using HarfBuzzSharp; |
|||
using Buffer = HarfBuzzSharp.Buffer; |
|||
using GlyphInfo = HarfBuzzSharp.GlyphInfo; |
|||
|
|||
namespace Avalonia.Direct2D1.Media |
|||
{ |
|||
internal class TextShaperImpl : ITextShaperImpl |
|||
{ |
|||
private static readonly ConcurrentDictionary<int, Language> s_cachedLanguage = new(); |
|||
|
|||
public ShapedBuffer ShapeText(ReadOnlyMemory<char> text, TextShaperOptions options) |
|||
{ |
|||
var textSpan = text.Span; |
|||
var typeface = options.Typeface; |
|||
var fontRenderingEmSize = options.FontRenderingEmSize; |
|||
var bidiLevel = options.BidiLevel; |
|||
var culture = options.Culture; |
|||
|
|||
using (var buffer = new Buffer()) |
|||
{ |
|||
// HarfBuzz needs the surrounding characters to correctly shape the text
|
|||
var containingText = GetContainingMemory(text, out var start, out var length).Span; |
|||
buffer.AddUtf16(containingText, start, length); |
|||
|
|||
MergeBreakPair(buffer); |
|||
|
|||
buffer.GuessSegmentProperties(); |
|||
|
|||
buffer.Direction = (bidiLevel & 1) == 0 ? Direction.LeftToRight : Direction.RightToLeft; |
|||
|
|||
var usedCulture = culture ?? CultureInfo.CurrentCulture; |
|||
|
|||
buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); |
|||
|
|||
var font = ((GlyphTypefaceImpl)typeface).Font; |
|||
|
|||
font.Shape(buffer, GetFeatures(options)); |
|||
|
|||
if (buffer.Direction == Direction.RightToLeft) |
|||
{ |
|||
buffer.Reverse(); |
|||
} |
|||
|
|||
font.GetScale(out var scaleX, out _); |
|||
|
|||
var textScale = fontRenderingEmSize / scaleX; |
|||
|
|||
var bufferLength = buffer.Length; |
|||
|
|||
var shapedBuffer = new ShapedBuffer(text, bufferLength, typeface, fontRenderingEmSize, bidiLevel); |
|||
|
|||
var glyphInfos = buffer.GetGlyphInfoSpan(); |
|||
|
|||
var glyphPositions = buffer.GetGlyphPositionSpan(); |
|||
|
|||
for (var i = 0; i < bufferLength; i++) |
|||
{ |
|||
var sourceInfo = glyphInfos[i]; |
|||
|
|||
var glyphIndex = (ushort)sourceInfo.Codepoint; |
|||
|
|||
var glyphCluster = (int)(sourceInfo.Cluster); |
|||
|
|||
var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale) + options.LetterSpacing; |
|||
|
|||
var glyphOffset = GetGlyphOffset(glyphPositions, i, textScale); |
|||
|
|||
if (glyphCluster < containingText.Length && containingText[glyphCluster] == '\t') |
|||
{ |
|||
glyphIndex = typeface.GetGlyph(' '); |
|||
|
|||
glyphAdvance = options.IncrementalTabWidth > 0 ? |
|||
options.IncrementalTabWidth : |
|||
4 * typeface.GetGlyphAdvance(glyphIndex) * textScale; |
|||
} |
|||
|
|||
shapedBuffer[i] = new Avalonia.Media.TextFormatting.GlyphInfo(glyphIndex, glyphCluster, glyphAdvance, glyphOffset); |
|||
} |
|||
|
|||
return shapedBuffer; |
|||
} |
|||
} |
|||
|
|||
private static void MergeBreakPair(Buffer buffer) |
|||
{ |
|||
var length = buffer.Length; |
|||
|
|||
var glyphInfos = buffer.GetGlyphInfoSpan(); |
|||
|
|||
var second = glyphInfos[length - 1]; |
|||
|
|||
if (!new Codepoint(second.Codepoint).IsBreakChar) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (length > 1 && glyphInfos[length - 2].Codepoint == '\r' && second.Codepoint == '\n') |
|||
{ |
|||
var first = glyphInfos[length - 2]; |
|||
|
|||
first.Codepoint = '\u200C'; |
|||
second.Codepoint = '\u200C'; |
|||
second.Cluster = first.Cluster; |
|||
|
|||
unsafe |
|||
{ |
|||
fixed (GlyphInfo* p = &glyphInfos[length - 2]) |
|||
{ |
|||
*p = first; |
|||
} |
|||
|
|||
fixed (GlyphInfo* p = &glyphInfos[length - 1]) |
|||
{ |
|||
*p = second; |
|||
} |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
second.Codepoint = '\u200C'; |
|||
|
|||
unsafe |
|||
{ |
|||
fixed (GlyphInfo* p = &glyphInfos[length - 1]) |
|||
{ |
|||
*p = second; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static Vector GetGlyphOffset(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale) |
|||
{ |
|||
var position = glyphPositions[index]; |
|||
|
|||
var offsetX = position.XOffset * textScale; |
|||
|
|||
var offsetY = -position.YOffset * textScale; |
|||
|
|||
return new Vector(offsetX, offsetY); |
|||
} |
|||
|
|||
private static double GetGlyphAdvance(ReadOnlySpan<GlyphPosition> glyphPositions, int index, double textScale) |
|||
{ |
|||
// Depends on direction of layout
|
|||
// glyphPositions[index].YAdvance * textScale;
|
|||
return glyphPositions[index].XAdvance * textScale; |
|||
} |
|||
|
|||
private static ReadOnlyMemory<char> GetContainingMemory(ReadOnlyMemory<char> memory, out int start, out int length) |
|||
{ |
|||
if (MemoryMarshal.TryGetString(memory, out var containingString, out start, out length)) |
|||
{ |
|||
return containingString.AsMemory(); |
|||
} |
|||
|
|||
if (MemoryMarshal.TryGetArray(memory, out var segment)) |
|||
{ |
|||
start = segment.Offset; |
|||
length = segment.Count; |
|||
return segment.Array.AsMemory(); |
|||
} |
|||
|
|||
if (MemoryMarshal.TryGetMemoryManager(memory, out MemoryManager<char> memoryManager, out start, out length)) |
|||
{ |
|||
return memoryManager.Memory; |
|||
} |
|||
|
|||
// should never happen
|
|||
throw new InvalidOperationException("Memory not backed by string, array or manager"); |
|||
} |
|||
|
|||
private static Feature[] GetFeatures(TextShaperOptions options) |
|||
{ |
|||
if (options.FontFeatures is null || options.FontFeatures.Count == 0) |
|||
{ |
|||
return Array.Empty<Feature>(); |
|||
} |
|||
|
|||
var features = new Feature[options.FontFeatures.Count]; |
|||
|
|||
for (var i = 0; i < options.FontFeatures.Count; i++) |
|||
{ |
|||
var fontFeature = options.FontFeatures[i]; |
|||
|
|||
features[i] = new Feature( |
|||
Tag.Parse(fontFeature.Tag), |
|||
(uint)fontFeature.Value, |
|||
(uint)fontFeature.Start, |
|||
(uint)fontFeature.End); |
|||
} |
|||
|
|||
return features; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,185 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Linq; |
|||
using System.Runtime.InteropServices; |
|||
using Avalonia.Media.Fonts; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Media.Fonts |
|||
{ |
|||
public class UnmanagedFontMemoryTests |
|||
{ |
|||
private static byte[] BuildFont(OpenTypeTag tag, byte[] tableData) |
|||
{ |
|||
const int recordsStart = 12; |
|||
const int numTables = 1; |
|||
var directoryBytes = recordsStart + numTables * 16; // 12 + 16 = 28
|
|||
var offset = directoryBytes; |
|||
var result = new byte[offset + tableData.Length]; |
|||
|
|||
// Simple SFNT header (version 0x00010000)
|
|||
result[0] = 0; |
|||
result[1] = 1; |
|||
result[2] = 0; |
|||
result[3] = 0; |
|||
// numTables (big-endian)
|
|||
result[4] = 0; |
|||
result[5] = 1; |
|||
// rest of header (6 bytes) left as zero
|
|||
|
|||
// Table record at offset 12
|
|||
uint v = tag; |
|||
result[12] = (byte)(v >> 24); |
|||
result[13] = (byte)(v >> 16); |
|||
result[14] = (byte)(v >> 8); |
|||
result[15] = (byte)v; |
|||
|
|||
// checksum (4 bytes) left as zero
|
|||
|
|||
// offset (big-endian) at bytes 20..23
|
|||
result[20] = (byte)(offset >> 24); |
|||
result[21] = (byte)(offset >> 16); |
|||
result[22] = (byte)(offset >> 8); |
|||
result[23] = (byte)offset; |
|||
|
|||
// length (big-endian) at bytes 24..27
|
|||
var len = tableData.Length; |
|||
result[24] = (byte)(len >> 24); |
|||
result[25] = (byte)(len >> 16); |
|||
result[26] = (byte)(len >> 8); |
|||
result[27] = (byte)len; |
|||
|
|||
Buffer.BlockCopy(tableData, 0, result, offset, len); |
|||
|
|||
return result; |
|||
} |
|||
|
|||
[Fact] |
|||
public unsafe void TryGetTable_ReturnsTableData_WhenExists() |
|||
{ |
|||
var tag = OpenTypeTag.Parse("test"); |
|||
var data = new byte[] { 1, 2, 3, 4, 5 }; |
|||
var font = BuildFont(tag, data); |
|||
|
|||
using var ms = new MemoryStream(font); |
|||
using var mem = UnmanagedFontMemory.LoadFromStream(ms); |
|||
|
|||
Assert.True(mem.TryGetTable(tag, out var table)); |
|||
Assert.Equal(data, table.ToArray()); |
|||
|
|||
// Second call should also succeed (cache path)
|
|||
Assert.True(mem.TryGetTable(tag, out var table2)); |
|||
Assert.Equal(table.Length, table2.Length); |
|||
|
|||
// Ensure both ReadOnlyMemory instances reference the same underlying memory
|
|||
ref byte r1 = ref MemoryMarshal.GetReference(table.Span); |
|||
ref byte r2 = ref MemoryMarshal.GetReference(table2.Span); |
|||
|
|||
fixed (byte* p1 = &r1) |
|||
fixed (byte* p2 = &r2) |
|||
{ |
|||
Assert.Equal((IntPtr)p1, (IntPtr)p2); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void TryGetTable_ReturnsFalse_ForUnknownTag() |
|||
{ |
|||
var tag = OpenTypeTag.Parse("TEST"); |
|||
var other = OpenTypeTag.Parse("OTHR"); |
|||
var data = new byte[] { 9, 8, 7 }; |
|||
var font = BuildFont(tag, data); |
|||
|
|||
using var ms = new MemoryStream(font); |
|||
using var mem = UnmanagedFontMemory.LoadFromStream(ms); |
|||
|
|||
Assert.False(mem.TryGetTable(other, out _)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void TryGetTable_ReturnsFalse_ForInvalidFont() |
|||
{ |
|||
// Too short to be a valid SFNT
|
|||
var shortData = new byte[8]; |
|||
|
|||
using var ms = new MemoryStream(shortData); |
|||
using var mem = UnmanagedFontMemory.LoadFromStream(ms); |
|||
|
|||
Assert.False(mem.TryGetTable(OpenTypeTag.Parse("test"), out _)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetSpan_ReturnsUnderlyingData() |
|||
{ |
|||
var tag = OpenTypeTag.Parse("span"); |
|||
var tableData = Enumerable.Range(0, 64).Select(i => (byte)i).ToArray(); |
|||
var font = BuildFont(tag, tableData); |
|||
|
|||
using var ms = new MemoryStream(font); |
|||
using var mem = UnmanagedFontMemory.LoadFromStream(ms); |
|||
|
|||
var span = mem.GetSpan(); |
|||
Assert.Equal(font.Length, span.Length); |
|||
Assert.Equal(font, span.ToArray()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Pin_IncrementsPinCount_And_Dispose_Throws_WhenPinned() |
|||
{ |
|||
var tag = OpenTypeTag.Parse("pin "); |
|||
var data = new byte[] { 1, 2, 3 }; |
|||
var font = BuildFont(tag, data); |
|||
|
|||
using var ms = new MemoryStream(font); |
|||
UnmanagedFontMemory mem = UnmanagedFontMemory.LoadFromStream(ms); |
|||
UnmanagedFontMemory? fresh = null; |
|||
|
|||
try |
|||
{ |
|||
var handle = mem.Pin(); |
|||
|
|||
try |
|||
{ |
|||
// Attempting to dispose while pinned should throw
|
|||
Assert.Throws<InvalidOperationException>(() => mem.Dispose()); |
|||
} |
|||
finally |
|||
{ |
|||
// Release the pin via the handle. After the failed Dispose the original
|
|||
// instance may be in an invalid state, so prefer releasing the pin
|
|||
// through the handle rather than calling methods on the possibly corrupted instance.
|
|||
try |
|||
{ |
|||
handle.Dispose(); |
|||
} |
|||
catch { } |
|||
} |
|||
|
|||
// After the exception the original instance may be unusable; construct a new instance
|
|||
// for further operations and assertions.
|
|||
fresh = UnmanagedFontMemory.LoadFromStream(new MemoryStream(font)); |
|||
|
|||
// Now disposing the fresh instance should not throw
|
|||
fresh.Dispose(); |
|||
} |
|||
finally |
|||
{ |
|||
// Ensure final cleanup if something went wrong
|
|||
try |
|||
{ |
|||
mem.Dispose(); |
|||
} |
|||
catch { } |
|||
|
|||
if (fresh != null) |
|||
{ |
|||
try |
|||
{ |
|||
fresh.Dispose(); |
|||
} |
|||
catch { } |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,131 @@ |
|||
using System; |
|||
using System.Buffers; |
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO; |
|||
using Avalonia.Media; |
|||
using Avalonia.Media.Fonts; |
|||
using Avalonia.Platform; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests.Media |
|||
{ |
|||
public class GlyphTypefaceTests |
|||
{ |
|||
private static string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests"; |
|||
|
|||
[Fact] |
|||
public void Should_Load_Inter_Font() |
|||
{ |
|||
var assetLoader = new StandardAssetLoader(); |
|||
|
|||
using var stream = assetLoader.Open(new Uri(s_InterFontUri)); |
|||
|
|||
var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); |
|||
|
|||
Assert.Equal("Inter", typeface.FamilyName); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Have_CharacterToGlyphMap_For_Common_Characters() |
|||
{ |
|||
var assetLoader = new StandardAssetLoader(); |
|||
|
|||
using var stream = assetLoader.Open(new Uri(s_InterFontUri)); |
|||
|
|||
var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); |
|||
|
|||
var map = typeface.CharacterToGlyphMap; |
|||
|
|||
Assert.NotNull(map); |
|||
|
|||
Assert.True(map.ContainsKey('A')); |
|||
Assert.True(map['A'] != 0); |
|||
|
|||
Assert.True(map.ContainsKey('a')); |
|||
Assert.True(map['a'] != 0); |
|||
|
|||
Assert.True(map.ContainsKey(' ')); |
|||
Assert.True(map[' '] != 0); |
|||
} |
|||
|
|||
[Fact] |
|||
public void GetGlyphAdvance_Should_Return_Advance_For_GlyphId() |
|||
{ |
|||
var assetLoader = new StandardAssetLoader(); |
|||
|
|||
using var stream = assetLoader.Open(new Uri(s_InterFontUri)); |
|||
|
|||
var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); |
|||
|
|||
var map = typeface.CharacterToGlyphMap; |
|||
|
|||
Assert.True(map.ContainsKey('A')); |
|||
|
|||
var glyphId = map['A']; |
|||
|
|||
// Ensure metrics are available for this glyph
|
|||
Assert.True(typeface.TryGetGlyphMetrics(glyphId, out var metrics)); |
|||
|
|||
var advance = typeface.GetGlyphAdvance(glyphId); |
|||
|
|||
// Advance returned by GetGlyphAdvance should match the metrics width
|
|||
Assert.Equal(metrics.Width, advance); |
|||
} |
|||
|
|||
private class CustomPlatformTypeface : IPlatformTypeface |
|||
{ |
|||
private readonly UnmanagedFontMemory _fontMemory; |
|||
|
|||
public CustomPlatformTypeface(Stream stream) |
|||
{ |
|||
_fontMemory = UnmanagedFontMemory.LoadFromStream(stream); |
|||
} |
|||
|
|||
public FontWeight Weight => FontWeight.Normal; |
|||
|
|||
public FontStyle Style => FontStyle.Normal; |
|||
|
|||
public FontStretch Stretch => FontStretch.Normal; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
_fontMemory.Dispose(); |
|||
} |
|||
|
|||
public unsafe bool TryGetStream([NotNullWhen(true)] out Stream stream) |
|||
{ |
|||
var memory = _fontMemory.Memory; |
|||
|
|||
var handle = memory.Pin(); // MemoryHandle merken
|
|||
stream = new PinnedUnmanagedMemoryStream(handle, memory.Length); |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private sealed class PinnedUnmanagedMemoryStream : UnmanagedMemoryStream |
|||
{ |
|||
private MemoryHandle _handle; |
|||
|
|||
public unsafe PinnedUnmanagedMemoryStream(MemoryHandle handle, long length) |
|||
: base((byte*)handle.Pointer, length) |
|||
{ |
|||
_handle = handle; |
|||
} |
|||
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
try |
|||
{ |
|||
base.Dispose(disposing); |
|||
} |
|||
finally |
|||
{ |
|||
_handle.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public bool TryGetTable(OpenTypeTag tag, out ReadOnlyMemory<byte> table) => _fontMemory.TryGetTable(tag, out table); |
|||
} |
|||
} |
|||
} |
|||
Binary file not shown.
@ -0,0 +1,113 @@ |
|||
using System; |
|||
using System.Buffers.Binary; |
|||
using Avalonia.Media.Fonts.Tables.Cmap; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Skia.UnitTests.Media.TextFormatting.Tables |
|||
{ |
|||
public class CmapTableTests |
|||
{ |
|||
[Fact] |
|||
public void BuildFormat4Subtable_Should_Map_Range() |
|||
{ |
|||
// Build a subtable mapping U+0030–U+0039 (digits 0–9) to glyphs 1–10
|
|||
byte[] subtable = CmapTestHelper.BuildFormat4Subtable(0x0030, 0x0039, 1); |
|||
|
|||
var cmap = new CmapFormat4Table(subtable); |
|||
|
|||
for (int i = 0; i < 10; i++) |
|||
{ |
|||
int cp = 0x30 + i; |
|||
ushort glyph = cmap[cp]; |
|||
var expectedGlyph = (ushort)(i + 1); |
|||
Assert.Equal(expectedGlyph, glyph); |
|||
} |
|||
|
|||
// Outside range should map to 0
|
|||
Assert.Equal((ushort)0, cmap[0x0041]); // 'A'
|
|||
} |
|||
} |
|||
|
|||
public static class CmapTestHelper |
|||
{ |
|||
/// <summary>
|
|||
/// Builds a Format 4 subtable for a TrueType font's 'cmap' table, which maps a range of character codes to
|
|||
/// glyph indices.
|
|||
/// </summary>
|
|||
/// <remarks>The Format 4 subtable is used in TrueType fonts to define mappings from character
|
|||
/// codes to glyph indices for a contiguous range of character codes. This method generates a minimal Format 4
|
|||
/// subtable with one segment for the specified range and a sentinel segment, as required by the TrueType
|
|||
/// specification. <para> The generated subtable includes the necessary header fields, segment arrays, and delta
|
|||
/// values to ensure that the specified range of character codes maps correctly to the corresponding glyph
|
|||
/// indices. </para> <exception cref="ArgumentException"> Thrown if <paramref name="endCode"/> is less than
|
|||
/// <paramref name="startCode"/>. </exception></remarks>
|
|||
/// <param name="startCode">The starting character code of the range to map.</param>
|
|||
/// <param name="endCode">The ending character code of the range to map.</param>
|
|||
/// <param name="firstGlyphId">The glyph index corresponding to the <paramref name="startCode"/>. Subsequent character codes in the range
|
|||
/// will map to consecutive glyph indices.</param>
|
|||
/// <returns>A byte array representing the Format 4 subtable, which can be embedded in a TrueType font's 'cmap' table.</returns>
|
|||
public static byte[] BuildFormat4Subtable(ushort startCode, ushort endCode, ushort firstGlyphId = 1) |
|||
{ |
|||
if (endCode < startCode) |
|||
throw new ArgumentException("endCode must be >= startCode"); |
|||
|
|||
// We will build exactly one real segment + sentinel
|
|||
ushort segCount = 2; // one real + one sentinel
|
|||
ushort segCountX2 = (ushort)(segCount * 2); |
|||
|
|||
// Correct search parameters (searchRange = 2 * (2^floor(log2(segCount))))
|
|||
int highestPowerOfTwo = 1; |
|||
while (highestPowerOfTwo * 2 <= segCount) |
|||
highestPowerOfTwo *= 2; |
|||
ushort searchRange = (ushort)(2 * highestPowerOfTwo); |
|||
ushort entrySelector = (ushort)(Math.Log(highestPowerOfTwo, 2)); |
|||
ushort rangeShift = (ushort)(segCountX2 - searchRange); |
|||
|
|||
// idDelta so that startCode maps to firstGlyphId
|
|||
short idDelta = (short)(firstGlyphId - startCode); |
|||
|
|||
// Calculate length: header (14) + endCode(segCount*2) + reservedPad(2) + startCode(segCount*2)
|
|||
// + idDelta(segCount*2) + idRangeOffset(segCount*2) + (no glyphIdArray)
|
|||
int headerSize = 14; |
|||
int segArraysSize = segCount * 2 /*endCode*/ + 2 /*reservedPad*/ + segCount * 2 /*startCode*/ + segCount * 2 /*idDelta*/ + segCount * 2 /*idRangeOffset*/; |
|||
int length = headerSize + segArraysSize; |
|||
|
|||
var buffer = new byte[length]; |
|||
int pos = 0; |
|||
|
|||
void WriteUInt16(ushort v) |
|||
{ BinaryPrimitives.WriteUInt16BigEndian(buffer.AsSpan(pos, 2), v); pos += 2; } |
|||
void WriteInt16(short v) |
|||
{ BinaryPrimitives.WriteInt16BigEndian(buffer.AsSpan(pos, 2), v); pos += 2; } |
|||
|
|||
// Header
|
|||
WriteUInt16(4); // format
|
|||
WriteUInt16((ushort)length); // length
|
|||
WriteUInt16(0); // language
|
|||
WriteUInt16(segCountX2); |
|||
WriteUInt16(searchRange); |
|||
WriteUInt16(entrySelector); |
|||
WriteUInt16(rangeShift); |
|||
|
|||
// endCode[] (one real segment then sentinel)
|
|||
WriteUInt16(endCode); |
|||
WriteUInt16(0xFFFF); |
|||
|
|||
WriteUInt16(0); // reservedPad
|
|||
|
|||
// startCode[]
|
|||
WriteUInt16(startCode); |
|||
WriteUInt16(0xFFFF); |
|||
|
|||
// idDelta[]
|
|||
WriteInt16(idDelta); |
|||
WriteInt16(1); // sentinel delta (commonly 1)
|
|||
|
|||
// idRangeOffset[]
|
|||
WriteUInt16(0); |
|||
WriteUInt16(0); |
|||
|
|||
return buffer; |
|||
} |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue