diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index a3375652b8..e2076d34b6 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; +using Avalonia.Media.Fonts.Tables; using Avalonia.Platform; namespace Avalonia.Media.Fonts @@ -55,35 +56,40 @@ namespace Avalonia.Media.Fonts } } - //Try to find a match in any font family - foreach (var pair in _glyphTypefaceCache) + return TryMatchInAnyFamily(isLastResort: false, out match) || + TryMatchInAnyFamily(isLastResort: true, out match); + + bool TryMatchInAnyFamily(bool isLastResort, out Typeface match) { - if (pair.Key == familyName) + //Try to find a match in any font family + foreach (var pair in _glyphTypefaceCache) { - //We already tried this before - continue; - } - - glyphTypefaces = pair.Value; + if (pair.Key == familyName) + { + //We already tried this before + continue; + } - if (TryGetNearestMatch(glyphTypefaces, key, out var glyphTypeface)) - { - if (glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _)) + if (TryGetNearestMatchCore(pair.Value, key, isLastResort, out var glyphTypeface)) { - var platformTypeface = glyphTypeface.PlatformTypeface; + if (glyphTypeface.CharacterToGlyphMap.TryGetGlyph(codepoint, out _)) + { + var platformTypeface = glyphTypeface.PlatformTypeface; - // Found a match - match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), - platformTypeface.Style, - platformTypeface.Weight, - platformTypeface.Stretch); + // Found a match + match = new Typeface(new FontFamily(null, Key.AbsoluteUri + "#" + glyphTypeface.FamilyName), + platformTypeface.Style, + platformTypeface.Weight, + platformTypeface.Stretch); - return true; + return true; + } } } - } - return false; + match = default; + return false; + } } public virtual bool TryCreateSyntheticGlyphTypeface( @@ -558,7 +564,7 @@ namespace Avalonia.Media.Fonts /// provided collection of glyph typefaces. /// /// This method attempts to find the best match for the specified font key by considering - /// various fallback strategies, such as normalizing the font style, stretch, and weight. + /// various fallback strategies, such as normalizing the font style, stretch, and weight. /// If no suitable match is found, the method will return the first available non-null from the /// collection, if any. /// A collection of glyph typefaces, indexed by . @@ -567,10 +573,22 @@ namespace Avalonia.Media.Fonts /// key, if a match is found; otherwise, . /// if a matching is found; otherwise, . - protected bool TryGetNearestMatch(IDictionary glyphTypefaces, + protected bool TryGetNearestMatch(IDictionary glyphTypefaces, FontCollectionKey key, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { - if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && glyphTypeface != null) + return TryGetNearestMatchCore(glyphTypefaces, key, isLastResort: false, out glyphTypeface) + || TryGetNearestMatchCore(glyphTypefaces, key, isLastResort: true, out glyphTypeface); + } + + private static bool TryGetNearestMatchCore( + IDictionary glyphTypefaces, + FontCollectionKey key, + bool isLastResort, + [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) + { + if (glyphTypefaces.TryGetValue(key, out glyphTypeface) && + glyphTypeface != null && + glyphTypeface.IsLastResort == isLastResort) { return true; } @@ -582,14 +600,14 @@ namespace Avalonia.Media.Fonts if (key.Stretch != FontStretch.Normal) { - if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + if (TryFindStretchFallback(glyphTypefaces, key, isLastResort, out glyphTypeface)) { return true; } if (key.Weight != FontWeight.Normal) { - if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, out glyphTypeface)) + if (TryFindStretchFallback(glyphTypefaces, key with { Weight = FontWeight.Normal }, isLastResort, out glyphTypeface)) { return true; } @@ -598,12 +616,12 @@ namespace Avalonia.Media.Fonts key = key with { Stretch = FontStretch.Normal }; } - if (TryFindWeightFallback(glyphTypefaces, key, out glyphTypeface)) + if (TryFindWeightFallback(glyphTypefaces, key, isLastResort, out glyphTypeface)) { return true; } - if (TryFindStretchFallback(glyphTypefaces, key, out glyphTypeface)) + if (TryFindStretchFallback(glyphTypefaces, key, isLastResort, out glyphTypeface)) { return true; } @@ -611,7 +629,7 @@ namespace Avalonia.Media.Fonts //Take the first glyph typeface we can find. foreach (var typeface in glyphTypefaces.Values) { - if (typeface != null) + if (typeface != null && isLastResort == typeface.IsLastResort) { glyphTypeface = typeface; @@ -695,12 +713,14 @@ namespace Avalonia.Media.Fonts /// A dictionary mapping font collection keys to their corresponding glyph typefaces. Used as the source for /// searching fallback typefaces. /// The font collection key specifying the desired font stretch and other font attributes to match. + /// Whether to match last resort fonts. /// When this method returns, contains the found glyph typeface with a similar stretch if one exists; otherwise, /// null. /// true if a suitable fallback glyph typeface is found; otherwise, false. private static bool TryFindStretchFallback( IDictionary glyphTypefaces, FontCollectionKey key, + bool isLastResort, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -711,7 +731,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; stretch + i < 9; i++) { - if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithStretch(stretch, out glyphTypeface)) { return true; } @@ -721,13 +741,18 @@ namespace Avalonia.Media.Fonts { for (var i = 0; stretch - i > 1; i++) { - if (glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)(stretch - i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithStretch(stretch, out glyphTypeface)) { return true; } } } + bool TryGetWithStretch(int effectiveStretch, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) + => glyphTypefaces.TryGetValue(key with { Stretch = (FontStretch)effectiveStretch }, out glyphTypeface) && + glyphTypeface != null && + glyphTypeface.IsLastResort == isLastResort; + return false; } @@ -742,12 +767,14 @@ namespace Avalonia.Media.Fonts /// for a suitable fallback. /// The font collection key specifying the desired font attributes, including weight, for which a fallback glyph /// typeface is sought. + /// Whether to match last resort fonts. /// When this method returns, contains the matching glyph typeface if a suitable fallback is found; otherwise, /// null. /// true if a fallback glyph typeface matching the requested weight is found; otherwise, false. private static bool TryFindWeightFallback( IDictionary glyphTypefaces, FontCollectionKey key, + bool isLastResort, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) { glyphTypeface = null; @@ -759,7 +786,7 @@ namespace Avalonia.Media.Fonts //Look for available weights between the target and 500, in ascending order. for (var i = 0; weight + i <= 500; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight, out glyphTypeface)) { return true; } @@ -768,7 +795,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights less than the target, in descending order. for (var i = 0; weight - i >= 100; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight, out glyphTypeface)) { return true; } @@ -777,7 +804,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights greater than 500, in ascending order. for (var i = 0; weight + i <= 900; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight, out glyphTypeface)) { return true; } @@ -789,7 +816,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; weight - i >= 100; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight, out glyphTypeface)) { return true; } @@ -798,7 +825,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights less than the target, in descending order. for (var i = 0; weight + i <= 900; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight, out glyphTypeface)) { return true; } @@ -810,7 +837,7 @@ namespace Avalonia.Media.Fonts { for (var i = 0; weight + i <= 900; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight + i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight, out glyphTypeface)) { return true; } @@ -819,7 +846,7 @@ namespace Avalonia.Media.Fonts //If no match is found, look for available weights less than the target, in descending order. for (var i = 0; weight - i >= 100; i += 50) { - if (glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)(weight - i) }, out glyphTypeface) && glyphTypeface != null) + if (TryGetWithWeight(weight, out glyphTypeface)) { return true; } @@ -827,6 +854,11 @@ namespace Avalonia.Media.Fonts } return false; + + bool TryGetWithWeight(int effectiveWeight, [NotNullWhen(true)] out GlyphTypeface? glyphTypeface) + => glyphTypefaces.TryGetValue(key with { Weight = (FontWeight)effectiveWeight }, out glyphTypeface) && + glyphTypeface != null && + glyphTypeface.IsLastResort == isLastResort; } void IDisposable.Dispose() diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs index 9a461afb0b..83db40ba62 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Text; namespace Avalonia.Media.Fonts.Tables.Cmap { @@ -16,9 +14,10 @@ namespace Avalonia.Media.Fonts.Tables.Cmap public readonly struct CharacterToGlyphMap #pragma warning restore CA1815 // Override equals not needed for readonly struct { - private readonly CmapFormat _format; private readonly CmapFormat4Table? _format4; - private readonly CmapFormat12Table? _format12; + private readonly CmapFormat12Or13Table? _format12Or13; + + internal CmapFormat Format { get; } /// /// Initializes a new instance of the CharacterToGlyphMap class using the specified Format 4 cmap table. @@ -27,21 +26,21 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] internal CharacterToGlyphMap(CmapFormat4Table table) { - _format = CmapFormat.Format4; + Format = CmapFormat.Format4; _format4 = table; - _format12 = null; + _format12Or13 = null; } /// /// Initializes a new instance of the CharacterToGlyphMap class using the specified Format 12 character-to-glyph /// mapping table. /// - /// The Format 12 cmap table that defines the mapping from Unicode code points to glyph indices. Cannot be null. + /// The Format 12 or 13 cmap table that defines the mapping from Unicode code points to glyph indices. Cannot be null. [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal CharacterToGlyphMap(CmapFormat12Table table) + internal CharacterToGlyphMap(CmapFormat12Or13Table table) { - _format = CmapFormat.Format12; - _format12 = table; + Format = table.Format; + _format12Or13 = table; _format4 = null; } @@ -65,10 +64,10 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public ushort GetGlyph(int codePoint) { - return _format switch + return Format switch { CmapFormat.Format4 => _format4!.GetGlyph(codePoint), - CmapFormat.Format12 => _format12!.GetGlyph(codePoint), + CmapFormat.Format12 or CmapFormat.Format13 => _format12Or13!.GetGlyph(codePoint), _ => 0 }; } @@ -81,10 +80,10 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool ContainsGlyph(int codePoint) { - return _format switch + return Format switch { CmapFormat.Format4 => _format4!.ContainsGlyph(codePoint), - CmapFormat.Format12 => _format12!.ContainsGlyph(codePoint), + CmapFormat.Format12 or CmapFormat.Format13 => _format12Or13!.ContainsGlyph(codePoint), _ => false }; } @@ -102,20 +101,20 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public void GetGlyphs(ReadOnlySpan codePoints, Span glyphIds) { - switch (_format) + switch (Format) { case CmapFormat.Format4: _format4!.GetGlyphs(codePoints, glyphIds); return; case CmapFormat.Format12: - _format12!.GetGlyphs(codePoints, glyphIds); + case CmapFormat.Format13: + _format12Or13!.GetGlyphs(codePoints, glyphIds); return; default: glyphIds.Clear(); return; } } - /// /// Attempts to retrieve the glyph identifier corresponding to the specified Unicode code point. @@ -127,10 +126,10 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGetGlyph(int codePoint, out ushort glyphId) { - switch (_format) + switch (Format) { case CmapFormat.Format4: return _format4!.TryGetGlyph(codePoint, out glyphId); - case CmapFormat.Format12: return _format12!.TryGetGlyph(codePoint, out glyphId); + case CmapFormat.Format12 or CmapFormat.Format13: return _format12Or13!.TryGetGlyph(codePoint, out glyphId); default: glyphId = 0; return false; } } @@ -142,7 +141,7 @@ namespace Avalonia.Media.Fonts.Tables.Cmap [MethodImpl(MethodImplOptions.AggressiveInlining)] public CodepointRangeEnumerator GetMappedRanges() { - return new CodepointRangeEnumerator(_format, _format4, _format12); + return new CodepointRangeEnumerator(Format, _format4, _format12Or13); } } } diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Or13Table.cs similarity index 88% rename from src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs rename to src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Or13Table.cs index b4440e7884..cc20e735d5 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Or13Table.cs @@ -1,30 +1,31 @@ using System; using System.Buffers.Binary; -using System.Collections; -using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; namespace Avalonia.Media.Fonts.Tables.Cmap { - internal sealed class CmapFormat12Table + internal sealed class CmapFormat12Or13Table { private readonly ReadOnlyMemory _table; private readonly int _groupCount; private readonly ReadOnlyMemory _groups; + public CmapFormat Format { get; } + /// /// Gets the language code for the cmap subtable. /// For non-language-specific tables, this value is 0. /// public uint Language { get; } - public CmapFormat12Table(ReadOnlyMemory table) + public CmapFormat12Or13Table(ReadOnlyMemory table) { var reader = new BigEndianBinaryReader(table.Span); ushort format = reader.ReadUInt16(); - Debug.Assert(format == 12, "Format must be 12."); + Debug.Assert(format is 12 or 13, "Format must be 12 or 13."); + Format = (CmapFormat)format; ushort reserved = reader.ReadUInt16(); Debug.Assert(reserved == 0, "Reserved field must be 0."); @@ -101,7 +102,7 @@ namespace Avalonia.Media.Fonts.Tables.Cmap // Optimization: check if codepoint is in the same group as previous if (lastGroup >= 0 && codePoint >= lastStart && codePoint <= lastEnd) { - glyphIds[i] = (ushort)(lastStartGlyph + (codePoint - lastStart)); + glyphIds[i] = CalcEffectiveGlyph(codePoint, lastStart, lastStartGlyph); continue; } @@ -122,27 +123,13 @@ namespace Avalonia.Media.Fonts.Tables.Cmap lastEnd = ReadUInt32BE(groups, groupIndex, 4); lastStartGlyph = ReadUInt32BE(groups, groupIndex, 8); - glyphIds[i] = (ushort)(lastStartGlyph + (codePoint - lastStart)); + glyphIds[i] = CalcEffectiveGlyph(codePoint, lastStart, lastStartGlyph); } } public bool TryGetGlyph(int codePoint, out ushort glyphId) { - int groupIndex = FindGroupIndex(codePoint); - - if (groupIndex < 0) - { - glyphId = 0; - return false; - } - - var groups = _groups.Span; - - uint start = ReadUInt32BE(groups, groupIndex, 0); - uint startGlyph = ReadUInt32BE(groups, groupIndex, 8); - - glyphId = (ushort)(startGlyph + (codePoint - start)); - + glyphId = this[codePoint]; return glyphId != 0; } @@ -180,10 +167,21 @@ namespace Avalonia.Media.Fonts.Tables.Cmap uint start = ReadUInt32BE(groups, groupIndex, 0); uint startGlyph = ReadUInt32BE(groups, groupIndex, 8); + return CalcEffectiveGlyph(codePoint, start, startGlyph); + } + } - // Calculate glyph index - return (ushort)(startGlyph + (codePoint - start)); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private ushort CalcEffectiveGlyph(int codePoint, uint start, uint startGlyph) + { + // Format 13, all codepoints in the group map to a single glyph + if (Format == CmapFormat.Format13) + { + return (ushort)startGlyph; } + + // Format 12, calculate glyph index + return (ushort)(startGlyph + (codePoint - start)); } // Optimized binary search that works directly with cached span diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs index f526658133..7774294e76 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs @@ -49,22 +49,28 @@ namespace Avalonia.Media.Fonts.Tables.Cmap } // Try to find the best Format 12 subtable entry - if (TryFindFormat12Entry(entries, out var format12Entry)) + if (TryFindFormat12Or13Entry(entries, CmapFormat.Format12, out var format12Entry)) { // Prefer Format 12 if available - return new CharacterToGlyphMap(new CmapFormat12Table(format12Entry.GetSubtableMemory(table))); + return new CharacterToGlyphMap(new CmapFormat12Or13Table(format12Entry.GetSubtableMemory(table))); } - // Fallback to Format 4 + // Then Format 4 if (TryFindFormat4Entry(entries, out var format4Entry)) { return new CharacterToGlyphMap(new CmapFormat4Table(format4Entry.GetSubtableMemory(table))); } + // Fallback to Format 13, which is a "last resort" format mapping many codepoints to a single glyph + if (TryFindFormat12Or13Entry(entries, CmapFormat.Format13, out var format13Entry)) + { + return new CharacterToGlyphMap(new CmapFormat12Or13Table(format13Entry.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) + static bool TryFindFormat12Or13Entry(CmapSubtableEntry[] entries, CmapFormat expectedFormat, out CmapSubtableEntry result) { result = default; var foundPlatformScore = int.MaxValue; @@ -72,7 +78,7 @@ namespace Avalonia.Media.Fonts.Tables.Cmap foreach (var entry in entries) { - if (entry.Format != CmapFormat.Format12) + if (entry.Format != expectedFormat) { continue; } diff --git a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs index b631c264d1..627ce1b6b6 100644 --- a/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs +++ b/src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs @@ -6,21 +6,21 @@ namespace Avalonia.Media.Fonts.Tables.Cmap /// Enumerates contiguous ranges of Unicode code points present in a character map (cmap) table. /// /// This enumerator is typically used to iterate over all code point ranges defined by a cmap - /// table in an OpenType or TrueType font. It supports both Format 4 and Format 12 cmap subtables. The enumerator is - /// a ref struct and must be used within the stack context; it cannot be stored on the heap or used across await or - /// yield boundaries. + /// table in an OpenType or TrueType font. It supports Format 4, Format 12, and Format 13 cmap subtables. + /// The enumerator is a ref struct and must be used within the stack context; it cannot be stored on the + /// heap or used across await or yield boundaries. public ref struct CodepointRangeEnumerator { private readonly CmapFormat _format; private readonly CmapFormat4Table? _f4; - private readonly CmapFormat12Table? _f12; + private readonly CmapFormat12Or13Table? _f12Or13; private int _index; - internal CodepointRangeEnumerator(CmapFormat format, CmapFormat4Table? f4, CmapFormat12Table? f12) + internal CodepointRangeEnumerator(CmapFormat format, CmapFormat4Table? f4, CmapFormat12Or13Table? f12Or13) { _format = format; _f4 = f4; - _f12 = f12; + _f12Or13 = f12Or13; _index = -1; } @@ -52,8 +52,9 @@ namespace Avalonia.Media.Fonts.Tables.Cmap return result; } case CmapFormat.Format12: + case CmapFormat.Format13: { - var result = _f12!.TryGetRange(_index, out var range); + var result = _f12Or13!.TryGetRange(_index, out var range); Current = range; diff --git a/src/Avalonia.Base/Media/GlyphTypeface.cs b/src/Avalonia.Base/Media/GlyphTypeface.cs index 3b3875b5de..cb554f3e22 100644 --- a/src/Avalonia.Base/Media/GlyphTypeface.cs +++ b/src/Avalonia.Base/Media/GlyphTypeface.cs @@ -114,6 +114,9 @@ namespace Avalonia.Media HeadTable.TryLoad(this, out var headTable); + IsLastResort = (headTable is not null && (headTable.Flags & HeadFlags.LastResortFont) != 0) || + _cmapTable.Format == CmapFormat.Format13; + var postTable = PostTable.Load(this); var isFixedPitch = postTable.IsFixedPitch; @@ -354,6 +357,11 @@ namespace Avalonia.Media } } + /// + /// Gets whether the font should be used as a last resort, if no other fonts matched. + /// + internal bool IsLastResort { get; } + /// /// Attempts to retrieve the horizontal advance width for the specified glyph. /// diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs index 7f629c8e69..905d782b48 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs @@ -12,7 +12,8 @@ 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"; + private static readonly string s_InterFontUri = "resm:Avalonia.Base.UnitTests.Assets.Inter-Regular.ttf?assembly=Avalonia.Base.UnitTests"; + private static readonly string s_blankFontUri = "resm:Avalonia.Base.UnitTests.Assets.AdobeBlank2VF.ttf?assembly=Avalonia.Base.UnitTests"; [Fact] public void Should_Load_Inter_Font() @@ -321,6 +322,26 @@ namespace Avalonia.Base.UnitTests.Media Assert.NotEqual(glyphA, glyphB); } + [Fact] + public void CharacterToGlyphMap_With_Format13_Should_Have_Same_Glyph_For_Different_Characters() + { + var assetLoader = new StandardAssetLoader(); + + using var stream = assetLoader.Open(new Uri(s_blankFontUri)); + + var typeface = new GlyphTypeface(new CustomPlatformTypeface(stream)); + + var map = typeface.CharacterToGlyphMap; + + Assert.True(map.ContainsGlyph('A')); + Assert.True(map.ContainsGlyph('B')); + + var glyphA = map['A']; + var glyphB = map['B']; + + Assert.Equal(glyphA, glyphB); + } + [Fact] public void FontMetrics_LineSpacing_Should_Be_Calculated_Correctly() { diff --git a/tests/Avalonia.RenderTests/Assets/AdobeBlank2VF.ttf b/tests/Avalonia.RenderTests/Assets/AdobeBlank2VF.ttf new file mode 100644 index 0000000000..a1ac3440ef Binary files /dev/null and b/tests/Avalonia.RenderTests/Assets/AdobeBlank2VF.ttf differ diff --git a/tests/Avalonia.Skia.UnitTests/Fonts/TestFontNoCmap412.ttf b/tests/Avalonia.Skia.UnitTests/Fonts/TestFontNoCmap412.ttf index 03a0fbe537..161ce20c4d 100644 Binary files a/tests/Avalonia.Skia.UnitTests/Fonts/TestFontNoCmap412.ttf and b/tests/Avalonia.Skia.UnitTests/Fonts/TestFontNoCmap412.ttf differ diff --git a/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs index 66afed7a3a..7e49eabe03 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs @@ -11,8 +11,8 @@ namespace Avalonia.Skia.UnitTests.Media { public class CustomFontCollectionTests { - private const string NotoMono = - "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"; + private const string AssetsNamespace = "Avalonia.Skia.UnitTests.Assets"; + private const string AssetFonts = $"resm:{AssetsNamespace}?assembly=Avalonia.Skia.UnitTests"; [Fact] public void Should_AddGlyphTypeface_By_Stream() @@ -27,23 +27,45 @@ namespace Avalonia.Skia.UnitTests.Media var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var assets = assetLoader.GetAssets(new Uri(NotoMono, UriKind.Absolute), null).ToArray(); - - Assert.NotEmpty(assets); - - var notoMonoLocation = assets.First(); - - using var notoMonoStream = assetLoader.Open(notoMonoLocation); - - Assert.NotNull(notoMonoStream); + var infos = new[] + { + new FontAssetInfo($"{AssetsNamespace}.AdobeBlank2VF.ttf", "Adobe Blank 2 VF R"), + new FontAssetInfo($"{AssetsNamespace}.Inter-Regular.ttf", "Inter"), + new FontAssetInfo($"{AssetsNamespace}.Manrope-Light.ttf", "Manrope Light"), + new FontAssetInfo($"{AssetsNamespace}.MiSans-Normal.ttf", "MiSans Normal"), + new FontAssetInfo($"{AssetsNamespace}.NotoMono-Regular.ttf", "Noto Mono"), + new FontAssetInfo($"{AssetsNamespace}.NotoSans-Italic.ttf", "Noto Sans"), + new FontAssetInfo($"{AssetsNamespace}.NotoSansArabic-Regular.ttf", "Noto Sans Arabic"), + new FontAssetInfo($"{AssetsNamespace}.NotoSansDeseret-Regular.ttf", "Noto Sans Deseret"), + new FontAssetInfo($"{AssetsNamespace}.NotoSansHebrew-Regular.ttf", "Noto Sans Hebrew"), + new FontAssetInfo($"{AssetsNamespace}.NotoSansMiao-Regular.ttf", "Noto Sans Miao"), + new FontAssetInfo($"{AssetsNamespace}.NotoSansTamil-Regular.ttf", "Noto Sans Tamil"), + new FontAssetInfo($"{AssetsNamespace}.SourceSerif4_36pt-Italic.ttf", "Source Serif 4 36pt"), + new FontAssetInfo($"{AssetsNamespace}.TwitterColorEmoji-SVGinOT.ttf", "Twitter Color Emoji") + }; + + var assets = assetLoader.GetAssets(new Uri(AssetFonts, UriKind.Absolute), null) + .OrderBy(uri => uri.AbsoluteUri, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + Assert.Equal(infos.Length, assets.Length); + + for (var i = 0; i < infos.Length; ++i) + { + var info = infos[i]; + var asset = assets[i]; - Assert.True(fontCollection.TryAddGlyphTypeface(notoMonoStream, out var glyphTypeface)); + Assert.Equal(info.Path, asset.AbsolutePath); - Assert.Equal("Inter", glyphTypeface.FamilyName); + using var fontStream = assetLoader.Open(asset); + Assert.NotNull(fontStream); - Assert.True(fontManager.TryGetGlyphTypeface(new Typeface("fonts:custom#Inter"), out var secondGlyphTypeface)); + Assert.True(fontCollection.TryAddGlyphTypeface(fontStream, out var glyphTypeface)); + Assert.Equal(info.FamilyName, glyphTypeface.FamilyName); - Assert.Equal(glyphTypeface, secondGlyphTypeface); + Assert.True(fontManager.TryGetGlyphTypeface(new Typeface($"fonts:custom#{info.FamilyName}"), out var secondGlyphTypeface)); + Assert.Same(glyphTypeface, secondGlyphTypeface); + } } } @@ -60,7 +82,7 @@ namespace Avalonia.Skia.UnitTests.Media var assetLoader = AvaloniaLocator.Current.GetRequiredService(); - var assets = assetLoader.GetAssets(new Uri(NotoMono, UriKind.Absolute), null).Where(x => x.AbsolutePath.EndsWith(".ttf")).ToArray(); + var assets = assetLoader.GetAssets(new Uri(AssetFonts, UriKind.Absolute), null).Where(x => x.AbsolutePath.EndsWith(".ttf")).ToArray(); foreach (var asset in assets) { @@ -157,11 +179,10 @@ namespace Avalonia.Skia.UnitTests.Media var fontCollection = new CustomFontCollection(new Uri("fonts:custom", UriKind.Absolute)); fontManager.AddFontCollection(fontCollection); - // Use the NotoMono resource as FontSource - var notoMonoUri = new Uri(NotoMono, UriKind.Absolute); + var allFontsUri = new Uri(AssetFonts, UriKind.Absolute); // Add the font resource - Assert.True(fontCollection.TryAddFontSource(notoMonoUri)); + Assert.True(fontCollection.TryAddFontSource(allFontsUri)); // Get the loaded family names var families = fontCollection.ToArray(); @@ -184,5 +205,7 @@ namespace Avalonia.Skia.UnitTests.Media { public override Uri Key { get; } = key; } + + private record struct FontAssetInfo(string Path, string FamilyName); } } diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index 76d4857e64..2c45c1a936 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -437,6 +437,41 @@ namespace Avalonia.Skia.UnitTests.Media } } + [Fact] + public void Should_Use_Last_Resort_Font_Last_MatchCharacter() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + FontManager.Current.AddFontCollection( + new EmbeddedFontCollection( + new Uri("fonts:MyCollection"), //key + new Uri("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests"))); //source + + var fontFamily = new FontFamily("fonts:MyCollection#Noto Sans"); + + const string characters = "א𪜶"; + + var codepoint1 = Codepoint.ReadAt(characters, 0, out _); + Assert.Equal(0x5D0, codepoint1); // א + + // Typeface should come from the font collection - falling back to Noto Sans Hebrew + Assert.True(FontManager.Current.TryMatchCharacter(codepoint1, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface1)); + Assert.NotNull(typeface1.FontFamily.Key); + Assert.Equal("Noto Sans Hebrew", typeface1.GlyphTypeface.FamilyName); + + var codepoint2 = Codepoint.ReadAt(characters, 1, out _); + Assert.Equal(0x2A736, codepoint2); // 𪜶 + + // Typeface should come from the font collection - falling back to Adobe Blank 2 VF R as a last resort + Assert.True(FontManager.Current.TryMatchCharacter(codepoint2, FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface2)); + Assert.NotNull(typeface2.FontFamily.Key); + Assert.Equal("Adobe Blank 2 VF R", typeface2.GlyphTypeface.FamilyName); + } + } + } + [InlineData("Arial")] [InlineData("#Arial")] [Win32Theory("Windows specific font")]