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()