From ef9269bd62f31e39d39cc59cd34354b53eae6661 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Wed, 20 Oct 2021 10:16:36 +1100 Subject: Thumbnails: refactor extraction to use one code-path for all platforms Thumbnail extraction now shares code between Linux/Windows, allowing thumbnails from Zstd compressed blend files to be extracted. The main logic is placed in blendthumb_extract.cc and is built as static library. For windows there is DLL which is registered during blender install and which then reads and generates thumbnails. For other platforms there is blender-thumbnailer executable file which takes blend file as an input and generates PNG file. As a result Python script blender-thumbnailer.py is no longer needed. The thumbnail extractor shares the same code-path as Blenders file reading, so there is no need to duplicate any file reading logic. This means reading compressed blend files is supported (broken since the recent move Zstd compression - D5799). This resolves T63736. Contributors: - @alausic original patch. - @LazyDodo windows fixes/support. - @campbellbarton general fixes/update. - @lukasstockner97 Zstd support. Reviewed By: sybren, mont29, LazyDodo, campbellbarton Ref D6408 --- source/blender/blendthumb/src/BlendThumb.def | 5 - source/blender/blendthumb/src/BlendThumb.rc | 26 -- source/blender/blendthumb/src/BlenderThumb.cpp | 321 --------------------- source/blender/blendthumb/src/Dll.cpp | 293 ------------------- .../blender/blendthumb/src/blender_thumbnailer.cc | 113 ++++++++ source/blender/blendthumb/src/blendthumb.hh | 65 +++++ .../blender/blendthumb/src/blendthumb_extract.cc | 257 +++++++++++++++++ source/blender/blendthumb/src/blendthumb_png.cc | 158 ++++++++++ source/blender/blendthumb/src/blendthumb_win32.cc | 237 +++++++++++++++ source/blender/blendthumb/src/blendthumb_win32.def | 5 + source/blender/blendthumb/src/blendthumb_win32.rc | 26 ++ .../blender/blendthumb/src/blendthumb_win32_dll.cc | 293 +++++++++++++++++++ 12 files changed, 1154 insertions(+), 645 deletions(-) delete mode 100644 source/blender/blendthumb/src/BlendThumb.def delete mode 100644 source/blender/blendthumb/src/BlendThumb.rc delete mode 100644 source/blender/blendthumb/src/BlenderThumb.cpp delete mode 100644 source/blender/blendthumb/src/Dll.cpp create mode 100644 source/blender/blendthumb/src/blender_thumbnailer.cc create mode 100644 source/blender/blendthumb/src/blendthumb.hh create mode 100644 source/blender/blendthumb/src/blendthumb_extract.cc create mode 100644 source/blender/blendthumb/src/blendthumb_png.cc create mode 100644 source/blender/blendthumb/src/blendthumb_win32.cc create mode 100644 source/blender/blendthumb/src/blendthumb_win32.def create mode 100644 source/blender/blendthumb/src/blendthumb_win32.rc create mode 100644 source/blender/blendthumb/src/blendthumb_win32_dll.cc (limited to 'source/blender/blendthumb/src') diff --git a/source/blender/blendthumb/src/BlendThumb.def b/source/blender/blendthumb/src/BlendThumb.def deleted file mode 100644 index 71f9236735f..00000000000 --- a/source/blender/blendthumb/src/BlendThumb.def +++ /dev/null @@ -1,5 +0,0 @@ -EXPORTS - DllGetClassObject PRIVATE - DllCanUnloadNow PRIVATE - DllRegisterServer PRIVATE - DllUnregisterServer PRIVATE \ No newline at end of file diff --git a/source/blender/blendthumb/src/BlendThumb.rc b/source/blender/blendthumb/src/BlendThumb.rc deleted file mode 100644 index 5dfd416b0c5..00000000000 --- a/source/blender/blendthumb/src/BlendThumb.rc +++ /dev/null @@ -1,26 +0,0 @@ -#define IDR_VERSION1 1 - -IDR_VERSION1 VERSIONINFO -FILEVERSION 1,4,0,0 -PRODUCTVERSION 2,78,0,0 -FILEOS 0x00000004 -FILETYPE 0x00000002 -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "FFFF04B0" - BEGIN - VALUE "FileVersion", "1.4\0" - VALUE "ProductVersion", "2.78\0" - VALUE "FileDescription", "Blender Thumbnail Handler\0" - VALUE "OriginalFilename", "BlendThumb.dll\0" - VALUE "ProductName", "Blender\0" - VALUE "LegalCopyright", "GPL2, 2016\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x0409, 0x04B0 - END -END - diff --git a/source/blender/blendthumb/src/BlenderThumb.cpp b/source/blender/blendthumb/src/BlenderThumb.cpp deleted file mode 100644 index 939e7bbf67c..00000000000 --- a/source/blender/blendthumb/src/BlenderThumb.cpp +++ /dev/null @@ -1,321 +0,0 @@ -/* - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -#include -#include -#include // For IThumbnailProvider. - -#pragma comment(lib, "shlwapi.lib") - -// this thumbnail provider implements IInitializeWithStream to enable being hosted -// in an isolated process for robustness - -class CBlendThumb : public IInitializeWithStream, public IThumbnailProvider { - public: - CBlendThumb() : _cRef(1), _pStream(NULL) - { - } - - virtual ~CBlendThumb() - { - if (_pStream) { - _pStream->Release(); - } - } - - // IUnknown - IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv) - { - static const QITAB qit[] = { - QITABENT(CBlendThumb, IInitializeWithStream), - QITABENT(CBlendThumb, IThumbnailProvider), - {0}, - }; - return QISearch(this, qit, riid, ppv); - } - - IFACEMETHODIMP_(ULONG) AddRef() - { - return InterlockedIncrement(&_cRef); - } - - IFACEMETHODIMP_(ULONG) Release() - { - ULONG cRef = InterlockedDecrement(&_cRef); - if (!cRef) { - delete this; - } - return cRef; - } - - // IInitializeWithStream - IFACEMETHODIMP Initialize(IStream *pStream, DWORD grfMode); - - // IThumbnailProvider - IFACEMETHODIMP GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha); - - private: - long _cRef; - IStream *_pStream; // provided during initialization. -}; - -HRESULT CBlendThumb_CreateInstance(REFIID riid, void **ppv) -{ - CBlendThumb *pNew = new (std::nothrow) CBlendThumb(); - HRESULT hr = pNew ? S_OK : E_OUTOFMEMORY; - if (SUCCEEDED(hr)) { - hr = pNew->QueryInterface(riid, ppv); - pNew->Release(); - } - return hr; -} - -// IInitializeWithStream -IFACEMETHODIMP CBlendThumb::Initialize(IStream *pStream, DWORD) -{ - HRESULT hr = E_UNEXPECTED; // can only be inited once - if (_pStream == NULL) { - // take a reference to the stream if we have not been inited yet - hr = pStream->QueryInterface(&_pStream); - } - return hr; -} - -#include "Wincodec.h" -#include -#include -const unsigned char gzip_magic[3] = {0x1f, 0x8b, 0x08}; - -// IThumbnailProvider -IFACEMETHODIMP CBlendThumb::GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha) -{ - ULONG BytesRead; - HRESULT hr = S_FALSE; - LARGE_INTEGER SeekPos; - - // Compressed? - unsigned char in_magic[3]; - _pStream->Read(&in_magic, 3, &BytesRead); - bool gzipped = true; - for (int i = 0; i < 3; i++) - if (in_magic[i] != gzip_magic[i]) { - gzipped = false; - break; - } - - if (gzipped) { - // Zlib inflate - z_stream stream; - stream.zalloc = Z_NULL; - stream.zfree = Z_NULL; - stream.opaque = Z_NULL; - - // Get compressed file length - SeekPos.QuadPart = 0; - _pStream->Seek(SeekPos, STREAM_SEEK_END, NULL); - - // Get compressed and uncompressed size - uLong source_size; - uLongf dest_size; - // SeekPos.QuadPart = -4; // last 4 bytes define size of uncompressed file - // ULARGE_INTEGER Tell; - //_pStream->Seek(SeekPos,STREAM_SEEK_END,&Tell); - // source_size = (uLong)Tell.QuadPart + 4; // src - //_pStream->Read(&dest_size,4,&BytesRead); // dest - dest_size = 1024 * 70; // thumbnail is currently always inside the first 65KB...if it moves or - // enlargens this line will have to change or go! - source_size = (uLong)max(SeekPos.QuadPart, dest_size); // for safety, assume no compression - - // Input - Bytef *src = new Bytef[source_size]; - stream.next_in = (Bytef *)src; - stream.avail_in = (uInt)source_size; - - // Output - Bytef *dest = new Bytef[dest_size]; - stream.next_out = (Bytef *)dest; - stream.avail_out = dest_size; - - // IStream to src - SeekPos.QuadPart = 0; - _pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL); - _pStream->Read(src, source_size, &BytesRead); - - // Do the inflation - int err; - err = inflateInit2(&stream, 16); // 16 means "gzip"...nice! - err = inflate(&stream, Z_FINISH); - err = inflateEnd(&stream); - - // Replace the IStream, which is read-only - _pStream->Release(); - _pStream = SHCreateMemStream(dest, dest_size); - - delete[] src; - delete[] dest; - } - - // Blender version, early out if sub 2.5 - SeekPos.QuadPart = 9; - _pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL); - char version[4]; - version[3] = '\0'; - _pStream->Read(&version, 3, &BytesRead); - if (BytesRead != 3) { - return E_UNEXPECTED; - } - int iVersion = atoi(version); - if (iVersion < 250) { - return S_FALSE; - } - - // 32 or 64 bit blend? - SeekPos.QuadPart = 7; - _pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL); - - char _PointerSize; - _pStream->Read(&_PointerSize, 1, &BytesRead); - - int PointerSize = _PointerSize == '_' ? 4 : 8; - int HeaderSize = 16 + PointerSize; - - // Find and read thumbnail ("TEST") block - SeekPos.QuadPart = 12; - _pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL); - int BlockOffset = 12; - while (_pStream) { - // Scan current block - char BlockName[5]; - BlockName[4] = '\0'; - int BlockSize = 0; - - if (_pStream->Read(BlockName, 4, &BytesRead) == S_OK && - _pStream->Read((void *)&BlockSize, 4, &BytesRead) == S_OK) { - if (strcmp(BlockName, "TEST") != 0) { - SeekPos.QuadPart = BlockOffset += HeaderSize + BlockSize; - _pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL); - continue; - } - } - else { - break; // eof - } - - // Found the block - SeekPos.QuadPart = BlockOffset + HeaderSize; - _pStream->Seek(SeekPos, STREAM_SEEK_SET, NULL); - - int width, height; - _pStream->Read((char *)&width, 4, &BytesRead); - _pStream->Read((char *)&height, 4, &BytesRead); - BlockSize -= 8; - - // Isolate RGBA data - char *pRGBA = new char[BlockSize]; - _pStream->Read(pRGBA, BlockSize, &BytesRead); - - if (BytesRead != (ULONG)BlockSize) { - return E_UNEXPECTED; - } - - // Convert to BGRA for Windows - for (int i = 0; i < BlockSize; i += 4) { -#define RED_BYTE pRGBA[i] -#define BLUE_BYTE pRGBA[i + 2] - - char red = RED_BYTE; - RED_BYTE = BLUE_BYTE; - BLUE_BYTE = red; - } - - // Flip vertically (Blender stores it upside-down) - unsigned int LineSize = width * 4; - char *FlippedImage = new char[BlockSize]; - for (int i = 0; i < height; i++) { - if (0 != memcpy_s(&FlippedImage[(height - i - 1) * LineSize], - LineSize, - &pRGBA[i * LineSize], - LineSize)) { - return E_UNEXPECTED; - } - } - delete[] pRGBA; - pRGBA = FlippedImage; - - // Create image - *phbmp = CreateBitmap(width, height, 1, 32, pRGBA); - if (!*phbmp) { - return E_FAIL; - } - *pdwAlpha = WTSAT_ARGB; // it's actually BGRA, not sure why this works - - // Scale down if required - if ((unsigned)width > cx || (unsigned)height > cx) { - float scale = 1.0f / (max(width, height) / (float)cx); - LONG NewWidth = (LONG)(width * scale); - LONG NewHeight = (LONG)(height * scale); - -#ifdef _DEBUG -# if 0 - MessageBox(0, "Attach now", "Debugging", MB_OK); -# endif -#endif - IWICImagingFactory *pImgFac; - hr = CoCreateInstance( - CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pImgFac)); - - IWICBitmap *WICBmp; - hr = pImgFac->CreateBitmapFromHBITMAP(*phbmp, 0, WICBitmapUseAlpha, &WICBmp); - - BITMAPINFO bmi = {}; - bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader); - bmi.bmiHeader.biWidth = NewWidth; - bmi.bmiHeader.biHeight = -NewHeight; - bmi.bmiHeader.biPlanes = 1; - bmi.bmiHeader.biBitCount = 32; - bmi.bmiHeader.biCompression = BI_RGB; - - BYTE *pBits; - HBITMAP ResizedHBmp = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, (void **)&pBits, NULL, 0); - hr = ResizedHBmp ? S_OK : E_OUTOFMEMORY; - if (SUCCEEDED(hr)) { - IWICBitmapScaler *pIScaler; - hr = pImgFac->CreateBitmapScaler(&pIScaler); - hr = pIScaler->Initialize(WICBmp, NewWidth, NewHeight, WICBitmapInterpolationModeFant); - - WICRect rect = {0, 0, NewWidth, NewHeight}; - hr = pIScaler->CopyPixels(&rect, NewWidth * 4, NewWidth * NewHeight * 4, pBits); - - if (SUCCEEDED(hr)) { - DeleteObject(*phbmp); - *phbmp = ResizedHBmp; - } - else { - DeleteObject(ResizedHBmp); - } - - pIScaler->Release(); - } - WICBmp->Release(); - pImgFac->Release(); - } - else { - hr = S_OK; - } - break; - } - return hr; -} diff --git a/source/blender/blendthumb/src/Dll.cpp b/source/blender/blendthumb/src/Dll.cpp deleted file mode 100644 index 7f10777f884..00000000000 --- a/source/blender/blendthumb/src/Dll.cpp +++ /dev/null @@ -1,293 +0,0 @@ -/* - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 2 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ - -/** \file - * \ingroup blendthumb - * - * Thumbnail from Blend file extraction for MS-Windows (DLL). - */ - -#include -#include -#include /* For #SHChangeNotify */ -#include -#include /* For IThumbnailProvider */ - -extern HRESULT CBlendThumb_CreateInstance(REFIID riid, void **ppv); - -#define SZ_CLSID_BLENDTHUMBHANDLER L"{D45F043D-F17F-4e8a-8435-70971D9FA46D}" -#define SZ_BLENDTHUMBHANDLER L"Blender Thumbnail Handler" -const CLSID CLSID_BlendThumbHandler = { - 0xd45f043d, 0xf17f, 0x4e8a, {0x84, 0x35, 0x70, 0x97, 0x1d, 0x9f, 0xa4, 0x6d}}; - -typedef HRESULT (*PFNCREATEINSTANCE)(REFIID riid, void **ppvObject); -struct CLASS_OBJECT_INIT { - const CLSID *pClsid; - PFNCREATEINSTANCE pfnCreate; -}; - -/* Add classes supported by this module here. */ -const CLASS_OBJECT_INIT c_rgClassObjectInit[] = { - {&CLSID_BlendThumbHandler, CBlendThumb_CreateInstance}}; - -long g_cRefModule = 0; - -/** Handle the DLL's module */ -HINSTANCE g_hInst = nullptr; - -/** Standard DLL functions. */ -STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *) -{ - if (dwReason == DLL_PROCESS_ATTACH) { - g_hInst = hInstance; - DisableThreadLibraryCalls(hInstance); - } - return TRUE; -} - -STDAPI DllCanUnloadNow() -{ - /* Only allow the DLL to be unloaded after all outstanding references have been released. */ - return (g_cRefModule == 0) ? S_OK : S_FALSE; -} - -void DllAddRef() -{ - InterlockedIncrement(&g_cRefModule); -} - -void DllRelease() -{ - InterlockedDecrement(&g_cRefModule); -} - -class CClassFactory : public IClassFactory { - public: - static HRESULT CreateInstance(REFCLSID clsid, - const CLASS_OBJECT_INIT *pClassObjectInits, - size_t cClassObjectInits, - REFIID riid, - void **ppv) - { - *ppv = nullptr; - 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(); - } - /* Match found. */ - break; - } - } - return hr; - } - - CClassFactory(PFNCREATEINSTANCE pfnCreate) : _cRef(1), _pfnCreate(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 InterlockedIncrement(&_cRef); - } - - IFACEMETHODIMP_(ULONG) Release() - { - long cRef = InterlockedDecrement(&_cRef); - if (cRef == 0) { - delete this; - } - return cRef; - } - - /** #IClassFactory */ - IFACEMETHODIMP CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv) - { - return punkOuter ? CLASS_E_NOAGGREGATION : _pfnCreate(riid, ppv); - } - - IFACEMETHODIMP LockServer(BOOL fLock) - { - if (fLock) { - DllAddRef(); - } - else { - DllRelease(); - } - return S_OK; - } - - private: - ~CClassFactory() - { - DllRelease(); - } - - long _cRef; - PFNCREATEINSTANCE _pfnCreate; -}; - -STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv) -{ - return CClassFactory::CreateInstance( - clsid, c_rgClassObjectInit, ARRAYSIZE(c_rgClassObjectInit), riid, ppv); -} - -/** - * A struct to hold the information required for a registry entry. - */ -struct REGISTRY_ENTRY { - HKEY hkeyRoot; - PCWSTR pszKeyName; - PCWSTR pszValueName; - DWORD dwValueType; - /** These two fields could/should have been a union, but C++ */ - PCWSTR pszData; - /** Only lets you initialize the first field in a union. */ - DWORD dwData; -}; - -/** - * 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, - nullptr, - &hKey, - nullptr)); - if (SUCCEEDED(hr)) { - /* All this just to support #REG_DWORD. */ - DWORD size; - DWORD data; - BYTE *lpData = (LPBYTE)pRegistryEntry->pszData; - switch (pRegistryEntry->dwValueType) { - case REG_SZ: - size = ((DWORD)wcslen(pRegistryEntry->pszData) + 1) * sizeof(WCHAR); - break; - case REG_DWORD: - size = sizeof(DWORD); - data = pRegistryEntry->dwData; - lpData = (BYTE *)&data; - break; - default: - return E_INVALIDARG; - } - - hr = HRESULT_FROM_WIN32(RegSetValueExW( - hKey, pRegistryEntry->pszValueName, 0, pRegistryEntry->dwValueType, lpData, size)); - RegCloseKey(hKey); - } - return hr; -} - -/** - * Registers this COM server. - */ -STDAPI DllRegisterServer() -{ - HRESULT hr; - - WCHAR szModuleName[MAX_PATH]; - - if (!GetModuleFileNameW(g_hInst, szModuleName, ARRAYSIZE(szModuleName))) { - hr = HRESULT_FROM_WIN32(GetLastError()); - } - else { - const REGISTRY_ENTRY rgRegistryEntries[] = { - /* `RootKey KeyName ValueName ValueType Data` */ - {HKEY_CURRENT_USER, - L"Software\\Classes\\CLSID\\" SZ_CLSID_BLENDTHUMBHANDLER, - nullptr, - REG_SZ, - SZ_BLENDTHUMBHANDLER}, - {HKEY_CURRENT_USER, - L"Software\\Classes\\CLSID\\" SZ_CLSID_BLENDTHUMBHANDLER L"\\InProcServer32", - nullptr, - REG_SZ, - szModuleName}, - {HKEY_CURRENT_USER, - L"Software\\Classes\\CLSID\\" SZ_CLSID_BLENDTHUMBHANDLER L"\\InProcServer32", - L"ThreadingModel", - REG_SZ, - L"Apartment"}, - {HKEY_CURRENT_USER, - L"Software\\Classes\\.blend\\", - L"Treatment", - REG_DWORD, - 0, - 0}, /* This doesn't appear to do anything. */ - {HKEY_CURRENT_USER, - L"Software\\Classes\\.blend\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}", - nullptr, - REG_SZ, - SZ_CLSID_BLENDTHUMBHANDLER}, - }; - - hr = S_OK; - for (int i = 0; i < ARRAYSIZE(rgRegistryEntries) && SUCCEEDED(hr); i++) { - hr = CreateRegKeyAndSetValue(&rgRegistryEntries[i]); - } - } - if (SUCCEEDED(hr)) { - /* This tells the shell to invalidate the thumbnail cache. - * This is important because any `.blend` 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 rgpszKeys[] = { - L"Software\\Classes\\CLSID\\" SZ_CLSID_BLENDTHUMBHANDLER, - L"Software\\Classes\\.blend\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}"}; - - /* Delete the registry entries. */ - for (int i = 0; i < ARRAYSIZE(rgpszKeys) && SUCCEEDED(hr); i++) { - hr = HRESULT_FROM_WIN32(RegDeleteTreeW(HKEY_CURRENT_USER, rgpszKeys[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; -} diff --git a/source/blender/blendthumb/src/blender_thumbnailer.cc b/source/blender/blendthumb/src/blender_thumbnailer.cc new file mode 100644 index 00000000000..8dd9d5c0c0a --- /dev/null +++ b/source/blender/blendthumb/src/blender_thumbnailer.cc @@ -0,0 +1,113 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup blendthumb + * + * This file defines the thumbnail generation command (typically used on UNIX). + * + * To run automatically with a file manager such as Nautilus, save this file + * in a directory that is listed in PATH environment variable, and create + * `blender.thumbnailer` file in `${HOME}/.local/share/thumbnailers/` directory + * with the following contents: + * + * \code{.txt} + * [Thumbnailer Entry] + * TryExec=blender-thumbnailer + * Exec=blender-thumbnailer %u %o + * MimeType=application/x-blender; + * \endcode + */ + +#include +#include + +#include +#ifndef WIN32 +# include /* For read close. */ +#else +# include "BLI_winstuff.h" +# include "winsock2.h" +# include /* For open close read. */ +#endif + +#include "BLI_fileops.h" +#include "BLI_filereader.h" +#include "BLI_vector.hh" + +#include "blendthumb.hh" + +/** + * This function opens .blend file from src_blend, extracts thumbnail from file if there is one, + * and writes `.png` image into `dst_png`. + * Returns exit code (0 if successful). + */ +static eThumbStatus extract_png_from_blend_file(const char *src_blend, const char *dst_png) +{ + eThumbStatus err; + + /* Open source file `src_blend`. */ + const int src_file = BLI_open(src_blend, O_BINARY | O_RDONLY, 0); + if (src_file == -1) { + return BT_FILE_ERR; + } + + /* Thumbnail reading is responsible for freeing `file` and closing `src_file`. */ + FileReader *file = BLI_filereader_new_file(src_file); + if (file == nullptr) { + close(src_file); + return BT_FILE_ERR; + } + + /* Extract thumbnail from file. */ + Thumbnail thumb; + err = blendthumb_create_thumb_from_file(file, &thumb); + if (err != BT_OK) { + return err; + } + + /* Write thumbnail to `dst_png`. */ + const int dst_file = BLI_open(dst_png, O_BINARY | O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (dst_file == -1) { + return BT_FILE_ERR; + } + + std::optional> png_buf_opt = blendthumb_create_png_data_from_thumb( + &thumb); + if (png_buf_opt == std::nullopt) { + err = BT_ERROR; + } + else { + blender::Vector png_buf = *png_buf_opt; + err = (write(dst_file, png_buf.data(), png_buf.size()) == png_buf.size()) ? BT_OK : + BT_FILE_ERR; + } + close(dst_file); + + return err; +} + +int main(int argc, char *argv[]) +{ + if (argc < 3) { + std::cerr << "Usage: blender-thumbnailer " << std::endl; + return -1; + } + + eThumbStatus ret = extract_png_from_blend_file(argv[1], argv[2]); + + return (int)ret; +} diff --git a/source/blender/blendthumb/src/blendthumb.hh b/source/blender/blendthumb/src/blendthumb.hh new file mode 100644 index 00000000000..c029a1766d6 --- /dev/null +++ b/source/blender/blendthumb/src/blendthumb.hh @@ -0,0 +1,65 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2008-2021 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup blendthumb + * + * Shared thumbnail extraction logic. + * + * Used for both MS-Windows DLL and Unix command line. + */ + +#pragma once + +#include + +#include "BLI_array.hh" +#include "BLI_vector.hh" + +struct FileReader; + +struct Thumbnail { + blender::Array data; + int width; + int height; +}; + +enum eThumbStatus { + BT_OK = 0, + BT_FILE_ERR = 1, + BT_COMPRES_ERR = 2, + BT_DECOMPRESS_ERR = 3, + BT_INVALID_FILE = 4, + BT_EARLY_VERSION = 5, + BT_INVALID_THUMB = 6, + BT_ERROR = 9 +}; + +std::optional> blendthumb_create_png_data_from_thumb( + const Thumbnail *thumb); +eThumbStatus blendthumb_create_thumb_from_file(struct FileReader *rawfile, Thumbnail *thumb); + +/* INTEGER CODES */ +#ifdef __BIG_ENDIAN__ +/* Big Endian */ +# define MAKE_ID(a, b, c, d) ((int)(a) << 24 | (int)(b) << 16 | (c) << 8 | (d)) +#else +/* Little Endian */ +# define MAKE_ID(a, b, c, d) ((int)(d) << 24 | (int)(c) << 16 | (b) << 8 | (a)) +#endif diff --git a/source/blender/blendthumb/src/blendthumb_extract.cc b/source/blender/blendthumb/src/blendthumb_extract.cc new file mode 100644 index 00000000000..99b13c89994 --- /dev/null +++ b/source/blender/blendthumb/src/blendthumb_extract.cc @@ -0,0 +1,257 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2008 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup blendthumb + * + * Expose #blendthumb_create_thumb_from_file that creates the PNG data + * but does not write it to a file. + */ + +#include + +#include "BLI_alloca.h" +#include "BLI_endian_defines.h" +#include "BLI_endian_switch.h" +#include "BLI_fileops.h" +#include "BLI_filereader.h" +#include "BLI_string.h" + +#include "blendthumb.hh" + +static bool blend_header_check_magic(const char header[12]) +{ + /* Check magic string at start of file. */ + if (!BLI_str_startswith(header, "BLENDER")) { + return false; + } + /* Check pointer size and endianness indicators. */ + if (((header[7] != '_') && (header[7] != '-')) || ((header[8] != 'v') && (header[8] != 'V'))) { + return false; + } + /* Check version number. */ + if (!isdigit(header[9]) || !isdigit(header[10]) || !isdigit(header[11])) { + return false; + } + return true; +} + +static bool blend_header_is_version_valid(const char header[12]) +{ + /* Thumbnails are only in files with version >= 2.50 */ + char num[4]; + memcpy(num, header + 9, 3); + num[3] = 0; + return atoi(num) >= 250; +} + +static int blend_header_pointer_size(const char header[12]) +{ + return header[7] == '_' ? 4 : 8; +} + +static bool blend_header_is_endian_switch_needed(const char header[12]) +{ + return (((header[8] == 'v') ? L_ENDIAN : B_ENDIAN) != ENDIAN_ORDER); +} + +static void thumb_data_vertical_flip(Thumbnail *thumb) +{ + uint32_t *rect = (uint32_t *)thumb->data.data(); + int x = thumb->width, y = thumb->height; + uint32_t *top = rect; + uint32_t *bottom = top + ((y - 1) * x); + uint32_t *line = (uint32_t *)malloc(x * sizeof(uint32_t)); + + y >>= 1; + for (; y > 0; y--) { + memcpy(line, top, x * sizeof(uint32_t)); + memcpy(top, bottom, x * sizeof(uint32_t)); + memcpy(bottom, line, x * sizeof(uint32_t)); + bottom -= x; + top += x; + } + free(line); +} + +static int32_t bytes_to_native_i32(const uint8_t bytes[4], bool endian_switch) +{ + int32_t data; + memcpy(&data, bytes, 4); + if (endian_switch) { + BLI_endian_switch_int32(&data); + } + return data; +} + +static bool file_read(FileReader *file, uint8_t *buf, size_t buf_len) +{ + return (file->read(file, buf, buf_len) == buf_len); +} + +static bool file_seek(FileReader *file, size_t len) +{ + if (file->seek != nullptr) { + if (file->seek(file, len, SEEK_CUR) == -1) { + return false; + } + return true; + } + + /* File doesn't support seeking (e.g. gzip), so read and discard in chunks. */ + constexpr size_t dummy_data_size = 4096; + blender::Array dummy_data(dummy_data_size); + while (len > 0) { + const size_t len_chunk = std::min(len, dummy_data_size); + if ((size_t)file->read(file, dummy_data.data(), len_chunk) != len_chunk) { + return false; + } + len -= len_chunk; + } + return true; +} + +static eThumbStatus blendthumb_extract_from_file_impl(FileReader *file, + Thumbnail *thumb, + const size_t bhead_size, + const bool endian_switch) +{ + /* Iterate over file blocks until we find the thumbnail or run out of data. */ + uint8_t *bhead_data = (uint8_t *)BLI_array_alloca(bhead_data, bhead_size); + while (file_read(file, bhead_data, bhead_size)) { + /* Parse type and size from `BHead`. */ + const int32_t block_size = bytes_to_native_i32(&bhead_data[4], endian_switch); + + /* We're looking for the thumbnail, so skip any other block. */ + switch (*((int32_t *)bhead_data)) { + case MAKE_ID('T', 'E', 'S', 'T'): { + uint8_t shape[8]; + if (!file_read(file, shape, sizeof(shape))) { + return BT_INVALID_THUMB; + } + thumb->width = bytes_to_native_i32(&shape[0], endian_switch); + thumb->height = bytes_to_native_i32(&shape[4], endian_switch); + + /* Verify that image dimensions and data size make sense. */ + size_t data_size = block_size - 8; + const size_t expected_size = thumb->width * thumb->height * 4; + if (thumb->width < 0 || thumb->height < 0 || data_size != expected_size) { + return BT_INVALID_THUMB; + } + + thumb->data = blender::Array(data_size); + if (!file_read(file, thumb->data.data(), data_size)) { + return BT_INVALID_THUMB; + } + return BT_OK; + } + case MAKE_ID('R', 'E', 'N', 'D'): { + if (!file_seek(file, block_size)) { + return BT_INVALID_THUMB; + } + /* Check the next block. */ + break; + } + default: { + /* Early exit if there are no `TEST` or `REND` blocks. + * This saves scanning the entire blend file which could be slow. */ + return BT_INVALID_THUMB; + } + } + } + + return BT_INVALID_THUMB; +} + +/** + * This function extracts the thumbnail from the .blend file into thumb. + * Returns #BT_OK for success and the relevant error code otherwise. + */ +eThumbStatus blendthumb_create_thumb_from_file(FileReader *rawfile, Thumbnail *thumb) +{ + /* Read header in order to identify file type. */ + char header[12]; + if (rawfile->read(rawfile, header, sizeof(header)) != sizeof(header)) { + rawfile->close(rawfile); + return BT_ERROR; + } + + /* Rewind the file after reading the header. */ + rawfile->seek(rawfile, 0, SEEK_SET); + + /* Try to identify the file type from the header. */ + FileReader *file = nullptr; + if (BLI_str_startswith(header, "BLENDER")) { + file = rawfile; + rawfile = nullptr; + } + else if (BLI_file_magic_is_gzip(header)) { + file = BLI_filereader_new_gzip(rawfile); + if (file != nullptr) { + rawfile = nullptr; /* The Gzip #FileReader takes ownership of raw-file. */ + } + } + else if (BLI_file_magic_is_zstd(header)) { + file = BLI_filereader_new_zstd(rawfile); + if (file != nullptr) { + rawfile = nullptr; /* The Zstd #FileReader takes ownership of raw-file. */ + } + } + + /* Clean up rawfile if it wasn't taken over. */ + if (rawfile != nullptr) { + rawfile->close(rawfile); + } + + if (file == nullptr) { + return BT_ERROR; + } + + /* Re-read header in case we had compression. */ + if (file->read(file, header, sizeof(header)) != sizeof(header)) { + file->close(file); + return BT_ERROR; + } + + /* Check if the header format is valid for a .blend file. */ + if (!blend_header_check_magic(header)) { + file->close(file); + return BT_INVALID_FILE; + } + + /* Check if the file is new enough to contain a thumbnail. */ + if (!blend_header_is_version_valid(header)) { + file->close(file); + return BT_EARLY_VERSION; + } + + /* Depending on where it was saved, the file can use different pointer size or endianness. */ + int bhead_size = 16 + blend_header_pointer_size(header); + const bool endian_switch = blend_header_is_endian_switch_needed(header); + + /* Read the thumbnail. */ + eThumbStatus err = blendthumb_extract_from_file_impl(file, thumb, bhead_size, endian_switch); + file->close(file); + if (err != BT_OK) { + return err; + } + + thumb_data_vertical_flip(thumb); + return BT_OK; +} diff --git a/source/blender/blendthumb/src/blendthumb_png.cc b/source/blender/blendthumb/src/blendthumb_png.cc new file mode 100644 index 00000000000..d8156150078 --- /dev/null +++ b/source/blender/blendthumb/src/blendthumb_png.cc @@ -0,0 +1,158 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * The Original Code is Copyright (C) 2008 Blender Foundation. + * All rights reserved. + */ + +/** \file + * \ingroup blendthumb + * + * Expose #blendthumb_create_png_data_from_thumb that creates the PNG data + * but does not write it to a file. + */ + +#include +#include +#include + +#include "blendthumb.hh" + +#include "BLI_endian_defines.h" +#include "BLI_endian_switch.h" +#include "BLI_vector.hh" + +static void png_extend_native_int32(blender::Vector &output, int32_t data) +{ + if (ENDIAN_ORDER == L_ENDIAN) { + BLI_endian_switch_int32(&data); + } + output.extend_unchecked(blender::Span((uint8_t *)&data, 4)); +} + +/** The number of bytes each chunk uses on top of the data that's written. */ +#define PNG_CHUNK_EXTRA 12 + +static void png_chunk_create(blender::Vector &output, + const uint32_t tag, + const blender::Vector &data) +{ + uint32_t crc = crc32(0, nullptr, 0); + crc = crc32(crc, (uint8_t *)&tag, sizeof(tag)); + crc = crc32(crc, (uint8_t *)data.data(), data.size()); + + png_extend_native_int32(output, data.size()); + output.extend_unchecked(blender::Span((uint8_t *)&tag, sizeof(tag))); + output.extend_unchecked(data); + png_extend_native_int32(output, crc); +} + +static blender::Vector filtered_rows_from_thumb(const Thumbnail *thumb) +{ + /* In the image data sent to the compression step, each scan-line is preceded by a filter type + * byte containing the numeric code of the filter algorithm used for that scan-line. */ + const size_t line_size = thumb->width * 4; + blender::Vector filtered{}; + size_t final_size = thumb->height * (line_size + 1); + filtered.reserve(final_size); + for (int i = 0; i < thumb->height; i++) { + filtered.append_unchecked(0x00); + filtered.extend_unchecked(blender::Span(&thumb->data[i * line_size], line_size)); + } + BLI_assert(final_size == filtered.size()); + return filtered; +} + +static std::optional> zlib_compress(const blender::Vector &data) +{ + unsigned long uncompressed_size = data.size(); + uLongf compressed_size = compressBound(uncompressed_size); + + blender::Vector compressed(compressed_size, 0x00); + + int return_value = compress2((uchar *)compressed.data(), + &compressed_size, + (uchar *)data.data(), + uncompressed_size, + Z_NO_COMPRESSION); + if (return_value != Z_OK) { + /* Something went wrong with compression of data. */ + return std::nullopt; + } + compressed.resize(compressed_size); + return compressed; +} + +std::optional> blendthumb_create_png_data_from_thumb( + const Thumbnail *thumb) +{ + if (thumb->data.is_empty()) { + return std::nullopt; + } + + /* Create `IDAT` chunk data. */ + blender::Vector image_data{}; + { + auto image_data_opt = zlib_compress(filtered_rows_from_thumb(thumb)); + if (image_data_opt == std::nullopt) { + return std::nullopt; + } + image_data = *image_data_opt; + } + + /* Create the IHDR chunk data. */ + blender::Vector ihdr_data{}; + { + const size_t ihdr_data_final_size = 4 + 4 + 5; + ihdr_data.reserve(ihdr_data_final_size); + png_extend_native_int32(ihdr_data, thumb->width); + png_extend_native_int32(ihdr_data, thumb->height); + ihdr_data.extend_unchecked({ + 0x08, /* Bit Depth. */ + 0x06, /* Color Type. */ + 0x00, /* Compression method. */ + 0x00, /* Filter method. */ + 0x00, /* Interlace method. */ + }); + BLI_assert((size_t)ihdr_data.size() == ihdr_data_final_size); + } + + /* Join it all together to create a PNG image. */ + blender::Vector png_buf{}; + { + const size_t png_buf_final_size = ( + /* Header. */ + 8 + + /* `IHDR` chunk. */ + (ihdr_data.size() + PNG_CHUNK_EXTRA) + + /* `IDAT` chunk. */ + (image_data.size() + PNG_CHUNK_EXTRA) + + /* `IEND` chunk. */ + PNG_CHUNK_EXTRA); + + png_buf.reserve(png_buf_final_size); + + /* This is the standard PNG file header. Every PNG file starts with it. */ + png_buf.extend_unchecked({0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}); + + png_chunk_create(png_buf, MAKE_ID('I', 'H', 'D', 'R'), ihdr_data); + png_chunk_create(png_buf, MAKE_ID('I', 'D', 'A', 'T'), image_data); + png_chunk_create(png_buf, MAKE_ID('I', 'E', 'N', 'D'), {}); + + BLI_assert((size_t)png_buf.size() == png_buf_final_size); + } + + return png_buf; +} diff --git a/source/blender/blendthumb/src/blendthumb_win32.cc b/source/blender/blendthumb/src/blendthumb_win32.cc new file mode 100644 index 00000000000..d757bb1c97e --- /dev/null +++ b/source/blender/blendthumb/src/blendthumb_win32.cc @@ -0,0 +1,237 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup blendthumb + * + * Thumbnail from Blend file extraction for MS-Windows. + */ + +#include +#include +#include +#include +#include /* for #IThumbnailProvider */ + +#include "Wincodec.h" + +#include "blendthumb.hh" + +#include "BLI_filereader.h" + +#pragma comment(lib, "shlwapi.lib") + +/** + * This thumbnail provider implements #IInitializeWithStream to enable being hosted + * in an isolated process for robustness. + */ +class CBlendThumb : public IInitializeWithStream, public IThumbnailProvider { + public: + CBlendThumb() : _cRef(1), _pStream(NULL) + { + } + + virtual ~CBlendThumb() + { + if (_pStream) { + _pStream->Release(); + } + } + + IFACEMETHODIMP QueryInterface(REFIID riid, void **ppv) + { + static const QITAB qit[] = { + QITABENT(CBlendThumb, IInitializeWithStream), + QITABENT(CBlendThumb, IThumbnailProvider), + {0}, + }; + return QISearch(this, qit, riid, ppv); + } + + IFACEMETHODIMP_(ULONG) AddRef() + { + return InterlockedIncrement(&_cRef); + } + + IFACEMETHODIMP_(ULONG) Release() + { + ULONG cRef = InterlockedDecrement(&_cRef); + if (!cRef) { + delete this; + } + return cRef; + } + + /** IInitializeWithStream */ + IFACEMETHODIMP Initialize(IStream *pStream, DWORD grfMode); + + /** IThumbnailProvider */ + IFACEMETHODIMP GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha); + + private: + long _cRef; + IStream *_pStream; /* provided in Initialize(). */ +}; + +HRESULT CBlendThumb_CreateInstance(REFIID riid, void **ppv) +{ + CBlendThumb *pNew = new (std::nothrow) CBlendThumb(); + HRESULT hr = pNew ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) { + hr = pNew->QueryInterface(riid, ppv); + pNew->Release(); + } + return hr; +} + +IFACEMETHODIMP CBlendThumb::Initialize(IStream *pStream, DWORD) +{ + if (_pStream != NULL) { + /* Can only be initialized once. */ + return E_UNEXPECTED; + } + /* Take a reference to the stream. */ + return pStream->QueryInterface(&_pStream); +} + +/** + * #FileReader compatible wrapper around the Windows stream that gives access to the .blend file. + */ +typedef struct { + FileReader reader; + + IStream *_pStream; +} StreamReader; + +static ssize_t stream_read(FileReader *reader, void *buffer, size_t size) +{ + StreamReader *stream = (StreamReader *)reader; + + ULONG readsize; + stream->_pStream->Read(buffer, size, &readsize); + stream->reader.offset += readsize; + + return (ssize_t)readsize; +} + +static off64_t stream_seek(FileReader *reader, off64_t offset, int whence) +{ + StreamReader *stream = (StreamReader *)reader; + + DWORD origin = STREAM_SEEK_SET; + switch (whence) { + case SEEK_CUR: + origin = STREAM_SEEK_CUR; + break; + case SEEK_END: + origin = STREAM_SEEK_END; + break; + } + LARGE_INTEGER offsetI; + offsetI.QuadPart = offset; + ULARGE_INTEGER newPos; + stream->_pStream->Seek(offsetI, origin, &newPos); + stream->reader.offset = newPos.QuadPart; + + return stream->reader.offset; +} + +static void stream_close(FileReader *reader) +{ + StreamReader *stream = (StreamReader *)reader; + delete stream; +} + +IFACEMETHODIMP CBlendThumb::GetThumbnail(UINT cx, HBITMAP *phbmp, WTS_ALPHATYPE *pdwAlpha) +{ + HRESULT hr = S_FALSE; + + StreamReader *file = new StreamReader; + file->reader.read = stream_read; + file->reader.seek = stream_seek; + file->reader.close = stream_close; + file->reader.offset = 0; + file->_pStream = _pStream; + + file->reader.seek(&file->reader, 0, SEEK_SET); + + /* Extract thumbnail from stream. */ + Thumbnail thumb; + if (blendthumb_create_thumb_from_file(&file->reader, &thumb) != BT_OK) { + return S_FALSE; + } + + /* Convert to BGRA for Windows. */ + for (int i = 0; i < thumb.width * thumb.height; i++) { + std::swap(thumb.data[4 * i], thumb.data[4 * i + 2]); + } + + *phbmp = CreateBitmap(thumb.width, thumb.height, 1, 32, thumb.data.data()); + if (!*phbmp) { + return E_FAIL; + } + *pdwAlpha = WTSAT_ARGB; + + /* Scale down the thumbnail if required. */ + if ((unsigned)thumb.width > cx || (unsigned)thumb.height > cx) { + float scale = 1.0f / (std::max(thumb.width, thumb.height) / (float)cx); + LONG NewWidth = (LONG)(thumb.width * scale); + LONG NewHeight = (LONG)(thumb.height * scale); + + IWICImagingFactory *pImgFac; + hr = CoCreateInstance( + CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pImgFac)); + + IWICBitmap *WICBmp; + hr = pImgFac->CreateBitmapFromHBITMAP(*phbmp, 0, WICBitmapUseAlpha, &WICBmp); + + BITMAPINFO bmi = {}; + bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader); + bmi.bmiHeader.biWidth = NewWidth; + bmi.bmiHeader.biHeight = -NewHeight; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + BYTE *pBits; + HBITMAP ResizedHBmp = CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, (void **)&pBits, NULL, 0); + hr = ResizedHBmp ? S_OK : E_OUTOFMEMORY; + if (SUCCEEDED(hr)) { + IWICBitmapScaler *pIScaler; + hr = pImgFac->CreateBitmapScaler(&pIScaler); + hr = pIScaler->Initialize(WICBmp, NewWidth, NewHeight, WICBitmapInterpolationModeFant); + + WICRect rect = {0, 0, NewWidth, NewHeight}; + hr = pIScaler->CopyPixels(&rect, NewWidth * 4, NewWidth * NewHeight * 4, pBits); + + if (SUCCEEDED(hr)) { + DeleteObject(*phbmp); + *phbmp = ResizedHBmp; + } + else { + DeleteObject(ResizedHBmp); + } + + pIScaler->Release(); + } + WICBmp->Release(); + pImgFac->Release(); + } + else { + hr = S_OK; + } + return hr; +} diff --git a/source/blender/blendthumb/src/blendthumb_win32.def b/source/blender/blendthumb/src/blendthumb_win32.def new file mode 100644 index 00000000000..71f9236735f --- /dev/null +++ b/source/blender/blendthumb/src/blendthumb_win32.def @@ -0,0 +1,5 @@ +EXPORTS + DllGetClassObject PRIVATE + DllCanUnloadNow PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE \ No newline at end of file diff --git a/source/blender/blendthumb/src/blendthumb_win32.rc b/source/blender/blendthumb/src/blendthumb_win32.rc new file mode 100644 index 00000000000..5dfd416b0c5 --- /dev/null +++ b/source/blender/blendthumb/src/blendthumb_win32.rc @@ -0,0 +1,26 @@ +#define IDR_VERSION1 1 + +IDR_VERSION1 VERSIONINFO +FILEVERSION 1,4,0,0 +PRODUCTVERSION 2,78,0,0 +FILEOS 0x00000004 +FILETYPE 0x00000002 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "FFFF04B0" + BEGIN + VALUE "FileVersion", "1.4\0" + VALUE "ProductVersion", "2.78\0" + VALUE "FileDescription", "Blender Thumbnail Handler\0" + VALUE "OriginalFilename", "BlendThumb.dll\0" + VALUE "ProductName", "Blender\0" + VALUE "LegalCopyright", "GPL2, 2016\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x0409, 0x04B0 + END +END + diff --git a/source/blender/blendthumb/src/blendthumb_win32_dll.cc b/source/blender/blendthumb/src/blendthumb_win32_dll.cc new file mode 100644 index 00000000000..7f10777f884 --- /dev/null +++ b/source/blender/blendthumb/src/blendthumb_win32_dll.cc @@ -0,0 +1,293 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +/** \file + * \ingroup blendthumb + * + * Thumbnail from Blend file extraction for MS-Windows (DLL). + */ + +#include +#include +#include /* For #SHChangeNotify */ +#include +#include /* For IThumbnailProvider */ + +extern HRESULT CBlendThumb_CreateInstance(REFIID riid, void **ppv); + +#define SZ_CLSID_BLENDTHUMBHANDLER L"{D45F043D-F17F-4e8a-8435-70971D9FA46D}" +#define SZ_BLENDTHUMBHANDLER L"Blender Thumbnail Handler" +const CLSID CLSID_BlendThumbHandler = { + 0xd45f043d, 0xf17f, 0x4e8a, {0x84, 0x35, 0x70, 0x97, 0x1d, 0x9f, 0xa4, 0x6d}}; + +typedef HRESULT (*PFNCREATEINSTANCE)(REFIID riid, void **ppvObject); +struct CLASS_OBJECT_INIT { + const CLSID *pClsid; + PFNCREATEINSTANCE pfnCreate; +}; + +/* Add classes supported by this module here. */ +const CLASS_OBJECT_INIT c_rgClassObjectInit[] = { + {&CLSID_BlendThumbHandler, CBlendThumb_CreateInstance}}; + +long g_cRefModule = 0; + +/** Handle the DLL's module */ +HINSTANCE g_hInst = nullptr; + +/** Standard DLL functions. */ +STDAPI_(BOOL) DllMain(HINSTANCE hInstance, DWORD dwReason, void *) +{ + if (dwReason == DLL_PROCESS_ATTACH) { + g_hInst = hInstance; + DisableThreadLibraryCalls(hInstance); + } + return TRUE; +} + +STDAPI DllCanUnloadNow() +{ + /* Only allow the DLL to be unloaded after all outstanding references have been released. */ + return (g_cRefModule == 0) ? S_OK : S_FALSE; +} + +void DllAddRef() +{ + InterlockedIncrement(&g_cRefModule); +} + +void DllRelease() +{ + InterlockedDecrement(&g_cRefModule); +} + +class CClassFactory : public IClassFactory { + public: + static HRESULT CreateInstance(REFCLSID clsid, + const CLASS_OBJECT_INIT *pClassObjectInits, + size_t cClassObjectInits, + REFIID riid, + void **ppv) + { + *ppv = nullptr; + 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(); + } + /* Match found. */ + break; + } + } + return hr; + } + + CClassFactory(PFNCREATEINSTANCE pfnCreate) : _cRef(1), _pfnCreate(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 InterlockedIncrement(&_cRef); + } + + IFACEMETHODIMP_(ULONG) Release() + { + long cRef = InterlockedDecrement(&_cRef); + if (cRef == 0) { + delete this; + } + return cRef; + } + + /** #IClassFactory */ + IFACEMETHODIMP CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv) + { + return punkOuter ? CLASS_E_NOAGGREGATION : _pfnCreate(riid, ppv); + } + + IFACEMETHODIMP LockServer(BOOL fLock) + { + if (fLock) { + DllAddRef(); + } + else { + DllRelease(); + } + return S_OK; + } + + private: + ~CClassFactory() + { + DllRelease(); + } + + long _cRef; + PFNCREATEINSTANCE _pfnCreate; +}; + +STDAPI DllGetClassObject(REFCLSID clsid, REFIID riid, void **ppv) +{ + return CClassFactory::CreateInstance( + clsid, c_rgClassObjectInit, ARRAYSIZE(c_rgClassObjectInit), riid, ppv); +} + +/** + * A struct to hold the information required for a registry entry. + */ +struct REGISTRY_ENTRY { + HKEY hkeyRoot; + PCWSTR pszKeyName; + PCWSTR pszValueName; + DWORD dwValueType; + /** These two fields could/should have been a union, but C++ */ + PCWSTR pszData; + /** Only lets you initialize the first field in a union. */ + DWORD dwData; +}; + +/** + * 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, + nullptr, + &hKey, + nullptr)); + if (SUCCEEDED(hr)) { + /* All this just to support #REG_DWORD. */ + DWORD size; + DWORD data; + BYTE *lpData = (LPBYTE)pRegistryEntry->pszData; + switch (pRegistryEntry->dwValueType) { + case REG_SZ: + size = ((DWORD)wcslen(pRegistryEntry->pszData) + 1) * sizeof(WCHAR); + break; + case REG_DWORD: + size = sizeof(DWORD); + data = pRegistryEntry->dwData; + lpData = (BYTE *)&data; + break; + default: + return E_INVALIDARG; + } + + hr = HRESULT_FROM_WIN32(RegSetValueExW( + hKey, pRegistryEntry->pszValueName, 0, pRegistryEntry->dwValueType, lpData, size)); + RegCloseKey(hKey); + } + return hr; +} + +/** + * Registers this COM server. + */ +STDAPI DllRegisterServer() +{ + HRESULT hr; + + WCHAR szModuleName[MAX_PATH]; + + if (!GetModuleFileNameW(g_hInst, szModuleName, ARRAYSIZE(szModuleName))) { + hr = HRESULT_FROM_WIN32(GetLastError()); + } + else { + const REGISTRY_ENTRY rgRegistryEntries[] = { + /* `RootKey KeyName ValueName ValueType Data` */ + {HKEY_CURRENT_USER, + L"Software\\Classes\\CLSID\\" SZ_CLSID_BLENDTHUMBHANDLER, + nullptr, + REG_SZ, + SZ_BLENDTHUMBHANDLER}, + {HKEY_CURRENT_USER, + L"Software\\Classes\\CLSID\\" SZ_CLSID_BLENDTHUMBHANDLER L"\\InProcServer32", + nullptr, + REG_SZ, + szModuleName}, + {HKEY_CURRENT_USER, + L"Software\\Classes\\CLSID\\" SZ_CLSID_BLENDTHUMBHANDLER L"\\InProcServer32", + L"ThreadingModel", + REG_SZ, + L"Apartment"}, + {HKEY_CURRENT_USER, + L"Software\\Classes\\.blend\\", + L"Treatment", + REG_DWORD, + 0, + 0}, /* This doesn't appear to do anything. */ + {HKEY_CURRENT_USER, + L"Software\\Classes\\.blend\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}", + nullptr, + REG_SZ, + SZ_CLSID_BLENDTHUMBHANDLER}, + }; + + hr = S_OK; + for (int i = 0; i < ARRAYSIZE(rgRegistryEntries) && SUCCEEDED(hr); i++) { + hr = CreateRegKeyAndSetValue(&rgRegistryEntries[i]); + } + } + if (SUCCEEDED(hr)) { + /* This tells the shell to invalidate the thumbnail cache. + * This is important because any `.blend` 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 rgpszKeys[] = { + L"Software\\Classes\\CLSID\\" SZ_CLSID_BLENDTHUMBHANDLER, + L"Software\\Classes\\.blend\\ShellEx\\{e357fccd-a995-4576-b01f-234630154e96}"}; + + /* Delete the registry entries. */ + for (int i = 0; i < ARRAYSIZE(rgpszKeys) && SUCCEEDED(hr); i++) { + hr = HRESULT_FROM_WIN32(RegDeleteTreeW(HKEY_CURRENT_USER, rgpszKeys[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; +} -- cgit v1.2.3