...

четверг, 9 января 2014 г.

Разработка Quick Look plugin для OS X

Quick Look — приложение в OS X, которое создает thumbnails (иконки) и previews (окно с описанием/содержимым файла по нажатию пробела в Finder). Оно поддерживает ряд стандартных файлов, для не поддерживаемых можно устанавливать QL plugins — генераторы иконок и/или превью. Они имеют формат .qlgenerator, размещаются в ~/Library/QuickLook и /Library/QuickLook.

Я пишу приложения под iOS, иногда под OSX. Со сторонними QuickLook генераторами столкнулся, когда увидел плагин для первью .mobileprovisionProvisioning.

.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 — я буду рад любым предложениям и пул-реквестам по улучшению функциональности в рамках задачи плагина.


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий