mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
macOS/QuickLook: support rich thumbnail in Finder
Support thumbnail that shows the file contents instead of the default blend file icon for all files in Finder. Some files may still have the usual blender icon as thumbnail depending on their contents. blender-thumbnailer process is kept alive by the system in the background and is invoked by QuickLook when needed. The checkbox to disable the extension is present in the Settings app. It will NOT be an interactive "Preview" that allows richer interactions like Panning viewport, or rotating 3D objects.
This commit is contained in:
parent
92cd4ca295
commit
b858319dd3
8 changed files with 305 additions and 12 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>blender-thumbnailer</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Blender Thumbnailer</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>blender-thumbnailer</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.blenderfoundation.blender.thumbnailer</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>${MACOSX_BUNDLE_LONG_VERSION_STRING}, Blender Foundation</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>QLSupportedContentTypes</key>
|
||||
<array>
|
||||
<!-- The supported file UTIs. Not inherited from parent bundle. -->
|
||||
<string>org.blenderfoundation.blender.file</string>
|
||||
</array>
|
||||
<key>QLThumbnailMinimumDimension</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.quicklook.thumbnail</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<!-- Must be the same as the class implementing the reply method. -->
|
||||
<string>ThumbnailProvider</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
12
release/darwin/thumbnailer_entitlements.plist
Normal file
12
release/darwin/thumbnailer_entitlements.plist
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- Extension must be codesigned even locally. -->
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<!-- XCode adds this by default to a QuickLook extension. -->
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
@ -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 $<TARGET_FILE:blender-thumbnailer>
|
||||
)
|
||||
# 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 $<TARGET_BUNDLE_DIR:blender-thumbnailer>
|
||||
)
|
||||
elseif(UNIX)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Build `blender-thumbnailer` executable
|
||||
|
||||
|
|
|
|||
12
source/blender/blendthumb/src/thumbnail_provider.h
Normal file
12
source/blender/blendthumb/src/thumbnail_provider.h
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
/* 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
|
||||
174
source/blender/blendthumb/src/thumbnail_provider.mm
Normal file
174
source/blender/blendthumb/src/thumbnail_provider.mm
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/* 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 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<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 *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
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue