📜 ⬆️ ⬇️

Development Quick Look plugin for OS X

Quick Look is an application in OS X that creates thumbnails (icons) and previews (a window with a description / content of the file by pressing the space bar in the Finder). It supports a number of standard files, for non-supported QL plugins can be installed - icon generators and / or previews. They have the format .qlgenerator , placed in ~/Library/QuickLook and /Library/QuickLook .

I write apps for iOS, sometimes for OSX. I encountered third-party QuickLook generators when I saw the plugin for the first .mobileprovision - Provisioning .
.mobileprovision/.provisionprofile is a profile containing certificates approved for device installation, some parameters for deploying iOS & OSX applications.

This is how the folder with profiles looks without any plugins for Quick Look:

Selecting a profile directly is necessary, for example, when using it in a script to automatically deploy an application via TestFlight. To understand for which application which profile to take is absolutely impossible.
')
First, I began to use open-source Provisioning , then closed, but more beautiful and detailed ipaql . The need to write an open solution arose after the author ipaql added compatibility with OS X Mavericks only six months after the system was released, and the display of the icons has not been fixed yet.

That's what I got - ProvisionQL .
Supported file types for creating icons and previews:



Under the cut, I will talk about the main steps in creating Quick Look plugins.

Project Setup


In Xcode, we create a new project: File> New> Project ... OS X> System plug-in> Quick Look Plug-in. In the basic template, let's go right away edit Info.plist:

Expand CFBundleDocumentTypes and add the desired file types to the LSItemContentTypes array. To generate icons in lists and tables, I changed QLThumbnailMinimumSize from 17 to 16. Pay attention to QLPreviewHeight and QLPreviewWidth - they are used only when the generator generates a preview for too long. In the case of ipa, I need to extract several files from a zip archive, which is quite long (from 0.06 to 0.12 s) - in my case the system uses values ​​from plist. If your generator quickly gives preview - the system will cancel the window by the image or HTML that you give.

Next, if you prefer obj-c and Foundation classes, feel free to rename GenerateThumbnailForURL.c GeneratePreviewForURL.c in GenerateThumbnailForURL.m GeneratePreviewForURL.m and add to their headings:
 #import <Foundation/Foundation.h> #import <Cocoa/Cocoa.h> 


Since I need to generate both icons (GenerateThumbnailForURL) and a preview window (GeneratePreviewForURL) - I highlighted the common include / import and functions in Shared.h / m. I quote 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); 

The final structure of the ProvisionQL project:


NSBezierPath + IOS7RoundedRect is a function for cutting iOS7 icons rounded out from the square.
Install.sh - script for automatic installation of the generator when building a project:
 #!/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 execute it, go to the Target settings, in the menu select Editor> Add Build Phase> Add Run Script Build Phase and enter the path to the script in the project folder:


You may also need to debug the plugin. Since it is not an executable file itself - you need to go to the project schema settings - Edit Scheme ...> Run> Info> Executable> Other> click Cmd + Shft + G> / usr / bin /> Go> qlmanage:


Then in the Arguments tab, specify the -t flag (for debugging icons) or -p (for debugging previews) in the launch arguments and then the full path to the test file (in my case, I test the icon drawing on .ipa):


Icon generation


In this example, I will show how to display a pre-prepared icon ( defaultIcon.png ). ProvisionQL implements the selection of an icon from the ipa file, as well as displaying the number of devices and the status of the action (expired or not) for the provision.

Here is the ready 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 } 

Attention should be paid to a couple of points:


Generate a preview


In the example, consider how to fill out and display HTML as a preview.
You must first prepare the template template.html (there you can also include styles for design):
 <!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> 

All that is allocated __KEY__ will be filled from the generator.

Here is the 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, first we open Info.plist (or extract it from the archive), then save some data from it in synthesizedInfo . All keys from synthesizedInfo set accordingly in the line loaded from template.html . The resulting string is given qlmanage along with parameters describing the returned data type as HTML.

Conclusion


In this tutorial, you can quickly create a plugin to quickly view and generate icons for your proprietary format, or for any common format that is not standardly defined by the system.

As for ProvisionQL , I will be glad to get any suggestions and pull requests to improve the functionality of the plug-in task.

Source: https://habr.com/ru/post/208552/


All Articles