Skip to content

Commit

Permalink
Allow multiple font sources per FontFamily and make sure combinations…
Browse files Browse the repository at this point in the history
… of system and embedded fonts can be used (AvaloniaUI#12871)
  • Loading branch information
Gillibald authored Sep 14, 2023
1 parent fac9e55 commit cb461ab
Show file tree
Hide file tree
Showing 9 changed files with 286 additions and 89 deletions.
16 changes: 16 additions & 0 deletions src/Avalonia.Base/Media/CompositeFontFamilyKey.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.Fonts;

namespace Avalonia.Media
{
internal class CompositeFontFamilyKey : FontFamilyKey
{
public CompositeFontFamilyKey(Uri source, FontFamilyKey[] keys) : base(source, null)
{
Keys = keys;
}

public IReadOnlyList<FontFamilyKey> Keys { get; }
}
}
104 changes: 67 additions & 37 deletions src/Avalonia.Base/Media/FontFamily.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using Avalonia.Media.Fonts;
using Avalonia.Utilities;

namespace Avalonia.Media
{
Expand Down Expand Up @@ -34,19 +36,42 @@ public FontFamily(Uri? baseUri, string name)
throw new ArgumentNullException(nameof(name));
}

var fontFamilySegment = GetFontFamilyIdentifier(name);
var fontSources = GetFontSourceIdentifier(name);

if (fontFamilySegment.Source != null)
FamilyNames = new FamilyNameCollection(fontSources);

if (fontSources.Count == 1)
{
if (baseUri != null && !baseUri.IsAbsoluteUri)
if(fontSources[0].Source is Uri source)
{
throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri));
}
if (baseUri != null && !baseUri.IsAbsoluteUri)
{
throw new ArgumentException("Base uri must be an absolute uri.", nameof(baseUri));
}

Key = new FontFamilyKey(fontFamilySegment.Source, baseUri);
Key = new FontFamilyKey(source, baseUri);
}
}
else
{
var keys = new FontFamilyKey[fontSources.Count];

for (int i = 0; i < fontSources.Count; i++)
{
var fontSource = fontSources[i];

FamilyNames = new FamilyNameCollection(fontFamilySegment.Name);
if(fontSource.Source is not null)
{
keys[i] = new FontFamilyKey(fontSource.Source, baseUri);
}
else
{
keys[i] = new FontFamilyKey(new Uri(FontManager.SystemFontScheme + ":" + fontSource.Name, UriKind.Absolute));
}
}

Key = new CompositeFontFamilyKey(new Uri(FontManager.CompositeFontScheme + ":" + name, UriKind.Absolute), keys);
}
}

/// <summary>
Expand Down Expand Up @@ -88,44 +113,49 @@ public static implicit operator FontFamily(string s)
return new FontFamily(s);
}

private struct FontFamilyIdentifier
private static FrugalStructList<FontSourceIdentifier> GetFontSourceIdentifier(string name)
{
public FontFamilyIdentifier(string name, Uri? source)
{
Name = name;
Source = source;
}

public string Name { get; }

public Uri? Source { get; }
}
var result = new FrugalStructList<FontSourceIdentifier>(1);

private static FontFamilyIdentifier GetFontFamilyIdentifier(string name)
{
var segments = name.Split('#');
var segments = name.Split(',');

switch (segments.Length)
for (int i = 0; i < segments.Length; i++)
{
case 1:
{
return new FontFamilyIdentifier(segments[0], null);
}
var segment = segments[i];
var innerSegments = segment.Split('#');

case 2:
{
var source = segments[0].StartsWith("/", StringComparison.Ordinal)
? new Uri(segments[0], UriKind.Relative)
: new Uri(segments[0], UriKind.RelativeOrAbsolute);
FontSourceIdentifier identifier;

return new FontFamilyIdentifier(segments[1], source);
}
switch (innerSegments.Length)
{
case 1:
{
identifier = new FontSourceIdentifier(innerSegments[0].Trim(), null);
break;
}

case 2:
{
var source = innerSegments[0].StartsWith("/", StringComparison.Ordinal)
? new Uri(innerSegments[0], UriKind.Relative)
: new Uri(innerSegments[0], UriKind.RelativeOrAbsolute);

identifier = new FontSourceIdentifier(innerSegments[1].Trim(), source);

break;
}

default:
{
identifier = new FontSourceIdentifier(name, null);
break;
}
}

default:
{
return new FontFamilyIdentifier(name, null);
}
result.Add(identifier);
}

return result;
}

/// <summary>
Expand Down
126 changes: 83 additions & 43 deletions src/Avalonia.Base/Media/FontManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ namespace Avalonia.Media
/// </summary>
public sealed class FontManager
{
internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts");
internal static Uri SystemFontsKey = new Uri("fonts:SystemFonts", UriKind.Absolute);

public const string FontCollectionScheme = "fonts";
public const string SystemFontScheme = "systemfont";
public const string CompositeFontScheme = "compositefont";

private readonly ConcurrentDictionary<Uri, IFontCollection> _fontCollections = new ConcurrentDictionary<Uri, IFontCollection>();
private readonly IReadOnlyList<FontFallback>? _fontFallbacks;
Expand Down Expand Up @@ -95,69 +97,86 @@ public bool TryGetGlyphTypeface(Typeface typeface, [NotNullWhen(true)] out IGlyp

var fontFamily = typeface.FontFamily;

if(typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
if (typeface.FontFamily.Name == FontFamily.DefaultFontFamilyName)
{
return TryGetGlyphTypeface(new Typeface(DefaultFontFamily, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}

if (fontFamily.Key is FontFamilyKey key)
if (fontFamily.Key is FontFamilyKey)
{
var source = key.Source;

if (!source.IsAbsoluteUri)
if (fontFamily.Key is CompositeFontFamilyKey compositeKey)
{
if (key.BaseUri == null)
for (int i = 0; i < compositeKey.Keys.Count; i++)
{
throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
}
var key = compositeKey.Keys[i];

source = new Uri(key.BaseUri, source);
var familyName = fontFamily.FamilyNames[i];

if (TryGetGlyphTypefaceByKeyAndName(typeface, key, familyName, out glyphTypeface) &&
glyphTypeface.FamilyName.Contains(familyName))
{
return true;
}
}
}

if (!_fontCollections.TryGetValue(source, out var fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
else
{
var embeddedFonts = new EmbeddedFontCollection(source, source);

embeddedFonts.Initialize(PlatformImpl);

if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
if (TryGetGlyphTypefaceByKeyAndName(typeface, fontFamily.Key, fontFamily.FamilyNames.PrimaryFamilyName, out glyphTypeface))
{
fontCollection = embeddedFonts;
return true;
}
}

if (fontCollection != null && fontCollection.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName,
typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
return false;
}
}
else
{
if (SystemFonts.TryGetGlyphTypeface(fontFamily.FamilyNames.PrimaryFamilyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return true;
}
}

if (!fontFamily.FamilyNames.HasFallbacks)
{
return false;
}
if (typeface.FontFamily == DefaultFontFamily)
{
return false;
}

for (var i = 0; i < fontFamily.FamilyNames.Count; i++)
//Nothing was found so use the default
return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
}

private bool TryGetGlyphTypefaceByKeyAndName(Typeface typeface, FontFamilyKey key, string familyName, [NotNullWhen(true)] out IGlyphTypeface? glyphTypeface)
{
var source = key.Source;

if (source.Scheme == SystemFontScheme)
{
var familyName = fontFamily.FamilyNames[i];
return SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface);
}

if (SystemFonts.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
if (!source.IsAbsoluteUri)
{
if (key.BaseUri == null)
{
if (!fontFamily.FamilyNames.HasFallbacks || glyphTypeface.FamilyName != DefaultFontFamily.Name)
{
return true;
}
throw new NotSupportedException($"{nameof(key.BaseUri)} can't be null.");
}

source = new Uri(key.BaseUri, source);
}

if(typeface.FontFamily == DefaultFontFamily)
if (TryGetFontCollection(source, out var fontCollection) &&
fontCollection.TryGetGlyphTypeface(familyName, typeface.Style, typeface.Weight, typeface.Stretch, out glyphTypeface))
{
return false;
if (glyphTypeface.FamilyName.Contains(familyName))
{
return true;
}
}

//Nothing was found so use the default
return TryGetGlyphTypeface(new Typeface(FontFamily.DefaultFontFamilyName, typeface.Style, typeface.Weight, typeface.Stretch), out glyphTypeface);
glyphTypeface = null;

return false;
}

/// <summary>
Expand Down Expand Up @@ -230,24 +249,45 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fon
}

//Try to match against fallbacks first
if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
if (fontFamily != null && fontFamily.Key is CompositeFontFamilyKey compositeKey)
{
for (int i = 1; i < fontFamily.FamilyNames.Count; i++)
for (int i = 0; i < compositeKey.Keys.Count; i++)
{
var key = compositeKey.Keys[i];
var familyName = fontFamily.FamilyNames[i];

foreach (var fontCollection in _fontCollections.Values)
if (TryGetFontCollection(key.Source, out var fontCollection) &&
fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
if (fontCollection.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, familyName, culture, out typeface))
{
return true;
};
return true;
}
}
}

//Try to find a match with the system font manager
return PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontStretch, culture, out typeface);
}

private bool TryGetFontCollection(Uri source, [NotNullWhen(true)] out IFontCollection? fontCollection)
{
if(source.Scheme == SystemFontScheme)
{
source = SystemFontsKey;
}

if (!_fontCollections.TryGetValue(source, out fontCollection) && (source.IsAbsoluteResm() || source.IsAvares()))
{
var embeddedFonts = new EmbeddedFontCollection(source, source);

embeddedFonts.Initialize(PlatformImpl);

if (embeddedFonts.Count > 0 && _fontCollections.TryAdd(source, embeddedFonts))
{
fontCollection = embeddedFonts;
}
}

return fontCollection != null;
}
}
}
17 changes: 17 additions & 0 deletions src/Avalonia.Base/Media/FontSourceIdentifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;

namespace Avalonia.Media
{
internal readonly record struct FontSourceIdentifier
{
public FontSourceIdentifier(string name, Uri? source)
{
Name = name;
Source = source;
}

public string Name { get; init; }

public Uri? Source { get; init; }
}
}
14 changes: 14 additions & 0 deletions src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,20 @@ public FamilyNameCollection(string familyNames)
HasFallbacks = _names.Length > 1;
}

internal FamilyNameCollection(FrugalStructList<FontSourceIdentifier> fontSources)
{
_names = new string[fontSources.Count];

for (int i = 0; i < fontSources.Count; i++)
{
_names[i] = fontSources[i].Name;
}

PrimaryFamilyName = _names[0];

HasFallbacks = _names.Length > 1;
}

private static string[] SplitNames(string names)
#if NET6_0_OR_GREATER
=> names.Split(',', StringSplitOptions.TrimEntries);
Expand Down
Loading

0 comments on commit cb461ab

Please sign in to comment.