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..9243cedd20c --- /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/thumbnail_entitlements.plist b/release/darwin/thumbnail_entitlements.plist new file mode 100644 index 00000000000..5401397cffb --- /dev/null +++ b/release/darwin/thumbnail_entitlements.plist @@ -0,0 +1,11 @@ + + + + + + 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 feec85d9056..17b574f4fbf 100644 --- a/source/blender/blendthumb/CMakeLists.txt +++ b/source/blender/blendthumb/CMakeLists.txt @@ -39,7 +39,23 @@ if(WIN32) target_link_libraries(BlendThumb bf_blenlib dbghelp.lib Version.lib) set_target_properties(BlendThumb PROPERTIES LINK_FLAGS_DEBUG "/NODEFAULTLIB:msvcrt") -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) + target_link_libraries(blender-thumbnailer + bf_blenlib + # Avoid linker error about undefined _main symbol. + "-e _NSExtensionMain" + "-framework QuickLookThumbnailing" + ) +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..2f3718aa5a2 --- /dev/null +++ b/source/blender/blendthumb/src/thumbnail_provider.h @@ -0,0 +1,14 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +#pragma once + +#include + +#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..a228b38e712 --- /dev/null +++ b/source/blender/blendthumb/src/thumbnail_provider.mm @@ -0,0 +1,167 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include +#include +#include +#include + +#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. See the template Info.plist + * under release/darwin for more info. + * + * # 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 even to run the plugin locally. + * com.apple.security.get-task-allow 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 + * + * # 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 + * - Is it registered with lsregister and there isn't a conflict with another plugin taking + * precedence? `lsregister -dump | grep blender-thumbnailer.appex` + * - Is it codesigned and sandboxed? + * `codesign -d --entitlements ent.plist -f -s - /path/to/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 /path/to/file.blend + * - qlmanage -t -s 512 -o /tmp/ /path/to/file.blend + */ + +class FileDescriptorRAII : blender::NonCopyable, blender::NonMovable { + int src_file_ = -1; + + public: + explicit FileDescriptorRAII(const char *file_path) + { + src_file_ = BLI_open(file_path, O_BINARY | O_RDONLY, 0); + } + + ~FileDescriptorRAII() + { + if (good()) { + close(src_file_); + } + } + + bool good() + { + return src_file_ != -1; + } + + int get() + { + return src_file_; + } +}; + +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()]; + CGDataProviderRef provider = CGDataProviderCreateWithCFData((CFDataRef)ns_data); + CGColorRenderingIntent intent = kCGRenderingIntentDefault; + bool should_interpolate = true; + CGFloat *decode = nullptr; + CGImageRef image_ref = CGImageCreateWithPNGDataProvider( + provider, decode, should_interpolate, intent); + NSImage *ns_image = [[NSImage alloc] initWithCGImage:image_ref size:NSZeroSize]; + CGImageRelease(image_ref); + CGDataProviderRelease(provider); + 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.UTF8String, &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)]; + return YES; + }], + nil); + } +} + +@end diff --git a/source/creator/CMakeLists.txt b/source/creator/CMakeLists.txt index 3462fcd5b99..25ad8b119b3 100644 --- a/source/creator/CMakeLists.txt +++ b/source/creator/CMakeLists.txt @@ -1317,6 +1317,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" @@ -1352,7 +1362,14 @@ elseif(APPLE) if(WITH_BLENDER_THUMBNAILER) install( TARGETS blender-thumbnailer - DESTINATION Blender.app/Contents/MacOS/ + DESTINATION Blender.app/Contents/Plugins + ) + set(THUMBNAIL_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/release/darwin/thumbnail_entitlements.plist") + install(CODE + "execute_process(COMMAND codesign + --deep --force --sign - --entitlements \"${THUMBNAIL_ENTITLEMENTS}\" --timestamp=none + \"${EXECUTABLE_OUTPUT_PATH}/Blender.app/Contents/Plugins/blender-thumbnailer.appex\" + )" ) endif()