和SharpDX坑爹的Variant刚正面
几个月前我写了这篇文章《.NET中生成动态验证码》文章,其实里面藏着一个大坑。运行里面的代码,会发现运行的 gif
图片并没有循环播放:
细心的网友也注意到了这个问题:
……但后来他备注说“已解决”,我当时也不知道该怎么解决的,所以我追问了一下,但他一直没有回复。但思路肯定是有的,再不济,也可以将保存为字节数组的数据,用其它的库进行重新解析,然后指定循环次数即可,当然这个方法肯定很搓,想象中较好的办法应该是调用 SharpDX
内置的 API
来完成。
踩坑之路
就此我开始了 SharpDX
的踩坑之路,我找到了许多资料,找到不少示例代码,最后不断实验,最终成功。
C++示例
首先我在网上找到了 SharpDX
生成循环 gif
文件的 C++
开源代码示例,代码源自于https://github.com/GarethRichards/GifSaver/blob/master/GifSaver.cpp:
PROPVARIANT propValue; PropVariantInit(&propValue);
propValue.vt = VT_UI1 | VT_VECTOR;
propValue.caub.cElems = 11;
DX::ThrowIfFailed(m_imagingFactory->CreateEncoder(GUID_ContainerFormatGif, &GUID_VendorMicrosoft, &m_wicBitmapEncoder));
DX::ThrowIfFailed(m_wicBitmapEncoder->Initialize(m_stream.Get(), WICBitmapEncoderNoCache));
ComPtr<IWICMetadataQueryWriter> pEncoderMetadataQueryWriter;
DX::ThrowIfFailed(m_wicBitmapEncoder->GetMetadataQueryWriter(&pEncoderMetadataQueryWriter));
string elms = "NETSCAPE2.0";
propValue.caub.pElems = const_cast<UCHAR *>(reinterpret_cast<const UCHAR *>(elms.c_str()));
DX::ThrowIfFailed(pEncoderMetadataQueryWriter->SetMetadataByName(L"/appext/Application", &propValue));
// Set animated GIF format
propValue.vt = VT_UI1 | VT_VECTOR;
propValue.caub.cElems = 5;
UCHAR buf[5];
propValue.caub.pElems = &buf[0];
*(propValue.caub.pElems) = 3; // must be > 1,
*(propValue.caub.pElems + 1) = 1; // defines animated GIF
*(propValue.caub.pElems + 2) = 0; // LSB 0 = infinite loop.
*(propValue.caub.pElems + 3) = 0; // MSB of iteration count value
*(propValue.caub.pElems + 4) = 0; // NULL == end of data
DX::ThrowIfFailed(pEncoderMetadataQueryWriter->SetMetadataByName(L"/appext/Data", &propValue));
// ...
注意其中 /appext/Data
实际本质是一个字节数组,其内容为 31000
,代表无限循环 gif
(注释中说得很清楚),这就应该是循环 gif
的关键所在。
可见,首先需要创建一个 PROPVARIANT
对象,然后调用 IWICBitmapEncoder
中的 GetMetadataQueryWriter
方法,获取 IWICMetadataQueryWriter
,然后通过该方法中的 SetMetadataByName
,将 /appext/Application
以及 /appext/Data
按照指定的格式传入即可,还能有问题什么呢?
问题可大咯!
坑人的Variant
刚好 SharpDX
对这些 C++/COM
接口有看似正确的移植,首先 GifBitmapEncoder
提供了 MetadataQueryWriter
属性(而不是 Get
函数),非常贴心,该类中也包含了名字相同的设置函数方法,其签名如下:
public unsafe void SetMetadataByName(string name, object value) { /* ... */ }
嗯,非常合理,一个键值对而已,能有什么问题?我便用了起来,我自以为代码可能也许大概应该长这个样子:
encoder.MetadataQueryWriter.SetMetadataByName("/appext/Application", "NETSCAPE2.0");
encoder.MetadataQueryWriter.SetMetadataByName("/appext/Data", new byte[] { 3, 1, 0, 0, 0 });
然而运行报错了,错误信息为:
SharpDX.SharpDXException: HRESULT: [0x80070057], Module: [General], ApiCode: [E_INVALIDARG/Invalid Arguments], Message: 参数错误。
at SharpDX.Result.CheckError() in C:\projects\sharpdx\Source\SharpDX\Result.cs:line 197
at SharpDX.WIC.MetadataQueryWriter.SetMetadataByName(String name, IntPtr varValueRef) in C:\projects\sharpdx\Source\SharpDX.Direct2D1\Generated\REFERENCE\WIC\Interfaces.cs:line 4923
at SharpDX.WIC.MetadataQueryWriter.SetMetadataByName(String name, Object value) in C:\projects\sharpdx\Source\SharpDX.Direct2D1\WIC\MetadataQueryWriter.cs:line 91
at UserQuery.SaveD2DBitmap(Int32 width, Int32 height, String text) in C:\Users\sdfly\AppData\Local\Temp\LINQPad6\_uciwedks\kncljn\LINQPadQuery:line 79
at UserQuery.Main() in C:\Users\sdfly\AppData\Local\Temp\LINQPad6\_uciwedks\kncljn\LINQPadQuery:line 5
反编译它这个 SetMetadataByName
一看,其代码如下:
public unsafe void SetMetadataByName(string name, object value)
{
byte* variant = stackalloc byte[512];
Variant* variantStruct = (Variant*)variant;
variantStruct->Value = value;
SetMetadataByName(name, (IntPtr)(void*)variant);
}
internal unsafe void SetMetadataByName(string name, IntPtr varValueRef) { /* ... */ }
原来 objectvalue
的本质是它内部骚操作创建了一个 Variant
,该 Variant
就对应了 C++
中的 PROPVARIANT
,然后后续调用的正确性取决于 Variant.Value
属性的设置方法,该属性的源代码如下:
// SharpDX.Win32.Variant
using SharpDX.Mathematics.Interop;
using System;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
public unsafe object Value
{
get { /* ... */ }
set
{
if (value == null)
{
Type = VariantType.Default;
ElementType = VariantElementType.Null;
return;
}
Type type = value.GetType();
Type = VariantType.Default;
if (type.GetTypeInfo().get_IsPrimitive())
{
if ((object)type == typeof(byte)) // ...
if ((object)type == typeof(sbyte)) // ...
if ((object)type == typeof(int)) // ...
if ((object)type == typeof(uint)) // ...
if ((object)type == typeof(long)) // ...
if ((object)type == typeof(ulong)) // ...
if ((object)type == typeof(short)) // ...
if ((object)type == typeof(ushort)) // ...
if ((object)type == typeof(float)) // ...
if ((object)type == typeof(double)) // ...
}
else
{
if (value is ComObject) // ...
if (value is DateTime) // ...
if (value is string) // ...
}
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture, "Type [{0}] is not handled", new object[1]
{
type.get_Name()
}));
}
}
在原代码,该属性全部代码长达约 300
行,可见作者是花了些心思的,判断了那个 object
的“各种”情况,根据上文中的 C++
代码,我需要的是 VT_UI1|VT_VECTOR
然而这么多情况,就是没找到我需要的那一种????。
最重要的是,直接传 IntPtr
的另一个重载它居然是 internal
的,导致我几乎没什么操作空间了。
逢山开路,遇水搭桥
我花了很多时间怎么在 SharpDX
已有提供的 Variant
对象上做手脚,但感觉代码会比较复杂,所以我走向了另一条路,从那个 internal
,传 IntPtr
的接口出发解决。
调用私有方法
传 IntPtr
的方法是私有的,不能直接调用,但可以使用反射来创建一个指向该私有方法的委托,然后通过该委托调用这个方法:
var setMetadataMethod = encoder.MetadataQueryWriter
.GetType()
.GetMethod(nameof(WIC.MetadataQueryWriter.SetMetadataByName), BindingFlags.NonPublic | BindingFlags.Instance);
var setMetadata = (Action<string, IntPtr>)setMetadataMethod
.CreateDelegate(typeof(Action<string, IntPtr>), encoder.MetadataQueryWriter);
这样一来即可通过 setMetadata
委托来调用。
不一样的Variant
SharpDX
提供了一个 Variant
,不代表必须要用他的 Variant
,他想通过高达 300
行代码的 if/else
搞定一切,导致代码超级复杂。而通过查询 C++
接口,我能将 Variant
简化为如下一个 struct
:
[StructLayout(LayoutKind.Explicit)]
struct PV
{
[FieldOffset(0)] short VT;
[FieldOffset(8)] int Length;
[FieldOffset(8)] IntPtr StringBuffer;
[FieldOffset(8)] ushort UShortValue;
[FieldOffset(16)] IntPtr Buffer;
public static PV CreateUByteVector(int length, IntPtr buffer) => new PV
{
VT = (short)VariantType.Vector + (short)VariantElementType.UByte,
Length = length,
Buffer = buffer
};
public static PV CreateString(IntPtr buffer) => new PV
{
VT = (short)VariantElementType.StringPointer,
StringBuffer = buffer,
};
public static PV CreateUI2(ushort val) => new PV
{
VT = (short)VariantElementType.UShort,
UShortValue = val,
};
}
指定/appext/Application
这是一个指向字符串的指针,正常可能要用 fixed
关键字或者 Marshal.AllocHGlobal
方法,但这里有更简单的办法,我可以使用 stackalloc
,因为此处我具备该内存的所有权,且能精准控制它的生命周期,而且它性能也更好:
// /appext/Application: NETSCAPE2.0
byte* bytes = stackalloc byte[11] { 78, 69, 84, 83, 67, 65, 80, 69, 50, 46, 48 };
PV pv = PV.CreateUByteVector(11, (IntPtr)bytes);
setMetadata("/appext/Application", (IntPtr)(void*)&pv);
指定/appext/Data
同样的道理,将 /appext/data
的 31000
分配在栈上代码最简单,性能也最好:
// /appext/Data: 3, 1, [0, 0], 0
byte* bytes2 = stackalloc byte[5] { 3, 1, 0, 0, 0, };
PV pv2 = PV.CreateUByteVector(5, (IntPtr)bytes2);
setMetadata("/appext/Data", (IntPtr)(void*)&pv2);
注意,不是所有内存都能分配到栈上,它有两大限制,首先栈内存是有限的,其次,该内存必须立即被使用,否则栈销毁后这些内存指针也会立即失效。
优化
其实有了这个,还能做更多的操作。默认图片的运行速度比较慢,可以将图片的速度调快一点,这也是通过设置 /grctlext/Delay
实现的,只是该 metadata
是存在于帧上(而不是图片容器上):
// frame delay by 50ms.
var setMetadata = (Action<string, IntPtr>)setMetadataMethod
.CreateDelegate(typeof(Action<string, IntPtr>), frame.MetadataQueryWriter);
var pv = PV.CreateUI2(5);
setMetadata("/grctlext/Delay", (IntPtr)(void*)&pv);
我设置的是 5
,代表 50ms
,该值的意思就是乘以 10ms
,如 10
就代表 100ms
,每一帧都可以不一样。如果不设置,那就由呈现器决定 gif
播放的速度,大部分播放器都是 100ms
。如果设置为 1
或者 0
,则会被呈现器忽略,如果想要在浏览器上运行,我亲测最低可以设置的值为 2
。
下图为默认效果:
下图为 50ms
效果:
下图为 20ms
效果:
还能如指定 Gif
的 Comment
字段,可以给自己做一个签名:
// "/commentext/TextEntry": "Created by Flysha.Zhou\0"
var commentsBytes = Encoding.UTF8.GetBytes("Created by Flysha.Zhou\0");
fixed (byte* p = commentsBytes)
{
var pv3 = PV.CreateString((IntPtr)p);
setMetadata("/commentext/TextEntry", (IntPtr)(void*)&pv3);
}
结语
上述代码都已经上传到了我的博客 Github
中: https://github.com/sdcb/blog-data/tree/master/2019/20191127-the-variant-bug-in-sharpdx
由于 SharpDX
官方已经停止维护,但迟迟又没有其它包顶上,官方是不可能修这些东西????,这些坑只能自己默默踩了(这波“福报”只能默默领好了)。有时我觉得要是脾气来了,我甚至可以 SharpDX
拿过来自己维护一个发行版,因为 SharpDX
在平时的生活工作中实在是太常用、太常见了。
微信不能评论,有想法的朋友可以点击阅读原文,或者到这个地址(博客园)进行点赞/评论:https://www.cnblogs.com/sdflysha/p/20191127-the-variant-bug-in-sharpdx.html
喜欢的朋友 请关注我的微信公众号:【DotNet骚操作】