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")]