NSImage와 이미지 표현형에 대해 – Cocoa

코코아에서 이미지를 표현하는 클래스로 기본적으로 NSImage를 쓴다. 앱킷에서 이 클래스는 다양한 기본 포맷의 이미지에 대해서 사용할 수 있고, 이미지를 로드하거나, 이미지를 그리는 등의 거의 모든 작업에서 주로 사용되는 클래스이다.

사실 NSImage는 어떤 이미지값을 감싸는 wrapper인데, 우리의 상식과는 달리 이 클래스 자체는 자신의 내부에 들어있는 이미지 데이터에 대해서 별로 아는게 없다. 어떤 이미지는 단일 이미지 내에 표현형에 따라서 여러벌의 이미징 데이터를 가지고 있는 경우가 있기 때문에, NSImage는 이러한 데이터를 담는 일종의 배열처럼 동작할 뿐이고, 실제로 렌더링되는 이미지 데이터는 이미지 표현형 객체에 의해서 다뤄진다. 이러한 이미지 표현형 객체에는 NSImageRep의 여러 서브 클래스들이 있고, 대표적으로 TIFF, JPEG 등의 비트맵 이미지 데이터를 위한 NSBitmapImageRep 클래스가 많이 쓰인다.

NSImage가 하는 일

NSImage는 이러한 진실(?)에도 불구하고 대부분의 경우에 코드상에서는 ‘이미지 객체’를 대표한다라고 취급될 수 있다. 그리고 다음과 같은 작업에는 NSImage 클래스를 사용한다.

  • 특정 URL 및 디스크의 파일로부터 이미지 데이터를 로딩한다.
  • 페이스트보드에 복사된 이미지데이터를 로딩한다.
  • 뷰나 그래픽 컨텍스트에 실제 이미지를 그린다.
  • CALayer 객체의 콘텐츠를 제공해주는 역할을 담당할 수 있다.
  • 일련의 드로잉 명령을 캡쳐하여 새로운 이미지를 만들 수 있다.
  • 특정 이미지에 대해서 여러 가지 다른 파일 포맷 버전을 만들 수 있다.

표현형

NSImage 객체의 representations 속성을 이용하여 해당 이미지가 가지고 있는 표현형들의 목록을 알아낼 수 있다. 이미지 표현형은 주로 NSImageRep 의 서브 클래스의 인스턴스이다. 표현형은 소스데이터로부터 특정한 타입의 이미지를 그릴 때 사용되며, NSImage 가 실제로는 알지 못하는 이미지에 대한 정보들을 제공하기도 한다. 결국 NSIamgedraw(*...) 족 메소드들은 모두 해당 이미지의 표현형의 동일 메소드를 호출하는 것으로 보면 된다. 그리고 이를 위해서 NSImag는 특정한 상황에서 최적의 ㅍ현형을 자동으로 선택하는 기능을 탑재하고 있다.

NSBitmapImageRep

NSBitmapImageRep은 GIF, JPG, PNG, TIFF 포맷을 포함한 여러 비트맵 데이터를 렌더링하기 위해 사용되는 클래스이다. 위에서 언급한 포맷의 비트맵 데이터를 통해서 생성하거나, CGImage, CIImage로 부터 변환될 수 있고, 포커스가 잠긴 (즉 현재 그래픽 컨텍스트와 연결된) 뷰의 특정 영역을 캡쳐하는 방식으로 생성될 수 있다.

특정 뷰의 내용을 캡쳐하기

init(focusedViewRect:)로 생성할 수 있고, 이 때 현재 포커스 고정1 된 뷰의 내용을 캡쳐하여 비트맵 이미지로 만들 수 있다. 다음 함수는 특정한 뷰를 받아서 해당 뷰의 내용을 캡쳐한 PNG 데이터를 생성하여 리턴하는 함수이다.

func getCapturedPNGImage(from targetView: NSView) -> Data?
{
  var pngData: Data?
  targetView.lockFocus()
  if let rep = NSBitmapImageRep(focusedViewRect: targetView.bounds)
  {
    pngData = rep.representation(using:.png, properties:[:])
  }
  targetView.unlockFocus()
  return pngData
}

뷰 내용을 캡쳐하는 다른 방법

뷰 내용을 캡쳐하는 다른 방법들도 있다. NSView의 내용을 PDF로 획득한 후 해당 데이터로부터 이미지 표현형을 생성하는 방법도 있다. 그외에도 NSImage를 포커스 고정하고 뷰의 내용을 가져다 그리거나, 레이어 기반 뷰의 경우에는 이미지 객체의 컨텍스트에다가 해당 레이어를 렌더하게 하는 방법등 방법 자체는 매우 여러가지가 있다.

파일포맷과 표현형의 차이

NSBitmapImageRep 클래스는 표시되는 이미지의 표현형(representation)을 의미한다고 했는데, 이것이 파일 포맷을 그대로 가리키지는 않는다. 표현형은 특정한 데이터로부터 표현되는 이미지를 말하는 것이며, 파일 포맷은 이미지 데이터를 파일에 저장하거나 전송하기 위해 특정한 형식에 맞춰 정리하는 개념이다.

따라서 NSImage가 두 개 이상의 표현형을 가질 수 있다는 말은, 예를 들어 썸네일과 원본 이미지의 두 개의 표현형을 가지고, 렌더링 되어야 하는 뷰의 크기에 따라서 알맞은 표현형을 선택한다는 맥락에서 이해할 수 있으며, 이 때 이미지의 정보를 담고 있는 데이터 자체는 표현형 마다 고유하게 하나의 데이터가 존재한다.

단일 표현형에 대해서 이미지 파일로 이미지를 저장하고자 할 때, 파일 포맷을 선택해야 하며, JPEG, PNG, BMP, GIF, TIFF 등의 파일 포맷을 선택할 수 있다.

참고로 NSImageRep이 제공하는 파일 포맷의 종류는 NSBitmapImageRep.FileType에 정의되어 있다. (https://developer.apple.com/documentation/appkit/nsbitmapimagerep.filetype)

UIImage와의 차이

iOS를 위한 코코아 터치에서는 NSImage를 사용하지 않고 UIImage를 사용한다. 이 클래스는 iOS기기에서 지원가능한 모든 파일 포맷에 대하여 내부적으로 지원할 수 있으며, 표현형을 이미지 클래스와 분리하지 않고 하나의 클래스로 결합해두었다.

따라서 뷰 캡쳐를 위해서 사용할 수 있는 표현형 클래스는 따로 존재하지 않으며 대신 UIGraphicsGetImageFromCurrentImageContext()라는 프레임워크 레벨의 함수에 의해서 캡쳐된 드로잉명령들을 이용해서 이미지에 그림을 그릴 수 있으며, 파일을 생성하고자 하는 경우에는 UIImagePNGRepresentation(_:)UIImageJPEGRepresentation(_:)을 이용해서 이미지 클래스를 파일 데이터로 변환할 수 있다.


  1. 포커스가 고정되었다는 의미는 first responder가 되어 포커스링이 생긴 뷰라는 의미가 아니라, 앱 킷의 drawing 명령을 받는 상태가 되었다는 의미이다. 예를 들어 NSView를 커스텀하게 되면 draw(in:) 메소드를 오버라이드 하게 되는데, 이 때 이 메소드 내부의 모든 드로잉명령은 “현재 그래픽 컨텍스트”에 대해서 적용된다는 것을 가정하며, 해당 메소드가 호출되기 직전에 앱 킷은 그 뷰를 포커스 뷰로 만든 후 호출한다. 

[Cocoa] 여러 이미지를 이어 붙이기

찾으면 찾으면 있겠지만, 손쉽게 여러 장의 이미지를 가로나 세로로 이어 붙여서 하나로 만들어주는 그런 앱이 있으면 참 좋겠다고 생각하다가, 까짓거 하나 만들면 되지 않겠냐 -_- 고 생각이 들어서 써보는 그런 포스팅

이미지 이어 붙이기

앱을 처음부터 끝까지 만들어보기에는 너무 힘든 포스트가 될 것 같아서, 여러 개의 NSImage를 이어붙이는 부분에 대해서만 살펴보기로 하자

대략의 구상

그러니까 레이어(CGLayer)같은 곳에는 이미지를 원하는 크기로 붙여넣을 수 있으니, 이미지(NSImage)들을 CGImage로 바꾸고, 최종 크기의 비트맵 컨텍스트를 만들어 여기에 이미지들을 각각 그려서 이 컨텍스트로부터 비트맵 이미지(CGImage)를 얻어서 저장한다…라고 생각했다. 언뜻 생각하면 상당히 귀찮을 것 같지만 될 것 같다.

기본으로 돌아가서

하지만 이리 저리 생각해본 끝에 방법을 바꾸기로 했다. NSImage도 하나의 뷰이기 때문에 여기에 각 이미지의 drawInRect:fromRect:operation:fraction: 메소드를 사용해서 그냥 그리면 되지 않을까?

  1. 이어붙일 이미지들의 배열을 하나 준비한다.
  2. 세로로 이어 붙이는 경우, 고정된 가로 크기 값을 정한다.
  3. 각 이미지들을 가로 크기로 변경했을 때의 세로크기들에 대해 그 총합을 구한다. 이는 최종 결과물의 세로 크기가 된다.
  4. 가로/세로 크기를 구했으니, NSImage 객체를 initWithSize: 를 사용해서 하나 생성한다.
  5. 새로 생성한 이미지에 lockfocus 한다.
  6. 각 이미지들을 순서대로 적절한 위치에 그려넣는다. 이때, 가로크기가 줄어드는 비율에 대해 세로크기도 함께 줄여주어야 한다. 또한, OSX에서는 왼쪽 아래가 원점이므로, 맨 위에서 부터 그리도록 한다. 빈 이미지가 포커스되었으므로, 이 이미지들은 빈 이미지에 차곡차곡 붙어서 그려진다.
  7. 다 그렸으면 unlockfocus 한다.
  8. 최종 생성된 파노라마 이미지를 데이터로 만들어 저장한다.

실제 구현

먼저, 이어붙일 이미지들은 배열에 담겨있다. 이 배열은 imageList라는 인스턴스 변수로 참조한다. 이들을 담아둘 이미지의 크기를 구해야 한다. 이는 매크로를 사용하여 RESULT_IMAGE_WIDTH 로 지정했다고 가정한다. 우선 최종 생성될 이미지의 크기를 구해야 한다. 가로는 정해졌으니, 세로 크기를 구해보자.

-(float)getResultImageHeight {
    float totalHeight = 0;
    for(NSImage* anImage in imageList) {
        NSSize theSize = anImage.size;
        totalHeight += theSize.height * RESULT_IMAGE_WIDTH / theSize.width;
    }
    return totalHeight;
}

이제 이미지를 만들고 여기에 이미지들을 하나 하나 그려넣으면 된다. 노가다일뿐 어렵지 않다.

-(NSImage *)compositeImage {
    NSSize resultImageSize;
    resultImageSize.width = RESULT_IMAGE_WIDTH;
    resultImageSize.height = [self getResultImageHeight];
    NSImage *resultImage = [[NSImage alloc] initWithSize:resultImageSize];

    NSPoint startPoint;
    startPoint.x = 0;
    startPoint.y = theSize.height;

    [resultImage lockFocus];
    for (NSImage *anImage in imageList) {
        NSSize drawnSize;
        drawnSize.width = RESULT_IMAGE_WIDTH;
        startPoint.y -= drawnSize.height;
        drawnSize.height = anImage.size.height * RESULT_IMAGE_WIDTH / anImage.size.width;
        NSRect drawRect = NSMakeRect(startPoint.x,startPoint.y,drawnSize.width,drawnSize.height);
        [anImage drawInRect:drawRect 
                   fromRect:NSZeroRect 
                  operation:NSCompositeSourceOver 
                   fraction:1.0];
    }
    [resultImage unlockFocus]
        return resultImage;
}

1) lockFocus는 해당 뷰를 포커스된 뷰로 만들어 준다. 이후에 일어나는 드로잉 메소드는 모두 이 곳에 그림을 그리게 된다. 첨엔 “눈에 보이는 뷰”에 대해서만 가능한 줄 알았는데, 그거랑은 상관 없더라.

2) NSZeroRect는 0*0 크기의 사각형인데, 때문에 fromRect:는 이미지 자신의 전체 영역을 그리게 된다.

3) operation은 컴포지션 방법을 정의한다. 만약 비어있지 않은 이미지에 다른 이미지를 그린다면 겹치는 픽셀을 어떻게 표현할 것인지 지정한다.

4) fraction은 덧그려지는 이미지의 불투명도를 0~1 사이의 값으로 지정한다.