HotPDF THotPDF.CompressDocument는 BeginDoc이 컴포넌트가 쓸 수 있는 가장 작은 무손실 PDF를 만들게 하는 단일 스위치입니다. 최대 레벨의 FlateDecode, object stream을 갖춘 cross-reference stream, 폰트 서브셋팅, 그리고 명시적인 /CIDToGIDMap 뒤에서 남은 글리프를 재번호 매기는 컴팩트 폰트 서브셋이 그 안건입니다. EndDoc은 그다음 여러분의 설정을 되돌려 놓습니다. Arial과 SimSun을 쓴 세 페이지 테스트 문서는 렌더링이 동일한 채 10.2 MB에서 20 KB로 떨어졌습니다
CompressDocument가 실제로 켜는 것은?
CompressDocument는 한 문서 동안 여섯 개의 기록자 설정과 object-stream 상한을 오버라이드하고 나중에 전부 되돌립니다. BeginDoc에서, PDF 버전이 확정되기 전에, HotPDF는 여러분의 값을 기록해 둔 뒤 Compression을 cmFlateDecode로, CompressionLevel을 clMaximum으로 세팅하고, EnableFontSubsetting과 CompactFontSubsetting을 켜며, UseXRefStream과 UseObjectStreams(ISO 32000-1 §7.5.7, §7.5.8)를 활성화합니다. object stream은 PDF 1.5가 필요하므로, 잠기지 않았다면 낮은 Version은 1.5로 올라갑니다. PDF/A-1은 두 구조를 모두 금지하므로, PDF/A-1 문서는 클래식 cross-reference 테이블을 유지하며 Flate와 폰트 작업만 받습니다. 이미지는 임베드한 그대로 놓아 둡니다
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.AutoLaunch := False;
Pdf.FileName := 'invoice-2026-1042.pdf';
Pdf.CompressDocument := True; // BeginDoc이 적용, EndDoc이 되돌림
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, 'Invoice 2026-1042');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
되돌리기는 EndDoc의 최외곽 finally에서 일어나므로, 리포트 한가운데의 예외가 오래 사는 컴포넌트를 다음 작업을 위해 최대 압축에 꽂아 두지 않습니다. CompressDocument 속성 자체는 True로 남습니다. 빌린 여섯 설정만 돌아갑니다. 버전은 더 조심스럽게 다뤄집니다. HotPDF는 문서가 여전히 1.5로 끝날 때만 자기가 올린 1.5를 되돌리므로, 실행 중 다른 기능이 파일을 1.6으로 밀었다면(예컨대 임베드된 OpenType 폰트), 압축이 없었을 때처럼 높은 버전이 남습니다
압축이 없으면 폰트 서브셋은 여전히 큰 이유는?
클래식 TrueType 서브셋은 그리지 않는 아웃라인은 버리지만 모든 glyph ID는 원래 자리에 두는데, 그 번호 매기기가 무거운 이유입니다. 콘텐츠 스트림은 원래 GID와 같은 CID를 보여 주므로, 서브셋은 유지하는 가장 높은 글리프까지 모든 슬롯에, 비었더라도, loca 오프셋과 hmtx 엔트리를 두어야 합니다. 라틴 폰트면 그 오버헤드는 잡음 수준입니다. 하지만 표의문자가 아주 큰 glyph 테이블 깊은 곳에 자리 잡은 SimSun 같은 CJK 폰트에서는, 한자 두 글자가 폰트 전체 크기로 잡힌 테이블을 끌고 다닙니다. 쉐이핑된 글리프의 폰트 서브셋 클로저 규칙이 어떤 글리프가 살아남는지 정하고, 압축은 살아남은 것들이 얼마나 드는지의 문제입니다
CompactFontSubsetting은 남은 글리프를 0부터 시작하는 빽빽한 범위로 재번호 매기고, CIDFont에 /CIDToGIDMap 스트림을 씁니다. ISO 32000-1 §9.7.4.2는 이것을 CID로 인덱스하는 2바이트 GID 테이블로 정의합니다. 그 테이블이 트릭의 전부입니다. 콘텐츠 스트림, /W 너비 배열, ToUnicode CMap은 모두 원래 CID를 유지하므로 이미 쓰인 것은 하나도 바꿀 필요가 없고, CID에서 글리프로의 조회만 맵으로 옮겨 갑니다. 이 기능을 낳은 테스트에서, 두 글자의 SimSun은 24.8 KB의 폰트 데이터에서 3.1 KB로 줄었습니다
압축에는 단단한 한계가 있고, 실패하는 대신 조용히 저하합니다. HotPDF는 Type 0 TrueType 폰트에만 컴팩트 서브셋을 만듭니다. 서브셋팅을 켠 채 SetFont로 세팅한 폰트와 RegisterUnicodeTTF로 등록한 폰트, 그 둘 모두입니다. 단순 TrueType 폰트는 폰트 프로그램 안의 cmap으로 글리프를 찾는데 재번호 매기기가 그것을 깨뜨리므로, 성긴 서브셋을 유지합니다. OpenType-CFF 폰트에도 컴팩트 경로는 없습니다. 실패한 컴팩트 빌드는 raise하는 대신 성긴 서브셋으로 폴백합니다. 속성은 디폴트로 꺼져 있어 기존 출력은 바이트까지 동일하게 유지되지만, PDF/A 아래에서는 등록된 Unicode 폰트가 항상 컴팩트 서브셋을 받습니다
Pdf.EnableFontSubsetting := True;
Pdf.CompactFontSubsetting := True; // CompressDocument 없이도 사용 가능
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('SimSun', [], 12);
Pdf.CurrentPage.TextOut(40, 40, 0, WideString('Total: '#$4E2D#$6587));
Pdf.EndDoc;
패킹된 기록자는 파일 구조를 어떻게 짜내나?
폰트와 스트림이 작아지고 나면 딕셔너리와 cross-reference 데이터가 남은 비용 중 가장 커지므로, CompressDocument 뒤의 object-stream 기록자는 그것들도 다듬습니다. object stream과 증분 업데이트 가이드가 컨테이너 포맷 자체를 다루며, 압축 경로는 그 위에 네 가지 다듬기를 얹습니다:
- ISO 32000-1 §7.2.2에 따른 컴팩트 문법: 공백은 두 토큰이 일반 문자로 붙어 읽힐 경우에만 씁니다. 그래서
/Type /Page는/Type/Page가 됩니다 - cross-reference 스트림 필드는 §7.5.8.2가 허용하는 어떤 폭이든 취하므로, 16 MB 미만의 파일은 각 오프셋을 4바이트 대신 3바이트에 저장합니다
- 흔한 100 대신 최대 250개의 오브젝트가 각 object stream에 들어갑니다.
ConfigureAdaptiveObjectStreamPacking으로 자기 상한을 정하지 않았다면요 - 파일이 암호화되지 않았다면 Catalog와 Info 딕셔너리도 object stream으로 패킹됩니다. 암호화된 출력은 그것들을 최상위에 둡니다
기록자를 확장할 사람이라면 알아 둘 만한 함정이 컴팩트 문법에 함께 왔습니다. 서명은 파일이 쓰인 뒤 literal 플레이스홀더 /ByteRange (와 /Contents <를 바이트에서 검색해 채워 넣는데, 컴팩트 철자는 그것들을 /ByteRange(와 /Contents<로 바꿔 버려서 검색이 절대 찾지 못합니다. 그래서 서명 딕셔너리(Type Sig 또는 DocTimeStamp, FT Sig)와 암호화 딕셔너리는 공백 있는 배치를 유지합니다. 관련 결함은 v2.766.41 이전 빌드에 영향을 주었습니다. object-stream 저장은 CompressDocument 포함 전부, 두 줄의 %PDF- 헤더로 시작했으니, 엄격한 검증기가 출력에 표시를 하면 업그레이드하세요
이미 불러온 PDF도 압축할 수 있을까?
예, 옵션 오버로드 CompressLoadedDocument(Options, Info)로 가능합니다. 기존 파일에 같은 무손실 단계를 돌립니다. THPDFLoadedDocumentCompressionOptions.Default에서는 안 쓰는 페이지 리소스를 제거하고, 같은 폰트와 폼을 병합하며, 컴팩트 서브셋을 켠 채 임베드된 폰트를 서브셋팅하고, 결과가 더 작을 때만 필터 없음, Flate, LZW, ASCII, RunLength 스트림을 Flate로 재압축하며, 다음 저장이 object stream을 쓰게 만듭니다. HighRatioFlate는 디폴트로 꺼져 있고, object stream은 PDF/A-1과 증분 저장에서는 건너뜁니다. 파라미터 없는 CompressLoadedDocument 오버로드는 압축 안 된 스트림을 Flate로 압축하는 것만 하는 더 오래되고 좁은 호출입니다
var
Doc: THotPDF;
Options: THPDFLoadedDocumentCompressionOptions;
Info: THPDFLoadedDocumentCompressionInfo;
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
Doc.LoadFromFile('quarterly-report.pdf');
Options := THPDFLoadedDocumentCompressionOptions.Default;
Doc.CompressLoadedDocument(Options, Info);
if Info.RefusedBySignaturePolicy then
Writeln(Format('Left untouched: %d signature fields', [Info.SignatureCount]))
else
begin
Writeln(Format('Compact fonts: %d, stream bytes saved: %d',
[Info.Fonts.CompactSubsetFontCount, Info.BytesSaved]));
Doc.SaveLoadedDocument('quarterly-report-compact.pdf');
end;
finally
Doc.Free;
end;
end;
불러온 경로에서는 두 경계가 중요합니다. 모든 단계가 서명이 덮는 바이트를 다시 쓰므로, 서명 필드가 있는 문서는 통째로 거부됩니다. 호출은 0을 반환하고 RefusedBySignaturePolicy를 세팅하며 아무것도 바꾸지 않습니다. AllowSignatureInvalidation을 세팅하지 않는 한이고, 세팅하면 Info.SignaturesInvalidated가 무엇을 포기했는지 알려 줍니다. 압축은 생성 경로보다 여기서 더 보수적입니다. HotPDF는 Identity /CIDToGIDMap을 가진 CIDFontType2 폰트만이 쓰는 폰트 프로그램, 그러니까 CID가 GID와 같은 경우만 압축하고, 이미 있는 맵 스트림이나 /CIDSet, COLR, sbix, CBDT, SVG 같은 컬러 글리프 테이블을 가진 프로그램은 건너뜁니다. 컴팩트 재빌드가 컬러 레이어를 버리기 때문입니다. 또한 Info.BytesSaved는 리소스, 폰트, 스트림 단계만 합산합니다. object-stream 이득은 파일이 쓰일 때 나타납니다
실전에서 기대할 결과는?
이득은 페이지 수가 아니라 파일의 얼마가 압축 안 된 구조와 과대한 폰트 데이터인지를 따라갑니다. Arial과 SimSun의 세 페이지 샘플은 CompressDocument로 생성했을 때 10.2 MB에서 20 KB로, 압축 안 된 원본을 불러 CompressLoadedDocument로 돌렸을 때 10.2 MB에서 19.8 KB로 줄었고, 양쪽 모두 렌더링은 동일했습니다. 이미 컴팩트한 PDF는 거의 움직이지 않습니다. 회귀 세트에서 그런 파일들은 원래 크기의 -0.07%에서 +0.06% 사이로 저장되었습니다. 사진 위주의 파일은 이득이 적습니다. 어느 경로도 이미지 데이터는 건드리지 않으니까요
매일 밤 같은 CJK 리포트를 생성한다면, 컴팩트 서브셋을 디스크의 영속 폰트 서브셋 캐시와 짝지어 런마다 서브셋팅 작업을 반복하지 않게 하고, 압축된 출력의 diff는 바이트가 아니라 오브젝트 콘텐츠로 비교하세요. 바뀐 필드 하나가 object stream 전체를 다시 Flate하니까요. 전체 속성과 레코드 레퍼런스는 HotPDF Delphi PDF component 제품 페이지에 있습니다