Google PDFium 엔진을 감싸는 Delphi/C++Builder 래퍼인 PDFiumPas는 TPdf.SaveAs 메서드의 PdfVersion 매개변수를 통해 문서를 1.3부터 1.7까지 정확한 PDF 버전으로 저장한다. PDFium 자체의 FPDF_SaveWithVersion 호출은 문서의 실제 콘텐츠가 그 버전에서 합법적인지 확인하지 않은 채 %PDF-M.m 헤더만 다시 쓴다. PDFiumPas는 활성 크로스 레퍼런스 리비전 체인을 순회하고 Adobe Extension Level 선언을 확인하는 저장 후 적합성 검사로 이 공백을 메운다
이 구분은 인쇄 제작 현장에서 가장 중요하다. PDF/X 프로필은 정확한 PDF 버전을 명시하고 프리플라이트 도구나 RIP는 자신의 헤더와 조용히 어긋나는 무엇이든 거부하는데, 이는 PDFiumPas로 인쇄용 PDF/X 문서 검증하기에서 출력 쪽 관점으로 다룬다. SaveAs는 대상 버전을 TPdfVersion 열거형으로 노출한다. pv13부터 pv17까지, 더 오래된 pv10부터 pv12까지의 값과 함께, 증분 저장이나 전체 재작성을 위한 독립적인 TSaveOption도 있다. PdfVersion을 넘기면 PDFiumPas는 한 번의 호출로 두 가지 작업을 한다: PDFium에 요청된 헤더를 찍도록 요청한 다음, 갓 작성된 바이트를 다시 읽어 그 버전에서 합법적으로 존재할 수 없는 활성 콘텐츠를 가진 파일을 돌려주기를 거부한다
var
Pdf: TPdf;
begin
Pdf:= TPdf.Create(nil);
try
Pdf.FileName:= 'source.pdf';
Pdf.Active:= True;
try
Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
except
on E: Exception do
// E.Message names the offending feature and the version or
// extension level it actually needs, for example:
// "RichMedia annotations and RichMediaExecute actions require
// /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
// or newer."
raise;
end;
finally
Pdf.Free;
end;
end;
파일 안의 마지막 객체 정의를 왜 신뢰하면 안 되는가?
PDF 파일 안에서 어떤 번호를 가진 마지막 물리적 객체가 오늘날 표준을 준수하는 리더가 그 번호에 대해 해석할 객체라는 보장은 없다. 여러 번의 증분 업데이트를 거친 PDF는 하나의 객체 그래프를 갖는 것이 아니라, 단일 파일 안에 층층이 쌓인 그 역사를 갖는다. 그리고 매 추가 사이클마다 객체를 해제하거나, 새 세대 번호 아래 다시 정의하거나, 더 이상 그것을 가리키는 크로스 레퍼런스 항목이 없는 채로 낡은 물리적 본문을 두 endobj 마커 사이에 그대로 남겨둘 수 있다
PDFiumPas는 xref 리비전을 명시적으로 추적하기 전에 정확히 이 실패 모드를 겪었다: 나중의 페이지 객체 재작성으로 고아가 된 Redact 주석, 또는 더 이상 아무 xref 항목도 가리키지 않는 채로 물리적으로 존재하는 /MarkInfo 딕셔너리는 바이트 스캔에서 여전히 발견될 수 있었고, 실제로 리더가 열게 될 문서에는 더 이상 적용되지 않는 버전 기능 검사를 여전히 촉발시킬 수 있었다. 실패의 방향은 오탐이 아니라 오기각이었다: 현재 리비전에서 진짜로 그 기능을 벗어난 파일이 이제 아무도 도달할 수 없는 콘텐츠 때문에 더 낮은 버전으로 저장하는 것이 차단당할 수 있었다
PDFiumPas는 어느 객체 정의가 실제로 활성 상태인지 어떻게 판단하는가?
PDFiumPas는 표준을 준수하는 리더와 같은 방식으로 활성 객체 집합을 해석한다. 즉 객체 헤더를 위해 바이트를 스캔하는 대신 크로스 레퍼런스 체인을 순회한다. 이 해석기는 파일 안의 마지막 startxref 오프셋에서 시작해 각 /Prev 링크를 거슬러 올라가며 더 오래된 리비전을 따라가고, 그 과정에서 고전적인 크로스 레퍼런스 테이블, 하이브리드 /XRefStm로 연결된 스트림, 순수 크로스 레퍼런스 스트림을 파싱한다. 이 순회는 최신에서 최고(最古) 순으로 실행되며 각 객체 번호를 처음 발견한 시점에 확정하므로, 나중 리비전의 해제 항목은 이전 리비전에 작성된 객체 본문을 올바르게 가리고, 새 오프셋이나 세대 아래의 재정의는 항상 그것이 대체하는 것을 이긴다
객체 스트림 멤버는 단순한 오프셋 조회만으로는 제공할 수 없는 추가 검사를 거치며, 이 메커니즘은 PDFiumPas로 객체 스트림과 크로스 레퍼런스 스트림 검증하기에서 더 깊이 다룬다. /ObjStm에서 복원된 압축된 객체는 같은 순회에서 부모 스트림이 활성 상태임이 확인되어야 하고, PDFiumPas가 그것을 살아있는 콘텐츠로 취급하기 전에 그 인덱스는 그 스트림의 헤더 안에서의 멤버 자신의 위치와 일치해야 한다. ISO 32000-1 7.5.8.4절은 고전적인 호환성 테이블이 객체를 해제된 것으로 표시하는 동시에 트레일러의 /XRefStm 항목이 같은 객체를 다른 곳의 압축된 멤버로 정의하는 하이브리드 참조 경우까지 서술한다; PDFiumPas는 고전적인 항목이 적용되기 전에 보충 xref 스트림을 같은 리비전에 병합하므로, 스펙이 의도하는 대로 압축된 정의가 이긴다
Adobe Extension Level: 버전 번호 위의 관문
%PDF-1.7 헤더는 ISO 32000-1이 2008년에 표준화한 기능 집합만 약속하는 반면, 오늘날 PDF 생성기가 의존하는 여러 기능은 그 이후 같은 버전 번호 위에 얹힌 Adobe 전용 보충 규격으로 출시되었다. Adobe는 각 보충 규격을 문서 카탈로그의 /Extensions 딕셔너리 안에 개발자 접두사(Adobe 자체 확장의 경우 ADBE) 아래 기록된 BaseVersion과 ExtensionLevel 쌍으로 등록해서, 리더가 순수한 PDF 1.7 파일과 번호가 매겨진 확장 레벨도 구현한 파일을 구분할 수 있게 했다. 그 선언 없이 pv17로 저장하는 것 자체는 오류가 아니다; 활성 콘텐츠가 그 선언이 커버해야 할 기능에 실제로 의존하는 순간에만 오류가 된다
어떤 고버전 기능이 명시적 버전 관문을 촉발하는가?
PDFiumPas는 버전 번호만으로 추측하는 대신 스펙에 근거한 구체적인 목록을 검사한다. 명시적인 /SMaskInData 항목이나 16의 /BitsPerComponent 값을 가진 이미지 딕셔너리는 둘 다 PDF 1.5를 요구하는데, 16비트 경우는 PDF Reference 1.5 4.8절의 이미지 성분 규칙을 직접 따른다. RichMedia 주석과 RichMediaExecute 액션은 /BaseVersion /1.7과 /ExtensionLevel 3 이상을 요구한다. /Type /3D와 /Subtype /PRC를 둘 다 가진 딕셔너리로 식별되는 PRC 3D 스트림은 같은 베이스 버전을 요구하지만 /ExtensionLevel 1만 필요로 한다. 지리공간 Measure 딕셔너리와 Projection 주석은 RichMedia가 의존하는 것과 같은 Adobe 보충 규격인 /BaseVersion /1.7과 /ExtensionLevel 3을 요구한다
지리공간 검사는 PDFiumPas 위에 자신만의 버전 관문 로직을 직접 만든다면 알아둘 가치가 있는 스펙 해석상의 세부사항을 담고 있다. ISO 32000-1 Table 254는 Measure 딕셔너리의 /Type 항목을 선택 사항으로 표시하며 "있다면 Measure여야 한다"고만 명시하는 반면, Table 311은 PRC 콘텐츠가 담기는 3D 스트림 딕셔너리에 대해서는 /Type을 필수로 만든다. 매핑 도구가 만들어내는 실제 GeoPDF 출력물은 Measure 딕셔너리에 /Type을 흔히 생략하고 /Subtype /GEO만 작성한다. 그래서 PDFiumPas의 지리공간 감지기는 PRC 3D 감지기가 안전하게 요구할 수 있는 두 키 모두를 요구하는 대신 /Subtype 하나만으로 매칭한다. 두 딕셔너리 모두에 /Type을 요구했다면, 표준을 준수하는 GeoPDF 콘텐츠가 관문을 감지되지 않은 채 빠져나가, 그것을 뒷받침할 확장 레벨 선언도 없이 평범한 PDF 1.7 파일 안에 놓이게 되었을 것이다
PDFiumPas는 지원되지 않는 기능을 자동으로 다운그레이드하는가?
일반적인 기능으로는 그렇지 않으며, 그렇다고 가정하는 것이 여기서 피해야 할 실수다. SaveAs는 대상 버전을 내부 루틴인 ValidatePdfVersionCompliance로 흘려보내며, 그 루틴이 대상 버전이나 그 확장 레벨 선언이 지원할 수 없는 기능을 발견하면, SaveAs는 파일을 쓰는 대신 그 루틴의 오류 텍스트를 담은 예외를 일으킨다; 호출자는 정확하고 기능 이름이 명시된 이유를 돌려받으며, 조용히 다시 작성된 문서를 받는 일은 절대 없다. PDFiumPas가 콘텐츠를 자동으로 다시 쓰는 유일한 곳은 PDF 1.3 대상인데, 여기서는 PDFium이 대상 버전과 무관하게 항상 ExtGState 딕셔너리에 써넣는 의미상 중립적인 /BM /Normal, /CA 1, /ca 1 투명도 기본값을 벗겨낸다. 이 특정 값들은 시각적 의미를 전혀 담지 않고 PDF 1.3은 그 키들이 생기기 전이기 때문이다
// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode
진짜 비기본값 투명도와 이미지 소프트 마스크는 여전히 PDF 1.3 대상에서 완전히 실패하는데, 이를 제거하면 페이지가 실제로 보이는 방식이 바뀌게 되고, PDFiumPas는 여러분을 대신해 그 판단을 내리지 않기 때문이다. 정확한 버전을 배치 파이프라인에 넣기 전에 계획해 둘 가치가 있는 관련된 두 가지 제한이 더 있다. 명시적 버전 출력은 절대 /Encrypt 딕셔너리를 담지 않는다; 소스가 보호되어 있으면 저장은 즉시 실패하는데, 이는 어차피 암호화를 금지하는 PDF/X와 PDF/A 프로필과 우연히 일치하지만, 복호화가 SaveAs가 대신해 주는 무언가가 아니라 워크플로 안의 별도 단계라는 뜻이기도 하다. PDFiumPas는 또한 카탈로그에 /Extensions /ADBE 선언을 쓰는 공개 메서드가 없으므로, RichMedia, PRC 3D, 또는 지리공간 콘텐츠를 담고 있지만 그 선언이 없는 소스 파일은 어떤 PdfVersion을 요청하든 관문을 통과하지 못한다; 그 선언은 이미 소스 안에 존재해야 하는데, 보통 작성 도구가 그것을 기록했기 때문이거나, 아니면 저장 전에 그 기능을 빼야 한다. 읽기 전용 TPdf.PdfVersion 속성은 정확한 버전 저장을 시도하기 전에 확인해 볼 가치가 있는데, 이는 저장 시점 검증기 자체가 의존하는 것과 같은 카탈로그 인식 실효 버전(헤더든 /Version 오버라이드든 현재 무엇이든)을 해석하기 때문이다
Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);
정확한 버전 대상에서의 SaveAs 예외를 버그가 아니라 프리플라이트 보고서로 취급하라: 그 메시지는 소스 문서가 위반하고 있는 정확한 조항을 명시하며, 이는 파일이 더 나아가기 전에 인쇄소나 아카이브 파이프라인이 정확히 필요로 하는 정보다. 여기서 설명한 명시적 버전 저장 경로, 활성 xref 리비전 해석기, Adobe Extension Level 검사는 Delphi와 C++Builder용 PDFiumPas 컴포넌트 표준판의 일부로 제공된다; 제품 페이지에는 나머지 적합성 및 폼 API와 함께 전체 TPdf.SaveAs 레퍼런스가 실려 있다