Pull to refresh

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

Reading time 8 min
Views 12K
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 — я буду рад любым предложениям и пул-реквестам по улучшению функциональности в рамках задачи плагина.
Tags:
Hubs:
+44
Comments 12
Comments Comments 12

Articles