From 57f32e2dd0fe032f3ac6013f40a28bde50d072ea Mon Sep 17 00:00:00 2001 From: Gaspar Capello Date: Thu, 31 Jul 2025 10:52:32 -0300 Subject: [PATCH] Fix thumbnails for macOS Sequoia (fix #5009) --- src/CMakeLists.txt | 24 +++- src/desktop/osx/CMakeLists.txt | 40 ++---- src/desktop/osx/Info.plist | 59 +++------ src/desktop/osx/main.mm | 234 --------------------------------- src/desktop/osx/thumbnail.h | 19 --- src/desktop/osx/thumbnail.mm | 64 +++++++-- 6 files changed, 105 insertions(+), 335 deletions(-) delete mode 100644 src/desktop/osx/main.mm delete mode 100644 src/desktop/osx/thumbnail.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ae600d500..0b861e549 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -97,6 +97,13 @@ add_subdirectory(app) set(DATA_OUTPUT_DIR ${CMAKE_BINARY_DIR}/bin/data) +if(ENABLE_ASEPRITE_EXE) + set(main_target aseprite) + if(APPLE) + set(DATA_OUTPUT_DIR ${CMAKE_BINARY_DIR}/bin/${main_target}.app/Contents/Resources/data) + endif() +endif() + ###################################################################### # Clone "strings" repo with translations into bin/data/strings.git @@ -176,15 +183,20 @@ add_custom_target(copy_data DEPENDS clone_strings ${out_data_files}) # Aseprite application if(ENABLE_ASEPRITE_EXE) - set(main_target aseprite) - if(WIN32) set(main_resources main/win/resources_win32.rc main/win/settings.manifest) endif() - add_executable(${main_target} + set(BUNDLE_PARAM "") + set(INSTALL_OPT RUNTIME) + if(APPLE) + set(BUNDLE_PARAM MACOSX_BUNDLE) + set(INSTALL_OPT BUNDLE) + endif() + + add_executable(${main_target} ${BUNDLE_PARAM} main/main.cpp ${main_resources}) if(LAF_BACKEND STREQUAL "skia") @@ -193,6 +205,10 @@ if(ENABLE_ASEPRITE_EXE) endif() endif() set_target_properties(${main_target} PROPERTIES LINK_FLAGS "${LAF_BACKEND_LINK_FLAGS}") + if(APPLE) + set_target_properties(${main_target} PROPERTIES + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/main/osx/Info.plist") + endif() target_link_libraries(${main_target} app-lib) add_dependencies(${main_target} copy_data) @@ -202,7 +218,7 @@ if(ENABLE_ASEPRITE_EXE) endif() install(TARGETS ${main_target} - RUNTIME DESTINATION bin) + ${INSTALL_OPT} DESTINATION bin) install(DIRECTORY ../data DESTINATION share/aseprite) diff --git a/src/desktop/osx/CMakeLists.txt b/src/desktop/osx/CMakeLists.txt index 8e806ee9f..1bbd16c0a 100644 --- a/src/desktop/osx/CMakeLists.txt +++ b/src/desktop/osx/CMakeLists.txt @@ -1,37 +1,19 @@ # 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 +set(extension_target AsepriteThumbnailer) + +add_executable(${extension_target} MACOSX_BUNDLE thumbnail.mm) -target_link_libraries(AsepriteThumbnailer - laf-base +target_link_libraries(${extension_target} dio-lib render-lib - ${QUARTZ_LIBRARY}) + ${QUICKLOOK_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) +set_target_properties(${extension_target} PROPERTIES + BUNDLE_EXTENSION appex + LINK_FLAGS "-Wl,-e,_NSExtensionMain" + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_LIST_DIR}/Info.plist") diff --git a/src/desktop/osx/Info.plist b/src/desktop/osx/Info.plist index 968caa461..4b7c33b75 100644 --- a/src/desktop/osx/Info.plist +++ b/src/desktop/osx/Info.plist @@ -2,57 +2,36 @@ - CFBundleDocumentTypes - - - CFBundleTypeRole - QLGenerator - LSItemContentTypes - - dyn.ah62d4rv4ge80c65f - dyn.ah62d4rv4ge80c65fsb3gw7df - - - CFBundleExecutable AsepriteThumbnailer CFBundleIdentifier - org.aseprite.AsepriteThumbnailer + org.aseprite.Aseprite.AsepriteThumbnailer CFBundleName AsepriteThumbnailer - CFBundleInfoDictionaryVersion - 6.0 - CFBundlePackageType - XPC! CFBundleShortVersionString 1.0 CFBundleVersion 1.0 - CFPlugInFactories + CFBundlePackageType + XPC! + NSExtension - A5E9417E-6E7A-4B2D-85A4-84E114D7A960 - QuickLookGeneratorPluginFactory + NSExtensionAttributes + + QLSupportedContentTypes + + org.aseprite.Aseprite.sprite + + QLThumbnailMinimumDimension + 32 + + NSExtensionPointIdentifier + com.apple.quicklook.thumbnail + NSExtensionPrincipalClass + ThumbnailProvider - 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. + Copyright © 2001-2025, Igara Studio S.A. +All rights reserved. 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 diff --git a/src/desktop/osx/thumbnail.mm b/src/desktop/osx/thumbnail.mm index e7f2ff123..04a31558f 100644 --- a/src/desktop/osx/thumbnail.mm +++ b/src/desktop/osx/thumbnail.mm @@ -1,11 +1,10 @@ // Desktop Integration -// Copyright (c) 2022 Igara Studio S.A. +// Copyright (c) 2022-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" - +#include "app/file_system.h" #include "dio/decode_delegate.h" #include "dio/decode_file.h" #include "dio/file_interface.h" @@ -13,8 +12,7 @@ #include "render/render.h" #include - -#include +#include namespace desktop { @@ -87,7 +85,7 @@ public: } // 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]; if (!data) @@ -126,6 +124,18 @@ CGImageRef get_thumbnail(CFURLRef url, CFDictionaryRef options, CGSize maxSize) 0, gfx::ClipF(0, 0, 0, 0, image->width(), image->height())); + // Alpha premultiplication. + // CGBitmapContextCreate doesn't support unpremultiplied alpha images (kCGImageAlphaFirst). + doc::LockImageBits bits(image.get()); + doc::LockImageBits::iterator it, end; + for (it = bits.begin(), end = bits.end(); it != end; ++it) { + const int a = doc::rgba_geta(*it); + *it = doc::rgba(doc::rgba_getr(*it) * a / 255, + doc::rgba_getg(*it) * a / 255, + doc::rgba_getb(*it) * a / 255, + a); + } + w = image->width(); h = image->height(); } @@ -134,9 +144,6 @@ CGImageRef get_thumbnail(CFURLRef url, CFDictionaryRef options, CGSize maxSize) return nullptr; } - // TODO Premultiply alpha because CGBitmapContextCreate doesn't - // support unpremultiplied alpha (kCGImageAlphaFirst). - CGColorSpaceRef cs = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); CGContextRef gc = CGBitmapContextCreate(image->getPixelAddress(0, 0), w, @@ -153,3 +160,42 @@ CGImageRef get_thumbnail(CFURLRef url, CFDictionaryRef options, CGSize maxSize) } } // namespace desktop + +@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 = desktop::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