Austin Wise
Go to home page
2022-03-01

Loading a WOFF font in SkiaSharp on Windows

Recently I found myself trying to use a font in WOFF format with SkiaSharp. SkiaSharp does not natively support WOFF fonts. Fortunatly the DirectWrite component in Windows can turn these fonts into the SFNT (aka TrueType) format that SkiaSharp supports.

There should be a way to do this that does not invlove using Windows-specific components. This JavaScript converter tool does not have a lot of code in it. So it should be possible to create a pure-C# implementation of the font conversion. But I could not find one and this code only has to run on Windows.

Code

I'm using CsWin32 to generate the native interop bindings. I put this code in a separate library, as it seems like the source generator slows down Visual Studio quite a bit. Hopefully the CsWin32 source generator converts to an incremental generator and this becomes unnecessary.

First we define the dependency in our csproj file:

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net6.0-windows7.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <AdditionalFiles Include="NativeMethods.txt" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference
        Include="Microsoft.Windows.CsWin32"
        Version="0.1.635-beta">
          <PrivateAssets>all</PrivateAssets>
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
        <PackageReference Include="SkiaSharp" Version="2.80.3" />
    </ItemGroup>

</Project>

Next we define which native APIs we want to call in NativeMethods.txt:

IDWriteFactory5
DWriteCreateFactory

And now we can extract the font from the WOFF file:

using SkiaSharp;
using System;
using Windows.Win32;
using Windows.Win32.Graphics.DirectWrite;

namespace FontInterop;

public static class WoffFontLoader
{
    static readonly Guid IID_IDWriteFactory5 = Guid.Parse("958DB99A-BE2A-4F09-AF7D-65189803D1D3");

    public unsafe static SKTypeface LoadWoffFont(ReadOnlySpan<byte> woffFont)
    {
        object factory;
        var hr = PInvoke.DWriteCreateFactory(
            DWRITE_FACTORY_TYPE.DWRITE_FACTORY_TYPE_SHARED,
            IID_IDWriteFactory5,
            out factory);
        hr.ThrowOnFailure();
        var writerFactory = (IDWriteFactory5)factory;
        fixed (byte* pBytes = woffFont)
        {
            IDWriteFontFileStream stream;
            writerFactory.UnpackFontFile(
                DWRITE_CONTAINER_TYPE.DWRITE_CONTAINER_TYPE_WOFF,
                (void*)pBytes,
                (uint)woffFont.Length,
                out stream);
            ulong fileSize = 0;
            stream.GetFileSize(&fileSize);
            void* fragStart = (void*)IntPtr.Zero;
            void* cookie = (void*)IntPtr.Zero;
            try
            {
                stream.ReadFileFragment(&fragStart, 0, fileSize, &cookie);
                var data = SKData.CreateCopy((IntPtr)fragStart, (int)fileSize);
                return SKTypeface.FromData(data);
            }
            finally
            {
                if (new IntPtr(cookie) != IntPtr.Zero)
                {
                    stream.ReleaseFileFragment(cookie);
                }
            }
        }
    }
}

This COM interop could probably be done better. Perhaps the IDWriteFontFileStream should be cleaned eagerly up with Marshal.ReleaseComObject Perhaps the IDWriteFactory5 should be created once and cached. But for my short-lived console program, this runs fast enough as is.