Direct2DでSVGファイルを画像化する
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