🦔

Direct2DでSVGファイルを画像化する

2022/05/19に公開

Direct2DにはSVG(のサブセット)をレンダリングする機能があります。

Win2DではC#で利用しやすい形になっていますが、UWPアプリ/WinUI3用であるため、
それを .NET Framework/.NETのWinFormsやWPFでは利用できません。

そこでCsWin32で生成したP/Invokeを使用して、WinFormsのビットマップに変換するコードを書いてみました。

P/InvokeではなくSharpDX.Direct2Dでも実現できますが、ライブラリなしで実行できるようにP/Invokeにしてみました。(しかしながらSharpDXの方が圧倒的に扱いやすいです。)

NativeMethods.txtには下記の記述がある前提で、マーシャリングは有りです。

ID2D1Factory6
D2D1CreateFactory
SHCreateStreamOnFileEx

ファクトリーオブジェクトの作成

CsWin32 0.1.647-betaでは、マーシャリングありでもポインターを返すメソッドしか定義されませんでした。

そのため、objectを返すメソッドを手動で定義しました。
あとついでにオーバーロードを足しておきます。

namespace Windows.Win32
{
    internal static partial class PInvoke
    {
        [DllImport("d2d1", ExactSpelling = true)]
        [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
        internal static extern unsafe Foundation.HRESULT D2D1CreateFactory(
            D2D1_FACTORY_TYPE factoryType, in Guid riid,
            D2D1_FACTORY_OPTIONS* pFactoryOptions,
            [MarshalAs(UnmanagedType.Interface)]
            out object ppIFactory);

        private static unsafe T* AsPointer<T>(in T value) where T : unmanaged
        {
            return (T*)Unsafe.AsPointer(ref Unsafe.AsRef(in value));
        }

        internal static unsafe T D2D1CreateFactory<T>(
            D2D1_FACTORY_TYPE factoryType,
            in D2D1_FACTORY_OPTIONS pFactoryOptions) where T : class, ID2D1Factory
        {
            D2D1CreateFactory(factoryType, typeof(T).GUID, AsPointer(in pFactoryOptions), out var o).ThrowOnFailure();
            return (T)o;
        }
    }
}

デバイスコンテキストの作成

SVGをサポートしたID2D1DeviceContext5インターフェースが必要になります。

正攻法ではID2D1Factory6::CreateDeviceから作成するのでしょうが、DXGIのセットアップが面倒なのでID2D1RenderTargetからキャストして取得します。
そのため、環境により動かないかもしれません。

var factory = PInvoke.D2D1CreateFactory<ID2D1Factory>(
    D2D1_FACTORY_TYPE.D2D1_FACTORY_TYPE_SINGLE_THREADED, factoryOption);

factory.CreateDCRenderTarget(new D2D1_RENDER_TARGET_PROPERTIES()
{
    type = D2D1_RENDER_TARGET_TYPE.D2D1_RENDER_TARGET_TYPE_DEFAULT,
    pixelFormat = new D2D1_PIXEL_FORMAT()
    {
        format = Windows.Win32.Graphics.Dxgi.Common.DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM,
        alphaMode = D2D1_ALPHA_MODE.D2D1_ALPHA_MODE_PREMULTIPLIED,
    },
    usage = D2D1_RENDER_TARGET_USAGE.D2D1_RENDER_TARGET_USAGE_NONE,
    minLevel = D2D1_FEATURE_LEVEL.D2D1_FEATURE_LEVEL_DEFAULT,
}, out ID2D1DCRenderTarget rt);

var dc5 = (ID2D1DeviceContext5)rt;

入力ストリームの作成

C# では、ストリームは通常System.IO.Streamクラスですが、COMのIStreamインターフェースがが必要です。
作成する方法はいくつかありますが、今回はWindows APIのSHCreateStreamOnFileEx関数で作成します。

PInvoke.SHCreateStreamOnFileEx(path,
    (uint)Windows.Win32.System.Com.StructuredStorage.STGM.STGM_READ, Constants.FILE_ATTRIBUTE_NORMAL, false, null, out var svgstream)

汎用的にするなら、Streamオブジェクトをラップしたクラスを作成したほうgあ良いでしょう。

ビューポートのサイズの求め方

こちらを参考にして、ビューポートを属性を取得しました。

dc5.CreateSvgDocument(svgstream, new D2D_SIZE_F() { height = 1, width = 1 }, out var svgDoc);

svgDoc.GetRoot(out var root);
D2D1_SVG_VIEWBOX viewbox;
if (root.IsAttributeSpecified("viewBox", null))
{
    root.GetAttributeValue("viewBox", D2D1_SVG_ATTRIBUTE_POD_TYPE.D2D1_SVG_ATTRIBUTE_POD_TYPE_VIEWBOX, &viewbox, (uint)sizeof(D2D1_SVG_VIEWBOX));
}
else if (root.IsAttributeSpecified("width", null) && root.IsAttributeSpecified("height", null))
{
    root.GetAttributeValue("width", D2D1_SVG_ATTRIBUTE_POD_TYPE.D2D1_SVG_ATTRIBUTE_POD_TYPE_FLOAT, &viewbox.width, (uint)sizeof(float));
    root.GetAttributeValue("height", D2D1_SVG_ATTRIBUTE_POD_TYPE.D2D1_SVG_ATTRIBUTE_POD_TYPE_FLOAT, &viewbox.height, (uint)sizeof(float));
}
else
{
    viewbox.height = 100;
    viewbox.width = 100;
}

svgDoc.SetViewportSize(new D2D_SIZE_F() { height = viewbox.height, width = viewbox.width });

描画

ビットマップからGraphicsオブジェクトを作成してHDCを取得し、それをID2D1DCRenderTargetに紐づけるだけです。

using var bitmap = new Bitmap((int)viewbox.width, (int)viewbox.height, PixelFormat.Format32bppPArgb);

using (var g = Graphics.FromImage(bitmap))
{
    var hdc = g.GetHdc();

    var rect = new RECT() { bottom = bitmap.Height, right = bitmap.Width };
    rt.BindDC(new Windows.Win32.Graphics.Gdi.HDC(hdc), &rect);
    dc5.BeginDraw();
    dc5.DrawSvgDocument(svgDoc);
    dc5.EndDraw();
}

必要に応じてビットマップを保存します。

完全なサンプルコード

長いので折りたたんでます。
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Drawing.Imaging;
using System.Windows.Forms;
using Windows.Win32;
using Windows.Win32.Foundation;
using static Windows.Win32.PInvoke;
using Windows.Win32.Graphics.Direct2D;
using Windows.Win32.Graphics.Direct2D.Common;
using System.Drawing;

unsafe static class Program
{
    [STAThread]
    unsafe static void Main(string[] args)
    {
        const string path = "sample.svg";
        var factoryOption = new D2D1_FACTORY_OPTIONS()
        {
            debugLevel = D2D1_DEBUG_LEVEL.D2D1_DEBUG_LEVEL_INFORMATION,
        };
        var factory = PInvoke.D2D1CreateFactory<ID2D1Factory>(
            D2D1_FACTORY_TYPE.D2D1_FACTORY_TYPE_SINGLE_THREADED, factoryOption);

        factory.CreateDCRenderTarget(new D2D1_RENDER_TARGET_PROPERTIES()
        {
            type = D2D1_RENDER_TARGET_TYPE.D2D1_RENDER_TARGET_TYPE_DEFAULT,
            pixelFormat = new D2D1_PIXEL_FORMAT()
            {
                format = Windows.Win32.Graphics.Dxgi.Common.DXGI_FORMAT.DXGI_FORMAT_B8G8R8A8_UNORM,
                alphaMode = D2D1_ALPHA_MODE.D2D1_ALPHA_MODE_PREMULTIPLIED,
            },
            usage = D2D1_RENDER_TARGET_USAGE.D2D1_RENDER_TARGET_USAGE_NONE,
            minLevel = D2D1_FEATURE_LEVEL.D2D1_FEATURE_LEVEL_DEFAULT,
        }, out ID2D1DCRenderTarget rt);

        var dc5 = (ID2D1DeviceContext5)rt;
        PInvoke.SHCreateStreamOnFileEx(path,
            (uint)Windows.Win32.System.Com.StructuredStorage.STGM.STGM_READ, Constants.FILE_ATTRIBUTE_NORMAL, false, null, out var svgstream).ThrowOnFailure();
        dc5.CreateSvgDocument(svgstream, new D2D_SIZE_F() { height = 1, width = 1 }, out var svgDoc);

        svgDoc.GetRoot(out var root);
        D2D1_SVG_VIEWBOX viewbox;
        if (root.IsAttributeSpecified("viewBox", null))
        {
            root.GetAttributeValue("viewBox", D2D1_SVG_ATTRIBUTE_POD_TYPE.D2D1_SVG_ATTRIBUTE_POD_TYPE_VIEWBOX, &viewbox, (uint)sizeof(D2D1_SVG_VIEWBOX));
        }
        else if (root.IsAttributeSpecified("width", null) && root.IsAttributeSpecified("height", null))
        {
            root.GetAttributeValue("width", D2D1_SVG_ATTRIBUTE_POD_TYPE.D2D1_SVG_ATTRIBUTE_POD_TYPE_FLOAT, &viewbox.width, (uint)sizeof(float));
            root.GetAttributeValue("height", D2D1_SVG_ATTRIBUTE_POD_TYPE.D2D1_SVG_ATTRIBUTE_POD_TYPE_FLOAT, &viewbox.height, (uint)sizeof(float));
        }
        else
        {
            viewbox.height = 100;
            viewbox.width = 100;
        }

        svgDoc.SetViewportSize(new D2D_SIZE_F() { height = viewbox.height, width = viewbox.width });

        using var bitmap = new Bitmap((int)viewbox.width, (int)viewbox.height, PixelFormat.Format32bppPArgb);

        using (var g = Graphics.FromImage(bitmap))
        {
            var hdc = g.GetHdc();

            var rect = new RECT() { bottom = bitmap.Height, right = bitmap.Width };
            rt.BindDC(new Windows.Win32.Graphics.Gdi.HDC(hdc), &rect);
            dc5.BeginDraw();
            dc5.DrawSvgDocument(svgDoc);
            dc5.EndDraw();
        }
        bitmap.Save("sample.png", ImageFormat.Png);
    }
}

namespace Windows.Win32
{
    using winmdroot = Windows.Win32;

    public static class Constants
    {
        public const uint FILE_ATTRIBUTE_NORMAL = 0x80;
    }
    internal static partial class PInvoke
    {
        [DllImport("d2d1", ExactSpelling = true)]
        [DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
        internal static extern unsafe Foundation.HRESULT D2D1CreateFactory(
            D2D1_FACTORY_TYPE factoryType, in Guid riid,
            D2D1_FACTORY_OPTIONS* pFactoryOptions,
            [MarshalAs(UnmanagedType.Interface)]
            out object ppIFactory);

        private static unsafe T* AsPointer<T>(in T value) where T : unmanaged
        {
            return (T*)Unsafe.AsPointer(ref Unsafe.AsRef(in value));
        }

        internal static unsafe T D2D1CreateFactory<T>(
            D2D1_FACTORY_TYPE factoryType,
            in D2D1_FACTORY_OPTIONS pFactoryOptions) where T : class, ID2D1Factory
        {
            D2D1CreateFactory(factoryType, typeof(T).GUID, AsPointer(in pFactoryOptions), out var o).ThrowOnFailure();
            return (T)o;
        }
    }
}

Discussion