和SharpDX坑爹的Variant剛正面


和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實際本質是一個字節數組,其內容為3 1 0 0 0,代表無限循環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) { /* ... */ }

原來object value的本質是它內部騷操作創建了一個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/data3 1 0 0 0分配在棧上代碼最簡單,性能也最好:

// /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效果:

還能如指定GifComment字段,可以給自己做一個簽名:

// "/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在平時的生活工作中實在是太常用、太常見了。

喜歡的朋友 請關注我的微信公眾號:【DotNet騷操作】

DotNet騷操作


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM