macOS/QuickLook: support rich thumbnail #107072
|
@ -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>
|
|
@ -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
Brecht Van Lommel
commented
Inconsistent indentation. Inconsistent indentation.
|
|||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -39,7 +39,23 @@ if(WIN32)
|
||||||
target_link_libraries(BlendThumb bf_blenlib dbghelp.lib Version.lib)
|
target_link_libraries(BlendThumb bf_blenlib dbghelp.lib Version.lib)
|
||||||
set_target_properties(BlendThumb PROPERTIES LINK_FLAGS_DEBUG "/NODEFAULTLIB:msvcrt")
|
set_target_properties(BlendThumb PROPERTIES LINK_FLAGS_DEBUG "/NODEFAULTLIB:msvcrt")
|
||||||
|
|
||||||
else()
|
elseif(APPLE)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
ankitm marked this conversation as resolved
Outdated
Brecht Van Lommel
commented
Code is more clearly mutually exclusive if you use:
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
|
# Build `blender-thumbnailer` executable
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1317,6 +1317,16 @@ elseif(APPLE)
|
||||||
MACOSX_BUNDLE_LONG_VERSION_STRING "${BLENDER_VERSION}.${BLENDER_VERSION_PATCH} ${BLENDER_DATE}"
|
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.
|
# Gather the date in finder-style.
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND date "+%m/%d/%Y/%H:%M"
|
COMMAND date "+%m/%d/%Y/%H:%M"
|
||||||
|
@ -1352,7 +1362,14 @@ elseif(APPLE)
|
||||||
if(WITH_BLENDER_THUMBNAILER)
|
if(WITH_BLENDER_THUMBNAILER)
|
||||||
install(
|
install(
|
||||||
TARGETS blender-thumbnailer
|
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
Brecht Van Lommel
commented
Not using relative path is more clear I think, their relative location has no particular importance: Don't invent new 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
|
||||||
Brecht Van Lommel
commented
Does this now run on every 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?
Ankit Meel
commented
insignificant ```
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()
|
endif()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
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.
I'd leave it out and let the system decide.