14

I was looking for a way to render a CALayer or NSView and get NSImage back.

I have a custom class, which is a subclass of NSView. At the beginning I simply make a gradient layer to cover NSView.

class ContentView: NSView {

   override func draw(_ dirtyRect: NSRect) {
      fillGradientLayer()
   }

   func fillGradientLayer() {
      gradientLayer = CAGradientLayer()
      gradientLayer.colors = [#colorLiteral(red: 0, green: 0.9909763549, blue: 0.7570167824, alpha: 1),#colorLiteral(red: 0, green: 0.4772562545, blue: 1, alpha: 1)].map({return $0.cgColor})
      gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
      gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
      gradientLayer.frame = self.frame.insetBy(dx: margin, dy: margin)
      gradientLayer.zPosition = 2
      gradientLayer.name = "gradientLayer"
      gradientLayer.contentsScale = (NSScreen.main()?.backingScaleFactor)!
      self.layer?.addSublayer(gradientLayer)
   }
}

At some point I want to get a NSImage (or CGImage) from CALayers. I found some samples around the Internet and turned them into this:

extension CALayer {

func getBitmapImage() -> NSImage {
    
    let btmpImgRep = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(self.frame.width), pixelsHigh: Int(self.frame.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSDeviceRGBColorSpace, bytesPerRow: 0, bitsPerPixel: 0)
    
    let  image: NSImage = NSImage(size: self.frame.size)
    image.lockFocus()
    let ctx = NSGraphicsContext(bitmapImageRep: btmpImgRep!)
    let cgCtxt = ctx!.cgContext
    
    self.render(in: cgCtxt)
    cgCtxt.draw(layer: CGLayer, in: CGRect)
    
    image.unlockFocus()
    return image
  }
}

The first problem is that draw(layer: CGLayer, in: CGRect) method expects a CGLayer but not CALayer.

Secondly, I don't know if the code I ended up with will eventually render my layer or I'm doing something wrong.

Thanks for any help.


Edited: Solution to get a CGImage from CALayer.

extension CALayer {

func getBitmapImage() -> NSImage {
    
    let btmpImgRep =
        NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: Int(self.frame.width), pixelsHigh: Int(self.frame.height), bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: NSDeviceRGBColorSpace, bytesPerRow: 0, bitsPerPixel: 32)

    let ctx = NSGraphicsContext(bitmapImageRep: btmpImgRep!)
    let cgContext = ctx!.cgContext
    
    self.render(in: cgContext)
    
    let cgImage = cgContext.makeImage()

    let nsimage = NSImage(cgImage: cgImage!, size: CGSize(width: self.frame.width, height: self.frame.height))

        return nsimage    
  }
}

How to get NSImage from NSView is in the answer below.

P.S. I don't know much about NSBitmapImageRep properties, so this part might be wrong.

Alexei
  • 511
  • 1
  • 10
  • 22
  • Maybe try working up a step - the UIView, since every view has a CALayer. The answer in this link shows how to make a PNG from a UIView: http://stackoverflow.com/questions/31020608/convert-uiview-to-png-in-swift. I'm hoping this also works for NSView. –  Dec 29 '16 at 20:13
  • I've seen how to perform it with UIView in UIKit, can't figure out how to do it on mac. – Alexei Dec 29 '16 at 20:22
  • Ow. Sorry. I tried to help anyways! –  Dec 29 '16 at 20:23
  • 1
    Unrelated, but I wouldn't recommend calling `addSublayer` from `draw`. You're going to be repeatedly re-adding this subview every time the view is rendered. At best, it's inefficient and it's logically the wrong place to do that. You use `draw` if you're going to draw something yourself (e.g. with Core Graphics or what have you). If you're going to add the `CAShapeLayer` as a sublayer, you can do that somewhere else (e.g. from `layout` method or something like that, e.g., https://gist.github.com/robertmryan/963240c9b7f9d22c72fb1711c1da2f88). – Rob Dec 29 '16 at 23:35

1 Answers1

30

Here are some NSView options:

extension NSView {

    /// Get `NSImage` representation of the view.
    ///
    /// - Returns: `NSImage` of view

    func image() -> NSImage {
        let imageRepresentation = bitmapImageRepForCachingDisplay(in: bounds)!
        cacheDisplay(in: bounds, to: imageRepresentation)
        return NSImage(cgImage: imageRepresentation.cgImage!, size: bounds.size)
    }
}

Or

extension NSView {

    /// Get `Data` representation of the view.
    ///
    /// - Parameters:
    ///   - fileType: The format of file. Defaults to PNG.
    ///   - properties: A dictionary that contains key-value pairs specifying image properties.
    /// - Returns: `Data` for image.

    func data(using fileType: NSBitmapImageRep.FileType = .png, properties: [NSBitmapImageRep.PropertyKey: Any] = [:]) -> Data {
        let imageRepresentation = bitmapImageRepForCachingDisplay(in: bounds)!
        cacheDisplay(in: bounds, to: imageRepresentation)
        return imageRepresentation.representation(using: fileType, properties: properties)!
    }
}

Some CALayer options:

extension CALayer {

    /// Get `NSImage` representation of the layer.
    ///
    /// - Returns: `NSImage` of the layer.

    func image() -> NSImage {
        let width = Int(bounds.width * contentsScale)
        let height = Int(bounds.height * contentsScale)
        let imageRepresentation = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: width, pixelsHigh: height, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)!
        imageRepresentation.size = bounds.size

        let context = NSGraphicsContext(bitmapImageRep: imageRepresentation)!

        render(in: context.cgContext)

        return NSImage(cgImage: imageRepresentation.cgImage!, size: bounds.size)
    }
}

Or

extension CALayer {

    /// Get `Data` representation of the layer.
    ///
    /// - Parameters:
    ///   - fileType: The format of file. Defaults to PNG.
    ///   - properties: A dictionary that contains key-value pairs specifying image properties.
    ///
    /// - Returns: `Data` for image.

    func data(using fileType: NSBitmapImageRep.FileType = .png, properties: [NSBitmapImageRep.PropertyKey: Any] = [:]) -> Data {
        let width = Int(bounds.width * contentsScale)
        let height = Int(bounds.height * contentsScale)
        let imageRepresentation = NSBitmapImageRep(bitmapDataPlanes: nil, pixelsWide: width, pixelsHigh: height, bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false, colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0)!
        imageRepresentation.size = bounds.size

        let context = NSGraphicsContext(bitmapImageRep: imageRepresentation)!

        render(in: context.cgContext)

        return imageRepresentation.representation(using: fileType, properties: properties)!
    }
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Is there any way to render CALayer, not only NSView? – Alexei Dec 29 '16 at 23:55
  • I accepted the answer, and added to my question the method to render CALayer into NSImage. – Alexei Dec 30 '16 at 01:51
  • @Alex - See revised answer above for some `CALayer` alternatives. Note, I'm following some of the [Advanced Optimization Techniques: Create and Render Bitmaps to Accommodate High Resolution](https://developer.apple.com/library/content/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/CapturingScreenContents/CapturingScreenContents.html#//apple_ref/doc/uid/TP40012302-CH10-SW30) so this captures the screen resolutions (setting bitmap context to multiply by scaling factor, but resetting the "user size" to capture DPI). – Rob Dec 30 '16 at 07:49
  • Thank you @Rob for this great solutions! – ixany Sep 11 '17 at 11:29
  • Would there be a way to render the content of a CALayer into an image with a given size? Because here, depending on the size of my NSView backed by my CALayer, I get an image with the same size as the view itself, and I would like to get an image with a size I provide to it. – Sebastien Oct 09 '18 at 21:57
  • 2
    This line is pretty inefficient `return NSImage(cgImage: imageRepresentation.cgImage!, size: bounds.size)` as it creates a `CGImage` object which isn't required. Better `let result = NSImage(size: bounds.size); result.addRepresentation(imageRepresentation); return result` A NSImage is only a container with meta data and representations. The image data itself only exists in the representations. – Mecki Jun 13 '19 at 08:24
  • You are a savior! Thank you! Wa ha ha ha ha! – El Tomato Jan 05 '20 at 03:33