Search code examples
swiftmacoscocoacgcontextquartz-2d

The image created by CGContext.makeImage is different from the original text


I want to create a mask using text and need to convert the text into an image. But after converting, I found that it did not match exactly with the original text. I conducted the following tests.

class MyView: NSView{
    
    override func draw(_ dirtyRect: NSRect) {
        guard let g = NSGraphicsContext.current?.cgContext else{ return }
        
        let antialiasing = false //true or false is useless
        g.setAllowsAntialiasing(antialiasing)
        g.setShouldAntialias(antialiasing)
        
        let text: NSString = "HELLO"
        let pos: CGPoint = .init(x: 20, y: 20)
        let font: NSFont = .init(name: "Arial Black", size: 96)!
        var textAttr: [NSAttributedString.Key: Any] = [
            .font : font
        ]
        
        //Draw the blue text in new CGContext, generate an image, draw the image in current CGContext
        let image = createImage(dirtyRect: dirtyRect) { g in
            g.setAllowsAntialiasing(antialiasing)
            g.setShouldAntialias(antialiasing)
            
            textAttr[.foregroundColor] = NSColor.blue
            text.draw(at: pos, withAttributes: textAttr)
        }
        g.draw(image!, in: dirtyRect, byTiling: false)
        
        //Draw the yellow text with all same properties
        let attr: [NSAttributedString.Key: Any] = [
            .font : font,
            .foregroundColor : NSColor.yellow
        ]
        text.draw(at: pos, withAttributes: attr)
    }

    private func createImage(dirtyRect: NSRect, drawingHandler: ((_ g: CGContext) -> Void)?) -> CGImage?{
        NSGraphicsContext.saveGraphicsState()
        let g = CGContext(data: nil,
                          width: Int(dirtyRect.width),
                          height: Int(dirtyRect.height),
                          bitsPerComponent: 8,
                          bytesPerRow: 0,
                          space: CGColorSpaceCreateDeviceRGB(),
                          bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)!
        NSGraphicsContext.current = NSGraphicsContext(cgContext: g, flipped: false)
        drawingHandler?(g)
        let image = g.makeImage()
        NSGraphicsContext.restoreGraphicsState()
        return image
    }
}

As can be seen, the two texts cannot completely overlap after being stacked, and the blue text below reveals a circle of edges.

I tried other createImage methods, such as creating an NSImage and drawing text on it, or creating a CGLayer and drawing text on this layer then create the image, and the results were similar.

The only perfect solution is to use the current CGContext and disable Anti-aliasing to generate text image. But the problem with this method is that the generated image will contain all the drawn content, some of which may not be necessary for me.

private func createImage(dirtyRect: NSRect, drawingHandler:((_ g: CGContext) -> Void)?) -> CGImage?{
    drawingHandler?(NSGraphicsContext.current!.cgContext)
    return NSGraphicsContext.current?.cgContext.makeImage()
}

Any solutions? Thanks.

enter image description here


Solution

  • Try printing out the gs in both draw(_:) and createImage. They are of different types, so it is not surprising that they behave differently (though I cannot tell you exactly what the difference is).

    The two texts do overlap, if you draw into an NSImage, using this initialiser, then convert that image to CGImage using the current context.

    private func createImage(dirtyRect: NSRect, drawingHandler: @escaping (_ g: CGContext) -> Void) -> CGImage?{
        let image = NSImage(size: dirtyRect.size, flipped: false) { _ in
            guard let g = NSGraphicsContext.current?.cgContext els { return false }
            drawingHandler(g)
            return true
        }
        // presumably passing context: nil uses the current context,
        // but you can also use context: NSGraphicsContext.current
        return image.cgImage(forProposedRect: nil, context: nil, hints: nil)
    }