OSX Quick Look plugin development
Quick Look — OS X service that creates thumbnails and previews for files in Finder. It supports a number of standard file types, for others there are QL plugins — custom thumbnails and preview generators. They have .qlgenerator extension and can be placed in ~/Library/QuickLook
or /Library/QuickLook
.
In this article, I will tell about main stages of creating custom QL plugins.
I am iOS and OSX app developer. First time I ever encountered QL plugin is when I saw Craig Hockenberry’s Provisioning preview generator for .mobileprovision files (.mobileprovision
/.provisionprofile
- profile containing certificates, device ids and other parameters needed for iOS & OSX app deployment).
This is how profile folder looks without any custom Quick Look plugins:
Manual profile selection may be sometimes required. In TestFlight autouploading script, for example. Choosing right profile from this pile of identical icons is quite a problem.
As a first solution, I started using the open-source Provisioning project, then closed-source (but more beautiful and detailed) ipaql. Need in own open solution arisen after the ipaql
maintainer added OS X Mavericks compatibility only six months after the release and still repairing apps’ icons generation.
Here’s what I got - ProvisionQL. Supported file types for thumbnails and previews generation:
ipa
- iOS packaged application (both Xcode and AppStore supported)app
- iOS application bundlemobileprovision
- iOS provisioning profileprovisionprofile
- OSX provisioning profile
And here is the result of QL plugin work:
Project settings
Create a new project in Xcode: File > New > Project… OS X > System plug-in > Quick Look Plug-in
. In base template go straight to Info.plist
:
Unfold CFBundleDocumentTypes
and add desired file types in LSItemContentTypes
array. If you want to generate small icons in lists and tables, change QLThumbnailMinimumSize
from 17 to 16. Note QLPreviewHeight
and QLPreviewWidth
properties — they are used when generator takes too long to generate a preview. In my case – with ipa
– extraction of multiple files from a zip file is required, which is quite a long task (0,06 – 0,12 s), so system always uses these plist properties. If your generator will generate a file quickly — qlmanage will use dimensions of an image or a HTML you return.
Next, if you prefer obj-c and Foundation classes — rename GenerateThumbnailForURL.c
and GeneratePreviewForURL.c
to GenerateThumbnailForURL.m
and GeneratePreviewForURL.m
and add Foundation headers there:
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
I need to generate both thumbnails (GenerateThumbnailForURL) and preview (GeneratePreviewForURL), so I defined common imports and functions in Shared.h/m
. Here is my Shared.h
:
#include <CoreFoundation/CoreFoundation.h>
#include <CoreServices/CoreServices.h>
#include <QuickLook/QuickLook.h>
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#import <Security/Security.h>
#import <NSBezierPath+IOS7RoundedRect.h>
static NSString * const kPluginBundleId = @"com.FerretSyndicate.ProvisionQL";
static NSString * const kDataType_ipa = @"com.apple.itunes.ipa";
static NSString * const kDataType_app = @"com.apple.application-bundle";
static NSString * const kDataType_ios_provision = @"com.apple.mobileprovision";
static NSString * const kDataType_ios_provision_old = @"com.apple.iphone.mobileprovision";
static NSString * const kDataType_osx_provision = @"com.apple.provisionprofile";
#define SIGNED_CODE 0
NSImage *roundCorners(NSImage *image);
NSImage *imageFromApp(NSURL *URL, NSString *dataType, NSString *fileName);
NSString *mainIconNameForApp(NSDictionary *appPropertyList);
int expirationStatus(NSDate *date, NSCalendar *calendar);
Final ProvisionQL project structure:
NSBezierPath+IOS7RoundedRect
— function for masking icon with iOS7-style rounded corners. Install.sh
— autoinstall script for generator:
#!/bin/sh
PRODUCT="${PRODUCT_NAME}.qlgenerator"
QL_PATH=~/Library/QuickLook/
rm -rf "$QL_PATH/$PRODUCT"
test -d "$QL_PATH" || mkdir -p "$QL_PATH" && cp -R "$BUILT_PRODUCTS_DIR/$PRODUCT" "$QL_PATH"
qlmanage -r
echo "$PRODUCT installed in $QL_PATH"
To run it, go to Target settings, select Editor > Add Build Phase > Add Run Script Build Phase
and enter the path to the script in the project folder:
You may need to debug the plugin. Because it is not an executable itself, you must go to project scheme settings – Edit Scheme… > Run > Info > Executable > Other > press Cmd + Shft + G
> /usr/bin/
> Go > qlmanage
:
Then, in the Arguments tab, specify the arguments: starting with -t
(for thumbnails debugging) or -p
(for preview debugging), following with full path to the test file (in example case I’m testing thumbnail generation for .ipa
):
Thumbnails generation
In this example I will show how to display a dummy icon (defaultIcon.png). In ProvisionQL, you can check out an implementation for ipa
file extraction, as well as displaying the number of devices and the status of the provision.
This is GenerateThumbnailForURL.m
:
#import "Shared.h"
OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize);
void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail);
/* -----------------------------------------------------------------------------
Generate a thumbnail for file
This function's job is to create thumbnail for designated file as fast as
possible
----------------------------------------------------------------------------- */
OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize) {
@autoreleasepool {
NSString *dataType = (__bridge NSString *)contentTypeUTI;
NSImage *appIcon;
if([dataType isEqualToString:kDataType_app] || [dataType isEqualToString:kDataType_ipa]) {
NSURL *iconURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"defaultIcon" withExtension:@"png"];
appIcon = [[NSImage alloc] initWithContentsOfURL:iconURL];
} else {
return noErr;
}
if (QLThumbnailRequestIsCancelled(thumbnail)) {
return noErr;
}
NSSize canvasSize = appIcon.size;
NSRect renderRect = NSMakeRect(0.0, 0.0, appIcon.size.width, appIcon.size.height);
CGContextRef _context = QLThumbnailRequestCreateContext(thumbnail, canvasSize, false, NULL);
if (_context) {
NSGraphicsContext* _graphicsContext = [NSGraphicsContext graphicsContextWithGraphicsPort:(void *)_context flipped:NO];
[NSGraphicsContext setCurrentContext:_graphicsContext];
[appIcon drawInRect:renderRect];
//draw anything you want here
QLThumbnailRequestFlushContext(thumbnail, _context);
CFRelease(_context);
}
}
return noErr;
}
void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail) {
// Implement only if supported
}
Note some things:
- you can’t use
NSImage imageNamed:
- this method will look for the resource in qlmanage’s (executable file) bundle, not in our plugin - check
QLThumbnailRequestIsCancelled(thumbnail)
before any time-consuming operation
Preview generation
In the example, we will fill and return an HTML file as a preview. First, prepare your template.html
(you can also include styles there).
<!DOCTYPE html>
<html lang="en">
<body>
<div>
<h1>App info</h1>
Name: <strong>__CFBundleDisplayName__</strong><br />
Version: __CFBundleShortVersionString__ (__CFBundleVersion__)<br />
BundleId: __CFBundleIdentifier__<br />
</div>
</body>
</html>
Everything inside the __KEY__
will be filled with the generator.
Here is our final GeneratePreviewForURL.m
:
#import "Shared.h"
OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options);
void CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview);
/* -----------------------------------------------------------------------------
Generate a preview for file
This
function's job is to create preview for designated file
----------------------------------------------------------------------------- */
OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options) {
@autoreleasepool {
NSURL *URL = (__bridge NSURL *)url;
NSString *dataType = (__bridge NSString *)contentTypeUTI;
NSData *appPlist = nil;
if([dataType isEqualToString:kDataType_app]) {
// get the embedded plist for the iOS app
appPlist = [NSData dataWithContentsOfURL:[URL URLByAppendingPathComponent:@"Info.plist"]];
} else if([dataType isEqualToString:kDataType_ipa]) {
// get the embedded plist from an app archive using: unzip -p <URL> <files to unzip> (piped to standart output)
NSTask *unzipTask = [NSTask new];
[unzipTask setLaunchPath:@"/usr/bin/unzip"];
[unzipTask setStandardOutput:[NSPipe pipe]];
[unzipTask setArguments:@[@"-p", [URL path], @"Payload/*.app/Info.plist"]];
[unzipTask launch];
[unzipTask waitUntilExit];
appPlist = [[[unzipTask standardOutput] fileHandleForReading] readDataToEndOfFile];
} else {
return noErr;
}
if(QLPreviewRequestIsCancelled(preview)) {
return noErr;
}
NSMutableDictionary *synthesizedInfo = [NSMutableDictionary dictionary];
NSURL *htmlURL = [[NSBundle bundleWithIdentifier:kPluginBundleId] URLForResource:@"template" withExtension:@"html"];
NSMutableString *html = [NSMutableString stringWithContentsOfURL:htmlURL encoding:NSUTF8StringEncoding error:NULL];
NSDictionary *appPropertyList = [NSPropertyListSerialization propertyListWithData:appPlist options:0 format:NULL error:NULL];
[synthesizedInfo setObject:[appPropertyList objectForKey:@"CFBundleDisplayName"] forKey:@"CFBundleDisplayName"];
[synthesizedInfo setObject:[appPropertyList objectForKey:@"CFBundleIdentifier"] forKey:@"CFBundleIdentifier"];
[synthesizedInfo setObject:[appPropertyList objectForKey:@"CFBundleShortVersionString"] forKey:@"CFBundleShortVersionString"];
[synthesizedInfo setObject:[appPropertyList objectForKey:@"CFBundleVersion"] forKey:@"CFBundleVersion"];
for (NSString *key in [synthesizedInfo allKeys]) {
NSString *replacementValue = [synthesizedInfo objectForKey:key];
NSString *replacementToken = [NSString stringWithFormat:@"__%@__", key];
[html replaceOccurrencesOfString:replacementToken withString:replacementValue options:0 range:NSMakeRange(0, [html length])];
}
NSDictionary *properties = @{ // properties for the HTML data
(__bridge NSString *)kQLPreviewPropertyTextEncodingNameKey : @"UTF-8",
(__bridge NSString *)kQLPreviewPropertyMIMETypeKey : @"text/html" };
QLPreviewRequestSetDataRepresentation(preview, (__bridge CFDataRef)[html dataUsingEncoding:NSUTF8StringEncoding], kUTTypeHTML, (__bridge CFDictionaryRef)properties);
}
return noErr;
}
void CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview) {
// Implement only if supported
}
As you can see, at first we open Info.plist
(or extract it from the archive if needed), then process some data from it in the synthesizedInfo
. All keys of the synthesizedInfo
are filled respectively in keys loaded from template.html
. This string is returned to the qlmanage
alongside with parameters describing the return type of the data as an HTML.
Conclusion
Following this guide, you can quickly create a plugin for quick preview and icons generating for your proprietary format or to any common format, which is not handled by the system.
Regarding ProvisionQL - I am opened to any suggestions and pull requests that can improve functionality within the plugin scope.