Browse Source

Handle font cmap format 13 (#20740)

* Handle font cmap format 13

* Fallback to last resort fonts as... a last resort

* Tests all fonts in Should_AddGlyphTypeface_By_Stream

* Add last resort font test

* Remove cmap subtable 13 from TestFontNoCmap412.ttf
pull/20752/head
Julien Lebosquain 4 weeks ago
committed by GitHub
parent
commit
a08e125a14
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 102
      src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs
  2. 39
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CharacterToGlyphMap.cs
  3. 46
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Or13Table.cs
  4. 16
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapTable.cs
  5. 15
      src/Avalonia.Base/Media/Fonts/Tables/Cmap/CodepointRangeEnumerator.cs
  6. 8
      src/Avalonia.Base/Media/GlyphTypeface.cs
  7. 23
      tests/Avalonia.Base.UnitTests/Media/GlyphTypefaceTests.cs
  8. BIN
      tests/Avalonia.RenderTests/Assets/AdobeBlank2VF.ttf
  9. BIN
      tests/Avalonia.Skia.UnitTests/Fonts/TestFontNoCmap412.ttf
  10. 61
      tests/Avalonia.Skia.UnitTests/Media/CustomFontCollectionTests.cs
  11. 35
      tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs

102
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(
@ -570,7 +576,19 @@ namespace Avalonia.Media.Fonts
protected bool TryGetNearestMatch(IDictionary<FontCollectionKey, GlyphTypeface?> 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<FontCollectionKey, GlyphTypeface?> 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
/// <param name="glyphTypefaces">A dictionary mapping font collection keys to their corresponding glyph typefaces. Used as the source for
/// searching fallback typefaces.</param>
/// <param name="key">The font collection key specifying the desired font stretch and other font attributes to match.</param>
/// <param name="isLastResort">Whether to match last resort fonts.</param>
/// <param name="glyphTypeface">When this method returns, contains the found glyph typeface with a similar stretch if one exists; otherwise,
/// null.</param>
/// <returns>true if a suitable fallback glyph typeface is found; otherwise, false.</returns>
private static bool TryFindStretchFallback(
IDictionary<FontCollectionKey, GlyphTypeface?> 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.</param>
/// <param name="key">The font collection key specifying the desired font attributes, including weight, for which a fallback glyph
/// typeface is sought.</param>
/// <param name="isLastResort">Whether to match last resort fonts.</param>
/// <param name="glyphTypeface">When this method returns, contains the matching glyph typeface if a suitable fallback is found; otherwise,
/// null.</param>
/// <returns>true if a fallback glyph typeface matching the requested weight is found; otherwise, false.</returns>
private static bool TryFindWeightFallback(
IDictionary<FontCollectionKey, GlyphTypeface?> 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()

39
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; }
/// <summary>
/// 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;
}
/// <summary>
/// Initializes a new instance of the CharacterToGlyphMap class using the specified Format 12 character-to-glyph
/// mapping table.
/// </summary>
/// <param name="table">The Format 12 cmap table that defines the mapping from Unicode code points to glyph indices. Cannot be null.</param>
/// <param name="table">The Format 12 or 13 cmap table that defines the mapping from Unicode code points to glyph indices. Cannot be null.</param>
[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,13 +101,14 @@ namespace Avalonia.Media.Fonts.Tables.Cmap
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void GetGlyphs(ReadOnlySpan<int> codePoints, Span<ushort> 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();
@ -116,7 +116,6 @@ namespace Avalonia.Media.Fonts.Tables.Cmap
}
}
/// <summary>
/// Attempts to retrieve the glyph identifier corresponding to the specified Unicode code point.
/// </summary>
@ -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);
}
}
}

46
src/Avalonia.Base/Media/Fonts/Tables/Cmap/CmapFormat12Table.cs → 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<byte> _table;
private readonly int _groupCount;
private readonly ReadOnlyMemory<byte> _groups;
public CmapFormat Format { get; }
/// <summary>
/// Gets the language code for the cmap subtable.
/// For non-language-specific tables, this value is 0.
/// </summary>
public uint Language { get; }
public CmapFormat12Table(ReadOnlyMemory<byte> table)
public CmapFormat12Or13Table(ReadOnlyMemory<byte> 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

16
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;
}

15
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.
/// </summary>
/// <remarks>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.</remarks>
/// 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.</remarks>
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;

8
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
}
}
/// <summary>
/// Gets whether the font should be used as a last resort, if no other fonts matched.
/// </summary>
internal bool IsLastResort { get; }
/// <summary>
/// Attempts to retrieve the horizontal advance width for the specified glyph.
/// </summary>

23
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()
{

BIN
tests/Avalonia.RenderTests/Assets/AdobeBlank2VF.ttf

Binary file not shown.

BIN
tests/Avalonia.Skia.UnitTests/Fonts/TestFontNoCmap412.ttf

Binary file not shown.

61
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<IAssetLoader>();
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<IAssetLoader>();
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);
}
}

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

Loading…
Cancel
Save