技術記事

DelphiのFactur-X XMP向けPDF/A-3拡張スキーマ

Factur-Xの請求書を作り終え、コンテナのチェックはすべて通過しているとします。カタログには/AF配列があり、EmbeddedFilesの名前ツリーは正しいファイル仕様に解決され、埋め込まれたfactur-x.xmlは/AFRelationshipとして正しくAlternativeを持ち、組み込みのValidateFacturXInvoiceは1を返します。ところが同じファイルを、税務ポータルが使う参照チェッカーであるveraPDFに通すと、ドキュメント全体が有効なPDF/A-3ではないと判定されます。構造は正しいのです。問題はメタデータであり、この失敗は電子請求書のワークフロー全体の中でも取り逃がしやすいものの1つです

その理由は最後まで理解しておく価値があります。可視ページにも添付ファイルにも一切関係がなく、XMPが自分自身をどう記述するかにすべてがかかっている、というPDF/Aの欠陥の一群を説明してくれるからです。これこそが、緑色のコンテナチェックの裏に隠れる罠です

ファイルを不合格にする4つのプロパティ

Factur-Xの請求書は、下流のソフトウェアが埋め込みXMLを解析せずに請求書のプロファイルを読めるように、XMPパケットへ4つのカスタムプロパティを書き込みます。それらはfx接頭辞の下、Factur-Xの名前空間に置かれます。fx:DocumentFileName、fx:DocumentType、fx:Version、fx:ConformanceLevelです。このPDFがバージョン1.0のfactur-x.xmlという名前のEN 16931請求書を運んでいると読み手が知るために、まさに必要なメタデータです

この4つのプロパティは、いずれもPDF/Aがあらかじめ定義しているXMPスキーマの一部ではありません。Dublin Core、XMP Basic、PDF、そしてPDF/A識別の各スキーマは準拠リーダーにとって既知ですが、fx:はそうではありません。veraPDFがXMPをたどって、認識できない名前空間のプロパティに行き当たると、そのプロパティが何を意味するのかを教えてくれる宣言を探します。その宣言がなければ、ISO 19005-3の条項6.6.2.3.1に対する失敗として報告します。この条項は、あらかじめ定義されたスキーマに由来しないすべてのプロパティをPDF/A拡張スキーマの中で記述するよう求めています。宣言のないプロパティが4つ、ファイルが拒否される道筋も4つ、しかもそのどれ1つとしてコンテナチェックからは見えません

バリデーターがあらかじめ定義された4つのXMPスキーマを探してもfxプロパティ向けのPDF/A拡張スキーマが見つからず、Factur-Xの請求書が不合格になるPDF Library for Delphiの図
veraPDFはファイルが一度も書かなかったスキーマ宣言を探し回ります — fxプロパティが4つ、条項6.6.2.3.1で不合格になる機会も4つ

PDF/Aが裸のカスタムプロパティを拒む理由

この規則は、PDF/Aが何のためにあるのかを思い出すまでは杓子定規に見えます。この形式は、2026年の慣習など一度も教わっていないソフトウェアによって、数十年後にもファイルを開いて理解できるようにするために存在します。準拠リーダーは、参照すべき外部のレジストリなしに、ドキュメントだけからその内容を読み解けることを期待されています

カスタムメタデータは、ファイル自身がその説明を携えていない限り、この約束を破ります。裸のfx:ConformanceLevelプロパティを渡されただけでは、将来のリーダーはfx接頭辞がどの名前空間URIに束縛されるのかも、値がテキストなのか日付なのか整数なのかも、そのプロパティがドキュメント自身を記述しているのか外部リソースを記述しているのかも知りようがありません。PDF/Aの拡張スキーマの仕組みは、その隙間を埋めます。名前空間、接頭辞、そして各プロパティについて値の型とinternalまたはexternalという区分を、決まったXMPの構造の中でファイル自身に宣言させるのです。その宣言があればプロパティは自己記述的になり、条項6.6.2.3.1は満たされます。宣言がなければ、バリデーターはそのプロパティを理解不能なものとして扱い、ファイルを不合格にするほかありません。ここでは区分の違いが効いてきます。今回のような請求書のプロパティはPDFプロセッサの外部から来るデータを記述するので、internalではなくexternalとして宣言されます

拡張スキーマの宣言に含まれるもの

宣言は、AIIMが定義した3つの名前空間pdfaExtension、pdfaSchema、pdfaPropertyを使う、XMPパケット内のrdf:Descriptionです。pdfaExtension:schemasというバッグの中に、Factur-Xスキーマに名前を与え、そのpdfaSchema:namespaceURIとpdfaSchema:prefixを示し、続いてpdfaSchema:propertyのシーケンスへ4つのプロパティを列挙するスキーマ項目が1つ収まります。各プロパティは名前と、TextというpdfaProperty:valueType、そしてexternalというpdfaProperty:categoryを持ちます。下の例示的なマークアップは、そのブロックの形を示しています

Factur-Xスキーマをその名前空間URI、fx接頭辞、4つのexternalなTextプロパティとともに宣言するPDF/A-3拡張スキーマの構造を示したPDF Library for Delphiの図
宣言が主張する内容は請求書が実際に書き出すfxブロックと一致していなければならず、そうでなければバリデーターは依然としてファイルを拒みます
<rdf:Description rdf:about=""
    xmlns:pdfaExtension="http://www.aiim.org/pdfa/ns/extension/"
    xmlns:pdfaSchema="http://www.aiim.org/pdfa/ns/schema#"
    xmlns:pdfaProperty="http://www.aiim.org/pdfa/ns/property#">
  <pdfaExtension:schemas>
    <rdf:Bag>
      <rdf:li rdf:parseType="Resource">
        <pdfaSchema:schema>Factur-X PDFA Extension Schema</pdfaSchema:schema>
        <pdfaSchema:namespaceURI>urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#</pdfaSchema:namespaceURI>
        <pdfaSchema:prefix>fx</pdfaSchema:prefix>
        <pdfaSchema:property>
          <rdf:Seq>
            <rdf:li rdf:parseType="Resource">
              <pdfaProperty:name>DocumentFileName</pdfaProperty:name>
              <pdfaProperty:valueType>Text</pdfaProperty:valueType>
              <pdfaProperty:category>external</pdfaProperty:category>
              <pdfaProperty:description>name of the embedded XML invoice file</pdfaProperty:description>
            </rdf:li>
            <!-- DocumentType、Version、ConformanceLevelも同じ方法で宣言する -->
          </rdf:Seq>
        </pdfaSchema:property>
      </rdf:li>
    </rdf:Bag>
  </pdfaExtension:schemas>
</rdf:Description>

名前空間URIと接頭辞は固定の文字列ではありません。プロファイルに従います。Factur-Xのドキュメントはfx接頭辞とともにurn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#を使い、zugferd-invoice.xmlを通じて選ばれたZUGFeRD 2.0のファイルは、独自のスキーマ名の下で別のURIに解決されます。拡張スキーマは、プロパティのブロックが実際に使っているのと同じ名前空間URIを宣言しなければならず、さもなければバリデーターは両者を結び付けられません。PDF Library for Delphiは、渡されたファイル名とバージョンから両方の値を導出するので、宣言とプロパティのブロックは常に一致します

ヘルパーが両方の半分をまとめて書く仕組み

PDF Library for Delphiでは、そのXMLを手で組み立てることはありません。ドキュメントをPDF/A-3モードに置き、メソッドを1つ呼ぶだけです。最初に決めるべきは適合フラグです。Factur-XがPDF/A-3を要求するからです。SetPDFAMode(7)を呼ぶとPDF/A-3uのレベルが選ばれ、識別スキーマのpdfaid:partが3に、pdfaid:conformanceがUに設定されます。これでXMPパケットは、請求書のメタデータが追加される前に正しいpartとconformanceを備えます

var
  FileID: Integer;
begin
  PDF.SetPDFAMode(7);            // PDF/A-3u:pdfaid:part=3、conformance=U
  PDF.NewDocument;
  // ここで人間可読の請求書ページを描画する

  FileID := PDF.AddFacturXAssociatedFileFromString(
    InvoiceXML,                  // 生のUTF-8 XMLバイト列
    'EN16931',                   // ConformanceLevel
    'factur-x.xml',              // 埋め込みファイル名
    'Factur-X invoice XML',      // /Descのテキスト
    'Alternative',               // /AFRelationship
    '1.0',                       // プロファイルのバージョン
    '');                         // 任意の国コード
  if FileID = 0 then
    Exit;                        // PDF/A-3でないか、XMLとプロファイルの不一致

  PDF.SaveToFile('factur-x.pdf');
end;

AddFacturXAssociatedFileFromStringの1回の呼び出しが、不合格になったファイルに欠けていた仕事をこなします。指定した関係でXMLをPDF/A-3の関連ファイルとして埋め込み、選んだプロファイルのスキーマ名、名前空間URI、接頭辞とともに4つのfxプロパティを記録します。ドキュメントを保存すると、ApplyFacturXMetadataという内部の手順が、プロパティのブロックと対応するpdfaExtension:schemas宣言の両方をXMPパケットへ注入するので、カスタムプロパティは記述済みの状態で届きます。ドキュメントがPDF/A-3モードでない場合、あるいはXMLが宣言されたプロファイルと合わない場合、このメソッドは0を返します。これは、不正な請求書がそもそもファイルへ到達するのを止めるのと同じガードです

コンテナチェックには見えない死角

ここははっきり名指ししておくべき部分です。バグが隠れる理由がここにあるからです。ValidateFacturXInvoiceはコンテナを検査します。カタログに/AFエントリがあること、EmbeddedFilesの名前ツリーが存在すること、請求書XMLがあること、埋め込みファイル名がプロファイルと一致すること、XML内のガイドラインIDが適合レベルと合致すること、そして/AFRelationshipがPDF/A-3の許す値であることを確認します。これらは本物の検査であり、本物の欠陥を捕まえます。GetFacturXValidationIssuesは、MissingCatalogAF、NotPDFA3、ConformanceGuidelineMismatch、InvalidAFRelationship、InvalidFileNameProfileといった識別子で、それらを名前付きで報告します

検査しないのは、XMP拡張スキーマが存在し正しいかどうかです。コンテナは完璧なのにfxプロパティが未宣言のファイルは、あらゆる問題チェックを通過して1を返します。あの一覧のどれ1つとしてpdfaExtension:schemasブロックを調べないからです。手で組み立てた請求書や、宣言なしにプロパティのブロックだけを書いたパイプラインの成果物が、組み込みバリデーターをすり抜けたうえで条項6.6.2.3.1についてveraPDFに落とされるのは、まさにこれが理由です。コンテナのバリデーターとPDF/Aのメタデータのバリデーターは別々の問いに答えており、2つ目に答えるのは完全なPDF/Aチェッカーだけです

同じFactur-X請求書がValidateFacturXInvoiceのコンテナチェックをすべて通過する一方、pdfaExtension schemasブロックを読む層がないためveraPDFに拒否される様子を示すPDF Library for Delphiの図
コンテナのバリデーターとPDF/Aのバリデーターは、同じファイルについて別々の問いに答えます

どちらの層が壊れたか分かるように問題を読む

2つの層は独立に失敗するので、正しい診断の習慣は、まずコンテナの問題を読み、結果がきれいであってもそれはコンテナについての言明にすぎず、PDF/Aメタデータについてのものではないと受け止めることです。組み込みの検証を実行し、問題の一覧を集め、外部ツールへ手を伸ばす前にそれらへ対処してください

var
  Issues: WideString;
begin
  if PDF.ValidateFacturXInvoice = 0 then
  begin
    Issues := PDF.GetFacturXValidationIssues('|');
    // コンテナレベルの識別子。例:
    //   MissingCatalogAF、NotPDFA3、MissingEmbeddedFilesNameTree、
    //   ConformanceGuidelineMismatch、InvalidAFRelationship
    WriteLn('Container issues: ', Issues);
  end
  else
    WriteLn('Container OK; verify XMP extension schema with a PDF/A checker.');
end;

その呼び出しが問題名を返したなら、不具合はコンテナにあり、メッセージがどの部分かを教えてくれます。きれいな結果が返るのにveraPDFがなおファイルを拒むなら、不具合はほぼ確実にXMP拡張スキーマであり、直し方はプロパティのブロックを自分で組み立てるのではなく、AddFacturXAssociatedFileFromStringにメタデータを書かせることです。この2つの問いを自分の頭の中で分けておくことが、当惑させられる拒否を1行の診断へ変えます。コンテナの問題は問題一覧を通じて表に出て、スキーマ宣言の問題はPDF/Aバリデーターを通じてしか表に出ず、その2つを混同することがバグを隠しているのです

ビルドを離れる前にプリフライトを走らせる方法も含め、PDF/AとPDF/UAの適合をより広く見渡す話はPDF/AとPDF/UAのプリフライト手順で扱っています。請求書にアクセシビリティも求められるなら、PDF/A-3aとタグ付きPDFが依拠する構造ツリーはタグ付きPDFのアクセシビリティの記事の主題です。ここで説明した拡張スキーマの扱いは、このブログ全体で解説しているFactur-X、ZUGFeRD、XRechnungのプロファイル対応とともにPDF Library for DelphiのDelphi向けPDFライブラリの一部として出荷されています