Avalonia: NullReferenceException without explicit font specification

Created on 23 Sep 2018  路  21Comments  路  Source: AvaloniaUI/Avalonia

Running my app on Linux Mate on a Raspberry gives me the following error

Unhandled Exception: System.NullReferenceException: Object reference not set to an instance of an object.
   at Avalonia.Skia.TypefaceCache.GetTypeface(String name, FontKey key)
   at Avalonia.Skia.FormattedTextImpl..ctor(String text, Typeface typeface, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList`1 spans)
   at Avalonia.Skia.PlatformRenderInterface.CreateFormattedText(String text, Typeface typeface, TextAlignment textAlignment, TextWrapping wrapping, Size constraint, IReadOnlyList`1 spans)
   at Avalonia.Controls.TextBlock.MeasureOverride(Size availableSize)
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize)
   at Avalonia.Layout.Layoutable.Measure(Size availableSize)
   at Avalonia.Controls.Grid.<>c__DisplayClass25_0.<MeasureOverride>g__MeasureOnce|2(Control child, Size size)
   at Avalonia.Controls.Grid.<>c__DisplayClass25_0.<MeasureOverride>b__0(Control child)
   at Avalonia.Controls.Utils.GridLayout.AppendMeasureConventions[T](IDictionary`2 source, Func`2 getDesiredLength)
   at Avalonia.Controls.Grid.MeasureOverride(Size constraint)
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize)
   at Avalonia.Layout.Layoutable.Measure(Size availableSize)
   at Avalonia.Controls.Grid.<>c__DisplayClass25_0.<MeasureOverride>g__MeasureOnce|2(Control child, Size size)
   at Avalonia.Controls.Grid.<>c__DisplayClass25_0.<MeasureOverride>b__0(Control child)
   at Avalonia.Controls.Utils.GridLayout.AppendMeasureConventions[T](IDictionary`2 source, Func`2 getDesiredLength)
   at Avalonia.Controls.Grid.MeasureOverride(Size constraint)
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize)
   at Avalonia.Layout.Layoutable.Measure(Size availableSize)
   at Avalonia.Layout.LayoutHelper.MeasureChild(ILayoutable control, Size availableSize, Thickness padding)
   at Avalonia.Controls.Presenters.ContentPresenter.MeasureOverride(Size availableSize)
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize)
   at Avalonia.Layout.Layoutable.Measure(Size availableSize)
   at Avalonia.Layout.LayoutHelper.MeasureChild(ILayoutable control, Size availableSize, Thickness padding)
   at Avalonia.Controls.Primitives.AdornerDecorator.MeasureOverride(Size availableSize)
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize)
   at Avalonia.Layout.Layoutable.Measure(Size availableSize)
   at Avalonia.Layout.LayoutHelper.MeasureChild(ILayoutable control, Size availableSize, Thickness padding)
   at Avalonia.Controls.Border.MeasureOverride(Size availableSize)
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize)
   at Avalonia.Layout.Layoutable.Measure(Size availableSize)
   at Avalonia.Layout.Layoutable.MeasureOverride(Size availableSize)
   at Avalonia.Controls.Window.MeasureOverride(Size availableSize)
   at Avalonia.Layout.Layoutable.MeasureCore(Size availableSize)
   at Avalonia.Layout.Layoutable.Measure(Size availableSize)
   at Avalonia.Layout.LayoutManager.Measure(ILayoutable control)
   at Avalonia.Layout.LayoutManager.ExecuteInitialLayoutPass(ILayoutRoot root)
   at Avalonia.Controls.Window.Show()
   at Avalonia.Application.Run(Window mainWindow)
   at Avalonia.Controls.AppBuilderBase`1.Start[TMainWindow](Func`1 dataContextProvider)
   at MyApp.Program.Main(String[] args) in C:\MyApp\Program.cs:line 11
Aborted

The error is resolved by setting the FontFamily property on my main window. In this case I have to set it to _Ubuntu_. However, when I run the app on Linux Raspian it fails again with the same error. Setting the FontFamily to _Piboto_ solves it. Actually all I want is that the app uses the default desktop font, whatever that is, without specifying it explicitly. Is this is problem in Skia or Avalonia or are we as application developers to required to fulfill any prerequisites?

All 21 comments

@grokys @Gillibald I think this is our default font (which is pretty ugly) is not available on all OS, so we need to provide a set of fallbacks for default font to minimize the chance of no font being found.

I wonder if the crash could have been avoided too

We can define the default with a fallback that should be available everywhere or just embed a font that can be used if everything fails. Maybe we can't define a default for all platforms and each platform should define a font that can be used as a fallback.

We could probably embed the regular version of Selawik or Open Sans as the last resort/default font fallback. Though iirc some apps just specify a "Sans" string on font names. I'm not sure/Can't test right now if that works

It would be great to have the app at least not crash. But what I don't understand: on Windows an app that hasn't specified any font seems to use the systems default font. Doesn't that make sense? Is this not possible on Linux in some way? The app would use "Ubuntu" on Ubuntu and "Piboto" on Raspian, f.e.. I think that would give it a consistent look with other applications. If I want a specific font to be used and that font is not found on the system, should then not also the system default font be used? Why is there a need for an embedded font? (Sorry for my ignorance, I guess you have good reasons...)

I checked my app with "Sans". On Raspian it doesn't work, same exception.

Would there be a way to specify the font in a configuration file? That would at least allow me to use the same built of my app on multiple distros. I don't know where to load the font name. It looks like the exception is thrown before I have a chance to set the font programmatically.

In theory it should be possible to set the FontFamily on every window to override the default.

I don't think there is a font API that is present on every system. Embedding a default font would just make it easier to support more platforms without implementing platform specific stuff. You can always override the default as written above.

Ok, to solve my current problem I call the following in my MainWindow constructor

if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
    this.FontFamily = "Piboto";
}

This works on Windows and Raspian (which uses Piboto). Windows just applies its default font. However, for Ubuntu I need to set the "Ubuntu" font. I had hoped that this would work

this.FontFamily = "Ubuntu, Piboto";

But on Raspian it again fails with NullReferenceException.
Is it possible at all to specify multiple fonts like in HTML/CSS?

If not, I would have to detect the actual platform and set the font accordingly.

(In practice I could of course read the font to use from a configuration file, then I would be open for new platforms)

https://github.com/AvaloniaUI/Avalonia/pull/1564 You can define fallbacks that should be used if a Font isn't installed on the system.

FontFamily = "Ubuntu, Piboto" should work if at least one is installed.

If that fails it is a bug.

Ok, thanks for the link. I put my font list into a config file and load it in the constructor so I can easily test different configurations on the Raspberry PI. The result is striking. On Ubuntu and Windows the font fallback list works as expected. On Linux Raspian this works

FontFamily = "Piboto, Ubuntu"

this fails

FontFamily = "Ubuntu, Piboto"

with NullReferenceException. I checked with fc-list that Piboto is available and Ubuntu is not! On Ubuntu Piboto is missing and Ubuntu is (of course) available and it works in any order! Isn't that strange?

There is a bug in the fallback logic that I will fix tomorrow.

I actually quite like the idea that if you don't specify a font, it just uses the system font. It would require communication between the rendering backend and the windowing backend, but I think that would be do-able. We'd also need a way to select the system font explicitly too though.

Looks like I was wrong that there is a bug with the font fallback. At least on Windows I get the system default font if I a font can't be found. Could be some issue with your Skia version. Is Fontconfig included in your binary? I think Skia uses Fontconfig to obtain this kind of information but could be wrong.

Can you try this line on your Raspberry?

var typeface = SKTypeface.FromFamilyName(FontFamily.Default.Name);

Referencing SkiaSharp in your project should be enough.

Regarding libSkiaSharp, I built this my own as no one provides binaries for ARM. Took quite a time :-( But I am pretty sure that FontConfig was included because in the first run the compiler complained about the missing header and later the linker complained about the missing *.so file. So I had to add these files to the cross compiler build environment.

I tested your code on the Raspian system which makes the trouble. This is the result:

FontFamily.Default.Name: Courier New
typeface.FamilyName: Liberation Mono

I still don't get why the order of the font fallbacks matters. Any more tests I can do for you?

So Liberation Mono is the system default. I thought this would fail and therefore causes the exception.

All I can say is that when I don't specify any font or a font that doesn't exist, the exception is thrown on Raspian, not so on Ubuntu/Mate!
I have now done this test:

var typeface = SKTypeface.FromFamilyName("Ubuntu");

and it returns null on Raspian, for whatever reason! Looking at the code of the TypefaceCache

typeface = SKTypeface.FromFamilyName(familyKey, key.Weight, SKFontStyleWidth.Normal, key.Slant);

if (typeface.FamilyName != name)
{
    typeface = Default;
}

this is of course going to throw and exception. Wouldn't

if (typeface != null && typeface.FamilyName != name)
{
    typeface = Default;
}

be a better option? But then I don't know how the fallback is handled. Because when I specify "Ubuntu, Piboto" I don't want to end up with Default but with Piboto!

The thing is SKTypeface.FromFamilyName(null) should always work. So should SKTypeface.FromFamilyName("Ubuntu"). If a font doesn't exist it should return the system's default.
We can check for null but what would we use as the last possible fallback? I don't know how Skia resolves this. It must be a Fontconfig setting I guess.

At some point, we probably need some kind of FontManager in Avalonia and could introduce a way to define the system default. That way we don't rely on Fontconfig etc.

Ok, so it is most likely a problem with either Skia or FontConfig. I will further investigate it there...

One thing I don't understand though:
let's assume again my FontFamily property contains the list "Ubuntu, Piboto". The Raspian system contains Piboto but at the moment it never gets there because the exception for the check for "Ubuntu" terminates the application. When you say the check for a missing font should always return the default font. Wouldn't that mean that it then would use the default font and never gets to check for Piboto?

Apart from that I think we can close the issue.

If the font name doesn't match the default is returned.

if (typeface.FamilyName != name)
{
    typeface = Default;
}

This part tries to resolve the fallback. If none is present on the system it uses the default.

if (typeface.FontFamily.FamilyNames.HasFallbacks)
{
    foreach (var familyName in typeface.FontFamily.FamilyNames)
    {
        skiaTypeface = TypefaceCache.GetTypeface(
            familyName,
            typeface.Style,
            typeface.Weight);

            if (skiaTypeface != TypefaceCache.Default) break;
    }
}

Ok I see. Thanks.

I reported this issue in the SkiaSharp repository. See Matthews answer:
https://github.com/mono/SkiaSharp/issues/643

I had a look and it appears that in _some_ cases, the fontconfig font manager will return null. I don't think there was ever a guarantee that if you request a font it would return a system default.

See this line: https://github.com/google/skia/blob/chrome/m68/src/ports/SkFontMgr_fontconfig.cpp#L894

In the next (m68) release, I added a SKTypeface.Default property that can be used and _should never_ be null as per the comment in the source: https://github.com/google/skia/blob/chrome/m68/include/core/SkTypeface.h#L90-L91

In the meantime, a fallback is to pass null to SKTypeface.FromFamilyName(null). The source for that reaches here: https://github.com/google/skia/blob/chrome/m68/src/core/SkTypeface.cpp#L135-L144 and the GetDefaultTypeface reference is the same used in SkTypeface::MakeDefault(): https://github.com/google/skia/blob/chrome/m68/src/core/SkTypeface.cpp#L97-L112

Will change the logic to use SKTypeface.FromFamilyName(null) when null is returned.

@mattleibow Thanks for making this clear.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

grokys picture grokys  路  4Comments

TheColonel2688 picture TheColonel2688  路  3Comments

MarchingCube picture MarchingCube  路  4Comments

maxkatz6 picture maxkatz6  路  3Comments

stdcall picture stdcall  路  4Comments