Delphi에서 PDF 파일 크기를 줄이기 위해 losLab PDF Library는 파일 크기를 늘리는 세 가지 주범을 해결하는 세 가지 API를 제공합니다. SubsetEmbeddedFonts는 내장된 모든 TrueType 글꼴 프로그램을 문서가 실제로 렌더링하는 문자 모양(glyph)으로만 압축하여 다시 작성하고, DownsampleImages는 대상 DPI를 초과하는 래스터 이미지를 재표본화하며, NormalizeLZWStreams는 기존의 레거시 LZWDecode 압축 방식을 FlateDecode로 교체합니다. 각각의 메서드는 변경된 객체의 수를 반환하므로, 반환값이 0이면 자동 오류가 아닌 아무 작업도 수행되지 않았음을 의미합니다
병합되거나 프로그래밍 방식으로 생성된 PDF 파일의 크기가 지나치게 커지는 데는 대개 세 가지 이유가 있습니다. 완전히 내장된 글꼴, 표시 해상도보다 지나치게 높게 샘플링된 이미지, 그리고 여전히 구식 LZW 필터로 압축된 스트림이 그것입니다. ISO 32000-1 §9.9 규격은 생성기가 완전한 글꼴 프로그램을 내장할 수 있도록 허용하며, 대부분의 생성기는 안전한 기본값으로 이 방식을 따릅니다. 완전한 Arial FontFile2의 크기는 수백 킬로바이트에 달합니다. 12개의 원본 파일에 이를 각각 내장하고 병합하면, 문서에 한 번도 사용되지 않은 문자들의 외곽선 정보를 담은 글꼴 복사본 12개를 고스란히 짊어지게 됩니다. 병합 자체가 낭비를 유발하는 것은 아니며, 단지 흩어져 있던 낭비 요소를 단일 파일로 모아 전체 크기를 표면화할 뿐입니다
두 번째 원인은 이미지입니다. 4분의 1 페이지 크기 프레임에 배치된 가로 4800픽셀 크기의 스캔 이미지는 일반적인 300 DPI 인쇄 작업 시 필요한 픽셀 데이터보다 약 40배나 많은 데이터를 포함하고 있습니다. 세 번째 원인은 겉으로 드러나지 않는 LZWDecode 필터 스트림입니다. ISO 32000-1 §7.4.4는 LZWDecode와 FlateDecode 압축을 모두 규정하며 Flate가 보통 동등 수준 이상의 압축률을 보인다고 명시합니다. 실제로 동일 데이터에 대해 Flate 방식의 압축 결과가 일관되게 더 작으며, LZW는 주로 1990년대 수준의 구형 도구를 거친 오래된 아카이브 파일들에서 주로 발견됩니다. 본 문서는 이러한 문제들을 해결하는 losLab PDF Library의 세 가지 처리 단계를 살펴보고, 이를 단일 파이프라인으로 결합하는 방법을 설명합니다
SubsetEmbeddedFonts를 통한 글꼴 서브셋 적용
SubsetEmbeddedFonts는 로드된 문서 내에 내장된 모든 TrueType 글꼴을 문서가 실제로 사용하는 문자로 축소하며, 콘텐츠 스트림 자체에서 보존할 문자 목록을 도출하므로 인수가 필요하지 않습니다. 내부적으로 이 과정은 GetTextRuns를 사용하여 각 페이지의 콘텐츠 스트림을 훑어가며 각 글꼴 리소스에서 참조된 문자 코드를 수집하고, 보존 목록을 작성한 후 원본 글꼴 프로그램을 Windows FontSub 엔진(CreateFontPackage)으로 전달하여 서브셋을 만듭니다. 새로 기록된 프로그램은 기존의 FontFile2 스트림을 바로 덮어쓰고, BaseFont 이름에는 ISO 32000-1 §9.6.4 규격에 따른 여섯 글자의 대문자와 더하기 기호 형식인 LOSABC+ 접두사가 자동으로 추가됩니다. 이 접두사 덕분에 이 함수 호출은 멱등성(idempotent)을 가지게 됩니다. 즉, 함수를 두 번 실행하더라도 이미 서브셋이 완료된 글꼴을 감지하여 건너뛰므로, 파일을 반복적으로 재방문하는 배치 처리 작업에 연결하여 실행해도 안전합니다
var
Lib: TPDFlib;
Fonts: Integer;
begin
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile('merged-report.pdf', '') = 1 then
begin
Fonts := Lib.SubsetEmbeddedFonts;
// Fonts = number of FontFile2 programs rewritten;
// 0 means nothing embedded, or everything already subsetted
Lib.SaveToFile('merged-report-subset.pdf');
end;
finally
Lib.Free;
end;
end;
이 API의 한계를 설명하는 두 가지 구현 세부 사항을 알아두면 좋습니다. 첫째, 이 처리는 FontFile2만을 대상으로 하므로 내장된 TrueType 프로그램만 커버합니다. 위험을 줄이기 위해 Type 1이나 순수 CFF 형식으로 내장된 글꼴은 처리하지 않고 그대로 둡니다. 둘째, 이 기능은 Windows 환경에서만 작동하는 FontSub 엔진에 의존하므로 SubsetEmbeddedFonts 역시 Windows 환경에서만 작동합니다. 구현 관점의 또 다른 미묘한 점은, 글꼴의 서브셋 대상 여부를 판단할 때 내장 플래그 헤리스틱(heuristic)을 신뢰하는 대신 실제 FontDescriptor → FontFile2 참조 체인을 해석하여 결정한다는 점입니다. 로드된 문서의 글꼴들은 플래그를 생성하는 작성자 측의 내부 정보를 가지고 있지 않기 때문입니다. 해석된 스트림이 존재하면 서브셋 후보로 분류되고, 그렇지 않으면 오류 없이 건너뜁니다
한 가지 트레이드오프가 존재합니다. 서브셋 처리된 글꼴은 처리 당시에 사용된 문자 모양만 포함합니다. 이후 사후 처리 도구나 직접 구현한 코드에서 해당 글꼴을 사용하여 텍스트를 추가할 때, 서브셋 범위를 벗어나는 문자는 외곽선 정보가 없어 렌더링 시 누락된 모양으로 나타납니다. 따라서 서브셋 적용은 콘텐츠를 편집하는 단계가 모두 완료된 최종 단계에서 실행해야 합니다. 나중에 글꼴을 추출하여 재사용하려는 경우에도 동일한 주의가 필요합니다. PDFlibPas를 통한 텍스트, 이미지 및 글꼴 추출 기사에서 추출된 서브셋 프로그램이 지원할 수 있는 범위와 불가능한 부분에 대해 확인해 볼 수 있습니다
DownsampleImages는 축소할 이미지를 어떻게 판별하나요?
DownsampleImages(MaxDPI, Quality, Filter)는 보수적인 DPI 계산 기준을 적용하여 해상도가 과도하게 높다고 확실히 보장할 수 있는 이미지만 재표본화합니다. PDF 이미지 XObject는 픽셀 크기 정보는 저장하지만 신뢰할 만한 물리적 해상도 정보가 누락되는 경우가 많으며, 원본 이미지의 DPI 태그 정보 역시 로드-편집-저장 단계를 거치면 손상되기 쉽습니다. 따라서 라이브러리는 SrcDPI = PixelWidth / 8.5 공식을 사용하여 '이 이미지가 Letter 규격 페이지의 전체 가로 너비를 가득 채운다면 실제 해상도가 어떻게 될 것인가?'를 평가합니다. 이 결과가 MaxDPI를 초과하는 이미지만이 대상이 됩니다. 이는 의도된 보수적 접근입니다. 페이지 내에 조그맣게 배치된 이미지는 실제 계산 결과보다 고해상도 상태를 유지하게 되므로, 물리 해상도를 측정할 수 없는 고품질 인쇄용 자산의 화질을 인위적으로 저하시키는 오작동을 차단합니다
Quality 매개변수(1~100)는 JPEG의 압축 품질을 지정하며, 0으로 설정하면 무손실 PNG 유형의 Flate 압축 상태를 그대로 유지합니다. Filter 매개변수는 재표본화 방식을 선택합니다(0은 박스 평균, 1은 이중 선형). 일반 사무용 스캔 문서의 경우 DownsampleImages(150, 75, 1)이 무난한 시작 기준점입니다. 고품질 인쇄가 예정된 인쇄물의 경우 MaxDPI를 300으로 상향하거나 본 처리 단계를 완전히 생략하는 것을 권장합니다. 다운샘플링은 세 가지 기법 중 유일하게 화질 손실을 수반하는 처리이므로, 사용자가 켜고 끌 수 있는 설정 항목으로 격리하여 제공해야 합니다
NormalizeLZWStreams를 통한 레거시 LZW 스트림 변환
NormalizeLZWStreams는 화질 손실 없이 모든 LZWDecode 스트림을 압축 해제하고 FlateDecode 압축 방식으로 인플레이스(in-place) 재가공한 뒤 변환된 스트림의 총 개수를 반환해 줍니다. 단일 /Filter /LZWDecode 항목 형태는 물론, 다중 필터 체인 배열 내에 섞여 있는 LZW 필터까지 포함하여 오직 LZW 변환 체인 링크만을 Flate로 매끄럽게 교체하며 나머지 체인들은 그대로 유지합니다. 예측자 매개변수(Predictor, Columns, Colors, BitsPerComponent)들은 스트림의 DecodeParms 정보에서 읽어와 디컴프레서에 안전하게 전달되므로, 복잡한 필터링 방식으로 예측 압축 인코딩된 이미지 원본 데이터도 올바르게 역변환을 지원합니다. 두 압축 방식 모두 무손실 방식이므로 변환 전후의 복원 데이터 바이트 구조는 완벽히 동일하며, 오직 압축 컨테이너 압축 효율성만 개선되므로, 별다른 제약 사항 없이 모든 문서 파일에 안전하게 적용해 보실 수 있습니다
LZW 스트림이 없는 문서에 대해 함수를 호출하면 단순히 0을 반환하며 아무런 수정도 가하지 않습니다. 이는 새로 생성된 Flate 전용 파일에서 변환 횟수가 0으로 기록됨을 보증하는 라이브러리의 테스트 규격과도 일치합니다. 이러한 무작동(no-op) 보증 사양은 2024년에 작성된 파일과 1998년에 작성된 파일이 마구 섞인 수천 개의 이기종 파일을 자동 파이프라인으로 일괄 처리할 때 매우 중요합니다
Delphi에서의 종합 파일 크기 최적화 파이프라인
세 가지 최적화 처리 단계는 단일 '로드-최적화-저장' 함수로 손쉽게 묶어 처리할 수 있습니다. 각 기법이 서로 중첩되지 않는 고유 개체 유형인 글꼴, 이미지 XObject, 스트림 필터들을 독립적으로 제어하기 때문에 실행 순서는 성능에 큰 영향을 미치지 않습니다. 다만 편집 순서의 제약이 존재하는 글꼴 서브셋 처리를 가장 먼저 실행하는 배치 순서가 가장 깔끔합니다
function OptimizePDF(const Src, Dst: string): Boolean;
var
Lib: TPDFlib;
Fonts, Images, Streams: Integer;
begin
Result := False;
Lib := TPDFlib.Create;
try
if Lib.LoadFromFile(Src, '') <> 1 then
Exit;
Fonts := Lib.SubsetEmbeddedFonts; // TrueType FontFile2 -> subset
Images := Lib.DownsampleImages(150, 75, 1); // >150 DPI -> JPEG q75, bilinear
Streams := Lib.NormalizeLZWStreams; // LZWDecode -> FlateDecode
Result := Lib.SaveToFile(Dst) = 1;
// Log Fonts/Images/Streams: three zeros mean the file was already lean
finally
Lib.Free;
end;
end;
라이브러리가 스스로를 검증하는 것과 동일한 방식으로 파이프라인의 복구 신뢰성을 점검하십시오. v3.130 리그레션 테스트 사양은 문서를 생성하고, 저장하고, 다시 로드하여 최적화를 실행한 뒤 다시 저장합니다. 이후 출력 크기가 감소했는지, 변환 횟수 리턴값이 설계된 스펙과 일치하는지, 압축 최적화된 파일이 로더에서 무사히 열려 페이지가 정상 렌더링되는지 3단 검증을 수행합니다. 직접 생성한 대량의 생산 파일을 샘플링하여 이러한 '생성-최적화-재로드' 검증 루프를 돌려보고 변환 전후의 텍스트가 정상 추출되는지 한 번 점검해 보는 작업은, 고객사에서 파손된 인보이스 파일이 열리지 않는다는 불만을 수신하기 전에 예외 충돌 통합 오류를 예방하는 가장 확실한 예방 투자입니다
// Round-trip check: the optimized file must still load cleanly
Lib := TPDFlib.Create;
try
Assert(Lib.LoadFromFile('merged-report-opt.pdf', '') = 1);
Assert(Lib.GetPageCount > 0);
finally
Lib.Free;
end;
최적화 파이프라인은 전체 병합(merge) 흐름상 어느 단계에 배치해야 할까요? 정답은 병합 완료 직후 단계입니다. 병합을 먼저 완료한 최종 병합본 파일에 최적화를 1회 수행해야, 개별 원본 문서에 산발적으로 내장되어 있던 폰트 자원들의 글꼴 세트를 종합해 1개의 파일로 온전히 압축할 수 있습니다. 병합 속도 성능이 병목 지점이라면, 객체 데이터 해석 단계를 건너뛰고 바이트 수치 계산식 좌표값 계산을 활용해 문서를 결합하는 바이트 참조 시프팅을 활용한 고속 PDF 병합 기법을 응용하십시오. 또한 메모리 용량을 초과하는 대용량 문서 병합의 경우, 직접 파일 액세스를 활용한 대용량 PDF 병합 및 분할 가이드에서 해결책을 확인하십시오. 두 가지 방식 모두 병합 작업 완료 후 진행하는 최적화 패스와 잘 맞물려 작동합니다
세 가지 최적화 처리가 해결해주지 못하는 영역
losLab PDF Library의 최적화 트리오는 문서의 논리적 의미 구조를 훼손하는 잠재적 모험 작업은 철저히 배제합니다. SubsetEmbeddedFonts는 결합된 서로 다른 문서 간에 중복으로 삽입되어 있는 동일 글꼴 세트를 하나로 합쳐주는 디듀플리케이션(중복 제거) 기능까지는 제공하지 않으며, 각 개별 글꼴 독립 상태에서 파일 크기 축소만을 지원합니다. 중복 제거는 구조적 위험성이 높은 별도의 복잡한 변환 작업이기 때문입니다. DownsampleImages는 사람이 보기에 크기가 다소 불필요하게 클지라도 계산된 보수적 해상도 기준치 이하라면 처리를 무조건 건너뜁니다. 또한 이 처리들은 문서의 뼈대인 객체 구조 자체를 건드리지 않으므로, 수많은 고아 객체(orphan object)로 인해 파일 크기가 커진 경우에는 이러한 스트림 수준의 처리보다는 전체 재작성 방식의 저장을 수행해야 합니다. 이러한 합리적 경계 내에서 글꼴 서브셋 적용, 이미지 해상도 조절 및 LZW-to-Flate 변환의 유기적인 조합은 문서 파일의 비대화를 방지하는 매우 신뢰도 높은 해결책을 선사할 것입니다. 이 세 가지 함수는 본 기사에 소개된 파일 결합, 추출 및 렌더링 API 사양들과 함께 Delphi, C# 및 VB.NET을 지원하는 losLab PDF Library의 기본 제공 사양으로 함께 배포됩니다