Monday, September 29, 2003


Today, Just van Rossum dropped a complete mind bomb on the Python/OS X/PyObjC community called DrawBot.

From Just's announcement:

I'm happy to announce a first public version of "DrawBot", an environment for 2D graphics programming.

DrawBot is a minimal "IDE", that allows you to write simple Python scripts that generate two-dimensional graphics. The builtin graphics primitives are currently pretty braindead, this version only supports rectangles, ovals and (bezier) paths and polygons. A future version will also support text and images.

DrawBot is written in Python. The binary download is fully self-sufficient (ie. it doesn't need a Python install around), but if you use or build it from the source you'll need Python.framework 2.3 as well as PyObjC 1.0b1 or later. In either case, you need MacOSX 10.2 or later.

Source and Binary are available. Since it is written entirely in Python, relying on the PyObjC bridge to access Cocoa features, the binary release effectively contains the source-- just look in the app wrapper. Update: Just pontificated that the binary download does not include the source. The compiled python code is included in a single zip archive similar to a jar file from the Java world. It makes for a convenient and compact distribution, but you do lose "the binary is the source" feature of the old style deployment.

The source itself is interesting because it is a full featured Cocoa application that is built without using Project Builder. Instead, it uses the buildapp.py script from the MacPython team which allows entire applications (or bundles) to be built using nothing but a simple python script.

Building a first class Mac OS X application requires structuring a tree of directories and files to meet a particular specification (which can be gleaned from the developer documentation). This is specified within buildapp.py as:

buildapp(
    name = "DrawBot",
    mainprogram = "DrawBot.py",
    resources = ["Resources/English.lproj",
                 "Resources/DrawBotFile.icns",
                 "Resources/Credits.rtf",],
    nibname = "MainMenu",
    plist = plist,
    iconfile = "Resources/DrawBot.icns",
    includePackages = ["encodings"],
)

Of course, you also need to specify a property list that contains information about how the application should be integrated with the operating system. Prior to calling the above, the plist variable is defined as:

plist = Plist(
    CFBundleDocumentTypes = [
        Dict(
            CFBundleTypeExtensions = ["py"],
            CFBundleTypeName = "Python Source File",
            CFBundleTypeRole = "Editor",
            NSDocumentClass = "DrawBotDocument",
            CFBundleTypeIconFile = "DrawBotFile.icns",
        ),
    ],
    CFBundleIdentifier = "com.letterror.DrawBot",
    LSMinimumSystemVersion = "10.2.0",
    CFBundleShortVersionString = "0.9a1",
)

Easy!

Using the above, Just created a great environment for exploring 2D graphics using the various APIs provided by Cocoa and OS X. While you can make direct calls like the following (Yes, that really is transparent invocation of methods on Cocoa objects via the PyObjC bridge!):

import os
from Foundation import *
from AppKit import *

appBundle = NSBundle.mainBundle() path = appBundle.pathForResource_ofType_("DrawBotFile.icns", None) image = NSImage.alloc().initWithContentsOfFile_(path)

t = NSAffineTransform.transform() t.scaleBy_(3) t.rotateByDegrees_(5) t.translateXBy_yBy_(15, 5) t.set()

srcRect = ((0, 0), image.size()) image.drawAtPoint_fromRect_operation_fraction_((0, 0), srcRect, NSCompositeSourceOver, 1.0)

Just also provides a set of classes that effectively alias the various drawing operations into a more natural language:

# size: 400 400
from math import *
NPOINTS = 12
# center point:
X = 200
Y = 200
# outer radius:
OUTER = 180
# inner radius:
INNER = 90
# create a new path
p = BezierPath()
# move the pen to the initial position
p.moveto(X, Y + OUTER)
# iterate points
for i in range(1, int(2 * NPOINTS)):
    angle = i * pi / NPOINTS
    x = sin(angle)
    y = cos(angle)
    if i % 2:
        radius = INNER
    else:
        radius = OUTER
    x = X + radius * x
    y = Y + radius * y
    p.lineto(x, y)
p.fill()

Both of the above code snippets represent entire examples that draw pretty pictures.

Neat.
9:45:25 PM  pontificate