Norway


Update 7/02/18 (The Quartz has undergone some radical changes over the years. We’re updating our popular series to work well with the current version of Swift, so here is an update to the second installment.)
In medias res? Check out part one of our posts on Core Graphics.

The context lies at the heart of Quartz: you need to interact with the current Core Graphics context in some manner to actually draw stuff, so it’s good to get comfortable with it, what it does, and why it’s there.

One fundamental operation in Core Graphics is creating a path. A path is a mathematical description of a shape. A path can be a rectangle, or a circle, a cowboy hat, or even the Taj Mahal. This path can be filled with a color—that is, all points within the path are to a particular color. The path can also be outlined, a.k.a. stroked. This is like taking a calligraphy pen and drawing around the path leaving an outline. Here’s a hat that’s been stroked, filled, and then both filled with yellow and stroked in blue:

Hat path  - hat path - Core Graphics, Part 2: Contextually Speaking

As you can see, the actual outline can get pretty complex. It can be drawn in a particular color. The line can have a dash pattern. It could be stroked with a wide line or a narrow line. The ends of lines can have square or round ends, and on and on and on. That’s a lot of attributes.

If you peruse the Core Graphics API, you won’t see a call that takes all the settings:

CoreGraphics.stroke(path: path, color: green, lineWidth: 2.0,
                    dashPattern: dots, bloodType: .oPositive, endCap: .miter)

Instead, you have this call:

Where do all those extra values come from, then? They come from the context.

Bucket of Bits

The context holds a pile of global state about drawing, which are a bunch of independent values:

  • current fill and stroke colors
  • line width and pattern
  • line cap and join (miter) styles
  • alpha (transparency), antialiasing and blend mode
  • shadows
  • transformation matrix
  • text attributes (font, size, matrix)
  • esoteric things like line flatness and interpolation quality
  • and more

That’s a lot of state. The entire set of state that Core Graphics maintains is undocumented, so there may be even more settings lurking under the hood. Different kinds of contexts (an image vs. a PDF, for example) may contain additional settings.

Whenever Core Graphics is told to draw something, such as “fill this rectangle,” it looks to the current context for the necessary bits of drawing info. The same sequence of code can have different results depending on what’s in the context. On one hand, this is very powerful: a generic bit of drawing code can be manipulated via the context into dramatically different results. On the other hand, the context is a big pile of global state, and global state is easy mess up unintentionally.

Say you have code like this:

draw orange square:
    set color to orange in the current context
    fill a rectangle

You’ll end up with an orange square. Now assume you’re drawing a valentine too:

draw red valentine:
    set color to red in the current context
    fill a valentine

Yay! A red heart. Now say you add the valentine drawing code in your first :

draw orange square:
    set color to orange in the current context
    draw red valentine
    fill a rectangle

Your rectangle will come out red instead of orange. Why? The valentine drawing code has clobbered the current drawing color. The color used to be orange by the you filled the rectangle, but now it’s red. How can you avoid bugs like this?

There are two approaches. One way is to save off state before you change it—if you’re changing the global color, save off the current color, change it, do your drawing, and then restore it. That’s ok with one or two parameters, but doesn’t scale if you’re changing a dozen of them. There are also some context values that can get changed as side effects, so you’d have to account for those. Oh, and it’s actually impossible to do in Core Graphics because there are no getters for the current context. Sorry about that.

A stack of buckets

The other approach is to save the entire context before you change anything. Save the context, make your adjustments to the color or line width, do your drawing, and then restore the entire context. The Core Graphics API provides calls to save and restore the settings of the current context. These settings are known as the graphics state, or GState. A Core Graphics context keeps a stack of GStates behind the scenes.

Saving a context’s settings means you are pushing a copy of the settings on to the context’s stack. When you restore the graphics state, the previously saved GState gets popped off the stack and becomes the context’s current set of values, undoing any changes you may have made.

Changing the valentine drawing code like this fixes the “orange rectangle is red” bug:

draw red valentine:
    save graphics state
      set color to red in the current context
      fill a valentine
    restore graphics state

Then, the entire sequence of drawing calls will look like this:

    set color to orange in the current context
    save graphics state
      set color to red in the current context
      fill a valentine
    restore graphics state
    fill a rectangle

Here are the GState manipulations for this sequence of drawing calls. Time moves from left to right:

GState manipulations  - gstates - Core Graphics, Part 2: Contextually Speaking

Core Graphics API

There are a couple of flavors of the CG API. One is the Core Foundation / C-based version used in C, C++, and Objective-C. It uses pseudo-object opaque types such as CGContextRef or CGColorRef. It’s pretty old-school with a lot of C functions that take their “objects” as the first parameter, and then a pile of arguments afterwards. Swift has overlays that provide a Swifty API on top of the C API. The Swift API is what I’ll be talking about here and in future postings.

Getting the context

CGContext is the Swift type for a core graphics context. Usually you’ll get this context from your UI toolkit. In desktop Cocoa you ask NSGraphicsContext:

let context = NSGraphicsContext.current()?.cgContext  // type is CGContext?

and UIKit:

let context = UIGraphicsGetCurrentContext()  // type is CGContext?

You can also get contexts that render into a bitmap image (check out UIGraphicsBeginImageContext and friends).

Once you have a context, you can do things like change the color color, change the line width, or tell it to stroke/fill specific shapes.

For example, this outlines a rectangle:

let context = ...  // CGContext
let bounds = someThing.bounds // CGRect
context.stroke(bounds)

We’ve got more information about rectangles (part 1, part 2) and paths for the more curious.

CGContext.stroke(_:) outlines a given rectangle. If the context is an image context, or the context used to render graphics on the screen, a rectangle’s-border worth of pixels will be laid down using the context’s current settings. If you’re drawing into a PDF context, then a couple of bytes of instructions are recorded to ultimately outline a rectangle when the PDF is rendered at some future time.

Context Hygiene

GrafDemo is a Cocoa desktop app that demonstrates various parts of Core Graphics for this series of postings. You can poke around that for examples of CG code.

GrafDemo’s Simple demo contains an NSView that draws a green circle, surrounded by a thick blue line, on a white background, with a thin black border around the entire view.

Good vs. sloppy drawing  - good vs sloppy drawing - Core Graphics, Part 2: Contextually Speaking

There are two versions of the code: one that has good GState hygiene and one that doesn’t. Notice that in the sloppy version, the thick blue line leaks out and is contaminating the border. (When you run the program you’ll actually see two views side-by-side when you run the program. One is implemented in Objective-C and the other in Swift.)

There’s a convenience property for getting the current context from inside of an
NSView’s draw(_:) method:

extension NSView {
    var currentContext : CGContext {
        let context = NSGraphicsContext.current()
        return context!.cgContext
    }
}

You can make a similar extension on UIView to unify accessing the current context.

Here’s the sloppy drawing method:

    func drawSloppily () {
        let context = currentContext
        context.setStrokeColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) // Black
        context.setFillColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) // White
        
        context.setLineWidth(3.0)
        
        drawSloppyBackground()
        drawSloppyContents()
        drawSloppyBorder()
    }

The background and border methods are pretty straightforward:

    func drawSloppyBackground() {
        currentContext.fill(bounds)
    }

    func drawSloppyBorder() {
        currentContext.stroke(bounds)
    }

They both assume the context is configured the same way that draw(_:) set it up. But! There is a problem:

    func drawSloppyContents() {
        let innerRect = bounds.insetBy(dx: .0, dy: 20.0)
        
        let context = currentContext
        context.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // Green
        context.fillEllipse(in: innerRect)
        
        context.setStrokeColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0) // Blue
        context.setLineWidth(6.0)
        context.strokeEllipse(in: innerRect)
    }

Notice the changes to the color and line width. The current context holds a pile of global state, so the existing fill and stroke color, and the existing line width, totally get clobbered.

Push-me Pull-you

The way to fix this problem is to copy the graphics context before drawing the contents. CGContext.saveGState() pushes a copy of the existing graphics context/graphics state onto a stack. CGContext.restoreGState() pops off the top of the stack and replaces the current context.

Here’s a nicer version of the content drawing that saves the graphics state:

    func drawNiceContents() {
        let innerRect = bounds.insetBy(dx: 20.0, dy: 20.0)

        let context = currentContext
        context.saveGState()  // Push the current context settings

        context.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // Green
        context.fillEllipse(in: innerRect)
        
        context.setStrokeColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0) // Blue
        context.setLineWidth(6.0)
        context.strokeEllipse(in: innerRect)

        context.restoreGState()  // Pop them off and undo
    }

Wrapping a save/restoreGState around the drawing prevents this method from polluting other methods.

Scoping it out

Because this drawing happens inside of a “scope” defined by GState saving and restoring,
I like to make that scope explicit in my code – this code is unambigiously protected
by saving the GState, without having to scan for save/restoreGState calls. You can even see
me making a scope via indentation in the orange-rectangle / red-heart example earlier.

I have another extension that wraps a GState push/pop in a closure:

import CoreGraphics

extension CGContext{
    func protectGState(_ drawStuff: () -> Void) {
        saveGState()
        drawStuff()
        restoreGState()
    }
}

Which makes the more hygenic drawing look like this:

    func drawNiceContents() {
        let innerRect = bounds.insetBy(dx: 20.0, dy: 20.0)
        let context = currentContext

        context.protectGState {
            context.setFillColor(red: 0.0, green: 1.0, blue: 0.0, alpha: 1.0) // Green
            context.fillEllipse(in: innerRect)
            
            context.setStrokeColor(red: 0.0, green: 0.0, blue: 1.0, alpha: 1.0) // Blue
            context.setLineWidth(6.0)
            context.strokeEllipse(in: innerRect)
        }
    }

Objective-C

OK, so what about Objective-C? GrafDemo has parallel Swift and Objective-C implementations, so feel to peruse the sample code of your choice.

Back in the old days, Objective-C and Swift use of Core Graphics was nearly identical.
That made sharing source code easy (copy and paste, search and replace semicolons, tweak
your variable declaratons.) Swift 3 converted the old C-based API into
a nice object-oriented API.

The actual operations are identical – save a gstate, set a color, make a path, stroke or
fill a color, restore a gstate. They’re just spelled differently.

Other Platforms

OK, so what about other Apple platforms? Core Graphics is a lower-level framework that lives below AppKit, UIKit (iOS and TVos), and WatchKit. This means that your Core Graphics code can be pretty much identical on macOS and iOS. The main differences are how you initially get a context to draw in to (which is easily hidden behind extensions) and some of the more esoteric functions are only avaialble on macOS. You also don’t have easy cross-platform access to the higher-level abstractions (e.g. UIBezierPath / NSBezierPath and UIImage / NSImage).

The higher-level APIs do mix and match well with the lower-level ones. For example, you can push a GState, then use UIColor.purple.set() to change the drawing color, and then fill/stroke a path.

GState of the Union

This time, you met Core Graphics contexts, which are buckets of various drawing attributes. A context is an opaque structure, so you have no idea what is really lurking inside. Because of this opaque state, and given the fact that some Core Graphics calls come with side effects, it’s impossible to save drawing attributes before changing them.

Core Graphics has the concept of a graphics state stack, which comes from Postscript. You can push a copy of the current graphics state onto a stack with CGContext.saveGState() and can undo any changes made to the context by popping the saved state with CGContext.restoreGState(). Got code that’s polluting the context so that subsequent drawing is wrong? Wrap it in a Save/Restore.

Core Graphics code is nearly identical across Apple’s platforms, so Core Graphics code can be pretty portable amongst the different parts of the Apple ecosystem.

Coming up next time: Lines! (as in, Lines-excitement-yay-happy-fun-times, not Lines-implicity-unwrapped-optionals.)



Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here