diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index ebcc34eb3..942367488 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