PDFium Componentは、PDF/AのInfoとXMPメタデータの等価性をTPdf.InspectPdfAMetadataで検査し、TPdf.NormalizePdfAMetadataで修復します。ISO 19005-1(Cor.1による修正を含む)は、マップされた8つのInfoエントリ、TitleからModDateまでのそれぞれに対し、対応するXMPプロパティと同じ値を持つことを要求します。存在するだけでよいわけではありません。検査は正しいRDF構造を読み、名前空間をURIで照合し、日付を瞬間として比較します
この話のきっかけになる障害報告は、一見無害です。文書管理システムが増分保存のたびにInfoディクショナリへ新しい/ModDateを押し込み、XMPパケットには触れない。半年後、アーカイブ監査が数千のファイルを不適合としてフラグします。両方の日付は存在します。最初の編集で食い違っただけで、存在チェックはそれに気づかなかったのです。Info専用API経由で行われたタイトル編集も、ツールによって2つのdc:creator項目に分割されたFinance; ControllingのようなAuthor文字列も、同じように引っかかります
両側に存在するメタデータをPDF/Aが拒む理由
PDF/Aが拒むのは、ISO 19005-1 §6.7.3が値のルールであって存在のルールではないからです。Table 1は8つのInfoキーをXMPプロパティへマップし、Infoキーが存在した時点で、マップ先のXMPプロパティは等価な値を保持しなければなりません。PDFium ComponentによるPDF/Aプレフライト検証で説明したバイトレベルスキャナは、xmp:CreateDateとxmp:ModifyDateが存在することしか確認しません(pvaiMissingXmpDates)。v3.72.0から、TPdf.ValidatePdfAはさらに完全な値比較を実行し、XMPパケットが存在するのにInfoと食い違う場合(パースできないパケットは食い違いと数える)、pvaiInfoXmpValueMismatchを問題セットへ追加します。パケットが欠けている場合はpvaiMissingXmpMetadataのまま報告されるため、2つの問題が同じ欠陥を二重に数えることはありません
各マップ先XMPプロパティが必要とするRDF構造
8つのマッピングにはそれぞれ固定の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文字列全体を保持するテキスト項目をちょうど1つ持つrdf:Seq。セミコロン区切りの著者リストも1つのエントリのままにする - 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のような非ゼロの小数秒もミスマッチを強制します。PDF日付にはそれを表現する手段がなく、黙って丸めれば本物の食い違いを隠してしまうからです。.000は受け入れられます。パースできない値はpamsInvalidInfoDateかpamsInvalidXmpDateとして別々に報告されます
存在には専用のルールがあります。TPdfAMetadataValues.Presentはアクティブtrailerの/Infoディクショナリをたどって満たされるセットであり、「キーが無い」と「キーはあるが空文字列」を区別します。無いキーはpamsNotRequiredとなり、XMPには何も要求しません。/Title ()は存在するので、XMPパケットも空のx-defaultタイトルを運ばなければなりません
保存前にInfoとXMPメタデータを検査する方法
TPdf.InspectPdfAMetadataはフィールドごとに1つのTPdfAMetadataComparisonを持つTPdfAMetadataReportを返します。それぞれがInfo値、XMP値、TPdfAMetadataStateを保持するため、失敗を1つの検証フラグを逆算することなく説明できます。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は1項目の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のルールの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を組み立てるパイプラインに向いています。1つだけ罠があります。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は1項目の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に搭載されています