diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8550fdb9644..b14bbaadbb8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -216,20 +216,18 @@ Build \"BlendThumb.dll\" helper for Windows explorer integration to support extr
thumbnails from `.blend` files."
ON
)
-else()
- set(_option_default ON)
- if(APPLE)
- # In future, can be used with `quicklookthumbnailing/qlthumbnailreply`
- # to create file thumbnails for say Finder.
- # Turn it off for now, even though it can build on APPLE, it's not likely to be useful.
- set(_option_default OFF)
- endif()
+elseif(UNIX AND NOT APPLE)
option(WITH_BLENDER_THUMBNAILER "\
Build stand-alone \"blender-thumbnailer\" command-line thumbnail extraction utility, \
intended for use by file-managers to extract PNG images from `.blend` files."
- ${_option_default}
+ ON
+ )
+elseif(APPLE)
+ option(WITH_BLENDER_THUMBNAILER "\
+Build \"blender-thumbnailer.appex\" extension for macOS Finder/ QuickLook to support viewing \
+thumbnails from `.blend` files."
+ ON
)
- unset(_option_default)
endif()
option(WITH_INTERNATIONAL "Enable I18N (International fonts and text)" ON)
diff --git a/build_files/cmake/config/blender_release.cmake b/build_files/cmake/config/blender_release.cmake
index 43ac9a40df2..9a68a4911ff 100644
--- a/build_files/cmake/config/blender_release.cmake
+++ b/build_files/cmake/config/blender_release.cmake
@@ -69,6 +69,7 @@ set(WITH_MEM_JEMALLOC ON CACHE BOOL "" FORCE)
if(APPLE)
set(WITH_COREAUDIO ON CACHE BOOL "" FORCE)
set(WITH_CYCLES_DEVICE_METAL ON CACHE BOOL "" FORCE)
+ set(WITH_BLENDER_THUMBNAILER ON CACHE BOOL "" FORCE)
endif()
if(WIN32)
set(WITH_WASAPI ON CACHE BOOL "" FORCE)
diff --git a/release/darwin/Blender.app/Contents/PlugIns/blender-thumbnailer.appex/Contents/Info.plist b/release/darwin/Blender.app/Contents/PlugIns/blender-thumbnailer.appex/Contents/Info.plist
new file mode 100644
index 00000000000..7650187a785
--- /dev/null
+++ b/release/darwin/Blender.app/Contents/PlugIns/blender-thumbnailer.appex/Contents/Info.plist
@@ -0,0 +1,50 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleName
+ blender-thumbnailer
+ CFBundleDisplayName
+ Blender Thumbnailer
+ CFBundleExecutable
+ blender-thumbnailer
+ CFBundleIdentifier
+ org.blenderfoundation.blender.thumbnailer
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundlePackageType
+ XPC!
+ CFBundleSupportedPlatforms
+
+ MacOSX
+
+ CFBundleShortVersionString
+ ${MACOSX_BUNDLE_SHORT_VERSION_STRING}
+ CFBundleVersion
+ ${MACOSX_BUNDLE_LONG_VERSION_STRING}
+ CFBundleGetInfoString
+ ${MACOSX_BUNDLE_LONG_VERSION_STRING}, Blender Foundation
+ LSMinimumSystemVersion
+ 10.15
+ NSExtension
+
+ NSExtensionAttributes
+
+ QLSupportedContentTypes
+
+
+ org.blenderfoundation.blender.file
+
+ QLThumbnailMinimumDimension
+ 0
+
+ NSExtensionPointIdentifier
+ com.apple.quicklook.thumbnail
+ NSExtensionPrincipalClass
+
+ ThumbnailProvider
+
+
+
diff --git a/release/darwin/thumbnailer_entitlements.plist b/release/darwin/thumbnailer_entitlements.plist
new file mode 100644
index 00000000000..a894c2d6d43
--- /dev/null
+++ b/release/darwin/thumbnailer_entitlements.plist
@@ -0,0 +1,12 @@
+
+
+
+
+
+ com.apple.security.app-sandbox
+
+
+ com.apple.security.files.user-selected.read-only
+
+
+
diff --git a/source/blender/blendthumb/CMakeLists.txt b/source/blender/blendthumb/CMakeLists.txt
index e4120506407..3b9678be979 100644
--- a/source/blender/blendthumb/CMakeLists.txt
+++ b/source/blender/blendthumb/CMakeLists.txt
@@ -41,7 +41,37 @@ if(WIN32)
set_target_properties(BlendThumb PROPERTIES LINK_FLAGS_DEBUG "/NODEFAULTLIB:msvcrt")
set_target_properties(BlendThumb PROPERTIES VS_GLOBAL_VcpkgEnabled "false")
-else()
+elseif(APPLE)
+ # -----------------------------------------------------------------------------
+ # Build `blender-thumbnailer.appex` app extension.
+ set(SRC_APPEX
+ src/thumbnail_provider.mm
+ src/thumbnail_provider.h
+ )
+
+ add_executable(blender-thumbnailer MACOSX_BUNDLE ${SRC} ${SRC_APPEX})
+ setup_platform_linker_flags(blender-thumbnailer)
+ setup_platform_linker_libs(blender-thumbnailer)
+ target_link_libraries(blender-thumbnailer
+ bf_blenlib
+ # Avoid linker error about undefined _main symbol.
+ "-e _NSExtensionMain"
+ "-framework QuickLookThumbnailing"
+ )
+ # CMake needs the target defined in the same file as add_custom_command.
+ # The rpath here points to the main Blender Resources/lib directory. Avoid duplicating the large dylibs (~300MB).
+ add_custom_command(
+ TARGET blender-thumbnailer POST_BUILD
+ COMMAND install_name_tool -add_rpath @loader_path/../../../../Resources/lib $
+ )
+ # It needs to be codesigned (ad-hoc in this case) even on developer machine to generate thumbnails.
+ # Command taken from XCode build process.
+ add_custom_command(
+ TARGET blender-thumbnailer POST_BUILD
+ COMMAND codesign --deep --force --sign - --entitlements "${CMAKE_SOURCE_DIR}/release/darwin/thumbnailer_entitlements.plist"
+ --timestamp=none $
+ )
+elseif(UNIX)
# -----------------------------------------------------------------------------
# Build `blender-thumbnailer` executable
diff --git a/source/blender/blendthumb/src/thumbnail_provider.h b/source/blender/blendthumb/src/thumbnail_provider.h
new file mode 100644
index 00000000000..6407264cde4
--- /dev/null
+++ b/source/blender/blendthumb/src/thumbnail_provider.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+#pragma once
+
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface ThumbnailProvider : QLThumbnailProvider
+
+@end
+
+NS_ASSUME_NONNULL_END
diff --git a/source/blender/blendthumb/src/thumbnail_provider.mm b/source/blender/blendthumb/src/thumbnail_provider.mm
new file mode 100644
index 00000000000..84f400adcb5
--- /dev/null
+++ b/source/blender/blendthumb/src/thumbnail_provider.mm
@@ -0,0 +1,174 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#import
+
+#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 sandboxed 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.
+ *
+ * # 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 executable? 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 -s 512 -o /tmp/ /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 = 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;
+ }
+
+ FileReader *file_content = 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. */
+ Thumbnail thumb;
+ eThumbStatus err = blendthumb_create_thumb_from_file(file_content, &thumb);
+ if (err != BT_OK) {
+ error = create_nserror_from_string(@"Failed to create thumbnail from file");
+ return nil;
+ }
+
+ std::optional> 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 *ns_image = generate_nsimage_for_file(request.fileURL.path.fileSystemRepresentation,
+ error);
+ if (ns_image == nil) {
+ handler(nil, error);
+ return;
+ }
+ handler([QLThumbnailReply replyWithContextSize:request.maximumSize
+ currentContextDrawingBlock:^BOOL {
+ [ns_image drawInRect:NSMakeRect(0,
+ 0,
+ request.maximumSize.width,
+ request.maximumSize.height)];
+ // Release the ns_image that was strongly captured by the block.
+ [ns_image release];
+ return YES;
+ }],
+ nil);
+ }
+ NSLog(@"Thumbnail generation succcessfully completed");
+}
+
+@end
diff --git a/source/creator/CMakeLists.txt b/source/creator/CMakeLists.txt
index 4ce34055c4a..77347a31b6c 100644
--- a/source/creator/CMakeLists.txt
+++ b/source/creator/CMakeLists.txt
@@ -325,6 +325,9 @@ if(WITH_PYTHON_MODULE)
if(APPLE)
set_target_properties(blender PROPERTIES MACOSX_BUNDLE TRUE)
+ if(WITH_BLENDER_THUMBNAILER)
+ set_target_properties(blender-thumbnailer PROPERTIES MACOSX_BUNDLE TRUE)
+ endif()
endif()
if(WIN32)
@@ -438,6 +441,9 @@ elseif(APPLE)
set(TARGETDIR_SITE_PACKAGES "${TARGETDIR_VER}/python/lib/python${PYTHON_VERSION}/site-packages")
# Skip re-linking on CPACK / install.
set_target_properties(blender PROPERTIES BUILD_WITH_INSTALL_RPATH true)
+ if(WITH_BLENDER_THUMBNAILER)
+ set_target_properties(blender-thumbnailer PROPERTIES BUILD_WITH_INSTALL_RPATH true)
+ endif()
endif()
@@ -1581,6 +1587,16 @@ elseif(APPLE)
MACOSX_BUNDLE_LONG_VERSION_STRING "${BLENDER_VERSION}.${BLENDER_VERSION_PATCH} ${BLENDER_DATE}"
)
+ if(WITH_BLENDER_THUMBNAILER)
+ set(OSX_THUMBNAILER_SOURCEDIR ${OSX_APP_SOURCEDIR}/Contents/PlugIns/blender-thumbnailer.appex)
+ set_target_properties(blender-thumbnailer PROPERTIES
+ BUNDLE_EXTENSION appex
+ MACOSX_BUNDLE_INFO_PLIST ${OSX_THUMBNAILER_SOURCEDIR}/Contents/Info.plist
+ MACOSX_BUNDLE_SHORT_VERSION_STRING "${BLENDER_VERSION}.${BLENDER_VERSION_PATCH}"
+ MACOSX_BUNDLE_LONG_VERSION_STRING "${BLENDER_VERSION}.${BLENDER_VERSION_PATCH} ${BLENDER_DATE}"
+ )
+ endif()
+
# Gather the date in finder-style.
execute_process(
COMMAND date "+%m/%d/%Y/%H:%M"
@@ -1617,7 +1633,7 @@ elseif(APPLE)
if(WITH_BLENDER_THUMBNAILER)
install(
TARGETS blender-thumbnailer
- DESTINATION "Blender.app/Contents/MacOS"
+ DESTINATION "./Blender.app/Contents/PlugIns"
)
endif()