Search code examples
swifttextcore-graphicsdrawglyph

swift text.draw method messes up CGContext


I am using CoreGraphics to draw single glyphs alongside with primitives in a CGContext. The following code works in a swift playground in XCode 9.2 When started in the playground a small rectangle with twice the letter A should appear at the given coordinates in the playground liveView.

import Cocoa
import PlaygroundSupport

class MyView: NSView {

    init(inFrame: CGRect) {
        super.init(frame: inFrame)
    }

    required init?(coder decoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(_ rect: CGRect) {

        // setup context properties

        let context: CGContext = NSGraphicsContext.current!.cgContext

        context.setStrokeColor(CGColor.black)
        context.setTextDrawingMode(.fill)
        context.setFillColor(CGColor(red: 0.99, green: 0.99, blue: 0.85, alpha: 1))
        context.beginPath()
        context.addRect(rect)
        context.fillPath()
        context.setFillColor(.black)

        // prepare variables and constants

        var font = CTFontCreateWithName("Helvetica" as CFString, 48, nil)
        var glyph = CTFontGetGlyphWithName(font, "A" as CFString)
        var glyph1Position = CGPoint(x: self.frame.minX, y: self.frame.maxY/2)
        var glyph2Position = CGPoint(x: self.frame.minX+150, y: self.frame.maxY/2)

        let text = "Hello"
        var textOrigin = NSPoint(x: self.frame.minX+50, y: self.frame.maxY/2)

        // draw one character

        CTFontDrawGlyphs(font, &glyph, &glyph1Position, 1, context)

        // ***   ***   when the next line is uncommented the bug appears   ***   ***

        // text.draw(at: textOrigin)

        CTFontDrawGlyphs(font, &glyph, &glyph2Position, 1, context)
    }
}

var frameRect = CGRect(x: 0, y: 0, width: 200, height: 100)

PlaygroundPage.current.liveView = MyView(inFrame: frameRect)

Now I want to draw regular text in the same context. However, when the text string is drawn between the drawing of the two glyphs via its own drawing method, the current context seems to be messed up, the second glyph will not show. When the text is drawn after both single glyphs are drawn everything is fine.

So obviously drawing the text seems to have an impact on the current CGContext, but I cannot find out what exactly is happening. I tried the saveGstate() method befor drawing the string and restoring afterwards, but without success.

I also tried using CoreText methods to create an attributed String with CTFramesetterCreateWithAttributedString and showing it with CTFramesetterCreateFrame, it also does not work, here after the creation of the framesetter the context is messed up.

My actual playground is more complex, there the glyphs do not entirely disappear but are shown at a wrong vertical position, but the basic problem - and question is the same:

How can I draw the text into the currentContext whithout any other changes to the context being done in the background?


Solution

  • You need to set the text matrix (the transform applied to text drawing). You should always set this before drawing text, because it isn't part of the graphics state and may get trashed by other drawing code such as AppKit's string drawing extensions.

    Add the following before your call to CTFontDrawGlyphs:

        context.textMatrix = .identity
    

    This should be called in the initial setup of the context since there is no promise that the text matrix will be identity before calling drawRect. Then, any time you have made calls to something that modifies the text matrix you will need to set it back to what you want (identity in this case, but it could be something else if you wanted to draw in a fancy way).

    It is not always obvious what will modify the text matrix. AppKit drawing functions almost always do (though I'm not aware of any documentation indicating this). Core Text functions that modify the context, like CTFrameDraw and CTLineDraw, will generally document this fact with a statement such as:

    This call can leave the context in any state and does not flush it after the draw operation.

    Similarly CTFontDrawGlyphs warns:

    This function modifies graphics state including font, text size, and text matrix if these attributes are specified in font. These attributes are not restored.

    As a rule I discourage mixing text drawing systems. Use AppKit or Core Text, but don't mix them. If you pick one and stick to it, then this generally isn't a problem (as long as you initialize the matrix once at the top of drawRect). For example, if you did all the drawing with CTFontDrawGlyphs, you wouldn't need to reset the matrix each time; you'd stay in the Core Text matrix and it'd be fine (which is why this works when you comment out the draw(at:) call).