Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \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 <iostream>
#include <optional>
#include <fcntl.h>
#ifndef WIN32
# include <unistd.h> /* For read close. */
#else
# include "BLI_winstuff.h"
# include "winsock2.h"
# include <io.h> /* For open close read. */
#endif
#include "BLI_fileops.h"
#include "BLI_filereader.h"
#include "BLI_vector.hh"
#include "blendthumb.hh"
using namespace blender;
/**
* 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<Vector<uint8_t>> png_buf_opt = blendthumb_create_png_data_from_thumb(&thumb);
if (!png_buf_opt) {
err = BT_ERROR;
}
else {
Vector<uint8_t> 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 <input.blend> <output.png>" << std::endl;
return -1;
}
eThumbStatus ret = extract_png_from_blend_file(argv[1], argv[2]);
return int(ret);
}

View File

@@ -0,0 +1,52 @@
/* SPDX-FileCopyrightText: 2008-2021 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup blendthumb
*
* Shared thumbnail extraction logic.
*
* Used for both MS-Windows DLL and Unix command line.
*/
#pragma once
#include <optional>
#include "BLI_array.hh"
#include "BLI_vector.hh"
namespace blender {
struct FileReader;
struct Thumbnail {
Array<uint8_t> 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<Vector<uint8_t>> blendthumb_create_png_data_from_thumb(const Thumbnail *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);
/* INTEGER CODES */
/* NOTE: this is endianness-sensitive. */
#define MAKE_ID(a, b, c, d) (int(d) << 24 | int(c) << 16 | (b) << 8 | (a))
} // namespace blender

View File

@@ -0,0 +1,181 @@
/* SPDX-FileCopyrightText: 2008 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup blendthumb
*
* Expose #blendthumb_create_thumb_from_file that creates the PNG data
* but does not write it to a file.
*/
#include <cctype>
#include <cstring>
#include "BLI_alloca.h"
#include "BLI_endian_defines.h"
#include "BLI_fileops.h"
#include "BLI_filereader.h"
#include "BLI_string.h"
#include "BLO_core_bhead.hh"
#include "BLO_core_blend_header.hh"
#include "BLO_core_file_reader.hh"
#include "blendthumb.hh"
namespace blender {
BLI_STATIC_ASSERT(ENDIAN_ORDER == L_ENDIAN, "Blender only builds on little endian systems")
static void thumb_data_vertical_flip(Thumbnail *thumb)
{
uint32_t *rect = reinterpret_cast<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 = static_cast<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])
{
int32_t data;
memcpy(&data, bytes, 4);
/* NOTE: this is endianness-sensitive. */
/* PNG is always little-endian, and would require switching on a big-endian system. */
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;
Array<char> 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 BlenderHeader &header)
{
BLI_assert(header.endian == L_ENDIAN);
/* Iterate over file blocks until we find the thumbnail or run out of data. */
while (true) {
/* Read next BHead. */
const std::optional<BHead> bhead = BLO_readfile_read_bhead(file, header.bhead_type());
if (!bhead.has_value()) {
/* File has ended. */
return BT_INVALID_THUMB;
}
if (bhead->len < 0) {
/* Avoid parsing bad data. */
return BT_INVALID_THUMB;
}
switch (bhead->code) {
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]);
thumb->height = bytes_to_native_i32(&shape[4]);
/* Verify that image dimensions and data size make sense. */
size_t data_size = bhead->len - sizeof(shape);
const uint64_t expected_size = uint64_t(thumb->width) * uint64_t(thumb->height) * 4;
if (thumb->width < 0 || thumb->height < 0 || data_size != expected_size) {
return BT_INVALID_THUMB;
}
thumb->data = Array<uint8_t>(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, bhead->len)) {
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;
}
eThumbStatus blendthumb_create_thumb_from_file(FileReader *rawfile, Thumbnail *thumb)
{
FileReader *file = BLO_file_reader_uncompressed(rawfile);
if (file == nullptr) {
return BT_ERROR;
}
const BlenderHeaderVariant header_variant = BLO_readfile_blender_header_decode(file);
if (!std::holds_alternative<BlenderHeader>(header_variant)) {
file->close(file);
return BT_ERROR;
}
const BlenderHeader &header = std::get<BlenderHeader>(header_variant);
/* Check if the file is new enough to contain a thumbnail. */
if (header.file_version < 250) {
file->close(file);
return BT_EARLY_VERSION;
}
/* Check if the file was written from a big-endian build. */
if (header.endian != L_ENDIAN) {
file->close(file);
return BT_INVALID_FILE;
}
BLI_assert(header.endian == ENDIAN_ORDER);
/* Read the thumbnail. */
eThumbStatus err = blendthumb_extract_from_file_impl(file, thumb, header);
file->close(file);
if (err != BT_OK) {
return err;
}
thumb_data_vertical_flip(thumb);
return BT_OK;
}
} // namespace blender

View File

@@ -0,0 +1,147 @@
/* SPDX-FileCopyrightText: 2008 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup blendthumb
*
* Expose #blendthumb_create_png_data_from_thumb that creates the PNG data
* but does not write it to a file.
*/
#include <cstring>
#include <optional>
#include <zlib.h>
#include "blendthumb.hh"
#include "BLI_endian_defines.h"
#include "BLI_endian_switch.h"
#include "BLI_vector.hh"
namespace blender {
static void png_extend_native_int32(Vector<uint8_t> &output, int32_t data)
{
/* NOTE: this is endianness-sensitive. */
/* PNG is big-endian, its values need to be switched on little-endian systems. */
BLI_endian_switch_int32(&data);
output.extend_unchecked(Span(reinterpret_cast<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(Vector<uint8_t> &output,
const uint32_t tag,
const Vector<uint8_t> &data)
{
uint32_t crc = crc32(0, nullptr, 0);
crc = crc32(crc, reinterpret_cast<uint8_t *>(const_cast<uint32_t *>(&tag)), sizeof(tag));
crc = crc32(crc, const_cast<uint8_t *>(data.data()), data.size());
png_extend_native_int32(output, data.size());
output.extend_unchecked(
Span(reinterpret_cast<uint8_t *>(const_cast<uint32_t *>(&tag)), sizeof(tag)));
output.extend_unchecked(data);
png_extend_native_int32(output, crc);
}
static Vector<uint8_t> 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;
Vector<uint8_t> 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(Span(&thumb->data[i * line_size], line_size));
}
BLI_assert(final_size == filtered.size());
return filtered;
}
static std::optional<Vector<uint8_t>> zlib_compress(const Vector<uint8_t> &data)
{
ulong uncompressed_size = data.size();
uLongf compressed_size = compressBound(uncompressed_size);
Vector<uint8_t> compressed(compressed_size, 0x00);
int return_value = compress2(static_cast<uchar *>(compressed.data()),
&compressed_size,
const_cast<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<Vector<uint8_t>> blendthumb_create_png_data_from_thumb(const Thumbnail *thumb)
{
if (thumb->data.is_empty()) {
return std::nullopt;
}
/* Create `IDAT` chunk data. */
Vector<uint8_t> 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. */
Vector<uint8_t> 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. */
Vector<uint8_t> 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;
}
} // namespace blender

View File

@@ -0,0 +1,226 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup blendthumb
*
* Thumbnail from Blend file extraction for MS-Windows.
*/
#include <cmath>
#include <new>
#include <shlwapi.h>
#include <string>
#include <thumbcache.h> /* for #IThumbnailProvider */
#include "Wincodec.h"
#include "blendthumb.hh"
#include "BLI_filereader.h"
#pragma comment(lib, "shlwapi.lib")
using namespace blender;
/**
* 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(nullptr) {}
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 != nullptr) {
/* 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.
*/
struct StreamReader {
FileReader reader;
IStream *_pStream;
};
static int64_t stream_read(FileReader *reader, void *buffer, size_t size)
{
StreamReader *stream = reinterpret_cast<StreamReader *>(reader);
ULONG readsize;
stream->_pStream->Read(buffer, size, &readsize);
stream->reader.offset += readsize;
return int64_t(readsize);
}
static off64_t stream_seek(FileReader *reader, off64_t offset, int whence)
{
StreamReader *stream = reinterpret_cast<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 = reinterpret_cast<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 up the thumbnail if required. */
if (uint(thumb.width) < cx && uint(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, nullptr, 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(
nullptr, &bmi, DIB_RGB_COLORS, (void **)&pBits, nullptr, 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;
}

View File

@@ -0,0 +1,5 @@
EXPORTS
DllGetClassObject PRIVATE
DllCanUnloadNow PRIVATE
DllRegisterServer PRIVATE
DllUnregisterServer PRIVATE

View File

@@ -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

View File

@@ -0,0 +1,281 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup blendthumb
*
* Thumbnail from Blend file extraction for MS-Windows (DLL).
*/
#include <new>
#include <objbase.h>
#include <shlobj.h> /* For #SHChangeNotify */
#include <shlwapi.h>
#include <thumbcache.h> /* 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 final : 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;
}

View File

@@ -0,0 +1,14 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#import <QuickLookThumbnailing/QuickLookThumbnailing.h>
NS_ASSUME_NONNULL_BEGIN
@interface ThumbnailProvider : QLThumbnailProvider
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,186 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#import <AppKit/NSImage.h>
#include "BLI_fileops.h"
#include "BLI_filereader.h"
#include "BLI_utility_mixins.hh"
#include "blendthumb.hh"
#include "thumbnail_provider.h"
/**
* This section intends to list the important steps for creating a thumbnail extension.
* qlgenerator has been deprecated and removed in platforms we support. App extensions are the way
* forward. But there's little guidance on how to do it outside Xcode.
*
* The process of thumbnail generation goes something like this:
* 1. If an app is launched, or is registered with lsregister, its plugins also get registered.
* 2. When a file thumbnail in Finder or QuickLook is requested, the system looks for a plugin
* that supports the file type UTI.
* 3. The plugin is launched in a sand-boxed environment and should call the handler with a reply.
*
* # Plugin Info.plist
* The Info.plist file should be properly configured with supported content type.
*
* # Codesigning
* The plugin should be codesigned with entitlements at least for sandbox and read-only/
* read-write (for access to the given file). It's needed to even run the plugin locally.
* com.apple.security.get-task-allow entitlement is required for debugging.
*
* # Registering the plugin
* The plugin should be registered with lsregister. Either by calling lsregister or by launching
* the parent app.
* /System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister
* \ -dump | grep blender-thumbnailer
*
* # Debugging
* Since read-only entitlement is there, creating files to log is not possible. So NSLog and
* viewing it in Console.app (after triggering a thumbnail) is the way to go. Interesting processes
* are: qlmanage, quicklookd, kernel, blender-thumbnailer, secinitd,
* com.apple.quicklook.ThumbnailsAgent
*
* LLDB/ Xcode etc., debuggers can be used to get extra logs than CLI invocation but breakpoints
* still are a pain point. /usr/bin/qlmanage is the target executable. Other args to qlmanage
* follow. lldb qlmanage -- -t -x a.blend
*
* # Troubleshooting
* - The appex shouldn't have any quarantine flag.
* xattr -rl bin/Blender.app/Contents/Plugins/blender-thumbnailer.appex
* - Is it registered with lsregister and there isn't a conflict with another plugin taking
* precedence? lsregister -dump | grep blender-thumbnailer.appex
* - For RBSLaunchRequest error: is the executable flag set? chmod u+x
* bin/Blender.app/Contents/PlugIns/blender-thumbnailer.appex/Contents/MacOS/blender-thumbnailer
* - Is it codesigned and sandboxed?
* codesign --display --verbose --entitlements - --xml \
* bin/Blender.app/Contents/Plugins/blender-thumbnailer.appex codesign --deep --force --sign - \
* --entitlements ../blender/release/darwin/thumbnailer_entitlements.plist --timestamp=none \
* bin/Blender.app/Contents/Plugins/blender-thumbnailer.appex
* - Sometimes blender-thumbnailer running in background can be killed.
* - qlmanage -r && killall Finder
* - The code cannot attempt to do anything outside sandbox like writing to blend.
*
* # Triggering a thumbnail
* - qlmanage -t -x /path/to/file.blend
*
* # External resources
* https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/Quicklook_Programming_Guide/Introduction/Introduction.html#//apple_ref/doc/uid/TP40005020-CH1-SW1
*/
class FileDescriptorRAII : blender::NonCopyable, blender::NonMovable {
private:
int src_fd = -1;
public:
explicit FileDescriptorRAII(const char *file_path)
{
src_fd = blender::BLI_open(file_path, O_BINARY | O_RDONLY, 0);
}
~FileDescriptorRAII()
{
if (good()) {
int ok = close(src_fd);
if (!ok) {
NSLog(@"Blender Thumbnailer Error: Failed to close the blend file.");
}
}
}
bool good()
{
return src_fd > 0;
}
int get()
{
return src_fd;
}
};
static NSError *create_nserror_from_string(NSString *errorStr)
{
NSLog(@"Blender Thumbnailer Error: %@", errorStr);
return [NSError errorWithDomain:@"org.blenderfoundation.blender.thumbnailer"
code:-1
userInfo:@{NSLocalizedDescriptionKey : errorStr}];
}
static NSImage *generate_nsimage_for_file(const char *src_blend_path, NSError *error)
{
/* Open source file `src_blend`. */
FileDescriptorRAII src_file_fd = FileDescriptorRAII(src_blend_path);
if (!src_file_fd.good()) {
error = create_nserror_from_string(@"Failed to open blend");
return nil;
}
blender::FileReader *file_content = blender::BLI_filereader_new_file(src_file_fd.get());
if (file_content == nullptr) {
error = create_nserror_from_string(@"Failed to read from blend");
return nil;
}
/* Extract thumbnail from file. */
blender::Thumbnail thumb;
blender::eThumbStatus err = blendthumb_create_thumb_from_file(file_content, &thumb);
if (err != blender::BT_OK) {
error = create_nserror_from_string(@"Failed to create thumbnail from file");
return nil;
}
std::optional<blender::Vector<uint8_t>> png_buf_opt = blendthumb_create_png_data_from_thumb(
&thumb);
if (!png_buf_opt) {
error = create_nserror_from_string(@"Failed to create png data from thumbnail");
return nil;
}
NSData *ns_data = [NSData dataWithBytes:png_buf_opt->data() length:png_buf_opt->size()];
NSImage *ns_image = [[NSImage alloc] initWithData:ns_data];
return ns_image;
}
@implementation ThumbnailProvider
- (void)provideThumbnailForFileRequest:(QLFileThumbnailRequest *)request
completionHandler:(void (^)(QLThumbnailReply *_Nullable reply,
NSError *_Nullable error))handler
{
NSLog(@"Generating thumbnail for %@", request.fileURL.path);
@autoreleasepool {
NSError *error = nil;
NSImage *image = generate_nsimage_for_file(request.fileURL.path.fileSystemRepresentation,
error);
if (image == nil || image.size.width <= 0 || image.size.height <= 0) {
handler(nil, error);
return;
}
const CGFloat width_ratio = request.maximumSize.width / image.size.width;
const CGFloat height_ratio = request.maximumSize.height / image.size.height;
const CGFloat scale_factor = MIN(width_ratio, height_ratio);
const NSSize context_size = NSMakeSize(image.size.width * scale_factor,
image.size.height * scale_factor);
const NSRect context_rect = NSMakeRect(0, 0, context_size.width, context_size.height);
QLThumbnailReply *thumbnailReply = [QLThumbnailReply replyWithContextSize:context_size
currentContextDrawingBlock:^BOOL {
[image drawInRect:context_rect];
/* Release the image that was strongly
* captured by this block. */
[image release];
return YES;
}];
/* Return the thumbnail reply. */
handler(thumbnailReply, nil);
}
NSLog(@"Thumbnail generation successfully completed");
}
@end