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