技術記事

DelphiでPDFを一つのカラースペースに変換する方法

1 0 0 rg、つまり純粋な赤で塗られた見出しは、グレイ変換からは0.299 gで戻ってきます。純粋な青は0.114 gに着地し、黒に十分近いので、ネイビーのロゴと本文は見分けにくくなります。この数字は丸めの事故ではありません。BT.601の輝度重みです。ビューアーがグレイデバイスにRGBページを表示するときに適用するのと同じ重みで、カラーパンフレットをグレイにしなければならないとき、どれだけのコントラストが生き残るかを決めます

RecolorDocumentが色ごとに計算するもの

PDF Library for Delphiは、この変換をTPDFlibのRecolorDocument(TargetSpace)として公開します。グレイが1、RGBが2、CMYKが3です。戻り値は変換した件数で、ジオメトリ、テキスト位置、画像の配置は元の場所に留まります。変わるのは色の値と、その周りのカラースペース名だけです

すべての色はターゲットへの途上でRGBを通ります。グレイは輝度の総和0.299 R + 0.587 G + 0.114 Bです。CMYKがRGBへ来るのはISO 32000-1 §10.3の素朴なデバイス則で、各チャネルは1からインクと黒の合計を引いたもの、ゼロにクランプされます。RGBがCMYKへ来るときは、最大成分を黒へ引き込むやり方です。K = 1 - max(R, G, B)で、残りのインクは残り物から縮尺されます。プリンターに何かを送る前に知っておくと良い結果がいくつかあります:

  • 0.4 gのグレイをCMYKへ変換すると0 0 0 0.6 k、黒インクだけになります。テキストと罫線にプレスが望むものです
  • RGB 1 0.5 0(オレンジ)は0 0.5 1 0 kになり、黒はまったく入りません
  • 0.6 0.4 0.4 1 kのようなリッチブラックは、グレイへの途上で0 gになります。余分なインクはただ消えるのです
  • 純粋なシアン、1 0 0 0 kは0.701 gに変わります。彩度の高いインクからほとんどの人が期待するより明るい

変換されたオペランドは、小数点以下3桁まで、しかも小数点区切りは常にピリオドで書かれます。ジョブを走らせるマシンのロケールが何であれ、です。ISO 32000-1 §7.3.3がコンテンツストリームに許すのはそれ以外何もなく、システムロケールで数字を整形するコンバーターは、ドイツのワークステーションで0,5 gと書きます。リーダーはそれを、数字0の後に未知のオペレーターが続いたものとしてパースします

var
  Lib: TPDFlib;
  Converted: Integer;
begin
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile('brochure.pdf', '') <> 1 then
      raise Exception.Create('Could not open brochure.pdf');
    Converted := Lib.RecolorDocument(1);   // 1 = グレイ、2 = RGB、3 = CMYK
    WriteLn(Format('%d colors, images and gradients converted', [Converted]));
    Lib.SaveToFile('brochure-gray.pdf');
  finally
    Lib.Free;
  end;
end;

すでにターゲットスペースにある色を持つページは、元のコンテンツバイトを保持します。RecolorDocumentが数えるのは、実際にスペースをまたぐ変換だけなので、すでにグレイのドキュメントは0を報告し、中身は何も書き直されません。大きなファイルではこれは効いてきます。入出力のdiffが、変わったページを正確に教えてくれるのです

なぜRecolorPageは表紙の写真をカラーのままにするのか

RecolorPageは選択されたページのカラーオペレーターを変換し、そこで止まります。画像とグラデーションはドキュメントレベルのオブジェクトで、しばしば複数ページに共有されており、これらを変換するのはRecolorDocumentだけです。だから表紙だけを変換する素朴なやり方、ページ1を選んでRecolorPage(3)を呼ぶやり方は、RGB写真の上にCMYKの見出しと罫線を載せて終わります

この分割は意図的なものです。表紙に描かれた画像XObjectは、ページ12にも描かれているかもしれません。あるページのために変換すれば、両方のために変換したことになります。静かに共有リソースへ手を伸ばすページスコープの呼び出しは、呼び出し側が選んでいないページを変えてしまいます。2つの驚きのうち、こちらの方がひどいのです。だからページ単位の呼び出しは、ページに属するものにだけ触れます

// オペレーターは表紙だけ。画像とグラデーションはここでは変換されない
Lib.SelectPage(1);
Changed := Lib.RecolorPage(3);
if Changed = 0 then
  WriteLn('Cover already uses CMYK operators (or the target was out of range)');
Lib.SaveToFile('cover-cmyk-operators.pdf');

// 全体を1つのスペースにしなければならないドキュメントでは、全部を変換する
Lib.RecolorDocument(3);
Lib.SaveToFile('all-cmyk.pdf');

このスニペットでは戻り値に注目してください。どちらの呼び出しも、ターゲットが1から3の外なら0を返します。そして0は、変更の必要がなかったページへの答えでもあります。ターゲットの打ち間違いは、すでに正しいドキュメントとまったく同じに見えるのです。呼び出しの前に、引数は自分で検証してください。RecolorDocumentは走る前に選択されていたページを復元するので、ページごとのジョブの途中で呼んで、後で再選択する必要はありません

スコープの細部がもう1つ。オペレーターのパスは各ページ自身のコンテンツストリーム、つまりページ自体に列挙された描画命令を読み、g、rg、kとそれらのストローク形、そして現在スペースがデバイススペースのときのsc/scnを変換します。/DeviceRGB csが変換されるscの前に来ていたら、スペース名もターゲットへ書き直します。手書きのコンバーターがこの改名を飛ばすと、リーダーは、今やグレイ値が1つ座っている場所に3つのRGB成分を探し続けます

画像とパレット:どれが変わり、どれがバイトを保持するのか

画像はオペレーターと同じくらい重要です。オペレーターだけを処理してグレイにしたパンフレットは、グレイのページの上にカラーの写真を表示し続けます。オリジナルよりも完全変換よりも、悪く読めるのです。RecolorDocumentが画像を変換するのは、コンポーネントあたり8ビットで、デバイスカラースペースを持ち、圧縮がないか、デコードパラメータなしの素朴なFlateのいずれかのときです

インデックス画像は別の経路を通ります。サンプルはカラーテーブルへのインデックスなので、テーブルが変換され、各サンプルはすでに持っていたインデックスを保ちます。256色パレット画像として保存されたスクリーンショットやチャートは、サンプルのパックや圧縮の仕方にかかわらず、このやり方で変換されます。サンプルは決してデコードされないからです。パレットのベースはデバイススペースでなければなりません。ICCプロファイルの上のパレットはテーブルを保持します

Delphi で PDF の RecolorDocument がどの部分を書き換え、JPEG 画像やソフトマスクなどどの部分がバイト同一のまま通過するかを示す図
RecolorDocument の 1 パスはオペレーター、対象画像、インデックスパレット、グラデーションのペアを書き換えますが、JPEG バイト、マスク、プロファイルに結び付いた色はそのまま通します

JPEGやファクス符号化の画像はバイトを保持します。色を変えるには、デコード、変換、再圧縮が必要で、それはJPEGアーティファクトの第二世代を足し、誰も頼んでいない形でファイルサイズを変えます。ジョブが本当にすべての写真をグレイに要るなら、再エンコードはそれ自身の品質設定を持つ別の決断です。色変換が呼び出し側に代わって決めるよりも、呼び出し側に明示的に決めてもらう方が良い

ソフトマスクとステンシルマスクは、別の理由でそのまま留まります。ソフトマスクはたいていDeviceGray画像ですが、その値が意味するのはカバレッジです。3成分のRGBへ変換すると、ビューアーはそれを透明性として扱うのをやめ、整形の対象だったコンテンツの上にグレイのアートワークとして描かれたままになります。RecolorDocumentは画像を変換する前に、/SMaskや/Maskを通して参照されるすべてのオブジェクトを集め、/ImageMask trueを持つ画像には、そもそも変換すべき色がありません

オペレーターレベルでICCBased、Separation、Indexed、Patternスペースを通して設定された色も、書かれたまま残ります。スポットカラーやプロファイルタグ付きの値を、プロファイルを読まずにデバイスグレイへ変えると、ページの見た目が変わります。見た目を保つことを仕事とするパスは、プロファイルが何を意味したかを推測し始めるわけにはいきません

グラデーション:カラースペースとストップは一緒に動く

シェーディングはカラースペースを1回宣言し、すべてのストップを関数に任せます。関数がまだ3つの数字を返しているのに、/ColorSpaceをDeviceRGBからDeviceGrayへ改名すると、リーダーはその最初の1つ、赤チャネルをグレイレベルとして取ります。青から白へのグラデーションは、黒から始まる帯として描かれるのです。RecolorDocumentはまず関数ツリー全体を読み取り専用で歩き、すべてのノードが変換可能であるかぎり何も変換しません。すべてが可能なときにだけ、ストップを書き直し、スペースを改名します

書き直す関数は、2つの端の色/C0と/C1が素朴な配列である指数型と、それらから組み立てられたスティッチング関数です。/Backgroundの色も、ほかと一緒に変換されます。サンプリングテーブルやPostScriptプログラムで定義されたシェーディングは定義を保持し、メッシュ型4から7も同じです。色を関数ではなくストリームの中に保存しているからです。どちらの書き直しも再生産であって、色の変換とは別の仕事です

PDF Library for Delphi の図。グラデーション変換がスタンドアロンオブジェクト、パターン、ページシェーディングリソース全体でシェーディングカラースペースとストップ関数を対にし、sampled、PostScript、mesh シェーディングは独自定義を維持
グラデーションの変換とは、シェーディングの在り処にかかわらず、そのカラースペースと背後のストップを一緒に移すことです。メッシュやサンプル関数のシェードは、代わりに独自の定義を保持します

グラデーションを見つけること自体が罠です。デザインツールはめったにシェーディングに独自のオブジェクトを与えません。たいていそれを塗るパターン辞書のインラインに、あるいはページの/Shadingリソースの中に座っています。グラデーションパスを作ったとき、トップレベルオブジェクトの走査はほとんど何も見つけませんでした。探索は今、オブジェクトグラフを辿ります。深さ12で頭打ちし、各オブジェクト番号を1回だけ訪れるので、全ページに共有されるリソースは1回だけ変換され、オブジェクトが自分自身を指すファイルもループできません

プレス提出の前に再彩色が収まる場所

RecolorDocumentは正規化として扱います。デバイススペース間で値を変換するのであって、ICCプロファイルを適用せず、レンダリングインテントを優先せず、プレス条件をシミュレートしません。契約プルーフに合わせなければならないジョブには、プロファイル駆動のワークフローが正しい道具で、再彩色パスはせいぜい、そのワークフローが走る前にすべてを1つのスペースへ入れるステップです

本領を発揮するのは、日常の生産現場のケースです。グレイでなければならないのにRGBロゴ付きで届く提出物、カラーで印刷されてはならない社内レポート、プレフライトステップがすべてのファイルを1つのスペースに揃えるまで拒むバッチ。社外に出るものはすべてRecolorDocumentを通すべきで、RecolorPageだけでは決してありません。半分だけ変換されたファイルは手早い目視検査を通って、プリンターで落ちるからです。評定は同じジョブで走らせてください。PDF/AとPDF/UAのプレフライトとコンプライアンスと署名ワークベンチのノートがやり方を示します

// IsValidRecolorTargetは自分で足すヘルパー。ライブラリにそんな呼び出しはありません
function IsValidRecolorTarget(Space: Integer): Boolean;
begin
  Result := (Space >= 1) and (Space <= 3);
end;

procedure NormalizeFile(const Src, Dst: string; Space: Integer);
var
  Lib: TPDFlib;
begin
  if not IsValidRecolorTarget(Space) then
    raise EArgumentException.CreateFmt('Bad recolor target %d', [Space]);
  Lib := TPDFlib.Create;
  try
    if Lib.LoadFromFile(Src, '') <> 1 then
      raise Exception.CreateFmt('Could not open %s', [Src]);
    if Lib.RecolorDocument(Space) = 0 then
      WriteLn(Src, ': already in the target space, copied unchanged');
    Lib.SaveToFile(Dst);
  finally
    Lib.Free;
  end;
end;

出力に走らせられる手早いチェックがあります。グレイ変換の後、変換されたページのコンテンツを、どんなPDFインスペクターでも開いてみてください。ページ自身のコンテンツには、もうrg、RG、k、Kのオペレーターが1つもないはずです。パスを生き延びる色は、上のケースから来ます。JPEG、プロファイルタグ付きかスポットの色、サンプリングかメッシュのグラデーション、あるいはフォームXObjectの中に描かれたアートワークで、その命令はページコンテンツではなく自分用のストリームに住んでいます

変換だけでなくベクターアートワークを組み立てるのもあるなら、ベクターグラフィックス、シェーダー、グラデーションの解説が、シェーディングやパターンがそもそもどう作られるかを扱っています。上のグラデーションのルールが予測しやすくなるはずです

色のパスはインプロセスで走ります。PDF Library for DelphiはDelphi、C++Builder、Lazarus向けのネイティブPascalなので、届いたファイル群の正規化に、サーバー上で外部コンバーターやコマンドラインツールは要りません。PDF Library for Delphi製品ページに、ドキュメント処理APIの残りが一覧されています