6.힌팅과 커닝

.힌팅

외곽선 폰트의 가장 큰 장점은 확대와 축소가 자유롭다는 점이다. 물론 비트맵 폰트도 제한적이기는 하지만 어느 정도 확대, 축소가 가능은 하다. 그러나 래스터 방식의 비트맵은 확대시 계단 현상이 나타난다는 면에서 외곽선 폰트에 비해 질적으로 열세이다. 외곽선 폰트는 곡선의 정보가 벡터 방식의 좌표로 저장되어 있으며 확대 배율만큼 곡선을 다시 계산하여 그리므로 언제나 부드러운 모양으로 출력된다.

그러나 외곽선 폰트도 완전하지는 않아서 약간의 문제가 있다. 확대, 축소란 원래의 디자인된 모양에 인위적인 가공을 하는 것이다 보니 원치않게 부자연스러운 모양이 만들어지기도 한다. 확대할 때도 문제가 있기는 하지만 특히 축소할 때 문제점이 눈에 띄게 드러난다. 어떤 문제점이 있는지 축소할 때의 출력 결과들을 가정해 보자. 다음은 알파벳 H 문자를 단계적으로 축소한 예이다.

축소할 때는 축소 배율에 따라 글리프에 저장된 좌표를 곱하고 나누어 픽셀 좌표를 결정하는데 이 과정에서 실수 절삭이 발생한다. 100을 2로 나누면 정확하게 50이 되지만 3으로 나누면 33.3333이 되며 이때 소수점 이하를 버리면 0.3333만큼의 오차가 발생하는 것이다. 아무리 정밀하게 계산을 한다 하더라도 최종 출력 장치가 실수 단위의 좌표를 표현하지 못하는 래스터 장비이기 때문에 이 오차를 어떻게 할 방법은 없다.

더 심각한 문제는 문자의 진행 폭처럼 순서대로 누적되는 값은 위치에 따라 오차의 발생 여부가 달라진다. 똑같은 글자라도 또는 같은 글자의 다른 부분이라도 어느 위치에서 시작하는가에 따라 실수 절삭이 발생할 수도 있고 그렇지 않을 수도 있다. 이런 이유로 축소를 하다 보면 H 문자의 양쪽 수직 획의 굵기가 다르게 출력될 수 있다. 두 획의 굵기가 똑같이 계산되었더라도 출력 좌표와 더해 반올림을 하다 보면 어떤 경우는 1이 되고 어떤 경우는 2가 되어 버리는 것이다. 보다시피 글자 모양이 굉장히 궁색해 보인다. 다음은 좀 더 심각한 경우를 보자.

해상도가 낮은 장비의 경우 축소를 하다보면 획이 사라지기도 한다. 영문 소문자 m은 다리가 세개이며 각 다리 사이에 여백이 최소한 한 줄씩은 들어가야 한다. 이 문자를 표현하려면 여백을 제외한 글리프의 폭이 최소한 5픽셀은 되어야 하나 그보다 작아지면 다리가 합쳐져 n 문자와 구별되지 않는다. 이 문제는 출력 품질이 예쁘고 안 예쁘고의 문제가 아니라 아예 다른 문자가 되어 버리므로 굉장히 심각하다.

영문의 경우는 그래도 글자 모양이 단순해서 왠만해서는 이런 경우가 드물지만 한글의 경우는 글자가 복잡해서 꼭 필요한 획이 사라지는 경우가 많다. 예를 들어 "황" 같은 글자를 8 픽셀 이하로 축소하면 작은 획이 사라져 "횡"이 될 수도 있고 "힁"처럼 보이기도 한다. "ㅏ"의 가로획이 너무 짧아 아예 사라져 버린 것이다. 한글이 이 지경인데 훨씬 더 복잡한 문자인 한자는 어떠할지 굳이 설명하지 않아도 짐작이 될 것이다.

물론 해상도가 낮아 글자의 가독성이 떨어지는 것은 어쩔 수 없는 문제이다. 깨알만한 크기로 축소해 놓고도 그 글자가 제대로 보이기를 바랄 수는 없다. 하지만 인위적인 조작을 통해 출력되는 외곽선 폰트는 해상도가 적당해도 이런 경우가 생각보다 빈번히 발생한다는 것이 문제다. 해상도가 낮을 경우 글자의 특정 부위를 다소 과정해서라도 판독 가능하게 출력해야 하며 해상도가 충분할 경우라도 최대한 균형을 맞추어 예쁘게 출력하기 위해 최선을 다해야 한다.

최종 글자의 출력 모양을 다듬기 위해서는 글리프의 곡선 정보 외에도 추가적인 정보가 필요한데 이 정보를 힌트(Hint)라고 한다. 외곽선 폰트는 수학적 과정을 거쳐 만들어진 글리프에 힌트 정보를 적용하여 최종적으로 한번 더 가다듬음으로써 좀 더 보기 좋고 가독성이 높은 모양으로 가공한다. 힌트 정보의 예를 들자면 "H의 두 획은 크기가 같아야 한다" 라든가 "m의 다리 셋은 모두 보여야 한다", "아무리 작아도 이 획은 생략해서는 안된다" 는 식으로 되어 있을 것다. 물론 요렇게 말로 되어 있을 리는 없고 압축된 형태로 부호화되어 있다.

힌팅(또는 그리드 핏팅이라고도 한다)은 힌트 정보를 참조하여 글꼴에 인위적인 변형을 가하는 동작이다. 폰트 포맷별로 힌트 정보를 구성하는 방법과 적용하는 방법이 다른데 크게 다음과 같은 방식으로 분류할 수 있다.

 

■ 명시적 힌팅 : 힌팅을 수행하는 프로그램을 폰트에 내장하는 방식이다. 폰트는 플랫폼 독립적이어야 하므로 이 프로그램은 가상 머신 기반의 op 코드로 되어 있다. 폰트 드라이버는 매 글리프를 출력할 때마다 op 코드를 수행하여 모양을 다듬는다. 윈도우즈의 트루 타입 폰트가 이 방식으로 되어 있다.

■ 암시적 힌팅 : 각 글리프에 힌트 정보를 내장한다. 코드가 아닌 데이터만 저장되므로 훨씬 더 단순하고 크기가 작다. 이 정보는 폰트를 그리는 드라이버에 의해 해석되어 글리프에 적용된다. Type1 폰트가 이 방식으로 되어 있다.

■ 자동 힌팅 : 글리프에 어떠한 힌트 정보도 포함되지 않는다. 글리프를 사용하는 프로그램이 추측을 통해 힌트를 생성해내고 적용한다. 상식적인 수준에서 힌팅을 하는 방식이다. 폰트는 가벼워지지만 대신 랜더러의 부담이 증가한다.

 

각 방식은 각자 장단점이 있다.

 

 

속도

크기

품질

일관성

명시적

느리다

크다

가장 우수하다

일관되다

암시적

빠르다

작다

우수하다

랜더러에 좌우됨

자동

랜더러에 좌우됨

가장 작다

낮다.

랜더러에 좌우됨

 

명시적 힌팅은 느리지만 품질이 우수하고 암시적 힌팅은 빠르지만 품질이 조금 떨어진다. 품질이나 속도 외에도 일관성 여부도 중요한 차이점이다. 명시적 힌팅은 힌팅 알고리즘이 폰트에 내장되어 있으므로 어디에서 출력하나 항상 동일한 모양으로 출력된다. 그러나 암시적 힌팅은 힌트 정보를 해석하여 적용하는 랜더러에 따라 모양이 달라질 수도 있다. 비록 미세한 차이겠지만 똑같은 문서를 윈도우즈에서 출력했을 때와 매킨토시에서 출력했을 때 결과가 달라진다면 이 또는 무시못할 단점이라고 할 수 있다.

운영체제의 고수준 출력 함수나 FreeType 같은 저수준 라이브러리나 모두 힌트 정보를 알아서 사용하도록 되어 있으므로 힌팅에 대해 특별히 신경을 쓸 필요는 없다. FreeType은 트루 타입 바이트 코드에 대한 인터프리터까지 내장하고 있어 우수하고도 일관된 결과를 만들어 낸다. 뿐만 아니라 힌트 정보가 없는 폰트를 위해 자동화된 오토 힌트 기능까지 제공된다.

힌팅은 굉장히 복잡하고 어려운 기술이지만 다행히 힌팅에 대해서는 개발자가 이래라 저래라 할 필요가 거의 없는 셈이다. 다만 아주 특수한 경우를 위해 FreeType은 글리프를 로드할 때 FT_LOAD_NO_HINTING, FT_LOAD_NO_AUTOHINT 등의 플래그로 힌팅의 적용 여부나 방식에 대해 개입할 수 있는 장치를 제공한다. 이 플래그들도 나름대로 실용적으로 쓸 때가 있겠지만 솔직히 왠만해서는 쓸 일이 없다.

.커닝

커닝(Kerning)이란 연속되는 두 글자의 간격을 보기 좋게 재배치하는 기술이다. 정사각형의 한글에서는 이런 기법이 굳이 필요치 않아 앞 글자의 폭만큼 이동한 후 다음 글자를 출력하기만 하면 된다. 그러나 모양이 좀 더 자유 분방한 알파벳과 기호들은 단순히 폭만큼 간격을 띄우기만 해서는 어색한 모양이 나온다. 일반적인 워드 프로세서들은 모두 커닝을 지원하므로 워드를 열어서 커닝이 어떤 기술인지 체험해 보자.

BRAVO라는 문자열을 두 번 출력했는데 위쪽은 커닝을 적용하지 않고 단순히 글자의 폭만큼 띄워가며 문자들을 찍은 것이다. B와 R 사이의 간격이나 R과 A의 간격은 눈에 거슬리지 않지만 A와 V의 간격은 다른 글자들에 비해 상당히 벌어진듯한 느낌이 든다. 육안으로 잘 관찰해 보면 A의 발가락과 V의 어깨 좌표가 거의 일치해 심하게 많이 벌어진 것은 아니다. 하지만 두 글자의 좌우에 삼각형의 여백들이 합쳐져서 뭔가 허전한 공간이 느껴지는 것이다.

A와 V처럼 외양상 벌어져 보일 수 있는 글리프 조합에 대해서는 일정 정도 간격을 줄일 필요가 있는데 이 정보를 커닝이라고 한다. 자연스럽게 출력해도 결과가 만족스럽지 못하기 때문에 인위적으로 간격을 더 줄여 최대한 보기 좋게 만드는 것이다. 아래쪽은 커닝을 적용한 것인데 A와 V가 실제 문자폭보다 조금 더 작게 평가되어 가깝게 붙어 있으며 전체적으로 문자의 간격들이 균일한 것처럼 보인다. 엄밀하게 따지자면 V가 A의 영역을 침범했지만 어쨌거나 사람 눈에는 이게 더 좋아 보인다. 워드 프로세서들은 디폴트로 커닝을 적용하도록 되어 있다.

좀 더 예쁜 출력을 위해 페이스는 커닝 정보를 포함하지만 모든 폰트가 다 커닝 정보를 제공하는 것은 아니므로 페이스에 커닝 정보가 있는지를 먼저 점검해야 한다. face의 플래그를 읽어 FT_FACE_FLAG_KERNING 비트가 설정되어 있는지를 확인해 보면 된다. 이 플래그를 확인하는 매크로가 정의되어 있으므로 매크로만 호출해 보면 커닝값의 존재 여부를 쉽게 파악할 수 있다.

 

#define FT_HAS_KERNING( face ) ( face->face_flags & FT_FACE_FLAG_KERNING )

 

트루 타입의 경우 커닝이 페이스에 포함되어 있지만 Type1의 경우는 페이스 파일과는 별도의 파일에 커닝 정보가 배포되므로 별도로 파일을 읽어서 페이스에 첨부해야 한다. 다행히 FreeType 라이브러리는 커닝 정보의 위치에 상관없이 원하는 두 문자에 대해 커닝값을 구하는 함수를 제공한다.

 

FT_EXPORT( FT_Error ) FT_Get_Kerning( FT_Face face, FT_UInt left_glyph, FT_UInt right_glyph, FT_UInt kern_mode, FT_Vector *akerning );

 

페이스 객체와 좌우 두 문자의 인덱스를 전달하면 두 문자 사이의 커닝값이 리턴된다. 커닝 모드는 커닝값을 어떤 식으로 리턴할 것인가를 지정하는데 FT_KERNING_DEFAULT로 지정하면 스케일링과 힌팅을 적용한 후 반올림까지 한 커닝값이 26.6 포맷으로 리턴된다. 대개의 경우 스케일링과 힌팅을 한 후의 커닝을 적용하므로 이 모드로 커닝값을 읽으면 충분하다. 그러나 다른 이유로 스케일링 이전의 커닝값이나 힌팅을 하기전의 커닝값이 필요하면 다른 모드를 지정하면 된다.

리턴된 커닝값은 양쪽 문자를 더 가깝게 붙이는 역할을 하므로 대개의 경우는 음수일 것이다. 이 값을 펜의 현재 위치에서 더하만 하면 된다. 음수를 더했으므로 펜 위치가 왼쪽으로 살짝쿵 이동하여 두 문자의 간격이 좁혀진다. 다음 예제는 문자열을 두 번 출력하되 커닝 적용 여부를 달리했다.

 

: Kerning

void DrawGlyph(HDC hdc,int bx, int by, FT_Face face)

{

     int x, y;

     int Color;

     int width, height;

     width=face->glyph->bitmap.width;

     height=face->glyph->bitmap.rows;

 

     for (y=0;y<height;y++) {

          for (x=0;x<width;x++) {

              Color=255-face->glyph->bitmap.buffer[y*width+x];

              if (Color != 255) {

                   SetPixelV(hdc,bx+face->glyph->bitmap_left+x,

                        by-face->glyph->bitmap_top+y,RGB(Color,Color,Color));

              }

          }

     }

}

 

LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     HDC hdc;

     PAINTSTRUCT ps;

     FT_Face face;

     static TCHAR str[]=TEXT("AV T.Test");

     int idx;

     int penx,peny;

     unsigned left=0,right=0;

     FT_Vector kerning;

 

     switch (iMessage) {

     case WM_CREATE:

          hWndMain=hWnd;

          return 0;

     case WM_PAINT:

          hdc=BeginPaint(hWnd, &ps);

 

          FT_New_Face(library,"c:\\windows\\fonts\\times.ttf",0,&face);

          FT_Set_Char_Size(face, 128 * 64, 0,

              GetDeviceCaps(hdc,LOGPIXELSX), GetDeviceCaps(hdc,LOGPIXELSY));

 

          // 커닝 미적용

          penx=0 * 64;

          peny=150 * 64;

          for (idx=0;idx<lstrlen(str);idx++) {

              FT_Load_Char(face,str[idx],FT_LOAD_RENDER | FT_LOAD_NO_BITMAP);

              DrawGlyph(hdc,(penx >> 6), (peny >> 6), face);

              penx += face->glyph->advance.x;

              peny += face->glyph->advance.y;

          }

 

          // 커닝 적용

          if (FT_HAS_KERNING(face)) {

              penx=0 * 64;

              peny=300 * 64;

              for (idx=0;idx<lstrlen(str);idx++) {

                   right = FT_Get_Char_Index(face,str[idx]);

                   if (left != 0) {

                        FT_Get_Kerning(face,left,right,FT_KERNING_DEFAULT,&kerning);

                        penx += kerning.x;;

                   }

                   FT_Load_Glyph(face,right,FT_LOAD_DEFAULT | FT_LOAD_NO_BITMAP);

                   FT_Render_Glyph(face->glyph, FT_RENDER_MODE_NORMAL);

                   DrawGlyph(hdc,(penx >> 6), (peny >> 6), face);

                   penx += face->glyph->advance.x;

                   peny += face->glyph->advance.y;

                   left=right;

              }

          }

 

          FT_Done_Face(face);

          EndPaint(hWnd, &ps);

          return 0;

     case WM_DESTROY:

          PostQuitMessage(0);

          return 0;

     }

     return(DefWindowProc(hWnd,iMessage,wParam,lParam));

}

 

단순한 문자열 출력 코드이되 커닝을 적용할 때는 좌우 문자의 인덱스를 저장해 두었다가 조사된 커닝값만큼 펜의 x 좌표에 더하는 코드가 추가되어 있다. 실행 결과는 다음과 같다.

커닝 여부에 따라 A와 V의 간격이 틀려지며 T와 마침표의 간격도 다르다. 마침표는 앞 글자 바로 다음에 찍어야 하지만 T나 F 처럼 오른쪽 아래가 움푹 들어간 글자일 때는 조금 왼쪽에 찍는 것이 더 보기에 좋다. 커닝을 적용하면 전체적으로 문자열의 길이가 짧아진다.

참고로 GDI의 TextOut이나 GDI+의 DrawString은 힌팅과 안티 알리아싱은 자동으로 수행하지만 커닝은 처리하지 않는다. 고수준의 함수로는 커닝을 바로 처리할 수 없으며 저수준 함수를 직접 호출하거나 아니면 개별 문자를 하나씩 따로 출력하는 방법을 사용해야 한다. 운영체제 수준에서는 커닝이 필요한 문자들의 목록을 구해주는 정도의 서비스(GetKerningPairs)만 제공한다.

 

이상으로 FreeType 라이브러리에 대한 강좌를 마친다. 짧은 시간에 경황없이 쓴 강좌이지만 FreeType의 주요 기능은 거의 다 소개한 것 같아 나름대로 만족스럽다. 뭔가 새로운 것을 공부한다는 것은 언제나 즐거운 일이지만 습득한 지식을 글로 정리하는 것은 결코 쉽지가 않다. 항상 시간이 충분치 않다는 점이 안타까울 따름이다.