타입 지우기 – Type Erasure (Swift)

프로토콜 타입

우리가 만약 타입을 알 수 없는 어떤 객체의 특정한 메소드를 호출해야 하는 상황을 생각해보자. (어렵게 생각할 것 없이, 델리게이트 패턴에서 이것은 매우 흔한 일이다.) 타입을 알 수 없다는 것은, 그 객체가 공개하고 있는 인터페이스를 알 수 없다는 뜻이며, 따라서 어떤 메시지를 보내는 것이 불가능하다는 것을 의미한다. 하지만, 서로 다른 타입들이 같은 이름의 메소드를 구현해 둘 것을 약속만 한다면, 이야기가 달라진다.

프로토콜은 미리 정의된 인터페이스의 모음으로, 이를 따르는 타입들은 그 프로토콜에 명시된 인터페이스를 구현해놓은 것으로 가정할 수 있다. 동적 프로그래밍에서는 어떤 객체가 A타입처럼 행동하면 A타입으로 간주할 수 있다고 한다. (어떤 새가 오리처럼 날고, 오리처럼 꽥꽥거린다면 그 새를 오리라 부르지 않을 이유가 무엇인가) 굳이 동적 언어가 아니더라도, 어떤 객체가 그 실제 타입 T가 무엇이든간데, 프로토콜 P를 준수하고 있다면 우리는 P 타입에 대한 상호작용만 한다는 가정하에서 그 객체를 P 타입으로 보아도 무방할 것이다. 아니면 다른 S타입의 객체가 P를 준수한다고 하면, 두 객체를 여전히 같은 P 타입으로도 볼 수 있을 것이다.

타입 지우기 – Type Erasure (Swift) 더보기

오일러 프로젝트 81

문제

\begin{pmatrix}  131  & 673 & 234 & 103 & 18\\ 201  & 96 & 342 & 965 & 150 \\ 630 & 803 & 746 & 422 & 111 \\ 537 & 699 & 497 & 121 & 965 \\ 805  & 732 & 524 & 37 & 331 \end{pmatrix}

위와 같은 5 x 5 행렬에서 좌측 상단에서 출발하여 오른쪽이나 아래쪽으로만 움직이면서 우측 하단까지 가는 경로의 합을 구해 보면 아래와 같이 빨갛게 표시된 경로가 2427로서 가장 작습니다.

31KB 짜리 파일 matrix.txt에는 80 x 80 행렬의 정보가 들어있습니다. 위와 같은 방법으로 이 행렬의 좌측 상단에서 출발하여 우측 하단까지 갈 때, 경로합의 최소값은 얼마입니까?

오일러 프로젝트 81 더보기

오일러 프로젝트 76

76번 문제는 예전 31번(영국화폐 조합의 수)와 사실상 같은 문제이다. 임의의 자연수 N 을 N보다 작은 자연수들의 합으로 나타내는 경우의 수를 분할수라고 하는데, 이는 결국 1…N-1 의 액면가를 가지는 동전들로 N 만큼의 금액을 만드는 것과 동일한 연산이다.

오일러 프로젝트 76 더보기

NSImage와 CGImage 변환하는 법

NSImage > CGImage로 변환하기

NSImagecgImage(forProposedRect:Context:hints:)라는 메소드를 가지고 있는데, 이는 어떤 영역에 그려질 최적의 CGImage 객체를 찾아서 리턴하는 기능을 수행한다.  이 때 모든 파라미터는 옵션이며, 전부 nil로 넣어도 상관없다. 다만 파라미터들은 NSImage가 가지고 있는 여러 개의 CGImage 표현형 중에서 어떤 것을 선택할지를 결정하는데 도움을 주는 힌트에 해당한다. 

proposedDestRect는 CG이미지를 사용할 영역에 대한 참조로 주로 어느 해상도(크기)에서 사용될 것인지를 정한다. 만약 nil을 전달하면 NSImage의 크기 영역을 기준으로 삼게 된다. context는 그래픽 컨텍스트이며, hints 값은 그외의 힌트가 된다. 

이미지 표현형을 통해서 얻기

사실 이 방법은 위의 메소드를 호출하는 것과 별반 다르지 않을텐데, NSImage가 이미지를 ‘다루는’데 목표를 둔 반면에 실제로 어떤 그림인지에 대해서는 신경쓰지 않는 특이한 구조에 착안하는 것이다. 표현되는 그림이 무엇인지는 representations 파라미터를 통해서 알 수 있는 것이다. 이는 [NSImageRep] 타입으로 실제 이미지를 나타내는 데이터들이 모여 있다. (그 중에는 비트맵도 있을 수 있고 벡터나 PDF 등 그 형식은 매우 다양할 수 있다.) 

하지만 해당 NSImage가 비트맵 이미지를 기반으로 생성되었다는 사실을 보장할 수 있다면, 해당 이미지의 표현형 중에서 NSBitmapImageRep 인 것을 고르고 그 중 하나 (대표적으로 가장 앞에 있는 것)의 cgImage 속성을 취하는 것이다. 

if let cgImage = (nsImage.representations as? [NSBitmapImageRep])?.first.cgImage {
  // use cgImage...
}

CGImage -> NSImage 로 변환하기

CGImageNSImageView등에서 표현하기 위해서는 다시 NSImage로 변환해야 한다. NSImage의 편의 이니셜라이저 중에는 init(cgImage:size:) 가 있는데 이것을 사용하면 된다. 

참고로 이 때 size 파라미터에 NSZeroSize를 사용하면, 주어진 CGImage의 픽셀 폭/높이를 기준으로 사이즈를 삼게된다. 

다른 한가지 방법으로는 앞에서 비트맵이미지 표현형(NSBitmapImageRep)을 사용한 방법을 거꾸로 한 것이 있다.  NSBitmapImageRepCGImage로 바로 생성이 가능하다. (또 CIImage로도!) 이렇게 만들어진 표현형을 빈 NSImage에 추가하는 것이다.

let rep = NSBitmapImageRep(cgImage: cgimage)
let image = NSImage()
image.addRepresentation(rep)


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)
}