좀 더 나은 폰트 렌더링을 위해 (수정됨)
발단
개인적인 의견이지만, 저는 윈도우의 ClearType이 심미적으로 굉장히 좋지 않다고 생각합니다. 실제로도 디자인한 작업을 구현했을 때 어딘가 마음에 안 들어서 하나하나 뜯어보면 폰트 렌더링의 문제가 꽤나 큽니다. 저는 이 문제를 해결하기 위해 많은 호환성 이슈를 떠앉으면서까지 MacType을 깔아서 사용하고 있습니다. 하지만, 모든 윈도우에 MacType을 깔 수는 없는 셈이기에 제가 만든 웹 사이트가 타 윈도우 유저 입장에서 안 이쁘게 보이는 문제는 어떻게 해결할 수가 없습니다.
그래서 저는 항상 ClearType 하에서도 잘 보이는 폰트를 찾아 헤매고는 합니다. 그 결과로 KoPub 과 같이 ClearType을 썼을 때도 이쁘게 보이는 폰트들을 몇 개 찾기는 했지만, 가뜩이나 적은 한글 본문용 산세리프 OFL 폰트 중에서 ClearType 하에서도 예쁘게 보이는 폰트는 많지 않습니다.
그 이유로 이 블로그의 스킨인 Kaede는 로컬에 KoPubWorldDotum이 깔려있다면 그걸 먼저 쓰고, 그게 아닌 경우에는 Noto Sans KR을 받아오도록 설정되어 있습니다. (아쉽게도 KoPub 폰트는 출판인회의에 연락해본 결과 임베딩을 현재 허용하고 있지 않다고 합니다.)
하지만 Noto Sans KR도 힌팅의 문제인지 안티 앨리어싱이 어딘가 2% 부족한 느낌이 든다고 개인적으로 생각합니다. 그러던 도중에 한 티스토리 블로그에서 좀 더 나아진 듯 한 외곽선을 가진 Noto Sans KR을 사용하고 있는 사실을 발견했습니다.
그래서 그 Noto Sans KR는 어떤 차이점이 있어서 구글 폰트의 Noto Sans KR과 다르게 렌더링이 되는지 폰트 레벨에서 분석해보기로 마음을 먹었습니다.
참고로 이 글에 첨부된 이미지는 윈도우 10 (1809) 크롬88 기준으로 렌더링된 것입니다. 크롬이니 아마 Skia로 렌더링 됐지 않을까 싶은데 자세히는 모르겠습니다.
그러나 파이어폭스, 엣지(EdgeHTML)와 같은 다른 브라우저에서도 notokr과 Google Fonts 간의 유의미한 렌더링 차이는 존재했습니다.
분석
우선은 해당 파일이 어디서 왔는지부터 알아볼 필요가 있었습니다. 구글에 해당 파일의 파일명인 notokr을 검색해본 결과 다음과 같은 사이트에서 배포하고 있는 것을 발견하였습니다. martian36
폰트를 얻은 후에는 구글폰트 버전과 notokr 버전을 비교해보았습니다.
먼저 fonttools로 주어진 woff2 파일을 읽고, 구글 폰트에서 배포하는 woff2를 읽어 차이점을 확인해보았습니다. 가장 처음 보이는 차이점은 바로 가지고 있는 테이블이 다른 것이었습니다.
- notokr:
'FFTM', 'GDEF', 'OS/2', 'cmap', 'gasp', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'webf'
- Noto Sans KR:
'CFF ', 'GPOS', 'GSUB', 'OS/2', 'VORG', 'cmap', 'head', 'hhea', 'hmtx', 'maxp', 'name', 'post', 'vhea', 'vmtx'
둘다 prep
이나 fpgm
과 같은 테이블은 보이지 않았습니다.
조금 더 쉽게 데이터를 확인하기 위해 fonttools를 이용해 xml 형식인 ttx 파일로 내보내기 해서 작업했습니다. TTFont.saveXML
을 이용하면 ttx 파일로 내보내실 수 있습니다.
모르는 테이블은 다음과 같은 도큐먼트를 참고해서 데이터를 확인해 보았습니다.
그런데 커스텀 테이블과 같이 보이는 모르는 테이블이 있어 검색해보니 다음과 같은 좋은 글도 있었습니다.
gasp 테이블
제일 처음 의심한 것은 바로 gasp 테이블이었습니다. notokr에는 \x00\x01\x00\x01\xff\xff\x00\x0f
로 구성되어 있는 gasp 테이블이 들어있는 반면에, 구글 폰트 버전에는 들어있지 않았습니다.
해당 내용을 분석해보면 1개의 gasp range가 들어있는데, FF FF (65535)
ppem 까지 GASP_GRIDFIT
, GASP_DOGRAY
, GASP_SYMMETRIC_GRIDFIT
, GASP_SYMMETRIC_SMOOTHING
을 적용하라고 되어 있었습니다.
특히 GASP_SYMMETRIC_SMOOTHING
의 설명을 보면 Use smoothing along multiple axes with ClearType 이라고 되어 있었습니다.
그래서 이것으로 안티 앨리어싱에 차이가 발생했나 하고 gasp 테이블을 바꿔보았습니다.
from fontTools.ttLib import TTFont, newTable
target = "./noto-sans-kr-v13-latin_korean-regular"
def patch_font():
font = TTFont("./%s.woff2" % target)
print("Exporting TTX File...")
font.saveXML("./%s.ttx" % target)
print("Adding gasp table...")
gaspTable = newTable('gasp')
gaspTable.gaspRange = {
0xffff: 0xf
}
font['gasp'] = gaspTable
print("Exporting WOFF2 File....")
font.flavor = 'woff2'
font.save("./%s_patched.woff2" % target, reorderTables = True)
print("Done! :)")
gasp 테이블을 바꿔본 결과 놀랍게도 렌더링 결과에 별 차이가 없었습니다.
제가 봤을 때는 정말 1픽셀의 오차도 없이 같은 것 같은데, gasp 테이블이 렌더링에 미치는 영향이 원래 별로 없는지 아니면 제가 뭔가 실수를 했는지 좀 더 자세한 분석이 필요해 보입니다.
OpenType vs TrueType
아무튼 gasp 테이블은 별로 영향이 없는 것 같고, 다른점을 조금 더 찾아보았습니다. 우선은 구글폰트의 버전은 OpenType (CFF)로 되어 있고, notokr은 TrueType (glyf)로 되어 있습니다.
먼저 알아두어야 할 것은 오픈타입은 3차 베지어 커브를 사용하고 트루타입은 2차 베지어 커브를 사용하기 때문에 완벽하게 오픈타입에서 트루타입으로 변환할 수 있는 방법은 없습니다. 하지만, 최대한 가깝게 근사를 해서 변환할 수는 있습니다.
그래서 일단 Cu2qu를 이용해서 구글폰트의 버전을 트루타입으로 변환해 보았습니다. (다음 프로젝트를 참고했습니다.) 변환한 결과 근사 알고리즘이 달랐는지 notokr과 Contour 정보가 다르긴 했습니다. 하지만, 변환된 파일 또한 notokr과 비슷한 렌더링 결과를 낳았습니다.
라인 메트릭 등이 조금 다르긴 하지만 그래도 거의 유사하게 렌더링 되는 것을 확인할 수 있습니다.
결론
확실하지는 않지만, OTF 글리프 정보 (CFF)를 TTF 글리프 정보 (glyf)로 변환할 경우에 다음과 같은 결과가 일어나는 것을 확인할 수 있었습니다.
결론은 굉장히 간단하긴 한데, 그래도 이렇게 폰트를 뜯어보면서 폰트의 구조에 대해서도 조금 더 자세히 이해할 수 있어서 더 좋았던 것 같습니다.
그리고 처음 알게된 것 중 하나에는 많이 흥미로운 내용도 있습니다. 그것은 바로 fpgm 테이블에 들어가는 인스트럭션은 튜링완전하다는 건데, 참고했던 nixeneko님의 블로그에 이를 이용해 의사난수를 집어넣어 테두리가 랜덤하게 움직이는 폰트를 만드는 글도 있었습니다.
이걸 가지고 신기한 걸 많이 만들 수 있을 것 같은데 나중에 한 번 시도해 봐야겠습니다.
여담
notokr은 메타데이터를 확인해본 결과 FontSquirrel Webfont Generator로 만들어진 것 같습니다. 이 사이트에는 아까 봤던 gasp을 설정해주는 것과 같은 옵션들 또한 있었습니다.
또한, 해당 사이트에서 변환된 파일들이 FFTM 테이블을 가지고 있는 것으로 보아 내부적으로 FontForge를 사용해서 변환하는 것 같습니다. 저는 처음에 FontForge로 폰트를 저장하는 과정에서 폰트에 변형이 일어난다고 생각했는데, 완전 오산이었습니다. 이 글도 사실 그런 잘못된 정보를 담고 있었다가, 실험을 몇 번 더 해보며 잘못됐다는 것을 깨닫고 다시 쓴 글입니다.
사실 변형 자체는 일어나는게 맞는 것 같습니다. Contour 정보가 다르긴 했는데, 렌더링에 유의미한 결과는 주지 못한 것 같습니다.