Extending NSBezierPath

Yesterday I wrote about how to extend NSImage so it can save to a file. Today we’ll tackle NSBezierPath. NSBezierPath is pretty cool for drawing, but it doesn’t support arbitrary regular polygons, just rects and ovals (and lines and arcs). And there’s not an easy way to extract the points that make up a path. And if you could extract the points, there isn’t a way to draw dots for the points instead of stroking or filling the path. OK, enough already, let’s look at some code.

First thing in the code, we’ll define some basic trigonometry routines to calculate the points for a polygon. Then we’ll create the class itself.

from objc import Category
from AppKit import NSBezierPath
import math
def poly_point(center, r, degrees):
    x = r * math.cos(degrees) + center[0]
    y = r * math.sin(degrees) + center[1]
    return x,y
def polypoints(center, r, numPoints, degreesRotation=0):
    if numPoints < 3:
        raise ValueError, 'Must have at least 3 points in a polygon'
    rotation = math.radians(degreesRotation)
    theta = (math.pi * 2) / numPoints
    return [poly_point(center, r, i*theta+rotation)
        for i in range(numPoints)]
class NSBezierPath(Category(NSBezierPath)):
    def points(self):
        points = []
        for i in range(self.elementCount()):
        elem, pts = self.elementAtIndex_associatedPoints_(i)
        points += pts
        return points
def appendBezierPathWithPolygonWithCenter_radius_numberOfPoints_(self, center, radius, numberOfPoints):
	''' Creates a regular polygon '''
	pts = polypoints(center, radius, numberOfPoints) self.moveToPoint_(pts[0])
	for pt in pts[1:]:
		self.lineToPoint_(pt)
	self.closePath()
    def dot(self):
        '''
        Similar to stroke: and fill:, but draws dots for each point in the
        path. Dot size is based on linewidth. Not as efficient, because it
        creates a separate NSBezierPath each time it is called.
        '''
        tmp_path = NSBezierPath.alloc().init()
        width = self.lineWidth()
        offset = width / 2
        for point in self.points():
            rect = (point[0] - offset, point[1] - offset),(width, width)
            tmp_path.appendBezierPathWithOvalInRect_(rect)
        tmp_path.fill()

OK, hopefully the above is reasonably clear. You can follow along with any calls which are unfamiliar by firing up AppKiDo or the Apple documentation for NSBezierPath. If you’re going to use the dot: method a lot you might want to cache the path so you’re not creating a new NSBezierPath every time, it depends on what you need.

Here’s a short script you can run on the command line to create a hexagon and demonstrate fill:, stroke: and dot:

from AppKit import NSApplication, NSBezierPath, NSColor, NSImage
from Foundation import NSInsetRect, NSMakeRect
import image_ext, bezier_path_ext

app = NSApplication.sharedApplication()
image = NSImage.alloc().initWithSize_((64,64))
image.fillWithColor_(NSColor.clearColor())
image.lockFocus()
hex = NSBezierPath.alloc().init()
hex.appendBezierPathWithPolygonWithCenter_radius_numberOfPoints_((32,32), 26, 6)
NSColor.greenColor().set()
hex.fill()
hex.setLineWidth_(2)
NSColor.blueColor().set()
hex.stroke()
hex.setLineWidth_(8)
NSColor.redColor().set()
hex.dot()
image.unlockFocus()
image.writeToFilePath_('hex.png')

Which results in this: 

Why am I so interested in points and dots? Well, they let me visualize control points for arcs for one thing. Perhaps tomorrow we can explore more along those lines.

Extending NSImage

I’m going to try to post smaller snippets and experiments, more frequently. I’ve been struggling with creating images in code for a game I’m working on and had some successes lately that I wanted to share. Along the way we get to play with Categories, which allow existing Cocoa classes to be extended with new methods at runtime without access to the source code. Categories are in the latest release of PyObJC, but the example I’m giving uses a fresh checkout from the Subversion repository because I turned up a bug which Ronald Oussoren was kind enough to identify and immediately fix. Since this example is already on the bleeding edge, I’ll also dabble with Python2.4 descriptors.

The specific problem I was faced with is that it is easy enough to create images programmatically, or to read them in from a file, but there was no obvious way to save them to a file. A bit of googling led me to Mark Dalrymple’s quickies which has lots of tips and tricks for programming Cocoa in Objective-C and which I got the basics for writing to an image to a file from. I pythonified it and turned it into a method of NSImage through the magic of Categories, adding a bit more along the way.

image_ext.py
from objc import Category
from AppKit import *
from os.path import splitext
_fileRepresentationMapping = { '.png': NSPNGFileType,
                               '.gif': NSGIFFileType,
                               '.jpg': NSJPEGFileType,
                               '.jpeg': NSJPEGFileType,
                               '.bmp': NSBMPFileType,
                               '.tif': NSTIFFFileType,
                               '.tiff': NSTIFFFileType, }
def _getFileRepresentationType(filepath):
    base, ext = splitext(filepath)
    return _fileRepresentationMapping[ext.lower()]
class NSImage(Category(NSImage)):

    def rect(self):
        return (0,0),self.size()

    # If you're using the current release of PyObjC and don't feel like grabbing the fix
    # from the repository, remove this method altogether and read in from files as
    # usual (reading isn't so tricky)
    @classmethod # If you're using Python 2.3 comment this line and uncomment below
    def imageWithFilePath_(cls, filepath):
        return NSImage.alloc().initWithContentsOfFile_(filepath)
    #imageWithFilePath_ = classmethod(imageWithFilePath_)

    def writeToFilePath_(self, filepath):
        self.lockFocus()
        image_rep = NSBitmapImageRep.alloc().initWithFocusedViewRect_(self.rect())
        self.unlockFocus()
        representation = _getFileRepresentationType(filepath)
        data = image_rep.representationUsingType_properties_(representation, None)
        data.writeToFile_atomically_(filepath, False)
    def fillWithColor_(self, color):
        self.lockFocus()
        color.set()
        NSBezierPath.fillRect_(self.rect())
        self.unlockFocus()

I probably should have just elided the imageWithFilePath: method, since it is the only part which is really bleeding-edge, but I was so happy to get it working that I couldn’t bring myself to drop it. In any case, it’s the writeToFilePath: method which I was looking for. In case its not obvious, this will grab the extension of the path you pass in and determine the right type of file to save. The key to saving images in Cocoa is that they have to pass through a subclass of NSImageRep and of NSData before they’re ready to write. This just encapsulates is all in one place.

While I was at it I added fillWithColor: because I thought and image should be able to do that %-)

Next up: Teaching NSBezierPath new tricks.

Not dead yet

I’m still here. I’m going to post the previous examples .dmg with some corrections pointed out by Bob Ippolito (4-space indents, don’t modify data directly in the Bundle). I’ve been quiet because a) I’ve been hitting some walls with Renaissance and investigating work-arounds and alternatives, and b) my coding/blogging time is pretty much between the time I get the kids to bed and the time my wife comes home from tutoring.

I’m investigating the PyGUI and Wax APIs, to see if they are worth porting to run on top of Cocoa (PyGUI runs on Carbon, Wax runs on top of wxPython). Both are attempts to make GUI creation more “Pythonic,” which is a Good Thing™. I have figured out how to get the menus initialized using pure Python (on top of PyObjC, of course), or maybe the newer pyobjc/py2app has fixed the problem, but it is possible to build applications in Python with no Nib file (or Renaissance .gsmarkup file) at all. My earlier inabillity to do that is what drove me to Renaissance in the first place.

I’ve also discovered the nibtool utility, which I did not know about. This allows you to see a textual representation of the nibs created by Interface Builder, search and replace strings (class names, etc.). This is a major discovery. Now if you could take the textual representation and put it back… I’m going to have to investigate this further.

In other news, I will be giving a presentation on Tuesday, February 1 at the Vancouver Zope and Python Users Group (VanPyZ) at 7:00 p.m. It will be a variation on the talk I gave in December to the XML users group, updated with what I’ve been exploring since then. Specifically I will show a simple (Hello World) application built three different ways, with Renaissance, with Interface Builder, and in pure Python. I’ll also show some apps written in other toolkits (wxPython, tkinter) for comparison. I hope some of my readers are close enough to make it.

I’ll also be attending the Northern Voice blogging conference here in Vancouver on Saturday, February 19th. I’m looking forward to meeting some fellow bloggers face to face, rather than RSS to RSS.

Finally, I managed to install Python 2.4 today, and so far nothing has been obviously screwed up, so I’ll be exploring some of the crunchy new features here in the near future.

More posts coming soon. Honest!

google

google

asus