NSBitmpaImageRep의 사용법

CGImage는 CoreGraphics에서 사용하는 픽셀단위 비트맵이미지 데이터를 다루는 클래스이다. 이 클래스가 주로 이미지 렌더링이나 오프스크린 드로잉등에 사용되는 관계로 주요 API가 이러한 작업에 치중하고 있어서 실제로 이미지 파일로부터 데이터를 읽어서 생성하거나 데이터를 저장하는 작업은 클래스내에서 처리할 수 없다.

그나마 CIImageinit?(contentsOf:)를 제공하기 때문에 지원가능한 파일을 읽어들여서 인스턴스를 만드는 작업을 바로 수행할 수 있다. 다만 실제로 CIImage는 렌더링되는 실제 이미지라기 보다는 코어이미지 내의 필터를 적용하는 레시피로 기능하기 때문에 상황은 약간 다른다.

또한 iOS의 경우에는 주로 사용되는 UIImageCGImageCIImage로 이니셜라이저만으로 상호변환이 가능하며, JPEG 및 PNG 포맷 데이터도 간단하게 생성할 수 있기 때문에 문제가 되지 않는데, macOS의 경우에는 파일데이터와 이미지데이터가 모두 따로 놀기 때문에 혼선이 있을 수도 있고, 여러 모로 귀찮은 부분들이 많이 존재한다.

이번 시간에는 비트맵 이미지 데이터와 이미지 파일 사이를 연결해주는 NSBitmapImageRep에 대해서 좀 알아보도록 하자.

NSBitmapImageRep

NSBitmapImageRepNSImageRep의 서브클래스로 비트맵이미지를 렌더링하는 역할을 담당한다. NSImageRepNSImage의 실제 ‘그림 내용’이 되는 셈인데 왜 이런식으로 이미지를 다루고자 할 때 두 개의 클래스가 동원되어야 했을까?

이는 macOS의 그래픽환경의 역사와 관련이 깊다. 전통적으로 애플은 macOS에서 화면으로 표시될 수 있는 모든 것이, 그대로 프린터 등의 다른 장치로 표시될 수 있기를 원했다. 말 그대로 WYSIWYG가 OS의 그래픽 환경에서부터 그대로 구현되기를 원했던 것이다. 따라서 지금은 코어 그래픽이 된 쿼츠(Quartz)에서부터 ‘그래픽 컨텍스트’라는 개념을 도입하여 일련의 동일한 드로잉 명령으로 시각적 이미지를 생성하고, 출력되는 장치에 맞게 그 내용을 렌더링하는 것이다.

예를 들어 NSView를 예로들어보자. 화면에 그려지는 대부분의 뷰는 NSView의 서브클래스이고, 이들 뷰들은 draw(in:) 메소드를 통해서 자기 스스로를 그리는 방법을 알고 있다.

만약 화면에 그려진 내용을 출력하거나, PDF로 만들기를 원한다면 프로그래머는 그에 상응하는 드로잉 메소드를 따로 구현할 필요가 없다. 왜냐하면 프린터로 출력하거나 PDF 데이터로 출력할 때에도 NSView의 가족들은 화면에 자신을 그릴 때와 동일한 드로잉 명령을 사용하기 때문이다. 이 동작에서의 차이는 각각의 뷰들이 자신을 그리는 대상이 ‘서로 다른 컨텍스트’라는 것 뿐이라는 것이다.

이러한 맥락에서, 다양한 출력환경에 단일 이미지가 대응하는 최선의 방법을 생각해보자. 출력해상도가 높은 프린터와 같은 환경에 대응하기 위해서는 계단 현상이 없는 벡터 이미지를 사용하는 것이 가장 좋은 선택이 될 수 있다. 반면 잦은 갱신이 필요한 화면 디스플레이에서는 매번 벡터이미지를 비트맵으로 래스터화하는 것이 부담스러울 수 있으므로 적절한 해상도의 비트맵 이미지를 사용하는 것이 권장된다. 디스플레이 역시 고밀도 픽셀 (쉽게 말해 레티나) 환경이라면 더욱 큰 비트맵을 사용하는 것이 더 나은 품질의 결과를 보일 것이다.

이렇게 상황에 따라서 다른 소스를 쓰면서, 고수준 API는 하나로 통일하는 방법으로 NSImage는 일종의 ‘이미지의 배열’이면서 그 내부의 실제 ‘그림’하고는 상관없는 클래스가 되었다. NSImage는 여러 개의 “표현형”을 가지고 있으면서 이미지를 렌더링할 때 가장 최적의 표현형을 선택해주는 역할을 담당하는 셈이다.

NSImage의 표현형으로서 기능하는 클래스가 NSImageRep이며, 이는 이미지 자체의 종류에 따라 NS*imageRep의 여러 서브클래스로 나눠진다. 대표적으로 많이 쓰이는 것이 NSBitmapImageRep, NSCIImageRep, NSPICTImageRep, NSPDFImageRep, NSEPSImageRep이다.

즉 만약 어떤 NSImage 객체가 비트맵, PDF, EPS 타입의 표현형을 모두 가지고 있다면, 화면에 비트맵으로 출력되거나 PDF로 만들어지거나, 프린터로 출력될 때 모두 가장 적절한 표현형을 자동으로 사용하여 최적의 결과로 렌더링될 수 있다는 것을 의미한다.

생성

NSBitmapImagRep은 크게 세 가지 방법으로 생성할 수 있다.

  • init?(data: Data)
  • init(cgImage: CGImage)
  • init(ciImage: CIImage)

데이터는 NSBitmapImageRep가 지원하는 타입의 파일 데이터를 넘겨주면 된다. 실질적으로 이미지 파일로부터 비트맵이미지를 생성하는 셈이다. 지원가능한 타입은 NSBitmapImageRep.FileType이라는 열거타입에 정의되어 있는데, .bmp, .gif, .jpg, .jpeg2000, .png, .tiff 가 있다.

CGImage, CIImage로의 변환

init?(cgImage:)를 사용하면 CGImage로부터 비트맵표현형을 만들어낼 수 있으며, cgImage:CGImage? 프로퍼티를 통해서 곧바로 CGImage 객체를 얻을 수 있다.  반대로 CIImage의 경우에는 init(bitmapImageRep:) 을 통해서 생성할 수 있다.

따라서 파일경로로부터 데이터를 읽어들여 CGImage를 만드는 과정은 다음과 같이 처리할 수 있다.

func readCGImage(from filepath: String) -> CGImage? {
  let url = URL(fileURLWithPath: filepath)
  if let data = try? Data(contentsOf: url),
     let rep = NSBitmapImageRep(data: data) {
    return rep.cgImage
  }
  return nil
}

이미지 파일로 저장하기

비트맵 이미지 표현형이 CIImage, CGImage로 자유롭게 변환가능하다는 것은 곧 해당 타입의 이미지들을 파일로 저장할 때, NSBitmapImageRep를 사용할 수 있다는 것을 의미한다.  representation(using:properties:)는 이미지 표현형으로부터 특정 이미지 타입의 파일 데이터를 생성한다.  (이 때 properties 파라미터는 거의 쓸 일이 없다고 보면 되는데, 궁금하다면 해당 레퍼런스를 찾아보자.)

// saving image
let url = ...
let rep = NSBitmapImageRep(cgImage: source)
if let data = rep.representation(using:.png, properties:[:]) {
  try? data.write(to: url)
}