Skip to content

Commit

Permalink
Fix more things with fonts (embedded) and unpackaged apps (#26515)
Browse files Browse the repository at this point in the history
* Fix embedded fonts in unpackaged apps

* Add UI tests

* ws

* ws

* reviewability

* comments

* Update src/Core/src/Fonts/EmbeddedFontLoader.Windows.cs

Co-authored-by: Copilot <[email protected]>

* Update src/Core/src/Fonts/EmbeddedFontLoader.Windows.cs

Co-authored-by: Copilot <[email protected]>

* fix

* ws

* wow

* Update path from AppDataDirectory to LocalCacheFolder

* Fix embedded fonts in unpackaged apps

* Add UI tests

* ws

* ws

* reviewability

* comments

* Update src/Core/src/Fonts/EmbeddedFontLoader.Windows.cs

Co-authored-by: Copilot <[email protected]>

* Update src/Core/src/Fonts/EmbeddedFontLoader.Windows.cs

Co-authored-by: Copilot <[email protected]>

* fix

* ws

* wow

* new category

* Add binlogs and snapshots

* android and ios images

* echo contents

* revert

* log the loggable logging loggers loggity log

* oops

* fix paths

* Fix unpackaged tests

* mac images

* try and get a fail

* Everything MUST fail

* logging

* logging

* what does CI see now?

* try something

* A bunch of work to re-enable tests and fix things

* more info

* tests

* - add mac screen shot

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Shane Neuville <[email protected]>
  • Loading branch information
3 people authored Jan 8, 2025
1 parent 8b9f89e commit a0ea0bc
Show file tree
Hide file tree
Showing 35 changed files with 360 additions and 47 deletions.
2 changes: 1 addition & 1 deletion eng/pipelines/common/ui-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ parameters:
- 'Border,BoxView,Brush,Button'
- 'CarouselView,Cells,CheckBox,CollectionView,ContextActions,CustomRenderers'
- 'DatePicker,Dispatcher,DisplayAlert,DisplayPrompt,DragAndDrop'
- 'Editor,Effects,Entry,FlyoutPage,Focus,Frame,Gestures,GraphicsView'
- 'Editor,Effects,Entry,FlyoutPage,Focus,Fonts,Frame,Gestures,GraphicsView'
- 'Image,ImageButton,IndicatorView,InputTransparent,IsEnabled,IsVisible'
- 'Label,Layout,Lifecycle,ListView'
- 'ManualReview,Maps,Navigation'
Expand Down
8 changes: 8 additions & 0 deletions src/Controls/tests/CustomAttributes/Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,14 @@ public enum CarouselView
IsBounceEnabled
}

public enum Fonts
{
FromBundle_Label,
FromBundle_Image,
FromEmbedded_Label,
FromEmbedded_Image,
}

public enum ImageLoading
{
FromBundleSvg,
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions src/Controls/tests/TestCases.HostApp/Concepts/FontsGalleryPage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace Maui.Controls.Sample;

internal class FontsGalleryPage : CoreGalleryBasePage
{
protected override void Build()
{
AddImage(Test.Fonts.FromEmbedded_Image, "q", "Dokdo");
AddLabel(Test.Fonts.FromEmbedded_Label, "q", "Dokdo");

AddImage(Test.Fonts.FromBundle_Image, "\xf133", "FA");
AddLabel(Test.Fonts.FromBundle_Label, "\xf133", "FA");
}

ViewContainer<View> AddImage(Test.Fonts test, string text, string fontFamily) =>
Add(test, new Image
{
Source = new FontImageSource
{
Glyph = text,
FontFamily = fontFamily,
Size = 40,
Color = Colors.Red
},
WidthRequest = 50,
HeightRequest = 50,
});

ViewContainer<View> AddLabel(Test.Fonts test, string text, string fontFamily) =>
Add(test, new Label
{
Text = text,
FontFamily = fontFamily,
FontSize = 40,
TextColor = Colors.Red,
WidthRequest = 50,
HeightRequest = 50,
});

ViewContainer<View> Add(Test.Fonts test, View view) =>
Add(new ViewContainer<View>(test, view));
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@
<MauiImage Include="Resources\AppIcons\appicon.svg" ForegroundFile="Resources\AppIcons\appicon_foreground.svg" IsAppIcon="true" />
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#FFFFFF" BaseSize="168,208" />
<MauiFont Include="Resources\Fonts\**" />
<MauiFont Remove="Resources\Fonts\Dokdo-Regular.ttf" />
<EmbeddedResource Include="Resources\Fonts\Dokdo-Regular.ttf" />
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public override string ToString()
// Concepts & Abstracts
new GalleryPageFactory(() => new BorderGallery(), "Border Gallery"),
new GalleryPageFactory(() => new DragAndDropGallery(), "Drag and Drop Gallery"),
new GalleryPageFactory(() => new FontsGalleryPage(), "Fonts Gallery"),
new GalleryPageFactory(() => new GestureRecognizerGallery(), "Gesture Recognizer Gallery"),
new GalleryPageFactory(() => new InputTransparencyGalleryPage(), "Input Transparency Gallery"),
new GalleryPageFactory(() => new ImageLoadingGalleryPage(), "Image Loading Gallery"),
Expand Down
1 change: 1 addition & 0 deletions src/Controls/tests/TestCases.HostApp/MauiProgram.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public static MauiApp CreateMauiApp()
appBuilder.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddEmbeddedResourceFont(typeof(MauiProgram).Assembly, "Dokdo-Regular.ttf", "Dokdo");
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("FontAwesome.ttf", "FA");
fonts.AddFont("ionicons.ttf", "Ion");
Expand Down
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using NUnit.Framework;
using UITest.Appium;
using UITest.Core;

namespace Microsoft.Maui.TestCases.Tests
{
[Category(UITestCategories.Fonts)]
public class FontsGalleryTests : CoreGalleryBasePageTest
{
public FontsGalleryTests(TestDevice device)
: base(device)
{
}

protected override void NavigateToGallery()
{
App.NavigateToGallery("Fonts Gallery");
}

[Test]
public void FromEmbedded_Image() => LoadAndVerify(Test.Fonts.FromEmbedded_Image);

[Test]
public void FromEmbedded_Label() => LoadAndVerify(Test.Fonts.FromEmbedded_Label);

[Test]
public void FromBundle_Image() => LoadAndVerify(Test.Fonts.FromBundle_Image);

#if WINDOWS
[Ignore("Windows App SDK 1.6 broke this test. See more details in https://github.com/dotnet/maui/issues/26749")]
#endif
[Test]
public void FromBundle_Label() => LoadAndVerify(Test.Fonts.FromBundle_Label);

void LoadAndVerify(Test.Fonts test)
{
var remote = new EventViewContainerRemote(UITestContext, test);
remote.GoTo(test.ToString());

App.WaitForElement($"{test}VisualElement");

VerifyScreenshot();
}
}
}
21 changes: 15 additions & 6 deletions src/Controls/tests/TestCases.Shared.Tests/Tests/LabelUITests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,29 @@ public void SpanTapped()
ClassicAssert.AreEqual("Event: SpanTapped (fired 1)", textAfterClick);
}

[Test]
public void FontFamilyLoadsDynamically()
{
var remote = GoToStateRemote("FontFamily");

remote.TapStateButton();
VerifyScreenshot("LabelUITests_FontFamily_Ionicons");
}

#if WINDOWS
[Ignore("Windows App SDK 1.6 broke this test. See more details in https://github.com/dotnet/maui/issues/26749")]
#endif
[Test]
public void FontFamily()
{
var remote = GoToStateRemote();

//VerifyScreenshot("LabelUITests_FontFamily_FontAwesome");
VerifyScreenshot("LabelUITests_FontFamily_FontAwesome");

// This works though?!
remote.TapStateButton();
VerifyScreenshot("LabelUITests_FontFamily_Ionicons");

// TODO: fix me -- broken in WinAppSDK 1.6 for some unknown reason
// Issue12153 covers font awesome tests, so not totally sure what's going on
//remote.TapStateButton();
//VerifyScreenshot("LabelUITests_FontFamily_FontAwesome");
remote.TapStateButton();
VerifyScreenshot("LabelUITests_FontFamily_FontAwesome");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,6 @@ internal static class UITestCategories
public const string Brush = "Brush";
public const string Compatibility = "MovedFromCompatibility";
public const string GraphicsView = "GraphicsView";
public const string Fonts = "Fonts";
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
125 changes: 110 additions & 15 deletions src/Core/src/Fonts/EmbeddedFontLoader.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,60 +2,155 @@
using System;
using System.IO;
using Microsoft.Extensions.Logging;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Storage;
using Windows.Storage;

namespace Microsoft.Maui
{
/// <inheritdoc/>
public partial class EmbeddedFontLoader
{
const string FontCacheFolderName = "fonts";
const string FontCacheFolderName = "Fonts";

/// <inheritdoc/>
public string? LoadFont(EmbeddedFont font)
{
var tmpdir = ApplicationData.Current.LocalFolder.CreateFolderAsync(FontCacheFolderName, CreationCollisionOption.OpenIfExists).AsTask().Result;
if (string.IsNullOrWhiteSpace(font.FontName))
{
throw new InvalidOperationException("FontName for embedded font was null or empty.");
}
if (font.ResourceStream is null)
{
throw new InvalidOperationException("ResourceStream for embedded font was null.");
}

return AppInfoUtils.IsPackagedApp
? LoadFontPackaged(font.FontName, font.ResourceStream)
: LoadFontUnpackaged(font.FontName, font.ResourceStream);
}

private string? LoadFontPackaged(string fontName, Stream resourceStream)
{
var tmpdir = ApplicationData.Current.TemporaryFolder.CreateFolderAsync(FontCacheFolderName, CreationCollisionOption.OpenIfExists).AsTask().Result;

var file = tmpdir.TryGetItemAsync(font.FontName).AsTask().Result;
if (file != null)
return CleanseFilePath(file.Path);
// return an existing file from the cache
var existingFile = tmpdir.TryGetItemAsync(fontName).AsTask().Result;
if (existingFile is not null)
{
return CleansePackagedFilePath(existingFile.Path);
}

// copy the file into the cache
StorageFile? newFile = null;
try
{
if (font.ResourceStream == null)
throw new InvalidOperationException("ResourceStream was null.");

newFile = tmpdir.CreateFileAsync(font.FontName).AsTask().Result;
newFile = tmpdir.CreateFileAsync(fontName).AsTask().Result;
using (var fileStream = newFile.OpenStreamForWriteAsync().Result)
{
font.ResourceStream.CopyTo(fileStream);
resourceStream.CopyTo(fileStream);
}

return CleanseFilePath(newFile.Path);
return CleansePackagedFilePath(newFile.Path);
}
catch (Exception ex)
{
_serviceProvider?.CreateLogger<FontManager>()?.LogWarning(ex, "Unable copy font {Font} to local file system.", font.FontName);
_serviceProvider?.CreateLogger<FontManager>()?.LogWarning(ex, "Unable to copy font {Font} to the local file system.", fontName);
}

// something went wrong!

if (newFile != null)
// first, clean up the font just in case we wrote some bad data
if (newFile is not null)
{
try
{
newFile.DeleteAsync().AsTask().Wait();
}
catch (Exception ex)
{
_serviceProvider?.CreateLogger<FontManager>()?.LogWarning(ex, "Unable to delete font {Font} from local file system.", fontName);
}
}

// then, return null
return null;
}

static string CleanseFilePath(string filePath)
static string CleansePackagedFilePath(string filePath)
{
var fontName = Path.GetFileName(filePath);

filePath = Path.Combine("local", FontCacheFolderName, fontName);
filePath = Path.Combine("temp", FontCacheFolderName, fontName);

var baseUri = new Uri("ms-appdata://");
var uri = new Uri(baseUri, filePath);
var relativePath = uri.ToString().TrimEnd('/');

return relativePath;
}

static string CleanseUnpackagedFilePath(string filePath)
{
// yes, ms-appx, yes, it is the way... sometimes #acceptance
//
// The ms-appx prefix is used for fonts, regardless of the location,
// whether in the package or in the local file system.
// The times it is not used is when it is in another, special
// location, such as in the app data.

var baseUri = new Uri("ms-appx://");
var uri = new Uri(baseUri, "ms-appx://" + filePath);
var relativePath = uri.ToString().TrimEnd('/');

return relativePath;
}

private string? LoadFontUnpackaged(string fontName, Stream resourceStream)
{
var tmpdir = Path.Combine(FileSystem.CacheDirectory, "..", FontCacheFolderName);
tmpdir = Path.GetFullPath(tmpdir);

// return an existing file from the cache
var file = Path.Combine(tmpdir, fontName);
if (File.Exists(file))
{
return CleanseUnpackagedFilePath(file);
}

// copy the file into the cache
try
{
Directory.CreateDirectory(tmpdir);
using (var fileStream = File.Create(file))
{
resourceStream.CopyTo(fileStream);
}

return CleanseUnpackagedFilePath(file);
}
catch (Exception ex)
{
_serviceProvider?.CreateLogger<FontManager>()?.LogWarning(ex, "Unable to copy font {Font} to the local file system.", fontName);
}

// something went wrong!

// first, clean up the font just in case we wrote some bad data
try
{
if (File.Exists(file))
{
File.Delete(file);
}
}
catch (Exception ex)
{
_serviceProvider?.CreateLogger<FontManager>()?.LogWarning(ex, "Unable to delete font {Font} from local file system.", fontName);
}

// then, return null
return null;
}
}
}
7 changes: 5 additions & 2 deletions src/Core/src/Fonts/FontManager.Windows.cs
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,13 @@ IEnumerable<string> GetAllFontPossibilities(string fontFamily)
{
var fontUri = new Uri(fontFile, UriKind.RelativeOrAbsolute);

// unpackaged apps can't load files using packaged schemes
// Win2D in unpackaged apps can't load files using packaged schemes, such as `ms-appx://`
// so we have to first convert it to a `file://` scheme will the full file path.
// At this part of the load operation, the font URI does NOT yet have the font family name
// fragment component, so we don't have to remove it.
if (!AppInfoUtils.IsPackagedApp)
{
var path = fontUri.AbsolutePath.TrimStart('/');
var path = fontUri.LocalPath.TrimStart('/');
if (FileSystemUtils.TryGetAppPackageFileUri(path, out var uri))
fontUri = new Uri(uri, UriKind.RelativeOrAbsolute);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,20 +124,22 @@ string GetFontSource(IFontImageSource imageSource)
}
}

// unpackaged apps can't load files using packaged schemes
// Win2D in unpackaged apps can't load files using packaged schemes, such as `ms-appx://`
// so we have to first convert it to a `file://` scheme will the full file path.
if (!AppInfoUtils.IsPackagedApp)
{
// the Uri type encodes the fragment, so let's remove first
// At this part of the load operation, the font URI contains the font family name fragment
// component, so we first have to remove it.
var fragment = "";
if (fontSource.IndexOf('#', StringComparison.OrdinalIgnoreCase) is int index && index >= 0)
{
fragment = fontSource.Substring(index);
fontSource = fontSource.Substring(0, index);
fragment = fontSource[index..];
fontSource = fontSource[..index];
}

// Now we can convert the URI to a `file://` scheme with the full file path.
var fontUri = new Uri(fontSource, UriKind.RelativeOrAbsolute);

var path = fontUri.AbsolutePath.TrimStart('/');
var path = fontUri.LocalPath.TrimStart('/');
if (FileSystemUtils.TryGetAppPackageFileUri(path, out var uri))
{
fontSource = uri + fragment;
Expand Down
Loading

0 comments on commit a0ea0bc

Please sign in to comment.