In WPF we would like to use ttf
fonts as embedded resources without copying or installing these to the system and without actually writing these to disk. Without memory leak issues.
None of the solutions detailed in:
How to include external font in WPF application without installing it
are useable in this scenario due to the WPF memory leak around this:
WPF TextBlock memory leak when using Font
Installing fonts from memory and process only is possible in GDI via AddFontMemResourceEx . Since this installs the font for the process, it should work for WPF as well, but there seems to be issues around the FontFamily
that we get after installing the font via AddFontMemResourceEx
. E.g.:
var font = new FontFamily("Roboto");
This works in that it does not give any errors, but the font isn't actually changed, some line spacing and other metrics are changed, but the font looks exactly like Segoe UI
for some reason.
The question is then is it and how is it possible to use fonts installed with AddFontMemResourceEx
in WPF?
PS: Here the P/Invoke code:
const string GdiDllName = "gdi32";
[DllImport(GdiDllName, ExactSpelling= true)]
private static extern IntPtr AddFontMemResourceEx(byte[] pbFont, int cbFont, IntPtr pdv, out uint pcFonts);
public static void AddFontMemResourceEx(string fontResourceName, byte[] bytes, Action<string> log)
{
var handle = AddFontMemResourceEx(bytes, bytes.Length, IntPtr.Zero, out uint fontCount);
if (handle == IntPtr.Zero)
{
log?.Invoke($"Font install failed for '{fontResourceName}'");
}
else
{
var message = $"Font installed '{fontResourceName}' with font count '{fontCount}'";
log?.Invoke(message);
}
}
This code succeeeds with log messages like:
Font installed 'Roboto-Regular.ttf' with font count '1'
Support code for loading embedded resource as byte array:
public static byte[] ReadResourceByteArray(Assembly assembly, string resourceName)
{
using (var stream = assembly.GetManifestResourceStream(resourceName))
{
var bytes = new byte[stream.Length];
int read = 0;
while (read < bytes.Length)
{
read += stream.Read(bytes, read, bytes.Length - read);
}
if (read != bytes.Length)
{
throw new ArgumentException(
$"Resource '{resourceName}' has unexpected length " +
$"'{read}' expected '{bytes.Length}'");
}
return bytes;
}
}
Which means installing embedded fonts can be done like, with assembly
being the assembly containing the embedded font resources and EMBEDDEDFONTNAMESPACE
being the namespace of the embedded resources e.g. SomeProject.Fonts
:
var resourceNames = assembly.GetManifestResourceNames();
string Prefix = "EMBEDDEDFONTNAMESPACE" + ".";
var fontFileNameToResourceName = resourceNames.Where(n => n.StartsWith(Prefix))
.ToDictionary(n => n.Replace(Prefix, string.Empty), n => n);
var fontFileNameToBytes = fontFileNameToResourceName
.ToDictionary(p => p.Key, p => ReadResourceByteArray(assembly, p.Value));
foreach (var fileNameBytes in fontFileNameToBytes)
{
AddFontMemResourceEx(fileNameBytes.Key, fileNameBytes.Value, log);
}