A Simple Drawing Example


This chapters first code sample demonstrates some of the Quartz 2D concepts discussed. The code sample draws the logo for the Ace of Hearts Bakery, an imaginary establishment renowned this chapter over for its fine artisan breads and pastries. The Ace of Hearts is looking to update its corporate image with a new logo. As you might imagine, the bakery's napkin artwork will be a vital part of the branding initiative. The sample code generates a logo for the baker's approval. Figure 3.3 is the image created by the sample code.

Figure 3.3. The Ace of Hearts Napkin Artwork


For this sample look at the source code that draws the image. The discussion that follows glosses over some of the details in the code to highlight the overall drawing process rather than individual routines. The fundamental techniques used in the code demonstrate how to use the Quartz 2D imaging model.

The sample code draws the image into an offscreen bitmap and generates a PNG file from the results. The code is as simple as the graphic. However, even in this illustration there is occasion to use line art and text, the Painter's Model, the Crayon Model, and some simple transformations. Listing 3.1 is the complete source of our drawing sample.

Listing 3.1. Python Code To Draw The Ace Of Hearts Napkin Artwork

#include <Carbon/Carbon.h> const float kBlackColor[] = { 0.0, 0.0, 0.0, 1.0}; const float kRedColor[] = { 1.0, 0.0, 0.0, 1.0}; const float kWhiteColor[] = { 1.0, 1.0, 1.0, 1.0}; // Handy routine for centering some text on the origin. void CenterTextInContextAtOrigin(            CGContextRef cgContext,            const  char *fontName,            float textSize,            const char *text) {    CGContextSaveGState(cgContext);    CGContextSelectFont(cgContext, fontName,           textSize, kCGEncodingMacRoman);    // Save the GState again for the text drawing mode    CGContextSaveGState(cgContext);    CGContextSetTextDrawingMode(cgContext, kCGTextInvisible);    CGContextShowTextAtPoint(cgContext, 0, 0, text, strlen(text));    CGPoint destination = CGContextGetTextPosition(cgContext);    CGContextRestoreGState(cgContext);    // Draw the text    CGContextSetFillColor(cgContext, kWhiteColor);    CGContextShowTextAtPoint(cgContext,            -destination.x / 2.0, -textSize / 2, text, strlen(text));    CGContextRestoreGState(cgContext); } int main(int argc, char* argv[]) {     const int kImageSize = 512;     const int kHalfSize = kImageSize / 2;     const CFIndex kBytesPerRow = (CFIndex)kImageSize * 4;     const CFIndex kDataLength = kBytesPerRow * kImageSize;     CGColorSpaceRef kGenericRGBColorSpace =             CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);     UInt8 *bitmapData = (UInt8*) malloc(kDataLength);     CGContextRef cgContext = CGBitmapContextCreate(                    bitmapData,                    kImageSize, kImageSize,                    8, kBytesPerRow,                    kGenericRGBColorSpace, kCGImageAlphaNoneSkipFirst);     // Set up the color environment     CGContextSetFillColorSpace(cgContext, kGenericRGBColorSpace);     CGContextSetStrokeColorSpace(cgContext, kGenericRGBColorSpace);     // Draw a white background     CGContextSetFillColor(cgContext, kWhiteColor);     CGContextAddRect(cgContext,             CGRectMake(0, 0, kImageSize, kImageSize));     CGContextFillPath(cgContext);     // put the origin at the center of the context     CGContextTranslateCTM(cgContext, kHalfSize, kHalfSize);     // draw the checkered background     CGContextSetFillColor(cgContext, kBlackColor);     CGContextBeginPath(cgContext);     CGContextAddRect(cgContext,             CGRectMake(-kHalfSize, -kHalfSize, kHalfSize, kHalfSize));     CGContextAddRect(cgContext, CGRectMake(0, 0, kHalfSize, kHalfSize));     CGContextFillPath(cgContext);     // use a stroke to frame the entire image     CGContextSetStrokeColor(cgContext, kBlackColor);     CGContextSetLineWidth(cgContext, 4);     CGContextAddRect(cgContext,             CGRectMake(-kHalfSize + 2, -kHalfSize + 2,                    kImageSize - 4, kImageSize - 4));     CGContextStrokePath(cgContext);     // construct and draw a heart shaped path     CGContextSaveGState(cgContext);     CGContextTranslateCTM(cgContext, kHalfSize / 2, kHalfSize / 2);     CGContextMoveToPoint(cgContext, 0, 21);     CGContextAddCurveToPoint(cgContext, -16, 49.8,             -53.2, 41.0, -49.6, 5.8);     CGContextAddCurveToPoint(cgContext, -46, -29.4,             -9.4, -53.4, 0, -69.8);     CGContextAddCurveToPoint(cgContext, 9.4, -53.4,             46, -29.4, 49.6, 5.8);     CGContextAddCurveToPoint(cgContext, 53.2, 41, 16, 49.8, 0, 21);     CGContextClosePath(cgContext);     // ask the computer to fill the path     CGContextSetFillColor(cgContext, kRedColor);     CGContextFillPath(cgContext);     // draw some text on top of the path.     CenterTextInContextAtOrigin(cgContext, "Zapfino",             24, "Ace of Hearts");     CGContextRestoreGState(cgContext);     CGContextSaveGState(cgContext);     CGContextTranslateCTM(cgContext,            -kHalfSize / 2.0, -kHalfSize / 2.0);     CenterTextInContextAtOrigin(cgContext, "Zapfino",            18, "Fine baked goods");     CGContextRestoreGState(cgContext);     CGContextRelease(cgContext);     // Create an image from the bitmap data     CGDataProviderRef dataProvider =             CGDataProviderCreateWithData(NULL, bitmapData,                    kDataLength, NULL);     CGImageRef image = CGImageCreate(                    kImageSize, kImageSize,                    8, 32, kBytesPerRow,                    kGenericRGBColorSpace, kCGImageAlphaNoneSkipFirst,                    dataProvider,                    NULL, true,                    kCGRenderingIntentDefault);     CGDataProviderRelease(dataProvider);     // Find the user's desktop folder and create a     // URL to a file named AceOfHeartsBakery.png on the desktop     FSRef usersDesktop;     FSFindFolder(kUserDomain, kDesktopFolderType, false, &usersDesktop);     CFURLRef desktopURL = CFURLCreateFromFSRef(NULL, &usersDesktop);     CFURLRef destinationURL =             CFURLCreateWithFileSystemPathRelativeToBase(NULL,                    CFSTR("AceOfHeartsBakery.png"),                    kCFURLPOSIXPathStyle, false, desktopURL);     CFRelease(desktopURL);     // Export our image into that png file.     CGImageDestinationRef exportDestination =             CGImageDestinationCreateWithURL(destinationURL, kUTTypePNG,                     1, NULL);     CGImageDestinationAddImage(exportDestination, image, NULL);     CGImageDestinationFinalize(exportDestination);     CFRelease(exportDestination);     CFRelease(destinationURL);     CGImageRelease(image);     free(bitmapData);     CFRelease(kGenericRGBColorSpace);     return 0; } 

The source code begins by defining some color constants. Note that each color is specified using four floating point numbers. This sample uses RGB colors, so the four components are the Red, Green, Blue, and Alpha (transparency) components for each color. The valid range for color components is between 0 and 1.0.

Following this is the definition of a short routine called CenterTextInContextAtOrigin. We'll look at the details of this routine's implementation shortly, but the name is fairly descriptive. The routine draws a text string with a given font and size centered on the origin (of user space). To use this routine to draw text at an arbitrary location on the device, we have to change the location of user space using transformations.

Creating the CGContext

As mentioned before, Quartz 2D requires your application to create a CGCon-text, the drawing space that will receive drawing commands. This code sample creates its image in an RGB bitmap. After defining some constants that describe the bitmap you want to create, the code creates an object that identifies color space of the bitmap. This code is designed to save its image into a file on disk and later to display it on screen. An RGB color space is used. To do that, use the routine CGColorSpaceCreateWithName and the name constant kCGColorSpaceGenericRGB. This creates an instance of the generic Macintosh RGB color space. From here the sample creates a bitmap graphics context using CGBitmapContextCreateWithColor. The code looks like this:

CGColorSpaceRef kGenericRGBColorSpace =     CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); UInt8 *bitmapData = (UInt8*) malloc(kDataLength); CGContextRef cgContext = CGBitmapContextCreate(                            bitmapData,                            kImageSize, kImageSize,                            8, kBytesPerRow,                            kGenericRGBColorSpace,                            kCGImageAlphaNoneSkipFirst); // Set up the color environment CGContextSetFillColorSpace(cgContext, kGenericRGBColorSpace); CGContextSetStrokeColorSpace(cgContext, kGenericRGBColorSpace); 


This code uses malloc to obtain memory for the offscreen bitmap and then creates the Quartz drawing context on that memory. For more information on drawing into off-screen bitmaps, see Chapter 12, "Drawing Offscreen." That chapter also explains the various parameters to the CGBitmapContextCreate routine.

The last two lines of this section of the code establish the color environment for subsequent drawing. In this case Quartz 2D is told to use the generic RGB color space for both lines and fills in the graphic.

The First Bit of Drawing

The first bit of drawing in this code sample paints the entire context with a white rectangle. That code looks like this:

// Draw a white background CGContextSetFillColor(cgContext, kWhiteColor); CGContextAddRect(cgContext, CGRectMake(0, 0, kImageSize, kImageSize)); CGContextFillPath(cgContext); 


Recall that in the Painter's Model graphics are constructed from the back to the front. This code is the first to draw and fills the entire background with opaque white. To do that, it first builds a rectangle that outlines the entire drawing surface and then asks the context to fill that rectangle. More information about constructing paths, like the rectangle in this system, is covered in Chapter 5. Chapter 7 discusses the primitive drawing operations, fill and stroke, along with the context parameters that affect them.

Setting up the Coordinate System

When Quartz 2D creates a drawing context on a device, it sets up the coordinate system of user space so that its origin is in the lower left corner of the device. From that origin, the positive X axis points to the right, and the positive Y axis points upward. When working with a bitmap context, Quartz 2D will scale the coordinate system so that one unit of user space covers the same distance of one pixel on the output device. This location and orientation is illustrated in the left half of Figure 3.4.

Figure 3.4. Changing the Location of User Space on the Bitmap


Certainly you could draw the napkin artwork with the coordinate system in this location. The artwork, however, has a symmetry about the center of the drawing that might be exploitable. With that in mind, use the ability of Quartz 2D to change the location of user space, relative to the destination coordinate system. The sample code accomplishes this with this code:

// put the origin at the center of the context CGContextTranslateCTM(cgContext, kHalfSize, kHalfSize); 


The end result of this line is that the origin of user space is moved to the point (256, 256) in its current coordinate system. In this case, the end result is that the origin of user space moves to the center of the bitmap.

Drawing the Checkerboard

Now continue to apply the Painters Model to the artwork. To draw the image properly, you need to draw the graphics elements in the proper order. We begin with the rest of the background consisting of the checkerboard motif and the black frame that surrounds the image. These elements are line art, and they'll be created using the Crayon Model. The first step is to specify a path in user space to color. Secondly, you ask Quartz 2D to either stroke it or fill it. Here is the code that draws the background:

// draw the checkered background CGContextSetFillColor(cgContext, kBlackColor); CGContextBeginPath(cgContext); CGContextAddRect(cgContext, C     GRectMake(-kHalfSize, -kHalfSize, kHalfSize, kHalfSize)); CGContextAddRect(cgContext, CGRectMake(0, 0, kHalfSize, kHalfSize)); CGContextFillPath(cgContext); // use a stroke to frame the entire image CGContextSetStrokeColor(cgContext, kBlackColor); CGContextSetLineWidth(cgContext, 4); CGContextAddRect(cgContext,     CGRectMake(-kHalfSize + 2, -kHalfSize + 2,             kImageSize - 4, kImageSize - 4)); CGContextStrokePath(cgContext); 


This code first stores the color chosen for the fill in the graphics state of the context. For the background of the napkin, both the frame and the checkerboard motif are drawn in black. This equates to the RGB color (0, 0, 0), and given that our paint is intended to be completely opaque, we also use 1.0 for the alpha channel. Next, the code begins the path that will draw both squares of the checkerboard in the background.

Recall from the brief discussion that a path can contain disjoint areas. A CGContext contains a couple of paths. One of these, called the current path, is simply the path under construction in the context. The call to CGContextBeingPath clears the current path in the context and prepares it to receive new drawing commands. Each of the calls to CGContextAddRect adds a rectangular area to the current path. The utility routine CGRectMake simply fills out a CGRect structure that Quartz 2D uses to represent rectangles. The four parameters to this routine are the X and Y locations of the rectangle's origin (the lower left corner of the rectangle), followed by its height and width.

Once the code constructs the path, it calls the CGContextFillPath context method. This routine fills the current path with the current fill color. The fill method also empties the context's current path. Because the path has disjoint sections, both of the filled background rectangles are drawn in one drawing operation.

To demonstrate the use of a stroke, the code also adds a frame around the entire image. The width to use when stroking a path is part of the graphics state in a drawing context, and the code sets it to four units wide by calling CGContextSetLineWidth. The computer centers the stroke path with half of the stroke width on either side of the path, so the rectangle used to define the path must be inset from the edge of the image by half the stroke width. Again CGContextAddRect is used to put a rectangle into a current path. The call to CGContextStrokePath draws the frame and, of course, clears the current path of the context.

Drawing the Heart

By now the process the code uses to draw the heart of the napkin should be familiar to you. The next step is to create a heart shaped path in user space and then ask the computer to fill it. Up to this point, the paths shown have been formed from simple rectangles. The heart path is a bit more complex than that. It consists of four curved segments, two for each side of the heart. Bezier curves will be used to construct the heart. The discussion of bezier curves and how they define the outlines of paths is given in Chapter 6. The following illustration shows the path that the points trace out.

In Figure 3.5 the coordinate axes have also been drawn. Notice that the curves that define the heart path define it centered at the origin. You could move the heart to a different location by adjusting the points of the path so they define a heart some distance away from the origin. Another option, however, is to simply move the origin before drawing the heart, which is the approach the source code takes. The code that draws the heart and the text on top of it looks like the following.

Figure 3.5. The Heart Shaped Path All Alone


// construct and draw a heart shaped path CGContextSaveGState(cgContext); CGContextTranslateCTM(cgContext, kHalfSize / 2, kHalfSize / 2); CGContextMoveToPoint(cgContext, 0, 21); CGContextAddCurveToPoint(cgContext, -16, 49.8, -53.2, 41.0, -49.6, 5.8); CGContextAddCurveToPoint(cgContext, -46, -29.4, -9.4, -53.4, 0, -69.8); CGContextAddCurveToPoint(cgContext, 9.4, -53.4, 46, -29.4, 49.6, 5.8); CGContextAddCurveToPoint(cgContext, 53.2, 41, 16, 49.8, 0, 21); CGContextClosePath(cgContext); // ask the computer to fill the path CGContextSetFillColor(cgContext, kRedColor); CGContextFillPath(cgContext);     // draw some text on top of the path. CenterTextInContextAtOrigin(cgContext, "Zapfino", 24, "Ace of Hearts"); CGContextRestoreGState(cgContext); 


This code indirectly introduces another important aspect of the CGContext, the graphics state stack. Some of the fields of the graphics state of a CGCon-text have already been shown. The current fill and stroke colors, for example, are part of the graphics state. The fields of the graphics state are stored in a stack, and the call to CGContextSaveGState pushes a copy of the current values of those fields onto the stack. The application can then modify the settings to do some drawing. To restore the old settings the code pops the stack using CGContextRestoreGstate, and the older settings are restored.

The current mapping between user space and the device is also part of the graphics state of a CGContext. When the heart's code snippet begins, the origin of user space is still set to the center of our bitmap. The code saves all the fields in the graphics state, including the location and orientation of user space with the call to CGContextSaveGState. It then changes the origin with a translate call and draws both the heart and the text. When that is done, the code calls CGContextRestoreGstate. This returns the origin of user space back to the center of the bitmap.

The actual drawing of the heart path begins with a call to CGContextMoveToPoint. This call identifies the starting point of our heart-shaped path and corresponds to the pointy corner at the top of the heart. The following calls to CGContextAddCurveToPoint append curved segments to the path. The first two points in each call define the shape of the curve, and the last point is that segment's end point. Notice that the last point of the path matches the first point. From there it is simply a matter of setting the current fill color to red and using CGContextFillPath to paint the heart.

Drawing the Text

The sample code draws text by calling a sub-routine defined at the beginning of the source file:

CenterTextInContextAtOrigin(cgContext, "Zapfino", 24, "Ace of Hearts"); 


This call tells the routine to draw the text "Ace of Hearts" using 24 point Zapfino. The font name, in this case, is actually the PostScript name of the font. In the case of Zapfino, the PostScript name and the family name of the font happen to be the samebut that will not be the case for all fonts. For example, if you wanted to change this code so it draws in the plain style of the Times font, you would have to use that font's PostScript name, Times-Roman.

The Font Book application on Mac OS X can tell you the PostScript name of any font that is active on your system. Select a font and choose "Show Font Info..." from the Preview menu.


The routine that draws the text looks like the following:

void CenterTextInContextAtOrigin(            CGContextRef cgContext,            const  char *fontName,            float  textSize,            const  char *text) {    CGContextSaveGState(cgContext);    CGContextSelectFont(cgContext, fontName,            textSize, kCGEncodingMacRoman);    // Save the GState again for the text drawing mode    CGContextSaveGState(cgContext);    CGContextSetTextDrawingMode(cgContext, kCGTextInvisible);    CGContextShowTextAtPoint(cgContext, 0, 0, text, strlen(text));    CGPoint destination = CGContextGetTextPosition(cgContext);    CGContextRestoreGState(cgContext);    // Draw the text    CGContextSetFillColor(cgContext, kWhiteColor);    CGContextShowTextAtPoint(cgContext, -destination.x / 2.0,            -textSize / 2, text, strlen(text));    CGContextRestoreGState(cgContext); } 


In describing the text capabilities of Quartz 2D, it was noted that Quartz 2D does not have very advanced text handling capabilities itself. Applications would normally use higher-level utilities like ATSUI or the Cocoa text system to draw text through Quartz 2D. For this simple example, however, the rudimentary capabilities of Quartz 2D will suffice. The call to CGContextSelectFont tells Quartz to draw text using Zapfino and that the text specified will use the MacRoman encoding system.

To center the string, you need know how long the text is. In a more sophisticated application the measuring of text would best be left to a higher level text library. For this simple program, Quartz is asked to draw the text using the invisible text drawing mode. The context keeps track of the "next" location where it will draw text, which you use to your advantage.

When the sample draws the text using the invisible text mode, the computer calculates a point at the end of the text but does not draw anything. A width measurement is obtained for the text by taking note of how far the text position changes. This offers an approximate measurement for the width of the text. A more sophisticated text system could calculate a more accurate width for our text. This measurement technique is sufficient for our needs here.

The sample code saves the graphics state so it can restore the text drawing mode after measuring the text; it then sets the current text drawing mode to use invisible text and draws the text string at the origin. This adjusts the current text position, which the code retrieves from the context. The code restores the graphics state, thereby returning the text drawing mode to its previous value. Finally, the code draws the text again but at a point that is shifted to the left by half the text width. This centers the text on the origin!

The second set of text is drawn with this same routine after the origin is adjusted to a new portion of the napkin graphic.

Writing a PNG File

The final lines of the source code create a CGImage out of the graphic and then save that image into a PNG file on the desktop, as follows:

// Create an image from the bitmap data CGDataProviderRef dataProvider = CGDataProviderCreateWithData(     NULL, bitmapData, kDataLength, NULL); CGImageRef image = CGImageCreate(            kImageSize, kImageSize,            8, 32, kBytesPerRow,            kGenericRGBColorSpace, kCGImageAlphaNoneSkipFirst,            dataProvider,            NULL, true,            kCGRenderingIntentDefault); CGDataProviderRelease(dataProvider); // Find the user's desktop folder and create a // URL to a file named AceOfHeartsBakery.png on the desktop FSRef usersDesktop; FSFindFolder(kUserDomain, kDesktopFolderType, false, &usersDesktop); CFURLRef desktopURL = CFURLCreateFromFSRef(NULL, &usersDesktop); CFURLRef destinationURL =            CFURLCreateWithFileSystemPathRelativeToBase(NULL,                    CFSTR("AceOfHeartsBakery.png"), kCFURLPOSIXPathStyle,                    false, desktopURL); CFRelease(desktopURL); // Export our image into that png file. CGImageDestinationRef exportDestination =     CGImageDestinationCreateWithURL(             destinationURL, kUTTypePNG, 1, NULL); CGImageDestinationAddImage(exportDestination, image, NULL); CGImageDestinationFinalize(exportDestination); 


Chapter 8 discusses all the details of creating a CGImage. The basic idea in the code is to tell the image where to get its data using an instance of the CGDataProvider opaque data type. The image is then created by supplying many of parameters that define how the image data is laid out in memory.

The code in the middle, which determines the destination URL for the image, is Carbon framework code, not Quartz 2D. The call to FSFindFolder locates the user's desktop folder, and he converts the FSRef returned by that routine into a CFURL. We then tack the file name AceofHeartsBakery.png to the end of that URL to create the destination for our image.

To export the image, we use the techniques described in detail in Chapter 9. This sample uses Image I/O to export the image. The code creates an instance of the CGImageDestination opaque data type and indicates that the destination is used to store a single image. It then adds the image we've created to that destination and writes the image file with a call to CGImageDestinationFinalize.

The remainder of this code sample cleans up the various objects created as part of drawing the image.

All in all, this is a rather simple drawing example. Perhaps it is a bit more complex than it strictly needs to be to get the resulting image, but the code does demonstrate many of the techniques outlined in this chapter. It offers a glimpse into the device independence of the API, the use of color and color spaces, the Painter's Model for layering, and the Crayon Model for drawing. Understanding this drawing model is essential to using Quartz 2D effectively. This concludes the overview of the drawing model, and attention now turns to examining the Quartz 2D graphics model in detail.




Quartz 2D Graphics for Mac OS X Developers
Quartz 2D Graphics for Mac OS X Developers
ISBN: 0321336631
EAN: 2147483647
Year: 2006
Pages: 100

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net