NSView의 내용을 이미지로 캡쳐하기

 

NSView의 내용을 비트맵 그래픽 파일로 캡쳐하는 방법에 대해 설명하겠다. 일전에 간략히 적어둔 내용이 있었는데, 잘못된 부분도 있고 예전엔 돼었는데 제대로 동작 안하는 부분도 있어서 다시 정리한다.

PDF로 캡쳐하는 방법도 있으니 살펴보도록 하자.

이미지를 만든다기 보다는 특정한 포맷으로 표현할 수 있은 이미지 표현형을 획득할 수 있으면(NSBitmapImageRep) 이 클래스의 representation(using:properties:)를 사용해서 이미지 파일을 위한 데이터를 생성할 수 있다. 여기에는 크게 두 가지 방법이 있다.

  1. 뷰 자체를 캡쳐하는 NSBitmapImageRep의 이니셜라이저를 사용하기
  2. 뷰의 비트맵 캐시를 추출하기

첫번째 방법은 NSBitmapImageRep의 이니셜라이저 중에서 init(focusedViewRect:)를 사용하는 것이다. Xcode9 기분으로 이 메소드는 뷰의 draw(_:) 내에서 쓸 때에만 유효한 표현형 값이 생성되고, 그 외의 컨텍스트에서는 (제아무리 해당 뷰에 대해서 lockFocus()를 호출하더라도) 제대로 된 데이터가 생성되지 않는다.

두 번째 방법은 뷰의 비트맵 캐시를 추출하는 것이다.

// in NSView's Subclass
func imageRep() -> NSBitmapImageRep? {
  if let rep = bitmapImageRepForCachingDisplay(in: bounds) {
    cacheDisplay(in: bounds, to: rep)
    return rep
  }
  return nil
}

흔히 하는 실수가 bitmapImageRepForCachingDisplay(in:) 을 호출한 결과를 바로 리턴하는 것인데, 이 메소드는 뷰 캐싱을 위한 이미지 표현형 객체를 생성할 뿐이지 그 속에 실제 콘텐츠를 그리지는 않는다. 실제 콘텐츠를 그려넣는 cacheDisplay(in:to:)를 사용해서 콘텐츠를 복사한 후에 이를 사용한다.

비트맵 캐시를 추출할 수 있다면 다음과 같은 식으로 저장하기 메소드를 만들 수 있다.

@IBAction func saveAsPNG(_ sender: Any?) {
  guard let rep = imageRep() else { return  }
  let panel = NSSavePanel()
  panel.allowedFileTypes = ["png"]
  
  let handler: (NSApplication.ModalResponse) -> Void = { res in 
    if res == .OK,
      let data = rep.representation(using:.png, properties:[:])
    {
      do {
        try data.write(to: panel.url!)
      } catch {
        NSLog("Error while saving file.")
      }
  }
  
  if let window = self.window {
    panel.beginSheetModal(for:window, completionHandler: handler)
  }
}

만약 draw(_:) 내에서 그래픽 컨텍스트를 이용하여 그림을 그리는 함수를 가지고 있다면, 비트맵 이미지 표현형을 생성한 후에 이를 바탕으로 이미지를 그려서 만들어도 된다. (이는 기본적으로 비트맵 그래픽 컨텍스트를 만들고 여기에 그림을 그려서 CGImage를 생성하는 방법과 완전히 동일하다.)

func offscreenImage(ofSize repSize: CGSize) -> NSImage {
  let offscreenRep = NSBitmapImageRep(
      bitmapDataPlanes: nil, // nil을 넘기면 자동으로 할당한다.
      pixelsWide: Int(repSize.width),
      pixelsHigh: Int(repSize.height),
      bitsPerSample: 8,
      samplesPerPixel: 4,
      hasAlpha: true,
      isPlaner: false,
      colorSpaceName: NSColorSpaceName.diviceRGB,
      bitmapFormat: .alphaFirst,
      bytesPerRow: 0,   // 8 * 4 * width / 8 인데 계산하지 않고 0 을 넘긴다.
      bitsPerPixel: 0)  // 역시 계산하지 않고 0을 넘긴다.
  
  let g = NSGraphicsContext(bitmapImageRep: offscreenRep!)
  NSGraphicsContext.saveGraphicsState()
  NSGraphicsContext.current = g

  let ctx = g!.cgContext
  let imageFrame = CGRect(origin: CGPoint.zero, size: repSize)
  ctx.setFillColor(NSColor.red.cgColor)
  ctx.fillEllipse(in: imageFrame)
  ...
  
  let image = NSImage()
  image.size = repSize
  image.addRepresentation(offscreenRep!)
  return image
}

참고자료

관련내용

UIView를 UIImage로 캡쳐하는 방법도 있다.