기술 문서

PDF 글꼴 및 텍스트: 글리프가 상자로 변하는 이유

본인의 컴퓨터에서는 완벽하게 보이지만 다른 사람의 컴퓨터에서는 빈 상자들의 나열로 렌더링되는 PDF는 문서 소프트웨어에서 가장 흔한 글꼴 결함이며, 이는 거의 텍스트 자체가 잘못되었음을 의미하지 않습니다. 문자는 온전하고 인코딩은 문제없으며, 단지 글리프(glyph)가 없을 뿐입니다. 두 컴퓨터 사이에서 바뀐 것은 운영 체제에 설치된 글꼴뿐이며, 이식성이 뛰어난 파일과 깨지기 쉬운 파일 사이의 차이는 페이지가 작성될 때 내려진 단 하나의 결정, 즉 글꼴이 PDF 내부에 포함되어 이동했는지 아니면 수신 측에 존재한다고 가정했는지에 달려 있습니다

이러한 현상이 발생하는 이유와 검색 가능해 보이는 텍스트를 복사했을 때 왜 외계어처럼 붙여넣기되는 별개의 오류가 발생하는지 이해하려면 PDF가 텍스트를 저장하는 방식을 살펴봐야 합니다. PDF는 문장을 저장하지 않습니다. 글리프 코드와 글꼴 프로그램, 그리고 이 둘을 매핑하는 테이블을 저장하며, 모든 렌더링 또는 추출 버그는 이 세 가지 사이의 간극에 존재합니다. 다음은 ISO 32000을 바탕으로 한 이 메커니즘의 안내이자, 필요한 경우 이를 제어하는 Delphi 호출에 대한 설명입니다

문자, 코드, 글리프는 3가지 다른 개념입니다

일상 언어에서는 이 세 가지 구별되는 개념을 '글자(letter)'라는 단어로 합쳐서 부르기 때문에 어휘에서 혼란을 겪기 쉽습니다. 문자(character)는 추상적인 쓰기 단위, 즉 대문자 A라는 아이디어로, 유니코드에서는 U+0041로 식별됩니다. 글리프(glyph)는 그려진 모양으로, 특정 글꼴이 해당 문자를 묘사하기 위해 사용하는 곡선과 줄기(stem)의 윤곽선입니다. 그 둘 사이에 코드(code)가 존재합니다: 코드는 콘텐츠 스트림 내의 바이트 또는 바이트들로, 뷰어에게 현재 글꼴에서 어떤 글리프를 그릴지 알려줍니다

PDF는 코드로 작동합니다. 콘텐츠 스트림에 문자열이 나타날 때 그 바이트들은 유니코드가 아니라 활성 글꼴의 인덱스입니다. 글꼴의 인코딩은 코드 65가 "65번 파일에 있는 글리프 그리기"를 의미한다고 결정하며, 이 작업 자체는 그 결과가 사람에게 A로 보인다는 사실을 알지 못합니다. 이것이 글리프를 찾을 수 있는 곳이라면 어디서나 PDF가 동일하게 렌더링되는 이유이며, 추출이 표시와 별개의 문제가 되는 이유이기도 합니다. 그리기는 코드-글리프 매핑만 필요하지만 읽기는 코드-유니코드 매핑이 필요하며, 이 둘은 서로 다를 수 있고 독립적으로 누락될 수 있는 두 개의 다른 테이블입니다

실제로 만나게 될 글꼴 유형

ISO 32000은 여러 글꼴 딕셔너리 유형을 정의하지만, 실제로 수신하거나 생성하는 문서에서는 세 가지 중 하나를 사용합니다. 여러분이 보고 있는 유형이 무엇인지 알면 잘못될 수 있는 대부분의 원인을 설명할 수 있습니다

Type 1은 3차 베지에 곡선으로 구성된 Adobe의 원래 PostScript 윤곽선 형식입니다. 규격을 준수하는 모든 뷰어가 제공해야 하는 14개의 표준 글꼴, 즉 Helvetica, Times, Courier, Symbol, ZapfDingbats 제품군은 Type 1이며, 이를 지정하는 글꼴 딕셔너리는 법적으로 글꼴 프로그램을 생략할 수 있습니다. 이것은 글꼴을 포함시키지 않은 채 남겨두는 것이 운이 아니라 사양에 의해 안전하게 보장되는 유일한 경우입니다. 다른 모든 Type 1 글꼴의 경우 프로그램이 포함되어야 하며, 그렇지 않으면 뷰어는 대체 글꼴(일반적으로 자형의 계측값은 비슷하지만 시각적으로 다른 글꼴)로 대체합니다

TrueType은 2차 곡선을 사용하며 Apple과 Microsoft 세계에서 유래했습니다. 대부분의 시스템 글꼴이 이 유형이며, 가장 자주 포함시키게 될 글꼴입니다. PDF의 단순 TrueType 글꼴은 단일 바이트 코드로 제한되므로 한 번에 최대 256개의 글리프를 처리할 수 있습니다. 이 상한선은 한중일(CJK) 및 기타 방대한 문자들이 단순 글꼴로 구동될 수 없는 구조적 이유입니다

Type 0, 복합(composite) 또는 CID 키 글꼴은 이러한 제한에 대한 해답입니다. 다중 바이트 코드와 CMap을 사용하여 이를 하위 CIDFont로 라우팅하며, 이 CIDFont의 윤곽선 자체는 TrueType 또는 CFF/Type 1입니다. 이것은 수천 개의 글리프를 포함할 수 있는 유일한 글꼴 유형이므로, 중국어, 일본어, 한국어 또는 다양한 다국어가 섞인 PDF는 작성자가 의도했든 아니든 Type 0을 사용하고 있습니다. 그 대가는 복잡성입니다: 렌더링과 추출 양쪽 모두를 위해 정확하게 작동해야 하는 이동 요소(moving parts)가 더 많아집니다

PDF에서 12, 18, 24, 36 포인트로 렌더링된 단일 TrueType 글꼴로, 하나의 내장된 윤곽선이 임의의 크기로 확대/축소됨을 보여줍니다

저 사진 이면의 한 가지 세부 사항이 파일 크기를 결정합니다. 글꼴은 고정된 크기의 비트맵이 아니라 윤곽선의 라이브러리이므로 동일하게 내장된 프로그램이 페이지의 모든 포인트 크기를 제공합니다. 크기 조정(Scaling)은 그리기 시에 적용되는 변환입니다. 제목과 본문 텍스트가 하나의 포함된 글꼴을 공유하는 이유이며, 포함에 드는 비용이 크기당이 아니라 글꼴당 발생하는 이유이기도 합니다

포함(Embedding)은 이식성과 취약성의 차이입니다

포함(Embedding)이란 글꼴 프로그램, 즉 실제 윤곽선 데이터가 스트림 형태로 PDF에 기록된다는 뜻입니다. 해당 글꼴을 본 적 없는 컴퓨터의 뷰어는 파일에서 직접 이 윤곽선을 읽어 정확한 글리프를 그립니다. 포함을 건너뛰면 목적지에 동일한 이름의 글꼴이 있기를 도박하는 셈이며; 없을 경우 뷰어는 대체 수단에 의존합니다. 표준 14개 글꼴의 경우 이러한 대체가 정의되어 있고 무해합니다. 하지만 그 외의 모든 경우는 다른 서체의 근사치 렌더링에서부터 어떤 대체재도 해당 문자를 지원하지 못해 빈 상자만 나오는 결과에 이르기까지 다양합니다

HotPDF에서 이 제어는 문서가 열리기 전에 설정되는 단일 속성입니다. FontEmbedding은 라이브러리가 그리기에 사용하는 글꼴을 파일에 패킹하도록 지시합니다:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Compression := cmFlateDecode;
    Pdf.FontEmbedding := True;          // 윤곽선이 파일 내부에서 이동함
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Calibri', [], 11);
    Pdf.CurrentPage.TextOut(72, 760, 0, 'This renders the same on a machine without Calibri.');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

순서는 단순히 겉치레가 아닙니다. BeginDoc은 HotPDF가 문서 구조를 커밋하는 곳이므로 그 호출 이전에 FontEmbedding이 True여야 합니다. 그 이후에 값을 할당하면 오류나 경고 없이 단순히 글꼴이 포함되지 않은 채 조용히 파일이 생성됩니다. 이는 최악의 버그 유형입니다: 글꼴이 설치되어 있는 개발자 컴퓨터에서는 모든 테스트를 통과하지만, 글꼴이 없는 고객 컴퓨터에서만 표면화되기 때문입니다

또한 포함(Embedding)은 라이선스와 엔지니어링이 만나는 지점이기도 합니다. 글꼴 프로그램은 자유로운 포함, 미리보기 전용 포함, 혹은 포함 불가 등 허용 여부를 설명하는 플래그를 지닙니다. 이러한 플래그를 존중하는 것은 렌더러가 아닌 여러분의 책임이며, "작동했다"는 것이 "허용되었다"는 의미는 아닙니다

하위 집합화(Subsetting): 사용한 글리프만 포함시키기

전체 포함(Full embedding)은 전체 글꼴 프로그램을 파일에 씁니다. 대형 한중일(CJK) TrueType 글꼴은 몇 메가바이트에 달할 수 있으며, 고작 십여 개의 문자를 표시하기 위해 글꼴 전체를 포함시키는 것은 다중 페이지 문서에 걸쳐 누적되는 방식의 낭비입니다. 하위 집합화(Subsetting)는 문서가 참조하는 글리프만 쓴 다음, 하위 집합화된 PDF의 글꼴 목록에서 볼 수 있는 ABCDEF+Calibri 형식처럼 6자리 문자 태그와 더하기 기호로 글꼴의 이름을 변경함으로써 이를 해결합니다. 이렇게 하면 뷰어는 부분 글꼴을 이름이 같은 전체 시스템 글꼴과 절대 혼동하지 않습니다

대부분의 생성된 문서에서 하위 집합화는 올바른 기본값입니다. 이는 파일 크기를 원본 글꼴이 아닌 콘텐츠에 비례하도록 유지하며, 하위 집합화하지 않을 경우 파일을 지배하게 될 거대 다국어 글꼴에서 가장 중요한 기능입니다. 한 가지 주의할 점은 하위 집합에는 생성 시점에 사용된 내용만 포함된다는 것입니다. 후속 프로세스에서 나중에 하위 집합화된 글꼴에 텍스트를 추가하려고 할 때 필요한 글리프가 파일에 없을 수 있으며, 이는 다른 사람의 PDF를 증분 편집(incremental editing)할 때 실질적인 제약이 됩니다

유니코드 글꼴 및 한중일 빈 상자 문제

텍스트가 순수한 라틴어(Latin)가 아니면 단순 글꼴 경로는 끝이 나며, 그 해결책은 유니코드 지원 글꼴을 명시적으로 등록하고 HotPDF가 이를 기반으로 Type 0 글꼴을 구축하게 하는 것입니다. RegisterUnicodeTTF는 경로별로 TrueType 파일을 로드합니다. 그런 다음 등록된 이름은 다른 글꼴과 마찬가지로 SetFont에서 사용할 수 있습니다:

Pdf.FontEmbedding := True;
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansCJKsc-Regular.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('NotoSansCJKsc-Regular', [], 14);
Pdf.CurrentPage.TextOut(72, 720, 0, '你好,世界 こんにちは 안녕하세요');
Pdf.EndDoc;

두 가지 요소가 이 작업의 성패를 좌우합니다. 첫째, 글꼴은 문자열의 스크립트(문자)를 커버해야 합니다: 라틴어 전용 TrueType에 요청한다고 해서 중국어 글리프가 생겨나지 않으며, 그 결과는 이번에는 글리프가 해당 서체에 진정으로 존재하지 않기 때문에 다시 빈 상자가 표시되는 것입니다. 둘째, 등록된 TTF에서 조립된 Type 0 글꼴은 윤곽선을 찾을 수 없는 뷰어에게는 무의미하므로 포함 기능(embedding)이 계속 켜져 있어야 합니다. 혼합된 콘텐츠의 경우, 광범위한 커버리지를 제공하는 서체를 선택하는 것이 내구성 있는 선택입니다. Noto 및 Arial Unicode MS 제품군이 주로 정답이며, 이를 포함하고 하위 집합화(subsetting)해야 합니다

우측에서 좌측으로 쓰는 아랍어나 복잡한 문자 체계는 커버리지 위에 셰이핑(shaping) 계층을 추가로 요구합니다. HotPDF는 논리적 순서를 전달하면 라이브러리가 방향 전환 배치를 처리하도록 아랍어 및 히브리어를 위한 RtLTextOut을 노출합니다. 아랍어를 올바르게 처리하려면 커버리지, 셰이핑, 방향성이라는 세 가지 별개의 요소가 필요하며, 빈 상자가 표시된다면 이 중 하나라도 실패했음을 의미할 수 있습니다

ToUnicode 테이블: 복사-붙여넣기의 핵심

위의 모든 내용은 그리기에 관한 것입니다. 텍스트 추출은 그 반대의 과정이며 자체적인 이유로 실패합니다. 뷰어는 글꼴의 코드-글리프 매핑을 사용하여 페이지를 렌더링하지만, 사용자가 텍스트를 선택하고 복사할 때 뷰어는 동일한 코드를 다시 유니코드로 변환해야 합니다. 이 역방향 매핑이 바로 글꼴에 첨부되는 선택적 스트림인 ToUnicode CMap입니다

이 테이블이 존재하고 올바르면 복사된 텍스트가 적절한 문자로 출력됩니다. 이 테이블이 없거나 잘못되었거나 글꼴이 사용자 지정 글리프 코드로 하위 집합화되었음에도 ToUnicode가 작성되지 않은 경우, 페이지는 완벽하게 보이지만 클립보드에는 외계어가 채워집니다: 글리프 코드가 마치 유니코드인 것처럼 읽히는데, 사용자 지정으로 인코딩된 하위 집합의 경우 사실이 아니기 때문입니다. 이것이 OCR 텍스트 레이어가 있는 스캔 문서가 검색 가능한 반면 조심성 없는 생성기가 만든 태생적 디지털 PDF는 검색 불가능할 수 있는 이유입니다. 렌더링과 추출은 각기 다른 테이블을 이용하므로 파일은 하나를 만족시키고 다른 하나는 실패할 수 있습니다. 결과물에서 텍스트 추출이 중요하다면 올바른 ToUnicode 맵을 필수 요건으로 취급하고, 그것이 존재한다고 믿기보다는 샘플에서 텍스트를 복사해 검증해 보십시오

글꼴 버그를 빠르게 진단하는 방법

실패 모드가 어디를 봐야 할지 알려줍니다. 다른 컴퓨터에서 나타나는 빈 상자는 거의 항상 포함되지 않은 글꼴을 의미하므로 먼저 글꼴 포함 여부를 확인하고 다음으로 글리프 커버리지를 확인합니다. 본인 컴퓨터에서도 나타나는 상자는 커버리지 문제를 가리킵니다: 포함 여부와 관계없이 글꼴이 해당 문자를 포함하고 있지 않은 것입니다. 올바르게 렌더링되지만 이상한 텍스트로 복사되는 것은 렌더링 문제가 아니라 ToUnicode 문제이며, 그리기는 결코 망가지지 않았기 때문에 글꼴이나 포함 여부를 만지작거려도 해결되지 않습니다. 완성된 파일을 확인하려면 Acrobat에서 열고 문서 속성의 글꼴(Fonts) 탭을 살펴보세요: 정상적인 항목은 유형을 보여주고, '포함(Embedded)' 또는 '포함된 하위 집합(Embedded Subset)'이라고 명시하며 인코딩 이름을 지정합니다. 포함되어야 할 글꼴이 포함되지 않은 경우 고객이 문제를 알리기 전에 여기서 그 사실을 드러냅니다

문자, 코드, 글리프의 분리가 명확해지면 이 중 어느 것도 이국적이지 않습니다. 그릴 때 사용하는 글꼴을 포함시키고, 큰 글꼴은 하위 집합화하며, 텍스트가 라틴어를 벗어나는 순간 유니코드 서체 및 RegisterUnicodeTTF를 찾고, 누군가 텍스트를 추출할 예정이라면 올바른 ToUnicode 맵을 유지하십시오. 이것들을 바로잡으면 상자(깨진 글꼴)가 나타나는 일은 멈출 것입니다. 이를 둘러싼 기계적인 메커니즘을 더 자세히 알고 싶다면, 최소 PDF의 구조가 객체 트리에서 글꼴 딕셔너리가 위치하는 곳을 보여주며, 문서 구조 탐구에서는 여러 페이지에 걸쳐 리소스가 어떻게 공유되는지 다룹니다

여기서 소개된 SetFont, FontEmbeddingRegisterUnicodeTTF 호출은 Delphi 및 C++Builder용 HotPDF 컴포넌트의 일부입니다