Created
July 13, 2025 01:29
-
-
Save ariankordi/6e8a4ec77c87ee8e219370b744be3a4b to your computer and use it in GitHub Desktop.
Mii thumbnailer for Windows PoC using the FFL-Testing Mii renderer server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* \file FFLTestingThumbProvider.cpp | |
* \author Arian Kordi (https://github.com/ariankordi) | |
* \date 2025/07/12 | |
* | |
* \brief Windows thumbnail provider for Mii data files using | |
* the FFL-Testing Mii renderer server: https://github.com/ariankordi/FFL-Testing | |
* \details Requires setting up and running on default port of 12346. | |
* Not ideal for real use. Should be considered a proof-of-concept/toy. | |
* https://github.com/ariankordi | |
* | |
* Build (MSVC/Visual Studio): | |
cl /LD /Fefflthumb.dll FFLTestingThumbProvider.cpp ole32.lib gdi32.lib ^ | |
shlwapi.lib advapi32.lib ws2_32.lib shell32.lib ntdll.lib ^ | |
/link -EXPORT:DllGetClassObject,PRIVATE -EXPORT:DllCanUnloadNow,PRIVATE ^ | |
-EXPORT:DllRegisterServer,PRIVATE -EXPORT:DllUnregisterServer,PRIVATE | |
* Build (gcc/MinGW-w64): | |
x86_64-w64-mingw32-g++ -s -shared -o fflthumb.dll \ | |
FFLTestingThumbProvider.cpp -lole32 -lgdi32 -lshlwapi \ | |
-ladvapi32 -lws2_32 -luuid | |
* (Optionally, to exclude STL: gcc instead of g++, extra args: | |
* -nostdlib -lkernel32 -Wl,-entry,DllMain -fno-exceptions) | |
* | |
* | |
* Register for current user: | |
* regsvr32 fflthumb.dll (calls DllRegisterServer) | |
* Unregister: | |
* regsvr32 /u fflthumb.dll (calls DllUnregisterServer) | |
*/ | |
#define WIN32_LEAN_AND_MEAN | |
#include <windows.h> | |
#include <initguid.h> // To allow building on MinGW. | |
#include <shlwapi.h> // SHDeleteKeyW, SHChangeNotify | |
#include <thumbcache.h> // IThumbnailProvider, WTS_ALPHATYPE | |
#include <shlobj.h> | |
#include <strsafe.h> | |
// #include <new> | |
#include <winsock2.h> | |
#include <ws2tcpip.h> | |
#ifdef _MSC_VER | |
// Export symbols for MSVC. | |
#pragma comment(linker, "/EXPORT:DllGetClassObject,PRIVATE") | |
#pragma comment(linker, "/EXPORT:DllCanUnloadNow,PRIVATE") | |
#pragma comment(linker, "/EXPORT:DllRegisterServer,PRIVATE") | |
#pragma comment(linker, "/EXPORT:DllUnregisterServer,PRIVATE") | |
#endif | |
/// Address and port of the renderer server. | |
#define SERVER_ADDR "127.0.0.1" | |
#define SERVER_PORT "12346" | |
/// Definition to enable showing a message box when any COM method is called. | |
/// This is useful for knowing when to inject a debugger into dllhost.exe. | |
// #define MESSAGE_BOX_DEBUG | |
/// If this is defined, a static buffer for the thumbnail | |
/// will be used instead of dynamically allocating from heap. | |
#define IMAGE_STATIC_BUF | |
/// Defines the maximum resolution. | |
/// Requests for thumbnails higher than this will use the max. | |
#define THUMBNAIL_MAX_CX 1024 | |
/// The CLSID and title for our thumbnail provider. | |
#define THUMBNAIL_PROVIDER_CLSID L"{4C7CA1A0-C21C-4284-85DC-7AE838138FED}" | |
#define THUMBNAIL_PROVIDER_TITLE L"FFL-Testing Thumbnail Provider" | |
/// CLSID for the IThumbnailProvider shell extension. | |
/// See: https://learn.microsoft.com/en-us/windows/win32/shell/thumbnail-providers | |
#define SHELLEX_THUMBNAIL_CLSID L"ShellEx\\{E357FCCD-A995-4576-B01F-234630154E96}" | |
/// Macro to create a registry key string. | |
/// File extensions must include leading '.' | |
#define MAKE_EXT_KEY(ext) \ | |
L"Software\\Classes\\" ext L"\\" SHELLEX_THUMBNAIL_CLSID | |
/// The registry keys for extensions this handler supports. | |
static constexpr const wchar_t* g_extensionKeyPaths[] = { | |
// Wii U/3DS (Ver3StoreData) | |
MAKE_EXT_KEY(L".ffsd"), // Wii U (FFLStoreData) | |
MAKE_EXT_KEY(L".cfsd"), // 3DS (CFLStoreData) | |
MAKE_EXT_KEY(L".3dsmii"), // Custom: 3DS (kazuki-4ys / CFLiPackedMiiDataOfficial) | |
// Also custom: "cfcd"/"ffcd" (https://github.com/HEYimHeroic/MiiDataFiles/blob/main/README.md#wii-u3ds-formats) - but not used by any tools | |
// Wii | |
MAKE_EXT_KEY(L".rsd"), // Wii (RFLStoreData) - Usually preferred for newer Wii games | |
MAKE_EXT_KEY(L".rcd"), // Wii (RFLCharData) | |
MAKE_EXT_KEY(L".mii"), // Custom: Usually Wii | |
// .mii is also used in Mario Golf on Switch for CharInfo | |
MAKE_EXT_KEY(L".miigx"), // Custom: Wii (SaveGame Manager GX / RFLCharData) | |
MAKE_EXT_KEY(L".mae"), // Custom: Wii (My Avatar Editor / RFLCharData) | |
// Switch | |
MAKE_EXT_KEY(L".charinfo"), // NintendoSDK NX Add-On (nn::mii::CharInfo) | |
MAKE_EXT_KEY(L".mnms"), // Custom: studio.mii.nintendo.com web editor | |
MAKE_EXT_KEY(L".nfcd"), // Custom: NintendoSDK (nn::mii::CoreData for import file) | |
MAKE_EXT_KEY(L".nfsd") // Custom: NintendoSDK (nn::mii::StoreData for database) | |
}; | |
/// Global parsed CLSID object from g_thumbClsidString. | |
static CLSID g_thumbClsid; | |
/// Windows module handle saved from DllMain. | |
static HMODULE g_moduleHandle = nullptr; | |
// snippet below from: https://github.com/microsoft/CMake/blob/a5caf2fee0a42735b8f5f54e146da39099f1a8a6/Utilities/cmlibarchive/libarchive/archive_write_set_format_cpio_binary.c#L75 | |
// snippet to define that a struct should not have alignment | |
#ifdef __GNUC__ | |
#define PACKED(x) x __attribute__((packed)) | |
#elif defined(_MSC_VER) | |
#define PACKED(x) __pragma(pack(push, 1)) x __pragma(pack(pop)) | |
#endif | |
/// Packed request structure sent to the server. | |
/// See: https://github.com/ariankordi/FFL-Testing/blob/renderer-server-prototype/include/RenderRequest.h | |
/// Structure representing a render request, derived from | |
/// request query parameters by caller (web server). | |
PACKED(struct RenderRequest | |
{ | |
unsigned char data[96]; // just a buffer that accounts for maximum size | |
unsigned short dataLength; // determines the mii data format | |
unsigned char modelFlag; // FFLModelType + nose flatten @ bit 4 | |
// completely changes the response type: | |
unsigned char responseFormat; // indicates if response is gltf or tga | |
// note that arbitrary resolutions CRASH THE BACKEND | |
unsigned short resolution; // resolution for render buffer | |
// texture resolution controls mipmap enable (1 << 30) | |
short texResolution; // FFLResolution/u32, negative = mipmap | |
unsigned char viewType; // camera view (setViewTypeParams) | |
char resourceType; // FFLResourceType (default high/-1) | |
unsigned char shaderType; // custom ShaderType | |
unsigned char expression; // used if expressionFlag is all zeroes | |
unsigned int expressionFlag[3]; // casted to FFLAllExpressionFlag | |
// used for multiple expressions | |
// expressionFlag will only apply in gltf mode for now | |
short cameraRotate[3]; // converted deg2rad to vector | |
short modelRotate[3]; // same as above | |
unsigned char backgroundColor[4]; // passed to clearcolor | |
// controls scaling/anti-aliasing mode: | |
unsigned char aaMethod; // TODO: to be implemented perfectly | |
unsigned char drawStageMode; // custom DrawStageMode: opa, xlu, all | |
bool verifyCharInfo; // for FFLiVerifyCharInfoWithReason | |
bool verifyCRC16; // passed to pickupCharInfoFromData | |
bool lightEnable; // passed to IShader::bind() | |
char clothesColor; // favorite color, -1 for default | |
char pantsColor; // PantsColor, -1 = default shader | |
char bodyType; // BodyType, -1 = default for shader | |
char headwearIndex; // -1 = disabled. | |
char headwearColor; // -1 | |
unsigned char instanceCount; // for instanceCountNewRender loop | |
unsigned char instanceRotationMode; // model, camera, TODO | |
short lightDirection[3]; // unset if all negative, TODO | |
unsigned char splitMode; // none (default), front, back, both}; | |
}); | |
/// Indicator at the beginning of a response that there was an error. | |
static const char* socketErrorPrefix = "ERROR: "; | |
#define ERROR_LEN sizeof(socketErrorPrefix) - 1 // Exclude terminator. | |
#define TGA_HEADER_SIZE 18 | |
#ifdef IMAGE_STATIC_BUF | |
/// Static RGBA buffer for thumbnails. | |
/// NOTE: ASSUMES that only one thumbnail is made at a time (single-threaded) | |
static unsigned char gImageBuf[THUMBNAIL_MAX_CX * THUMBNAIL_MAX_CX * 4]; | |
#endif | |
/// \brief Helper: connect, send request, receive TGA, return HBITMAP. | |
/// \param req[in] Filled RenderRequest. | |
/// \param phbmp[out] Receives the GDI bitmap. | |
/// \returns S_OK or error. | |
static HRESULT DoRenderRequest(const RenderRequest& req, HBITMAP* phbmp) | |
{ | |
if (!phbmp) return E_POINTER; | |
*phbmp = nullptr; | |
WSADATA wsa = {}; | |
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) | |
return HRESULT_FROM_WIN32(WSAGetLastError()); | |
struct addrinfo hints = {}, * res; | |
hints.ai_family = AF_UNSPEC; | |
hints.ai_socktype = SOCK_STREAM; | |
if (getaddrinfo(SERVER_ADDR, SERVER_PORT, &hints, &res) != 0) | |
{ | |
WSACleanup(); | |
return E_FAIL; | |
} | |
SOCKET s = socket(res->ai_family, res->ai_socktype, res->ai_protocol); | |
if (s == INVALID_SOCKET) | |
{ | |
freeaddrinfo(res); | |
WSACleanup(); | |
return HRESULT_FROM_WIN32(WSAGetLastError()); | |
} | |
if (connect(s, res->ai_addr, (int)res->ai_addrlen) != 0) | |
{ | |
closesocket(s); | |
freeaddrinfo(res); | |
WSACleanup(); | |
return HRESULT_FROM_WIN32(WSAGetLastError()); | |
} | |
freeaddrinfo(res); | |
// Send the request. | |
int toSend = sizeof(req), sent = 0; | |
const char* buf = reinterpret_cast<const char*>(&req); | |
while (sent < toSend) | |
{ | |
int n = send(s, buf + sent, toSend - sent, 0); | |
if (n <= 0) | |
{ | |
closesocket(s); | |
WSACleanup(); | |
return E_FAIL; | |
} | |
sent += n; | |
} | |
// Need to peek at the first 7 bytes to tell if this is an error. | |
char headerBuf[ERROR_LEN] = {}; | |
int peeked = recv(s, headerBuf, ERROR_LEN, MSG_PEEK); | |
// STL: Can be replaced with memcmp. | |
if (peeked == ERROR_LEN && | |
RtlCompareMemory(headerBuf, socketErrorPrefix, ERROR_LEN) == ERROR_LEN) | |
{ | |
// socketErrorPrefix found in the message, meaning error. | |
closesocket(s); | |
WSACleanup(); | |
return E_FAIL; | |
} | |
// Receive 18-byte TGA header. | |
unsigned char tgaHeader[TGA_HEADER_SIZE]{}; | |
int rec = 0; | |
while (rec < TGA_HEADER_SIZE) | |
{ | |
int n = recv(s, (char*)tgaHeader + rec, TGA_HEADER_SIZE - rec, 0); | |
if (n <= 0) | |
{ | |
closesocket(s); | |
WSACleanup(); | |
return E_FAIL; | |
} | |
rec += n; | |
} | |
// Parse little-endian width/height from TGA header. | |
unsigned short width = static_cast<unsigned short>(tgaHeader[12]) | static_cast<unsigned short>(tgaHeader[13]) << 8; | |
unsigned short height = static_cast<unsigned short>(tgaHeader[14]) | static_cast<unsigned short>(tgaHeader[15]) << 8; | |
if (!width || !height) | |
{ | |
closesocket(s); | |
WSACleanup(); | |
return E_FAIL; | |
} | |
// Allocate pixel buffer RGBA | |
size_t pixelsize = size_t(width) * height * 4; // Calculate RGBA byte size. | |
#ifndef IMAGE_STATIC_BUF | |
// STL: Can be replaced with malloc (+ HeapFree = free) | |
unsigned char* pixels = (unsigned char*)HeapAlloc(GetProcessHeap(), 0, pixelsize); | |
if (!pixels) | |
{ | |
closesocket(s); | |
WSACleanup(); | |
return E_OUTOFMEMORY; | |
} | |
#else | |
unsigned char* pixels = gImageBuf; | |
#endif | |
// Receive all pixel bytes | |
size_t total = 0; | |
while (total < pixelsize) | |
{ | |
int n = recv(s, (char*)pixels + total, (int)(pixelsize - total), 0); | |
if (n <= 0) | |
{ | |
#ifndef IMAGE_STATIC_BUF | |
HeapFree(GetProcessHeap(), 0, pixels); | |
#endif | |
closesocket(s); | |
WSACleanup(); | |
return E_FAIL; | |
} | |
total += n; | |
} | |
closesocket(s); | |
WSACleanup(); | |
// Create DIBSection (BGRA) | |
BITMAPINFO bmi = {}; | |
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader); | |
bmi.bmiHeader.biWidth = width; | |
bmi.bmiHeader.biHeight = -static_cast<int>(height); | |
bmi.bmiHeader.biPlanes = 1; | |
bmi.bmiHeader.biBitCount = 32; | |
bmi.bmiHeader.biCompression = BI_RGB; | |
void* mem = nullptr; | |
HBITMAP hbitmap = CreateDIBSection(nullptr, &bmi, DIB_RGB_COLORS, &mem, nullptr, 0); | |
if (!hbitmap) | |
{ | |
#ifndef IMAGE_STATIC_BUF | |
HeapFree(GetProcessHeap(), 0, pixels); | |
#endif | |
return E_FAIL; | |
} | |
// Copy RGBA into BGRA (flip R/B) | |
BYTE* dst = (BYTE*)mem; | |
// for (size_t i = 0; i < pixelsize; i += 4) | |
// Ensure that a full 4-byte read is safe. | |
for (size_t i = 0; i + 3 < pixelsize; i += 4) | |
{ | |
dst[i + 0] = pixels[i + 2]; // B <- R | |
dst[i + 1] = pixels[i + 1]; // G | |
dst[i + 2] = pixels[i + 0]; // R <- B | |
dst[i + 3] = pixels[i + 3]; // A | |
} | |
#ifndef IMAGE_STATIC_BUF | |
HeapFree(GetProcessHeap(), 0, pixels); | |
#endif | |
*phbmp = hbitmap; | |
return S_OK; | |
} | |
/// Helper to make a RenderRequest template. | |
static RenderRequest MakeRenderRequest() | |
{ | |
RenderRequest req = {}; | |
req.lightEnable = 1; | |
req.resourceType = -1; | |
req.bodyType = -1; | |
req.clothesColor = -1; | |
req.headwearIndex = -1; | |
req.headwearColor = -1; | |
req.backgroundColor[0] = 255; | |
req.backgroundColor[1] = 255; | |
req.backgroundColor[2] = 255; | |
req.backgroundColor[3] = 0; | |
req.lightDirection[0] = -1; | |
req.lightDirection[1] = -1; | |
req.lightDirection[2] = -1; | |
req.modelFlag = 1 << 0; | |
req.shaderType = 1; // SHADER_TYPE_SWITCH | |
req.viewType = 5; // VIEW_TYPE_NNMII_VARIABLEICONBODY | |
return req; | |
} | |
/** | |
* \brief Compute a simple hash of up to the first 64 bytes of the stream. | |
* \param pStream[in] The input IStream (must be seekable). | |
* \param outColor[out] RGB color packed as 0x00RRGGBB. | |
* \return HRESULT S_OK on success, error otherwise. | |
*/ | |
/* | |
static HRESULT ComputeStreamColorHash(IStream* pStream, unsigned int& outColor) | |
{ | |
// Seek to beginning | |
LARGE_INTEGER zero = {}; | |
HRESULT hr = pStream->Seek(zero, STREAM_SEEK_SET, nullptr); | |
if (FAILED(hr)) return hr; | |
BYTE buffer[64] = {}; | |
ULONG bytesRead = 0; | |
hr = pStream->Read(buffer, sizeof(buffer), &bytesRead); | |
if (FAILED(hr)) return hr; | |
// Simple sum-based hash. | |
unsigned int sum = 0; | |
for (ULONG i = 0; i < bytesRead; ++i) | |
{ | |
sum += buffer[i]; | |
} | |
// Derive R, G, B from sum | |
BYTE r = static_cast<BYTE>(sum & 0xFF); | |
BYTE g = static_cast<BYTE>((sum >> 8u) & 0xFF); | |
BYTE b = static_cast<BYTE>((sum >> 16u) & 0xFF); | |
outColor = (r << 16u) | (g << 8u) | b; | |
return S_OK; | |
} | |
*/ | |
/** | |
* \brief Create a solid color BGRA HBITMAP of size cx x cx based on stream hash. | |
* \param pStream[in] The input IStream for hashing. | |
* \param cx[in] Desired thumbnail dimension (square). | |
* \param phbmp[out] Receives the created HBITMAP. | |
* \return HRESULT S_OK on success, error otherwise. | |
*/ | |
/* | |
static HRESULT CreateSolidColorThumbnail( | |
IStream* pStream, | |
UINT cx, | |
HBITMAP* phbmp) | |
{ | |
if (!pStream || !phbmp) | |
return E_POINTER; | |
unsigned int rgb = 0; | |
HRESULT hr = ComputeStreamColorHash(pStream, rgb); | |
if (FAILED(hr)) | |
return hr; | |
// Define 32bpp BGRA DIB | |
BITMAPINFO bmi = {}; | |
bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader); | |
bmi.bmiHeader.biWidth = static_cast<LONG>(cx); | |
bmi.bmiHeader.biHeight = -static_cast<LONG>(cx); ///< negative = top-down | |
bmi.bmiHeader.biPlanes = 1; | |
bmi.bmiHeader.biBitCount = 32; | |
bmi.bmiHeader.biCompression = BI_RGB; | |
void* pixels = nullptr; | |
HBITMAP hbm = CreateDIBSection( | |
nullptr, | |
&bmi, | |
DIB_RGB_COLORS, | |
&pixels, | |
nullptr, | |
0); | |
if (!hbm || !pixels) | |
return E_OUTOFMEMORY; | |
// Flip data from RGBA to BGRA. | |
BYTE* ptr = static_cast<BYTE*>(pixels); | |
BYTE blue = static_cast<BYTE>(rgb & 0xFF); | |
BYTE green = static_cast<BYTE>((rgb >> 8) & 0xFF); | |
BYTE red = static_cast<BYTE>((rgb >> 16) & 0xFF); | |
BYTE alpha = static_cast<BYTE>((rgb >> 24) & 0xFF); | |
for (UINT y = 0; y < cx; ++y) | |
{ | |
for (UINT x = 0; x < cx; ++x) | |
{ | |
ptr[0] = blue; | |
ptr[1] = green; | |
ptr[2] = red; | |
ptr[3] = alpha; | |
ptr += 4; | |
} | |
} | |
*phbmp = hbm; | |
return S_OK; | |
} | |
*/ | |
/// COM object implementing IInitializeWithStream and IThumbnailProvider. | |
class ThumbnailProvider : public IInitializeWithStream, public IThumbnailProvider | |
{ | |
private: | |
void ReleaseStream() | |
{ | |
if (m_pStream) | |
{ | |
m_pStream->Release(); | |
m_pStream = nullptr; | |
} | |
} | |
public: | |
/// Constructor, initializes ref count. | |
ThumbnailProvider() : m_refCount(1), m_pStream(nullptr) {} | |
/// Destructor, releases stored stream. | |
/// NOTE: This has been ditched in favor of releasing | |
/// the stream in GetThumbnail, in order to have no destructors. | |
//~ThumbnailProvider() | |
//{ | |
// ReleaseStream(); | |
//} | |
// IUnknown | |
IFACEMETHODIMP QueryInterface(REFIID riid, void** ppvObject) override | |
{ | |
if (!ppvObject) | |
return E_POINTER; | |
if (InlineIsEqualGUID(riid, IID_IUnknown) || | |
InlineIsEqualGUID(riid, IID_IInitializeWithStream)) | |
{ | |
*ppvObject = static_cast<IInitializeWithStream*>(this); | |
} | |
else if (InlineIsEqualGUID(riid, IID_IThumbnailProvider)) | |
{ | |
*ppvObject = static_cast<IThumbnailProvider*>(this); | |
} | |
else | |
{ | |
*ppvObject = nullptr; | |
return E_NOINTERFACE; | |
} | |
AddRef(); | |
return S_OK; | |
} | |
IFACEMETHODIMP_(ULONG) AddRef() override | |
{ | |
return InterlockedIncrement(&m_refCount); | |
} | |
IFACEMETHODIMP_(ULONG) Release() override | |
{ | |
ULONG count = InterlockedDecrement(&m_refCount); | |
//if (count == 0) | |
// delete this; | |
return count; | |
} | |
// IInitializeWithFile: Not implemented. | |
// IInitializeWithStream | |
IFACEMETHODIMP Initialize(IStream* pStream, DWORD /*grfMode*/) override | |
{ | |
if (!pStream) | |
return E_INVALIDARG; | |
#if defined(_DEBUG) && defined(MESSAGE_BOX_DEBUG) | |
if (!IsDebuggerPresent()) | |
{ | |
MessageBoxW(nullptr, | |
L"IInitializeWithStream::Initialize called", | |
NULL, | |
MB_OK); | |
} | |
#endif | |
// Store and addref the stream | |
pStream->AddRef(); | |
m_pStream = pStream; | |
return S_OK; | |
} | |
// IThumbnailProvider | |
IFACEMETHODIMP GetThumbnail( | |
UINT cx, | |
HBITMAP* phbmp, | |
WTS_ALPHATYPE* pdwAlpha) override | |
{ | |
#if defined(_DEBUG) && defined(MESSAGE_BOX_DEBUG) | |
if (!IsDebuggerPresent()) | |
{ | |
MessageBoxW(nullptr, | |
L"IThumbnailProvider::GetThumbnail called", | |
NULL, | |
MB_OK); | |
} | |
#endif | |
if (!m_pStream || !phbmp || !pdwAlpha) | |
return E_POINTER; | |
*phbmp = nullptr; | |
*pdwAlpha = WTSAT_ARGB; // use alpha | |
// Check if the resolution exceeds the max, and clamp if needed. | |
if (cx > THUMBNAIL_MAX_CX) | |
cx = THUMBNAIL_MAX_CX; | |
// Build a RenderRequest template. | |
RenderRequest req = MakeRenderRequest(); | |
// Read up to 96 bytes from the file into req.data. | |
LARGE_INTEGER zero = {}; | |
HRESULT hr = m_pStream->Seek(zero, STREAM_SEEK_SET, nullptr); | |
if (FAILED(hr)) return hr; | |
ULONG bytesRead = 0; | |
hr = m_pStream->Read(req.data, sizeof(req.data), &bytesRead); | |
if (FAILED(hr)) return hr; | |
req.dataLength = static_cast<unsigned short>(bytesRead); | |
req.responseFormat = 0; // Expect TGA, RGBA | |
req.resolution = static_cast<unsigned short>(cx); | |
req.texResolution = static_cast<short>(cx); | |
// HRESULT hr = CreateSolidColorThumbnail(m_pStream, cx, phbmp); | |
hr = DoRenderRequest(req, phbmp); | |
if (FAILED(hr)) | |
{ | |
#ifdef _DEBUG | |
wchar_t msg[64]; | |
swprintf_s(msg, _countof(msg), | |
L"GetThumbnail failed: HRESULT=0x%08X", (unsigned int)hr); | |
MessageBoxW(nullptr, msg, NULL, MB_OK); | |
#endif | |
return hr; | |
} | |
ReleaseStream(); | |
return S_OK; | |
} | |
private: | |
LONG m_refCount; ///< COM reference count | |
IStream* m_pStream; ///< Stored file stream | |
}; | |
static ThumbnailProvider g_ThumbnailProvider; | |
/// COM Class Factory for ThumbnailProvider. | |
class ClassFactory : public IClassFactory | |
{ | |
public: | |
ClassFactory() : m_refCount(1) {} | |
// ~ClassFactory() {} | |
// IUnknown | |
IFACEMETHODIMP QueryInterface(REFIID riid, void** ppvObject) override | |
{ | |
if (!ppvObject) | |
return E_POINTER; | |
if (InlineIsEqualGUID(riid, IID_IUnknown) || | |
InlineIsEqualGUID(riid, IID_IClassFactory)) | |
{ | |
*ppvObject = static_cast<IClassFactory*>(this); | |
AddRef(); | |
return S_OK; | |
} | |
*ppvObject = nullptr; | |
return E_NOINTERFACE; | |
} | |
IFACEMETHODIMP_(ULONG) AddRef() override | |
{ | |
return InterlockedIncrement(&m_refCount); | |
} | |
IFACEMETHODIMP_(ULONG) Release() override | |
{ | |
LONG count = InterlockedDecrement(&m_refCount); | |
//if (count == 0) | |
// delete this; | |
return count; | |
} | |
// IClassFactory | |
IFACEMETHODIMP CreateInstance( | |
IUnknown* pUnkOuter, | |
REFIID riid, | |
void** ppvObject) override | |
{ | |
if (pUnkOuter != nullptr) | |
return CLASS_E_NOAGGREGATION; | |
ThumbnailProvider* pObj = &g_ThumbnailProvider; | |
//ThumbnailProvider* pObj = new (std::nothrow) ThumbnailProvider(); | |
if (!pObj) | |
return E_OUTOFMEMORY; | |
HRESULT hr = pObj->QueryInterface(riid, ppvObject); | |
pObj->Release(); // balance ref from creation | |
return hr; | |
} | |
IFACEMETHODIMP LockServer(BOOL/* fLock*/) override | |
{ | |
// Not tracking locks; always succeed | |
return S_OK; | |
} | |
private: | |
LONG m_refCount; ///< Factory ref count | |
// char _pad[4]; | |
}; | |
/// DLL entry point. | |
/// Parses the CLSID string into g_thumbClsid. | |
extern "C" BOOL APIENTRY DllMain( | |
HMODULE hModule, | |
DWORD ul_reason_for_call, | |
LPVOID /*lpReserved*/) | |
{ | |
HRESULT hr; | |
switch (ul_reason_for_call) | |
{ | |
case DLL_PROCESS_ATTACH: | |
g_moduleHandle = hModule; | |
DisableThreadLibraryCalls(hModule); | |
// Parse our CLSID once | |
hr = CLSIDFromString(THUMBNAIL_PROVIDER_CLSID, | |
&g_thumbClsid); | |
if (FAILED(hr)) DbgRaiseAssertionFailure(); | |
break; | |
case DLL_PROCESS_DETACH: | |
break; | |
} | |
return TRUE; | |
} | |
static ClassFactory g_ClassFactory; | |
/// Standard COM export to retrieve our ClassFactory. | |
#ifdef _MSC_VER | |
_Check_return_ | |
#endif | |
STDAPI DllGetClassObject( | |
_In_ REFCLSID rclsid, | |
_In_ REFIID riid, | |
_Outptr_ void** ppv) | |
{ | |
if (!ppv) | |
return E_POINTER; | |
*ppv = nullptr; | |
if (!InlineIsEqualGUID(rclsid, g_thumbClsid)) | |
return CLASS_E_CLASSNOTAVAILABLE; | |
// ClassFactory* factory = new (std::nothrow) ClassFactory(); | |
ClassFactory* factory = &g_ClassFactory; | |
if (!factory) | |
return E_OUTOFMEMORY; | |
HRESULT hr = factory->QueryInterface(riid, ppv); | |
factory->Release(); | |
return hr; | |
} | |
/// COM unload check. Always allow unload. | |
#ifdef _MSC_VER | |
__control_entrypoint(DllExport) | |
#endif | |
STDAPI DllCanUnloadNow(void) | |
{ | |
return S_OK; | |
} | |
#define THUMBNAIL_PROVIDER_CLSID_KEY_PATH L"Software\\Classes\\CLSID\\" THUMBNAIL_PROVIDER_CLSID | |
#define THUMBNAIL_PROVIDER_APPID_KEY_PATH L"Software\\Classes\\AppID\\" THUMBNAIL_PROVIDER_CLSID | |
static inline DWORD InlineStringLenW(const wchar_t* str) | |
{ | |
DWORD len = 0; | |
while (*str++) ++len; | |
return len; | |
} | |
/// Register the COM server and file associations under HKEY_CURRENT_USER. | |
STDAPI DllRegisterServer(void) | |
{ | |
wchar_t modulePath[MAX_PATH] = {}; | |
if (!GetModuleFileNameW(g_moduleHandle, | |
modulePath, | |
_countof(modulePath))) | |
{ | |
return HRESULT_FROM_WIN32(GetLastError()); | |
} | |
// Register CLSID key. | |
HKEY hClsidKey = nullptr; | |
if (RegCreateKeyExW(HKEY_CURRENT_USER, | |
THUMBNAIL_PROVIDER_CLSID_KEY_PATH, | |
0, nullptr, | |
REG_OPTION_NON_VOLATILE, | |
KEY_WRITE, | |
nullptr, | |
&hClsidKey, | |
nullptr) != ERROR_SUCCESS) | |
{ | |
return E_FAIL; | |
} | |
// Set thumbnail provider title. | |
RegSetValueExW(hClsidKey, | |
nullptr, | |
0, | |
REG_SZ, | |
reinterpret_cast<const BYTE*>(THUMBNAIL_PROVIDER_TITLE), | |
static_cast<DWORD>(sizeof(THUMBNAIL_PROVIDER_TITLE))); | |
// InProcServer32 subkey. | |
HKEY hInproc = nullptr; | |
if (RegCreateKeyExW(hClsidKey, | |
L"InProcServer32", | |
0, nullptr, | |
REG_OPTION_NON_VOLATILE, | |
KEY_WRITE, | |
nullptr, | |
&hInproc, | |
nullptr) == ERROR_SUCCESS) | |
{ | |
RegSetValueExW(hInproc, | |
nullptr, | |
0, | |
REG_SZ, | |
reinterpret_cast<const BYTE*>(modulePath), | |
static_cast<DWORD>( | |
(InlineStringLenW(modulePath) + 1) * (DWORD)sizeof(wchar_t) | |
)); | |
RegSetValueExW(hInproc, | |
L"ThreadingModel", | |
0, | |
REG_SZ, | |
reinterpret_cast<const BYTE*>(L"Apartment"), | |
static_cast<DWORD>(sizeof(L"Apartment"))); | |
RegCloseKey(hInproc); | |
} | |
#ifdef _DEBUG | |
// Register surrogate for debugging (dllhost.exe) | |
RegSetValueExW(hClsidKey, | |
L"AppID", | |
0, | |
REG_SZ, | |
reinterpret_cast<const BYTE*>(THUMBNAIL_PROVIDER_CLSID), | |
static_cast<DWORD>(sizeof(THUMBNAIL_PROVIDER_CLSID))); | |
HKEY hAppId = nullptr; | |
if (RegCreateKeyExW(HKEY_CURRENT_USER, | |
THUMBNAIL_PROVIDER_APPID_KEY_PATH, | |
0, nullptr, | |
REG_OPTION_NON_VOLATILE, | |
KEY_WRITE, | |
nullptr, | |
&hAppId, | |
nullptr) == ERROR_SUCCESS) | |
{ | |
// Empty DllSurrogate: use dllhost.exe | |
RegSetValueExW(hAppId, | |
L"DllSurrogate", | |
0, | |
REG_SZ, | |
reinterpret_cast<const BYTE*>(L""), | |
sizeof(wchar_t)); | |
RegCloseKey(hAppId); | |
} | |
#endif | |
RegCloseKey(hClsidKey); | |
// Register each extension under HKCU\Software\Classes. | |
for (const wchar_t* extKeyPath : g_extensionKeyPaths) | |
{ | |
HKEY hExtKey = nullptr; | |
if (RegCreateKeyExW(HKEY_CURRENT_USER, | |
extKeyPath, | |
0, nullptr, | |
REG_OPTION_NON_VOLATILE, | |
KEY_WRITE, | |
nullptr, | |
&hExtKey, | |
nullptr) == ERROR_SUCCESS) | |
{ | |
RegSetValueExW(hExtKey, | |
nullptr, | |
0, | |
REG_SZ, | |
reinterpret_cast<const BYTE*>(THUMBNAIL_PROVIDER_CLSID), | |
static_cast<DWORD>(sizeof(THUMBNAIL_PROVIDER_CLSID))); | |
RegCloseKey(hExtKey); | |
} | |
} | |
// Notify shell of association change. | |
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); | |
return S_OK; | |
} | |
/// Unregister the COM server and file associations under HKEY_CURRENT_USER. | |
STDAPI DllUnregisterServer(void) | |
{ | |
// TODO: Also consider deleting the file extension keys if | |
// they are not being used by any other program, to avoid | |
// polluting the user's system registry. | |
// Remove extension associations. | |
for (const wchar_t* extKeyPath : g_extensionKeyPaths) | |
{ | |
RegDeleteTreeW(HKEY_CURRENT_USER, extKeyPath); | |
} | |
// Remove CLSID tree. | |
RegDeleteTreeW(HKEY_CURRENT_USER, THUMBNAIL_PROVIDER_CLSID_KEY_PATH); | |
#ifdef _DEBUG | |
// Remove AppID surrogate. | |
RegDeleteTreeW(HKEY_CURRENT_USER, THUMBNAIL_PROVIDER_APPID_KEY_PATH); | |
#endif | |
// Notify shell. | |
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr); | |
return S_OK; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment