
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:
- Import CoreGraphics and any other required modules.
- Create a bitmap context with the appropriate color space.
- Draw the desired contents into the bitmap context.
- 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.

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.
- Import the required modules.
- Read the PDF file name from the command line.
- Create the input PDF document using a
CGDataProvider.
- Draw each PDF page into an appropriately sized bitmap.
- 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:
- The full path to our test script
- The title of the document or window we printed
- All of the print settings and options from the Print dialog
- 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
|