[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 사이의 값으로 지정한다.

vi의 조금 편리한 기능들

사실은 조금 더 강력한 기능이랄까.

반복

  • . – (dot) 구두점은 마지막으로 실행한 명령을 다시 한 번 더 실행해준다. 명령의 반복은 다음과 같이 볼 수 있다.
  • {숫자} {명령} – 명령 앞에 숫자를 붙이면 그 숫자만큼 반복하게 된다. 이동할 때 3 w라고 하면 세 단어를 이동하는 데 이것이 사실은 거의 모든 명령에도 적용될 수 있다.
2dd     # 현재 줄을 포함하여 2줄을 삭제한다.
3p      # 잘라낸 2줄을 3번 붙인다.
100iDescription <ESC>  # 'Description '을 100번 삽입한다.
.       # 위의 것을 1번 더 즉 100번 더 쓴다.
3p      # 100번 쓰는 것을 3번 하니 300번을 더 쓴다.

특정 지점으로 이동

w로 다음 단어의 첫 글자로 이동하거나, e로 단어의 끝으로 이동하는 것 외에 몇 가지 추가적인 이동 명령. 역시 이동 명령은 ‘선택’하는 액션과 동일하게 동작하므로 살펴보면 도움이 된다.

  • f a – 다음에 나타나는 ‘a’로 이동. 3 f a는 다음에 나타내는 a 중 3번째로 이동. 대문자는 역 방향.
  • t a – 다음에 나타나는 ‘a’의 바로 앞으로 이동. 대문자는 역방향.
  • % – 괄호 (대/중/소)에서 입력하면 그에 상응하는 괄호로 이동함. 코드에서 괄호를 안 닫거나 하는 부분을 찾기가 아주 편리하다.
  • ] ) – 다음번 닫는 괄호로 이동한다. 여는 괄호로 이동은 ](. 다른 괄호 종류에 대해서도 동일하게 적용됨
  • ( , ) – 이전/다음 문장
  • {, } – 이전/다음 문단

Visual Mode

비주얼 모드는 텍스트의 일부 구간을 선택하는데 시각적으로 선택된 텍스트를 반전시켜 어떤 텍스트가 선택되는지를 눈으로 보여준다. 비주얼 모드와 비슷하게 비주얼 블록 모드가 있는데 이는 화면상에 사각형 영역으로 (행전체가 선택되지 않게) 선택할 수 있다. (일부 좋은 기능들이 많은 텍스트 편집기들이 이런 기능을 지원하고는 있음)

  • v i – 따옴표로 둘러싼 영역을 모두 선택
  • v a – 따옴표로 둘러싼 영역을 따옴표를 포함하여 선택
  • v – 비주얼 모드 시작. 비주얼 모드를 시작하면 현재 위치로부터 커서를 옮기는 만큼 색상이 반전되며 선택된다. 선택된 내용에 대해 d 나 y 로 오려두기/복사하기를 할 수 있다.
  •  ^v – (Ctrl + v)비주얼 블록 모드. 비주얼 블록 모드는 행 단위가아닌 열 단위로 선택하게 한다.
  • I, A – i, a 의 대문자. 비주얼 블록 모드로 선택한 여러 행에 걸친 위치에 같은 내용을 삽입한다. 예를 들어 1행부터 40행까지의 내용을 “// “으로 주석처리하고자 한다면 다음과 같이한다. (아래 [^v]는 control+v를 말하며 공백은 스페이스바를 입력했음을 의미한다.)
g g 0 ^v 3 9 j I// ESC
  • gg – 1번행으로 간다.
  • 0 – 맨 왼쪽으로 간다
  • [^v] – 비주얼 블록 모드 시작
  • 39j – 39행 아래로 이동하여 40행까지 첫번째 열을 모두 선택
  • I – multiple insert mode 시작
  • //  – “// ” 입력
  • [ESC] – 삽입모드 종료. 40행까지 모두 주석처리가 된다.
  • <,>  – 비주얼 모드에서 선택된 라인들에 대해 좌/우 방향으로 들여쓰기, 내어쓰기를 적용
  • = – 비주얼 모드에서 선택된 라인들에 대해 자동 들여쓰기 (정말 쿨함)

자동완성

insert mode에서 입력중에 ^p를 누르면 이전에 입력한 적 있는 단어 중에서 비슷한 단어를 자동완성해주는 기능이 있다.

창 분할

  • :split – 창을 상/하 분할한다. (분할된 공간에는 새 파일을 만들 수 있다.)
  • :vsplit – 창을 좌/우 분할한다.
  • ^w ^w – 분할된 창 사이를 이동할 수 있다.
  • ^w _ – 현재 창을 최대화 (분할된 공간을 최대화함)
  • ^w | – 현재 창을 최대화 (세로 분할인 경우)
  • :tabnew – 새로운 탭을 만든다.
  • g t – 다른 탭으로 이동한다.

[Python] SQLite 데이터베이스에 미리 manipulate 하기

영어사전 류의 앱을 만들 때는 미리 데이터를 정제하여 데이터베이스에 밀어 넣어 두는 것이 필요하다. 예를 들어 엑셀 파일이나 텍스트 파일등에 들어있는 데이터가 있다고 한다면 미리 sqlite 파일로 옮겨놓는 작업에 대해 잠깐 메모한다.

먼저 데이터 베이스 파일을 생성한다. 데이터베이스 파일의 생성은 firefox의 sqlmanager를 사용하여 GUI를 통해 생성할 수도 있다. 만약 필드 수가 많다면 일일이 코드를 타이핑하여 스크립트를 만드는 것보다 이를 사용하는 것이 훨씬 쉽다. 그렇지 않다면 다음과 같이 간단히 테이블 생성 쿼리를 보내 DB파일을 생성하는 스크립트를 실행해 준다.

#preparing DB
import sqlite3

filename = 'dictionary.sqlite'
db = sqlite3.connect(filename)
query = '''CREATE TABLE IF NOT EXISTS KRDIC (
ID INT PRIMARY KEY NOT NULL AUTOINCREMENT,
KEYWORD TEXT,
DESCRIPTION TEXT);'''
cursor = db.cursor()
cursor.excute(query)
db.commit()
db.close()

만약 원본 데이터가 엑셀에 있다면 엑셀을 사용해서 각 셀이 탭으로 구분된 텍스트 파일로 저장한다. SQLite에 넣으려면 기본적으로 UTF-8로 인코딩되어야 하는데, 엑셀이 유니코드를 지원하는지는 잘 모르겠다. 아마 CP949로 인코딩된 한글 텍스트 파일일 가능성이 크다. 따라서 스크립트 내에서 인코딩 문제는 처리해야 한다.

아래 스크립트의 주요 내용은 이러하다.

  1. 텍스트 파일을 열고 한 줄의 내용을 읽어들인다.
  2. 한 줄의 내용을 탭 문자로 분리하여 리스트(배열)로 만든다.
  3. KEY 값으로 주기 위해 number 타입 변수를 하나 생성한다.
  4. 각각의 줄에 대해서 KEY 및 각 필드의 값을 추출하여 리스트로 (혹은 튜플로) 만든다.
  5. 루프를 돌면서 한 줄씩 필드의 내용을 DB에 insert 한다
  6. 루프가 끝나면 DB와 파일을 각각 닫는다.
아래는 예제 코드. 마지막 부분의 if __name__ == “__main__”:은 파일을 바로 실행했을 때, 메인 함수처럼 동작하도록 하는 부분이다.
[python]
def fileInsert(sourceFilename = 'kordict.txt'):
db = sqlite3.connect('dictionary.sqlite')
cursor = db.cursor()
f = open(sourceFilename, 'r', encoding='cp949')
l = f.readline()
index = 0
while(l):
    lineItem =  [index,] + l.split('\t')
    recordList = []
    for i in lineItem:
        if (type(i)==type('a')):
            recordList.append(i.encode('utf-8'))
        else:
            recordList.append(i)

    cursor.execute('''INSERT INTO KRDICT VALUES (?,?,?)''', tuple(recordList[0:3]))
    index = index + 1
    try:
        l = f.leadline()
    except:
        print("error")

print("complete")

db.commit()
db.close()
f.close()

if __name__ == "__main__":
fileInsert() 
[/python]

[Objective-C] 프로토콜

프로토콜의 개념 잡기

프로토콜은 “선언만 되고 구현되지 않은” 메소드를 말한다. 이 프로토콜은 역시나 “너무 단순해서 쉽게 감이 잡히지 않는” Objective-C의 기능이다. 프로토콜은 크게 다음 3가지 경우에 유용하게 사용된다.

[Objective-C] 프로토콜 더보기

[Cocoa] NSImage를 파일로 저장하기

NSImage를 파일로 저장하기

이미지를 파일로 저장하기 위해서는 ‘표현형식'(representation)에 대해 알아야 한다. 즉 이미지 자체는 일련의 픽셀을 모아둔 데이터인데, 우리가 흔히 알고 있는 JPG, PNG 등의 파일 포맷은 이 이미지를 각각 정해진 방식으로 ‘정리’하여 파일에 저장하기 때문이다. 표현형식은 이런 파일에 저장된 비트맵 데이터를 다시 이미지로 렌더링하는 역할을 하고, 그 반대의 역할도 수행할 수 있다.

NSImage는 1) TIFFRepresentation 메소드를 사용하여 표현형식을 구성하는 데이터로 만든다. 이 데이터를 사용하여 2) NSBitmapImageRep 객체를 생성하고, 이 객체에서부터 3) 실제 저장이 가능한 그래픽 파일 포맷의 데이터를 생성해서 ( -representationUsingType: ), 그래픽 파일 포맷으로 만들어진 데이터를 저장해야 우리가 알고 있는 ‘그림 파일’이 만들어진다.

NSImage *currentImage; // 이 이미지는 이미 존재하는 이미지로 가정한다.  
NSString *pathToSave = ... // 파일을 저장할 경로  
NSData *imageData = [currentImage TIFFRepresentation];  
NSBitmapImageRep *rep = [NSBitmapImageRep imageRepWithData:imageData];  
NSData *dataToWrite = [rep representationUsingType:NSPNGFileType  
properties:nil];  
[dataToWrite writeToFile:pathToSave atomically:NO];

이 TIFF표현형으로부터 각 파일 포맷에 대한 저장용 데이터를 생성할 수 있다. 지원하는 포맷은 다음과 같다.

  • NSTIFFFileType : TIFF
  • NSBMPFileType : BMP
  • NSPNGFileType : PNG
  • NSJPEGFileType : JPG
  • NSGIFFileType : GIF
  • NSJPEG2000FileType : JPEG2000

이 때 전달하는 프로퍼티는 사전의 형태로 파일을 저장할 때 필요한 옵션을 지정한다. PNG로 저장하는 경우에는 인터레이스 여부만 필요하다. 그외 JPG포맷의 압축 정도나 애니메이션 GIF를 저장하는 설정 등을 다룬다. 자세한 내용은 개발자 문서를 참고.

이미지의 표현형에 대해 조금 익숙해진다면, 뷰 자체를 캡쳐하여 이를 저장하는 것도 손쉽게 가능하다. NSBitmapImageRep는 initWithFocusedViewRect: 메소드가 있어서 현재 포커스 뷰의 일부 혹은 전체 내용을 잘라, 이를 데이터로 변환한다. 역시 동일한 방식으로 뷰의 내용을 그대로 저장하는 것이 가능하다.

화면을 캡쳐할 때 iOS의 경우에는 좀 다른 방식으로 접근해야 한다. 비트맵 그래픽 컨텍스트를 만들어서 이 곳에 뷰의 CALayer를 렌더링해 넣는 방식으로 CGImage를 얻을 수 있다. 그리고 이 이미지를 다시 저장하면 된다. 이에 대해서는 별도의 포스팅으로 알아보도록 하겠다.