技術記事

DelphiのHotPDFでPDF 2.0、PDF/A-4、PDF/UA-2を出力

PDF/A-3のチェックリストはたいてい、XMPパケットがpdfaid:partとpdfaid:conformanceの2つの識別エントリを運ばねばならないと言います。正しく書かれたPDF/A-4ファイルはpdfaid:partに4、pdfaid:revに2020を運び、ベースプロファイルには適合エントリが一切ありません。これを「直そう」と文字を足すとファイルは誤りになります。ISO 19005-4はベース部を番号とリビジョン年で識別し、文字を保つのは2つの拡張だけです

PDF/UA-2にも似た罠が1階層下、構造ツリーの中にあります。独自のロールを独自の名前空間で宣言すれば仕事は終わりに見えます。名前空間こそpart 2が加えた機能なのですから。それは半分です。https://example.com/ns/widgetsのWidgetGroupのようなロールは、適合チェッカーが受け入れる前に標準ロールへマップされなければならず、マッピングが欠けていても描画されたページのどこにもヒントはありません

PDFAComplianceでPDF/A-4を選ぶ

HotPDFではPDF/A-4プロファイルは既存のPDFAComplianceプロパティの3つの値です。ベース部の'4'、エンジニアリング文書の'4E'、埋め込みファイルを運ぶファイルの'4F'です。'4'を選ぶと、XMPパケットはpdfaid:partに4、pdfaid:revに2020を受け取り、pdfaid名前空間からはそれ以外に何も持ちません。'4E'と'4F'はEかFのpdfaid:conformanceを加えます。デコーダ関数も同じ区別を報告します。3つの値すべてでPDFAPartは4を返し、PDFAConformanceLevelはE、F、ベースプロファイルでは#0を返します

HotPDF の PDF/A-4 セレクター値マトリクス。共有 PDF 2.0 制約の下、適合文字なしの基本 4 と、4E エンジニアリング、4F 埋め込みファイルプロファイルを並列
素の '4' は part 4 rev 2020 を文字なしで書き込み、文字を保つのは 4E と 4F だけです

3つともPDF 2.0ヘッダーを強制します。アップグレードはEndDocの内部で起きるため、PDF 1.4で始まったドキュメントは別の呼び出しなしに2.0として出てきます。それを止める唯一のケースがStrictVersionLockです。このフラグは自動アップグレードを禁じるため、EndDocは、メタデータでpart 4を名乗る1.7ファイルを書く代わりに「PDF/A-4 requires PDF 2.0; StrictVersionLock prevents the version upgrade」を上げます。古い印刷ワークフローのためにバージョンを固定しているコードベースは、誰かがプロファイルを切り替えた最初の時にまさにこのメッセージを見ることになります

PDF/A-4の2つの規則が、トレーラーとCatalogに入るものを変えます。ISO 19005-4 §6.1.3は、CatalogがPieceInfoエントリを持つときに限り文書情報辞書を許し、その場合でもModDateに制限するため、HotPDFはpart 4ではInfo辞書を書かず、タイトルはXMPのdc:titleで旅をします。TitleとLangはそれでも必須です。どちらか空のままだとEndDocが上げます。AddPDFAOutputIntentで出力インテントが追加されていなくても上げられ、PDF/Aプロファイル選択中はEnableEncryptが実行を拒否します。暗号化されたアーカイブファイルは、標準が許さない矛盾なのです

PDF/A-4fと4eの下での関連ファイル

関連ファイルこそPDF/A-4fを選ぶ主理由です。描画されたページの背後のXML請求書、下流システムが解析するJSONペイロード。AddPDFAssociatedFileはファイル名、MIMEタイプ、説明、AFRelationship値、バイト列を受け取り、間接FileSpec辞書を返します。任意のキーを自分で添付できます

var
  Pdf: THotPDF;
  Icc: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  Icc := TFileStream.Create('sRGB2014.icc', fmOpenRead or fmShareDenyWrite);
  try
    Pdf.FileName := 'invoice-archive.pdf';
    Pdf.PDFACompliance := '4F';        // part 4、rev 2020、conformance F
    Pdf.Title := 'Invoice 2026-0731';  // XMP の dc:title のみに書かれる
    Pdf.Lang := 'en-US';
    Pdf.BeginDoc;
    Pdf.AddPDFAOutputIntent('sRGB IEC61966-2.1', 'sRGB IEC61966-2.1',
      Icc, 3, 'DeviceRGB');
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-0731');
    // LoadInvoiceBytes は TBytes を返す自前の関数
    Pdf.AddPDFAssociatedFile('invoice.xml', 'text/xml',
      'Structured invoice data', 'Data', LoadInvoiceBytes);
    Pdf.EndDoc;
  finally
    Icc.Free;
    Pdf.Free;
  end;
end;

各呼び出しは4つのことをします。/SubtypeにMIMEタイプをPDF名として収めたEmbeddedFileストリームを書き込みます。サイズ、更新日、MD5チェックサムを運ぶ/Params辞書付きです。/Fと/UFの両方、/Descとしての説明、リレーションシップ名を持つFileSpecを組み立てます。それからそのFileSpecを2箇所、Catalogの/AF配列とEmbeddedFilesの名前ツリーに登録します。両方の登録が必須です。1箇所だけに登録されたファイルは、ビューアの添付リストからはまだ開けるかもしれません。見た目の抜き取り検査がアーカイブについてほとんど何も証明しないのは、このためです

リレーションシップ文字列はSource、Data、Alternative、Supplement、Unspecifiedのどれかでなければならず、他の値は1バイトも書かれる前に上がります。空のMIMEタイプも同様です。プロファイルも検査されます。AddPDFAssociatedFileはPDF/A-3、PDF/A-4e、PDF/A-4fを受け入れ、ベースの'4'プロファイルは関連ファイルを認めないため上がります。逆方向では、AddDocumentAttachmentはPDF/A-3、4e、4fの下で実行を拒否します。素の添付には、これらのプロファイルが要求する/AF登録がないからです。AddPDFA3AssociatedFileに対してPDF/A-3向けに書かれたコードは、コンパイルし続け、同じ挙動を示します。AddPDFAssociatedFileはそのメソッドのプロファイル中立な名前です

2つの拡張は残りの権限を分け合います。'4F'の下では、EndDocがCatalogがEmbeddedFilesの名前ツリーを運ぶことを確認し、AddFileAttachmentAnnotationが受け入れられます。ベースプロファイルと4eは拒否します。'4E'の下では、Add3DAnnotationとAddRichMediaAnnotationに加え、SetOCGStateとGoTo3DViewアクションが許されます。他のすべてのPDF/Aプロファイルは4つとも拒否します

名前空間付きのカスタムロールがPDF/UA-2に失敗し続ける理由

カスタムロールがPDF/UA-2を通るのは、2つの条件が揃ったときだけです。/NSエントリが構造ツリールートに列挙された名前空間辞書を指すこと、そしてロールがその名前空間のRoleMapNSを通って標準名前空間のロールへ解決することです。HotPDFは仕事を3つのメソッドに分けます。RegisterStructureNamespaceは間接の/Type /Namespace辞書を作り、StructTreeRootの/Namespacesに加えて返します。同じURIでの2度目の呼び出しは、新しい辞書を加える代わりに既存の辞書を返します。AddStructureElementNSは要素を作り/NSを設定し、ついでに名前空間を登録します。AddStructRoleMapNSがマッピングを書きます

フラットな PDF/UA-1 ロール語彙と、Delphi で RegisterStructureNamespace と AddStructureElementNS を通じて束縛される PDF/UA-2 名前空間の対比図
1 つの URI は常に単一の名前空間辞書を再利用します。重複した辞書の山はありません
const
  PDF2NS   = 'http://iso.org/pdf2/ssn';
  WidgetNS = 'https://example.com/ns/widgets';
var
  DocElem, Group: THPDFDictionaryObject;
begin
  Pdf.PDFUACompliance := True;
  Pdf.PDFUAPart := 2;                // PDF/UA-2 は PDF 2.0 を要求する
  Pdf.Title := 'Widget catalog';     // 空の Title は EndDoc で上がる
  Pdf.Lang := 'en-US';
  Pdf.BeginDoc;

  DocElem := Pdf.AddStructureElement('Document', nil);  // PDF2NS に束縛される
  Group := Pdf.AddStructureElementNS('WidgetGroup', WidgetNS, DocElem);
  // このマッピングがないと EndDoc が ua2.rolemap.unresolved を上げる
  Pdf.AddStructRoleMapNS('WidgetGroup', WidgetNS, 'Div', PDF2NS);

  Pdf.CurrentPage.SetFont('Arial', [], 18);
  Pdf.CurrentPage.BeginMarkedContentMCID('H1', 0);
  Pdf.CurrentPage.TextOut(50, 760, 0, 'Widgets');
  Pdf.CurrentPage.EndMarkedContent;
  Pdf.AddStructureElement('H1', Group, 0, 0);  // ページインデックス 0、MCID 0
  Pdf.EndDoc;
end;

この一覧の一部は、頼まれていないのに起こります。PDFUAPartが2に設定されると、AddStructureElement('Document', nil)は要素をPDF 2.0標準名前空間のhttp://iso.org/pdf2/ssnへ束縛します。PDF/UA-2ツリーのルートはあの名前空間のDocumentでなければならないからです。PDF 2.0語彙のロールはすべて同じ束縛を受けるため、H1やPに名前空間引数は要りません。PDF 1.7集合にしか存在しないロール、たとえばNote、Code、BlockQuoteは1.7名前空間に留まります

マッピング先は実際の選択です。AddStructRoleMapNSに空のターゲット名前空間を渡すと短い形式、PDF 1.7集合に対して解決される素のロール名が書かれます。PDF 2.0のURIを渡すと2項の[role namespace]形式が書かれます。PDF/UA-2ドキュメントではPDF 2.0名前空間へマップしてください。ツリー全体が1つの語彙に解決され、1.7に対抗物のないAsideやFENoteのようなターゲットが使えるようになります。EndDocでその場で拒否されるマッピングの形が2つあります。自分自身へ折り返す連鎖と、明示的な名前空間内のロールが同じ名前空間内の別のロールへマップされる形です

呼び出し順序は見た目より自由です。マッピングは要素の存在の前でも後でも追加できます。ツリーが完成するまで何も検査しないからです。即座に失敗するのは、PDFUAPartが1のときのAddStructureElementNSです。Part 1には名前空間がなく、この呼び出しはその場で「structure namespaces require PDFUAPart=2」を上げます

ファイルが書かれる前にEndDocが検査すること

PDFUAComplianceがオンでPDFUAPartが2のとき、EndDocは何かを直列化する前に完全なPDF/UA-2構造検証を実行し、最初のエラーが例外になります。その検査の4つがツリーの骨格を守ります。ルートはちょうど1つのトップレベルDocument要素を保持しなければなりません。すべての名前空間辞書は間接で、型がNamespaceで、1度しか現れない空でないURIを運ばなければなりません。すべての/NS参照は、ルートの/Namespaces配列に列挙された辞書へ解決されなければなりません。そしてすべてのロールは、直接にせよRoleMapとRoleMapNSを通ってにせよ、標準名前空間へ解決されなければなりません。Document要素から到達できない要素も報告されます

HotPDF が EndDoc で確認する 4 つの構造ツリーゲートのフロー。単一 Document ルート、清浄な名前空間ディクショナリ、解決可能な NS 参照、標準またはマップ済みロール
ゲートをこれより早い段階で捕まえると、正しい中間状態まで拒否してしまいます

同じパスがドキュメントレベルの要件もカバーします。空でない有効なLang、trueに設定されたMarkInfo /Marked、オンのDisplayDocTitle、空でないdc:titleとpdfuaid:part 2を持つXMPパケットです。PDF/UAがオンになると、HotPDFはマーク済みフラグ、ビューア設定、XMP識別を自分で設定し、part番号の隣にpdfuaid:rev 2024を書きます。タイトルと言語はユーザーの供給物で、どちらか空だと検証器が始動する前に上がります。見出し、リスト、表、注釈、フォームも同じ実行で検査されます

EndDocまで待つのは意図的です。生成の途中でツリーが不完全なのは正当な状態です。Document要素にまだ子がいない、名前空間がどの要素にも使われる前に登録されている、マッピングが要素の後に届く。呼び出しごとに検査していたら、これらの状態は拒否されていました。EndDocではツリーが完成してまだメモリにあり、例外は規則コードと、それを破った要素のパスを名指します

try
  Pdf.EndDoc;
except
  on E: Exception do
  begin
    // E.Message は次の形を持つ
    //   PDF/UA-2 validation failed [ua2.heading.skipped at ...]:
    //   H2 から H4 へ見出しレベルが飛んでいる
    LogBuildFailure(Pdf.FileName, E.Message);  // 自前のロガー
    raise;
  end;
end;

PDF/A-4出力は独自の検証器から同じ扱いを受けます。PDFAComplianceがpart 4の値を保持しているとき、EndDocは生成されたグラフをISO 19005-4に対して検査し、規則コードと条項付きで上がります。有効なリレーションシップのないFileSpecにはpdfa4.filespec.relationship、迷入したInfoキーにはpdfa4.trailer.info_entriesといった具合です。Enterpriseエディションは両検証器をValidatePDFUA2StructureとValidatePDFA4として公開し、最初のエラーで止まる代わりに、保持されたすべての診断を問題配列へ満たします。完全なリストを印刷すべきCIステップには、こちらが良い入口です

PDF 2.0のロールとH6を越える見出し

型付きのTHPDFStandardStructureType列挙型はsstDocumentFragment、sstAside、sstTitle、sstFENote、sstSub、sstEm、sstStrong、sstArtifactを獲得します。このうち3つは、普通のビジネス文書のタグ付けの仕方を変えます。Asideはサイドバーや引き抜き引用に、誤用されたSectではない居場所を与えます。FENoteは脚注と文末注を注としてマークし、スクリーンリーダーが文中の途中で読み上げる代わりに要求に応じて提示できるようにします。EmとStrongは強調を構造として運びます。PDF 1.7のツリーではSpanで包むことしかできなかったものです

見出しはPDF 2.0では上限がありません。列挙型はsstH6で止まりますが、AddStructureElementの文字列オーバーロードはH7、H8とその先を受け付けます。PDF 2.0集合は1以上の任意のnに対してHnを定義するからです。PDF 1.7はH6で止まり、深い技術文書にアウトラインの平坦化かレベルの再利用を強いました。標準文書、法令コード、部品カタログにとって、それだけでも出力をPDF 2.0へ動かす正当化になります

深い見出しにはより厳しい規則が付いてきます。EndDoc内のPDF/UA-2パスは見出しレベルの連続性を要求します。最初の見出しはH1でなければならず、H2からH4への跳びはua2.heading.skipped付きで上がります。階層が下から始めてよいのは、大きな文書から抜粋されたコンテンツのためのロールであるDocumentFragmentの中だけです。MarkdownやHTMLのアウトラインをHnロールへそのまま写すジェネレーターは、作者が見た目の大きさのために飛ばしたレベルをすべて引き継ぐため、タグ付けの前にアウトラインを正規化してください

実運用の出力をPDF 2.0へ切り替える前に

PDF 2.0はヘッダーの変更から始まり、ヘッダーこそ古い消費者が最初に読むものです。PDF 1.7までしか受け付けない取り込みサービス、印刷ワークフロー、基幹ビューアは、ファイルの残りがどれほど清浄でも%PDF-2.0で止まります。PDF/A-4プロファイルかPDF/UA part 2を選ぶと、頼んだ与否わらずPDF 2.0が選ばれます。切り替えの前に、後ではなく、消費する各システムを確認してください

最もリスクの低い展開は、自分で管理しない読み手へ出ていく文書にはPDF/A-3を保ち、取り込み側を自分が所有する社内アーカイブにはPDF/A-4fを使い、アクセシビリティポリシーが名指すところではPDF/UA-2を採用することです。StrictVersionLockは移行期の防波堤も兼ねます。1.7に留まらねばならないコード経路ではVersionをpdf17にしてロックを掛ければ、誤ったPDF/A-4選択は、開けないシステムへ届く代わりに、EndDocでメッセージ付きの失敗になります。PDF/A、PDF/X、PDF/UA検証とPDF/A-3上のZUGFeRDとFactur-Xハイブリッド請求書のガイドがバージョン番号より先に来るプロファイル決定を扱い、プリフライトレポートの自動化の解説は判定をビルドステップへ変える方法を見せます

これらすべては、DelphiとC++Builder向けコンポーネントの中でネイティブVCLコードとして走ります。PDF/A-4とPDF/UA-2の出力は実行ファイルと共に出荷され、外部エンジンを要りません。対応プロファイルとRAD Studioバージョンは、HotPDF Delphiコンポーネントのページに一覧があります