NativeAOTでSusieプラグイン(32bit)を作成する
Magick.NETという多数の画像フォーマットに対応したライブラリがあり、これをベースにSusieプラグインを作成したら便利ではないかと思ったので、試してみることにした。
クラスライブラリでプロジェクトを作成
開発はVisualStudio2022 Communityを使用する。
フレームワークは.NET9以上を選択。(x86のNativeAOTに必要)
構成マネージャでx86構成を作成
プロジェクトをネイティブAOTビルド用の設定にする
プロジェクトファイル(.csproj)を開いて、PublishAot、IsAotCompatible、AllowUnsafeBlocksを追加し、Trueに設定。ついでにバージョン関係も追加。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0-windows</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x86</Platforms>
<PublishAot>True</PublishAot>
<IsAotCompatible>True</IsAotCompatible>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
<FileVersion>1.0.0</FileVersion>
<AssemblyVersion>1.0.0</AssemblyVersion>
</PropertyGroup>
</Project>
NuGetでMagick.NETをインストール
32bit用なので、Magick.NET-Q8-x86を選択。
プラグインに必要な関数を実装する
【必須or実装した方がよいもの】
GetPluginInfo
IsSupported
GetPictureInfo
GetPicture
【実装しなくても大丈夫そう】
GetPreview(実装しないなら-1を返す)
ConfigurationDlg(実装しないなら-1を返す)
Susie Plug-in Specification Rev4+α on Win32
IsSupportedは、手抜き実装なら1固定で返すだけでいいかも…?
以下は、とりあえず実装したソース。(少し長いです)
ソース
using ImageMagick;
using System.Buffers;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace IfMagick;
public class Program
{
[DllImport("kernel32", CharSet = CharSet.Unicode)]
static extern IntPtr GetModuleHandle(string fileName);
[DllImport("kernel32", CharSet = CharSet.Unicode)]
static extern IntPtr LoadLibrary(string fileName);
[DllImport("kernel32", CharSet = CharSet.Unicode)]
static extern bool GetModuleFileName(IntPtr hModule, out char fileName, int size);
[DllImport("kernel32")]
static extern IntPtr LocalAlloc(uint uFlags, UIntPtr uBytes);
[StructLayout(LayoutKind.Sequential)]
public struct BITMAPINFOHEADER
{
public uint biSize;
public int biWidth;
public int biHeight;
public ushort biPlanes;
public ushort biBitCount;
public uint biCompression;
public uint biSizeImage;
public int biXPelsPerMeter;
public int biYPelsPerMeter;
public uint biClrUsed;
public uint biClrImportant;
public static BITMAPINFOHEADER CreateBMP32(int width, int height) =>
new BITMAPINFOHEADER() {
biSize = (uint)Marshal.SizeOf<BITMAPINFOHEADER>(),
biWidth = width,
biHeight = -height,
biPlanes = 1,
biBitCount = 32,
biCompression = BI_RGB,
biSizeImage = (uint)(width * Math.Abs(height) * 4),
};
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PictureInfo
{
public int left;
public int top;
public int width; /* 画像の幅(pixel) */
public int height; /* 画像の高さ(pixel) */
public short x_density; /* 画素の水平方向密度 */
public short y_density; /* 画素の垂直方向密度 */
public short colorDepth; /* 画素当たりのbit数 */
public IntPtr hInfo; /* 画像内のテキスト情報 */
}
private unsafe class UnsafeWriter<T> : IBufferWriter<T> where T : unmanaged
{
private void* _pointer;
private int _position;
private int _length;
private int Remaining => _length - _position;
public UnsafeWriter(void* buffer, int length)
{
_pointer = buffer;
_position = 0;
_length = length;
}
public void Advance(int count)
=> _position += count;
public Memory<T> GetMemory(int sizeHint = 0)
=> new Memory<T>(GetSpan(sizeHint).ToArray());
public Span<T> GetSpan(int sizeHint = 0)
{
if (sizeHint < 0 || Remaining <= 0)
return Span<T>.Empty;
var prevPosition = _position;
if (sizeHint == 0 || sizeHint >= Remaining)
{
return new Span<T>(_pointer, _length).Slice(_position);
}
return new Span<T>(_pointer, _length).Slice(_position, sizeHint);
}
}
public enum ImageSource : int
{
Disk = 0,
Memory = 1,
}
public enum PluginInfoNo : int
{
APIVersion = 0,
PluginName,
Extentions,
PluginNameDialog,
}
public enum DlgFuncNo : int
{
About = 0,
Config,
}
private static bool _enableLog = false;
private static readonly string _dllDirThis = "";
private static readonly string _dllPathThis = "";
private static readonly string _dllPathMagickImpl = "";
private static readonly string _logPath = "";
private static readonly string _version = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyFileVersionAttribute>()!
.Version;
private const int BI_RGB = 0;
private const string MagickImpl = "Magick.Native-Q8-x86.dll";
static Program()
{
var handle = GetModuleHandle($"{nameof(IfMagick)}.spi");
WriteLog($"handle:0x{handle:X16}");
if (handle == IntPtr.Zero)
return;
Span<char> buf = stackalloc char[260];
buf.Fill('\0');
if (!GetModuleFileName(handle, out buf[0], buf.Length))
return;
_dllPathThis = new string(buf).Trim('\0');
_dllDirThis = Path.GetDirectoryName(_dllPathThis)!;
_dllPathMagickImpl = Path.Combine(_dllDirThis, MagickImpl);
_logPath = Path.Combine(_dllDirThis, $"{nameof(IfMagick)}.log");
WriteLog($"_dllPathThis:{_dllPathThis}");
WriteLog($"_dllDirThis:{_dllDirThis}");
WriteLog($"_dllPathMagickImpl:{_dllPathMagickImpl}");
LoadLibrary(_dllPathMagickImpl);
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)], EntryPoint = "GetPluginInfo")]
public static unsafe int GetPluginInfo(PluginInfoNo infoNo, IntPtr buf, int buflen)
{
WriteLog($"GetPluginInfo infoNo:{infoNo} buf:0x{buf:X16} buflen:{buflen:X16}");
int result = 0;
try
{
string info;
switch (infoNo)
{
case PluginInfoNo.APIVersion:
info = "00IN";
break;
case PluginInfoNo.PluginName:
case PluginInfoNo.PluginNameDialog:
info = $"{nameof(IfMagick)} Plug-in ver{_version}";
break;
case PluginInfoNo.Extentions:
info = "*.ai;*.avif;*.bmp;*.dds;*.dib;*.cur;*.ico;*.gif;*.jpg;*.jpeg;*.png;*.psd;*.raw;*.svg;*.tif;*.tiff;*.webp";
break;
default:
return result;
}
if (buflen - 1 < info.Length)
{
info = info.Substring(0, buflen - 1);
}
info += "\0";
WriteLog($"info:{info}");
var pDst = (byte*)buf;
var src = System.Text.Encoding.ASCII.GetBytes(info);
Unsafe.CopyBlockUnaligned(ref pDst[0], ref src[0], (uint)src.Length);
result = src.Length;
return result;
}
finally
{
WriteLog($"return {result}");
}
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)], EntryPoint = "IsSupported")]
public static int IsSupported(IntPtr pFileName, IntPtr handleOrPtr)
{
var fileName = "(null)";
if (pFileName != IntPtr.Zero)
fileName = Marshal.PtrToStringAnsi(pFileName);
WriteLog($"IsSupported fileName:{fileName} handleOrPtr:0x{handleOrPtr:X16}");
var result = 1;
WriteLog($"return {result}");
return result;
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)], EntryPoint = "GetPictureInfo")]
public static unsafe int GetPictureInfo(IntPtr fileNameOrPtr, int offsetOrLength, ImageSource target, PictureInfo* lpInfo)
{
WriteLog($"GetPictureInfo fileNameOrPtr:0x{fileNameOrPtr:X16} offsetOrLength:{offsetOrLength} target:{target}");
int result = 0;
try
{
using (var image = LoadMagickImage(fileNameOrPtr, offsetOrLength, target))
{
lpInfo->left = 0;
lpInfo->top = 0;
lpInfo->width = (int)image.Width;
lpInfo->height = (int)image.Height;
lpInfo->x_density = (short)image.Density.X;
lpInfo->y_density = (short)image.Density.Y;
lpInfo->colorDepth = (short)image.DetermineBitDepth();
}
}
catch (Exception e)
{
WriteLog(e.ToString());
result = -1;
}
WriteLog($"return {result}");
return result;
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)], EntryPoint = "GetPicture")]
public static unsafe int GetPicture(IntPtr fileNameOrPtr, int offsetOrLength, ImageSource target, BITMAPINFOHEADER** phBIHeader, byte** ppImage, IntPtr lpPrgressCallback, int lData)
{
WriteLog($"GetPicture fileNameOrPtr:0x{fileNameOrPtr:X16} offsetOrLength:{offsetOrLength} target:{target}");
int result = 0;
try
{
using (var image = LoadMagickImage(fileNameOrPtr, offsetOrLength, target))
{
const uint LMEM_FIXED = 0;
var biHeader = BITMAPINFOHEADER.CreateBMP32((int)image.Width, (int)image.Height);
*phBIHeader = (BITMAPINFOHEADER*)LocalAlloc(LMEM_FIXED, (nuint)Marshal.SizeOf<BITMAPINFOHEADER>());
**phBIHeader = biHeader;
*ppImage = (byte*)LocalAlloc(LMEM_FIXED, biHeader.biSizeImage);
var writer = new UnsafeWriter<byte>(*ppImage, (int)biHeader.biSizeImage);
image.Write(writer, MagickFormat.Bgra);
}
}
catch (Exception e)
{
WriteLog(e.ToString());
result = -1;
}
WriteLog($"return {result}");
return result;
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)], EntryPoint = "GetPreview")]
public static unsafe int GetPreview(IntPtr fileNameOrPtr, int offsetOrLength, ImageSource target, BITMAPINFOHEADER* pHBInfo, IntPtr pImage, IntPtr lpPrgressCallback, int lData)
{
WriteLog($"GetPreview fileNameOrPtr:0x{fileNameOrPtr:X16} offsetOrLength:{offsetOrLength} target:{target}");
int result = -1;
WriteLog($"return {result}");
return result;
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)], EntryPoint = "ConfigurationDlg")]
public static int ConfigurationDlg(IntPtr hwnd, DlgFuncNo funcNo)
{
WriteLog($"ConfigurationDlg hwnd:0x{hwnd:X16} funcNo:{funcNo}");
int result = -1;
WriteLog($"return {result}");
return result;
}
private static unsafe MagickImage LoadMagickImage(IntPtr fileNameOrPtr, int offsetOrLength, ImageSource target)
{
if (target == ImageSource.Disk)
{
var fileName = Marshal.PtrToStringAnsi(fileNameOrPtr)!;
WriteLog($"fileName:{fileName}");
return new MagickImage(fileName);
}
else
{
return new MagickImage(new ReadOnlySpan<byte>((void*)fileNameOrPtr, offsetOrLength));
}
}
private static void WriteLog(string message)
{
if (!_enableLog || _logPath == "")
return;
File.AppendAllText(_logPath, message + Environment.NewLine);
}
}
Magick.NETは、もっと多くの画像形式に対応しているけれど、自分の使いそうな拡張子だけ入れている。真面目に実装するなら、ダイアログも実装して設定ファイル化しておくと良さそう。
ビルドする
IDE上で直接ネイティブDLLは出力出来なさそうなので、コマンドラインで出力する。(発行でEXEでは出力出来るのだが、何故かDLLだとアカンと言われる)
コマンド長いのでバッチファイルを作成しておくのを推奨。
dotnet publish /p:NativeLib=Shared --use-current-runtime -r win-x86
サイズはまあまあ大きいけれど、C#でネイティブDLL作れるのはいい感じ。
Dependenciesで、エクスポートされた関数を確認してみたが、きちんとエクスポートされているようだ。
アプリで動作確認
自分はMassiGraとLeeyesの二つの画像ビューワーを愛用しているので、とりあえずこの二つで動けばOKとする。作成したDLLの拡張子をspiに変更し、Magick.Native-Q8-x86.dll と共にプラグインフォルダにコピーする。
画像が無事に表示された。いくつかのプラグインをやめて、これ一つに集約できそうだ。
Discussion