Technical Article

경고 없이 폰트 서브세팅을 비활성화한 EndDoc 버그

보고서를 생성하고 TrueType 폰트를 임베드하면 출력 파일이 시도하는 모든 뷰어에서 올바르게 열립니다. 글리프가 올바르고 텍스트를 선택할 수 있으며 파일도 유효합니다. 유일한 문제는 크기입니다. 수십 개의 라틴 문자만 사용한 문서가 350KB의 전체 폰트를 포함하고 있습니다. 중국어 한 단락을 인쇄한 문서가 필요로 하는 0.5MB 슬라이스 대신 14MB CJK 폰트를 통째로 포함하고 있는 것입니다. 어떠한 예외도 발생하지 않았고 경고도 기록되지 않았으며 파일은 검증을 통과했습니다. 이것이 순서가 잘못된 마무리 단계가 외부에서 보기에 어떤 모습인지 보여주는 예입니다. 실패하는 것은 없으며 유일한 증거는 너무 큰 파일 크기뿐입니다.

이러한 문제를 일으킨 버그는 HotPDF의 특정 릴리스 라인에 존재했으며 이후 수정되었습니다. 이 오류는 단순한 결함 고지가 아니라 하나의 교훈으로 작성할 가치가 있습니다. 오류의 형태가 일반적이기 때문입니다. 모든 문서 엔진은 쓰기 직전에 개체를 변환하는 마무리 단계를 가지고 있으며, 해당 단계의 올바름은 직렬화와 비교한 단계의 순서에 전적으로 의존합니다. 단 한 단계라도 쓰기 작업의 잘못된 위치에 놓이게 되면 아무런 경고 없이 작동하지 않게 됩니다.

폰트 서브세팅의 원래 역할

서브셋 폰트는 문서에서 실제로 사용하는 TrueType 파일의 일부입니다. ISO 32000-1 §9.9에서는 임베디드 폰트 프로그램이 폰트 디스크립터에 의해 참조되는 스트림에 포함되는 방식을 설명하며, TrueType 프로그램의 경우 이 스트림은 압축되지 않은 바이트 수를 제공하는 /Length1이 포함된 /FontFile2입니다. 서브세팅은 스펙의 요구사항에 맞게 glyfloca 테이블을 다시 작성하여 문서가 참조하는 글리프만 포함하도록 하고, 글리프 식별자 번호를 다시 매기며, /BaseFont 이름 앞에 ABCDEF+와 같은 6자리 태그를 붙여 폰트가 서브셋임을 표시합니다. 라틴 서체 서브세팅을 통해 10KB 또는 15KB로 줄이는 것은 간결한 PDF를 만드는 것과 단 하나의 제목을 위해 전체 서체를 포함하는 PDF를 만드는 것의 차이를 만들어 냅니다.

이 작업이 수행되는 시점이 중요합니다. 서브세팅은 이미 디스크에 있는 바이트에 적용하는 변환이 아닙니다. 이는 메모리 내 개체 그래프를 편집하는 작업입니다. 즉, /FontFile2 스트림 콘텐츠를 축소하고, /Length1을 수정하며, /BaseFont 문자열을 다시 작성합니다. 직렬화 프로그램(serializer)이 그래프를 탐색하고 바이트를 방출할 때 이 모든 것이 제자리에 있어야 합니다. 만약 바이트가 작성된 후에 편집이 이루어지면 아무도 읽지 않을 메모리 내 개체만 업데이트하게 됩니다.

증상 및 경고가 발생하지 않은 이유

보고된 증상은 진단 메시지 없이 출력물에 전체 폰트가 그대로 포함되는 것이었습니다. 유니코드 TrueType 폰트를 등록하고 정상적인 문서를 생성한 사용자는 임베디드 폰트 개체의 크기가 소스 .ttf 파일과 동일하고, /BaseFont 이름에 6자리 서브셋 접두사가 포함되지 않은 것을 발견했습니다. 10개의 글리프를 사용하든 10,000개의 글리프를 사용하든 출력 파일의 크기는 전혀 줄어들지 않았습니다.

아무런 오류가 발생하지 않는다는 점이 이 버그를 해결하는 데 많은 비용이 들게 만드는 요인입니다. 잘못된 시점에 실행되는 서브세팅 루틴도 여전히 실행되기는 합니다. 이 루틴은 누적된 코드포인트 사용량을 탐색하고, 완벽하게 올바른 서브셋을 구축하며, 메모리의 개체 그래프에 이를 적용합니다. 내부적으로는 작업이 완료되고 호출이 정상적으로 반환됩니다. 유일하게 잘못된 점은 작성기(writer)가 이미 작업을 끝냈기 때문에 편집된 개체 그래프가 더 이상 파일로 작성되지 않는다는 것입니다. 호출자의 관점에서는 문서가 문제없이 생성되고 저장된 것처럼 보이며, 이는 자동 실패가 주는 전형적인 인상입니다.

근본 원인은 마무리 순서 오류

HotPDF에서 문서 종료 작업은 EndDoc 내부에서 발생합니다. 서브세팅 단계는 BuildAndApplyUnicodeFontSubset이라는 내부 루틴입니다. 이 루틴은 글리프가 표시될 때 텍스트 방출 경로가 채우는 비트맵에 보관된 문서당 사용된 코드포인트 집합을 읽고, 캐시된 코드포인트 대 글리프 테이블을 통해 각 사용된 코드포인트를 실제 글리프 식별자에 매핑하며, 해당 클로저를 기준으로 폰트 프로그램을 다시 작성합니다. 유니코드 TrueType 폰트가 등록되면 방출 경로는 그리는 모든 문자에 대해 사용된 코드포인트 집합의 비트를 설정하므로, 문서가 닫힐 때 엔진은 서브셋이 유지해야 하는 글리프가 무엇인지 정확히 알게 됩니다.

이 결함은 SaveToStream 또는 SaveToFile이 문서를 이미 직렬화한 후에 BuildAndApplyUnicodeFontSubset이 호출되었기 때문에 발생했습니다. 서브세터가 수행한 /FontFile2 편집, 수정된 /Length1 및 6자리 /BaseFont 접두사는 모두 이미 바이트로 변환된 개체 그래프를 대상으로 계산되었습니다. 해결 방법은 단 한 줄의 순서를 바꾸는 것이었습니다. 서브셋 호출을 직렬화보다 앞으로 이동하여 작성기가 원본 대신 서브셋 폰트를 내보내도록 하는 것입니다. 수정된 순서는 먼저 서브세터를 실행한 다음 직렬화를 수행합니다.

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
    Pdf.CurrentPage.TextOut(72, 760, 0, '报表标题 Report Heading');
    Pdf.EndDoc;                 // subsetting runs here, before the write
    Pdf.SaveToFile('Report.pdf');
  finally
    Pdf.Free;
  end;
end;

순서가 수정되면 호출 코드에 대해 변경할 사항은 없습니다. 유니코드 TrueType 폰트가 등록되면 서브세팅은 기본적으로 활성화됩니다. 폰트를 등록하고 문서를 시작하고 그린 다음 종료하면, 바이트가 메모리를 벗어나기 전에 사용한 글리프로부터 서브셋이 구축됩니다.

잘못 배치된 한 단계가 전체 카테고리 오류인 이유

이 문제가 사소한 결함이 아니라 중요한 교훈인 이유는 EndDoc이 여러 종료 단계 목록을 내보내며, 모든 단계가 쓰기 작업에 상대적인 위치에 민감하기 때문입니다. 폰트 서브세팅이 그 중 하나입니다. PDF/A 출력에는 서브셋에 존재하는 글리프 식별자를 정확히 열거하는 /CIDSet 스트림이 필요하며, 이는 검증기가 임베디드 프로그램이 폰트 디스크립터가 주장하는 바와 일치하는지 확인할 수 있도록 ISO 19005가 부과하는 제약 조건입니다. 이 스트림은 동일한 마무리 창에서 내보내지며 서브셋이 먼저 작성되어야 합니다. PDF/UA-1은 ISO 14289-1 §7.18.3에 따라 주석을 포함하는 모든 페이지가 /S 값과 함께 /Tabs를 선언하도록 요구하며, EnsurePDFUATabsOnAnnotatedPages라는 내부 루틴이 동일한 단계에서 해당 키를 표시합니다. 출력 목적(Output-intent) 검사도 이 단계에서 실행됩니다.

서브세팅을 비활성화한 것과 동일한 순서 오류로 인해 주석이 있는 페이지에서 PDF/UA 탭 순서 키도 누락되었습니다. 이 단계 역시 쓰기 작업의 잘못된 위치에 있었기 때문입니다. veraPDF 및 PAC는 /Tabs /S 누락을 Matterhorn 프로토콜 체크포인트 21-001 위반으로 보고합니다. 따라서 잘못 배치된 단 하나의 호출이 파일 크기만 늘린 것이 아니라, 오류 경고 없이 접근성 규정 준수 요구사항까지 동시에 깨뜨린 것입니다. 이것이 마무리 단계의 위험성입니다. 마무리 단계들은 전제 조건을 공유하며, 단 한 번의 순서 실수로 여러 단계가 한꺼번에 실패하더라도 모든 호출은 여전히 성공으로 반환됩니다.

경고 없는 내보내기 실패를 감지하는 방법

예외를 발생시키지 않는 버그는 단순히 프로그램을 실행하는 것만으로는 잡을 수 없습니다. 출력물을 검사하고 이를 입력물이 생성했어야 하는 형태와 비교하여 감지해야 합니다. 폰트 서브세팅의 경우 검사 방법이 구체적입니다. 출력 파일 크기를 예상치와 비교해 보십시오. 소수의 글리프만 사용한 문서의 크기가 전체 서체 파일 크기와 같아서는 안 됩니다. 임베디드 폰트 개체를 열고 바이트 길이를 확인하십시오. 라틴 서체용으로 서브셋된 /FontFile2는 소스 파일의 극히 일부에 불과해야 합니다. /BaseFont 이름을 읽고 6자리 접두사가 있는지 확인하십시오. 접두사가 없다는 것은 서브셋이 적용되지 않았다는 직접적인 신호입니다.

var
  Pdf: THotPDF;
  Output: TMemoryStream;
begin
  Output := TMemoryStream.Create;
  try
    Pdf := THotPDF.Create(nil);
    try
      Pdf.RegisterUnicodeTTF('C:\Fonts\DejaVuSans.ttf');
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('DejaVu Sans', [], 11);
      Pdf.CurrentPage.TextOut(72, 760, 0, 'Subset me');
      Pdf.EndDoc;
      Pdf.SaveToStream(Output);
    finally
      Pdf.Free;
    end;
    // A few glyphs from a ~700 KB face must not yield a multi-hundred-KB stream.
    if Output.Size > 100 * 1024 then
      raise Exception.Create('Font subset did not shrink the output');
  finally
    Output.Free;
  end;
end;

PDF/A 출력의 경우 검증기(validator)가 대신 작업을 수행하므로 검사가 훨씬 명확합니다. 적합성 수준을 설정하고 결과를 veraPDF를 통해 실행하십시오. 누락된 /CIDSet 또는 디스크립터와 일치하지 않는 서브셋은 직접 눈으로 확인하지 않아도 실패한 조항으로 보고됩니다. 이 마무리 작업을 주도하는 적합성 스위치는 문서의 속성입니다. PDFACompliance는 PDF/A-2 Level B를 나타내는 '2B'와 같은 문자열을 수용하며, PDFUACompliance는 태그가 지정된 PDF 및 탭 순서 요구사항을 활성화하는 부울 값입니다.

Pdf := THotPDF.Create(nil);
try
  Pdf.PDFACompliance := '2B';
  Pdf.PDFUACompliance := True;
  Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansSC-Regular.ttf');
  Pdf.BeginDoc;
  Pdf.CurrentPage.SetFont('Noto Sans SC', [], 12);
  Pdf.CurrentPage.TextOut(72, 760, 0, '合规报告');
  Pdf.EndDoc;
  Pdf.SaveToFile('Report_PDFA.pdf');
finally
  Pdf.Free;
end;

엔지니어링이 주는 교훈

여기서 두 가지 규칙을 도출할 수 있습니다. 첫째, 개체를 수정하는 모든 마무리 단계는 해당 개체가 직렬화되기 전에 실행되어야 하며, 문서 엔진의 종료 단계는 직렬화가 여러 작업 중 하나가 아니라 마지막 작업인 정렬된 파이프라인으로 해석되어야 합니다. 둘째, 내보내기 단계에서 오류가 없다는 것이 곧 성공의 증거는 아니라는 점입니다. 올바른 서브셋을 빌드하고 이를 이미 작성되어 잘못된 그래프에 적용하는 루틴은 자체적인 관점에서는 문제가 없기 때문에 오류를 보고하지 않습니다. 검증은 반환 코드가 아니라 결과물을 검사해야 합니다. 출력 크기를 확인하고, 임베디드 폰트의 바이트 길이와 /BaseFont 접두사를 읽어보며, 누락된 /CIDSet으로 인해 경고 없는 실패를 공식적인 실패 명세로 전환하는 veraPDF를 활용해 PDF/A 출력을 평가하십시오.

폰트 처리의 생성자 측면, 즉 보고서 출력을 위해 서체를 등록하고 임베드하는 방법은 보고서 출력의 폰트 및 이미지에 대한 아티클에서 다룹니다. 이러한 마무리 단계들을 표준과 비교하여 검증하는 방법은 PDF/A 및 PDF/UA 검증에 대한 가이드에서 다룹니다. 두 과정 모두 본 블로그의 다른 곳에서 다루는 로드, 편집, 암호화 및 서명 API와 함께 Delphi 및 C++Builder용 HotPDF Component의 일부로 제공되는 서브세팅 및 적합성 작업과 밀접하게 연관되어 있습니다.