September 94 - Making the Most of QuickDraw GX Bitmaps
Making the Most of QuickDraw GX Bitmaps
DAVID SUROVELL
Besides letting you do a lot of cool things with geometric shapes and typography,
QuickDraw GX has useful tools for manipulating bitmaps. For example, bitmap
shapes (the QuickDraw GX counterpart to pixMaps) can be skewed, rotated, and
scaled, and transforms allow these operations to be performed repeatedly without data
loss. Bitmap shapes can share image data, can be used to clip other shapes, and can
reside on disk instead of in memory. This article tells how you can use QuickDraw
GX to improve the way you handle bitmapped graphics.
New users of QuickDraw GX will probably start by going throughInside Macintosh: QuickDraw GX
Objects or the article "Getting Started With QuickDraw GX" indevelop Issue 15. If you're mainly a
QuickDraw programmer, however, you may have
a lot of questions about how QuickDraw GX applies specifically to bitmaps -- probably the most
commonly used graphic objects. As it turns out, it can do most anything QuickDraw can do, and
quite a few useful and exotic new things besides.
If you have at least a nodding familiarity with QuickDraw GX, this article will give useful tips on how
to apply your knowledge to bitmap shapes. If you're a QuickDraw GX neophyte, this article will
confuse you from time to time, but you may learn enough to decide to make the leap to QuickDraw
GX.
CREATING BITMAP SHAPES
It takes about the same information to create a bitmap shape in QuickDraw GX as
it does to make a pixMap in QuickDraw. The biggest difference is that while QuickDraw insists that
you calculate the size of the image buffer and allocate it explicitly, QuickDraw GX can optionally
allocate it for you when the shape is created. This is illustrated in the code in Listing 1, which creates
an indexed bitmap shape.
For indexed pixelSize values (1, 2, 4, or 8), you set the gxBitmap's space field to gxIndexedSpace and
its set field to a color set (the QuickDraw GX equivalent of a QuickDraw color table) with an
appropriate number of entries. Direct pixelSize values (16 or 32) require that the set field be nil. Forexample, to make the routine in Listing 1 create a 16-bit bitmap shape, you would set the gxBitmap's
space field to gxRGB16Space and its set field to nil.
Listing 1. Creating an indexed bitmap shape
gxShape CreateIndexedBitmapShape(long horiz, long vert,
long targetDepth)
{
gxBitmap bitShapeInfo;
gxColorSet targetSet;
gxShape resultShape;
if ((horiz <= 0) || (vert <= 0))
return nil;
if (targetDepth > 8)
return nil;
// Create a familiar "color" gxColorSet.
// (The default gxColorSet is a gray ramp.)
targetSet = GetStandardColorSet(targetDepth);
if (targetSet == nil)
return nil;
// Let QDGX calculate the image buffer block size and
// allocate it.
bitShapeInfo.image = nil;
bitShapeInfo.rowBytes = 0;
bitShapeInfo.width = horiz;
bitShapeInfo.height = vert;
bitShapeInfo.pixelSize = targetDepth;
bitShapeInfo.space = gxIndexedSpace;
bitShapeInfo.set = targetSet;
// Use the default color profile.
bitShapeInfo.profile = nil;
resultShape = GXNewBitmap(&bitShapeInfo, nil);
return resultShape;
}
Note that the gxBitmap's rowBytes is a long, not a short as in QuickDraw. This means no more
convoluted rowByte hacks, no more magic bits needed for flags, and no more unreasonable limits on
image width.
Note also that the gxBitmap contains a profile field, a reference to a gxColorProfile (essentially an
object with ColorSync data wrapped inside). If this field is nil, QuickDraw GX uses its default
profile. Color matching occurs only when the target view port has the gxEnableMatchPort attribute
set -- by default, it's off.
MANIPULATING BITMAP SHAPES
Once a bitmap shape is created, you can access and change its characteristics with GXGetBitmap and
GXSetBitmap.
GXGetBitmap(targetShape, &bitmapInfo, &origin);
// Alter the necessary gxBitmap fields here.
. . .
GXSetBitmap(targetShape, &bitmapInfo, &origin);
GXSetBitmap is similar to QuickDraw's UpdateGWorld; it lets you change bitmap depth, color
specification, and size. To change specific attributes, you may need to modify a combination of fields.
To change a bitmap's width or height, set the width or height field. If QuickDraw GX originally
allocated the image buffer, you can set rowBytes to 0 and the image field to nil, and QuickDraw GX
will reallocate the buffer. If you allocated the buffer yourself, you'll have to maintain it yourself.
An image isn't scaled when you change size this way. If you increase the width or height, the new areas
contain undefined values; if you decrease them, the image is truncated. Bitmap scaling is discussed later
in this article.*
To change a bitmap's pixel depth, set the pixelSize field to the desired depth. If the bitmap needs a
new color set (which it will, unless the new depth is greater than 8 bits), create it and assign it to the
set field. An example that changes the depth to
4-bit is shown in Listing 2.
To change a bitmap's color characteristics, just change the set, space, and profile fields. No changes
to pixel data will occur -- all pixel values will be interpreted in the new color set. To transform pixel
values, you'd need to set up a new bitmap shape and draw the existing bitmap into it. (The offscreen
library routine CopyToBitmaps is ideal for this.)
Listing 2. Changing the depth of a bitmap shape
void ChangeDepthToFour(gxShape bitmapShape)
{
gxBitmap imageInfo;
if ((bitmapShape != nil) &&
(GXGetShapeType(bitmapShape) == gxBitmapType))
{
GXGetBitmap(bitmapShape, &imageInfo, nil);
if (imageInfo.pixelSize != 4)
{
imageInfo.pixelSize = 4;
imageInfo.space = gxIndexedSpace;
imageInfo.set = GetStandardColorSet(4);
GXSetBitmap(bitmapShape, &imageInfo, nil);
}
}
}
USING DISK-BASED PIXEL IMAGES
QuickDraw GX provides support for disk-based bitmap shapes. They're structurally the same as
regular bitmaps, except that their image data is contained in a file, so they're always drawn from disk.
Ten calls to GXDrawShape(diskBitmap) means QuickDraw GX reads the entire file from disk ten
times. (QuickDraw GX can't assume that you didn't write into the file between accesses.) The idea is
that the file system's disk caches will do the work; if the file wasn't changed, subsequent reads should
be cached.
Make sure the file size is at least as large as the bitmap, or you'll get an "unexpected end of file" error. *
Disk-based bitmaps have limitations. For one thing, certain routines can't be performed on them --
GXSetShapePixel, for example. (SeeInside Macintosh: QuickDraw GX Graphics for the complete list.)
You can't use disk-based bitmap shapes as drawing destinations. If you draw into the data you trigger
an error. So how do you create a disk-based bitmap? As shown in Listing 3, you first set the gxBitmap's image
field to gxBitmapFileAliasImageValue. After creating the bitmap shape, create a tag of type
gxBitmapFileAliasTagType containing an alias record that references the file containing the target
raster data and attach it to the shape.
ACCESSING IMAGE DATA
You can manipulate the image data of bitmap shapes directly. If the image data is maintained by your
application, all you have to do is call GXChangedShape afterward. If the image data was allocated by
QuickDraw GX, it's more complicated:
- Force the shape to be heap-resident with GXSetShapeAttributes.
- Lock the shape with GXLockShape and check for an error.
- Call GXGetShapeStructure to obtain a reference to the image data.
- Read from or write to the image data as desired.
- If the image data was changed, call GXChangedShape.
- Unlock the shape with GXUnlockShape.
- Call GXSetShapeAttributes to allow the shape to be cached again.
Listing 3. Creating a disk-based bitmap
gxShape CreateDiskBitmap(FSSpec *fsData, gxBitmap *targetBM)
{
gxBitmap localBM;
gxShape targetShape;
gxTag targetTag;
if ((fsData == nil) || (targetBM == nil))
return nil;
targetShape = nil;
targetTag = CreateBitmapAliasTag(fsData, 0L);
if (targetTag != nil)
{
localBM = *targetBM;
localBM.image = gxBitmapFileAliasImageValue;
targetShape = GXNewBitmap(&localBM, nil);
if (targetShape != nil)
GXSetShapeTags(targetShape, gxBitmapFileAliasTagType,
1L, -1L, 1L, &targetTag);
GXDisposeTag(targetTag);
}
return targetShape;
}
gxTag CreateBitmapAliasTag(FSSpec *bitmapFS,
unsigned long fileOffset)
{
struct gxBitmapDataSourceAlias *aliasRecordPtr;
gxTag targetTag;
FSSpec targetFS;
AliasHandle aliasHdl;
OSErr iErr;
long aliasSize, aliasRecordSize;
Boolean wasChanged;
targetTag = nil;
aliasHdl = nil;
aliasRecordPtr = nil;
// Create an alias and resolve it.
iErr = NewAlias(nil, bitmapFS, &aliasHdl);
if (iErr == noErr)
iErr = ResolveAlias(nil, aliasHdl, &targetFS, &wasChanged);
// Build up a compact representation for inclusion into a gxTag.
if (iErr == noErr)
{
aliasSize = GetHandleSize((Handle)aliasHdl);
aliasRecordSize = aliasSize + 2 * sizeof(long);
aliasRecordPtr = (struct gxBitmapDataSourceAlias*)
NewPtr(aliasRecordSize);
iErr = MemError();
}
// Create the gxTag.
if (iErr == noErr)
{
// Create a gxBitmapDataSourceAlias with specified fileOffset
// and appropriate aliasRecordSize and aliasRecord.
aliasRecordPtr->fileOffset = fileOffset;
aliasRecordPtr->aliasRecordSize = aliasSize;
BlockMove(*aliasHdl, &aliasRecordPtr->aliasRecord[0],
aliasSize);
targetTag = GXNewTag(gxBitmapFileAliasTagType,
aliasRecordSize, aliasRecordPtr);
}
// Clean up.
if (aliasHdl != nil)
DisposeHandle((Handle)aliasHdl);
if (aliasRecordPtr != nil)
DisposePtr((Ptr)aliasRecordPtr);
return targetTag;
}
GXLockShape loads an image into memory, so it might not succeed if there isn't enough memory.
And don't forget to check a bitmap shape's space field before processing the shape -- don't assume
that bitmap images are always in RGB space.
See Listing 4 for an example of changing a bitmap shape's data directly.
MEMORY ISSUES
Raster surfers and Photoshop junkies know that raster images can be memory hogs; it's easy to run
out of application heap when you allocate them. So what happens when QuickDraw GX runs out of
memory? It doesn't. Well, almost never. Here are the steps it will go through, in order, to deliver the
memory you need:
- Flush out-of-date caches.
- Flush up-to-date caches.
- If allowed, grow the current gxHeap.
- Unload shapes and other objects to disk.
- Give up, and return an error.
Most QuickDraw developers resort to some sort of GrowZoneProc to handle a
tight application heap. QuickDraw GX provides a tiered response to abnormal occurrences. Items 1
through 4 above return notices (in the debugging version of QuickDraw GX); item 5 returns an
error. All you have to do is implement a routine to handle the notices and errors.
Listing 4. Directly changing an indexed bitmap shape
void InvertBitmapShape(gxShape sourceBits)
{
gxBitmap sourceInfo, *sourceInfoRef;
gxShapeAttribute curAttributes;
unsigned char *sourcePtr, *rowPtr;
long sourceRowSize, structLen, i, j;
Boolean isQDGXImage;
// Make sure that this is an indexed bitmap shape.
if (sourceBits == nil)
return;
if (GXGetShapeType(sourceBits) != gxBitmapType)
return;
GXGetBitmap(sourceBits, &sourceInfo, nil);
if (sourceInfo.pixelSize > 8)
return;
if (sourceInfo.image == gxBitmapFileAliasImageValue)
return;
// If the image data was allocated by QuickDraw GX...
isQDGXImage = (sourceInfo.image == nil);
if (isQDGXImage)
{
// Load and lock the image data.
curAttributes = GXGetShapeAttributes(sourceBits);
if (!(curAttributes & gxDirectShape))
GXSetShapeAttributes(sourceBits,
curAttributes | gxDirectShape);
GXLockShape(sourceBits);
if (GXGraphicsError(nil) != 0)
return;
// Get a reference to the image data.
sourceInfoRef =
(gxBitmap*)GXGetShapeStructure(sourceBits, &structLen);
if ((sourceInfoRef == nil) || (structLen < sizeof(gxBitmap)))
return;
sourceInfo = *sourceInfoRef;
}
// Invert index values, one row at a time.
sourcePtr = (unsigned char*)(sourceInfo.image);
for (i = sourceInfo.height; i > 0; i--)
{
rowPtr = sourcePtr;
sourceRowSize = sourceInfo.rowBytes;
while (sourceRowSize-- > 0)
{
*rowPtr = ~*rowPtr;
rowPtr++;
}
// Skip to the next row.
sourcePtr = (unsigned char*)sourcePtr + sourceInfo.rowBytes;
}
GXChangedShape(sourceBits);
if (isQDGXImage)
{
GXUnlockShape(sourceBits);
GXSetShapeAttributes(sourceBits, curAttributes);
}
}
GEOMETRIC OPERATIONS
One of the niftiest features of QuickDraw GX is the ability to perform geometric operations on
bitmap shapes. Most of the operators that apply to geometric shapes also apply to bitmaps: rotate,
scale, skew, perspective, and clip. In comparison, QuickDraw provides only three geometric
operators: scale, clip, and mask.
ALTERING THE TRANSFORM VERSUS THE GEOMETRY
When you change a bitmap shape's geometry (that is, its actual pixel data), whether by rotating,
skewing, applying perspective, or scaling, you normally lose image data -- it's often impossible to
return the image to its pristine state.
You can eliminate this data loss by instead applying geometric operators to a shape'stransform. A
shape can make use of a 3 x 3 matrix to mathematically change its appearance when rendered without
changing the underlying data. This is especially important for bitmaps. Figure 2 shows both
possibilities of multiple rotations of a bitmap.
Figure 2. Successive rotations of a bitmap
Rotation, translation (change in origin), skew, perspective, and scale operations can all be performed
on transforms directly, by GXRotateTransform, GXSkewTransform, and so forth, or indirectly,
using the gxMapTransformShape attribute.
When a shape's gxMapTransformShape attribute is set, geometric operations automatically apply to
its transform rather than its geometry. Bitmap and picture shapes default to having this attribute set;
other shapes begin with it off. This
means that if you convert a polygon shape (for example) to a bitmap shape, the
gxMapTransformShape attribute won't automatically be set.
Figure 3. Effect of GXRotateShape on bitmap geometry
When a QuickDraw GX routine modifies a bitmap shape's geometry, a clip shape is often attached to
define the geometric extent of the modified bitmap. More often than not, the bitmap's image buffer
is expanded, as shown in Figure 3. Rotating a bitmap's geometry can increase its memory
requirements by over 40%.
ROTATION
There aren't many QuickDraw programmers who haven't wished for a simple way to rotate bitmaps.
GXRotateShape takes parameters for the target shape, degrees clockwise to rotate, and center point
of rotation, as shown in Listing 5.
Listing 5. Rotating a bitmap shape
void RotateBitmap(gxShape targetShape, Fixed theta)
{
gxBitmap targetBM;
gxPoint origin, shCenter;
// Determine the bitmap shape's current center point.
GXGetBitmap(targetShape, &targetBM, &origin);
shCenter.x = ff(targetBM.width) / 2 + origin.x;
shCenter.y = ff(targetBM.height) / 2 + origin.y;
// Rotate it around its center point.
GXRotateShape(targetShape, theta, shCenter.x, shCenter.y);
}
SKEWING AND PERSPECTIVE
Skewing and perspective are just as much fun as rotation, and even more useful as general-purpose
graphic effects. The code in Listing 6 illustrates a simple type of perspective; Figure 4 shows the
results of this perspective mapping.
SCALING
You can expand or shrink bitmap shapes, like other shape types, with GXScaleShape. QuickDraw
pixMaps are scaled by setting the destination rectangle passed to CopyBits, whereas GXScaleShape
uses a scaling factor. To convert your QuickDraw bitmap scaling code into the equivalent
QuickDraw GX code, you have to calculate this scaling factor. Listing 7 shows how.
You can flip a bitmap horizontally or vertically by using negative scaling values. *
Listing 6. Applying perspective to a bitmap shape
void TrapezoidalWarp(void)
{
gxShape bitsShape, warpShape;
long trapezoidData[] =
{
1L, 4L,
ff(130), ff(100), ff(170), ff(100),
ff(200), ff(200), ff(100), ff(200)
};
bitsShape = CreateBasicBitmapShape();
warpShape = GXNewShapeVector(gxPolygonType, trapezoidData);
if (warpShape != nil)
{
ShapeSetPolyMap(bitsShape, warpShape);
GXDisposeShape(warpShape);
}
GXDrawShape(bitsShape);
}
void ShapeSetPolyMap(gxShape targetShape, gxShape mappingShape)
{
gxRectangle boundsRect;
gxPolygon *mapPoly, *targetPoly;
gxMapping theMapping;
gxShape targetBounds;
long ignored;
if (targetShape == nil)
return;
if ((mappingShape == nil)
|| (GXGetShapeType(mappingShape) != gxPolygonType))
return;
// Determine the dimensions of the target shape.
GXGetShapeBounds(targetShape, 0L, &boundsRect);
targetBounds = GXNewRectangle(&boundsRect);
if (targetBounds == nil)
return;
// Scale the mapping shape to the dimensions of the target shape.
GXSetShapeBounds(mappingShape, &boundsRect);
GXSetShapeType(targetBounds, gxPolygonType);
// Load & lock both shapes so that their structures can be
// accessed.
GXSetShapeAttributes(mappingShape,
GXGetShapeAttributes(mappingShape) | gxDirectShape);
GXLockShape(mappingShape);
GXSetShapeAttributes(targetBounds,
GXGetShapeAttributes(targetBounds) | gxDirectShape);
GXLockShape(targetBounds);
// NOTE: Structure is actually of type gxPolygon.
mapPoly =
(gxPolygon*)GXGetShapeStructure(mappingShape, &ignored);
targetPoly =
(gxPolygon*)GXGetShapeStructure(targetBounds, &ignored);
if ((mapPoly != nil) && (targetPoly != nil))
{
// Skip past the gxPolygons contour count to the first
// contour.
mapPoly = (gxPolygon*)((Ptr)mapPoly + sizeof(long));
targetPoly = (gxPolygon*)((Ptr)targetPoly + sizeof(long));
// Calculate the desired shape mapping.
// PolyToPolyMap() is in "mapping library.c."
PolyToPolyMap(targetPoly, mapPoly, &theMapping);
}
// Release both shapes from bondage.
GXUnlockShape(mappingShape);
GXSetShapeAttributes(mappingShape,
GXGetShapeAttributes(mappingShape) & ~gxDirectShape);
GXUnlockShape(targetBounds);
GXSetShapeAttributes(targetBounds,
GXGetShapeAttributes(targetBounds) & ~gxDirectShape);
// Set the target shape's mapping as desired.
GXSetShapeMapping(targetShape, &theMapping);
GXDisposeShape(targetBounds);
}
Figure 4. Applying perspective to a bitmap shape
Listing 7. Calculating a scaling factor
void BitmapShapeScaleQDStyle(gxShape targetShape, Rect *qdSourceR,
Rect *qdDestR)
{
gxPoint centerPt;
fixed scaleFactorH, scaleFactorV;
scaleFactorH = FixRatio(qdSourceR.right - qdSourceR.left,
qdDestR.right - qdDestR.left);
scaleFactorV = FixRatio(qdSourceR.bottom - qdSourceR.top,
qdDestR.bottom - qdDestR.top);
centerPt.x = ff((qdDestR.right + qdDestR.left) / 2);
centerPt.y = ff((qdDestR.bottom + qdDestR.top) / 2);
GXScaleShape(targetShape, scaleFactorH, scaleFactorV, centerPt.x,
centerPt.y);
GXMoveShapeTo(targetShape, ff(qdDestR.left), ff(qdDestR.top));
GXDrawShape(targetShape);
}
CLIPPING AND MASKING
QuickDraw GX can do some neat tricks with clipping. These tricks work with bitmap shapes, too.
For example, to create a gradient-filled polygon, you can make a rectangular bitmap shape with a
gradient and then set the polygon shape as the bitmap's clip shape. (For another example, see
Graphical Truffles in this issue.)
You can use 1-bit bitmap shapes as clip shapes, too. The effect is just like that of CopyMask; pixels in
the source shape are drawn only where the clipping bitmap pixel value is nonzero. (On this issue's
CD, you'll also find example code that does image processing similar to CopyDeepMask using the
new transfer modes.)
Clipping occurs in geometry space, before transform mapping, so a bitmap's clip shape should be based
on its bounds rectangle, not its rendered location. *
To convert geometric shapes into masking bitmap shapes, you can call the GXSetShapeType routine
to convert the shape to a 1-bit mask bitmap.
With GXCheckBitmapColor, you can generate a masking bitmap from an existing bitmap shape. If
you pass GXCheckBitmapColor a color set, it puts 0 in the result bitmap for source pixel values that
are in the color set. If you pass it a color profile, it puts 0 in the result bitmap for source pixel values
that are within the color profile's gamut. The result bitmap can be useful for color correction.
QUICKDRAW GX TRICKS FOR QUICKDRAW DOGS
QuickDraw GX has ways to do almost anything you can do with QuickDraw. All you need to know
is how their environments and feature sets compare, and you'll understand how to convert from one
to the other.
THE VIEW PORT LIST VERSUS THE GRAPHICS PORT
Most of the time you won't have to concern yourself with view ports at rendering time, because
there's no sense of the "current port" as there is in QuickDraw. Here's the recommended method for
drawing an existing shape into a new view port:
- Copy the shape's transform and install the desired destination view port into the
copy.
- Call GXDrawShape.
- Restore the original transform.
- Dispose of the copied transform.
Examples of preserving view port lists can be found in the library routine CopyToBitmaps and in the
DrawShapeOffscreen example later in this article
(Listing 9).
BITMAPS AND TRANSFER MODES
QuickDraw GX has a lot of transfer modes. This is a good thing, really. Not alltransfer modes are
functionally equivalent to those in QuickDraw, but the transferModelibrary is fairly complete. Many
of the capabilities of QuickDraw search procedures can be implemented using transfer modes. (The
first page ofInside Macintosh: QuickDraw GX Graphics has color pictures of the new transfer modes in
action.)
The transfer mode is contained in a shape's ink. Since transfer modes are applied on a per-
component basis, you can easily get some groovy effects. For example, you can add the hue of one
image to the brightness of another. Usually, though, you'll want all components to use the same
mode. The transferMode library routine SetCommonTransfer will do this for you.
There are some differences between QuickDraw GX transfer modes and those found in QuickDraw:
- Dithering is a view port feature, not a transfer mode. Halftoning is also available
on a per-gxViewPort basis. These two features are mutually exclusive; you can't
dither and halftone at the same time.
- Transparency is not a single mode. It's a whole family of modes based on alpha
component values.
- All QuickDraw GX transfer modes occur in color space, while some QuickDraw
transfer modes are bitwise.
ONSCREEN BITMAPS
QuickDraw GX maintains a view device list that mirrors the QuickDraw GDevice list. (Utility
routines are provided for getting one if you have the other.) The Window Manager is patched in a
couple of places so that a window's view port transforms and image memory are maintained when it
enters and leaves GDevice real estate.
Drawing a bitmap onscreen obeys the screen GDevice's index entry protections -- QuickDraw GX
doesn't use indexes reserved by the Palette Manager for other applications. If you want to draw an
image that uses animated palette entries, you'll need to clone references to the destination
viewDevice color set and profile, and then insert those references into the bitmap shape before
drawing. Example code that does this is on this issue's CD.
COPYBITS IN QUICKDRAW GX
Let's see what it takes to make GXDrawShape do what CopyBits does. CopyBits has several explicit
parameters: the source, destination, clipping region, and transfer mode. In QuickDraw GX, the
source is the bitmap shape. The destination is defined by the shape's view port list. The clipping
region is any shape that you attach to the bitmap shape with GXSetShapeClip. As mentioned before,
the transfer mode is contained in the shape's ink.
So, to do a CopyBits-style blit in QuickDraw GX:
- Set up the shape's view port list.
- Determine the transfer mode (usually just "copy," but it's your choice).
- Adjust the shape clip. Don't change the device clip or view port clip.
- Adjust the transform if you want to reposition, scale, skew, rotate, or apply
perspective to the shape.
- Call GXDrawShape.
- Clean up as needed.
QuickDraw GX doesn't implement all of the color capabilities of CopyBits. There's no colorizing and no
color interpolation for indexed values beyond the end of a bitmap's color set. *
DRAWING OFFSCREEN WITH QUICKDRAW GX
Successive QuickDraw implementations have presented newer and better ways to draw into a
offscreen image buffer. The QuickDraw GX offscreen library contains routines to help maintain the
data structures necessary to implement the equivalent of a GWorld.
The example in Listing 8 uses the CreateIndexedBitmapShape routine from Listing 1 and the library
routine CreateOffscreen to create a fully functional offscreen bitmap.
You might think drawing into a QuickDraw GX offscreen bitmap would be difficult, but it's not. To
draw a shape into the offscreen bitmap, set its view port list to the offscreen bitmap's view port and
call GXDrawShape (see Listing 9).
Listing 8. Creating an offscreen bitmap
OSErr MakeIndexedOffscreen(offscreen *targetOffWorld, long horiz,
long vert, long targetDepth)
{
gxShape bitsShape;
if (!CheckArguments(...))
return paramErr;
bitsShape = CreateIndexedBitmapShape(horiz, vert, targetDepth);
if (bitsShape == nil)
return paramErr;
CreateOffscreen(targetOffWorld, bitsShape);
return noErr;
}
Listing 9. Drawing into an offscreen bitmap
void DrawShapeOffscreen(offscreen *offGXWorld, gxShape targetShape)
{
gxTransform newXform, savedXform;
if ((offGXWorld == nil) || (targetShape == nil))
return;
if (offGXWorld->port == nil)
return;
savedXform = GXGetShapeTransform(targetShape);
newXform = GXCopyToTransform(nil, savedXform);
GXSetTransformViewPorts(newXform, 1L, &(offGXWorld->port));
GXSetShapeTransform(targetShape, newXform);
GXDrawShape(targetShape);
GXSetShapeTransform(targetShape, savedXform);
GXDisposeTransform(newXform);
}
BITMAP SHAPES VERSUS PIXMAPS
Sometimes, converting existing QuickDraw code to QuickDraw GX is impractical. If your
application needs to use the same data in both offscreen pixMaps and bitmap shapes, it can, provided
that the bitmap shape is packed the same as the pixMap -- that is, of identical width, height, pixel
depth, and color space.
To use bitmap shape data in a QuickDraw pixMap, build the pixMap with the baseAddr the same as
the gxBitmap.image. (Make sure that the bitmap shape is locked down.) To use pixMap data in
QuickDraw GX, create a gxBitmap with the image field set to the base address of the source pixMap.
THE QUICKDRAW GX LIBRARIES
Several libraries are included with the QuickDraw GX Software Developer's Kit. They contain,
among other things, routines for offscreen rendering and converting image data between QuickDraw
and QuickDraw GX. The library code instructs by example and is a good starting point for your own
library.
The library code is not completely tested. You should treat it as template code, not a final solution. *
The offscreen library. This library contains support for offscreen bitmaps, copyingbetween bitmap
shapes, and simple gradient fills. The offscreen image implementationis basic but solid (it lacks some
of the features found in QuickDraw GWorlds, such as automatic longword realignment of images).
The utility routine CopyToBitmaps is also useful; it shows a good example of saving a view port list.
The math library. This library contains a number of useful routines for manipulatingmappings. The
routine PolyToPolyMap is used in the trapezoidal warp example (Listing 6). The header file math
routine.h contains essential macros for conversion between fixed-point, floating-point, and integral
values.
The ramp library. Get your gradient fills here. Pleasing to the eye, easy on the code. A gradient-filled
bitmap can be rotated and clipped, and voilà! Gradient-filled shapes.
The qd and oval libraries. The qd library has facilities for conversion of bitmap and color data between
QuickDraw and QuickDraw GX formats. The oval library has real ovals, not those phony squished
QuickDraw things.
The transferMode library. This library facilitates access to a shape's transfer mode information and
contains routines for emulating most of the QuickDraw transfer modes. It also contains a bonus --
one of my favorite routines. If you've ever wanted to get the results of a QuickDraw transfer mode
on color values without having to use CopyBits, TransmogrifyColor is for you. Check it out.
The storage library. This library implements spooling routines for use with GXFlattenShape and
GXUnflattenShape, which you'll need for reading and writing shapes to and from files. These
routines detect errors but don't report them, so they're only useful as templates.
The camera library. Perspective is cool, but hard to use unless your math skills are well developed.
This library provides nifty 3-D techniques.
AND A FEW MORE THINGS . . .
Here I'll point out some caveats and additional interesting features of QuickDraw GX, just so you
know what to look for (and look out for).
EXECUTION OVERHEAD
How fast are QuickDraw GX blits? How slow does an offscreen, 256 x 256, 45º-rotated, 32-bit,
YXY, gradient-filled bitmap draw into a window on a 4-bit monitor? How much for all of these shiny
pebbles? It depends. Let's look at the issues involved. QuickDraw GX and QuickDraw have much in
common here:
- They're fastest when there's no conversion of value or image location.
- Common code paths are optimized inside the API: 8-bit to 8-bit,
1-bit to 1-bit, 24-bit to 8-bit, no clipping, rectangle clipped.
- Blits involving complex transformations are usually orders of magnitude slower.
Some transformations require more processing. QuickDraw GX does only as much work as the
transformation matrix mandates. From fastest to slowest, the order is: no transformation (or translation
only); scaling; skewing or rotation; perspective.*
The basic performance guidelines are similar to automotive fuel efficiency ratings -- though we have
no hard estimates, mileage is better on a smooth highway (no color mapping, skewing, or scaling)
than on surface streets.
A transform mutation can require a 3 x 3 matrix operation for each pixel value when rendered. That's
a lot of fixed-point multiplications. If execution speed is critical and the mutated version will be used
a lot, copy the bitmap shape, mutate the geometry, and draw like crazy. Otherwise, mutate the
transform and draw as needed.
SHARED IMAGE BUFFERS
A bitmap shape's raster image buffer can be shared by other bitmap shapes. Just make the source
bitmap shape's image field the same as that of another bitmap shape. GXCopyToShape uses this
sharing of image buffers. If you need a copy of a bitmap shape (or a picture that contains bitmap
shapes) to have its own image buffer, use GXCopyDeepToShape.
USING BITMAPS AS PATTERNS
Bitmap shapes can be used as patterns. Unlike QuickDraw, QuickDraw GX has no limitation on area
dimension or size of raster data in a pattern. To do simple tiling, you can just set the bitmap pattern
on the shape.
You can align the pattern to all destination view ports simply by setting the gxPortAlignPattern
attribute. This forces all shapes drawn with that pattern in a given view port to visually line up with
each other. Another pattern attribute, gxPortMapPattern, keeps a pattern from being affected by a
shape's transform; this
is useful, for example, when you want a shape rotated and its pattern unrotated.
BITMAP SHAPE EQUIVALENCE
You can test QuickDraw GX shapes for equivalence by calling GXEqualShape. However, this
routine doesn't account for mapping effects. For example, a bitmap gradient from black to white
would be considered not equal to a white-to-black gradient bitmap whose transform is rotated 180º,
even though the two shapes would produce identical results when drawn.
SIMPLIFICATION
GXSimplifyShape reduces an indexed bitmap to its simplest representation, even reducing the pixel
depth when possible. For example, if an 8-bit-deep bitmap shape contains only 15 colors,
GXSimplifyShape will convert it to a 4-bit-deep bitmap. If a bitmap is all one color, it will be
converted into a rectangle shape -- it won't be a bitmap shape any more.
SUBSET EDITING
QuickDraw GX provides tools for working with area subsets of bitmaps. A piece can be copied from
a source bitmap via GXGetBitmapParts, edited, and then blasted back into the source image with
GXSetBitmapParts.
Individual pixel values can be accessed with the GXGetShapePixel and GXSetShapePixel routines.
Unlike in QuickDraw, these routines don't need to reference a gxViewDevice to determine the color.
SO GET GOING
As you can see, QuickDraw GX does some really cool things with bitmaps. The transforms alone
make it worthwhile -- it's easy to get addicted to rotating and skewing your bitmaps without having
to do a lot of work. The new transfer modes are great. All the rest is a bonus. In the future, when
memory is cheap and every machine is fast, you'll see more and more Macintosh systems and
applications become dependent on QuickDraw GX.
PIXEL VALUE REPRESENTATION
A raster image is, naturally enough, an array of pixel values. For indexed color, each pixel value is an index
into an associated color set.
For direct color (16 or 32 bits per pixel), a pixel value is converted directly into a color value by expanding
bit fields of the 16- or 32-bit value into three or four 16-bit unsigned integer values.
The expansion of direct pixel values depends on the color space of the raster image and the "packing" of
the color components. QuickDraw supports only RGB and a handful of packing schemes, but QuickDraw GX
supports a whole family of color spaces and packing formats, some of which are shown in Figure 1.
The packing types are defined in the gxColorSpaces enum in the header file graphics types.h. You'll also
find definitions for extended color space specifications, such as gxRGB16Space (gxRGBSpace +
gxWord5ColorPacking) and gxARGB32Space (gxLong8ColorPacking + gxRGBASpace +
gxAlphaFirstPacking). Only explicitly defined permutations are valid -- you can't just make up your own.
DAVID SUROVELLWhere there was once one, there now are three: after approximately 1500 years of bachelorhood,
David recently married (Jane) and achieved fatherhood (Elliot Ivan). He once wrote a book on QuickDraw, but that was
long ago. When he's not sleeping under his
desk at Apple, David's passionate avocations include auditioning as a guitarist for bands that fail to play in public,
committing brutal fouls in otherwise friendly soccer matches and basketball games, and playing paintball with other rush-
hour commuters.*
Thanks to our technical reviewers Pete ("Luke") Alexander, Josh Horwich, and Chris Yerga. *