技術記事

HotPDF の PDF/X-1a カラーチェックで RGB コンテンツを捕まえる

PDFXCompliance を 'X-1a' に設定すると、HotPDF はジョブの最後でコンテンツカラーを検証します。EndDoc が THotPDF.EnsurePDFX1aContentColorSpaces を呼び、任意のページ上の DeviceRGB、CalRGB、ICCBased RGB コンテンツをすべて拒否し、問題のリソースを名指す例外を投げます。印刷所に突き返される PDF/X-1a 文書が書き出されることはありません

最後の部分こそが本題です。迷子の RGB ロゴを 1 つ抱えた PDF/X-1a ファイルは画面上では完璧に見え、通り一遍のレビューも通過し、そして版下の 2 日前に印刷所のプリフライトで落ちます。Delphi のビルドの中で、どのページのどの画像かを教えてくれるメッセージ付きで捕まえるほうが、怒った入稿担当者から知らされるよりずっと安上がりです

PDF/X-1a はなぜ RGB コンテンツを拒否するのか

PDF/X-1a が RGB を拒むのは、この形式が「すべての色は 1 つの特定の CMYK 印刷条件に対してすでに確定済み」と約束するからです。ISO 15930-1:2001 §6.2.3 はコンテンツカラーを DeviceCMYK、DeviceGray、スポットカラー原色に限定します。つまり代替が CMYK か Gray である Separation と DeviceN スペースです。ファイルに埋め込まれた GTS_PDFX の OutputIntent がその印刷条件(たとえば FOGRA39 コートオフセット)を名乗り、受け側のワークフローは、色変換なしに数値をそのまま版へ送ってよいことになります

RGB はこの契約を破ります。RGB 値はインクの処方ではなく、ディスプレイや作業用スペースを基準にした色を記述するため、下流の誰かが分割方法を決めなければなりません。X-1a はまさにその決定を受取側から奪うために存在します。ISO 32000-1 §8.6 は PDF のカラースペースをデバイス系、CIE ベース系、特殊系に分類し、RGB のメンバーは複数の系にまたがります。DeviceRGB はデバイススペース、3 コンポーネントの CalRGB と ICCBased は CIE ベース、そして Indexed や Separation スペースは RGB の基底や代替を下に隠せます。rg 演算子だけを探す検査では、その大半を見逃します。後発の PDF/X-1a:2003(ISO 15930-4)も同じ色規則を保ちます。なお HotPDF の 'X-1a' 値が XMP パケットに書き込む識別は PDF/X-1a:2001 のものです

コンテンツが本当に RGB であり、それが正しいなら、X-1a は誤った目標です。HotPDF はこの検査を 'X-1a' だけに限定しているため、ICC ベースや校正色を許す 'X-3' と 'X-4' は影響を受けません

EnsurePDFX1aContentColorSpaces が実際に検査するもの

EnsurePDFX1aContentColorSpaces は描画時フラグと、全ページのリソースをたどる再帰的な探索を組み合わせ、PDF/X ゲートの最後に走ります。EndDoc はまず一般的な PDF/X 要件を強制します。Title の設定、Trapped が 'True'、'False'、'Unknown' のいずれかであること、GTS_PDFX の OutputIntent の存在(EnsurePDFXOutputIntent)、全ページへの TrimBox か ArtBox の存在(EnsurePDFXPageBoxes)です。これらを通った後にだけ、カラーチェックが走ります

Delphi の HotPDF EndDoc 内における PDF/X ゲートの順序。メタデータ、OutputIntent、ページボックスを先に検査し、その後に RGB コンテンツカラーチェックが走る
カラーチェックは X-1a ゲートの最後に走り、描画時フラグがあるため、適合指定より前に描かれた RGB も捕まります

1 つ目の経路は文書全体のフラグです。HotPDF の RGB カラーセッタ、ビットマップ画像の埋め込み、注釈カラーの記録は、描いている間に DeviceRGB の使用を記録します。リソース辞書に現れないインラインの rg と RG 演算子もこれでカバーされます。2 つ目の経路は各ページの /Resources をたどり、次を検査します

  • /ColorSpace 内の名前付きカラースペース。インライン画像の省略記法 RGB(DeviceRGB の同義語)も含みます
  • すべての Image XObject の /ColorSpace
  • Form XObject とタイルパターン。各オブジェクトのネストした /Resources へ再帰してたどります
  • シェーディングパターンとページの /Shading 辞書のエントリ。シェーディングの /ColorSpace を通じて確認します
  • Indexed の基底スペース、Separation と DeviceN の代替、パターンの基底スペース。ネスト深さ 8 までたどります

拒否されるのは 3 つです。DeviceRGB、CalRGB、そしてプロファイルストリームが N = 3 を宣言する ICCBased スペースです。Form XObject とパターンの間で共有されるリソース辞書は 1 度だけ訪問されるため、互いに参照し合うフォーム集合が探索をループに落とすことはありません。このメソッドは THotPDF 上で公開されていますが、通常は自分で呼ぶことはありません。EndDoc が代わりに呼ぶからです

PDF ページリソースの中で RGB が隠れる場所。ネストしたフォーム、パターン、シェーディング、代替が Delphi の HotPDF の探索を DeviceRGB、CalRGB、ICCBased RGB へ導く
探索はネストしたリソースを深さ 8 までたどり、DeviceRGB、CalRGB、N = 3 の ICCBased スペースを現れた場所で拒否します

検査を通る X-1a ページを Delphi で描く

適合する HotPDF の X-1a 文書には、CMYK の OutputIntent、各ページの TrimBox、そして CMYK、Gray、スポットの API を通じてだけ設定された色が要ります。下の骨組みは FOGRA39 プロファイルを使います。AddPDFXOutputIntent は出力条件識別子、情報文字列、ICC プロファイルストリーム、そのコンポーネント数、代替スペースを受け取ります

var
  Pdf: THotPDF;
  Profile: TFileStream;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'brochure-x1a.pdf';
    Pdf.Title := 'Spring brochure, press edition';
    Pdf.Trapped := 'False';
    Pdf.PDFXCompliance := 'X-1a';
    Pdf.BeginDoc;

    Profile := TFileStream.Create('CoatedFOGRA39.icc', fmOpenRead or fmShareDenyWrite);
    try
      Pdf.AddPDFXOutputIntent('FOGRA39 (ISO 12647-2:2004)',
        'Coated FOGRA39 offset', Profile, 4, 'DeviceCMYK');
    finally
      Profile.Free;
    end;
    Pdf.CurrentPage.SetTrimBox(0, 0, 612, 792);   // 全ページで必須

    Pdf.CurrentPage.SetCMYKFillColor(0, 0.5, 0.8, 0.1);
    Pdf.CurrentPage.Rectangle(72, 600, 468, 120);
    Pdf.CurrentPage.Fill;

    Pdf.CurrentPage.SetGrayFillColor(0.2);         // DeviceGray のテキスト
    Pdf.CurrentPage.SetFont('Arial', [], 14);
    Pdf.CurrentPage.TextOut(72, 560, 0, 'Spring catalog, press edition');
    Pdf.EndDoc;                                    // ここでカラーチェックが走る
  finally
    Pdf.Free;
  end;
end;

この文脈では、TColor ベースの呼び出しすべてが罠です。SetRGBFillColor、SetRGBStrokeColor、SetRGBColor はいずれも GDI カラーを受け取り DeviceRGB を出力するため、画面向けレポートのために何年も昔に書かれたヘルパーは、再利用された瞬間に印刷ジョブを汚します。0 から 1 の範囲のコンポーネントで SetCMYKFillColor、SetCMYKStrokeColor、SetCMYKColor を使うか、Gray の対応物を使ってください

スポットカラーと CMYK グラデーション

スポットカラー原色は、代替が CMYK か Gray である限り X-1a で認められます。RegisterSeparation は Separation スペースを組み立ててリソース名を返し、RegisterAxialGradient は渡すコンポーネント数からシェーディングのカラースペースを選びます。4 コンポーネントは DeviceCMYK を意味します

var
  SpotCS, Ramp: AnsiString;
begin
  // 概算の CMYK ビルドを代替に持つスポットインク
  SpotCS := Pdf.RegisterSeparation('Corporate Red', 'DeviceCMYK',
    [0, 1, 0.8, 0.05]);
  Pdf.CurrentPage.SetFillColorSpace(SpotCS);
  Pdf.CurrentPage.SetFillColor([1.0]);           // 100% ティント
  Pdf.CurrentPage.Rectangle(72, 420, 200, 80);
  Pdf.CurrentPage.Fill;

  // ステップごとに 4 コンポーネント:シェーディングは DeviceRGB ではなく DeviceCMYK
  Ramp := Pdf.RegisterAxialGradient(72, 300, 540, 300,
    [0, 0, 0, 0], [1, 0.6, 0, 0.2]);
  Pdf.CurrentPage.SetFillPattern(Ramp);
  Pdf.CurrentPage.Rectangle(72, 260, 468, 80);
  Pdf.CurrentPage.Fill;
end;

同じグラデーション呼び出しに 3 コンポーネントを渡すと DeviceRGB のシェーディングになり、リソース探索はシェーディングパターンの名前でこれを拒否します。代替に 'DeviceRGB' を登録した Separation も同じです。スポット名は無害に見えますが、探索は代替をたどって下に RGB を見つけます。ビューアや RIP が代替へフォールバックしたときのスポットインクの合成は、Delphi での Separation と DeviceN スポットカラーのレンダリングの記事で扱っています

ラスター画像にも同じ規律が要ります。HotPDF は Flate 圧縮ビットマップを DeviceRGB に固定する一方、icJpeg で埋め込んだ JPEG は自身のコンポーネント数を保ちます。つまり 4 コンポーネントの CMYK JPEG やグレースケール JPEG は良くて、普通の RGB JPEG は駄目です。アートワークは PDF に届く前に、イメージングパイプライン内で印刷用プロファイルへ変換してください

なぜ /DefaultRGB は PDF/X-1a ファイルを救えないのか

/DefaultRGB が X-1a の助けにならないのは、規則が「プレスが受け取るもの」について語っており、「RGB が特性付けされているか」についてではないからです。PDF/A では HotPDF は RegisterDefaultColorSpace('DefaultRGB', ...) に DeviceRGB の使用を吸収させます。Default カラースペースが DeviceRGB を ICC プロファイルへマップした途端、コンテンツはデバイス非依存になり、PDF/A の整合性検査は受け入れます。そのワークフローを経験した開発者は、ここでも同じ手を使おうとしがちです

EnsurePDFX1aContentColorSpaces はそのマッピングを意図的に無視します。特性付けされた RGB 値はそれでも RGB であり、X-1a の OutputIntent は CMYK の印刷条件なので、誰かが変換しなければなりません。アーカイブ用と印刷用の 2 つのプロファイルの違いを 1 つだけ覚えるなら、これです。PDF/A は色が曖昧でないかを問い、PDF/X-1a は色がすでにインクであるかを問います

描画順序と、カウントされないページセットアップ

検査はすでに書かれたコンテンツに対して EndDoc で走るため、セットアップの順序は問われません。PDFXCompliance の割り当て前や、OutputIntent の登録前に描かれた RGB も、やはり捕まります。ページ構築を共有ルーチンで行い、出力プロファイルを後になってから、ときには文書の半分が存在した後でユーザー設定から決めるコードでは、これが効いてきます

Pdf.BeginDoc;
// ... OutputIntent と TrimBox は前と同じ ...
Pdf.CurrentPage.SetRGBFillColor(clRed);   // レガシーヘルパー、DeviceRGB を出力
Pdf.CurrentPage.Rectangle(72, 72, 200, 100);
Pdf.CurrentPage.Fill;

Pdf.PDFXCompliance := 'X-1a';             // 描画後に決定
Pdf.EndDoc;                               // やはり例外:DeviceRGB が記録されている

描画時フラグには、自分で似た検査を作るなら知っておきたい罠が 1 つあります。新しいページは SetRGBColor(0) による黒いグラフィックスステートで初期化され、これは 0 0 0 rg と 0 0 0 RG のペアを出力します。このページセットアップの定型が DeviceRGB フラグを立ててしまうと、この検査にとっても PDF/A のカラーチェックにとっても、すべての文書が RGB 汚染に見えることになります。HotPDF はその初期化の前後でフラグを保存・復元するため、実際に描いた色だけが数えられます

EndDoc が例外を投げたら、RGB コンテンツはどう探すのか

例外メッセージの冒頭にある位置情報を読んでください。どちらの経路が問題を見つけたのかが分かります。メッセージは PDFXCompliance 'X-1a': <where> uses <what> の形をとり、ISO 15930-1 の参照と、CMYK か Gray へ変換するか X-3 か X-4 に切り替えるようにという助言が続きます。バリデータは最初の違反で素の Exception を投げるため、例外クラスではなくテキストでマッチしてください

Delphi における HotPDF EndDoc の RGB 例外の解剖。描画時フラグは文書全体の使用を報告し、リソース探索は正確なページとキーを名指す
メッセージ冒頭の位置情報を読みます。文書全体の報告なら描画コードへ、ページとリソース名なら 1 つのキーへ誘導されます
  • document content uses DeviceRGB (rg/RG operators, RGB images or annotation colours recorded during drawing) は描画時フラグが発火したことを意味します。ページ番号はありません。フラグは文書全体のもので、探索の前に検査されるからです。描画コード内の SetRGB* 呼び出し、Flate 圧縮ビットマップ、RGB JPEG、TColor の注釈カラーを探してください
  • page 3 image XObject /Im2 uses DeviceRGB content や page 1 shading Pattern /P1 uses DeviceRGB content はリソース探索由来で、正確なリソースキーを指します
  • ColorSpace /... の位置に続く uses an ICCBased RGB colour space (N=3) は、RegisterICCProfile で sRGB プロファイルを登録し SetFillColorSpace で選択したときの典型的な結果です。描画時フラグが見えない経路です
  • ネストした位置は連鎖します。たとえば page 2 Form XObject /Fm1 ColorSpace /CS1 のように、ページから RGB スペースを運ぶリソースまで道をたどれます
try
  Pdf.EndDoc;
except
  on E: Exception do
  begin
    if Pos('X-1a', E.Message) > 0 then
      LogPrepressFailure(Pdf.FileName, E.Message);  // 自前のロガー
    raise;
  end;
end;

報告されるのは最初の違反だけなので、RGB の発生源を複数抱えたジョブの直しは反復になります。直し、ビルドし、次のメッセージを読む。バッチジョブではファイルごとにメッセージ全文をログへ残し、TColor をまだ受け付けるロゴビットマップやチャートヘルパーのような既知の犯人リストを短く持っておくと効きます

プリフライトの中での RGB ゲートの位置

EnsurePDFX1aContentColorSpaces はビルドの中で走る標的を絞った RGB ゲートであり、完全な PDF/X-1a プリフライトと組んで使うものであって、その置き換えではありません。RGB の各系統をカバーします。各ページの /Resources からのリソース探索と、HotPDF のカラーセッタがインラインの rg と RG 演算子のために管理する描画時フラグによるものです。ネストしたカラースペースは奇形サイクルへの防護として深さ 8 までたどられ、最初の違反が EndDoc ごとに報告されます。Lab や CalGray のような他の色系は完全なプリフライトの管轄なので、完成したファイルはビルドの一部として外部バリデータへ通し、完全な適合判定を得てください。適合出力の全体像はHotPDF による PDF/A、PDF/X、PDF/UA 出力のガイドに、バリデータの無人パイプラインへの組み込みはDelphi での PDF プリフライト検査の自動化にあります

HotPDF は Delphi と C++Builder 向けのネイティブ VCL PDF コンポーネントで、PDF/X、PDF/A、PDF/UA 出力、CMYK とスポットカラーの API、ICC OutputIntent 対応を備えます。詳細、ライセンス、トライアルダウンロードはHotPDF Delphi PDF component 製品ページにあります