Skip to content

Commit

Permalink
kram - ktx/ktx2/dds thumbnail provider for WIndows using libkram
Browse files Browse the repository at this point in the history
This uses libkram to decode and generate the thumbnail just like kramv does.  Windows only has 32, 96, 256, 1024 thumbnails.  Some of the smaller images in src/text the thumbnails are too small.  So need to find out how to make those bigger.  Windows nevery upscales, but in this case the thumbnails are pretty useless super tiny.  This thumbnail provider mechansim started in Win7, and has seen little in the way of updates from Microsoft.  A small update occurred in Vista.

This is based on the MIT license source from iOrange here https://github.com/iOrange/QOIThumbnailProvider.  Their code was easy enough to grok and modify.  The Microsoft Recipe sample thumbnail provider just seemed complex and broken with way too much code.

To install, do the following:
regsvr32.exe kram-thumb-win.dll

To uninstall, do the following:
regsvr32.exe /u kram-thumb-win.dll
  • Loading branch information
alecazam committed May 4, 2023
1 parent cee330f commit cda3c34
Show file tree
Hide file tree
Showing 11 changed files with 712 additions and 6 deletions.
14 changes: 10 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ if (BUILD_MAC)
add_subdirectory(plugin)
endif()

# hack hlslparser for win build into kram for now
# this is an Explorer thumbnail extension (run script to un/register), uses libkrma
if (BUILD_WIN)
add_subdirectory(kram-thumb-win)
endif()

# hack hlslparser for win build into kram for now, does not use kram
if (BUILD_WIN)
add_subdirectory(hlslparser)
endif()
Expand Down Expand Up @@ -150,14 +155,15 @@ if (BUILD_MAC)
install(TARGETS kramv BUNDLE DESTINATION ${BIN_DIR})
endif()

if (BUILD_WIN)
install(TARGETS kram-thumb-win LIBRARY DESTINATION ${BIN_DIR})
endif()

# don't install this yet
#if (BUILD_MAC)
# install(TARGETS kram-ps BUNDLE DESTINATION ${BIN_DIR})
#endif()

#-----------------------------------------------------


# hlslparser is also now in the kram build. Keep executables up to date.
# I would use the sln file, but msbuild doesn't like to be called from cibuld.sh
if (BUILD_WIN)
Expand Down
37 changes: 37 additions & 0 deletions kram-thumb-win/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

# dll output can be renamed for debug vs. release, but is hard to debug
set(myTargetLib kram-thumb-win)

# caller already set all this
# project(${myTargetLib} LANGUAGES CXX)

set(CMAKE_STATIC_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /NODEFAULTLIB:LIBCMT")
set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} /DEF:${CMAKE_CURRENT_SOURCE_DIR}/KramThumbProvider.def /NODEFAULTLIB:LIBCMT")

set(SOURCE_FILES
Dll.cpp
Dll.rc
KramThumbProvider.cpp
)

# Module is a DLL library
add_library(${myTargetLib} MODULE ${SOURCE_FILES})

# to turn off exceptions/rtti use /GR and /EHsc replacement
string(REGEX REPLACE "/GR" "/GR-" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
string(REGEX REPLACE "/EHsc" "/EHs-c-" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")

# all warnings, AVX1, and multiprocess compiles
target_compile_options(${myTargetLib} PRIVATE /W3 /arch:AVX -mf16c /MP /GF /FC)

target_compile_definitions(${myTargetLib} PRIVATE -D_ITERATOR_DEBUG_LEVEL=0 -D_HAS_EXCEPTIONS=0 -DUNICODE -D_UNICODE)

if (CMAKE_BUILD_TYPE EQUAL "Debug")
target_compile_definitions(${myTargetLib} PRIVATE "/INCREMENTAL")

elseif (CMAKE_BUILD_TYPE EQUAL "Release")
# only dead strip on Release builds since this disables Incremental linking, may want Profile build that doesn't use this
target_compile_definitions(${myTargetLib} PRIVATE "/OPT:REF")
endif()

target_link_libraries(${myTargetLib} PRIVATE shlwapi.lib libkram)
232 changes: 232 additions & 0 deletions kram-thumb-win/Dll.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// based on QOI Thumbnail Provider for Windows Explorer
// Written by iOrange in 2021
//
// Based on Microsoft's example
// https://github.com/microsoft/windows-classic-samples/tree/main/Samples/Win7Samples/winui/shell/appshellintegration/RecipeThumbnailProvider
//
// Also more info here:
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/cc144118(v=vs.85)

#include <objbase.h>
#include <shlwapi.h>
#include <thumbcache.h> // For IThumbnailProvider.
#include <shlobj.h> // For SHChangeNotify
#include <new>
#include <atomic>
#include <vector> // For std::size

// from KramThumbProvider.cpp
extern HRESULT KramThumbProvider_CreateInstance(REFIID riid, void** ppv);

#define SZ_KramTHUMBHANDLER L"Kram Thumbnail Handler"

// TODO: update CLSID here, is this a fixed id since Win7 vs. Vista said a provider is different ids?
// keepd in sync with kCLSID
// Just a different way of expressing CLSID in the two values below
// made with uuidgen.exe
#define SZ_CLSID_KramTHUMBHANDLER L"{a9a47ef5-c238-42a9-a4e6-a85558811dac}"
constexpr CLSID kCLSID_KramThumbHandler = {0xa9a47ef5, 0xc238, 0x42a9, {0xa4, 0xe6, 0xa8, 0x55, 0x58, 0x81, 0x1d, 0xac}};


typedef HRESULT(*PFNCREATEINSTANCE)(REFIID riid, void** ppvObject);
struct CLASS_OBJECT_INIT {
const CLSID* pClsid;
PFNCREATEINSTANCE pfnCreate;
};

// add classes supported by this module here
constexpr CLASS_OBJECT_INIT kClassObjectInit[] = {
{ &kCLSID_KramThumbHandler, KramThumbProvider_CreateInstance }
};


std::atomic_long gModuleReferences(0);
HINSTANCE gModuleInstance = nullptr;

// Standard DLL functions
STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void*) {
if (DLL_PROCESS_ATTACH == dwReason) {
gModuleInstance = hInstance;
::DisableThreadLibraryCalls(hInstance);
} else if (DLL_PROCESS_DETACH == dwReason) {
gModuleInstance = nullptr;
}
return TRUE;
}

STDAPI DllCanUnloadNow() {
// Only allow the DLL to be unloaded after all outstanding references have been released
return (gModuleReferences > 0) ? S_FALSE : S_OK;
}

void DllAddRef() {
++gModuleReferences;
}

void DllRelease() {
--gModuleReferences;
}

class CClassFactory : public IClassFactory {
public:
static HRESULT CreateInstance(REFCLSID clsid, const CLASS_OBJECT_INIT* pClassObjectInits, size_t cClassObjectInits, REFIID riid, void** ppv) {
*ppv = NULL;
HRESULT hr = CLASS_E_CLASSNOTAVAILABLE;
for (size_t i = 0; i < cClassObjectInits; ++i) {
if (clsid == *pClassObjectInits[i].pClsid) {
IClassFactory* pClassFactory = new (std::nothrow) CClassFactory(pClassObjectInits[i].pfnCreate);
hr = pClassFactory ? S_OK : E_OUTOFMEMORY;
if (SUCCEEDED(hr)) {
hr = pClassFactory->QueryInterface(riid, ppv);
pClassFactory->Release();
}
break; // match found
}
}
return hr;
}

CClassFactory(PFNCREATEINSTANCE pfnCreate)
: mReferences(1)
, mCreateFunc(pfnCreate) {
DllAddRef();
}

// IUnknown
IFACEMETHODIMP QueryInterface(REFIID riid, void** ppv) {
static const QITAB qit[] = {
QITABENT(CClassFactory, IClassFactory),
{ 0 }
};
return QISearch(this, qit, riid, ppv);
}

IFACEMETHODIMP_(ULONG) AddRef() {
return ++mReferences;
}

IFACEMETHODIMP_(ULONG) Release() {
const long refs = --mReferences;
if (!refs) {
delete this;
}
return refs;
}

// IClassFactory
IFACEMETHODIMP CreateInstance(IUnknown* punkOuter, REFIID riid, void** ppv) {
return punkOuter ? CLASS_E_NOAGGREGATION : mCreateFunc(riid, ppv);
}

IFACEMETHODIMP LockServer(BOOL fLock) {
if (fLock) {
DllAddRef();
} else {
DllRelease();
}
return S_OK;
}

private:
~CClassFactory() {
DllRelease();
}

std::atomic_long mReferences;
PFNCREATEINSTANCE mCreateFunc;
};

STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void** ppv) {
return CClassFactory::CreateInstance(clsid, kClassObjectInit, std::size(kClassObjectInit), riid, ppv);
}

// A struct to hold the information required for a registry entry
struct REGISTRY_ENTRY {
HKEY hkeyRoot;
PCWSTR pszKeyName;
PCWSTR pszValueName;
PCWSTR pszData;
};

// Creates a registry key (if needed) and sets the default value of the key
HRESULT CreateRegKeyAndSetValue(const REGISTRY_ENTRY* pRegistryEntry) {
HKEY hKey;
HRESULT hr = HRESULT_FROM_WIN32(RegCreateKeyExW(pRegistryEntry->hkeyRoot,
pRegistryEntry->pszKeyName,
0, nullptr, REG_OPTION_NON_VOLATILE,
KEY_SET_VALUE | KEY_WOW64_64KEY,
nullptr, &hKey, nullptr));
if (SUCCEEDED(hr)) {
hr = HRESULT_FROM_WIN32(RegSetValueExW(hKey, pRegistryEntry->pszValueName, 0, REG_SZ,
reinterpret_cast<const BYTE*>(pRegistryEntry->pszData),
static_cast<DWORD>(wcslen(pRegistryEntry->pszData) + 1) * sizeof(WCHAR)));
RegCloseKey(hKey);
}
return hr;
}

// Registers this COM server
STDAPI DllRegisterServer() {
HRESULT hr;
WCHAR szModuleName[MAX_PATH] = { 0 };

if (!GetModuleFileNameW(gModuleInstance, szModuleName, ARRAYSIZE(szModuleName))) {
hr = HRESULT_FROM_WIN32(GetLastError());
} else {
// List of registry entries we want to create
const REGISTRY_ENTRY registryEntries[] = {
// RootKey KeyName ValueName Data
{HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" SZ_CLSID_KramTHUMBHANDLER, nullptr, SZ_KramTHUMBHANDLER},
{HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" SZ_CLSID_KramTHUMBHANDLER L"\\InProcServer32", nullptr, szModuleName},
{HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" SZ_CLSID_KramTHUMBHANDLER L"\\InProcServer32", L"ThreadingModel", L"Apartment"},

// libkram can decode any of these and create a thumbnail
// The Vista GUID for the thumbnail handler Shell extension is E357FCCD-A995-4576-B01F-234630154E96.
{HKEY_CURRENT_USER, L"Software\\Classes\\.ktx", L"PerceivedType", L"image"},
{HKEY_CURRENT_USER, L"Software\\Classes\\.ktx\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}", nullptr, SZ_CLSID_KramTHUMBHANDLER},
{HKEY_CURRENT_USER, L"Software\\Classes\\.ktx2", L"PerceivedType", L"image"},
{HKEY_CURRENT_USER, L"Software\\Classes\\.ktx2\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}", nullptr, SZ_CLSID_KramTHUMBHANDLER},
{HKEY_CURRENT_USER, L"Software\\Classes\\.dds", L"PerceivedType", L"image"},
{HKEY_CURRENT_USER, L"Software\\Classes\\.dds\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}", nullptr, SZ_CLSID_KramTHUMBHANDLER},
//{HKEY_CURRENT_USER, L"Software\\Classes\\.png", L"PerceivedType", L"image"},
//{HKEY_CURRENT_USER, L"Software\\Classes\\.png\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}", nullptr, SZ_CLSID_KramTHUMBHANDLER},
};

hr = S_OK;
for (size_t i = 0; i < std::size(registryEntries) && SUCCEEDED(hr); ++i) {
hr = CreateRegKeyAndSetValue(&registryEntries[i]);
}
}

if (SUCCEEDED(hr)) {
// This tells the shell to invalidate the thumbnail cache. This is important because any .qoi files
// viewed before registering this handler would otherwise show cached blank thumbnails.
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
}

return hr;
}

// Unregisters this COM server
STDAPI DllUnregisterServer() {
HRESULT hr = S_OK;

const PCWSTR regKeys[] = {
L"Software\\Classes\\CLSID\\" SZ_CLSID_KramTHUMBHANDLER,
L"Software\\Classes\\.ktx",
L"Software\\Classes\\.ktx2",
L"Software\\Classes\\.dds",
// L"Software\\Classes\\.png", // only need this if Win png bg is bad
};

// Delete the registry entries
for (size_t i = 0; i < std::size(regKeys) && SUCCEEDED(hr); ++i) {
hr = HRESULT_FROM_WIN32(RegDeleteTreeW(HKEY_CURRENT_USER, regKeys[i]));
if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) {
// If the registry entry has already been deleted, say S_OK.
hr = S_OK;
}
}

return hr;
}
38 changes: 38 additions & 0 deletions kram-thumb-win/Dll.rc
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#include <winres.h>

#ifndef DEBUG
#define VER_DEBUG 0
#else
#define VER_DEBUG VS_FF_DEBUG
#endif

VS_VERSION_INFO VERSIONINFO
FILEVERSION 0,0,2,0
PRODUCTVERSION 0,0,2,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS VER_DEBUG
FILEOS VOS__WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "CompanyName", "ba"
VALUE "FileDescription", "Kram Thumbnail Provider"
VALUE "FileVersion", "0.0.2.0"
VALUE "InternalName", "KramThumbProvider.dll"
VALUE "LegalCopyright", "2023, Alec Miller"
VALUE "LegalTrademarks1", ""
VALUE "LegalTrademarks2", ""
VALUE "OriginalFilename", "KramThumbProvider.dll"
VALUE "ProductName", "KramThumbProvider"
VALUE "ProductVersion", "0, 0, 2, 0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END
Loading

0 comments on commit cda3c34

Please sign in to comment.