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로 캡쳐하는 방법도 있다.

Read more

워드프레스에서 고스트로 이전

워드프레스에서 고스트로 이전

이 글을 쓰면서도 믿기 힘든 사실인데, 블로그라는 걸 처음 시작한지가 20년이 되었습니다. 이글루스에서 처음 시작했다가, SK컴즈가 인수한다고 발표함과 동시에 워드프레스로 플랫폼을 옮겼죠. 워드프레스오 옮긴 이후에는 호스팅 환경을 이리 저리 옮기긴 했지만 거의 18년 가까이 워드프레스를 사용해온 것 같습니다. 그 동안 워드프레스는 블로깅 툴에서 명실상부한 범용CMS로 발전했습니다. 사실 웬만한 홈페이지들은 이제

By sooop
띄어쓰기에 대한 생각

띄어쓰기에 대한 생각

업무 메일을 쓸 때 가장 많이 쓰는 말 중에 하나가 메일 말미에 ‘업무에 참고 부탁 드립니다.‘인데요, 어느 날부터 아웃룩에서 이 ‘부탁 드립니다’가 틀렸다고 맞춤법 지적을 하기 시작했습니다. 맞는 말은 ‘부탁드립니다’라고 붙여 쓰는 거라고. 사실 아래아한글 시절부터 이전의 MS워드까지, 워드프로세서들의 한국어 맞춤법 검사 실력은 거의 있으나 마나 한

By sooop

구글 포토에서 아이클라우드로 탈출한 후기

한 때 구글 포토가 백업 용량을 무제한으로 제공해 주겠다고해서, 구글 포토를 사용해서 사진을 백업해왔습니다. 물론 이 이야기의 결말은 저나 이 글을 읽고 있는 여러분이나 모두 알고 있습니다. 사실 AI에게 학습 시킬 이미지 데이터를 모으기 위한 것일 뿐이라거나 하는 이야기는 그 당시에도 있었습니다만, 에이 그래도 구글인데 용량은 넉넉하게 주겠지…하는 순진한

By sooop

Julia의 함수 사용팁

연산자의 함수적 표기 Julia의 연산자는 기본적으로 함수이며, 함수 호출 표기와 같은 방식으로 호출하는 것이 가능합니다. 또한 그 자체로 함수이기 때문에 filter(), map() 과 같이 함수를 인자로 받는 함수에도 연산자를 그대로 적용하는 것이 가능합니다. 특히 + 연산자는 sum() 함수와 같이 여러 인자를 받아 인자들의 합을 구할 수 있습니다. 2 + 3 # = 5 +(2,

By sooop