Panther, Python, and CoreGraphicsby Mitch Chapman
Mac OS X Panther includes many updated developer tools. Among them is an enhanced version of Python 2.3 with its own SWIG-based bindings to the CoreGraphics library. Creating PDFs, JPEGs, and documents in other graphical formats just became a lot easier.
This article summarizes the capabilities in the Python CoreGraphics module and shows how to use CoreGraphics to rescale and decorate images for publication to the Web.
You can use Python's CoreGraphics bindings right out of the box. However, it's a good idea to install the Developer Tools that shipped with Panther, because the main source of documentation for the CoreGraphics module appears to be in the /Developer/Examples/Quartz/Python/ folder.
The API-Summary file in that directory shows how comprehensive the bindings are. They include functions for loading and saving graphics in JPEG, PDF, PNG, and TIFF formats, and for loading GIF images. The bindings also let you create new grayscale, RGB, and CMYK bitmap graphics contexts, as well as PDF contexts. Context instances offer a full set of Bezier path construction methods, and they support affine coordinate transformations. You can even control the rendering of shadows.
The CoreGraphics wrapper exposes much of the Quartz 2D graphics library to Python, but some pieces are still missing. For example, the bindings include a
CGContext.drawShading() method, which should fill a path with a gradient from one color to another. But the bindings provide no way to create the
CGShadingRef instance, which
drawShading() requires as an argument.
Despite the rough edges, the bindings are useful. In fact, as the contents of /usr/libexec/fax/ show, Panther itself uses Python and CoreGraphics to handle incoming faxes and to generate cover sheets for outgoing faxes. (And it cleverly generates cover sheets by formatting the pages as HTML, then using CoreGraphics to re-render the HTML to PDF format.)
Apple's own reliance on the CoreGraphics module is reason to hope that it will continue to be improved in future releases of Mac OS X.
Putting CoreGraphics to Work
Now I'll show you how to use CoreGraphics to prepare images for the Web. First we'll walk through a Python class,
ImageDecorator, which reads an existing image from disk, rescales it to fit within size parameters you specify, adds a white image border with a thin image outline and a drop shadow, and saves the results to a new image file. In other words, it takes you from this:
The source code for the
ImageDecorator class can be found in ImageDecorator.py.
ImageDecorator.py uses a wildcard import of the CoreGraphics module. Wildcard imports are discouraged in Python because they tend to pollute the namespace of the importing module and to increase the chance of namespace collisions. But all public attribute names in CoreGraphics have a
CG prefix, which reduces the risk of namespace collisions. And qualified name references would make the source code in this web-based article harder to read.
__init__ method for
ImageDecorator isn't presented here; you can find it in ImageDecorator.py. It simply records settings for all of the image decoration parameters.
In addition to the dimensions of the image and its decorations, the
__init__ parameters include settings for image background color and rescaling quality. I'll explain more about these later in the article.
Loading an Image
To load an image from disk using CoreGraphics, we just have to create an image data provider and pass it to
def _loadImage(self, pathname): return CGImageImport( CGDataProviderCreateWithFilename( pathname))
Computing the New Image Dimensions
We'll need to reserve room for the new image's border and the other decorations while ensuring that the image fits within the specified maximum size bounds. At the same time, we'll need to preserve the aspect ratio of the original image. Two methods,
_findRescaledSize(), satisfy these constraints.
_findInsideSize() computes the size of the region inside the decorated image where the rescaled source image will be rendered. It ensures that the new "inside" dimensions have the same aspect ratio as the original image.
def _getInsideSize(self, img, margin): width = img.getWidth() height = img.getHeight() aspect = float(width) / height if aspect > 1.0: internalWidth = (self._maxWidth - margin) internalHeight = (internalWidth / aspect) else: internalHeight = (self._maxHeight - margin) internalWidth = (internalHeight * aspect) return (internalWidth, internalHeight)
_findRescaledSize() takes the size computed by
_findInsideSize() and adds on the decoration margins. Then it stores the new image dimensions as instance attributes, for later reference.
def _findRescaledSize(self, img): margin = (self._borderWidth + self._margin) (insideWidth, insideHeight) = self._findInsideSize( img, margin) self._newWidth = insideWidth + margin self._newHeight = insideHeight + margin
Creating the Graphics Context
Given the size of the output image, we can create a graphics context into which to render it. We'll define another method,
_getGraphicsContext(), to do the job.
CoreGraphics provides several functions for creating bitmap graphics contexts. Assuming the source image contains color, we'll want to render to a color graphics context. So we'll use
CGBitmapContextCreateWithColor creates a graphics context with the given pixel dimensions, colorspace, and background color. In this case, we'll use an RGB colorspace. We'll also use the background color that was specified in the
def _getGraphicsContext(self): self._gc = CGBitmapContextCreateWithColor( self._newWidth, self._newHeight, CGColorSpaceCreateDeviceRGB(), self._bgColor)
The background color argument to
CGBitmapContextCreateWithColor() is a sequence of color component intensities. It has one value for each component in the provided colorspace, plus an extra value for the alpha, or transparency, component. Each value should be in the range 0.0 (lowest intensity) to 1.0 (highest intensity). For example, in RGB colorspace, a solid white background would be represented as (1.0, 1.0, 1.0, 1.0).
Ready to Draw
Once the graphics context is initialized, we can start drawing the image decorations. We'll define another method,
_drawShadowedBorder(), to render the bordered rectangle and the image shadow. To mimic the appearance of a bordered photographic print, we'll render the rectangle in white.
def _drawShadowedBorder(self): shadowedRect = CGRectMake( 0, self._margin, self._newWidth - self._margin, self._newHeight - self._margin) self._gc.addRect(shadowedRect) self._gc.setShadow( CGSizeMake(self._shadowOffset, -self._shadowOffset), self._shadowBlur) self._gc.setRGBFillColor(1, 1, 1, 1) self._gc.fillPath() # Remove the shadow: self._gc.setShadow(CGSizeMake(0, 0), 0)
shadowedRect defines the extent of the image's white border. Here we've offset the rectangle upwards by the size of the shadow margin,
self._margin. This leaves room for the shadow below and to the right of the image.
addRect() call simply adds the rectangle to the current drawing path.
setShadow() takes two arguments, one specifying the width and height by which to offset the shadow relative to its source graphic, and the other specifying how "sharp" the shadow should appear -- the distance over which it should fade out. Once more, we're using settings specified in the
fillPath() method fills the current drawing path with the current fill color. Since our graphics context uses the RGB colorspace we've set the color using
setRGBFillColor(). Other methods are available for use with other colorspaces.
After drawing the image border, we need to remove the shadow from the graphics context. Otherwise, every subsequent drawing operation would be rendered with its own separate shadow.
Pages: 1, 2