.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);
И вот какая структура проекта получилась:
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 — я буду рад любым предложениям и пул-реквестам по улучшению функциональности в рамках задачи плагина.
ссылка на оригинал статьи http://habrahabr.ru/post/208552/
Добавить комментарий