swift text.draw 方法搞乱了 CGContext

swift text.draw method messes up CGContext

我正在使用 CoreGraphics 在 CGContext 中绘制单个字形和图元。以下代码在 XCode 的 swift playground 中运行 9.2 当在 playground 中启动时,一个带有两倍字母 A 的小矩形应该出现在 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)

现在我想在相同的上下文中绘制常规文本。但是,当通过自己的绘制方法在两个字形的绘制之间绘制文本字符串时,当前上下文似乎乱七八糟,第二个字形不会显示。当在绘制两个单个字形后绘制文本时,一切都很好。

所以显然绘制文本似乎对当前的 CGContext 有影响,但我无法找出到底发生了什么。我在绘制字符串之前尝试了 saveGstate() 方法并在之后恢复,但没有成功。

我还尝试使用 CoreText 方法使用 CTFramesetterCreateWithAttributedString 创建属性字符串并使用 CTFramesetterCreateFrame 显示它,它也不起作用,在创建 framesetter 之后上下文被搞砸了。

我的实际 playground 更复杂,字形并没有完全消失,而是显示在错误的垂直位置,但基本问题 - 问题是相同的:

如何在不在后台对上下文进行任何其他更改的情况下将文本绘制到 currentContext 中?

您需要设置文本矩阵(应用于文本绘制的变换)。您应该始终在绘制文本之前设置它,因为它不是图形状态的一部分,并且可能会被其他绘图代码(例如 AppKit 的字符串绘图扩展)破坏。

在调用 CTFontDrawGlyphs 之前添加以下内容:

    context.textMatrix = .identity

这应该在上下文的初始设置中调用,因为在调用 drawRect 之前没有保证文本矩阵是恒等的。然后,任何时候你调用了修改文本矩阵的东西,你都需要将它设置回你想要的东西(在这种情况下是身份,但如果你想以一种奇特的方式绘制它可能是其他东西)。

修改文本矩阵的内容并不总是很明显。 AppKit 绘图函数几乎总是如此(尽管我不知道有任何文档表明这一点)。修改上下文的 Core Text 函数,如 CTFrameDrawCTLineDraw,通常会使用如下语句记录此事实:

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

同样CTFontDrawGlyphs警告:

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

通常我不鼓励混合文本绘图系统。使用 AppKit 或 Core Text,但不要混用。如果你选择一个并坚持下去,那么这通常不是问题(只要你在 drawRect 的顶部初始化一次矩阵)。例如,如果您使用 CTFontDrawGlyphs 绘制所有图形,则无需每次都重新设置矩阵;你会留在 Core Text 矩阵中并且它会很好(这就是为什么当你注释掉 draw(at:) 调用时它起作用)。