PDFlibPas는 PDF/VCR-1을 태그 구조 프로필로 구현합니다. 교체 정의는 StructTreeRoot 아래의 직접 자식 브랜치로 존재하며, 템플릿 루트가 /O /GTS_Template을 지녀 필드 집합을 선언하고 각 플레이스홀더가 /O /GTS_Replacement를 지녀 데이터 필드 하나를 묶습니다. 페이지 수준 딕셔너리는 전혀 아닙니다
바로 이 한 문장을 대부분의 구현이 틀리며, 틀리면 검증 실패보다 더 큰 비용이 듭니다. 교체 계획을 잘못 읽은 가변 데이터 작업은 크래시하지 않습니다. 그냥 대량으로, 잘못된 상자에 잘못된 필드를 넣어 인쇄되며, 가장 먼저 알아차리는 사람은 우편물 5만 통을 쥔 고객입니다
교체 정의가 실제로 사는 곳
PDF/VCR-1은 교체 모델을 개인 페이지 딕셔너리가 아니라 ISO 32000-1 §14.7이 설명하는 논리 구조 계층에 둡니다. 템플릿 루트는 StructTreeRoot에 직접 매달린 구조 요소로, attribute owner가 GTS_Template인 점으로 구분되며 GTS_Fields를 통해 전체 필드 어휘를 한곳에 선언합니다. 페이지 선택을 주도하는 필드를 이름 짓는 선택적 GTS_Pages도 함께 있습니다
플레이스홀더는 그 루트 안에 중첩된 구조 요소로, attribute owner가 GTS_Replacement입니다. 각각은 GTS_Generator(PDFlibPas는 PassThrough를 요구합니다), 선언된 필드 정확히 하나를 이름 짓는 GTS_Data 값, 그리고 선택적 GTS_BBox를 지닙니다. 페이지와의 바인딩도 페이지 주석이 아닙니다. 요소는 /Pg로 자기 페이지를, /K로 marked-content 시퀀스를 가리킵니다. 페이지 딕셔너리 대신 구조를 고르면 말하기는 쉽고 과소평가하기도 쉬운 결과가 따라옵니다. 구조 트리는 catalog에서 닿는 일반 그래프이므로, 프로필을 소비하는 무엇이든 사전 리더가 아니라 트리 워커여야 하며 그에 따른 모든 위험을 감수해야 합니다. 구조 트리를 다뤄 본 적이 없다면 태그된 PDF 구조와 접근성에서 메커니즘을 설명합니다
첫 최상위 브랜치만 확인하면 왜 나쁜 파일이 통과할까요?
StructTreeRoot는 직접 자식이 여러 개일 수 있는데, 템플릿 루트를 찾은 뒤 멈추는 검증기는 나머지를 전혀 보지 못하기 때문입니다. PDFlibPas가 피하도록 만들어진 바로 그 실패이며, 검사가 없는 것보다 더 나쁜 가짜 통과를 정확히 만들어냅니다. 적합하다고 보고하면서 두 번째 템플릿 루트를 담은 파일, 혹은 어떤 소비자도 존중하지 않을 템플릿 브랜치 밖에 놓인 플레이스홀더가 그것입니다
그래서 PDFlibPas의 ValidatePDFVCRStructure는 같은 트리를 두 번 순회합니다. 첫 패스는 StructTreeRoot의 모든 직접 자식을 열거하며 GTS_Template을 지닌 것을 셉니다. 정확히 하나가 아니면 플레이스홀더를 단 하나도 읽기 전에 거부합니다. 두 번째 패스는 템플릿 브랜치만이 아니라 모든 최상위 브랜치로 내려가 두 가지를 한 번에 거부합니다. 루트가 아닌데 GTS_Template을 지닌 중첩 요소와, 템플릿 브랜치 밖에서 발견된 GTS_Replacement 요소입니다
순회는 깊이 64레벨과 노드 100만 개로 제한되며, 방문하는 모든 요소의 객체 번호와 세대 번호를 기록해서 순환이나 반복 참조를 따라가지 않고 보고합니다. 이 숫자들은 성능 튜닝이 아닙니다. 구조 트리는 공격자가 통제하는 객체 그래프이며, 상한 없이 재귀하는 검증기는 확장자가 붙은 스택 오버플로입니다
템플릿 작성과 API가 굽히지 않는 제약
PDFlibPas는 저장 시점에 구조 규칙을 발견하는 대신 쓰기 시점에 강제하는 bracketing API로 작성 기능을 노출합니다. SetPDFVCRMode는 문서를 프로필 모드로 전환하며, 내부적으로 ISO 15930-7의 PDF/X-4 베이스 모드를 요구하고 mark-info 플래그를 켭니다. marked content가 없는 PDF/VCR-1 파일은 플레이스홀더가 가리킬 대상이 없기 때문입니다. 그 다음 BeginPDFVCRTemplate이 필드 목록과 페이지 선택 필드를 받는데, 데이터 스키마를 설계하기 전에 알아 둘 만큼 엄격합니다. 필드 이름은 PDFVCRValidFieldName을 거치며, ASCII 문자, 숫자, 밑줄, 하이픈, 마침표만 받아들입니다. 이스케이프 모호함 없이 PDF 이름으로 직렬화되는 문자들이 그 문자들이기 때문입니다. PDFVCRNormalizeFieldList는 목록의 대소문자를 유지하고 중복을 아예 거부하며, 페이지 선택 필드는 선언된 목록에 반드시 포함되어야 합니다
var
Lib: TPDFlib;
Diag: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
if Lib.SetPDFVCRMode(1) <> 1 then
raise Exception.Create('PDF/VCR mode was refused');
Lib.NewPage;
if Lib.BeginPDFVCRTemplate('Salutation,FullName,AccountNo',
'AccountNo') <> 1 then
raise Exception.Create('Field list rejected');
// 플레이스홀더 하나는 선언된 필드 정확히 하나를 묶습니다.
Lib.BeginPDFVCRPlaceholder('FullName', 72, 690, 320, 712);
Lib.DrawText(72, 694, 'SAMPLE NAME');
Lib.EndPDFVCRPlaceholder;
Lib.EndPDFVCRTemplate;
Diag := Lib.GetPDFVCRDiagnostics;
if Diag <> '' then
begin
Writeln('PDF/VCR blockers: ', Diag);
Exit;
end;
Lib.SaveToFile('campaign-template.pdf');
finally
Lib.Free;
end;
end;
bracketing이 주는 이점을 보세요. BeginPDFVCRPlaceholder는 템플릿이 활성화되어 있고 태그 스택이 정확히 한 레벨 깊이일 때만 열립니다. PDFlibPas가 플레이스홀더가 템플릿 루트의 직접 자식이고 서로 중첩되지 않음을 보장하는 방식입니다. 좌표도 검증되어 NaN, 무한대, 뒤집힌 사각형이 거부되므로, 잘못된 bounding box는 일주일 뒤 preflight 리포트가 아니라 호출 시점에 실패합니다
플레이스홀더는 왜 자체 MCID 직렬화 경로가 필요할까요?
PDF/VCR-1이 /K를 맨 정수로 원하기 때문이며, 일반 tagged-PDF writer가 구조 요소와 콘텐츠 사이의 연결을 직렬화하는 방식이 그렇지 않습니다. ISO 32000-1 §14.6에서 구조 요소는 보통, 요소가 이미 /Pg로 페이지를 식별하는 경우 직접 MCID 정수를 통해, 혹은 페이지를 명시적으로 이름 짓는 marked-content 참조 딕셔너리를 통해 marked content를 참조합니다. 여러 용도로 tagged 출력을 내는 라이브러리는 페이지를 가로질러 더 잘 조합되기 때문에 딕셔너리 형태로 표준화하는 경향이 있습니다
PDFlibPas는 여기서 일반적인 옵션 대신 좁은 옵션을 택했습니다. SetCurrentStructElemDirectMCIDKid는 현재 구조 요소에 플래그를 붙이고, 그 플래그를 지닌 요소만 /K를 직접 MCID로 직렬화하며 페이지는 /Pg에 실립니다. 나머지는 전부 기존 MCR 딕셔너리 경로를 그대로 둡니다. 이는 의도된 트레이드오프이고 소리 내어 이름 붙일 가치가 있는 종류입니다. 프로필을 추가한다고 다른 tagged 문서들이 이미 만들어 내는 출력 바이트가 바뀌어서는 안 되므로, 프로필은 전역 스위치 대신 자기 경로를 얻습니다. 기존 tagged 파일은 전부 이전 직렬화를 유지하며, 회귀 표면은 새 기능의 크기로 머물러 있습니다
순회 하나, 소비자 둘
PDFlibPas는 같은 2패스 순회에서 교체 매니페스트와 적합성 판정을 함께 유도하므로, 매니페스트를 작업 계획으로 신뢰할 수 있습니다. 순회는 검증하면서 TPDFVCRManifestData record를 채웁니다. 템플릿 루트에서 온 Fields, 선언된 경우의 PageSelectionField, 그리고 FieldName, PageNumber, MCID, HasBBox, BBox를 지닌 TPDFVCRPlaceholderInfo 항목들의 Placeholders 배열입니다. 적합성 엔트리 포인트는 CheckCompliancePDFVCR(InputStream, Password, Options, ResultList)이며, 검사를 한꺼번에 돌리는 대신 의존성 순서로 실행합니다. 먼저 전체 PDF/X 검사, 그다음 GTS_PDFXVersion을 PDF/X-4와 대조, 그다음 http://www.npes.org/pdfvcr/ns/id/ 네임스페이스에서 읽은 GTS_PDFVCRVersion, 그리고 나서야 구조 순회입니다. Options를 1로 넘기면 첫 문제에서 멈추는데, 배치 게이트에서 원하는 동작입니다. 0을 넘기면 전부 수집하며, 리포트에서 원하는 동작입니다
// 직접 만든 게 아니라 받은 템플릿을 검증하기
var
Issues: TStringList;
Stream: TFileStream;
I: Integer;
begin
Issues := TStringList.Create;
Stream := TFileStream.Create('supplier-template.pdf',
fmOpenRead or fmShareDenyWrite);
try
if CheckCompliancePDFVCR(Stream, '', 0, Issues) <> 100 then
for I := 0 to Issues.Count - 1 do
Writeln('PDF/VCR: ', Issues[I]);
finally
Stream.Free;
Issues.Free;
end;
end;
매니페스트는 계약이라 적합한 파일에만 존재합니다
BuildPDFVCRManifest는 문제 목록이 비어 돌아올 때만 JSON을 반환하고, 그 외에는 빈 문자열을 반환합니다. 이는 그 자체를 위한 방어적 코딩이 아닙니다. 매니페스트는 그걸로 실제 출력을 구동할 composition 엔진이 소비하며, 템플릿 루트가 두 개인 파일에서 뽑은 부분 필드 목록을 돌려주면 엔진이 부적합 구조를 신뢰할 만한 교체 계획으로 취급하게 됩니다. 문서 API에서 GetPDFVCRManifest(InputFileName, Password)는 같은 호출을 감싸며, 파일이 적격하지 않으면 마지막 오류 코드를 PDFLIB_ERROR_PDFVCR_COMPLIANCE(608)로 설정합니다. 그래서 실패한 추출과 빈 템플릿을 구분할 수 있습니다. JSON은 fields, string이나 null인 pageSelectionField, 그리고 항목마다 field, page, mcid와 네 숫자 bbox나 null을 주는 placeholders 배열을 보고합니다
var
Lib: TPDFlib;
Manifest: WideString;
begin
Lib := TPDFlib.Create;
try
Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
if Manifest = '' then
// 608은 파일이 PDF/VCR-1에 적합하지 않음을,
// 401은 파일을 열 수 없음을 뜻합니다
Writeln('No manifest, error ', Lib.LastErrorCode)
else
Writeln(Manifest);
finally
Lib.Free;
end;
end;
소비자로 가져갈 세부사항 하나. page 값은 뷰어에서 사람이 보는 1-based 번호가 아니라 라이브러리 전체의 zero-based 페이지 관례를 따릅니다. 이 둘을 섞으면 콘텐츠가 엉뚱한 시트에 실리는 조용한 off-by-one이 되는데, 두 숫자 모두 구조적으로 유효해서 어떤 검증기도 잡아내지 못합니다
경계는 어디에 있을까요?
PDFlibPas의 PDF/VCR-1 지원은 템플릿 작성, 저장 게이트, 독립 preflight, 매니페스트 추출을 다룹니다. 교체 자체는 수행하지 않습니다. 플레이스홀더에 데이터를 치환하고 개인화된 인스턴스를 만드는 것은 composition 단계이며, 일부러 라이브러리 밖에 있습니다. 매니페스트가 바로 composition 엔진을 독립적으로 작성하게 해 주는 인터페이스이기 때문입니다. 저장 게이트도 수리하는 대신 거부합니다. 잘못된 주석 플래그를 조용히 정규화할 수 있는 프로필과 달리, PDFVCRReadyForSave가 강제하는 모든 PDF/VCR-1 제약은 추측이 문서의 의미를 바꾸는 종류입니다. 템플릿 루트를 지어내거나, 바인딩되지 않은 플레이스홀더가 어떤 필드에 속하는지 골라 주면, 검증은 통과하면서 거짓말하는 파일이 만들어집니다. 같은 수리-대-거부 선은 다른 프로필에서 PDF/E-1 author 모드와 제한된 preflight에서 다루며, 문서 하나에 여러 적합성 검사를 돌리는 워크플로는 compliance와 signing workbench에서 다룹니다
작성, 제한된 구조 순회, 저장 게이트, 매니페스트 추출기는 전부 PDFlibPas Delphi PDF 라이브러리에 포함되어 있습니다. 즉 템플릿을 프로필 아래에서 만든 뒤, writer 상태를 전혀 보지 못한 별도의 코드 경로로 검증할 수 있고, 그 분리가 적합성 주장에 값이 붙는 유일한 이유입니다