技術記事

PDFlibPas による Delphi の PDF/X-1a、X-3、X-4 出力

PDFlibPas は 1 回の呼び出し、TPDFlib.SetPDFXMode で、Delphi から PDF/X 印刷ファイルを生成します。この呼び出しは PDF/X-1a:2003、PDF/X-3:2003、PDF/X-4 のいずれかを選択し、XMP の pdfxid 識別情報を書き込み、GTS_PDFX の出力インテントを追加し、/Trapped /False を宣言し、PDF バージョンを固定し、以後は選択したパートが禁じる描画呼び出しを拒否します。保存後のファイルは test 3 を指定した CheckFileCompliance で監査します

これが短い答えです。長い答えが重要なのは、印刷会社がファイルを醜いから拒否するのではなく、プリフライトで拒否するからです。たいてい金曜日に、たいてい生成コードが事前に捕まえられたはずの理由で。X-1a ジョブの中の RGB ロゴ、埋め込まずに参照されただけのフォント、仕上がりサイズのないページ。PDF/X はこうした失敗を視覚的な問題ではなく構造的な問題にするために存在し、ライブラリ側の仕事は、そもそもそうしたファイルを作らせないことです

Delphi アプリケーションはどの PDF/X パートを目標にすべきか

受け側のワークフローが特に指定しないなら PDF/X-4 を選び、指定してくるなら PDF/X-1a を選びます。3 つのパートの違いは、色、透過、ベース PDF バージョンのちょうど 3 つの軸に集約され、各軸は PDFlibPas 内部のゲートに対応します

  • PDF/X-1a:2003(ISO 15930-4): PDF 1.4 構文で、CMYK、グレー、スポットカラーのみ。ICC ベースの色は不可、透過も不可。ファイルはすでに印刷色へ分解されているため、印刷側は色変換をまったく行いません
  • PDF/X-3:2003(ISO 15930-6): やはり PDF 1.4 で透過は不可のままですが、カラーマネージドの色空間は許容され、出力インテント自体が RGB 条件を記述していればデバイス RGB も受け入れられます
  • PDF/X-4(ISO 15930-7): PDF 1.6 構文で、生きた透過とオプショナルコンテンツ(レイヤー)を扱えます。現代の RIP が対応するために作られているのがこのパートです。昔の X-1a の苦痛の多くは、出力の直前で透過をフラット化するところから来ていました

広告入稿や、考えずにそのままイメージングできるファイルを求める古い印刷ワークフローでは、X-1a が今も至るところにあります。印刷会社が受け付けてくれるなら、X-4 のほうがエンジニアリング上の正解です。透過はそのまま生き残り、色の判断は自分のコードではなく、実際の印刷プロファイルを持つ RIP 側で行われます。モード番号は順に 1、2、3 で、GetInformation(203) が現在有効なモードを返します。PDFlibPas は PDF 2.0 上の PDF/X-6 ファミリー向けに、より大きなモード番号も定義していますが、こちらはバージョンと出力インテントのルールが異なり、本記事の範囲外です

SetPDFXMode がファイルに書き込むもの

TPDFlib.SetPDFXMode は、PDF/X リーダーが最初に確認する識別情報と定型部分を処理してくれるため、アプリケーションコードが用意するのは実際の印刷プロファイルとコンテンツだけです。具体的に、成功した呼び出しは 4 つのことを行います

  • エディション文字列を XMP へ pdfxid:GTS_PDFXVersion として書き込む(PDF/X-1a:2003、PDF/X-3:2003、PDF/X-4 のいずれか)。2003 年版はこの文字列だけでパートを完全に識別するため、別途の適合プロパティは書き込まれません
  • 文書に出力インテントがなければ追加し、そのサブタイプを /S /GTS_PDFX に設定する。PDF/X リーダーが探すサブタイプはこれです(出力インテント辞書は ISO 32000-1 §14.11.5 で定義されています)
  • 文書情報辞書に /Trapped /False を設定する。このライブラリはトラッピングを行わず、PDF/X はトラッピング状態を不明のままにせず宣言することを要求するからです
  • バージョンのベースを固定する。X-1a と X-3 は 1.4、X-4 は 1.6 です

保存時にさらに 2 つの修正が行われます。TrimBox のないすべてのページには MediaBox と等しい TrimBox が与えられます。PDF/X は各ページに宣言された仕上がりサイズを要求するからです(ページ境界は ISO 32000-1 §14.11.2 が扱います)。さらに注釈には、規格が期待する印刷フラグが付きます。この TrimBox のフォールバックは安全網であってレイアウトの判断ではありません。フルブリードのジョブには、より大きなメディアボックスの内側に本物のトリムボックスが必要であり、PDFlibPas の TrimBox、BleedBox、CropBox デフォルトに関するガイドで、これらのボックスの設定方法と、継承がどう裏切るかを解説しています

uses
  PDFlibrary;

procedure BuildX1aAd(const OutFile, PressProfile: string);
var
  Pdf: TPDFlib;
begin
  Pdf := TPDFlib.Create;
  try
    if Pdf.SetPDFXMode(1) <> 1 then          // 1 = PDF/X-1a:2003
      raise Exception.CreateFmt('PDF/X mode refused (error %d)',
        [Pdf.LastErrorCode]);

    // 既定のプロファイルを実際の印刷条件に置き換える
    if Pdf.LoadOutputIntentProfile(PressProfile, 'DeviceCMYK') <> 1 then
      raise Exception.Create('Could not load the CMYK output profile');

    // Embed = 1 はフォントプログラム全体を埋め込む。PDF/X は 0 を受け付けない
    if Pdf.AddTrueTypeFont('Arial {1252}', 1) = 0 then
      raise Exception.Create('Font could not be loaded for embedding');

    Pdf.SetTextSize(24);
    Pdf.SetFillColorCMYK(0, 1, 1, 0);           // プロセスレッド。RGB は使わない
    Pdf.DrawText(72, 720, 'Spring catalog, page 1');

    Pdf.SaveToFile(OutFile);
  finally
    Pdf.Free;
  end;
end;

このスケッチの順序は意図的です。LoadOutputIntentProfile は既に存在する出力インテントのプロファイルを置き換えるものなので、SetPDFXMode がインテントを作った後に実行しなければなりません。この呼び出しは無からインテントを生み出すこともできません。出力インテントのない文書では単純に 0 を返します

既定の出力インテントを置き換える必要がある理由

SetPDFXMode がモード 1 から 3 に対して追加する出力インテントは DeviceCMYK を宣言しますが、その裏にある ICC バイト列は、ライブラリ同梱の sRGB プロファイルのコンポーネント数を 4 に書き換えただけのものです。この既定値は後方互換のために存在し、ファイルを構造上 PDF/X にするには十分ですが、埋め込まれたプロファイルはどの印刷機も記述していません。プロファイルヘッダを検査するプリフライトツールは、CMYK のラベルを貼った RGB プロファイルを見ることになり、校正のためにインテントを信用する印刷屋はデタラメを受け取ります

したがって、本番の印刷業務では LoadOutputIntentProfile(FileName, 'DeviceCMYK') を必須と考え、印刷業者がその紙と印刷機のために指定する ICC プロファイルを渡します。TPDFlib.LoadOutputIntentProfile は DestOutputProfile ストリームを差し替えて /N を更新し、インテント辞書の残りの部分は書き込み時のままにします。特に、既定インテントの出力コンディション識別子と説明的な文字列は sRGB 風の値のままです。自分の印刷会社が埋め込みプロファイルではなく識別子で照合するなら、出荷前に何を確認しているのか業者に確かめてください。これは 2003 年代の既定値に実在する隙間であって、自分のコードの誤りではありません

落とし穴その 1: 選んでいないバージョン

新しい PDFlibPas 文書はライター既定の PDF 1.5 で始まります。これは PDF/X-1a と PDF/X-3 が要求する 1.4 のベースより上です。通常のケースでは TPDFlib.SetPDFXMode がこれを黙って解決します。保存バージョンがロックされていなければ、文書バージョンをパートのベースまで下げて処理を続けます。最初の実装は拒否する側でした。新規文書に対する SetPDFXMode(1) はすべて 0 を返します。紙の上では正しく、実務では役に立たないルールの見本のような話です

拒否が正しい唯一のケースでは、今も拒否が残っています。先に何かがベースより上のバージョンで LockSaveVersion を呼んでいた場合、SetPDFXMode は LastErrorCode 601 とともに 0 を返します。呼び出し側が意図的にバージョンを固定したのであり、ライブラリがそれを覆さないためです。共通の文書セットアップコードの中で、無関係な理由からヘルパーがバージョンをロックしている場合、X-1a ジョブが死ぬのはまさにここです

Pdf := TPDFlib.Create;
try
  Pdf.LockSaveVersion('1.7');     // 汎用の「社内デフォルト」ヘルパーが設定
  if Pdf.SetPDFXMode(1) = 0 then
    // LastErrorCode = 601: 1.7 がロック済み、PDF/X-1a には 1.4 が必要。
    // 印刷ジョブではロックを飛ばすこと。PDF/X-4 も逃げ道にならない:
    // ベースの 1.6 も 1.7 のロックより下で、同じように失敗する。
    LogError('PDF/X refused, error %d', [Pdf.LastErrorCode]);
finally
  Pdf.Free;
end;

同じゲートが、関連する制限も説明します。PDF/X モードは文書作成時のスイッチです。SetPDFXMode は新規文書に対しては機能しますが、ファイルから読み込んだ文書に対しては 0 を返します。ライブラリは自分が書いていないコンテンツについて保証できないからです。既存の PDF を PDF/X へ変換するのは、検査と修復を要する別の仕事であって、フラグ 1 つの話ではありません

落とし穴その 2: 黙って拒否するゲート

PDF/X モードが有効になると、PDFlibPas は選択したパートが禁じるコンテンツ呼び出しを、その場で拒否します。後からファイルがプリフライトで落ちるのに任せたりしません。拒否の対象は、デバイス RGB の塗りと線(X-1a はすべて、X-3 と X-4 は出力インテントが RGB でない場合)、X-1a での ICC ベースの色、X-1a と X-3 での透過とブレンドモード、X-1a と X-3 でのオプショナルコンテンツ、JavaScript アクション、添付ファイル、LZW 画像フィルタ、埋め込まれない Standard 14 フォント、公開鍵暗号です。Encrypt によるパスワード暗号化は作成時にはゲートの対象外ですが、バリデータは暗号化されたファイルすべてにフラグを立てるため、印刷ジョブは暗号化しないままにします

拒否が共有していないのはエラーコードで、ここが罠です。SetFillColor、SetLineColor、ICC ベースの色呼び出し、EncryptPublicKey の色空間ゲートは LastErrorCode を 601 に設定します。PDF/A ゲートも使うライブラリの適合コードです。一方、透過、JavaScript、添付、LZW、AddStandardFont の各ゲートは単に 0 を返し、LastErrorCode をそのままにします。以前の呼び出しの値が残ったままになり得るということです。描画呼び出しの一括処理の後に LastErrorCode だけを確認するコードは、拒否の半分を見逃します。戻り値を 1 つずつ確認してください

procedure Require(Ok: Integer; const What: string);
begin
  if Ok = 0 then
    raise EPrintConformance.CreateFmt('%s refused under PDF/X', [What]);
end;

Require(Pdf.SetFillColorCMYK(0, 0, 0, 1), 'CMYK fill');
Require(Pdf.SetTransparency(40), 'transparency');   // X-1a / X-3 では 0
Require(Pdf.AddLinkToJavaScript(72, 72, 120, 20,
  'app.alert(1)', 0), 'JavaScript link');           // どのパートでも 0

フォントには独自の警告が必要です。AddStandardFont は PDF/X の下では 0 を返します。ライブラリは Standard 14 のアウトラインを同梱しておらず、埋め込めないからです。AddTrueTypeFont は異なります。PDF/A モードでは Embed = 0 を自動的に完全埋め込みへ格上げしてくれますが、PDF/X モードではそうしません。参照されたシステムフォントがそのままファイルに入り、それを捕まえるのはバリデータだけです。1(完全)か 2(サブセット)を自分で渡すか、セットアップ時に SetEmbedAllFonts(1) を 1 回呼んで、うっかりした 0 がすり抜けないようにしてください

印刷会社へ渡す前に PDF/X ファイルをどう検証するか

保存後のファイルに対して ComplianceTest = 3 を指定して CheckFileCompliance を実行します。TPDFlib.CheckFileCompliance は、指摘を 1 件も記録しなければ 0 を返し、そうでなければ PDFX- を接頭辞とする行を項目に持つ文字列リストハンドルを返します。PDF/X 監査は、PDFlibPas の PDF/A と PDF/UA プリフライトと同じストリーミングリーダーでファイルを読み、印刷プリフライトが最初に確認するルールを検査します

  • ファイルが暗号化されておらず、XMP が既知の GTS_PDFXVersion を運んでおり、PDF バージョンがパートのベースを超えていないこと
  • 出力インテントがちょうど 1 つ存在し、そのサブタイプが GTS_PDFX で、出力先プロファイルと出力コンディション識別子を運んでいること
  • 情報辞書が /Trapped を宣言していること
  • すべてのページが MediaBox の内側にある TrimBox を持ち、BleedBox がある場合はそれが TrimBox を包んでいること
  • ページで使われているすべてのフォントが埋め込みプログラムを持ち、Type0 フォントは子孫フォントを通じて確認されること
  • 禁止された注釈タイプ(RichMedia、Screen、3D、Movie、Sound、FileAttachment)がなく、JavaScript のオープンアクションや文書レベル JavaScript がなく、添付ファイルがなく、オブジェクトグラフのどこにも LZWDecode フィルタがないこと
function PreflightPDFX(Pdf: TPDFlib; const FileName: string;
  Report: TStrings): Boolean;
var
  ListId, I: Integer;
begin
  ListId := Pdf.CheckFileCompliance(FileName, '', 3, 0);  // 3 = PDF/X
  Result := ListId = 0;
  if Result then
    Exit;
  try
    for I := 1 to Pdf.GetStringListCount(ListId) do       // 項目は 1 始まり
      Report.Add(Pdf.GetStringListItem(ListId, I));
  finally
    Pdf.ReleaseStringList(ListId);
  end;
end;

この合格が何を意味するのかは、はっきりさせておくべきです。PDFlibPas のバリデータは上に挙げた構造ルールを監査します。ジェネレータのミスを捕まえるのはまさにこの層です。しかし色再現の正確なプリフライトではありません。総インキ量、画像解像度、埋め込みプロファイルが印刷業者の運用条件と一致するかどうかは確認しません。ビルドやエクスポートのパイプラインのゲートとして使い、印刷機固有のチェックは業者のプリフライトプロファイルに任せてください。どのみち業者はそちらを実行します

FAQ

PDFlibPas は PDF/X-4 の透過に対応しているのか

はい。SetPDFXMode(3) の下では、PDFlibPas は透過、ブレンドモード、オプショナルコンテンツグループを許可し、PDF 1.6 のファイルを書き込みます。同じ呼び出しでも PDF/X-1a と PDF/X-3 の下では 0 を返します

PDFlibPas は既存の PDF を PDF/X へ変換できるのか

いいえ。SetPDFXMode は現在の TPDFlib セッションで作成した文書にのみ適用され、読み込んだファイルに対しては 0 を返します。既存の PDF であっても、CheckFileCompliance と test 3 で監査することはできます

拒否された PDF/X 呼び出しはどんなエラーコードを返すのか

色空間と公開鍵暗号の拒否は LastErrorCode を 601 に設定します。透過、JavaScript、Standard 14 フォントといったその他のゲートは 0 を返すだけなので、必ず関数の戻り値を確認してください

PDF/X 出力、PDF/X バリデータ、その他の印刷とアーカイブのツールセットは、Delphi 向けの losLab ネイティブ Object Pascal PDF ライブラリである PDFlibPas の一部です。エディション、対応プラットフォーム、完全な API リファレンスについてはPDFlibPas Delphi PDF Library の製品ページを参照してください