From f1fb5699839dbbcaeaa32fa0ddac74b2b1e84fa7 Mon Sep 17 00:00:00 2001 From: Gaspar Capello Date: Wed, 18 Jun 2025 15:23:41 -0300 Subject: [PATCH] Fix thumbnails for macOS Sequoia (fix #5009) --- src/desktop/osx/CMakeLists.txt | 36 +-- src/desktop/osx/Info.plist | 58 ----- src/desktop/osx/app/Info.plist | 75 ++++++ src/desktop/osx/app/Info.plist.in | 51 ++++ src/desktop/osx/app/aseprite.entitlements | 18 ++ src/desktop/osx/appex/Info.plist | 34 +++ src/desktop/osx/appex/Info.plist.in | 34 +++ .../thumbnails.entitlements} | 6 +- .../osx/{thumbnail.mm => appex/thumbnails.mm} | 87 +++++-- src/desktop/osx/main.mm | 234 ------------------ src/desktop/osx/thumbnail.h | 19 -- 11 files changed, 283 insertions(+), 369 deletions(-) delete mode 100644 src/desktop/osx/Info.plist create mode 100644 src/desktop/osx/app/Info.plist create mode 100644 src/desktop/osx/app/Info.plist.in create mode 100644 src/desktop/osx/app/aseprite.entitlements create mode 100644 src/desktop/osx/appex/Info.plist create mode 100644 src/desktop/osx/appex/Info.plist.in rename src/desktop/osx/{AsepriteThumbnailer.entitlements => appex/thumbnails.entitlements} (58%) rename src/desktop/osx/{thumbnail.mm => appex/thumbnails.mm} (51%) delete mode 100644 src/desktop/osx/main.mm delete mode 100644 src/desktop/osx/thumbnail.h diff --git a/src/desktop/osx/CMakeLists.txt b/src/desktop/osx/CMakeLists.txt index 8e806ee9f..83cfa73be 100644 --- a/src/desktop/osx/CMakeLists.txt +++ b/src/desktop/osx/CMakeLists.txt @@ -1,37 +1,13 @@ # Desktop Integration -# Copyright (c) 2022 Igara Studio S.A. +# Copyright (c) 2022-2025 Igara Studio S.A. -find_library(QUARTZ_LIBRARY Quartz) +find_library(QUICKLOOK_LIBRARY QuickLookThumbnailing) -add_library(AsepriteThumbnailer SHARED - main.mm - thumbnail.mm) +add_library(AsepriteThumbnailer MODULE + appex/thumbnails.mm) target_link_libraries(AsepriteThumbnailer - laf-base dio-lib render-lib - ${QUARTZ_LIBRARY}) - -set_target_properties(AsepriteThumbnailer PROPERTIES - FRAMEWORK TRUE - MACOSX_FRAMEWORK_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist") - -add_custom_command( - OUTPUT ${CMAKE_BINARY_DIR}/lib/AsepriteThumbnailer.qlgenerator - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - COMMAND ${CMAKE_COMMAND} -E make_directory lib/AsepriteThumbnailer.qlgenerator - COMMAND ${CMAKE_COMMAND} -E make_directory lib/AsepriteThumbnailer.qlgenerator/Contents - COMMAND ${CMAKE_COMMAND} -E make_directory lib/AsepriteThumbnailer.qlgenerator/Contents/MacOS - COMMAND ${CMAKE_COMMAND} -E copy - lib/AsepriteThumbnailer.framework/Versions/A/AsepriteThumbnailer - lib/AsepriteThumbnailer.qlgenerator/Contents/MacOS - COMMAND ${CMAKE_COMMAND} -E copy - lib/AsepriteThumbnailer.framework/Versions/A/Resources/Info.plist - lib/AsepriteThumbnailer.qlgenerator/Contents - BYPRODUCTS ${CMAKE_BINARY_DIR}/lib/AsepriteThumbnailer.qlgenerator/Contents/MacOS/AsepriteThumbnailer - ${CMAKE_BINARY_DIR}/lib/AsepriteThumbnailer.qlgenerator/Contents/Info.plist - DEPENDS AsepriteThumbnailer) - -add_custom_target(AsepriteThumbnailer.qlgenerator - DEPENDS ${CMAKE_BINARY_DIR}/lib/AsepriteThumbnailer.qlgenerator) + ${QUICKLOOK_LIBRARY} + ) diff --git a/src/desktop/osx/Info.plist b/src/desktop/osx/Info.plist deleted file mode 100644 index 968caa461..000000000 --- a/src/desktop/osx/Info.plist +++ /dev/null @@ -1,58 +0,0 @@ - - - - - CFBundleDocumentTypes - - - CFBundleTypeRole - QLGenerator - LSItemContentTypes - - dyn.ah62d4rv4ge80c65f - dyn.ah62d4rv4ge80c65fsb3gw7df - - - - CFBundleExecutable - AsepriteThumbnailer - CFBundleIdentifier - org.aseprite.AsepriteThumbnailer - CFBundleName - AsepriteThumbnailer - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - XPC! - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1.0 - CFPlugInFactories - - A5E9417E-6E7A-4B2D-85A4-84E114D7A960 - QuickLookGeneratorPluginFactory - - CFPlugInTypes - - 5E2D9680-5022-40FA-B806-43349622E5B9 - - A5E9417E-6E7A-4B2D-85A4-84E114D7A960 - - - CFPlugInUnloadFunction - - QLThumbnailMinimumSize - 32 - QLPreviewWidth - 256 - QLPreviewHeight - 256 - QLNeedsToBeRunInMainThread - - QLSupportsConcurrentRequests - - NSHumanReadableCopyright - Copyright © Igara Studio S.A. All rights reserved. - - diff --git a/src/desktop/osx/app/Info.plist b/src/desktop/osx/app/Info.plist new file mode 100644 index 000000000..345577eb5 --- /dev/null +++ b/src/desktop/osx/app/Info.plist @@ -0,0 +1,75 @@ + + + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + org.aseprite.aseprite.sprite + UTTypeDescription + Aseprite Sprite + UTTypeConformsTo + + public.image + + UTTypeTagSpecification + + public.filename-extension + + aseprite + ase + + public.mime-type + image/aseprite + + + + CFBundleIdentifier + org.aseprite.aseprite + CFBundleVersion + 1.3.dev + CFBundleShortVersionString + 1.3.dev + BuildMachineOSBuild + 24C101 + CFBundleDevelopmentRegion + English + CFBundleExecutable + aseprite + CFBundleInfoDictionaryVersion + 6.0 + CFBundleLongVersionString + 1.3.dev + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleSupportedPlatforms + + MacOSX + + CSResourcesFileMapped + + LSHasPlugIns + + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTPlatformBuild + 24C94 + DTPlatformName + macosx + DTPlatformVersion + 15.2 + DTSDKBuild + 24C94 + DTSDKName + macosx15.2 + DTXcode + 1620 + DTXcodeBuild + 16C5032a + LSMinimumSystemVersion + 10.15 + + diff --git a/src/desktop/osx/app/Info.plist.in b/src/desktop/osx/app/Info.plist.in new file mode 100644 index 000000000..b1c5d8a69 --- /dev/null +++ b/src/desktop/osx/app/Info.plist.in @@ -0,0 +1,51 @@ + + + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + $ASEPRITE_BUNDLE_ID.sprite + UTTypeDescription + Aseprite Sprite + UTTypeConformsTo + + public.image + + UTTypeTagSpecification + + public.filename-extension + + aseprite + ase + + public.mime-type + image/aseprite + + + + CFBundleIdentifier + $ASEPRITE_BUNDLE_ID + CFBundleVersion + $VERSION + CFBundleShortVersionString + $VERSION + CFBundleExecutable + aseprite + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleSupportedPlatforms + + MacOSX + + CSResourcesFileMapped + + LSHasPlugIns + + LSMinimumSystemVersion + 10.15 + + diff --git a/src/desktop/osx/app/aseprite.entitlements b/src/desktop/osx/app/aseprite.entitlements new file mode 100644 index 000000000..d39d71ef5 --- /dev/null +++ b/src/desktop/osx/app/aseprite.entitlements @@ -0,0 +1,18 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-write + + com.apple.security.files.download.read-write + + com.apple.security.files.pictures.read-write + + com.apple.security.files.movies.read-write + + com.apple.security.files.music.read-write + + + diff --git a/src/desktop/osx/appex/Info.plist b/src/desktop/osx/appex/Info.plist new file mode 100644 index 000000000..f75f1d4f2 --- /dev/null +++ b/src/desktop/osx/appex/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDisplayName + AsepriteThumbnailer + CFBundleExecutable + AsepriteThumbnailer + CFBundleIdentifier + org.aseprite.aseprite.AsepriteThumbnailer + CFBundlePackageType + XPC! + CFBundleVersion + 1.3.dev + CFBundleShortVersionString + 1.3.dev + NSExtension + + NSExtensionAttributes + + QLSupportedContentTypes + + org.aseprite.aseprite.sprite + + QLThumbnailMinimumDimension + 32 + + NSExtensionPointIdentifier + com.apple.quicklook.thumbnail + NSExtensionPrincipalClass + ThumbnailProvider + + + diff --git a/src/desktop/osx/appex/Info.plist.in b/src/desktop/osx/appex/Info.plist.in new file mode 100644 index 000000000..d73542d00 --- /dev/null +++ b/src/desktop/osx/appex/Info.plist.in @@ -0,0 +1,34 @@ + + + + + CFBundleDisplayName + AsepriteThumbnailer + CFBundleExecutable + AsepriteThumbnailer + CFBundleIdentifier + ${THUMBNAILS_BUNDLE_ID} + CFBundlePackageType + XPC! + CFBundleVersion + ${VERSION} + CFBundleShortVersionString + ${VERSION} + NSExtension + + NSExtensionAttributes + + QLSupportedContentTypes + + ${ASEPRITE_BUNDLE_ID}.sprite + + QLThumbnailMinimumDimension + 32 + + NSExtensionPointIdentifier + com.apple.quicklook.thumbnail + NSExtensionPrincipalClass + ThumbnailProvider + + + diff --git a/src/desktop/osx/AsepriteThumbnailer.entitlements b/src/desktop/osx/appex/thumbnails.entitlements similarity index 58% rename from src/desktop/osx/AsepriteThumbnailer.entitlements rename to src/desktop/osx/appex/thumbnails.entitlements index f2ef3ae02..cd92d1bca 100644 --- a/src/desktop/osx/AsepriteThumbnailer.entitlements +++ b/src/desktop/osx/appex/thumbnails.entitlements @@ -2,9 +2,7 @@ - com.apple.security.app-sandbox - - com.apple.security.files.user-selected.read-only - + com.apple.security.app-sandbox + diff --git a/src/desktop/osx/thumbnail.mm b/src/desktop/osx/appex/thumbnails.mm similarity index 51% rename from src/desktop/osx/thumbnail.mm rename to src/desktop/osx/appex/thumbnails.mm index e7f2ff123..e1745a071 100644 --- a/src/desktop/osx/thumbnail.mm +++ b/src/desktop/osx/appex/thumbnails.mm @@ -1,11 +1,10 @@ -// Desktop Integration -// Copyright (c) 2022 Igara Studio S.A. +// Aseprite +// Copyright (c) 2025 Igara Studio S.A. // -// This file is released under the terms of the MIT license. -// Read LICENSE.txt for more information. - -#include "thumbnail.h" +// This program is distributed under the terms of +// the End-User License Agreement for Aseprite. +#include "app/file_system.h" #include "dio/decode_delegate.h" #include "dio/decode_file.h" #include "dio/file_interface.h" @@ -13,10 +12,7 @@ #include "render/render.h" #include - -#include - -namespace desktop { +#include namespace { @@ -38,13 +34,13 @@ class StreamAdaptor : public dio::FileInterface { public: StreamAdaptor(NSData* data) : m_data(data), m_ok(m_data != nullptr), m_pos(0) {} - bool ok() const { return m_ok; } + bool ok() const override { return m_ok; } - size_t tell() { return m_pos; } + size_t tell() override { return m_pos; } - void seek(size_t absPos) { m_pos = absPos; } + void seek(size_t absPos) override { m_pos = absPos; } - uint8_t read8() + uint8_t read8() override { if (!m_ok) return 0; @@ -57,7 +53,7 @@ public: } } - size_t readBytes(uint8_t* buf, size_t n) + size_t readBytes(uint8_t* buf, size_t n) override { if (!m_ok) return 0; @@ -75,21 +71,16 @@ public: } } - void write8(uint8_t value) - { - // Do nothing, we don't write in the file - } + void write8(uint8_t value) override {}; NSData* m_data; bool m_ok; size_t m_pos; }; -} // anonymous namespace - -CGImageRef get_thumbnail(CFURLRef url, CFDictionaryRef options, CGSize maxSize) +CGImageRef get_thumbnail(CFURLRef url, CGSize maxSize) { - auto data = [[NSData alloc] initWithContentsOfURL:(NSURL*)url]; + auto data = [[NSData alloc] initWithContentsOfURL:(__bridge NSURL*)url]; if (!data) return nullptr; @@ -152,4 +143,52 @@ CGImageRef get_thumbnail(CFURLRef url, CFDictionaryRef options, CGSize maxSize) return img; } -} // namespace desktop +} // namespace + +@interface ThumbnailProvider : QLThumbnailProvider +@end + +@implementation ThumbnailProvider + +- (void)provideThumbnailForFileRequest:(QLFileThumbnailRequest*)request + completionHandler: + (void (^)(QLThumbnailReply* _Nullable, NSError* _Nullable))handler +{ + CFURLRef url = (__bridge CFURLRef)(request.fileURL); + CGSize maxSize = request.maximumSize; + CGImageRef cgImage = get_thumbnail(url, maxSize); + if (!cgImage) { + handler(nil, nil); + return; + } + CGSize imageSize = CGSizeMake(CGImageGetWidth(cgImage), CGImageGetHeight(cgImage)); + handler([QLThumbnailReply replyWithContextSize:maxSize + currentContextDrawingBlock:^BOOL { + CGContextRef ctx = [[NSGraphicsContext currentContext] CGContext]; + CGContextSaveGState(ctx); + CGFloat scale = MIN(maxSize.width / imageSize.width, + maxSize.height / imageSize.height); + CGSize scaledSize = CGSizeMake(imageSize.width * scale, + imageSize.height * scale); + CGRect drawRect = CGRectMake((maxSize.width - scaledSize.width) / 2, + (maxSize.height - scaledSize.height) / 2, + scaledSize.width, + scaledSize.height); + CGContextDrawImage(ctx, drawRect, cgImage); + CGContextRestoreGState(ctx); + CGImageRelease(cgImage); + return YES; + }], + nil); +} + +@end + +// This constructor ensures that the ThumbnailProvider class is explicitly referenced +// during linking. Without this, the linker may discard the symbol, preventing the QuickLook +// from discovering the provider. This is required to ensure the symbol +// OBJC_CLASS_$_ThumbnailProvider is exported correctly from the .appex binary. +__attribute__((constructor)) static void forceExportThumbnailProvider() +{ + [ThumbnailProvider class]; +} diff --git a/src/desktop/osx/main.mm b/src/desktop/osx/main.mm deleted file mode 100644 index dd85779c6..000000000 --- a/src/desktop/osx/main.mm +++ /dev/null @@ -1,234 +0,0 @@ -// Desktop Integration -// Copyright (c) 2022 Igara Studio S.A. -// -// This file is released under the terms of the MIT license. -// Read LICENSE.txt for more information. - -#include -#include -#include -#include - -#include "base/debug.h" -#include "thumbnail.h" - -// Just as a side note: We're using the same UUID as the Windows -// Aseprite thumbnailer. -// -// If you're going to use this code, remember to change this UUID and -// change it in the Info.plist file. -#define PLUGIN_ID "A5E9417E-6E7A-4B2D-85A4-84E114D7A960" - -static HRESULT Plugin_QueryInterface(void*, REFIID, LPVOID*); -static ULONG Plugin_AddRef(void*); -static ULONG Plugin_Release(void*); -static OSStatus Plugin_GenerateThumbnailForURL(void*, - QLThumbnailRequestRef, - CFURLRef, - CFStringRef, - CFDictionaryRef, - CGSize); -static void Plugin_CancelThumbnailGeneration(void*, QLThumbnailRequestRef); -static OSStatus Plugin_GeneratePreviewForURL(void*, - QLPreviewRequestRef, - CFURLRef, - CFStringRef, - CFDictionaryRef); -static void Plugin_CancelPreviewGeneration(void*, QLPreviewRequestRef); - -static QLGeneratorInterfaceStruct Plugin_vtbl = { // kQLGeneratorTypeID interface - // IUnknown - nullptr, // void* reserved - Plugin_QueryInterface, - Plugin_AddRef, - Plugin_Release, - // QLGeneratorInterface - Plugin_GenerateThumbnailForURL, - Plugin_CancelThumbnailGeneration, - Plugin_GeneratePreviewForURL, - Plugin_CancelPreviewGeneration -}; - -// TODO it would be nice to create a C++ smart pointer/wrapper for CFUUIDRef type - -struct Plugin { - QLGeneratorInterfaceStruct* interface; // Must be a pointer - CFUUIDRef factoryID; - ULONG refCount = 1; // Starts with one reference when it's created - - Plugin(CFUUIDRef factoryID) - : interface(new QLGeneratorInterfaceStruct(Plugin_vtbl)) - , factoryID(factoryID) - { - CFPlugInAddInstanceForFactory(factoryID); - } - - ~Plugin() - { - delete interface; - if (factoryID) { - CFPlugInRemoveInstanceForFactory(factoryID); - CFRelease(factoryID); - } - } - - // IUnknown impl - - HRESULT QueryInterface(REFIID iid, LPVOID* ppv) - { - CFUUIDRef interfaceID = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault, iid); - - if (CFEqual(interfaceID, kQLGeneratorCallbacksInterfaceID)) { - *ppv = this; - AddRef(); - CFRelease(interfaceID); - return S_OK; - } - else { - *ppv = nullptr; - CFRelease(interfaceID); - return E_NOINTERFACE; - } - } - - ULONG AddRef() { return ++refCount; } - - ULONG Release() - { - if (refCount == 1) { - delete this; - return 0; - } - else { - ASSERT(refCount != 0); - return --refCount; - } - } - - // QLGeneratorInterfaceStruct impl - - static OSStatus GenerateThumbnailForURL(QLThumbnailRequestRef thumbnail, - CFURLRef url, - CFStringRef contentTypeUTI, - CFDictionaryRef options, - CGSize maxSize) - { - CGImageRef image = desktop::get_thumbnail(url, options, maxSize); - if (!image) - return -1; - - QLThumbnailRequestSetImage(thumbnail, image, nullptr); - CGImageRelease(image); - return 0; - } - - static void CancelThumbnailGeneration(QLThumbnailRequestRef thumbnail) - { - // TODO - } - - OSStatus GeneratePreviewForURL(QLPreviewRequestRef preview, - CFURLRef url, - CFStringRef contentTypeUTI, - CFDictionaryRef options) - { - CGImageRef image = desktop::get_thumbnail(url, options, CGSizeMake(0, 0)); - if (!image) - return -1; - - int w = CGImageGetWidth(image); - int h = CGImageGetHeight(image); - int wh = std::min(w, h); - if (wh < 128) { - w = 128 * w / wh; - h = 128 * h / wh; - } - - CGContextRef cg = QLPreviewRequestCreateContext(preview, CGSizeMake(w, h), YES, options); - CGContextSetInterpolationQuality(cg, kCGInterpolationNone); - CGContextDrawImage(cg, CGRectMake(0, 0, w, h), image); - QLPreviewRequestFlushContext(preview, cg); - CGImageRelease(image); - CGContextRelease(cg); - return 0; - } - - void CancelPreviewGeneration(QLPreviewRequestRef preview) - { - // TODO - } -}; - -static HRESULT Plugin_QueryInterface(void* p, REFIID iid, LPVOID* ppv) -{ - ASSERT(p); - return reinterpret_cast(p)->QueryInterface(iid, ppv); -} - -static ULONG Plugin_AddRef(void* p) -{ - ASSERT(p); - return reinterpret_cast(p)->AddRef(); -} - -static ULONG Plugin_Release(void* p) -{ - ASSERT(p); - return reinterpret_cast(p)->Release(); -} - -static OSStatus Plugin_GenerateThumbnailForURL(void* p, - QLThumbnailRequestRef thumbnail, - CFURLRef url, - CFStringRef contentTypeUTI, - CFDictionaryRef options, - CGSize maxSize) -{ - ASSERT(p); - return reinterpret_cast(p)->GenerateThumbnailForURL(thumbnail, - url, - contentTypeUTI, - options, - maxSize); -} - -static void Plugin_CancelThumbnailGeneration(void* p, QLThumbnailRequestRef thumbnail) -{ - ASSERT(p); - reinterpret_cast(p)->CancelThumbnailGeneration(thumbnail); -} - -static OSStatus Plugin_GeneratePreviewForURL(void* p, - QLPreviewRequestRef preview, - CFURLRef url, - CFStringRef contentTypeUTI, - CFDictionaryRef options) -{ - ASSERT(p); - return reinterpret_cast(p)->GeneratePreviewForURL(preview, url, contentTypeUTI, options); -} - -static void Plugin_CancelPreviewGeneration(void* p, QLPreviewRequestRef preview) -{ - ASSERT(p); - reinterpret_cast(p)->CancelPreviewGeneration(preview); -} - -// This is the only public entry point of the framework/plugin (the -// "QuickLookGeneratorPluginFactory" name is specified in the -// Info.list file): the factory of objects. Similar than the Win32 COM -// IClassFactory::CreateInstance() -// -// This function is used to create an instance of an object of -// kQLGeneratorTypeID type, which should implement the -// QLGeneratorInterfaceStruct interface. -extern "C" void* QuickLookGeneratorPluginFactory(CFAllocatorRef allocator, CFUUIDRef typeID) -{ - if (CFEqual(typeID, kQLGeneratorTypeID)) { - CFUUIDRef uuid = CFUUIDCreateFromString(kCFAllocatorDefault, CFSTR(PLUGIN_ID)); - auto plugin = new Plugin(uuid); - CFRelease(uuid); - return plugin; - } - return nullptr; // Unknown typeID -} diff --git a/src/desktop/osx/thumbnail.h b/src/desktop/osx/thumbnail.h deleted file mode 100644 index 15669a919..000000000 --- a/src/desktop/osx/thumbnail.h +++ /dev/null @@ -1,19 +0,0 @@ -// Desktop Integration -// Copyright (c) 2022 Igara Studio S.A. -// -// This file is released under the terms of the MIT license. -// Read LICENSE.txt for more information. - -#ifndef DESKTOP_THUMBNAIL_H_INCLUDED -#define DESKTOP_THUMBNAIL_H_INCLUDED - -#include -#include - -namespace desktop { - -CGImageRef get_thumbnail(CFURLRef url, CFDictionaryRef options, CGSize maxSize); - -} // namespace desktop - -#endif