Writing a Spotlight Importer

If your applications supports custom file formats, you should provide a Spotlight importer. A Spotlight importer parses the file format for relevant information and assigns that information to the appropriate metadata attributes. By providing an importer, you make it possible for users on OS X to search for matching files on local disks, and networked storage.

An example metadata importer is included in the CoreRecipes sample code project.

Creating the Metadata Importer Project

Xcode provides a project template, Spotlight Importer, that provides the functionality commonly shared by importers.

This template creates a project with the required frameworks, a template for the Info.plist file, a template for the schema file, a template for the localizable schema.strings file, a template for the main.c file that contains the necessary CFPlugin implementation, and GetMetadataForFile.c, a skeleton implementation of the required callback function. The target creates a CFPlugin bundle with an mdimporter extension.

In addition to writing the extraction code, you must modify the templates to specify the file types your importer handles and list the metadata attributes your importer provides.

Assigning a Unique ID to the Import Function

Each plug-in factory that can import metadata must have a unique identification number associated with it. A a single plug-in factory is necessary for each metadata importer, because the single function can handle many file types.

When you create a new metadata importer project, Xcode creates a UUID for your importer. Here is an example UUID generated by Xcode for a metadata importer:

8AED83B3-C412-11D8-85A3-000393D59866

This value is used in the importer’s Info.plist file, as well as the main.c file. Listing 2 shows the Info.plist template generated by Xcode.

Listing 2  Metadata importer Info.plist template

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
 "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>English</string>
    <key>CFBundleDocumentTypes</key>
    <array>
        <dict>
            <key>CFBundleTypeRole</key>
            <string>MDImporter</string>
            <key>LSItemContentTypes</key>
            <array>
                <string>SUPPORTED_UTI_TYPE</string>
            </array>
        </dict>
    </array>
    <key>CFBundleExecutable</key>
    <string>MyCustomImporter</string>
    <key>CFBundleIconFile</key>
    <string></string>
    <key>CFBundleIdentifier</key>
    <string>com.apple.yourcfbundle</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundlePackageType</key>
    <string>BNDL</string>
    <key>CFBundleSignature</key>
    <string>????</string>
    <key>CFBundleVersion</key>
    <string>1.0</string>
    <key>CFPlugInDynamicRegisterFunction</key>
    <string></string>
    <key>CFPlugInDynamicRegistration</key>
    <string>NO</string>
    <key>CFPlugInFactories</key>
    <dict>
        <key>8AED83B3-C412-11D8-85A3-000393D59866</key>
        <string>MetadataImporterPluginFactory</string>
    </dict>
    <key>CFPlugInTypes</key>
    <dict>
        <key>8B08C4BF-415B-11D8-B3F9-0003936726FC</key>
        <array>
            <string>8AED83B3-C412-11D8-85A3-000393D59866</string>
        </array>
    </dict>
    <key>CFPlugInUnloadFunction</key>
    <string></string>
</dict>
</plist>

The CFPlugInFactories entry is a dictionary that associates the metadata importer host ID to the UUIDs of the plug-in factory function that an importer requires. The CFPluginInTypes dictionary contains keys that associate the UUID of the factory function to the function. In both locations, Xcode inserted the newly generated UUID.

Listing 3 relevant line from a main.c template that Xcode would create.

Listing 3  Setting the importer ID in main.c

#define PLUGIN_ID "8AED83B3-C412-11D8-85A3-000393D59866"

Associating an Importer with a File Type

An importer must be associated with the file types that it can import. You do this by specifying the uniform type identifiers (UTIs) that correspond to the supported files.

The supported UTIs are specified in the LSItemContentTypes array in the importer’s Info.plist file. The template in Listing 2 includes a placeholder, SUPPORTED_UTI_TYPE, that must be replaced with the UTI an importer handles. If more than one file type is supported, you can add additional string entries to the LSItemContentTypes array in the Info.plist file. For example, an importer may have an UTI importer, with the SUPPORTED_UTI_Type set to com.apple.mycocoadocumentapp.mycustomdocument.

If your application does not define a UTI for its file types, you can declare one in your importer Info.plist file by adding the UTExportedTypeDeclarations key. Standalone importers that don't correspond to an application should declare the UTIs that they support by specifying a UTImportedTypeDeclarations key. The UTImportedTypeDeclarations format is the same as the UTExportedDeclarations format shown in Listing 4. See “Uniform Type Identifier Concepts” for more information on declaring UTIs.

Listing 4  UTExportedTypeDeclarations format

<key>UTExportedTypeDeclarations</key>
<array>
    <dict>
        <key>UTTypeIdentifier</key>
        <string>com.yourcompany.yourUTI</string>
        <key>UTTypeReferenceURL</key>
        <string>http://www.company.com/yourproduct</string>
        <key>UTTypeDescription</key>
        <string>Your Document Kind String</string>
        <key>UTTypeConformsTo</key>
        <array>
            <string>public.data</string>
            <string>public.content</string>
        </array>
        <key>UTTypeTagSpecification</key>
        <dict>
            <key>com.apple.ostype</key>
            <string>XXXX</string>
            <key>public.filename-extension</key>
            <array>
                <string>xxxx</string>
            </array>
        </dict>
    </dict>
</array>

Specifying Metadata Attributes

You must to specify the metadata attributes that your Spotlight importer returns by modifying the project’s schema.xml file. This file is an XML schema document that provides details on the returned attributes and also allows the definition of custom metadata attributes.

Listing 5 shows the schema.xml template generated by Xcode.

Listing 5  Metadata Importer schema.xml template

<?xml version="1.0" encoding="UTF-8"?>
 
<schema version="1.0" xmlns="http://www.apple.com/metadata"
                      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                      xsi:schemaLocation="http://www.apple.com/metadata
file:///System/Library/Frameworks/CoreServices.framework/Frameworks/Metadata.framework/Resources/MetadataSchema.xsd">
    <note>
        Custom attributes that this metadata importer supports.  Below
    is an example of a multivalued string attribute.  Other types
    are CFNumber, CFDate, CFBoolean and CFData.
    </note>
    <attributes>
        <attribute name="com_Foo_YourAttrName" multivalued="true" type="CFString"/>
    </attributes>
 
    <types>
        <type name="SUPPORTED_UTI_TYPE">
            <note>
        The keys that this metadata importer handles.
            </note>
            <allattrs>
        com_Foo_YourAttrName
            </allattrs>
            <displayattrs>
        com_Foo_YourAttrName
            </displayattrs>
        </type>
    </types>
</schema>

You must edit this template to suit your metadata importer.

  1. Replace the SUPPORTED_UTI_TYPE placeholder with the appropriate UTI type for your file.

  2. Edit the attributes element, editing or removing the attribute elements as required.

    The metadata keys are prefixed with the reverse DNS naming schema, replacing “_” with “.” for key-value coding compatibility. Each of these custom metadata values return a single CFString as specified by the type attribute of the attribute element.

    Metadata importers can return only the following CF types: CFString, CFNumber, CFBoolean, and CFDate. If an attribute returns an array of values, the type attribute specifies the CF type and the attribute element must include a multivalued attribute with a value of true. This is true even if the attribute contains only a single element.

    If your importer does not require custom metadata attributes, you can remove the attributes element entirely.

  3. Edit the allattrs element so that it contains all your metadata keys.

  4. Edit the displayattrs element so that it contains a subset of your metadata keys that are recommended for previewing.

  5. Edit the schema.strings file to provide display name and description strings for your custom metadata keys.

Listing 6 shows the schema.xml file that is included with the sample metadata importer project.

Listing 6  schema.xml file for the sample metadata importer

<?xml version="1.0" encoding="UTF-8"?>
 
<schema version="1.0" xmlns="http://www.apple.com/metadata"
                      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                      xsi:schemaLocation="http://www.apple.com/metadata
file:///System/Library/Frameworks/CoreServices.framework/Frameworks/Metadata.framework/Resources/MetadataSchema.xsd">
    <note>
        Custom attributes that this metadata importer supports.  Below
    is an example of a multivalued string attribute.  Other types
    are CFNumber, CFDate, CFBoolean and CFData.
    </note>
    <attributes>
        <attribute name="com_apple_myCocoaDocumentApp_myCustomDocument_notes" multivalued="false" type="CFString"/>
    </attributes>
 
    <types>
        <type name="com.apple.mycocoadocumentapp.mycustomdocument">
            <note>
        The keys that this metadata importer handles.
            </note>
            <allattrs>
        com_apple_myCocoaDocumentApp_myCustomDocument_notes
            </allattrs>
            <displayattrs>
        com_apple_myCocoaDocumentApp_myCustomDocument_notes
            </displayattrs>
        </type>
    </types>
</schema>

The example metadata importer declares one new attribute key, com_apple_myCocoaDocumentApp_myCustomDocument_notes. The key name is prefixed with the reverse DNS naming schema, replacing “_” with “.” for key-value coding compatibility. Each of these custom metadata values return a single CFString as specified by the type attribute of the attribute element.

Listing 7 shows the schema.strings file for the sample metadata importer.

Listing 7  Sample importer’s schema.strings file

"com_apple_myCocoaDocumentApp_myCustomDocument_notes" = "Notes";
"com_apple_myCocoaDocumentApp_myCustomDocument_notes.Description" = "What it is you're supposed to remember.";

The command-line tool mdcheckschema performs a simple validation on a schema and is useful when testing your own importer schema for validity.

Assigning Values to Metadata Attributes

When metadata is extracted for a file, the GetMetadataForFile function is called. The function is passed the plug-in interface, a mutable dictionary that you add the metadata attribute keys and values to, the UTI type of the target file, and the full path to the target file.

Listing 8 shows the GetMetadataForFile skeleton implementation provided by Xcode in GetMetadataForFile.c.

Listing 8  GetMetadataForFile template implementation

Boolean GetMetadataForFile(void* thisInterface,
               CFMutableDictionaryRef attributes,
               CFStringRef contentTypeUTI,
               CFStringRef pathToFile)
{
    /* Pull any available metadata from the file at the specified path */
    /* Return the attribute keys and attribute values in the dict */
    /* Return true if successful, false if there was no data provided */
 
    #warning To complete your importer please implement the function GetMetadataForFile in GetMetadataForFile.c
    return false;
}

Your implementation of this function should extract the metadata from the file and insert it into the dictionary with the appropriate keys and values. If it successfully returns metadata, the function should return with a value of true. If no metadata was extracted, it should return false.

The example’s custom file format is a simple property list containing the author, title and reminder notes. Note that the example makes use of Objective-C and the Foundation class NSDictionary to read the dictionary from the file. To use Objective-C in your GetMetadataForFile implementation, you must rename GetMetadataForFile.c to GetMetadataForFile.m. Listing 9 shows the GetMetadataForFile implementation of a GetMetadataForFile function that uses Objective-C.

Listing 9  Objective-C implementation of GetMetadataForFile for the sample metadata importer

Boolean GetMetadataForFile(void* thisInterface,
               CFMutableDictionaryRef attributes,
               CFStringRef contentTypeUTI,
               CFStringRef pathToFile)
{
    /* Pull any available metadata from the file at the specified path */
    /* Return the attribute keys and attribute values in the dict */
    /* Return true if successful, false if there was no data provided */
    Boolean success=NO;
    NSDictionary *tempDict;
    NSAutoreleasePool *pool;
 
    // Don't assume that there is an autorelease pool around the calling of this function.
    pool = [[NSAutoreleasePool alloc] init];
    // load the document at the specified location
    tempDict=[[NSDictionary alloc] initWithContentsOfFile:(NSString *)pathToFile];
    if (tempDict)
    {
    // set the kMDItemTitle attribute to the Title
    [(NSMutableDictionary *)attributes setObject:[tempDict objectForKey:@"title"]
                          forKey:(NSString *)kMDItemTitle];
 
    // set the kMDItemAuthors attribute to an array containing the single Author
    // value
    [(NSMutableDictionary *)attributes setObject:[NSArray arrayWithObject:[tempDict objectForKey:@"author"]]
                          forKey:(NSString *)kMDItemAuthors];
 
    // set our custom document notes attribute to the Notes value
    // (in the real world, you'd likely use the kMDItemTextContent attribute, however that
    // would make it hard to demonstrate using a custom key!)
    [(NSMutableDictionary *)attributes setObject:[tempDict objectForKey:@"notes"]
                          forKey:@"com_apple_myCocoaDocumentApp_myCustomDocument_notes"];
 
    // return YES so that the attributes are imported
    success=YES;
 
    // release the loaded document
    [tempDict release];
    }
    [pool release];
    return success;
}