|
PIDDLE: A Cross-Platform Drawing Library
Joseph J. Strout
La Jolla, CA
ABSTRACT |
A new open-source drawing library has been written in pure Python.
It is called PIDDLE, an acronym for "Plug-In Drawing, Does Little Else."
The name refers to two of PIDDLE's distinguishing features. First, PIDDLE
only handles drawing; it does not attempt to do page layout, GUI elements
such as menus or buttons, persistent graphics objects, or the like. Instead,
it provides a simple API for drawing lines, curves, polygons, figures,
and text. Second, PIDDLE uses "plug-in" backend renderers to do the actual
drawing, and these renderers may also be portable or make use of platform-specific
features. A backend may generate graphics interactively in a window, write
to a file, or draw to internal buffers. Backends included with the standard
PIDDLE distribution include PDF, PostScript, MacOS QuickDraw, Python Imaging
Library, and others. Use of the common PIDDLE API allows a Python program
to draw in an interactive window, write to a pixel-based file format such
as PNG, or generate a publication-quality PDF or EPS file with exactly
the same user code. Because the API was kept simple, it is fairly straightforward
to write new backends; backends being developed by various authors include
OpenGL, wxPython, Windows, and Adobe Illustrator. We believe that PIDDLE
fills an important niche in Python's toolset, and expect it to become a
standard for low-level, portable drawing operations. |
1. Introduction
1.1. Background
Python's expressiveness, clean design, and power have made it an increasingly
popular language for tasks ranging from CGI to scientific computing. It
includes standard modules for networking, file manipulation, Internet protocols,
and much more, allowing users to write code once that can run anywhere.
But to a large extent, this portability breaks down when graphics are
involved. Unlike newer languages such as Java, Python has no standard GUI
toolkit or drawing API. A variety of graphics solutions are available,
such as Tk, the Python Imaging Library, and various platform APIs (e.g.,
Windows, XLib, and MacOS QuickDraw). Each of these has a unique set of
concepts, functions, and variables, such that code written for one cannot
be used with another without extensive changes.
1.2. Purpose
We propose a standard drawing API to address this issue. An ideal drawing
system should meet the following requirements:
-
Portability -- it should work on any platform where Python can be
installed.
-
Multiple Outputs -- with a single API, we should be able to generate
interactive on-screen graphics, pixel-mapped graphics for use on the web,
print-quality graphics for use in publications and reports, etc.
-
Efficiency -- it should run quickly, especially for interactive
applications, and not require prohibitive amounts of memory.
-
Expressiveness -- the drawing system should not limit the user in
what she can draw.
-
Simplicity -- the API should be straightforward and easy to use.
A drawing system to meet these requirements has been developed. It is called
PIDDLE (Plug-In Drawing, Does Little Else).
1.3. Results
Though some of the requirements in the previous section tend to conflict,
PIDDLE achieves a workable balance. It is portable, written in standard
Python, and it has multiple outputs -- referred to hereafter as "backends".
Its expressiveness is good, providing standard support for Bezier curves,
figures (composed of lines, arcs, and curves), pixel-mapped images, and
rotated text. PIDDLE's quality is excellent, using floating-point precision
as needed for print-quality graphics. Its efficiency is generally good,
taking advantage of the built-in capabilities of the backend wherever possible.
Finally, the API is simple enough to learn in a matter of minutes.
Backends which are complete, or nearly so, at the time of this writing
include the following: PDF and Postscript for print-quality, portable graphics;
QuickDraw, Tk, OpenGL, and wxPython for interactive onscreen graphics;
Adobe Illustrator file format; and the Python Imaging Library for drawing
to a pixel map in memory, and from there saving to the large variety of
formats (such as GIF, JPEG, and PNG) supported by PIL. In addition, there
is a "VCR" backend; this records all the drawing commands given it, optionally
saving them to a human-readable file, and can play them back into another
backend.
2. Design
2.1. Overall class structure
The file and class relationships within PIDDLE are diagrammed in Figure
1. The main class is piddle.Canvas. This is an abstract class; a number
of methods in this class will raise NotImplementedError if called. Other
methods provide default implementations in terms of more basic drawing
primitives. For example, the drawCurve method in piddle.Canvas approximates
a Bezier curve as a polygon, then calls drawPolygon to finish the job.
But drawPolygon itself is an unimplemented method which must be defined
in derived classes.
Figure 1. Object Modeling Technique diagram of the PIDDLE modules
and classes.
The actual drawing is done by one of the specialized canvas types. The
naming conventions for these are as follows: a backend which draws to "Spam"
would be defined in a file called piddleSpam.py, and its specialized canvas
type would be call SpamCanvas. This allows a user to import several canvas
types into the same namespace. Each specialized canvas must implement all
of the methods in piddle.Canvas which would otherwise raise NotImplementedError.
Other methods may also be overridden if the backend rendering system offers
a more efficient or better-quality implementation; e.g., a backend with
built-in support for Bezier curves should use that rather than accept the
default, polygon-based implementation.
In addition to Canvas, the piddle file contains several accessory classes.
Color is an encapsulation of the red/green/blue color space, complete with
standard arithmatic, hashing, and conversion operators; there is also a
HexColor factory function (not shown) which constructs a Color from a six-digit
hexadecimal string as commonly used in HTML. The piddle module also includes
constants for all the standard HTML colors (e.g., "cyan", "olive", and
"skyblue"), as well as a special "transparent" constant used when no drawing
should be done.
Font is an encapsulation of a typeface, size, and attributes. The typeface
is stored as a string or list of strings (more detail is given in section
3 below). All compliant PIDDLE backends must support at least the seven
standard PIDDLE font names (times, helvetica, courier, symbol, monospaced,
serif, and sansserif), and may also support other fonts.
StateSaver is a sentry class which can be used to save and restore a
canvas's default line color, line width, fill color, and font.
2.2. API breakdown
All of the methods in piddle.Canvas are shown in Table 1; these
essentially form the PIDDLE drawing API. There are eleven drawing methods,
such as drawLine, drawRect, and drawImage. Four methods provide font metrics,
often needed for positioning text within the canvas. Three general-purpose
methods provide a way to flush a canvas (force all pending updates to be
written or drawn), to clear it, or set the "info line" -- a little message-display
area available on some interactive canvas types.
Drawing
drawLine
drawLines
drawString
drawCurve
drawRect
drawRoundRect
drawEllipse
drawArc
drawPolygon
drawFigure
drawImage |
Font Metrics
stringWidth
fontHeight
fontAscent
fontDescent |
General
clear
flush
setInfoLine |
Helpers
arcPoints
curvePoints
drawMultiLineString |
Capabilities
isInteractive
canUpdate |
Callbacks
onClick
onOver
onKey |
Table 1. Primary methods and callbacks in the PIDDLE API. Italics
indicate abstract methods -- i.e., those not implemented in the piddle.Canvas.
Two methods are provided for checking a canvas's capabilities; in particular,
some canvases are interactive (e.g., drawing in a window), while others
are noninteractive by their very nature (e.g., PDF). For interactive canvases,
three callback functions are defined, which will be called when the user
clicks in the canvas, moves the mouse over it, or presses a key while the
canvas is active. Finally, there are two helper methods which are seldom
needed by users.
Most of the methods in piddle.Canvas have default implementations. A
minimal derived canvas need only implement the abstract methods, shown
in italics in Table 1. These are four drawing methods (lines, strings,
polygons, and images), plus three font metric methods. Thus, a new PIDDLE
backend may be written by implementing only seven functions.
2.3. Drawing model
PIDDLE is a vector-based drawing system. Though it can also draw to pixel
representations (such as a Python Imaging Library canvas), it has no standard
facilities for manipulating individual pixels (since many backends may
have no concept of "pixels"). All drawing is done is immediate mode, as
opposed to retained; that is, once a line is drawn, there is no way to
refer to or modify the drawn line. A retained-mode, object-based drawing
system could easily be built on top of PIDDLE, but this is beyond the scope
of PIDDLE itself. Finally, note that while the drawing mode is immediate,
the actual output of any particular backend may not appear until the canvas
is flushed.
PIDDLE uses a standard graphics coordinate system, with x values
starting with 0 at the left side of the canvas and increasing to the right,
and y values of 0 at the top and increasing down. The unit of measure
is the "point" defined as 1/72 inch, though the actual size of a point
may vary (or even be undefined) on some backends. All coordinates are interpreted
as floating-point values, and fractional coordinates are explicitly allowed,
though some backends may round to the nearest point.
As noted before, PIDDLE uses the Red/Green/Blue (RGB) color space. Converting
from Cyan/Magenta/Yellow/Black coordinates to RGB is fairly straightforward,
but is not part of PIDDLE.
3. Usage
The standard drawing idiom consists of three steps:
-
instantiate a class derived from piddle.Canvas
-
call drawing methods on that object, such as drawLine or drawString
-
flush the canvas (which updates the file, screen, etc.)
For an interactive application, one would also assign callback functions
to the canvas, which will be invoked on a mouse or keyboard event. These
callbacks would do some additional drawing, then flush the canvas again.
To reset the canvas, one could use the clear() method.
Use of PIDDLE may be most easily illustrated with an example. Listing
1 demonstrates a variety of drawing commands, using the simple create-draw-flush
pattern. The output of this code, generated by piddleQD, is shown in Figure
2. Other backends produce very similar output.
from piddleQD import * # change these to whatever
canvas = QDCanvas() # backend you want to test
canvas.defaultLineColor = Color(0.7,0.7,1.0) # (light blue)
canvas.drawLines( map(lambda i:(i*10,0,i*10,300), range(30)) )
canvas.drawLines( map(lambda i:(0,i*10,300,i*10), range(30)) )
canvas.defaultLineColor = black
canvas.drawLine(10,200, 20,190, color=red)
canvas.drawEllipse( 130,30, 200,100, fillColor=yellow, edgeWidth=4 )
canvas.drawArc( 130,30, 200,100, 45,50, fillColor=blue, edgeColor=navy, edgeWidth=4 )
canvas.defaultLineWidth = 4
canvas.drawRoundRect( 30,30, 100,100, fillColor=blue, edgeColor=maroon )
canvas.drawCurve( 20,20, 100,50, 50,100, 160,160 )
canvas.drawString("This is a test!", 30,130, Font(face="times",size=16,bold=1),
color=green, angle=-45)
polypoints = [ (160,120), (130,190), (210,145), (110,145), (190,190) ]
canvas.drawPolygon(polypoints, fillColor=lime, edgeColor=red, edgeWidth=3, closed=1)
canvas.drawRect( 200,200,260,260, edgeColor=yellow, edgeWidth=5 )
canvas.drawLine( 200,260,260,260, color=green, width=5 )
canvas.drawLine( 260,200,260,260, color=red, width=5 )
canvas.flush()
|
Listing 1. A sample program using PIDDLE. The output is
shown in Figure 2.
Figure 2. Output from Listing 1.
PIDDLE comes with a reference manual describing each drawing function,
with default parameter values and example usage. The code also uses docstrings
throughout, allowing users to get quick help on classes and functions interactively
or via an automated tool. A number of examples (including the one above)
are available via the PIDDLE web site, and the distribution comes with
an interactive test program that allows one to pair any test with any standard
backend. Alpha testers have found use of PIDDLE to be straightforward.
4. Implementation Issues
4.1. Handling of nonstandard extensions
PIDDLE defines a fairly minimal vector drawing API. Many backends have
capabilities beyond this minimal set, and may choose to expose some of
these extra functions by additional methods on the derived canvas class.
For example, a PILCanvas has a "getImage" method which returns the underlying
PIL Image object, allowing the user to manipulate individual pixels, apply
filtering operations, etc. Similarly, PDFCanvas has some methods which
expose additional functionality of PDF.
Users who wish to write portable PIDDLE code are responsible for taking
care to avoid such non-standard extensions. The easiest way to test whether
a piece of drawing code uses only standard PIDDLE drawing functions is
to simply try it with another type of canvas. Several backends (PostScript,
PDF, VCR) run on all platforms with only standard Python modules, so multiple
canvas types are always available.
When possible, backend developers should code their custom canvases
in such a way that the nonstandard extensions go through some sort of "bottleneck"
method or object, as getImage does for PIL. This will make it far more
obvious to the user when she is going beyond the standard PIDDLE API.
Naturally, many specific applications require only a one or a limited
set of backends; e.g., a CGI-based application generating graphics for
the web may use only piddlePIL, and never need to generate print-quality
drawings. In this case, there is no reason not to use the nonstandard extensions
to their fullest extent.
4.2. Dealing with rotated strings
It was decided early in the design phase to include support for rotated
strings. This is an important feature, often needed for charts, graphs,
and diagrams. However, it is also a feature lacking in many low-level drawing
APIs, and so is a frequent sticking point for backend developers. Several
brief case studies will illustrate the various solutions used.
First, consider the Python Imaging Library. It has a simple drawing
API that includes drawing of non-rotated, but not rotated, strings. The
solution in this case was to create two temporary Image objects, into which
the string is drawn; one in color, and the other in black for use as a
mask. The images are then rotated (using another PIL built-in function),
and copied to the PILCanvas image using the mask. While somewhat complex,
this solution works and requires no Python extensions other than PIL itself.
MacOS QuickDraw also has no built-in support for rotated strings, and
no built-in way to rotate a pixel map either. Making a compliant PIDDLE
backend for QuickDraw therefore required making a compiled Python extension
module for this purpose. This module is made available via the PIDDLE web
site, and may be included in future distributions of MacPython. PiddleQD
can be used without this extension module, but attempts to draw a rotated
string will raise an exception.
A similar problem is faced by wxPython, but the difficulty here is compounded
by the cross-platform nature of the backend (wxWindows). Various strategies
are being investigated, but a satisfactory solution has not yet been found.
4.3. Other common difficulties
Rotate strings are often the most difficult feature of PIDDLE for new backends
to support. Two other features have also been sources of difficulty: polygon
filling, and display of images.
When the edges of a complex polygon cross several times, there can be
regions (such as the center of the star in Figure 2 which may be
considered either inside or outside the polygon, depending on the fill
rule used. The two common fill rules are "even-odd" and "winding". Both
are sensible rules, but unfortunately, the industry has not standardized
on one or the other; for example, MacOS QuickDraw uses only even-odd filling,
whereas Tk uses the winding rule. It was decided to allow this feature
to vary from backend to backend.
Second, PIDDLE supports the drawing of Python Imaging Library (PIL)
images on systems with PIL installed, thereby allowing the user to combine
pixel-mapped and vector graphics for maximum flexibility. This is often
a sticking point for new backends which emphasize vector graphics and have
only limited support for pixel maps; a conversion function between PIL
images and the backend pixel-map format is usually required. So far, this
has not been a prohibitive difficulty for any backend.
5. Conclusion & Future Plans
PIDDLE provides a simple yet powerful vector graphics API. It acts as a
standard interface layer between Python and a set of backend rendering
systems that is already usefully large, and growing larger. It is expected
that more backends will be added whenever the need arises.
A large part of PIDDLE's success is attributable to the simplicity of
its API; by keeping the feature set small, it has been relatively easy
to build, debug, and add new backends. However, a few features which did
not make it into the first version of PIDDLE are so handy that they will
probably be added in a future version. Foremost is clipping -- i.e., limiting
the drawing to a rectangular area. Coordinate transformations (probably
barring rotation) will also be considered.
6. Acknowledgements
PIDDLE has been a group effort from its very inception, and a shining example
of the power of the open-source model. It could not have succeeded without
the contributions of all participants. While the comments of all members
of the PIDDLE mailing list have been very helpful in shaping PIDDLE's design,
the author would like to particularly thank the following contributors
of code:
-
David Ascher: for the OpenGL backend and insightful comments on design
-
Bill Bedford: for the Adobe Illustrator backend
-
Magnus Lie Hetland: for the PostScript backend, and even more for playing
a critical part in the early design
-
Kevin Jacobs: for the wxPython backend
-
Christopher Lee: for jumping in wherever help was needed
-
Andy Robinson: for the PDF and PythonWin backends, assistance with PostScript
issues, and valuable add-ons and evangelism
-
Perry Stoll: for the Tk backend
-
Michelle Strout: for design input and moral support
|