Quick Look — приложение в OS X, которое создает thumbnails (иконки) и previews (окно с описанием/содержимым файла по нажатию пробела в Finder). Оно поддерживает ряд стандартных файлов, для не поддерживаемых можно устанавливать QL plugins — генераторы иконок и/или превью. Они имеют формат .qlgenerator
, размещаются в ~/Library/QuickLook
и /Library/QuickLook
.
Я пишу приложения под iOS, иногда под OSX. Со сторонними QuickLook генераторами столкнулся, когда увидел плагин для первью .mobileprovision
— Provisioning.
.mobileprovision/.provisionprofile
— профиль, содержащий сертификаты, допущенные для установки устройства, некоторые параметры для развертывания iOS & OSX приложений.
Вот так папка с профилями выглядит без всяких плагинов для Quick Look:
Выбирать профиль напрямую необходимо, например, при использовании его в скрипте для автоматического развертывания приложения по TestFlight. Понять для какого приложения какой профиль брать — совершенно невозможно.
Сперва я стал использовать open-source Provisioning, потом закрытый, но более красивый и подробный ipaql. Необходимость написания своего открытого решения возникла после того, как автор ipaql добавил совместимость с OS X Mavericks лишь спустя полгода после выхода системы, а отображение иконок не починил до сих пор.
Вот что у меня получилось — ProvisionQL.
Поддерживаемые типы файлов для создания иконок и превью:
.ipa
— iOS packaged application (как из Xcode, так и из AppStore).app
— iOS application bundle.mobileprovision
— iOS provisioning profile.provisionprofile
— OSX provisioning profile
Под катом я расскажу об основных шагах при создании Quick Look плагинов.
Настройка проекта
В Xcode создаем новый проект: File > New > Project… OS X > System plug-in > Quick Look Plug-in. В базовом шаблоне сразу пойдем редактировать Info.plist:
Разверните CFBundleDocumentTypes и добавьте нужные типы файлов в массив LSItemContentTypes. Чтобы генерировать иконки в списках и таблицах я изменил QLThumbnailMinimumSize с 17 на 16. Обратите внимание на QLPreviewHeight и QLPreviewWidth — они используются только в случае, когда генератор слишком долго генерирует preview. У меня в случае ipa требуется извлечение нескольких файлов из zip архива, что довольно долго (от 0,06 до 0,12 с) — в моем случае система использует значения из plist. Если ваш генератор быстро отдаст preview — система отресазит окно по картинке или HTML, который вы отдадите.
Далее, если вы предпочитаете obj-c и классы Foundation — смело переименуйте GenerateThumbnailForURL.c и GeneratePreviewForURL.c
в GenerateThumbnailForURL.m и GeneratePreviewForURL.m
и добавьте в их заголовки:
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
Т.к. мне необходимо генерировать и иконки (GenerateThumbnailForURL), и окно предварительного просмотра (GeneratePreviewForURL) — я выделил общие include/import и функции в Shared.h/m. Привожу мой 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);
Окончательная структура проекта ProvisionQL:
NSBezierPath+IOS7RoundedRect — функция для вырезания закругленной по типу iOS7 иконки из квадратной.
Install.sh — скрипт для автоматической установки генератора при сборке проекта:
#!/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"
Для его выполнения зайдите в настройки Target, в меню выберите Editor > Add Build Phase > Add Run Script Build Phase и введите путь до скрипта в папке проекта:
Еще может понадобится отлаживать плагин. Т.к. он сам по себе не является выполняемым фалом — необходимо зайти в настройки схемы проекта — Edit Scheme… > Run > Info > Executable > Other > нажать Cmd+Shft+G > /usr/bin/ > Go > qlmanage:
Затем во вкладке Arguments укажите в аргументах запуска флаг -t
(для дебага иконок) или -p
(для дебага превью) и затем полный путь к тестовому файлу (в моем случае я тестирую отрисовку иконки на .ipa):
Генерация иконок
В данном примере я покажу как выводить заранее приготовленную иконку (defaultIcon.png). В ProvisionQL реализован выбор иконки из ipa файла, а так же вывод количества устройств и статуса действия (истек по времени или нет) для provision.
Вот готовый 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
}
Следует обратить внимание на пару моментов:
- нельзя использовать
NSImage imageNamed:
— этот метод будет искать ресурс в бандле qlmanage (исполняемого файла), а не нашего плагина - проверяйте
QLThumbnailRequestIsCancelled(thumbnail)
перед операциями, которые могут занять значительное время
Генерация превью
В примере рассмотрим, как заполнять и выводить HTML в качестве preview.
Необходимо предварительно подготовить шаблон template.html (туда же можно включить стили для оформления):
<!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>
Все, что выделено __KEY__
будем заполнять из кода.
Привожу окончательный 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
}
Как видите, сперва мы открываем Info.plist (либо извлекаем его из архива), затем некоторые данные из него сохраняем в synthesizedInfo
. Все ключи из synthesizedInfo
выставляются соответственно в строке, загруженной из template.html
. Полученная строка отдается qlmanage наряду с параметрами, описывающими возвращаемый тип данных как HTML.
Заключение
По данному руководству можно быстро создать плагин для быстрого просмотра и генерации иконок для вашего проприетарного формата или же для какого-либо распространенного формата, который системой стандартно не определяется.
Что касается ProvisionQL — я буду рад любым предложениям и пул-реквестам по улучшению функциональности в рамках задачи плагина.
Автор: BenderRodriguez