macdevcenter.com
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Panther, Python, and CoreGraphics
Pages: 1, 2

Adding the Outline

The last bit of decoration on our image is a thin gray line around the image border. This outline will make it easier to distinguish the unshaded sides of the image border from the image background color.




    def _drawOutline(self):
        self._gc.setRGBStrokeColor(0.5, 0.5, 0.5, 0.5)
        self._gc.setLineWidth(0.5)
        margin = self._margin
        outlineRect = CGRectMake(
            0, margin + 0.5,
            self._newWidth - margin - 0.5,
            self._newHeight - margin - 1)
        self._gc.addRect(outlineRect)
        self._gc.strokePath()

Whereas setRGBFillColor() controls the color used to fill a path, setRGBStrokeColor() sets the color used to draw the path itself. In this case, we're drawing a thin gray line which is a half-pixel wide. The goal is to create an outline that is visible but doesn't look like it was rendered with a felt-tip pen.

The path along which we're drawing has slightly different dimensions from the filled image border, in order to compensate for the line width.

Drawing the Source Image

With the shadowed image border in place, we're almost ready to render the rescaled source image. First we need to specify the rectangular region into which it will be drawn.


    def _getBorderedRect(self):
        w = self._newWidth
        h = self._newHeight
        margin = self._margin
        border = self._borderWidth
        return CGRectMake(
            border, border + margin,
            w - margin - 2 * border,
            h - margin - 2 * border)

Then we need to set the interpolation quality to use when rescaling the source image and, at last, to draw the image.


    def _renderSrcImage(self, img):
        r = self._getBorderedRect()
        self._gc.setInterpolationQuality(
            self._quality)
        self._gc.drawImage(r, img)

If we could hard-wire the output format, saving an image would be even simpler than loading one. As it is, the ImageDecorator class lets clients save in any bitmap image format that CoreGraphics supports.


    def _saveAs(self, pathname):
        extToFormat = {
            ".png": kCGImageFormatPNG,
            ".jpg": kCGImageFormatJPEG,
            ".tif": kCGImageFormatTIFF,
            }
        extension = os.path.splitext(pathname)[1]
        try:
            imgFormat = extToFormat[extension.lower()]
            self._gc.writeToFile(pathname, imgFormat)
        except KeyError:
            raise ValueError(
                "Can't save image as %r -- "
                "unknown format %s" % (pathname,
                                       extension))

extToFormat is a Python dictionary that maps filename extensions to CoreGraphics format constants. The _saveAs() method extracts the filename extension from the provided output pathname, then uses it to look up the image format to use. (The lower() string method gets the extension in all-lowercase letters before performing the dictionary lookup.) If it can determine the format, _saveAs() uses the context's writeToFile() method to save the image data. Otherwise it reports the unrecognized filename extension by raising a Python ValueError.

Putting It All Together

The method names we've defined so far have all had leading underscores. That's a hint to other Python programmers that they should treat these as protected methods, invoking them only from within the ImageDecorator class or derived subclasses.

For other clients, we'll make ImageDecorator very simple to use. Aside from its constructor, it will provide just one public method, decorate():


    def decorate(self, srcPathname, destPathname):
        img = self._loadImage(srcPathname)
        self._findRescaledSize(img)
        self._getGraphicsContext()
        self._drawShadowedBorder()
        self._drawOutline()
        self._renderSrcImage(img)
        self._saveAs(destPathname)

Using ImageDecorator

Here's an example showing how to use ImageDecorator to process a directory full of images.


from ImageDecorator import ImageDecorator
import os, glob

decorator = ImageDecorator(bgColor=(1, 1, 1, 1))
images = glob.glob("test/*.jpg")

for srcname in images:
    basename = os.path.basename(srcname)
    dirname = os.path.dirname(srcname)

    destname = os.path.join(
        dirname, "decorated_%s" % basename)
    
    decorator.decorate(srcname, destname)
    os.system("open %r %r" % (srcname, destname))

This example uses Python's glob module to find all JPEG images in the test subdirectory of the current working directory. A decorated copy of each image is created with the same filename as the original, but with a prefix of "decorated_."

The os.system() call at the bottom of the loop takes advantage of the OS X open command to display the original and decorated images side by side. On my system, both images are displayed using the Preview application.

Image Format Trade-Offs

In the example above each decorated image was saved in JPEG format. Since JPEG doesn't support transparency, the example placed the decorated image on a solid white background. Obviously, if we'd wanted to include such an image in a web page, we would have had to choose a background color that didn't clash with the background color of the web page.

A better solution might be to store the image in a format that does allow transparency, such as PNG or GIF. Unfortunately, each of these formats has drawbacks with respect to the CoreGraphics bindings.

As noted earlier, CoreGraphics doesn't support saving images in GIF format. It actually does let you create GIF image files, but in every test I performed, the resulting files were zero bytes in size.

Almost as if to compensate, images saved in PNG format are huge. They are often five to 10 times as large as the corresponding JPEG images.

Interpolation Quality

In the ImageDecorator __init__ method, we used a default quality of kCGInterpolationHigh. In CoreGraphics, image interpolation quality controls how source image pixels are sampled when computing destination-pixel intensities.

Besides kCGInterpolationHigh two other settings may be used: kCGInterpolationNone or kCGInterpolationLow. These settings produce results that are often hard to distinguish from one another, but are significantly different from those of kCGInterpolationHigh.

The easiest way to demonstrate these differences is to compare their effects side by side. The following code sample will use ImageDecorator to produce three images, one with each interpolation quality setting. It will also generate an HTML page in which to display them. Once the page is generated the script will use the os.system("open ...") technique to display the results; on my system, the page appears in a new Safari window.

Let's start by defining a couple of HTML page templates. First is the template for the web page as a whole. It includes a placeholder, %(comparisonCells)s, into which we'll format a sequence of HTML table cells.


_template = '''<html>
<head>
<title>Image Interpolation Qualities</title>
</head>
<body>
<table>
<tr>
%(comparisonCells)s
</tr>
</table>
</body>
</html>'''

Next comes a template string for the content of each data cell in comparisonCells. The content will consist of an img whose URL is specified by %(imagePathname)s, followed by a caption describing the quality %(settingName)s used to generate the image.


_imageRowTemplate = '''
<td><img src="%(imagePathname)s"><br>
Quality: %(settingName)s</td>
'''

The main function in this example loops through the supported image quality settings, creating a separate copy of the source image for each. Each generated image is stored as a new entry in comparisonCells.


def main():
    comparisonCells = []
    sourceImage = "../images/image_1.jpg"
    for settingName, quality in [
        ["None", kCGInterpolationNone],
        ["Low", kCGInterpolationLow],
        ["High", kCGInterpolationHigh]]:
        imagePathname = ("image_1_%s.jpg" % 
                         settingName)
        decorator = ImageDecorator(
            maxWidth=200, maxHeight=200,
            quality=quality)
        decorator.decorate(sourceImage,
                           imagePathname)
        comparisonCells.append(
            _imageRowTemplate % locals())
    comparisonCells = "\n".join(comparisonCells)
    htmlFilename = "quality_comparison.html"
    outf = open(htmlFilename, "w")
    outf.write(_template % locals())
    outf.close()
    os.system("open %s" % htmlFilename)

comparisonCells begins life as a Python list of _imageRowTemplate strings. Once all of the images have been generated, comparisonCells is turned into a single string that joins its list elements together with newlines.


    comparisonCells = "\n".join(comparisonCells)

Finally, main() generates and displays the comparison HTML page.


    htmlFilename = "quality_comparison.html"
    outf = open(htmlFilename, "w")
    outf.write(_template % locals())
    outf.close()
    os.system("open %s" % htmlFilename)

After running the script, we can see the effects of the different interpolation quality settings:


Quality: None

Quality: Low

Quality: High

Final Thoughts

This article has shown how Python and Panther's CoreGraphics module can simplify some image processing tasks. I'm hoping it's piqued your curiosity -- there's much more to the CoreGraphics module than could be presented in this article. I encourage you to check out the documentation and code examples in /Developer/Examples/Quartz/Python, to scan through /usr/libexec/fax/, and to experiment with CoreGraphics on your own.

Mitch Chapman has nearly twenty years of experience as a software developer. He lives in Santa Fe, New Mexico.


Return to the Mac DevCenter