fontsfont-awesomesharpdxdirectwriteninjatrader

Adding an embedded font resource to SharpDX.DirectWrite.Factory


My dll is loaded by a .Net Framework 4.8 WPF application. It includes font files as embedded resources in .ttf and .otf format and I want to add that font in one format or the other into the SharpDX.DirectWrite.Factory instance already created by the hosting WPF application.

The application is NinjaTrader 8 and the font is FontAwesome, but presumably this question could apply to any WPF application and any font.

Desired usage: (In my case, the desired usage is inside the OnRender method of an Indicator in NinjaTrader 8.)

using System.Windows.Media;
using SharpDX;
using SharpDX.DirectWrite;

// "Globals.DirectWriteFactory" is the `SharpDX.DirectWrite.Factory` instance provided by the hosting WPF application
// "Font Awesome 6 Free Regular" is the name of the font embedded in my dll
// "\uf1c0" is a string representing one of thousands of icons available in this font.
// "RenderTarget" is a `SharpDX.Direct2D1.RenderTarget` object provided by the hosting WPF application

using var format = new TextFormat(Globals.DirectWriteFactory, "Font Awesome 6 Free Regular", 12);
using var layout = new TextLayout(Globals.DirectWriteFactory, "\uf1c0", format, 1000, 1000);
using var brush = Brushes.WhiteSmoke.ToDxBrush(RenderTarget);
RenderTarget.DrawTextLayout(new Vector2(100, 100), layout, brush);

Instead of rendering the icon however, the result is a little rendered square shape that represents an unknown character. I believe the issue is that the desired font isn't being used, and a default font is substituted. It looks like Arial when I render an English string.

The result I have so far

Here's the code I used to attempt to add FontAwesome to Globals.DirectWriteFactory:

Since breakpoints in the ResourceFontLoader class are never hit, I believe the DirectWrite.Factory object is ignoring the new registration that is made in the static constructor of the FontAwesome utility class.

internal static class FontAwesome
{
  public static TextFormat CreateTextFormat(float fontSize)
    => new TextFormat(Globals.DirectWriteFactory, "Font Awesome 6 Free Regular", fontSize);

  static FontAwesome()
  {
    var loader = new ResourceFontLoader();
    Globals.DirectWriteFactory.RegisterFontFileLoader(loader);
    Globals.DirectWriteFactory.RegisterFontCollectionLoader(loader);
  }

  private sealed class ResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
  {
    private readonly List<ResourceFontFileStream> _fontStreams = new List<ResourceFontFileStream>();
    private readonly List<ResourceFontFileEnumerator> _enumerators = new List<ResourceFontFileEnumerator>();
    private readonly DataStream _keyStream;

    public ResourceFontLoader()
    {
      foreach (var name in typeof(ResourceFontLoader).Assembly.GetManifestResourceNames())
      {
        // I have tried using both .otf and .ttf formats.

        //if (name.EndsWith(".otf") && name.IndexOf("awesome", StringComparison.OrdinalIgnoreCase) >= 0)
        if (name.EndsWith(".ttf") && name.IndexOf("fa-", StringComparison.OrdinalIgnoreCase) >= 0)
        {
          var fontBytes = Utilities.ReadStream(typeof(ResourceFontLoader).Assembly.GetManifestResourceStream(name));
          var stream = new DataStream(fontBytes.Length, true, true);
          stream.Write(fontBytes, 0, fontBytes.Length);
          stream.Position = 0;
          _fontStreams.Add(new ResourceFontFileStream(stream));
        }
      }

      _keyStream = new DataStream(sizeof(int) * _fontStreams.Count, true, true);
      for (int i = 0; i < _fontStreams.Count; i++)
        _keyStream.Write((int)i);
      _keyStream.Position = 0;
    }

    public DataStream Key
    {
      get
      {
        // Breakpoints here are never hit! It's simnply not used!

        return _keyStream;
      }
    }

    FontFileEnumerator FontCollectionLoader.CreateEnumeratorFromKey(Factory factory, DataPointer collectionKey)
    {
      // Breakpoints here are never hit! It's simnply not used!

      var enumerator = new ResourceFontFileEnumerator(factory, this, collectionKey);
      _enumerators.Add(enumerator);

      return enumerator;
    }

    FontFileStream FontFileLoader.CreateStreamFromKey(DataPointer fontFileReferenceKey)
    {
      // Breakpoints here are never hit! It's simnply not used!

      var index = Utilities.Read<int>(fontFileReferenceKey.Pointer);
      return _fontStreams[index];
    }
  }
  private sealed class ResourceFontFileStream : CallbackBase, FontFileStream
  {
    private readonly DataStream _stream;

    public ResourceFontFileStream(DataStream stream)
    {
      this._stream = stream;
    }

    /// <summary>
    /// Reads a fragment from a font file.
    /// </summary>
    /// <param name="fragmentStart">When this method returns, contains an address of a  reference to the start of the font file fragment.  This parameter is passed uninitialized.</param>
    /// <param name="fileOffset">The offset of the fragment, in bytes, from the beginning of the font file.</param>
    /// <param name="fragmentSize">The size of the file fragment, in bytes.</param>
    /// <param name="fragmentContext">When this method returns, contains the address of</param>
    /// <remarks>
    /// Note that ReadFileFragment implementations must check whether the requested font file fragment is within the file bounds. Otherwise, an error should be returned from ReadFileFragment.   {{DirectWrite}} may invoke <see cref="SharpDX.DirectWrite.FontFileStream"/> methods on the same object from multiple threads simultaneously. Therefore, ReadFileFragment implementations that rely on internal mutable state must serialize access to such state across multiple threads. For example, an implementation that uses separate Seek and Read operations to read a file fragment must place the code block containing Seek and Read calls under a lock or a critical section.
    /// </remarks>
    /// <unmanaged>HRESULT IDWriteFontFileStream::ReadFileFragment([Out, Buffer] const void** fragmentStart,[None] __int64 fileOffset,[None] __int64 fragmentSize,[Out] void** fragmentContext)</unmanaged>
    void FontFileStream.ReadFileFragment(out IntPtr fragmentStart, long fileOffset, long fragmentSize, out IntPtr fragmentContext)
    {
      lock (this)
      {
        fragmentContext = IntPtr.Zero;
        _stream.Position = fileOffset;
        fragmentStart = _stream.PositionPointer;
      }
    }

    /// <summary>
    /// Releases a fragment from a file.
    /// </summary>
    /// <param name="fragmentContext">A reference to the client-defined context of a font fragment returned from {{ReadFileFragment}}.</param>
    /// <unmanaged>void IDWriteFontFileStream::ReleaseFileFragment([None] void* fragmentContext)</unmanaged>
    void FontFileStream.ReleaseFileFragment(IntPtr fragmentContext)
    {
      // Nothing to release. No context are used
    }

    /// <summary>
    /// Obtains the total size of a file.
    /// </summary>
    /// <returns>the total size of the file.</returns>
    /// <remarks>
    /// Implementing GetFileSize() for asynchronously loaded font files may require downloading the complete file contents. Therefore, this method should be used only for operations that either require a complete font file to be loaded (for example, copying a font file) or that need to make decisions based on the value of the file size (for example, validation against a persisted file size).
    /// </remarks>
    /// <unmanaged>HRESULT IDWriteFontFileStream::GetFileSize([Out] __int64* fileSize)</unmanaged>
    long FontFileStream.GetFileSize()
    {
      return _stream.Length;
    }

    /// <summary>
    /// Obtains the last modified time of the file.
    /// </summary>
    /// <returns>
    /// the last modified time of the file in the format that represents the number of 100-nanosecond intervals since January 1, 1601 (UTC).
    /// </returns>
    /// <remarks>
    /// The "last modified time" is used by DirectWrite font selection algorithms to determine whether one font resource is more up to date than another one.
    /// </remarks>
    /// <unmanaged>HRESULT IDWriteFontFileStream::GetLastWriteTime([Out] __int64* lastWriteTime)</unmanaged>
    long FontFileStream.GetLastWriteTime()
    {
      return 0;
    }
  }

  private sealed class ResourceFontFileEnumerator : CallbackBase, FontFileEnumerator
  {
    private Factory _factory;
    private FontFileLoader _loader;
    private DataStream keyStream;
    private FontFile _currentFontFile;

    public ResourceFontFileEnumerator(Factory factory, FontFileLoader loader, DataPointer key)
    {
      _factory = factory;
      _loader = loader;
      keyStream = new DataStream(key.Pointer, key.Size, true, false);
    }

    /// <summary>
    /// Advances to the next font file in the collection. When it is first created, the enumerator is positioned before the first element of the collection and the first call to MoveNext advances to the first file.
    /// </summary>
    /// <returns>
    /// the value TRUE if the enumerator advances to a file; otherwise, FALSE if the enumerator advances past the last file in the collection.
    /// </returns>
    /// <unmanaged>HRESULT IDWriteFontFileEnumerator::MoveNext([Out] BOOL* hasCurrentFile)</unmanaged>
    bool FontFileEnumerator.MoveNext()
    {
      bool moveNext = keyStream.RemainingLength != 0;
      if (moveNext)
      {
        if (_currentFontFile != null)
          _currentFontFile.Dispose();

        _currentFontFile = new FontFile(_factory, keyStream.PositionPointer, 4, _loader);
        keyStream.Position += 4;
      }
      return moveNext;
    }

    /// <summary>
    /// Gets a reference to the current font file.
    /// </summary>
    /// <value></value>
    /// <returns>a reference to the newly created <see cref="SharpDX.DirectWrite.FontFile"/> object.</returns>
    /// <unmanaged>HRESULT IDWriteFontFileEnumerator::GetCurrentFontFile([Out] IDWriteFontFile** fontFile)</unmanaged>
    FontFile FontFileEnumerator.CurrentFontFile
    {
      get
      {
        ((IUnknown)_currentFontFile).AddReference();
        return _currentFontFile;
      }
    }
  }
}

Solution

  • There were 3 things I needed to fix:

    1. Create and store a FontCollection object using the FontLoader created in the question above.
    2. As a side task, use the FontCollection to get the correct font family names.
    3. Use that FontCollection in the constructor of the TextFormat object.

    Here is the complete solution:

    Font usage:

    using FFT.NT8.Fonts;
    using NinjaTrader.Gui;
    using NinjaTrader.Gui.Chart;
    using SharpDX.DirectWrite;
    
    namespace NinjaTrader.NinjaScript.Indicators.FFT_Code;
    
    public sealed class FontAwesomeIndicator : FFTIndicator
    {
      private TextFormat _format = null!;
    
      protected override void FFTOnStateChange()
      {
        switch (State)
        {
          case State.SetDefaults:
            IsOverlay = true;
            Calculate = Calculate.OnBarClose;
            IsSuspendedWhileInactive = true;
            break;
    
          case State.DataLoaded:
            _format = FontAwesome.CreateTextFormat(FontAwesome.Families.Regular, FontWeight.Regular, FontStyle.Normal, FontStretch.Normal, 26);
            break;
    
          case State.Terminated:
            _format?.Dispose();
            break;
        }
      }
    
      protected override void FFTOnRender(ChartControl chartControl, ChartScale chartScale)
      {
        // Draw two items of text on the chart using FontAwesome. 
        {
          using var textLayout = new TextLayout(Globals.DirectWriteFactory, "hello", _format, 1000, 1000);
          using var brush = MediaBrushes.WhiteSmoke.ToDxBrush(RenderTarget);
          RenderTarget.DrawTextLayout(new Vector2(100, 100), textLayout, brush);
        }
    
        {
          using var textLayout = new TextLayout(Globals.DirectWriteFactory, "\uf004", _format, 1000, 1000); // database icon
          using var brush = MediaBrushes.WhiteSmoke.ToDxBrush(RenderTarget);
          RenderTarget.DrawTextLayout(new(200, 200), textLayout, brush);
        }
      }
    }
    

    FontAwesome.cs:

    using System.Collections.ObjectModel;
    using SharpDX;
    using SharpDX.DirectWrite;
    
    namespace FFT.NT8.Fonts;
    
    internal static class FontAwesome
    {
      public enum Families
      {
        Regular,
        Solid,
        Brands,
      }
    
      private static readonly FontCollection _fontCollection;
      private static readonly IReadOnlyDictionary<Families, string> _familyNames;
    
      public static TextFormat CreateTextFormat(Families family, FontWeight fontWeight, FontStyle fontStyle, FontStretch fontStretch, float fontSize)
        => new TextFormat(Globals.DirectWriteFactory, _familyNames[family], _fontCollection, fontWeight, fontStyle, fontStretch, fontSize);
    
      static FontAwesome()
      {
        var loader = new ResourceFontLoader();
        Globals.DirectWriteFactory.RegisterFontFileLoader(loader);
        Globals.DirectWriteFactory.RegisterFontCollectionLoader(loader);
        _fontCollection = new FontCollection(Globals.DirectWriteFactory, loader, loader.Key);
        _familyNames = MapFamilyNames();
      }
    
      private static IReadOnlyDictionary<Families, string> MapFamilyNames()
      {
        var familyNames = Enumerable.Range(0, _fontCollection.FontFamilyCount)
          .Select(i => _fontCollection.GetFontFamily(i).FamilyNames.GetString(0));
    
        var families = new Dictionary<Families, string>();
        foreach (var name in familyNames)
        {
          if (name.IndexOf("brand", StringComparison.InvariantCultureIgnoreCase) >= 0)
            families[Families.Brands] = name;
          else if (name.IndexOf("solid", StringComparison.InvariantCultureIgnoreCase) >= 0)
            families[Families.Solid] = name;
          else
            families[Families.Regular] = name;
        }
    
        return new ReadOnlyDictionary<Families, string>(families);
      }
    
      private sealed class ResourceFontLoader : CallbackBase, FontCollectionLoader, FontFileLoader
      {
        private readonly List<ResourceFontFileStream> _fontStreams = new List<ResourceFontFileStream>();
        private readonly List<ResourceFontFileEnumerator> _enumerators = new List<ResourceFontFileEnumerator>();
        private readonly DataStream _keyStream;
    
        public ResourceFontLoader()
        {
          foreach (var name in typeof(ResourceFontLoader).Assembly.GetManifestResourceNames())
          {
            if (name.EndsWith(".otf") && name.IndexOf("awesome", StringComparison.OrdinalIgnoreCase) >= 0)
            {
              var fontBytes = Utilities.ReadStream(typeof(ResourceFontLoader).Assembly.GetManifestResourceStream(name));
              var stream = new DataStream(fontBytes.Length, true, true);
              stream.Write(fontBytes, 0, fontBytes.Length);
              stream.Position = 0;
              _fontStreams.Add(new ResourceFontFileStream(stream));
            }
          }
    
          _keyStream = new DataStream(sizeof(int) * _fontStreams.Count, true, true);
          for (int i = 0; i < _fontStreams.Count; i++)
            _keyStream.Write((int)i);
          _keyStream.Position = 0;
        }
    
        public DataStream Key
        {
          get
          {
            return _keyStream;
          }
        }
    
        FontFileEnumerator FontCollectionLoader.CreateEnumeratorFromKey(Factory factory, DataPointer collectionKey)
        {
          var enumerator = new ResourceFontFileEnumerator(factory, this, collectionKey);
          _enumerators.Add(enumerator);
    
          return enumerator;
        }
    
        FontFileStream FontFileLoader.CreateStreamFromKey(DataPointer fontFileReferenceKey)
        {
          var index = Utilities.Read<int>(fontFileReferenceKey.Pointer);
          return _fontStreams[index];
        }
      }
      private sealed class ResourceFontFileStream : CallbackBase, FontFileStream
      {
        private readonly DataStream _stream;
    
        public ResourceFontFileStream(DataStream stream)
        {
          this._stream = stream;
        }
    
        /// <summary>
        /// Reads a fragment from a font file.
        /// </summary>
        /// <param name="fragmentStart">When this method returns, contains an address of a  reference to the start of the font file fragment.  This parameter is passed uninitialized.</param>
        /// <param name="fileOffset">The offset of the fragment, in bytes, from the beginning of the font file.</param>
        /// <param name="fragmentSize">The size of the file fragment, in bytes.</param>
        /// <param name="fragmentContext">When this method returns, contains the address of</param>
        /// <remarks>
        /// Note that ReadFileFragment implementations must check whether the requested font file fragment is within the file bounds. Otherwise, an error should be returned from ReadFileFragment.   {{DirectWrite}} may invoke <see cref="SharpDX.DirectWrite.FontFileStream"/> methods on the same object from multiple threads simultaneously. Therefore, ReadFileFragment implementations that rely on internal mutable state must serialize access to such state across multiple threads. For example, an implementation that uses separate Seek and Read operations to read a file fragment must place the code block containing Seek and Read calls under a lock or a critical section.
        /// </remarks>
        /// <unmanaged>HRESULT IDWriteFontFileStream::ReadFileFragment([Out, Buffer] const void** fragmentStart,[None] __int64 fileOffset,[None] __int64 fragmentSize,[Out] void** fragmentContext)</unmanaged>
        void FontFileStream.ReadFileFragment(out IntPtr fragmentStart, long fileOffset, long fragmentSize, out IntPtr fragmentContext)
        {
          lock (this)
          {
            fragmentContext = IntPtr.Zero;
            _stream.Position = fileOffset;
            fragmentStart = _stream.PositionPointer;
          }
        }
    
        /// <summary>
        /// Releases a fragment from a file.
        /// </summary>
        /// <param name="fragmentContext">A reference to the client-defined context of a font fragment returned from {{ReadFileFragment}}.</param>
        /// <unmanaged>void IDWriteFontFileStream::ReleaseFileFragment([None] void* fragmentContext)</unmanaged>
        void FontFileStream.ReleaseFileFragment(IntPtr fragmentContext)
        {
          // Nothing to release. No context are used
        }
    
        /// <summary>
        /// Obtains the total size of a file.
        /// </summary>
        /// <returns>the total size of the file.</returns>
        /// <remarks>
        /// Implementing GetFileSize() for asynchronously loaded font files may require downloading the complete file contents. Therefore, this method should be used only for operations that either require a complete font file to be loaded (for example, copying a font file) or that need to make decisions based on the value of the file size (for example, validation against a persisted file size).
        /// </remarks>
        /// <unmanaged>HRESULT IDWriteFontFileStream::GetFileSize([Out] __int64* fileSize)</unmanaged>
        long FontFileStream.GetFileSize()
        {
          return _stream.Length;
        }
    
        /// <summary>
        /// Obtains the last modified time of the file.
        /// </summary>
        /// <returns>
        /// the last modified time of the file in the format that represents the number of 100-nanosecond intervals since January 1, 1601 (UTC).
        /// </returns>
        /// <remarks>
        /// The "last modified time" is used by DirectWrite font selection algorithms to determine whether one font resource is more up to date than another one.
        /// </remarks>
        /// <unmanaged>HRESULT IDWriteFontFileStream::GetLastWriteTime([Out] __int64* lastWriteTime)</unmanaged>
        long FontFileStream.GetLastWriteTime()
        {
          return 0;
        }
      }
    
      private sealed class ResourceFontFileEnumerator : CallbackBase, FontFileEnumerator
      {
        private Factory _factory;
        private FontFileLoader _loader;
        private DataStream keyStream;
        private FontFile _currentFontFile;
    
        public ResourceFontFileEnumerator(Factory factory, FontFileLoader loader, DataPointer key)
        {
          _factory = factory;
          _loader = loader;
          keyStream = new DataStream(key.Pointer, key.Size, true, false);
        }
    
        /// <summary>
        /// Advances to the next font file in the collection. When it is first created, the enumerator is positioned before the first element of the collection and the first call to MoveNext advances to the first file.
        /// </summary>
        /// <returns>
        /// the value TRUE if the enumerator advances to a file; otherwise, FALSE if the enumerator advances past the last file in the collection.
        /// </returns>
        /// <unmanaged>HRESULT IDWriteFontFileEnumerator::MoveNext([Out] BOOL* hasCurrentFile)</unmanaged>
        bool FontFileEnumerator.MoveNext()
        {
          bool moveNext = keyStream.RemainingLength != 0;
          if (moveNext)
          {
            if (_currentFontFile != null)
              _currentFontFile.Dispose();
    
            _currentFontFile = new FontFile(_factory, keyStream.PositionPointer, 4, _loader);
            keyStream.Position += 4;
          }
          return moveNext;
        }
    
        /// <summary>
        /// Gets a reference to the current font file.
        /// </summary>
        /// <value></value>
        /// <returns>a reference to the newly created <see cref="SharpDX.DirectWrite.FontFile"/> object.</returns>
        /// <unmanaged>HRESULT IDWriteFontFileEnumerator::GetCurrentFontFile([Out] IDWriteFontFile** fontFile)</unmanaged>
        FontFile FontFileEnumerator.CurrentFontFile
        {
          get
          {
            ((IUnknown)_currentFontFile).AddReference();
            return _currentFontFile;
          }
        }
      }
    }