다른 프로듀서의 PDF/UA 파일에 대해 검출 코드가 아무 선언도 없다고 보고하고, PDF/R 스캔은 편집기가 스티키 노트를 덧붙이는 순간 PDF/R이 아니게 됩니다. HotPDF는 적재된 문서에서 이런 선언을 IsLoadedPDFUA, GetLoadedPDFR1Version, GetLoadedPDFDeclarations 같은 접근자로 읽는데, 각각은 자기 규격이 식별자를 저장하는 자리를 들여다봅니다
각 PDF 규격이 식별자를 쓰는 자리
하위 규격들은 선언을 두는 자리 하나에 합의하지 못했습니다. 대다수는 카탈로그의 /Metadata 스트림 안 XMP 속성을 쓰고, 몇몇 오래된 규격은 여전히 문서 정보 사전의 키에 기대며, PDF/R은 파일 트레일러 곁에 주석 행을 놓고, PDF 2.0 세대는 두 메커니즘을 더합니다. 카탈로그의 개발자 확장 사전과 XMP 안의 PDF Declarations가 그것입니다. 식별자는 파일이 자기 자신에 대해 내놓는 진술이고, 콘텐츠가 그 진술에 부합하는지는 검증기가 판단할 일이며, 부합하는 PDF/A, PDF/X, PDF/UA 출력을 만들어 내기는 별개의 주제입니다. 라우팅(인쇄 큐는 PDF/X, 아카이브는 PDF/A, 접근성 파이프라인은 WTPDF)에는 올바른 자리에서 읽은 선언이 필요한 것입니다:
| 규격 | 선언이 사는 자리 | HotPDF 접근자 |
|---|---|---|
| PDF/A (ISO 19005) | XMP pdfaid:part와 pdfaid:conformance, PDF/A-4에는 pdfaid:rev | IsLoadedPDFA, GetLoadedPDFAVersion, GetLoadedPDFARevision |
| PDF/X (ISO 15930) | PDF/X-1a와 PDF/X-3은 Info 사전의 GTS_PDFXVersion, 이후 파트는 XMP | IsLoadedPDFX, GetLoadedPDFXVersion |
| PDF/UA (ISO 14289) | XMP pdfuaid:part | IsLoadedPDFUA, GetLoadedPDFUAVersion |
| PDF/VT (ISO 16612-2) | XMP pdfvtid:GTS_PDFVTVersion | IsLoadedPDFVT, GetLoadedPDFVTVersion |
| PDF/E (ISO 24517-1) | ISO_PDFEVersion, Info 사전, 카탈로그, XMP에서 읽힙니다 | IsLoadedPDFE, GetLoadedPDFEVersion |
| PDF/R-1 (ISO 23504-1) | %PDF-raster-x.y 주석이 마지막 startxref 바로 위에 | IsLoadedPDFR1, GetLoadedPDFR1Version |
| PDF/VCR-1 (ISO 16613-1) | 카탈로그나 XMP의 GTS_PDFVCRVersion | IsLoadedPDFVCR, GetLoadedPDFVCRVersion |
| WTPDF 1.0, ISO/TS 32005 | XMP의 pdfd:declarations 항목 | IsLoadedWTPDF, GetLoadedWTPDFVersion, IsLoadedISO32005 |
| PDF 2.0 개발자 확장 | 카탈로그의 /Extensions /ISO_ | HasLoadedISODeveloperExtension, GetLoadedISODeveloperExtensionLevels |
uses
System.SysUtils, HPDFDoc;
procedure ReportClaims(const FileName: string; const Password: AnsiString);
var
Pdf: THotPDF;
PDFX, PDFXLevel: AnsiString;
begin
Pdf := THotPDF.Create(nil); // 검사 전용으로 쓰는 별도 인스턴스
try
// 암호 오버로드는 스트림을 복호화하는데 XMP 패킷도 포함됩니다
// 프롬프트 없이 열리는 파일에는 ''를 넘깁니다
if Pdf.LoadFromFile(FileName, Password) <= 0 then
raise Exception.CreateFmt('Cannot open %s', [FileName]);
if Pdf.IsLoadedPDFA then
Writeln('PDF/A: ', Pdf.GetLoadedPDFAVersion); // 예: PDF/A-2b
PDFX := Pdf.GetLoadedPDFXVersion; // XMP, 그다음 Info
if PDFX <> '' then
begin
PDFXLevel := Pdf.GetLoadedInfoKeyValue('GTS_PDFXConformance');
if PDFXLevel <> '' then
Writeln('PDF/X: ', PDFX, ', conformance ', PDFXLevel)
else
Writeln('PDF/X: ', PDFX);
end;
if Pdf.IsLoadedPDFUA then
Writeln('PDF/UA: ', Pdf.GetLoadedPDFUAVersion); // 예: PDF/UA-1
if Pdf.IsLoadedPDFVT then
Writeln('PDF/VT: ', Pdf.GetLoadedPDFVTVersion);
if Pdf.IsLoadedPDFE then
Writeln('PDF/E: ', Pdf.GetLoadedPDFEVersion);
if Pdf.IsLoadedPDFR1 then
Writeln('PDF/R: ', Pdf.GetLoadedPDFR1Version); // 예: PDF/R-1.0
if Pdf.IsLoadedWTPDF then
Writeln('WTPDF: ', Pdf.GetLoadedWTPDFVersion);
finally
Pdf.Free;
end;
end;
예제가 그러하듯 검사에는 별도의 THotPDF 인스턴스를 쓰십시오. IsLoaded… 함수들은 컴포넌트가 쥐고 있는, 작성 중인 문서까지 포함해 무엇이든 그 문서에 대해 답합니다. 출력을 위해 PDFUACompliance가 켜져 있으면 IsLoadedPDFUA도 True이고, IsLoadedPDFX는 빈 것이 아닌 PDFXCompliance와 부합합니다. 작성 도중에 편리하지만, 두 역할을 한 컴포넌트로 겸하면 남의 파일에 대한 판정에 여러분의 출력 설정이 섞여 들어갑니다
식별자가 파트 사이를 옮겨 다닌 유일한 계열이 PDF/X입니다. PDF/X-1a와 PDF/X-3은 GTS_PDFXVersion을 문서 정보 사전에 넣고 XMP를 전혀 필요로 하지 않는 PDF 1.3 프로필인 반면, PDF/X-4와 그 이후 파트는 XMP 속성 pdfx:GTS_PDFXVersion을 요구합니다. GetLoadedPDFXVersion은 pdfx든 pdfxid든 그 접두어 아래에서 XMP를 먼저 들여다보고, 이어서 Info 사전, 그다음 일부 구형 프로듀서가 쓰는 카탈로그 키를 보고, 선언된 값을 그대로 돌려줍니다. PDF/X-1a에는 코딩해 둘 만한 군곡이 하나 있습니다. 버전 값은 PDF/X-1:2001이고 PDF/X-1a:2001 적합 수준은 제 키 /GTS_PDFXConformance에 살며, 예제는 X-1a를 평범한 X-1과 구별해야 할 때 GetLoadedInfoKeyValue로 그것을 읽습니다
올바른 XMP 선언은 어떻게 생겼는가?
XMP는 XML로 직렬화된 RDF이고, RDF/XML은 단순 속성을 rdf:Description 안에 두 가지로 쓰는 것을 허용합니다. 자식 요소로서의 <pdfuaid:part>1</pdfuaid:part>와, 따옴표는 작은따옴표든 큰따옴표든 상관없이 기술 자체의 속성으로서의 pdfuaid:part="1"이 그것입니다. ISO 16684-1은 둘 다 받아들이고, 패킷은 자기 속성을 여러 rdf:Description 요소에 나눠 놓을 수 있습니다. 이 나눔은 가정이 아닙니다. HotPDF는 PDF/UA, PDF/VT, PDF/E, PDF/X에 자식 요소 형태를 쓰는 반면, veraPDF 검사 말뭉치와 많은 외부 프로듀서는 단축형을 써서 선언 전체를 속성에 실습니다. pdfuaid:part="1"이나 pdfvtid:GTS_PDFVTVersion="PDF/VT-1" 같은 Description이 그 예입니다. Pos('<pdfuaid:part>', Xmp)를 중심으로 지어 자사 출력에서만 단련된 검출기는 로컬 검사는 모두 통과한 뒤 첫 veraPDF 표본을 무선언으로 불러 옵니다. HotPDF의 XMP 리더는 표에 있는 모든 접근자에 대해 두 형태를 다룹니다. GetLoadedPDFAVersion은 pdfaid:part와 pdfaid:conformance를 PDF/A-1b, PDF/A-2u, PDF/A-4, PDF/A-4f 같은 식별자로 엮고, GetLoadedPDFARevision은 PDF/A-4가 요구하고(2020) 파트 1부터 3은 정의하지 않는 판년도인 pdfaid:rev를 돌려줍니다:
procedure ReportPDFALevel(Pdf: THotPDF);
var
Level, Revision: AnsiString;
begin
Level := Pdf.GetLoadedPDFAVersion; // 'PDF/A-1b', 'PDF/A-2u', 'PDF/A-4f', ...
if Level = '' then
Exit;
Revision := Pdf.GetLoadedPDFARevision; // PDF/A-4는 '2020', 파트 1-3은 빈 값
if Revision <> '' then
Writeln(Level, ', edition ', Revision)
else
Writeln(Level);
// 기대: 프로듀서가 어느 쪽으로 썼든 "PDF/A-2b"
// <pdfaid:part>2</pdfaid:part>든 pdfaid:part="2"든
end;
접두어는 또 하나의 움직이는 부분입니다. XML에서 접두어는 지역 별칭에 불과하고, PDF/A 스키마를 밝히는 것은 네임스페이스 URI http://www.aiim.org/pdfa/ns/id/이며, 승인된 ISO 19005-4 정오표는 그 https:// 표기도 인정합니다. 프로듀서는 그 URI를 마음에 드는 어떤 접두어에도 묶을 수 있습니다. GetLoadedPDFAVersion은 파트와 수준을 읽기 전에 패킷이 그 URI에 대해 선언한 접두어를 찾아 보므로, pdfaid:part 대신 pdfa:part라고 쓴 패킷도 식별됩니다. GetLoadedXMPMetadata가 돌려주는 복호화된 패킷에서 여러분이 직접 읽는 그 밖의 XMP 속성도 같은 주의를 값습니다. 두 직렬화 모두, xmlns 선언에서 풀어 낸 접두어, 등호 주변의 공백 허용까지 챙기는 것입니다
암호화된 파일은 바이트 검색에서 선언을 숨깁니다
원시 파일을 pdfaid나 ISO_PDFEVersion으로 뒤지는 스캔은 구문 분석을 건너뛰고 두 가지 평범한 사례에서 부서집니다. 대다수 XMP 스트림은 Flate 압축되어 있어 속성 이름이 디스크 위에 텍스트로 나타나지 않습니다. 그리고 암호화된 문서에서 메타데이터 스트림은 암호문입니다. ISO 32000-2 §7.6.4.2는 기본값 true의 EncryptMetadata를 정의하므로, 프로듀서가 명시적으로 빠지지 않는 한 패킷은 나머지 모든 스트림과 함께 암호화됩니다. 그래서 암호화된 PDF/E-1 파일을 향한 바이트 검색은 ISO_PDFEVersion 선언이 거기 있는데도 없다고 보고합니다. 믿을 수 있는 순서는 적재, 복호화, 디코딩, 그다음 검색입니다. 파일은 암호 오버로드인 LoadFromFile(FileName, Password)로 열고, 소유자 암호만 실어 어떤 뷰어에서도 프롬프트 없이 열리는 파일에는 빈 문자열을 넘깁니다. 인수 하나짜리 LoadFromFile(FileName)은 스트림을 복호화하지 않고 문서 구조만 적재하므로 XMP 패킷은 읽을 수 없는 채로 남습니다. 암호 오버로드를 지나면 GetLoadedXMPMetadata와 위의 모든 접근자가 복호화되고 압축이 풀린 패킷 위에서 동작합니다
PDF/R 파일은 편집 한 번에 정체를 잃는가?
PDF/R-1은 다른 어떤 하위 규격도 쓰지 않는 방식으로 자기를 밝힙니다. ISO 23504-1 제5조는 startxref를 실은 마지막 행 바로 앞 행에 %PDF-raster-1.0(해당 버전이 무엇이든) 주석 행을 요구합니다. PDF 파서는 상호 참조 데이터를 읽는 동안 주석을 버려 버리므로 객체 모델은 보고할 것이 없고, 증거는 파일 끝의 원시 바이트에만 존재합니다. GetLoadedPDFR1Version은 문서가 적재된 원본으로 돌아가 최대 마지막 64 KiB를 읽고, 마지막 startxref를 찾아 그 위 행을 검사합니다. 이제 증분 갱신을 따라가 보십시오. 주석을 더하는 편집기는 원래 %%EOF 뒤에 새 객체와 새 상호 참조 구역, 새 트레일러, 새 startxref를 덧붙입니다. %PDF-raster-1.0 행은 여전히 파일 안에 있지만 이제 마지막에서 두 번째 startxref 위에 앉고, 어느 것이 유효한지는 규격이 단호합니다. 기대: 편집 도구가 자기 갱신 안에서 그 주석을 다시 내놓는 경우가 아니면 편집 뒤 IsLoadedPDFR1은 False를 돌려줍니다. 파일 전체를 %PDF-raster-로 뒤지는 검출기는 예스라고 할 것이고, 제5조의 규칙으로는 그 답이 틀립니다. 파일을 PDF/R로 지켜야 하는 캡처 워크플로는 주석을 사이드카 파일에 넣거나 파일 전체를 PDF/R로 다시 쓰는 도구에 맡깁니다
개발자 확장: 정수 수준과 이름, 배열
PDF 2.0 문서는 자기가 기대는 ISO 기술 사양(ISO/TS 32001의 SHA-3 해시, ISO/TS 32002의 ECC 서명, ISO/TS 32007의 glTF 자산)을 ISO 32000-2 §7.12에 정의된 카탈로그의 /Extensions 사전으로 알립니다. 등록된 접두어 /ISO_ 아래에서 표 48은 개발자 확장 사전 하나나, PDF 2.0에서 새로 나온 사전 배열을 허용하고, 표 49와 §7.12.5는 /ExtensionLevel을 정수로 만듭니다. 두 컨테이너 모양 중 하나만 다루는 리더는 다른 쪽으로 쓰인 파일을 모두 놓칩니다. HasLoadedISODeveloperExtension과 GetLoadedISODeveloperExtensionLevels는 둘 다 받아들이고 도중에 간접 참조도 풀어 주지만, §7.12.1은 이 사전들이 직접 객체여야 한다고 말합니다:
procedure ReportISOExtensions(Pdf: THotPDF);
var
Levels: THPDFIntegerArray;
Revision, URL: AnsiString;
I: Integer;
begin
Levels := Pdf.GetLoadedISODeveloperExtensionLevels;
for I := 0 to High(Levels) do
Writeln('ISO_ extension level ', Levels[I]);
if Pdf.HasLoadedISODeveloperExtension(32001, Revision, URL) then
Writeln('ISO/TS 32001 revision ', Revision, ', documented at ', URL);
// EnsureISO32001Extensions로 쓴 파일의 기대 출력:
// ISO/TS 32001 revision :2022, documented at https://www.iso.org/standard/45874.html
if Pdf.HasLoadedISODeveloperExtension(32007) then
Writeln('glTF 3D assets per ISO/TS 32007');
end;
ISO/TS 32007:2024 초판은 §4.8에 /ExtensionLevel /32007을 이름으로 쓰는 예가 있습니다. PDF 정오표 이슈 #835가 그것을 정수 32007로 바로잡았지만, 원래 예를 베낀 프로듀서는 여전히 이름을 씁니다. 그래서 HotPDF의 리더는 정수 조회가 비어서 돌아오면 선행 슬래시가 있든 없든 이름이나 문자열 값을 해석하는 것으로 물러서고, HasLoadedISODeveloperExtension(32007)은 두 표기를 다 찾아냅니다. 다른 정오는 /URL에 관한 것입니다. ISO 32000-2:2020의 표 49는 /URL을 선택으로 적고, 이슈 #732가 그것을 필수로 올립니다. 쓰는 쪽에서 HotPDF는 모든 EnsureISO…Extensions 호출에 정수 형태와 /URL을 내놓습니다. 읽는 쪽에서는 2020년 문본으로 쓰인 파일에 빈 URL out 매개변수가 정상이므로, 그것을 정보로 대하되 확장을 거절하는 근거로는 절대 쓰지 마십시오
프로듀서는 어느 WTPDF URI를 썼는가?
WTPDF 1.0과 ISO/TS 32005 선언은 PDF Declarations입니다. PDF Association의 PDF Declarations 기술 노트가 정의한 네임스페이스 http://pdfa.org/declarations/ 아래, XMP 안 pdfd:declarations 묶음의 항목들이지요. 각 항목은 conformsTo URI와 선택적 claimBy, claimDate, claimCredentials, claimReport 필드를 실으며, GetLoadedPDFDeclarations는 그것을 THPDFDeclarationEntry 레코드 배열로 돌려줍니다. 쓰는 쪽에서 AddPDFDeclaration은 같은 다섯 필드를 PDF 2.0, PDF/A-4, PDF/UA-2 식별 네임스페이스 곁에 XMP 패킷에 넣습니다. 평문 검색으로 그 묶음을 항목으로 자르는 데는 자기 함정이 있습니다. conformsTo가 나타날 때마다 자르면 자식 요소 형태의 닫는 태그 </pdfd:conformsTo>가 새 항목의 시작으로 세어져, 첫 선언이 자기 끝을 맞기 전에 잘립니다. HotPDF의 리더는 닫는 태그를 건너뛰고 두 직렬화를 다 받아들입니다
URI 자체가 더 다루기 힘든 부분입니다. 같은 선언이 http://로도 https://로도 도착하고, WTPDF 1.0 이슈 #395는 # 앞에 /가 앉는지를 바꾸었으므로 정오표 전의 파일과 후의 파일은 그 한 문자에서 갈라집니다. 평범한 문자열 비교는 둘을 다른 규격으로 칩니다. HasLoadedPDFDeclaration은 URI를 대소문자 무시로 비교하고, https://를 http://로 접으며, 양쪽에서 # 바로 앞의 /를 떼어 내므로 아래 질의는 프로듀서가 고른 표기가 무엇이든 부합합니다. 프래그먼트는 그대로입니다. #accessibility1.0과 #accessibility1.0-validated는 다른 선언이고, 두 번째는 검증기가 파일을 검사했다고 주장하니, 여러분 워크플로가 신경 쓰는 쪽을 요구하십시오. GetLoadedWTPDFVersion은 라우팅을 위해 둘을 'WTPDF 1.0 Accessibility'나 'WTPDF 1.0 Reuse'로 접어 줍니다
procedure ReportDeclarations(Pdf: THotPDF);
var
Decls: THPDFDeclarationEntryArray;
I: Integer;
begin
Decls := Pdf.GetLoadedPDFDeclarations;
for I := 0 to High(Decls) do
Writeln(Decls[I].ConformsTo, ' claimed by ', Decls[I].ClaimBy,
' on ', Decls[I].ClaimDate);
// http든 https든, '#' 앞 슬래시 유무와 무관하게 부합
if Pdf.HasLoadedPDFDeclaration(
'https://pdfa.org/declarations/wtpdf#accessibility1.0') then
Writeln('WTPDF 1.0 Accessibility (unvalidated claim)');
// Reuse와 Accessibility의 평문과 -validated 변형을 모두 커버
Writeln('WTPDF profile: ', Pdf.GetLoadedWTPDFVersion);
end;
캡처나 인쇄 라우터라면 연산 순서가 어느 접근자 하나보다 중요합니다. 주석이나 도장 단계가 자기 갱신을 덧붙이기 전, 도착한 그대로의 파일에서 IsLoadedPDFR1을 검사하고, PDF/X는 GetLoadedPDFXVersion으로 분류하십시오. 이 함수는 PDF/X-1a와 PDF/X-3 작업의 Info 사전과 PDF/X-4 작업의 XMP를 두루 커버하며, X-1a를 평범한 X-1과 구별해야 하는 자리에서만 /GTS_PDFXConformance를 읽습니다. 두 검사 모두 암호를 풀어 연 암호화 파일을 포함해 HotPDF Delphi Component에 적재된 어떤 문서에서든 돌아갑니다