PDFium Component는 TPdf.InspectPdfAMetadata로 PDF/A의 Info-XMP 메타데이터 동등성을 검사하고 TPdf.NormalizePdfAMetadata로 복구합니다. ISO 19005-1(Cor.1로 수정된 판)은 매핑된 여덟 Info 항목, Title부터 ModDate까지 각각이 단지 존재하기만이 아니라 자기 XMP 속성과 같은 값을 지녀야 합니다. 검사는 올바른 RDF 구조를 읽고, 네임스페이스를 URI로 대조하며, 날짜를 시각으로 비교합니다
이 이야기를 여는 버그 보고는 무해해 보입니다. 문서 관리 시스템이 증분 저장마다 Info 딕셔너리에 새 /ModDate를 찍고 XMP 패킷은 그대로 두면, 6개월 뒤 아카이브 감사가 수천 개 파일을 부적합으로 표시합니다. 두 날짜 다 있습니다. 첫 편집에서 의견이 갈리기 시작했을 뿐이고, 존재 검사는 그걸 영영 못 봤습니다. Info 전용 API로 이뤄진 Title 편집, 그리고 어떤 도구가 dc:creator 항목 둘로 쪼개 버린 Finance; Controlling 같은 Author 문자열도 같은 방식으로 실패합니다
PDF/A는 양쪽에 존재하는 메타데이터를 왜 거부할까요?
PDF/A가 거부하는 이유는 ISO 19005-1 §6.7.3이 존재 규칙이 아니라 값 규칙이기 때문입니다. Table 1이 여덟 Info 키를 XMP 속성에 매핑하고, Info 키가 존재하는 순간 매핑된 XMP 속성은 동등한 값을 지녀야 합니다. PDFium Component의 PDF/A preflight 검증에서 기술한 바이트 수준 스캐너는 xmp:CreateDate와 xmp:ModifyDate의 존재만 확인합니다(pvaiMissingXmpDates). v3.72.0부터 TPdf.ValidatePdfA는 추가로 전체 값 비교를 돌리고, XMP 패킷이 존재하는데 Info와 어긋나면 문제 집합에 pvaiInfoXmpValueMismatch를 더합니다(파싱할 수 없는 패킷은 어긋나는 것으로 칩니다). 패킷이 없는 경우는 계속 pvaiMissingXmpMetadata로 보고되어, 두 문제가 같은 결함을 이중으로 세는 일은 없습니다
매핑된 각 XMP 속성에는 어떤 RDF 구조가 필요할까요?
여덟 매핑 각각은 고정된 XMP 타입을 가지며, 잘못된 컨테이너에 든 올바른 값도 여전히 실패합니다. FPdfPdfa.pas의 ComparePdfAInfoAndXmp는 속성을 네임스페이스 URI로 찾으므로, http://purl.org/dc/elements/1.1/을 평범하지 않은 접두사에 묶은 패킷도 dc를 쓰는 패킷과 똑같이 읽힙니다. 요구되는 구조는 다음과 같습니다:
- Title →
dc:title, Subject →dc:description:rdf:Alt언어 대안이며 자기x-default항목만과 비교합니다(언어 태그는 대소문자 무시 매칭).x-default없는 Alt는 없는 것으로 칩니다 - Author →
dc:creator: Info 문자열 전체를 담은 텍스트 항목 정확히 하나짜리rdf:Seq입니다. 세미콜론으로 구분된 저자 목록도 단일 항목으로 유지됩니다 - Keywords →
pdf:Keywords, Producer →pdf:Producer(네임스페이스http://ns.adobe.com/pdf/1.3/): 단순 텍스트 속성입니다 - Creator →
xmp:CreatorTool, CreationDate →xmp:CreateDate, ModDate →xmp:ModifyDate(네임스페이스http://ns.adobe.com/xap/1.0/): 단순 텍스트 속성입니다
<dc:title><rdf:Alt><rdf:li xml:lang="x-default">Quarterly Report 2026</rdf:li></rdf:Alt></dc:title>
<dc:creator><rdf:Seq><rdf:li>Finance; Controlling</rdf:li></rdf:Seq></dc:creator>
<pdf:Producer>PDFium Component</pdf:Producer>
텍스트 값은 정확한 Unicode 코드포인트 시퀀스로 비교됩니다. 잘라내기도, 대소문자 접기도, 정규화도 없이요. 뒤따르는 공백 하나, 또는 한쪽은 미리 합쳐진 é이고 다른 쪽은 e에 결합 액센트를 얹은 것도 진짜 불일치입니다. Info 쪽은 언제나 FPDF_GetMetaText를 통한 PDFium 자체의 PDFDocEncoding과 UTF-16 텍스트 디코딩에서 나오므로, 라이브러리가 문자열 디코딩을 다시 구현하다 미묘하게 틀릴 여지가 없습니다. XMP 쪽은 그것을 만든 바이트만큼만 깨끗하고, 그래서 Free Pascal에서 XMP 메타데이터를 오염시키는 코드페이지 함정이 여기서도 문제됩니다
PDF 날짜와 XMP 날짜는 언제 같을까요?
PDF 날짜와 XMP 날짜는 초 단위까지 같은 시각을 기술하고 양쪽의 시간대 지식이 같을 때 같습니다. 두 파서 모두 합법적인 축약 정밀도를 받으므로 D:2026과 2026은 둘 다 2026년 1월 1일 00:00:00을 뜻합니다. 두 값이 모두 시간대를 지니면 비교 전에 UTC로 변환됩니다. D:20260827093659+08'00'은 2026-08-27T01:36:59Z와 같습니다. 어느 쪽도 시간대를 지니지 않으면 로컬 구성요소를 쓰인 그대로 비교합니다. 한쪽만 시간대를 지니면 결과는 pamsValueMismatch입니다. 오프셋을 지어내는 건 추측이기 때문입니다. XMP의 .250 같은 0이 아닌 소수 초도 불일치를 강제합니다. PDF 날짜에는 그것을 표현할 방법이 없고, 조용히 반올림해 버리면 진짜 어긋남을 숨기게 되기 때문입니다. .000은 받아들입니다. 파싱 불가능한 값은 pamsInvalidInfoDate 또는 pamsInvalidXmpDate로 따로 보고됩니다
존재에는 자체 규칙이 있습니다. TPdfAMetadataValues.Present는 활성 trailer의 /Info 딕셔너리를 훑어 채워지는 집합으로, "키 없음"과 "빈 문자열을 지닌 키 있음"을 구별해 둡니다. 없는 키는 pamsNotRequired를 내며 XMP에 아무것도 요구하지 않습니다. /Title ()은 존재하는 것이므로 XMP 패킷도 빈 x-default 제목을 지녀야 합니다
저장 전에 Info와 XMP 메타데이터는 어떻게 들여다볼까요?
TPdf.InspectPdfAMetadata는 필드마다 TPdfAMetadataComparison 하나씩을 지닌 TPdfAMetadataReport를 반환합니다. 각 항목은 Info 값, XMP 값, TPdfAMetadataState를 담으므로, 검증 플래그 하나를 역공학하지 않고도 실패를 설명할 수 있습니다. MismatchFields는 실패 집합을 요약하고, HasXmpPacket은 패킷을 찾았는지 알려 주며, XmpParseError는 패킷이 있는데 읽을 수 없을 때 파서 메시지를 실어 나릅니다
uses
System.SysUtils, PDFium, FPdfPdfa;
const
FieldNames: array[TPdfAMetadataField] of string = (
'Title', 'Author', 'Subject', 'Keywords',
'Creator', 'Producer', 'CreationDate', 'ModDate');
StateNames: array[TPdfAMetadataState] of string = (
'not required', 'equivalent', 'XMP missing', 'XMP type mismatch',
'value mismatch', 'invalid Info date', 'invalid XMP date');
procedure ReportMetadata(Pdf: TPdf);
var
Report: TPdfAMetadataReport;
Item: TPdfAMetadataComparison;
begin
Report := Pdf.InspectPdfAMetadata;
if Report.XmpParseError <> '' then
Writeln('XMP packet unreadable: ', Report.XmpParseError)
else if not Report.HasXmpPacket then
Writeln('No XMP packet at all');
for Item in Report.Comparisons do
if not Item.IsEquivalent then
Writeln(Format('%-12s %-18s Info="%s" XMP="%s"',
[FieldNames[Item.Field], StateNames[Item.State],
Item.InfoValue, Item.XmpValue]));
end;
NormalizePdfAMetadata는 무엇을 바꾸고 무엇을 거부할까요?
TPdf.NormalizePdfAMetadata는 Info 딕셔너리를 진실의 원천으로 취급하고 MismatchFields에 들어간 필드의 XMP 속성만 다시 씁니다. 패킷의 나머지는 전부 살아남습니다. Title과 Subject는 x-default 항목에 쓰이고 다른 언어 대안은 온전히 유지되며, Author는 항목 하나짜리 rdf:Seq가 되고, 모르는 네임스페이스와 무관한 속성은 보존되며, 없는 Info 키의 XMP 속성은 손대지 않습니다. 시간대 있는 Info 날짜는 Z 접미사를 단 정규형 UTC XMP 날짜로 쓰이고, 시간대 없는 날짜는 로컬 구성요소를 유지합니다. 파일 오버로드는 임시 파일과 원자적 교체로 저장하며, XMP 업데이트 자체는 증분 업데이트로 덧붙습니다
거부는 의도적입니다. XMP 패킷이 없으면 메서드는 EPdfError를 냅니다. 완전한 PDF/A 식별과 메타데이터 집합을 만드는 일은 SaveAsPdfA의 소관이고, PDFium Component로 PDF/A 아카이브 파일 만들기에서 다룹니다. 잘못된 Info 날짜는 그럴듯해 보이는 틀린 값을 쓰는 대신 EPdfXmpError를 내고, 아무것도 저장되지 않습니다. 서명된 문서는 호출자가 AllowSignedDocument = True를 넘기지 않으면 거부됩니다. 동등성은 ISO 19005-1의 규칙 하나일 뿐이므로, 정규화된 파일이 자동으로 적합한 것은 아닙니다
uses
System.SysUtils, PDFium, FPdfPdfa, FPdfXmp;
procedure NormalizeArchive(const Source, Target: string);
var
Pdf: TPdf;
Report: TPdfAMetadataReport;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := Source;
Pdf.Active := True;
Report := Pdf.InspectPdfAMetadata;
if Report.IsEquivalent then
Exit; // 이미 일치, 파일은 그대로 둠
if not Report.HasXmpPacket then
raise Exception.Create('No XMP packet: convert with SaveAsPdfA instead');
try
if not Pdf.NormalizePdfAMetadata(Target) then
raise Exception.Create('Normalized save failed');
except
on E: EPdfXmpError do // 잘못된 Info 날짜 또는 읽을 수 없는 패킷
raise Exception.CreateFmt('Cannot normalize %s: %s', [Source, E.Message]);
end;
finally
Pdf.Free;
end;
end;
자기 XMP 패킷에서 비교 돌리기
ComparePdfAInfoAndXmp와 SynchronizePdfAInfoToXmp는 FPdfPdfa의 평범한 함수로, 문서를 로드하지 않고도 TPdfXmpPacket으로 동작합니다. 템플릿에서 XMP를 조립하는 단위 테스트와 파이프라인에 어울립니다. 함정은 단 하나, Present입니다. Default(TPdfAMetadataValues)로 초기화한 레코드는 빈 집합을 지니고, 모든 필드가 pamsNotRequired를 보고하며, 무슨 값을 채워 넣었든 비교는 공허하게 통과합니다
uses
System.SysUtils, System.IOUtils, FPdfPdfa, FPdfXmp;
procedure AlignTemplate(const TemplateFile: string);
var
Info: TPdfAMetadataValues;
Packet: TPdfXmpPacket;
Changed: TPdfAMetadataFields;
begin
Info := Default(TPdfAMetadataValues);
Info.Title := 'Quarterly Report 2026';
Info.Author := 'Finance; Controlling';
Info.ModDate := 'D:20260827093659+08''00''';
// Present가 어떤 필드가 필수인지 정합니다; 값만으로는 무시됨
Info.Present := [pamfTitle, pamfAuthor, pamfModDate];
Packet := TPdfXmpPacket.Parse(TFile.ReadAllText(TemplateFile, TEncoding.UTF8));
try
Changed := SynchronizePdfAInfoToXmp(Info, Packet);
// xmp:ModifyDate는 이제 2026-08-27T01:36:59Z, dc:creator는 항목 하나짜리 rdf:Seq
if Changed <> [] then
TFile.WriteAllBytes(TemplateFile, Packet.ToUtf8);
finally
Packet.Free;
end;
end;
파이프라인이 다른 시스템이 계속 편집하는 문서를 아카이브한다면, 매일 밤 InspectPdfAMetadata 점검을 돌리고 어긋난 파일에는 NormalizePdfAMetadata를 짝지어 주고, 무엇이든 장기 보관소로 떠나기 전에는 ValidatePdfA를 게이트로 유지하세요. 타입화된 보고서, 복구 경로, 나머지 PDF/A 도구는 Delphi 및 C++Builder용 PDFium Component에 실려 있습니다