오일러 프로젝트 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)
}

aiohttp에서 큰 파일을 업로드하는 법

파일 업로드는 보통 요청의 body에 인코딩된 파일 데이터를 넣어서 POST 요청으로 서버에 전달되는데, aiohttp에서는 다음과 같이 post를 처리하는 핸들러를 사용해서 이를 처리할 수 있다.

async def store_mp3_handler(request):
  data = await request.post()
  mp3 = data['mp3']
  filename = mp3.filename
  mp3_file = mp3.file
  content = mp3_file.read()
  return web.Response(body=content, headers=
     MultiDict({ 'CONTENT-DISPOSITION': mp3_file}))

여기서 문제는 request.post() 메소드가 요청 데이터를 한꺼번에 메모리로 읽어들이기 때문에 메모리 부족으로 서버가 죽을 수 있는 상황이 있다는 것이다. 따라서 aiohttp에서 일반적으로 처리할 수 있는 요청의 크기는 2MB로 제한된다. 하지만 이 크기는 어지간한 사진 하나의 용량도 감당하기 어렵기 때문에 뭔가 다른 방법이 필요하다. (보통은 일종의 옵션 값 같은 걸로 최대 처리 요청 크기를 변경할 수 있을 줄 알았는데, 없었다.)

request.multipart는 이러한 문제를 피하는 멀티파트 리더로 기능할 수 있다. 리더 객체를 생성한 다음에는 멀티파트 요청을 단위 콘텐츠 별로 읽어들일 수 있다. requrest.multipart() 메소드는 멀티파트 요청을 각 파트별로 리턴하는 비동기 제너레이터인데, 각각의 파트(필드)는 read() 메소드를 통해서 통째로 읽어들이거나, read_chunk() 메소드를 통해서 필드의 일부분을 순차적으로 버퍼에 읽어들일 수 있다.

BodyPartReader.read_chunk(size=chunk_size::int)


https://docs.aiohttp.org/en/stable/multipart_reference.html#aiohttp.BodyPartReader.read_chunk

다음 코드는 mp3 파일을 폼 필드에서 mp3라는 필드 이름으로 업로드했을 때, 이를 받아서 저장하는 서버쪽 코드이다. 바이너리 파일을 버퍼로 읽어들여서 순차적으로 저장하는 것과 동일한 방식으로 처리한다. 기존 코드와 차이점이 있다면 awiat request.read()는 HTTP요청 자체를 하나의 사전과 비슷한 객체로 읽어들이는 반면, 개별 필드를 각각 요청하고 처리해야 한다.

async def store_mp3_handler(request):
  reader = await request.multipart()

  field = await reader.next()
  assert field.name == 'name'
  name = await field.read(decode=True)
  
  field = await reader.next()
  assert field.name == 'mp3'
  filename = filed.filename

  size = 0
  with open(os.path.join('/spool/yarrr-media/mp3/', filename), 'wb') as f:
    while True:
      chunk = await field.read_chunk()
      if not chunck:
        break
      size += len(chunk)
      f.write(chunk)
  return web.Response(text=f'{filename} sized of {size} successfully stored.')

이 방법을 사용하여 메모리에 부담을 주지 않고 대용량 파일을 업로드 받을 수 있다.