Apple Developer Connection
Advanced Search
Member Login Log In | Not a Member? Contact ADC

Using Python with Quartz 2D on Mac OS X

Python is a robust, object-oriented scripting language with powerful built-in libraries. On Mac OS X, Apple's Python Bindings for Quartz 2D give Python developers access to the Quartz 2D graphics API. This combination of rapid development and rich graphics makes Python on Mac OS X an excellent platform for graphics processing, dynamic content generation, and PDF document creation.

This article shows you how to leverage Python and the Quartz 2D API to quickly and easily create new solutions and opportunities by guiding you through three exercises. In addition, we'll explore some spots in Mac OS X where you can apply this Python and Quartz 2D knowledge.

Note: The Python Bindings for Quartz 2D first became available in Mac OS X 10.3 Panther. In the examples in this article, we'll be using a way of creating a generic, calibrated color space that is available on Tiger and later only. If you're following along on Panther, you'll need to replace the calls as noted in the comments.

The Basics

The Quartz 2D bindings for Python provide an object-oriented wrapper around the C-based API that Quartz 2D exposes. For the most part, most Quartz object references (CGImageRef, CGContextRef, etc.) are expressed as Python objects. The associated functions, usually named CGObjectAction (CGContextSetLineWidth( context, 3 ), for example), have been turned into methods of those Quartz object classes. In the case of our CGContextSetLineWidth example, the corresponding code in Python would be context.setLineWidth( 3 ). Likewise, CGContextDrawImage( context, rect, image ) becomes context.drawImage( rect, image ).

In order to use the Quartz 2D bindings in your Python code, you'll need to import the CoreGraphics module. For our examples, we'll be using from CoreGraphics import * which imports all of the top-level symbols in the module to keep things simpler.

Added Functionality

In addition to exposing the Quartz 2D APIs, the Python Bindings also provide other useful functionality such as:

  • Reading and writing images via QuickTime Importers and Exporters
  • Loading and rendering HTML, RTF, and Unicode text
  • Converting EPS and PS to PDF

These additions greatly increase the number of formats that you can deal with in Python, thus expanding the possible applications dramatically.

Example 1: Creating an Image File

Let's start with a very simple example (taken from Apple's bitmap.py example. You can find this in:

/Developer/Examples/Quartz/Python/

This example shows creating a bitmap file in the PNG image format. We can break the code into four easy steps:

  1. Import CoreGraphics and any other required modules.
  2. Create a bitmap context with the appropriate color space.
  3. Draw the desired contents into the bitmap context.
  4. Write the bitmap context pixel data out to a file.

The code to accomplish those steps in Python is shown in Listing 1.

Listing 1: Creating an Image File

# step 1: import the required modules
from CoreGraphics import *
import math

# step 2: create the bitmap context
# NOTE: on Panther use cs = CGColorSpaceCreateDeviceRGB()
cs = CGColorSpaceCreateWithName( kCGColorSpaceGenericRGB )
c = CGBitmapContextCreateWithColor( 256, 256, cs, (0,0,0,0) )

# step 3: draw a yellow square with a red outline
c.saveGState()
c.setRGBStrokeColor(1,0,0,1)			# red
c.setRGBFillColor(1,1,0,1)			# yellow
c.setLineWidth(3)
c.setLineJoin(kCGLineJoinBevel)
c.addRect( CGRectMake( 32.5, 32.5, 191, 191 ) )
c.drawPath( kCGPathFillStroke );
c.restoreGState()

# step 4: write out the file in PNG format
c.writeToFile ("out.png", kCGImageFormatPNG)
				

Type (or copy) the above code into a text file named example1.py, save it, and open the Terminal. Change to the directory where you saved your example1.py file and type:

python example1.py

If all goes well, you should see a new image file named out.png appear in the same directory. The contents of that file should look like Figure 1.

Output bitmap

Figure 1: Output image for the code in Listing 1.

There are a few interesting things about the code for this simple example. First, the bitmap starts out black and completely transparent after the CGBitmapContextCreateWithColor call due to the RGBA color value we passed in: (0,0,0,0).

Second, notice that we're also using full, four-component RGBA colors in our calls to setRGBFillColor and setRGBStrokeColor.

Third, it makes use of the handy CGRectMake function to create a rectangle parameter for the addRect method.

Finally, the Quartz 2D user coordinate space (the coordinate space we're drawing in) is independent of the device coordinate space (that of the bitmap context, a PDF context, or any other context) and Quartz 2D strokes pixels centered over their user space coordinates.

For our first example, the end result is that we needed to add 0.5 to the first two parameters (left and top) in the call to CGRectMake in order for our user space drawing to line up with the pixels in our bitmap (the device space). If you're the curious type, try changing the left and top coordinates back to 32 and examine the anti-aliasing artifacts along the rectangle outline to see for yourself.

Note: If you'd rather not have to use the command line to start your Python scripts, you can always use one of the many text editors (BBEdit and TextMate, for example) that have the capability to launch scripts in a shell or in the terminal.

Example 2: Splitting a PDF File

In this next example (based loosely on Apple's watermark.py sample), let's assume that you have a PDF document and you need to turn each page of that document into a PNG image file. We already know how to create image files. All we need to add is the ability to load a PDF document and draw each page into our bitmap.

  1. Import the required modules.
  2. Read the PDF file name from the command line.
  3. Create the input PDF document using a CGDataProvider.
  4. Draw each PDF page into an appropriately sized bitmap.
  5. Write each bitmap out to a file.

The code to accomplish those steps in Python is shown in Listing 1.

Listing 2: Splitting a PDF File

# step 1: import the required modules
import os, sys
from CoreGraphics import *

if len( sys.argv ) != 2:
	print "usage: python example2.py pdf_filename"
	sys.exit(1)

# step 2: read the pdf file name from the command line arguments
pdf_filename = sys.argv[1]
pdf_name, ext = os.path.splitext( pdf_filename )

# NOTE: on Panther use cs = CGColorSpaceCreateDeviceRGB()
cs = CGColorSpaceCreateWithName( kCGColorSpaceGenericRGB )

# step 3: create the input PDF document
provider = CGDataProviderCreateWithFilename( pdf_filename )
pdf = CGPDFDocumentCreateWithProvider( provider )
if pdf is None:
	print "Error reading PDF document - check that the supplied filename points to a PDF file"
	sys.exit(1)

# page number index is 1-based
for page_number in range( 1, pdf.getNumberOfPages() + 1 ):

	page_rect = pdf.getMediaBox( page_number )
	page_width = int(page_rect.getWidth())
	page_height = int(page_rect.getHeight())

	# step 4: create an appropriate bitmap and draw the PDF	
	bitmap = CGBitmapContextCreateWithColor( 
		page_width, page_height, cs, (1,1,1,1) )
	bitmap.drawPDFDocument( page_rect, pdf,  page_number )
	
	# step 5: write out the bitmap to a PNG file (and log each file)
	page_filename = "%s_%d.png" % (pdf_name, page_number)
	print "Writing image: %s, width x height: (%d x %d)" \
		% (page_filename, page_width, page_height)
	bitmap.writeToFile( page_filename, kCGImageFormatPNG )
				

Type (or copy) the above code into a text file named example2.py, and save it.

Now open the Terminal, and change to the directory where you saved your example2.py file and type:
python example2.py example2.pdf

where example2.pdf is the name of any PDF file in the same directory. If you don't have a PDF file sitting around, create one by printing this web page to PDF. For example, open Safari's Print dialog as usual, but select Save as PDF... from the PDF popup menu. Then navigate to the directory containing example2.py, and save the PDF file as example2.pdf.

As the script runs, it will print out a line to the console for each page of the PDF. Once the script completes, look in the script directory. You should see new PNG files, one per page in the original PDF. You can open them in Preview to verify their contents if you like.

Note: This example saves the images in the same directory as the original PDF file. This may or may not be what you want. Thankfully, as you'll see in our next example, you can easily control the destination directory for the images by changing the source code line that assembles the page filename.

Example 3: Going with the PDF Workflow

Now that we have a Python script that can split a PDF into PNG files, what can we do with it? Well, the printing system in Mac OS X makes heavy use of PDF files. In fact, the Print dialog provides that handy PDF popup menu we used to create our example2.pdf file in the previous section.

What you may not know is that the other items in that menu come from the PDF Services folders, one in /Library and one in ~/Library, and the items in those folders can be any of several different things:

  • A folder or an alias to a folder
  • An application or an alias to an application
  • A UNIX tool or an alias to a UNIX tool
  • An AppleScript file or an alias to an AppleScript file
  • An Automator Workflow or an alias to an Automator Workflow (Tiger and later only)

As you might suspect, Python scripts can fit into that list (as UNIX tools) with a few tweaks. Before we modify our PDF splitter script, however, let's take a look at how the printing system runs UNIX tools as PDF Services. Listing 3 shows a simple script that will shed some light on the calling conventions.

Listing 3: PDF Workflow Test Script

#!/usr/bin/python

import sys, os

def main ():
	output_path = os.path.expanduser("~/Desktop/test.txt")
	f = open(output_path, 'w')
	f.write( "Number of args: %d\n" % len(sys.argv) )
	i = 0
	for s in sys.argv:
		f.write( "Arg %d: %s\n" % (i, s) )
		f.write( '\n' )
		i += 1
	f.close()
	os.unlink(sys.argv[3])

if __name__ == '__main__':
	main ()
				

This script has a few new features as compared to our previous examples. First, there's a new comment line at the top, #!/usr/bin/python, that tells the UNIX shell executing our script that we want to be run using the Python interpreter.

Second, we've used os.path.expanduser to properly generate a path to a test.txt file on the current user's Desktop. Note that this code will wipe out any existing test.txt file so take care not to delete something that you would prefer to save.

Third, we examine the arguments passed into the script by iterating over the sys.argv list and writing each one to our output file.

Fourth, because the PDF Workflow system relies on PDF Workflow items to delete the temporary PDF file, we've used the unlink function from the os module to delete the file once we're done.

Finally, we've used a Python idiom, checking the __name__ variable, to know that we're supposed to execute our main function. That last bit isn't strictly necessary but it is a good habit to get into, especially once you move on to modules, unit testing, and other more advanced Python features.

Now that you know what the script is supposed to do, let's see it in action. Save the script as test.py in the "~/Library/PDF Services" directory. Note that you may have to create the "~/Library/PDF Services" directory yourself if it doesn't already exist. Then, navigate to that directory in a Terminal window and mark the script file as executable by typing chmod 755 test.py. If you list the directory contents with ls -l, you should permissions that look like -rwxr-xr-x. If you do, you're good to go.

To see the script in action, print this web page to PDF using Safari, but instead of picking Save as PDF..., choose test.py from the PDF popup menu. Once Safari has finished printing, switch to the Finder and double-click on test.txt. You should see something like the contents shown in Listing 4.

Listing 4: test.txt Contents

Number of args: 4
Arg 0: /Users/davidh/Library/PDF Services/test.py

Arg 1: <Title of the Safari window you printed>

Arg 2: <many, many print settings and options>

Arg 3: /tmp/printing.506.7/Print job.pdf
				

So, what have we learned from this little exercise? The Mac OS X printing system sends our test script four different arguments:

  1. The full path to our test script
  2. The title of the document or window we printed
  3. All of the print settings and options from the Print dialog
  4. The full path to the temporary PDF file containing the print job

If we're going to integrate our PDF splitter script, we'll need to use the argument at index 3, the path to the temporary PDF file, as our source PDF document. We'll also need to use the expanduser trick to save our PNG images to a folder on the current user's Desktop rather than dump them in the /tmp folder where they might get lost or deleted. Listing 5 shows a revised script with those modifications in place.

Listing 5: PDF Services Script

#! /usr/bin/python

# step 1: import the required modules
import os, sys
from CoreGraphics import *

def main ():
	# step 2: read the pdf file name from the command line arguments
	pdf_path = sys.argv[3]
	path, pdf_filename = os.path.split( pdf_path )
	pdf_name, ext = os.path.splitext( pdf_filename )
	
	# assemble an output directory path and make sure it exists
	output_directory = os.path.expanduser("~/Desktop/%s" % sys.argv[1])
	if not os.path.exists( output_directory ):
		os.mkdir( output_directory )
	
	# NOTE: on Panther use cs = CGColorSpaceCreateDeviceRGB()
	cs = CGColorSpaceCreateWithName( kCGColorSpaceGenericRGB )
	
	# step 3: create the input PDF document
	provider = CGDataProviderCreateWithFilename( pdf_path )
	pdf = CGPDFDocumentCreateWithProvider( provider )
	if pdf is None:
		print """Error reading PDF document - 
			check that the supplied filename points to a PDF file"""
		sys.exit(1)
	
	# page number index is 1-based
	for page_number in range( 1, pdf.getNumberOfPages() + 1 ):
	
		page_rect = pdf.getMediaBox( page_number )
		page_width = int(page_rect.getWidth())
		page_height = int(page_rect.getHeight())
	
		# step 4: create an appropriate bitmap and draw the PDF	
		bitmap = CGBitmapContextCreateWithColor( 
			page_width, page_height, cs, (1,1,1,1) )
		bitmap.drawPDFDocument( page_rect, pdf,  page_number )
		
		# step 5: write out the bitmap to a PNG file (and log each file)
		page_filename = "%s_%d.png" % (pdf_name, page_number)
		output_path = os.path.join( output_directory, page_filename )
		print "Writing image: %s, width x height: (%d x %d)" \
			% (output_path, page_width, page_height)
		bitmap.writeToFile( output_path, kCGImageFormatPNG )
	os.unlink(pdf_path)
	
if __name__ == "__main__":
	main()
				

This script is a combination of the previous PDF splitter script, our PDF Workflow test script, and an additional trick or two. Unlike the previous splitter code, this version saves the output images in a Desktop folder named after the print job title (argument 1) and the code makes sure that the directory exists before trying to write the files.

To test the script, save it as pdf_to_png.py, set the permissions so that it is executable, copy it to the ~/Library/PDF Services directory, and use the PDF Workflow menu to send a PDF print job to the pdf_to_png.py workflow item. You should see a new folder appear on your Desktop bearing the name of your print job and containing one PNG file per print job page.

Wrapping Up

In this article, we've given you a taste of what Python and Mac OS X can accomplish, as well as how you can apply Python and the Quartz 2D bindings throughout the system. If you'd like to learn more, check out the links below or explore on your own. There are many other ways you can use Python and Quartz 2D in Mac OS X, including:

  • Generating dynamic content for websites
  • Calling Python from Applescript via the do shell script command
  • Using a shell script action to execute Python code in an Automator Workflow
  • Using that Automator Workflow as a folder action
  • Using Python in an Xcode shell-script build phase to dynamically generate application resources

For More Information

Posted: 2007-01-16