macOS/QuickLook: support rich thumbnail #107072

Open
Ankit Meel wants to merge 10 commits from ankitm/blender:ankitm/2ql into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
6 changed files with 277 additions and 2 deletions

View File

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

Is this required, and common for these kinds of thumbnails generators? I don't see it for any other apps, but maybe I just happen to not have any apps that do this.

Is this required, and common for these kinds of thumbnails generators? I don't see it for any other apps, but maybe I just happen to not have any apps that do this.

Not required and not sure about commonness. But since there's no other way to disable it, I added it.

I could remove the key from plist and let the system decide.

Not required and not sure about commonness. But since there's no other way to disable it, I added it. I could remove the key from plist and let the system decide.

I'd leave it out and let the system decide.

I'd leave it out and let the system decide.
</dict>
</plist>

View File

@ -0,0 +1,11 @@
<?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/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
ankitm marked this conversation as resolved Outdated

Inconsistent indentation.

Inconsistent indentation.
</dict>
</plist>

View File

@ -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)
# -----------------------------------------------------------------------------
ankitm marked this conversation as resolved Outdated

Code is more clearly mutually exclusive if you use:

if(WIN32)
..
elseif(APPLE)
...
elseif(UNIX)
...
endif()
Code is more clearly mutually exclusive if you use: ``` if(WIN32) .. elseif(APPLE) ... elseif(UNIX) ... endif() ```
# 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

View File

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

View File

@ -0,0 +1,167 @@
/* SPDX-License-Identifier: GPL-2.0-or-later */
#include <AppKit/NSImage.h>
#include <CoreGraphics/CGDataProvider.h>
#include <CoreGraphics/CoreGraphics.h>
#include <Foundation/Foundation.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. 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<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()];
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

View File

@ -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")
ankitm marked this conversation as resolved Outdated

Not using relative path is more clear I think, their relative location has no particular importance: ${CMAKE_SOURCE_DIR}/release/darwin/thumbnail_entitlements.plist.

Don't invent new BL_ prefix for variable names, suggest to use THUMBNAIL_ENTITLEMENTS

Not using relative path is more clear I think, their relative location has no particular importance: `${CMAKE_SOURCE_DIR}/release/darwin/thumbnail_entitlements.plist`. Don't invent new `BL_` prefix for variable names, suggest to use `THUMBNAIL_ENTITLEMENTS`
install(CODE

Does this now run on every make install? That would slow down incremental builds. Is there a way to make it run only when the appex is updated?

Does this now run on every `make install`? That would slow down incremental builds. Is there a way to make it run only when the appex is updated?
Review
time codesign --entitlements release/darwin/thumbnail_entitlements.plist --force --deep --sign - ../build_darwin_debug_lite/bin/Blender.app/Contents/Plugins/blender-thumbnailer.appex
../build_darwin_debug_lite/bin/Blender.app/Contents/Plugins/blender-thumbnailer.appex: replacing existing signature
codesign --entitlements release/darwin/thumbnail_entitlements.plist --force    0.02s user 0.02s system 72% cpu 0.053 total

insignificant
Even my poor machine makes it unnoticeable. We aren't signing the full blender.app.

``` time codesign --entitlements release/darwin/thumbnail_entitlements.plist --force --deep --sign - ../build_darwin_debug_lite/bin/Blender.app/Contents/Plugins/blender-thumbnailer.appex ../build_darwin_debug_lite/bin/Blender.app/Contents/Plugins/blender-thumbnailer.appex: replacing existing signature codesign --entitlements release/darwin/thumbnail_entitlements.plist --force 0.02s user 0.02s system 72% cpu 0.053 total ``` insignificant Even my poor machine makes it unnoticeable. We aren't signing the full blender.app.
"execute_process(COMMAND codesign
--deep --force --sign - --entitlements \"${THUMBNAIL_ENTITLEMENTS}\" --timestamp=none
\"${EXECUTABLE_OUTPUT_PATH}/Blender.app/Contents/Plugins/blender-thumbnailer.appex\"
)"
)
endif()