트랜잭션 인쇄소가 8만 페이지 명세서 작업을 한 줄로 반려합니다. "PDF/VT 아님, RIP cannot cache." 파일은 책상 위의 어떤 뷰어에서도 잘 열리고, 색도 맞고, 데이터 병합도 올바릅니다. 하지만 디지털 프레스가 요구한 것은 그런 것이 아닙니다. 고속 가변 데이터 인쇄는 프레스가 1페이지의 고객 로고 블록과 4만 페이지의 고객 로고 블록이 바이트 단위로 완전히 같은 object 인지를 알아보고, 한 번만 렌더링한 뒤 재사용할 수 있느냐에 달려 있습니다. PDF/VT 는 그 약속을 기계적으로 검증 가능하게 만드는 표준이고, 화면에서 멀쩡해 보인다는 점이 오히려 함정입니다. RIP 가 읽는 구조는 화면에 보이지 않기 때문입니다
PDFiumPas 는 그 구조를 TPdf 위의 작은 API 면으로 노출합니다. SaveAsPdfVT 는 이를 기록하고, ValidatePdfVT 는 이를 검사합니다. 이 글은 두 메서드가 실제로 디스크에 무엇을 쓰고 무엇을 들여다보는지, ISO 16612-2 가 처음 보이는 것보다 어디에서 더 엄격한지, 그리고 어떤 부분이 완전한 preflight 가 아니라 정직한 구조 앵커인지에 대한 이야기입니다
PDF/VT 가 표준화하는 것과, 왜 PDF/X 가 먼저인지
PDF/VT(ISO 16612-2:2010)는 새로운 파일 형식이 아닙니다. 이것은 PDF/X 파일 위에 최적화 metadata 레이어를 얹은 것이며, 이 순서 자체가 핵심입니다. 표준은 세 가지 conformance level 을 정의하지만, 그중 실제 PDF 파일을 가리키는 것은 두 가지뿐입니다. 하나는 단일 독립 문서인 PDF/VT-1, 다른 하나는 페이지가 외부 공유 리소스를 참조하는 file-set 모델인 PDF/VT-2 입니다. 종종 보게 되는 세 번째 토큰 PDF/VT-2s 는 파일 수준 값이 아니라 Annex A 에 설명된 MIME stream header 에 들어갑니다. 어떤 코드가 문서 XMP 에 GTS_PDFVTVersion = "PDF/VT-2s" 를 찍고 있다면 그 코드는 잘못된 것입니다
단일 파일에서 절대 양보할 수 없는 규칙은 PDF/X 기반입니다. ISO 16612-2 §6.2.1 은 모든 PDF/VT-1 파일이 동시에 유효한 PDF/X-4 파일이어야 한다고 요구합니다. §6.2.2 에 따르면 PDF/VT-2 file set 은 PDF/X-4p, PDF/X-5g, PDF/X-5pg 중 하나를 기반으로 해야 합니다. 그래서 PDF/VT writer 는 식별자 key 몇 개만 붙여서는 안 됩니다. OutputIntent, embedded ICC destination profile, 일치하는 XMP 와 document Info entry, trailer /ID, 그리고 암호화 금지까지, 전체 PDF/X-4 marker 세트를 함께 실어야 합니다. 이 중 하나라도 빠지면 PDF/VT 를 주장하면서도 conforming consumer 가 base 를 확인하는 순간 실패하는 파일이 됩니다. PDFiumPas 는 PDF/VT 저장 과정에 PDF/X-4 레이어를 포함하므로, 먼저 별도의 SaveAsPdfX 를 호출할 필요가 없습니다. injector 가 한 번에 두 레이어를 모두 씁니다
SaveAsPdfVT 로 파일 쓰기
최소 호출은 활성 문서만 있으면 됩니다. TPdfVTSaveOptions.Default 가 내장 sRGB ICC profile 과 conformance pvc1 을 제공하기 때문입니다. 저장은 내부적으로 세 단계를 거칩니다. 먼저 보안을 제거합니다. 암호화된 object stream 에 plaintext marker 를 주입하면 문서가 손상되기 때문입니다. 다음으로 문서의 기존 Info dictionary 와 trailer /ID 를 marker 세트에 연결해 XMP 와 Info 값이 서로 일치하도록 맞춥니다. 마지막으로 PDF/X-4 및 PDF/VT object 를 incremental update 로 뒤에 덧붙입니다
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
if Pdf.LoadFromFile('statements-merged.pdf') then
begin
// Default options: built-in sRGB OutputIntent, PDF/VT-1, synthesised DPart
if Pdf.SaveAsPdfVT('statements-pdfvt.pdf') then
Writeln('PDF/VT-1 written')
else
Writeln('Save failed (document not active?)');
end;
finally
Pdf.Free;
end;
end;
실제 운영 출력에서는 거의 항상 일반 sRGB fallback 이 아니라 인쇄 장비의 characterization 으로 OutputIntent 를 덮어쓰고 싶을 것입니다. ICC 바이트와 condition identifier 는 TPdfVTSaveOptions 로 전달합니다
var
Pdf: TPdf;
Opt: TPdfVTSaveOptions;
Icc: TBytes;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('directmail-merged.pdf');
Icc := LoadIccProfile('GRACoL2013_CRPC6.icc'); // your own loader
Opt := TPdfVTSaveOptions.Default;
Opt.Conformance := pvc1; // pvc2 is normalised to pvc1 on write
Opt.IccProfileData := Icc;
Opt.OutputConditionIdentifier := 'CGATS21_CRPC6';
Opt.OutputCondition := 'Commercial print, coated, CRPC6';
Opt.RegistryName := 'http://www.color.org';
Opt.Title := 'Spring 2026 Direct Mail Run';
Opt.Trapped := ptvFalse; // PDF/X Info /Trapped state
Pdf.SaveAsPdfVT('directmail-pdfvt.pdf', Opt);
finally
Pdf.Free;
end;
end;
여기서 눈여겨볼 점 하나는 기능 제한이 아니라 의도적인 가드레일입니다. Opt.Conformance := pvc2 로 설정해도 PDF/VT-2 파일이 생성되지는 않습니다. writer 는 pvc1 이 아닌 요청을 모두 다시 pvc1 로 정규화합니다. PDF/VT-2 는 file-set 형식이고, 단일 출력 문서 하나만 덧붙이는 writer 로는 §6.2.2 가 요구하는 외부 리소스 집합을 물리적으로 조립할 수 없기 때문입니다. pvc2 값이 존재하는 이유는 읽기 경로 때문이며, ValidatePdfVT 가 기존 file-set 문서를 인식하고 보고할 수 있게 하기 위함입니다. 쓰기 대상이 아닙니다
DPart 트리: RIP 가 실제로 읽는 구조
PDF/VT 의 핵심은 Document Part, 즉 DPart 계층입니다. 이것이 있어야 프레스가 긴 작업을 record 단위로 나누고, recipient 나 우편 묶음으로 그룹화하며, downstream 장비가 각 조각을 라우팅하고 과금할 수 있도록 Document Part Metadata 를 붙일 수 있습니다. ISO 16612-2 §6.5 는 연결 구조를 규정합니다. catalog 는 /DPartRoot 를 가지며, root DPart node 는 /DPartRootNode 와 각 계층 수준을 명명하는 /NodeNameList 를 가집니다. leaf DPart 는 page tree 의 범위를 덮고, part 에 속한 각 page 는 page-level /DPart entry 로 해당 leaf 를 다시 가리킵니다
원본 문서에 이미 쓸 만한 계층이 있으면 SaveAsPdfVT 는 그것을 보존합니다. 없으면 writer 가 최소 형태를 합성합니다. 현재 page tree 전체를 순서대로 덮는 document-level DPart 하나를 만들고, 각 live page object 에 /DPart 역참조를 덧붙이며, 한 단계짜리 /NodeNameList [/Document] 를 씁니다. 이 최소 트리가 무엇인지에 대해서는 스스로 정직해야 합니다. 이것은 §6.5 의 형태 요구를 만족하는 구조 앵커이지 business metadata 가 아닙니다. 수신자, 우편물 경계, 제품 배치 정보를 만들어낼 수는 없습니다. 그런 정보는 원본에 없었기 때문입니다. 수신자별 데이터가 있다면 더 깊은 DPart 트리를 직접 만들고, 생성하는 계층 수준에 맞게 /NodeNameList 를 확장해야 합니다
키 존재 여부를 넘는 검증
ValidatePdfVT 는 세 가지를 담은 TPdfVTValidationResult record 를 반환합니다. 감지된 Conformance, Issues 집합, 그리고 conformance 가 실제 레벨이고 issue 집합이 비어 있을 때만 참이 되는 IsCompliant helper 입니다. issue enumeration 은 의도적으로 구체적이라서, 실패 시 단순히 "invalid" 라고만 하지 않고 어떤 조항을 놓쳤는지 알려줍니다
var
Pdf: TPdf;
Res: TPdfVTValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('statements-pdfvt.pdf');
Res := Pdf.ValidatePdfVT;
if Res.IsCompliant then
Writeln('PDF/VT compliant: ', VTLevelName(Res.Conformance))
else
begin
if pvviMissingDPartRoot in Res.Issues then
Writeln('DPart hierarchy missing or unusable');
if pvviMissingPdfXIdentifier in Res.Issues then
Writeln('PDF/X-4 base identifier absent');
if pvviMissingOutputIntent in Res.Issues then
Writeln('OutputIntent / ICC profile missing');
if pvviEncryptionPresent in Res.Issues then
Writeln('Encrypted - PDF/X forbids this');
end;
finally
Pdf.Free;
end;
end;
깊이 이해할 가치가 있는 검사는 두 가지입니다. 하나는 conformance 짝맞춤이고, 다른 하나는 DPart 순회입니다. 둘 다 예전에는 너무 느슨했고, 이후 사양에 맞게 조여졌습니다. 짝맞춤 쪽에서 validator 는 정확 일치를 요구합니다. "아무 PDF/X 나 되면 된다"가 아닙니다. PDF/VT-1 파일은 PDF/X-4 기반에서만 허용되고, PDF/VT-2 파일은 PDF/X-4p, PDF/X-5g, PDF/X-5pg 기반에서만 허용됩니다. PDF/X-1a 기반 위에 올라간 PDF/VT-1 marker 는 넘어가지 않고 보고됩니다
DPart 순회에는 가장 많은 엄격함이 들어 있습니다. catalog 에 /DPartRoot key 가 있는 것만으로는 충분하지 않습니다. 빈 object 를 위조했거나 page link 가 전혀 없다면 consumer 는 여전히 그것을 사용할 수 없기 때문입니다. HasValidDPartHierarchy 와 재귀 함수 ValidateDPartNode 는 전체 구조를 따라갑니다. parent link 를 확인하고, duplicate child 와 cycle 을 거부하며, /Start 와 /DParts 가 서로 배타적이어야 함을 강제하고, leaf page range 가 depth-first order 로 page tree 를 덮는지, 그리고 각 page 의 /DPart 가 자신을 포함하는 leaf 를 가리키는지까지 확인합니다. 이런 내부 결함은 public enum 을 늘리지 않고 모두 단일 issue bit 인 pvviMissingDPartRoot 로 접힙니다. 그러니 이 플래그는 문자 그대로 "root key 가 없다"가 아니라 "DPart 계층을 사용할 수 없다"로 읽어야 합니다
validator 가 이제 강제하는 세 가지 문법 함정
§6.5 Table 4 를 여러 차례 대조하는 과정에서, 예전 버전은 통과시켰지만 표준은 허용하지 않는 형태들이 드러났습니다. 직접 DPart 트리를 구성하면 쉽게 틀리는 부분이므로 분명히 짚어둘 가치가 있습니다
/DParts는 평면 배열이 아니라 배열들의 배열입니다. 바깥 배열의 각 원소는 다시 indirect-reference array 여야 합니다./DParts [9 0 R]같은 평평한 형태는 거부되고, 올바른 형태는/DParts [[9 0 R] [10 0 R]]입니다. 이렇게 해야 비계층 구조가 유효한 레벨인 척 가장하지 못합니다/End는 실제 다중 페이지 범위를 표시할 때만 사용됩니다. leaf DPart 는/End를 가질 수 있지만 그것은/Start가 함께 있을 때뿐이며,/End는 page-tree order 상/Start보다 뒤에 있어야 합니다. 퇴화된/Start 3 0 R /End 3 0 R형태는 이제 1페이지 part 로 읽히지 않고 계층 전체를 unusable 로 만듭니다/NodeNameList이름은 PDF name unescaping 후에도 XML NMTOKEN 이어야 합니다. 예를 들어/Bad#20Name은 공백이 들어간 이름으로 확장되며, 이는 유효한 token 이 아닙니다. 구현은 가벼운 ASCII 검사로 문자, 숫자,.,-,_,:, 그리고 비 ASCII 바이트만 허용해 공백과 delimiter 실수를 잡아내면서도 정상적인 localized 또는 vendor-specific 이름은 막지 않습니다
XMP marker: 같은 속성을 쓰는 두 가지 방법
PDF/VT 식별 정보는 XMP 의 pdfvtid namespace 아래, 특히 GTS_PDFVTVersion 과 GTS_PDFVTModDate 에 들어가며, 표준 xmp:CreateDate 와 xmp:ModifyDate 와 함께 위치합니다. 여기서 순진한 reader 가 자주 만드는 오탐이 하나 있습니다. 이 값들은 element text 로도 직렬화될 수 있고, description element 의 RDF attribute 로도 직렬화될 수 있습니다. 예를 들어 <pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion> 같은 형태와 attribute 형태가 모두 가능합니다. PDFiumPas 는 두 형태를 모두 읽으므로, 다른 도구가 attribute 스타일로 쓴 파일이라고 해서 불이익을 주지 않습니다. 또한 §6.3 의 일관성 규칙도 강제합니다. GTS_PDFVTModDate 는 xmp:ModifyDate 와 같아야 하며, 다르면 pvviModDateMismatch 를 올립니다
같은 조항에서 한 가지 더 있습니다. 알 수 없는 GTS_PDFVTVersion 값은 pvcUnknown 으로 보존되며 pvcNone 로 접히지 않습니다. 이 구분은 운영상 중요합니다. pvcNone 은 PDF/VT marker 가 전혀 없는 일반 PDF 를 뜻하고, pvcUnknown 은 validator 가 인식하지 못하는 버전을 누군가 찍어 넣었다는 뜻입니다. PDF/VT-2s 같은 경우도 여기에 포함됩니다. 둘을 하나로 합치면 잘못된 파일이 평범한 문서와 같은 바구니 안에 숨어 버립니다
보장이 끝나는 지점
가변 데이터 인쇄 적합성에는 실제 비용이 걸려 있으므로, 이 메서드들이 약속하는 경계를 분명히 하는 편이 좋습니다. DPart 와 짝맞춤 검사는 바이트 수준 구조 검증입니다. 최적화 skeleton, PDF/X-4 base marker, OutputIntent, XMP 가 존재하며 내부적으로 일관된지를 확인합니다. 이것은 content-level PDF/X-4 preflight 가 아닙니다. 모든 색이 선언된 output condition 안에 있는지, 모든 font 가 임베드되었는지, 금지된 transparency blending edge case 가 숨어 있지는 않은지까지 확인하지는 않습니다. 실제 인쇄 계약에 올릴 작업이라면 PDFiumPas 의 구조 검증에 전용 PDF/X preflight engine 과 시험 인쇄를 함께 붙이는 것이 맞습니다. 구조 레이어는 RIP caching 을 조용히 깨뜨리는 실패를 잡아냅니다. 완전한 검사의 절반이지 전부가 아닙니다
이 검사를 더 넓은 release gate 에 넣고 있다면, 같은 바이트 수준 스캔 접근이 라이브러리의 다른 표준 작업에도 쓰인다는 점도 참고할 만합니다. 파일이 preflight 에 도달하기 전에 object 및 cross-reference stream 을 검증 하는 기능이 있고, 문서를 처음부터 RIP 친화적으로 만드는 Form XObject 기반 재사용 페이지 스탬프 의 shared-object 규율도 같은 철학 위에 있습니다. 여기서 설명한 PDF/VT 및 PDF/X 저장과 검증 API 는 Delphi 와 C++Builder 용 PDFium VCL component 의 일부이며, 제품 페이지에서 전체 compliance reference 를 볼 수 있습니다