From cb461ab04383e7421b9a9da95129ae73fe15d692 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 14 Sep 2023 11:58:55 +0200 Subject: [PATCH] Allow multiple font sources per FontFamily and make sure combinations of system and embedded fonts can be used (#12871) --- .../Media/CompositeFontFamilyKey.cs | 16 +++ src/Avalonia.Base/Media/FontFamily.cs | 104 ++++++++++----- src/Avalonia.Base/Media/FontManager.cs | 126 ++++++++++++------ .../Media/FontSourceIdentifier.cs | 17 +++ .../Media/Fonts/FamilyNameCollection.cs | 14 ++ .../Media/Fonts/FontCollectionBase.cs | 6 +- .../Media/FontFamilyTests.cs | 2 +- .../Media/FontManagerImplTests.cs | 12 +- .../Media/FontManagerTests.cs | 78 +++++++++++ 9 files changed, 286 insertions(+), 89 deletions(-) create mode 100644 src/Avalonia.Base/Media/CompositeFontFamilyKey.cs create mode 100644 src/Avalonia.Base/Media/FontSourceIdentifier.cs diff --git a/src/Avalonia.Base/Media/CompositeFontFamilyKey.cs b/src/Avalonia.Base/Media/CompositeFontFamilyKey.cs new file mode 100644 index 00000000000..eed71cd4e2b --- /dev/null +++ b/src/Avalonia.Base/Media/CompositeFontFamilyKey.cs @@ -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 Keys { get; } + } +} diff --git a/src/Avalonia.Base/Media/FontFamily.cs b/src/Avalonia.Base/Media/FontFamily.cs index 498bcd43a08..80365aaf4d0 100644 --- a/src/Avalonia.Base/Media/FontFamily.cs +++ b/src/Avalonia.Base/Media/FontFamily.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using Avalonia.Media.Fonts; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -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); + } } /// @@ -88,44 +113,49 @@ public static implicit operator FontFamily(string s) return new FontFamily(s); } - private struct FontFamilyIdentifier + private static FrugalStructList 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(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; } /// diff --git a/src/Avalonia.Base/Media/FontManager.cs b/src/Avalonia.Base/Media/FontManager.cs index 17d1984286f..850d0011f5d 100644 --- a/src/Avalonia.Base/Media/FontManager.cs +++ b/src/Avalonia.Base/Media/FontManager.cs @@ -15,9 +15,11 @@ namespace Avalonia.Media /// 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 _fontCollections = new ConcurrentDictionary(); private readonly IReadOnlyList? _fontFallbacks; @@ -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; } /// @@ -230,18 +249,17 @@ 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; } } } @@ -249,5 +267,27 @@ public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, FontWeight fon //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; + } } } diff --git a/src/Avalonia.Base/Media/FontSourceIdentifier.cs b/src/Avalonia.Base/Media/FontSourceIdentifier.cs new file mode 100644 index 00000000000..a4c89bbcb8c --- /dev/null +++ b/src/Avalonia.Base/Media/FontSourceIdentifier.cs @@ -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; } + } +} diff --git a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs index f2350f5aeab..dabe935b768 100644 --- a/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Base/Media/Fonts/FamilyNameCollection.cs @@ -28,6 +28,20 @@ public FamilyNameCollection(string familyNames) HasFallbacks = _names.Length > 1; } + internal FamilyNameCollection(FrugalStructList 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); diff --git a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs index 713b3dafcd4..3daa19c788b 100644 --- a/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs +++ b/src/Avalonia.Base/Media/Fonts/FontCollectionBase.cs @@ -34,7 +34,7 @@ public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, { if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) { - match = new Typeface(glyphTypeface.FamilyName, style, weight, stretch); + match = new Typeface(Key.AbsoluteUri + "#" + glyphTypeface.FamilyName, style, weight, stretch); return true; } @@ -45,9 +45,9 @@ public bool TryMatchCharacter(int codepoint, FontStyle style, FontWeight weight, { if (TryGetGlyphTypeface(familyName, style, weight, stretch, out var glyphTypeface)) { - if (glyphTypeface.TryGetGlyph((uint)codepoint, out _)) + if (glyphTypeface.FamilyName.Contains(familyName) && glyphTypeface.TryGetGlyph((uint)codepoint, out _)) { - match = new Typeface(familyName, style, weight, stretch); + match = new Typeface(Key.AbsoluteUri + "#" + familyName, style, weight, stretch); return true; } diff --git a/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs b/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs index 1f8ae9bd8ba..73c46a92959 100644 --- a/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/FontFamilyTests.cs @@ -75,7 +75,7 @@ public void Should_Parse_FontFamily_With_Fallbacks() Assert.Equal("Courier New", fontFamily.Name); - Assert.Equal(2, fontFamily.FamilyNames.Count()); + Assert.Equal(2, fontFamily.FamilyNames.Count); Assert.Equal("Times New Roman", fontFamily.FamilyNames.Last()); } diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index 81ac9030bf3..15e85750eac 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Direct2D1.Media; +using Avalonia.Direct2D1.Media; using Avalonia.Media; using Avalonia.UnitTests; using Xunit; @@ -17,8 +16,9 @@ public void Should_Create_Typeface_From_Fallback() { Direct2D1Platform.Initialize(); - var glyphTypeface = - new Typeface(new FontFamily("A, B, Arial")).GlyphTypeface; + var typeface = new Typeface(new FontFamily("A, B, Arial")); + + var glyphTypeface = typeface.GlyphTypeface; Assert.Equal("Arial", glyphTypeface.FamilyName); } @@ -31,7 +31,9 @@ public void Should_Create_Typeface_From_Fallback_Bold() { Direct2D1Platform.Initialize(); - var glyphTypeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold).GlyphTypeface; + var typeface = new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold); + + var glyphTypeface = typeface.GlyphTypeface; Assert.Equal("Arial", glyphTypeface.FamilyName); diff --git a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs index 0818510bc21..98971426f14 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerTests.cs @@ -139,5 +139,83 @@ public void Should_Return_False_For_Invalid_DefaultFontFamily() } } } + + [Fact] + public void Should_Load_Embedded_Fallbacks() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + var fontFamily = FontFamily.Parse("NotFound, " + s_fontUri); + + var typeface = new Typeface(fontFamily); + + var glyphTypeface = typeface.GlyphTypeface; + + Assert.NotNull(glyphTypeface); + + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + } + } + } + + [Fact] + public void Should_Match_Chararcter_Width_Embedded_Fallbacks() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + var fontFamily = FontFamily.Parse("NotFound, " + s_fontUri); + + Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface)); + + var glyphTypeface = typeface.GlyphTypeface; + + Assert.NotNull(glyphTypeface); + + Assert.Equal("Noto Mono", glyphTypeface.FamilyName); + } + } + } + + [Fact] + public void Should_Match_Chararcter_From_SystemFonts() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, null, null, out var typeface)); + + var glyphTypeface = typeface.GlyphTypeface; + + Assert.NotNull(glyphTypeface); + + Assert.Equal(FontManager.Current.DefaultFontFamily.Name, glyphTypeface.FamilyName); + } + } + } + + [Fact] + public void Should_Match_Chararcter_Width_Fallbacks() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new FontManagerImpl()))) + { + using (AvaloniaLocator.EnterScope()) + { + var fontFamily = FontFamily.Parse("NotFound, Unknown"); + + Assert.True(FontManager.Current.TryMatchCharacter('A', FontStyle.Normal, FontWeight.Normal, FontStretch.Normal, fontFamily, null, out var typeface)); + + var glyphTypeface = typeface.GlyphTypeface; + + Assert.NotNull(glyphTypeface); + + Assert.Equal(FontManager.Current.DefaultFontFamily.Name, glyphTypeface.FamilyName); + } + } + } } }