Accessing More Frameworks with Python

This post is based on a column I wrote for MacTech magazine in 2012. MacTech used to make older columns available online, but they haven’t done that for the past several years for some reason.
I’m planning to go through my older columns and dust off and republish some that I think are still relevant or useful.

Recently, we built a command-line tool using Python and the PyObjC bridge to control display mirroring.
PyObjC supports a lot of OS X frameworks “out-of-the-box”, and accessing them from Python can be as simple as:

include CoreFoundation

But what if the problem you want to solve requires a framework that isn’t included with the PyObjC bindings? In turns out that you can create your own bindings. In this post we’ll explore this aspect of working with Python and OS X frameworks.

OUR SAMPLE PROBLEM

In my organization, we sometimes have a need to set displays to a certain ColorSync profile. The ColorSync profile to use for a given display is a per-user preference, so if you need to set it for all users of a machine, you can’t just manually set it while logged in as one user and call it good.

If you are managing display profiles for a group of machines, or a conference room machine that has network logins, you need a way to manage display profiles for all users. Using MCX or doing some defaults scripting might come to mind. Let’s look at that possibility.

It turns out that preferences mapping displays to a ColorSync profiles are stored in /Library/Preferences/.GlobalPreferences.plist and as ByHost preferences in the NSGlobalDomain.
We can read the preferences for the current user like so:

% defaults -currentHost read -g com.apple.ColorSync.Devices
{

    "Device.mntr.00000610-0000-9CDF-0000-0000042737C0" =     {
        CustomProfiles =         {
            1 = "/Users/gneagle/Library/ColorSync/Profiles/Color LCD Calibrated.icc";
        };
    };
}

On my laptop, you can see that a display has a ColorSync profile named “Color LCD Calibrated.icc”. If there are no preferences set for the current host and user, the preferences at /Library/Preferences/.GlobalPreferences.plist are used:

% defaults read /Library/Preferences/.GlobalPreferences com.apple.ColorSync.Devices
{
    "Device.mntr.00000610-0000-9CDF-0000-0000042737C0" =     {
        DeviceDescriptions =         {

…and on for many, many lines.

It might occur to you that all you need to do is set the desired preferences in /Library/Preferences/.GlobalPreferences.plist, and make sure they are cleared from the user’s ByHost preferences, or use MCX to manage the com.apple.ColorSync.Devices “always”. But there’s a complication:

Each device in the com.apple.ColorSyncDevices preference has an identifier like “Device.mntr.00000610-0000-9CDF-0000-0000042737C0”. We can guess that “Device.mntr” means “Device, type: monitor”. But what is the “00000610-0000-9CDF-0000-0000042737C0” bit? It’s a unique identifier. Each display connected to a machine gets a unique identifier. There’s our complication! If we create an MCX record or write a script to set the ColorSync profile for “Device.mntr.00000610-0000-9CDF-0000-0000042737C0”, it will work on one specific monitor. If that monitor is ever replaced, or you want to manage the same setting on a second machine/monitor combination, the unique identifier will be different, and our MCX or script will not have the desired effect, since it will be targeting the wrong display identifier.

If you wanted to instead script the setting of the ColorSync profile, you’d need to figure out the display identifier for the desired display and then use defaults to set a CustomProfile for that display identifier. This doesn’t seem very easy!

Several years ago, Apple provided a tool that could help with the task of setting ColorSync profiles: an AppleScriptable application called “ColorSyncScripting”. You could write a script like so:

tell application "ColorSyncScripting" 
	set display profile of display 1 to profile "Custom ColorSync Profile"
end tell

This bit of AppleScript would set the ColorSync profile for the primary display (the one with the menu bar). If you set such a script to run at login, it would set the ColorSync profile to your desired profile for all users.

This was the technique we used several years ago. But with the release of Snow Leopard, ColorSyncScripting disappeared. Like any good systems administrator, you start searching the web for a solution, and you find something like this: https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/ColorSync_Manager/index.html
— which is documentation on Apple’s ColorSync API. Hmm… Can we use Python and PyObjC to call these same functions? Unfortunately, there’s a complication:

% python
Python 2.7.2 (default, Oct 11 2012, 20:14:37) 
[GCC 4.2.1 Compatible Apple Clang 4.0 (tags/Apple/clang-418.0.60)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ColorSync
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ImportError: No module named ColorSync
>>> 

PyObjC doesn’t know anything about the ColorSync framework. That might not be terribly surprising. It’s poorly documented and seems to be pretty much ignored by Apple these days. Is there anything we can do? We certainly haven’t come this far, only to turn back now! Of course there is a way to use at least parts of the ColorSync framework from Python.

BRIDGESUPPORT

In OS X 10.5 Leopard, Apple introduced BridgeSupport files for its frameworks. BridgeSupport files are XML files that describe the API of frameworks or libraries. They are used in building bridges between languages other than C or Objective-C to Apple frameworks. Since that is exactly what we want to do, it seems like something we should explore.

Each OS X framework should contain a .bridgesupport file. The ColorSync framework is located inside the ApplicationServices.framework at /System/Library/Frameworks/ApplicationServices.framework/Frameworks/ColorSync.framework; within this framework, the bridgesupport file can be found at Resources/BridgeSupport/ColorSync.bridgesupport. That’s pretty deeply buried!

The developers of PyObjC provide some documentation on wrapping frameworks here: http://pythonhosted.org/pyobjc/metadata/bridgesupport.html. But if you attempt to use these techniques with the ColorSync framework, you’ll quickly run into some problems, which might be a clue as to why the framework is not currently available by default from PyObjC. Following the examples, you could create a file named “ColorSync.py” with these contents:

"""
Wrappers for the ColorSync framework
"""
import objc as _objc
from CoreFoundation import *

__bundle__ = _objc.initFrameworkWrapper("ColorSync",
    frameworkIdentifier="com.apple.ColorSync",
    frameworkPath=_objc.pathForFramework(
        "/System/Library/Frameworks/"
        "ApplicationServices.framework/"
        "Frameworks/ColorSync.framework"),
    globals=globals())

You can then fire up the Python interpreter and test:

% python
Python 2.7.2 (default, Oct 11 2012, 20:14:37) 
[GCC 4.2.1 Compatible Apple Clang 4.0 (tags/Apple/clang-418.0.60)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import ColorSync
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "ColorSync.py", line 13, in <module>
    globals=globals())
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC/objc/_bridgesupport.py", line 140, in initFrameworkWrapper
    _parseBridgeSupport(data, globals, frameworkName, dylib_path)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC/objc/_bridgesupport.py", line 42, in _parseBridgeSupport
    objc.parseBridgeSupport(data, globals, frameworkName, *args, **kwds)
ValueError: cftype for 'CMProfileRef' must include gettypeid_func, tollfree or both

Yuck. Fortunately, we don’t need to wrap the entire framework; we only need a small subset of the available functions. So we can cheat. The PyObjC BridgeSupport documentation mentions that obj.initFrameworkWrapper() “loads the information in the bridgesupport file (either one embedded in the framework or one next to the module that called initFrameworkWrapper()).” So we can make a copy of the BridgeSupport file and modify it. Assuming we are in the same directory as our ColorSync.py file:

% cp /System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/ColorSync.framework/Resources/BridgeSupport/ColorSync.bridgesupport PyObjC.bridgesupport

(It turns out that the bridgesupport file “next to the module that called initFrameworkWrapper()” must have a specific name: PyObjC.bridgesupport.)

If we import ColorSync again we still get the same error, but now we can do something about it. Open the PyObjC.bridgesupport file and find and delete this line, which is the cftype definition for CMProfileRef – the thing the error is complaining about:

<cftype type='^{OpaqueCMProfileRef=}' name='CMProfileRef'/>

Try importing ColorSync again. Now you get a different (though similar) error:

>>> import ColorSync
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "ColorSync.py", line 13, in <module>
    globals=globals())
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC/objc/_bridgesupport.py", line 131, in initFrameworkWrapper
    _parseBridgeSupport(data, globals, frameworkName, inlineTab=inlineTab)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/Extras/lib/python/PyObjC/objc/_bridgesupport.py", line 42, in _parseBridgeSupport
    objc.parseBridgeSupport(data, globals, frameworkName, *args, **kwds)
ValueError: cftype for 'CMWorldRef' must include gettypeid_func, tollfree or both

So we need to remove the definition for CMWorldRef. Find and delete this line:

<cftype type='^{OpaqueCMWorldRef=}' name='CMWorldRef'/>

and once again attempt to import ColorSync:

>>> import ColorSync

Success. (Of course, it would have been better to actually fix those two definitions, but I’m not entirely sure how to properly do that, and for the problem we are trying to solve, we don’t need those definitions anyway. So we whack them!) The C/Objective-C code we found earlier made use of a ColorSyncDeviceSetCustomProfiles function, so let’s make sure that’s available:

>>> ColorSync.ColorSyncDeviceSetCustomProfiles
<objc.function 'ColorSyncDeviceSetCustomProfiles' at 0x10b21f8f0>

Looks good so far.

GETTING DOWN TO BUSINESS

Now that we have our Python bindings to the ColorSync framework seemingly functional, we can work on writing the actual code. Looking once again at Apple’s ColorSync documentation, it appears that the call that does the thing we want is:

ColorSyncDeviceSetCustomProfiles(kColorSyncDisplayDeviceClass, id, dict)

id is a CFUUIDRef and appears to be a display identifier; dict is a CFDictionaryRef with a dictionary like so:

{ kColorSyncDeviceDefaultProfileID: cfurl_for_profile }

where cfurl_for_profile is a CFURL representation of the filesystem path to the desired ColorSync profile.

Let’s get back to that display identifier. The ColorSync framework contains a function called CGDisplayCreateUUIDFromDisplayID, which takes a DisplayID and returns a UUID. In my previous post, when we were working with Quartz Display Services, we used this bit of code to get the id of the main display:

# get id of main display
main_display_id = Quartz.CGMainDisplayID()

Let’s try passing that value to CGDisplayCreateUUIDFromDisplayID:

>>> import ColorSync
>>> import Quartz
>>> main_display_id = Quartz.CGMainDisplayID()
>>> display_uuid = ColorSync.CGDisplayCreateUUIDFromDisplayID(main_display_id)
>>> print display_uuid
<CFUUID 0x7f7fc3d012c0> 00000610-0000-9CDF-0000-0000042737C0

That UUID might look familiar! If you look back at when we were reading the com.apple.ColorSync.Devices preferences, you might remember this:

% defaults -currentHost read -g com.apple.ColorSync.Devices
{

    "Device.mntr.00000610-0000-9CDF-0000-0000042737C0" =     {
        CustomProfiles =         {
            1 = "/Users/gneagle/Library/ColorSync/Profiles/Color LCD Calibrated.icc";
        };
    };
}

The UUID in the “Device.mntr” key matches the one we got from CGDisplayCreateUUIDFromDisplayID, so we must be on the right track.

We can use CoreFoundation’s CFURL functions to create a CFURL. If we put everything together, it might look something like this:

import ColorSync
import Quartz
import CoreFoundation

# get main display UUID
main_display_id = Quartz.CGMainDisplayID()
display_uuid = ColorSync.CGDisplayCreateUUIDFromDisplayID(
    main_display_id)

# get CFURL for a ColorSync profile
path = ('/Library/Application Support/Adobe/Color/'
        'Profiles/SMPTE-C.icc')
profile_url = CoreFoundation.CFURLCreateWithFileSystemPath(
    CoreFoundation.kCFAllocatorDefault, 
    path, 
    CoreFoundation.kCFURLPOSIXPathStyle, False);

# assemble our profile dict
profile_dict = {
    ColorSync.kColorSyncDeviceDefaultProfileID: 
        profile_url
}

# set the ColorSync profile for the main display
result = ColorSync.ColorSyncDeviceSetCustomProfiles(
    ColorSync.kColorSyncDisplayDeviceClass, 
    display_uuid, profile_dict)

(If the ColorSync profile above isn’t available on your machine, substitute another.)
If all goes as expected, the color on your main display will shift with the new profile. Use the Color tab in the Displays preferences pane to switch the ColorSync profile back. If you had any issues making this work, you can download a working example here: https://dl.dropboxusercontent.com/u/8119814/ColorSyncProfileTest.zip

A PROPER COMMAND-LINE TOOL

Now that we’ve demonstrated the basic functionality works, you could adapt and extend the code for you own needs, but it’s not a flexible, general-purpose command-line tool in its current state. Fortunately, Tim Sutton has spent the time and effort to create such a utility. Check it out here: https://github.com/timsutton/customdisplayprofiles. It supports setting profiles for displays other than the main display, and other useful features. Some examples of its use:

customdisplayprofiles set /path/to/profile.icc
customdisplayprofiles set --display 2 /path/to/profile.icc
customdisplayprofiles info

PYTHON TO THE RESCUE, AGAIN

Once again we’ve been able to solve a systems administration problem using Python together with OS X frameworks. Python plus PyObjC is a powerful combination of tools for a Mac systems administrator’s tool belt.

THANK YOUS

Huge thanks must be given to Tim Sutton, who did a lot of investigation on accessing ColorSync from Python. Tim is also the author of the customdisplayprofiles tool. Tim’s “Mac Operations” blog can be found at http://macops.ca. He has a post on his ColorSync tool here: http://macops.ca/configuring-colorsync-display-profiles-using-the-command-line/
Check out some of the other things he’s working on here: https://github.com/timsutton

Also, Michael Lynn is a great source of Python information, especially when it comes to integrating Python and C/Objective-C code. Check out what he’s working on at https://github.com/pudquick.

Tim and Michael often hang out in the ##osx-server channel on irc.freenode.net as “tvsutton” and “frogor”, respectively. The ##osx-server channel is another excellent resource for Mac system administrators.

MORE INFORMATION

Following are some links to more detailed information on some of the topics we’ve covered in this post.

PyObjC documentation

http://pythonhosted.org/pyobjc/

BridgeSupport files

http://developer.apple.com/library/mac/documentation/Darwin/Reference/ManPages/man5/BridgeSupport.5.html

PyObjC and BridgeSupport

http://pythonhosted.org/pyobjc/metadata/bridgesupport.html
Documentation on using bridgesupport files with PyObjC.

Quartz Display Services Reference

https://developer.apple.com/library/mac/#documentation/GraphicsImaging/Reference/Quartz_Services_Ref/Reference/reference.html

CFURL reference

https://developer.apple.com/library/mac/documentation/CoreFoundation/Reference/CFURLRef/Reference/reference.html

PrintCore.framework example

http://wiki.afp548.com/index.php/Setting_a_Default_Printer
At this link you’ll find an example of using PyObjC and BridgeSupport to access the PrintCore framework, this time to set a default printer for all users of a machine.

Accessing More Frameworks with Python